Skip to content

Commit

Permalink
feat: add uv as installer for python
Browse files Browse the repository at this point in the history
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
  • Loading branch information
KingMichaelPark committed Oct 25, 2024
1 parent e2f7f90 commit db211a4
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 22 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,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.
Expand Down
4 changes: 4 additions & 0 deletions doc/mason.txt
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,10 @@ Example:
-- 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.
Expand Down
82 changes: 60 additions & 22 deletions lua/mason-core/installer/managers/pypi.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,18 @@ 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 M = {}

local VENV_DIR = "venv"
local use_uv = settings.current.pip.use_uv
local VENV_DIR
if use_uv then
VENV_DIR = ".venv"
else
VENV_DIR = "venv"
end

---@async
---@param candidates string[]
Expand All @@ -22,11 +29,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
Expand Down Expand Up @@ -76,14 +92,14 @@ local function create_venv(pkg)
local supported_python_versions = providers.pypi.get_supported_python_versions(pkg.name, pkg.version):get_or_nil()

-- 1. Resolve stock python3 installation.
local stock_candidates = platform.is.win and { "python", "python3" } or { "python3", "python" }
local stock_candidates = platform.is.win and { "python", "python3", "uv" } or { "python3", "python", "uv" }
local stock_target = resolve_python3(stock_candidates)
if stock_target then
log.fmt_debug("Resolved stock python3 installation version %s", stock_target.version)
end

-- 2. Resolve suitable versioned python3 installation (python3.12, python3.11, etc.).
local versioned_candidates = {}
local versioned_candidates = { "uv" }
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)
Expand All @@ -103,7 +119,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
Expand All @@ -125,9 +142,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
Expand All @@ -153,6 +175,9 @@ end
---@param args SpawnArgs
local function venv_python(args)
local ctx = installer.context()
if use_uv then
return ctx.spawn[{ "uv", "venv" }](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)
Expand All @@ -162,16 +187,29 @@ end
---@param pkgs string[]
---@param extra_args? string[]
local function pip_install(pkgs, extra_args)
return venv_python {
"-m",
"pip",
"--disable-pip-version-check",
"install",
"--ignore-installed",
"-U",
extra_args or vim.NIL,
pkgs,
}
if use_uv then
local ctx = installer.context()

local task = ctx.spawn["uv"] {
"pip",
"install",
"-U",
extra_args or vim.NIL,
pkgs,
}
return task
else
return venv_python {
"-m",
"pip",
"--disable-pip-version-check",
"install",
"--ignore-installed",
"-U",
extra_args or vim.NIL,
pkgs,
}
end
end

---@async
Expand All @@ -185,7 +223,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
Expand Down
3 changes: 3 additions & 0 deletions lua/mason-core/installer/registry/providers/pypi.lua
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,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,
},
}

Expand All @@ -48,11 +49,13 @@ function M.install(ctx, source)
},
upgrade_pip = source.pip.upgrade,
install_extra_args = source.pip.extra_args,
use_uv = source.pip.use_uv,
})
try(pypi.install(source.package, source.version, {
extra = source.extra,
extra_packages = source.extra_packages,
install_extra_args = source.pip.extra_args,
use_uv = source.pip.use_uv,
}))
end)
end
Expand Down
4 changes: 4 additions & 0 deletions lua/mason/settings.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ describe("pypi provider :: parsing", function()
pip = {
upgrade = true,
extra_args = { "--proxy", "http://localghost" },
use_uv = false,
},
},
pypi.parse({ extra_packages = { "extra" } }, purl())
Expand Down

0 comments on commit db211a4

Please sign in to comment.