From aa279cb7adef75cd655a147463a0b55f7c036520 Mon Sep 17 00:00:00 2001 From: Michael Park Date: Wed, 28 Feb 2024 20:18:02 +0000 Subject: [PATCH 1/3] feat: add uv as installer for python UV is a very fast installer for python packages that can be 10-100x faster to resolve packages. This adds an option for Mason to use it instead of pip to resolve python packages that are installed via Mason. More info about the replacement: https://github.com/astral-sh/uv I have no relationship with uv, it is just very fast and it would be nice to have updates for packages like sqlfluff take a lot less time than they currently do to resolve during updates. fix: ensure the virtual environment is .venv for uv fix: venv dir can stay venv feat: update stdio ctx reference feat: add uv as installer for python UV is a very fast installer for python packages that can be 10-100x faster to resolve packages. This adds an option for Mason to use it instead of pip to resolve python packages that are installed via Mason. More info about the replacement: https://github.com/astral-sh/uv I have no relationship with uv, it is just very fast and it would be nice to have updates for packages like sqlfluff take a lot less time than they currently do to resolve during updates. fix: ensure the virtual environment is .venv for uv fix: venv dir can stay venv feat: update stdio ctx reference --- README.md | 4 ++ doc/mason.txt | 4 ++ .../installer/compiler/compilers/pypi.lua | 2 + lua/mason-core/installer/managers/pypi.lua | 50 ++++++++++++++----- lua/mason/settings.lua | 4 ++ .../compiler/compilers/pypi_spec.lua | 1 + 6 files changed, 53 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index c233bee0b1..2d18989bf0 100644 --- a/README.md +++ b/README.md @@ -233,6 +233,10 @@ local DEFAULT_SETTINGS = { -- Whether to upgrade pip to the latest version in the virtual environment before installing packages. upgrade_pip = false, + ---@since 1.8.0 + -- Whether to use uv to install packages instead of pip + use_uv = false, + ---@since 1.0.0 -- These args will be added to `pip install` calls. Note that setting extra args might impact intended behavior -- and is not recommended. diff --git a/doc/mason.txt b/doc/mason.txt index 091982a20d..37108e877c 100644 --- a/doc/mason.txt +++ b/doc/mason.txt @@ -276,6 +276,10 @@ Example: -- Whether to upgrade pip to the latest version in the virtual environment before installing packages. upgrade_pip = false, + ---@since 2.2.1 + -- Whether to use uv to install packages instead of pip + use_uv = false, + ---@since 1.0.0 -- These args will be added to `pip install` calls. Note that setting extra args might impact intended behavior -- and is not recommended. diff --git a/lua/mason-core/installer/compiler/compilers/pypi.lua b/lua/mason-core/installer/compiler/compilers/pypi.lua index 3e2ca141ae..3addf7d474 100644 --- a/lua/mason-core/installer/compiler/compilers/pypi.lua +++ b/lua/mason-core/installer/compiler/compilers/pypi.lua @@ -7,6 +7,7 @@ local M = {} ---@class PypiSource : RegistryPackageSource ---@field extra_packages? string[] +---@field supported_platforms? string[] ---@param source PypiSource ---@param purl Purl @@ -20,6 +21,7 @@ function M.parse(source, purl) pip = { upgrade = settings.current.pip.upgrade_pip, extra_args = settings.current.pip.install_args, + use_uv = settings.current.pip.use_uv, }, } diff --git a/lua/mason-core/installer/managers/pypi.lua b/lua/mason-core/installer/managers/pypi.lua index 72b1b5037b..45543ac44c 100644 --- a/lua/mason-core/installer/managers/pypi.lua +++ b/lua/mason-core/installer/managers/pypi.lua @@ -9,8 +9,11 @@ local pep440 = require "mason-core.pep440" local platform = require "mason-core.platform" local providers = require "mason-core.providers" local semver = require "mason-core.semver" +local settings = require "mason.settings" local spawn = require "mason-core.spawn" +local use_uv = settings.current.pip.use_uv + local M = {} local VENV_DIR = "venv" @@ -154,6 +157,7 @@ local function find_venv_executable(ctx, executable) return Result.success(candidate) end end + return Result.failure(("Failed to find executable %q in Python virtual environment."):format(executable)) end @@ -161,25 +165,47 @@ end ---@param args SpawnArgs local function venv_python(args) local ctx = installer.context() - return find_venv_executable(ctx, "python"):and_then(function(python_path) - return ctx.spawn[path.concat { ctx.cwd:get(), python_path }](args) + return find_venv_executable(ctx, "python"):and_then(function(bin_path) + return ctx.spawn[path.concat { ctx.cwd:get(), bin_path }](args) end) end +---@async +---@param args SpawnArgs +local function uv_python(args) + local ctx = installer.context() + local global_uv = vim.fn.exepath "uv" + return ctx.spawn[global_uv](args) +end + ---@async ---@param pkgs string[] ---@param extra_args? string[] local function pip_install(pkgs, extra_args) - return venv_python { - "-m", - "pip", - "--disable-pip-version-check", - "install", - "--no-user", - "--ignore-installed", - extra_args or vim.NIL, - pkgs, - } + if use_uv then + return uv_python { + "pip", + "install", + "--color", + "never", + "--directory", + "venv", + "-U", + extra_args or vim.NIL, + pkgs, + } + else + return venv_python { + "-m", + "pip", + "--disable-pip-version-check", + "install", + "--no-user", + "--ignore-installed", + extra_args or vim.NIL, + pkgs, + } + end end ---@async diff --git a/lua/mason/settings.lua b/lua/mason/settings.lua index ebff1e0b44..bdc46699d2 100644 --- a/lua/mason/settings.lua +++ b/lua/mason/settings.lua @@ -60,6 +60,10 @@ local DEFAULT_SETTINGS = { -- Whether to upgrade pip to the latest version in the virtual environment before installing packages. upgrade_pip = false, + ---@since 2.2.1 + -- Whether to use uv to install packages instead of pip + use_uv = false, + ---@since 1.0.0 -- These args will be added to `pip install` calls. Note that setting extra args might impact intended behavior -- and is not recommended. diff --git a/tests/mason-core/installer/compiler/compilers/pypi_spec.lua b/tests/mason-core/installer/compiler/compilers/pypi_spec.lua index 03c57a9e69..7af2d1193d 100644 --- a/tests/mason-core/installer/compiler/compilers/pypi_spec.lua +++ b/tests/mason-core/installer/compiler/compilers/pypi_spec.lua @@ -31,6 +31,7 @@ describe("pypi compiler :: parsing", function() pip = { upgrade = true, extra_args = { "--proxy", "http://localghost" }, + use_uv = false, }, }, pypi.parse({ extra_packages = { "extra" } }, purl()) From e0481c81dce57843ce263c4abb03f61e1cfb2bb9 Mon Sep 17 00:00:00 2001 From: lljbash Date: Mon, 16 Mar 2026 16:58:29 +0800 Subject: [PATCH 2/3] fix: restore missing use_uv logic in pypi manager --- lua/mason-core/installer/managers/pypi.lua | 64 ++++++++++++++++------ 1 file changed, 46 insertions(+), 18 deletions(-) diff --git a/lua/mason-core/installer/managers/pypi.lua b/lua/mason-core/installer/managers/pypi.lua index 45543ac44c..e931a83d74 100644 --- a/lua/mason-core/installer/managers/pypi.lua +++ b/lua/mason-core/installer/managers/pypi.lua @@ -33,11 +33,20 @@ local function resolve_python3(candidates) a.scheduler() local available_candidates = _.filter(is_executable, candidates) for __, candidate in ipairs(available_candidates) do - ---@type string - local version_output = spawn[candidate]({ "--version" }):map(_.prop "stdout"):get_or_else "" - local ok, version = pcall(semver.new, version_output:match "Python (3%.%d+.%d+)") - if ok then - return { executable = candidate, version = version } + if use_uv and candidate == "uv" then + ---@type string + local version_output = spawn[candidate]({ "--version" }):map(_.prop "stdout"):get_or_else "" + local ok, version = pcall(semver.new, version_output:match "uv (%d+.%d+.%d+).*") + if ok then + return { executable = candidate, version = version } + end + elseif not use_uv then + ---@type string + local version_output = spawn[candidate]({ "--version" }):map(_.prop "stdout"):get_or_else "" + local ok, version = pcall(semver.new, version_output:match "Python (3%.%d+.%d+)") + if ok then + return { executable = candidate, version = version } + end end end return nil @@ -88,6 +97,9 @@ local function create_venv(pkg) -- 1. Resolve stock python3 installation. local stock_candidates = platform.is.win and { "python", "python3" } or { "python3", "python" } + if use_uv then + table.insert(stock_candidates, 1, "uv") + end local stock_target = resolve_python3(stock_candidates) if stock_target then log.fmt_debug("Resolved stock python3 installation version %s", stock_target.version) @@ -95,6 +107,9 @@ local function create_venv(pkg) -- 2. Resolve suitable versioned python3 installation (python3.12, python3.11, etc.). local versioned_candidates = {} + if use_uv then + table.insert(versioned_candidates, "uv") + end if supported_python_versions ~= nil then if stock_target and not pep440_check_version(tostring(stock_target.version), supported_python_versions) then log.fmt_debug("Finding versioned candidates for %s", supported_python_versions) @@ -114,7 +129,8 @@ local function create_venv(pkg) -- 3. If a versioned python3 installation was not found, warn the user if the stock python3 installation is outside -- the supported version range. if - target == stock_target + use_uv == false + and target == stock_target and supported_python_versions ~= nil and not pep440_check_version(tostring(target.version), supported_python_versions) then @@ -136,9 +152,14 @@ local function create_venv(pkg) end end - log.fmt_debug("Found python3 installation version=%s, executable=%s", target.version, target.executable) ctx.stdio_sink:stdout "Creating virtual environment…\n" - return ctx.spawn[target.executable] { "-m", "venv", "--system-site-packages", VENV_DIR } + if use_uv then + log.fmt_debug("Found uv installation version=%s, executable=%s", target.version, target.executable) + return ctx.spawn[target.executable] { "venv", VENV_DIR } + else + log.fmt_debug("Found python3 installation version=%s, executable=%s", target.version, target.executable) + return ctx.spawn[target.executable] { "-m", "venv", "--system-site-packages", VENV_DIR } + end end ---@param ctx InstallContext @@ -165,8 +186,11 @@ end ---@param args SpawnArgs local function venv_python(args) local ctx = installer.context() - return find_venv_executable(ctx, "python"):and_then(function(bin_path) - return ctx.spawn[path.concat { ctx.cwd:get(), bin_path }](args) + if use_uv then + return ctx.spawn["uv"](args) + end + return find_venv_executable(ctx, "python"):and_then(function(python_path) + return ctx.spawn[path.concat { ctx.cwd:get(), python_path }](args) end) end @@ -219,7 +243,7 @@ function M.init(opts) ctx:promote_cwd() try(create_venv(opts.package)) - if opts.upgrade_pip then + if opts.upgrade_pip and not use_uv then ctx.stdio_sink:stdout "Upgrading pip inside the virtual environment…\n" try(pip_install({ "pip" }, opts.install_extra_args)) end @@ -245,13 +269,17 @@ end ---@param pkg string function M.uninstall(pkg) log.fmt_debug("pypi: uninstall %s", pkg) - return venv_python { - "-m", - "pip", - "uninstall", - "-y", - pkg, - } + if use_uv then + return venv_python { "pip", "uninstall", "-y", pkg } + else + return venv_python { + "-m", + "pip", + "uninstall", + "-y", + pkg, + } + end end ---@param executable string From 6ca3f5fd6a57e7002c6c00342c17874c2bd5a029 Mon Sep 17 00:00:00 2001 From: Michael Park Date: Mon, 16 Mar 2026 09:57:42 +0000 Subject: [PATCH 3/3] chore: update supported python versions --- lua/mason-core/installer/managers/pypi.lua | 7 +++-- .../installer/managers/pypi_spec.lua | 26 ++++++++++--------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/lua/mason-core/installer/managers/pypi.lua b/lua/mason-core/installer/managers/pypi.lua index e931a83d74..bf49dffc6a 100644 --- a/lua/mason-core/installer/managers/pypi.lua +++ b/lua/mason-core/installer/managers/pypi.lua @@ -78,13 +78,12 @@ local function get_versioned_candidates(supported_python_versions) end return Optional.of(executable) end, { + { semver.new "3.14.0", "python3.14" }, + { semver.new "3.13.0", "python3.13" }, { semver.new "3.12.0", "python3.12" }, { semver.new "3.11.0", "python3.11" }, { semver.new "3.10.0", "python3.10" }, - { semver.new "3.9.0", "python3.9" }, - { semver.new "3.8.0", "python3.8" }, - { semver.new "3.7.0", "python3.7" }, - { semver.new "3.6.0", "python3.6" }, + -- the rest are EOL }) end diff --git a/tests/mason-core/installer/managers/pypi_spec.lua b/tests/mason-core/installer/managers/pypi_spec.lua index ef57411ba9..668b353c91 100644 --- a/tests/mason-core/installer/managers/pypi_spec.lua +++ b/tests/mason-core/installer/managers/pypi_spec.lua @@ -92,6 +92,8 @@ describe("pypi manager", function() stub(ctx.fs, "file_exists") stub(providers.pypi, "get_supported_python_versions", mockx.returns(Result.success ">=3.12")) stub(vim.fn, "executable") + vim.fn.executable.on_call_with("python3.14").returns(0) + vim.fn.executable.on_call_with("python3.13").returns(0) vim.fn.executable.on_call_with("python3.12").returns(1) stub(spawn, "python3.12") spawn["python3.12"].on_call_with({ "--version" }).returns(Result.success { stdout = "Python 3.12.0" }) @@ -120,13 +122,13 @@ describe("pypi manager", function() spy.on(ctx.stdio_sink, "stderr") stub(ctx, "promote_cwd") stub(ctx.fs, "file_exists") - stub(providers.pypi, "get_supported_python_versions", mockx.returns(Result.success ">=3.8")) + stub(providers.pypi, "get_supported_python_versions", mockx.returns(Result.success ">=3.10")) stub(vim.fn, "executable") + vim.fn.executable.on_call_with("python3.14").returns(0) + vim.fn.executable.on_call_with("python3.13").returns(0) vim.fn.executable.on_call_with("python3.12").returns(0) vim.fn.executable.on_call_with("python3.11").returns(0) vim.fn.executable.on_call_with("python3.10").returns(0) - vim.fn.executable.on_call_with("python3.9").returns(0) - vim.fn.executable.on_call_with("python3.8").returns(0) stub(spawn, "python3", mockx.returns(Result.success())) spawn.python3.on_call_with({ "--version" }).returns(Result.success { stdout = "Python 3.5.0" }) @@ -139,7 +141,7 @@ describe("pypi manager", function() end) assert.same( - Result.failure "Failed to find a python3 installation in PATH that meets the required versions (>=3.8). Found version: 3.5.0.", + Result.failure "Failed to find a python3 installation in PATH that meets the required versions (>=3.10). Found version: 3.5.0.", result ) assert @@ -154,13 +156,13 @@ describe("pypi manager", function() spy.on(ctx.stdio_sink, "stderr") stub(ctx, "promote_cwd") stub(ctx.fs, "file_exists") - stub(providers.pypi, "get_supported_python_versions", mockx.returns(Result.success ">=3.8")) + stub(providers.pypi, "get_supported_python_versions", mockx.returns(Result.success ">=3.10")) stub(vim.fn, "executable") + vim.fn.executable.on_call_with("python3.14").returns(0) + vim.fn.executable.on_call_with("python3.13").returns(0) vim.fn.executable.on_call_with("python3.12").returns(0) vim.fn.executable.on_call_with("python3.11").returns(0) vim.fn.executable.on_call_with("python3.10").returns(0) - vim.fn.executable.on_call_with("python3.9").returns(0) - vim.fn.executable.on_call_with("python3.8").returns(0) stub(spawn, "python3", mockx.returns(Result.success())) spawn.python3.on_call_with({ "--version" }).returns(Result.success { stdout = "Python 3.5.0" }) @@ -182,7 +184,7 @@ describe("pypi manager", function() } assert.spy(ctx.stdio_sink.stderr).was_called_with( match.is_ref(ctx.stdio_sink), - "Warning: The resolved python3 version 3.5.0 is not compatible with the required Python versions: >=3.8.\n" + "Warning: The resolved python3 version 3.5.0 is not compatible with the required Python versions: >=3.10.\n" ) end ) @@ -192,11 +194,11 @@ describe("pypi manager", function() spy.on(ctx.stdio_sink, "stderr") stub(ctx, "promote_cwd") stub(ctx.fs, "file_exists") - stub(providers.pypi, "get_supported_python_versions", mockx.returns(Result.success ">=3.8")) + stub(providers.pypi, "get_supported_python_versions", mockx.returns(Result.success ">=3.10")) stub(vim.fn, "executable") - vim.fn.executable.on_call_with("python3.12").returns(1) + vim.fn.executable.on_call_with("python3.14").returns(1) stub(spawn, "python3", mockx.returns(Result.success())) - spawn.python3.on_call_with({ "--version" }).returns(Result.success { stdout = "Python 3.8.0" }) + spawn.python3.on_call_with({ "--version" }).returns(Result.success { stdout = "Python 3.10.0" }) ctx:execute(function() pypi.init { @@ -208,7 +210,7 @@ describe("pypi manager", function() assert.spy(ctx.promote_cwd).was_called(1) assert.spy(ctx.spawn.python3).was_called(1) - assert.spy(ctx.spawn["python3.12"]).was_called(0) + assert.spy(ctx.spawn["python3.14"]).was_called(0) assert.spy(ctx.spawn.python3).was_called_with { "-m", "venv",