Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(pypi): attempt more python3 candidates #1608

Merged
merged 1 commit into from
Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 59 additions & 12 deletions lua/mason-core/installer/managers/pypi.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,71 @@ local installer = require "mason-core.installer"
local log = require "mason-core.log"
local path = require "mason-core.path"
local platform = require "mason-core.platform"
local semver = require "mason-core.semver"
local spawn = require "mason-core.spawn"

local M = {}

local VENV_DIR = "venv"

local is_executable = _.compose(_.equals(1), vim.fn.executable)

---@async
---@param candidates string[]
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 }
end
end
return nil
end

---@param min_version? Semver
local function get_versioned_candidates(min_version)
return _.filter_map(function(pair)
local version, executable = unpack(pair)
if not min_version or version > min_version then
return Optional.of(executable)
else
return Optional.empty()
end
end, {
{ 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" },
})
end

---@async
---@param py_executables string[]
local function create_venv(py_executables)
local function create_venv()
local stock_candidates = platform.is.win and { "python", "python3" } or { "python3", "python" }
local stock_target = resolve_python3(stock_candidates)
local _ = stock_target and log.fmt_debug("Resolved stock python3 installation version %s", stock_target.version)
local versioned_candidates = get_versioned_candidates(stock_target and stock_target.version)
log.debug("Resolving versioned python3 candidates", versioned_candidates)
local target = resolve_python3(versioned_candidates) or stock_target
local ctx = installer.context()
return Optional.of_nilable(_.find_first(function(executable)
return ctx.spawn[executable]({ "-m", "venv", VENV_DIR }):is_success()
end, py_executables)):ok_or "Failed to create python3 virtual environment."
if not target then
ctx.stdio_sink.stderr(
("Unable to find python3 installation. Tried the following candidates: %s.\n"):format(
_.join(", ", _.concat(stock_candidates, versioned_candidates))
)
)
return Result.failure "Failed to find python3 installation."
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", VENV_DIR }
end

---@param ctx InstallContext
Expand Down Expand Up @@ -70,15 +123,9 @@ function M.init(opts)
log.fmt_debug("pypi: init", opts)
local ctx = installer.context()

a.scheduler()

local executables = platform.is.win and { "python", "python3" } or { "python3", "python" }

-- pip3 will hardcode the full path to venv executables, so we need to promote cwd to make sure pip uses the final destination path.
ctx:promote_cwd()

ctx.stdio_sink.stdout "Creating virtual environment…\n"
try(create_venv(executables))
try(create_venv())

if opts.upgrade_pip then
ctx.stdio_sink.stdout "Upgrading pip inside the virtual environment…\n"
Expand Down
7 changes: 7 additions & 0 deletions tests/mason-core/installer/managers/pypi_spec.lua
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
local Result = require "mason-core.result"
local installer = require "mason-core.installer"
local match = require "luassert.match"
local path = require "mason-core.path"
local pypi = require "mason-core.installer.managers.pypi"
local spawn = require "mason-core.spawn"
local spy = require "luassert.spy"
local stub = require "luassert.stub"

Expand All @@ -16,6 +18,11 @@ local function venv_py(ctx)
end

describe("pypi manager", function()
before_each(function()
stub(spawn, "python3", mockx.returns(Result.success()))
spawn.python3.on_call_with({ "--version" }).returns(Result.success { stdout = "Python 3.12.0" })
end)

it("should init venv without upgrading pip", function()
local ctx = create_dummy_context()
stub(ctx, "promote_cwd")
Expand Down
Loading