From 0c4efc5859e8e172b495214259dddfd2b4127a15 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 6 Dec 2025 13:24:29 +0000 Subject: [PATCH 1/6] Initial plan From f1c90c25d34d954d84ff45cf99d5e9b78410635c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 6 Dec 2025 13:30:48 +0000 Subject: [PATCH 2/6] Add uv package manager support to pybumper.nvim Co-authored-by: mgierada <23472449+mgierada@users.noreply.github.com> --- README.md | 9 ++-- lua/pybumper/actions/change-version.lua | 4 ++ lua/pybumper/actions/delete.lua | 2 + lua/pybumper/actions/install.lua | 4 ++ lua/pybumper/actions/show.lua | 72 ++++++++++++++----------- lua/pybumper/actions/update.lua | 2 + lua/pybumper/config.lua | 10 ++++ lua/pybumper/utils/constants.lua | 1 + 8 files changed, 69 insertions(+), 35 deletions(-) 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..cee0209 100644 --- a/lua/pybumper/actions/show.lua +++ b/lua/pybumper/actions/show.lua @@ -5,43 +5,53 @@ 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 = {} --- Runs the show outdated dependencies action -- @return nil M.run = function(options) - if not state.is_loaded then - logger.warn("Not a valid pyproject.toml file") - return - end - reload() - options = options or { force = false } - if state.last_run.should_skip() and not options.force then - virtual_text.display() - reload() - return - end - local id = loading.new("|  Fetching latest versions") - job({ - json = false, - command = "poetry show -o | awk -F' +' '{print $1, $2, $3 \" _ \" ;}'", - ignore_error = false, - on_start = function() - loading.start(id) - end, - on_success = function(outdated_dependencies) - local extracted_dependencies = parser.extract_outdated_dependencies(outdated_dependencies) - state.dependencies.outdated = extracted_dependencies - virtual_text.display() - reload() - loading.stop(id) - state.last_run.update() - end, - on_error = function() - loading.stop(id) - end, - }) +if not state.is_loaded then +logger.warn("Not a valid pyproject.toml file") +return +end +reload() +options = options or { force = false } +if state.last_run.should_skip() and not options.force then +virtual_text.display() +reload() +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 = command, +ignore_error = false, +on_start = function() +loading.start(id) +end, +on_success = function(outdated_dependencies) +local extracted_dependencies = parser.extract_outdated_dependencies(outdated_dependencies) +state.dependencies.outdated = extracted_dependencies +virtual_text.display() +reload() +loading.stop(id) +state.last_run.update() +end, +on_error = function() +loading.stop(id) +end, +}) end return M 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/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 = { From a4afc5c7a31b8783f62aac29a4cff5e8ed8a2e59 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 6 Dec 2025 13:35:57 +0000 Subject: [PATCH 3/6] Fix indentation in show.lua to use tabs consistently Co-authored-by: mgierada <23472449+mgierada@users.noreply.github.com> --- lua/pybumper/actions/show.lua | 78 +++++++++++++++++------------------ 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/lua/pybumper/actions/show.lua b/lua/pybumper/actions/show.lua index cee0209..6097aa8 100644 --- a/lua/pybumper/actions/show.lua +++ b/lua/pybumper/actions/show.lua @@ -13,45 +13,45 @@ local M = {} --- Runs the show outdated dependencies action -- @return nil M.run = function(options) -if not state.is_loaded then -logger.warn("Not a valid pyproject.toml file") -return -end -reload() -options = options or { force = false } -if state.last_run.should_skip() and not options.force then -virtual_text.display() -reload() -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 = command, -ignore_error = false, -on_start = function() -loading.start(id) -end, -on_success = function(outdated_dependencies) -local extracted_dependencies = parser.extract_outdated_dependencies(outdated_dependencies) -state.dependencies.outdated = extracted_dependencies -virtual_text.display() -reload() -loading.stop(id) -state.last_run.update() -end, -on_error = function() -loading.stop(id) -end, -}) + if not state.is_loaded then + logger.warn("Not a valid pyproject.toml file") + return + end + reload() + options = options or { force = false } + if state.last_run.should_skip() and not options.force then + virtual_text.display() + reload() + 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 = command, + ignore_error = false, + on_start = function() + loading.start(id) + end, + on_success = function(outdated_dependencies) + local extracted_dependencies = parser.extract_outdated_dependencies(outdated_dependencies) + state.dependencies.outdated = extracted_dependencies + virtual_text.display() + reload() + loading.stop(id) + state.last_run.update() + end, + on_error = function() + loading.stop(id) + end, + }) end return M From 5dcbde52fcf1cdc0291ef4523f93d3c79a790a75 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Dec 2025 06:20:58 +0000 Subject: [PATCH 4/6] Add UV pyproject.toml parser support for dependencies array format Co-authored-by: mgierada <23472449+mgierada@users.noreply.github.com> --- lua/pybumper/parser.lua | 116 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 109 insertions(+), 7 deletions(-) diff --git a/lua/pybumper/parser.lua b/lua/pybumper/parser.lua index 720bb4a..f83ec9d 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,105 @@ 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 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 + inDevArray = false + inOptionalArray = 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 + inDevArray = false + -- If we're in an array, continue parsing + elseif inDependenciesArray or inDevArray or (inOptionalArray and line:match("^%w+%s*=%s*%[")) 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 + end + -- Check for new section (stop parsing optional deps) + elseif line:match("^%[.-%]") then + inOptionalArray = false + inDependenciesArray = false + inDevArray = 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 From abd38b7838d29f39133ea0c455c88f9a9a7d3747 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Dec 2025 09:00:31 +0000 Subject: [PATCH 5/6] Fix UV virtual text display and prevent duplicate dependencies Co-authored-by: mgierada <23472449+mgierada@users.noreply.github.com> --- .../helpers/get_dependency_name_from_line.lua | 15 ++++++++-- lua/pybumper/parser.lua | 30 ++++++++++++++----- 2 files changed, 36 insertions(+), 9 deletions(-) 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 f83ec9d..f5c6d75 100644 --- a/lua/pybumper/parser.lua +++ b/lua/pybumper/parser.lua @@ -63,9 +63,8 @@ local function extractUVDependencies(tomlContent) end currentArrayContent = "" end - -- Check for dev dependencies in [dependency-groups] + -- Check for dev dependencies in [dependency-groups] elseif line:match("^%[dependency%-groups%]") then - inDevArray = false inOptionalArray = false elseif line:match("^dev%s*=%s*%[") then inDevArray = true @@ -82,12 +81,29 @@ local function extractUVDependencies(tomlContent) end currentArrayContent = "" end - -- Check for optional dependencies in [project.optional-dependencies] + -- Check for optional dependencies in [project.optional-dependencies] elseif line:match("^%[project%.optional%-dependencies%]") then inOptionalArray = true - inDevArray = false - -- If we're in an array, continue parsing - elseif inDependenciesArray or inDevArray or (inOptionalArray and line:match("^%w+%s*=%s*%[")) then + -- 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 + inDevArray = true -- Reuse the dev array flag for optional deps + end + -- If we're in a multi-line array, continue parsing + elseif inDependenciesArray or inDevArray then currentArrayContent = currentArrayContent .. " " .. line -- Check if we've reached the end of the array @@ -103,7 +119,7 @@ local function extractUVDependencies(tomlContent) inDependenciesArray = false inDevArray = false end - -- Check for new section (stop parsing optional deps) + -- Check for new section (stop parsing optional deps) elseif line:match("^%[.-%]") then inOptionalArray = false inDependenciesArray = false From 62fa09a08bf47c320f8635644362a7f0849cdfbc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Dec 2025 09:02:39 +0000 Subject: [PATCH 6/6] Improve parser robustness with explicit flag management Co-authored-by: mgierada <23472449+mgierada@users.noreply.github.com> --- lua/pybumper/parser.lua | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lua/pybumper/parser.lua b/lua/pybumper/parser.lua index f5c6d75..bf84c97 100644 --- a/lua/pybumper/parser.lua +++ b/lua/pybumper/parser.lua @@ -41,6 +41,7 @@ local function extractUVDependencies(tomlContent) local inDependenciesArray = false local inDevArray = false local inOptionalArray = false + local inOptionalDepArray = false local currentArrayContent = "" for line in tomlContent:gmatch("[^\r\n]+") do @@ -66,6 +67,7 @@ local function extractUVDependencies(tomlContent) -- 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 @@ -84,6 +86,7 @@ local function extractUVDependencies(tomlContent) -- 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 @@ -100,10 +103,10 @@ local function extractUVDependencies(tomlContent) currentArrayContent = "" else -- Multi-line optional array, will continue in next elseif - inDevArray = true -- Reuse the dev array flag for optional deps + inOptionalDepArray = true end -- If we're in a multi-line array, continue parsing - elseif inDependenciesArray or inDevArray then + elseif inDependenciesArray or inDevArray or inOptionalDepArray then currentArrayContent = currentArrayContent .. " " .. line -- Check if we've reached the end of the array @@ -118,12 +121,14 @@ local function extractUVDependencies(tomlContent) 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