diff --git a/README.md b/README.md index 36630cf..5ec801b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # pybumper.nvim -## A wrapper around the `poetry` commands for nvim 🔌 +## A wrapper around Python package managers for nvim 🔌 @@ -27,7 +27,7 @@ - ✨ Add any new valid dependency. - ✨ Upgrade dependency on a current line. - ✨ Remove any dependency. -- 🏗 Automatic package manager detection (`poetry` supported at this moment. Support for `requirements.txt` would be added later.). +- 🏗 Automatic package manager detection (`poetry` and `uv` supported. Support for `requirements.txt` would be added later.). - 🏗 Loading animation hook (to be placed in status bar or anywhere else). ## 🔌 Available commands @@ -82,7 +82,8 @@ It should work with any fairly modern python tech stack. I tested that for the following: - neovim >= 0.9 and nightly 0.10-dev releases -- poetry >= 1.5.1 +- poetry >= 1.5.1 (if using poetry) +- uv >= 0.1.0 (if using uv) - python >= 3.10.8 - pip >= 23.2.1 @@ -121,7 +122,7 @@ The `pybumper.nvim` comes up with the following configuration. Any of of those c }, }, autostart = true, - package_manager = constants.PACKAGE_MANAGERS.poetry, + package_manager = constants.PACKAGE_MANAGERS.poetry, -- or constants.PACKAGE_MANAGERS.uv hide_up_to_date = false, hide_unstable_versions = false, }, diff --git a/lua/pybumper/actions/change-version.lua b/lua/pybumper/actions/change-version.lua index 5d4593e..43312b6 100644 --- a/lua/pybumper/actions/change-version.lua +++ b/lua/pybumper/actions/change-version.lua @@ -20,6 +20,8 @@ local M = {} M.__get_change_version_command = function(dependency_name, version) if config.options.package_manager == constants.PACKAGE_MANAGERS.poetry then return "poetry add " .. dependency_name .. "@" .. version + elseif config.options.package_manager == constants.PACKAGE_MANAGERS.uv then + return "uv add " .. dependency_name .. "==" .. version end end @@ -29,6 +31,8 @@ end M.__get_version_list_command = function(dependency_name) if config.options.package_manager == constants.PACKAGE_MANAGERS.poetry then return ("pip index versions " .. dependency_name) + elseif config.options.package_manager == constants.PACKAGE_MANAGERS.uv then + return ("uv pip index versions " .. dependency_name) end end diff --git a/lua/pybumper/actions/delete.lua b/lua/pybumper/actions/delete.lua index 84a8fec..b216741 100644 --- a/lua/pybumper/actions/delete.lua +++ b/lua/pybumper/actions/delete.lua @@ -17,6 +17,8 @@ local M = {} M.__get_command = function(dependency_name) if config.options.package_manager == constants.PACKAGE_MANAGERS.poetry then return "poetry remove " .. dependency_name + elseif config.options.package_manager == constants.PACKAGE_MANAGERS.uv then + return "uv remove " .. dependency_name end end diff --git a/lua/pybumper/actions/install.lua b/lua/pybumper/actions/install.lua index 8ce3f5b..a122768 100644 --- a/lua/pybumper/actions/install.lua +++ b/lua/pybumper/actions/install.lua @@ -19,12 +19,16 @@ M.__get_command = function(type, dependency_name) if type == constants.DEPENDENCY_TYPE.development then if config.options.package_manager == constants.PACKAGE_MANAGERS.poetry then return "poetry add --dev " .. dependency_name + elseif config.options.package_manager == constants.PACKAGE_MANAGERS.uv then + return "uv add --dev " .. dependency_name end end if type == constants.DEPENDENCY_TYPE.production then if config.options.package_manager == constants.PACKAGE_MANAGERS.poetry then return "poetry add " .. dependency_name + elseif config.options.package_manager == constants.PACKAGE_MANAGERS.uv then + return "uv add " .. dependency_name end end end diff --git a/lua/pybumper/actions/show.lua b/lua/pybumper/actions/show.lua index bf45ce3..6097aa8 100644 --- a/lua/pybumper/actions/show.lua +++ b/lua/pybumper/actions/show.lua @@ -5,6 +5,8 @@ local virtual_text = require("pybumper.virtual_text") local reload = require("pybumper.helpers.reload") local loading = require("pybumper.ui.generic.loading-status") local logger = require("pybumper.utils.logger") +local config = require("pybumper.config") +local constants = require("pybumper.utils.constants") local M = {} @@ -23,9 +25,17 @@ M.run = function(options) return end local id = loading.new("|  Fetching latest versions") + + local command + if config.options.package_manager == constants.PACKAGE_MANAGERS.poetry then + command = "poetry show -o | awk -F' +' '{print $1, $2, $3 \" _ \" ;}'" + elseif config.options.package_manager == constants.PACKAGE_MANAGERS.uv then + command = "uv pip list --outdated | tail -n +3 | awk '{print $1, $2, $3 \" _ \" ;}'" + end + job({ json = false, - command = "poetry show -o | awk -F' +' '{print $1, $2, $3 \" _ \" ;}'", + command = command, ignore_error = false, on_start = function() loading.start(id) diff --git a/lua/pybumper/actions/update.lua b/lua/pybumper/actions/update.lua index 84ea6c3..f31a8be 100644 --- a/lua/pybumper/actions/update.lua +++ b/lua/pybumper/actions/update.lua @@ -17,6 +17,8 @@ local M = {} M.__get_command = function(dependency_name) if config.options.package_manager == constants.PACKAGE_MANAGERS.poetry then return "poetry update " .. dependency_name + elseif config.options.package_manager == constants.PACKAGE_MANAGERS.uv then + return "uv lock --upgrade-package " .. dependency_name end end diff --git a/lua/pybumper/config.lua b/lua/pybumper/config.lua index 620a6f4..b87ce89 100644 --- a/lua/pybumper/config.lua +++ b/lua/pybumper/config.lua @@ -36,8 +36,18 @@ end -- @return nil -- TODO: Add support for requirements.txt approach with pip M.__register_package_manager = function() + local uv_lock = io.open("uv.lock", "r") local poetry_lock = io.open("poetry.lock", "r") + if uv_lock ~= nil then + M.options.package_manager = constants.PACKAGE_MANAGERS.uv + + io.close(uv_lock) + state.is_in_project = true + + return + end + if poetry_lock ~= nil then M.options.package_manager = constants.PACKAGE_MANAGERS.poetry diff --git a/lua/pybumper/helpers/get_dependency_name_from_line.lua b/lua/pybumper/helpers/get_dependency_name_from_line.lua index 9f78cc1..f7c7667 100644 --- a/lua/pybumper/helpers/get_dependency_name_from_line.lua +++ b/lua/pybumper/helpers/get_dependency_name_from_line.lua @@ -33,12 +33,23 @@ end -- @return string? return function(line) local value = {} - -- Try to match the name and version from the line + + -- Try to match Poetry format: name = "version" local name, version = line:match('^(%S+)%s*=%s*"([^"]+)"') - + if name and version then table.insert(value, name) table.insert(value, version) + else + -- Try to match UV format: "package==version" or "package>=version", etc. + local pkg_spec = line:match('^%s*"([^"]+)"') + if pkg_spec then + name, version = pkg_spec:match("^([^=<>~!]+)[=<>~!]+(.*)$") + if name and version then + table.insert(value, name) + table.insert(value, version) + end + end end -- If no version or name fail diff --git a/lua/pybumper/parser.lua b/lua/pybumper/parser.lua index 720bb4a..bf84c97 100644 --- a/lua/pybumper/parser.lua +++ b/lua/pybumper/parser.lua @@ -1,8 +1,11 @@ local state = require("pybumper.state") local clean_version = require("pybumper.helpers.clean_version") local logger = require("pybumper.utils.logger") +local config = require("pybumper.config") +local constants = require("pybumper.utils.constants") -local function extractDependenciesCurrent(tomlContent) +-- Parse Poetry-style dependencies: [tool.poetry.dependencies] +local function extractPoetryDependencies(tomlContent) local dependencies = {} local insideDependenciesSection = false local insideDevDependenciesSection = false @@ -13,14 +16,14 @@ local function extractDependenciesCurrent(tomlContent) if line:match("^%[tool.poetry.dependencies%]") then insideDependenciesSection = true + insideDevDependenciesSection = false elseif line:match("^%[tool.poetry.dev%-dependencies%]") then insideDevDependenciesSection = true - elseif insideDependenciesSection and line:match("^%[.-%]") then - -- We've reached the end of the dependencies section - break - elseif insideDevDependenciesSection and line:match("^%[.-%]") then - -- We've reached the end of the dev dependencies section - break + insideDependenciesSection = false + elseif line:match("^%[.-%]") then + -- We've reached the end of the current section + insideDependenciesSection = false + insideDevDependenciesSection = false elseif (insideDependenciesSection or insideDevDependenciesSection) and line ~= "" then -- Extract key-value pairs within the dependencies section local key, value = line:match('^(.-)%s*=%s*"(.-)"$') @@ -29,6 +32,126 @@ local function extractDependenciesCurrent(tomlContent) end end end + return dependencies +end + +-- Parse UV-style dependencies: dependencies = ["package==version", ...] +local function extractUVDependencies(tomlContent) + local dependencies = {} + local inDependenciesArray = false + local inDevArray = false + local inOptionalArray = false + local inOptionalDepArray = false + local currentArrayContent = "" + + for line in tomlContent:gmatch("[^\r\n]+") do + -- Trim leading and trailing spaces + line = line:match("^%s*(.-)%s*$") + + -- Check for start of dependencies array + if line:match("^dependencies%s*=%s*%[") then + inDependenciesArray = true + currentArrayContent = line + -- Check if array closes on same line + if line:match("%]%s*$") then + inDependenciesArray = false + -- Parse single-line array + for pkg in line:gmatch('"([^"]+)"') do + local name, version = pkg:match("^([^=<>~!]+)[=<>~!]+(.*)$") + if name and version then + dependencies[name] = version + end + end + currentArrayContent = "" + end + -- Check for dev dependencies in [dependency-groups] + elseif line:match("^%[dependency%-groups%]") then + inOptionalArray = false + inDevArray = false + elseif line:match("^dev%s*=%s*%[") then + inDevArray = true + currentArrayContent = line + -- Check if array closes on same line + if line:match("%]%s*$") then + inDevArray = false + -- Parse single-line array + for pkg in line:gmatch('"([^"]+)"') do + local name, version = pkg:match("^([^=<>~!]+)[=<>~!]+(.*)$") + if name and version then + dependencies[name] = version + end + end + currentArrayContent = "" + end + -- Check for optional dependencies in [project.optional-dependencies] + elseif line:match("^%[project%.optional%-dependencies%]") then + inOptionalArray = true + inOptionalDepArray = false + -- Handle optional dependency arrays starting (e.g., data = [...]) + elseif inOptionalArray and line:match("^%w+%s*=%s*%[") then + -- This is a new optional dependency array + currentArrayContent = line + -- Check if array closes on same line + if line:match("%]%s*$") then + -- Parse single-line optional array + for pkg in line:gmatch('"([^"]+)"') do + local name, version = pkg:match("^([^=<>~!]+)[=<>~!]+(.*)$") + if name and version then + dependencies[name] = version + end + end + currentArrayContent = "" + else + -- Multi-line optional array, will continue in next elseif + inOptionalDepArray = true + end + -- If we're in a multi-line array, continue parsing + elseif inDependenciesArray or inDevArray or inOptionalDepArray then + currentArrayContent = currentArrayContent .. " " .. line + + -- Check if we've reached the end of the array + if line:match("%]") then + -- Parse all packages in the array + for pkg in currentArrayContent:gmatch('"([^"]+)"') do + local name, version = pkg:match("^([^=<>~!]+)[=<>~!]+(.*)$") + if name and version then + dependencies[name] = version + end + end + currentArrayContent = "" + inDependenciesArray = false + inDevArray = false + inOptionalDepArray = false + end + -- Check for new section (stop parsing optional deps) + elseif line:match("^%[.-%]") then + inOptionalArray = false + inDependenciesArray = false + inDevArray = false + inOptionalDepArray = false + currentArrayContent = "" + end + end + + return dependencies +end + +local function extractDependenciesCurrent(tomlContent) + local dependencies = {} + + -- Try to determine which package manager format to use + if config.options.package_manager == constants.PACKAGE_MANAGERS.uv then + dependencies = extractUVDependencies(tomlContent) + elseif config.options.package_manager == constants.PACKAGE_MANAGERS.poetry then + dependencies = extractPoetryDependencies(tomlContent) + else + -- Fallback: try both formats + dependencies = extractPoetryDependencies(tomlContent) + if not next(dependencies) then + dependencies = extractUVDependencies(tomlContent) + end + end + return next(dependencies) and dependencies or nil, "Dependencies section not found" end diff --git a/lua/pybumper/utils/constants.lua b/lua/pybumper/utils/constants.lua index 6eef31a..d7de04f 100644 --- a/lua/pybumper/utils/constants.lua +++ b/lua/pybumper/utils/constants.lua @@ -7,6 +7,7 @@ M.HIGHLIGHT_GROUPS = { M.PACKAGE_MANAGERS = { poetry = "poetry", + uv = "uv", } M.DEPENDENCY_TYPE = {