1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
|
local M = {}
local min_version = '3.7'
local s_err ---@type string?
local s_host ---@type string?
local python_candidates = {
'python3',
'python3.12',
'python3.11',
'python3.10',
'python3.9',
'python3.8',
'python3.7',
'python',
}
--- @param prog string
--- @param module string
--- @return integer, string
local function import_module(prog, module)
local program = [[
import sys, importlib.util;
sys.path = [p for p in sys.path if p != ""];
sys.stdout.write(str(sys.version_info[0]) + "." + str(sys.version_info[1]));]]
program = program
.. string.format('sys.exit(2 * int(importlib.util.find_spec("%s") is None))', module)
local out = vim.system({ prog, '-W', 'ignore', '-c', program }):wait()
return out.code, assert(out.stdout)
end
--- @param prog string
--- @param module string
--- @return string?
local function check_for_module(prog, module)
local prog_path = vim.fn.exepath(prog)
if prog_path == '' then
return prog .. ' not found in search path or not executable.'
end
-- Try to load module, and output Python version.
-- Exit codes:
-- 0 module can be loaded.
-- 2 module cannot be loaded.
-- Otherwise something else went wrong (e.g. 1 or 127).
local prog_exitcode, prog_version = import_module(prog, module)
if prog_exitcode == 2 or prog_exitcode == 0 then
-- Check version only for expected return codes.
if vim.version.lt(prog_version, min_version) then
return string.format(
'%s is Python %s and cannot provide Python >= %s.',
prog_path,
prog_version,
min_version
)
end
end
if prog_exitcode == 2 then
return string.format('%s does not have the "%s" module.', prog_path, module)
elseif prog_exitcode == 127 then
-- This can happen with pyenv's shims.
return string.format('%s does not exist: %s', prog_path, prog_version)
elseif prog_exitcode ~= 0 then
return string.format(
'Checking %s caused an unknown error. (%s, output: %s) Report this at https://github.com/neovim/neovim',
prog_path,
prog_exitcode,
prog_version
)
end
return nil
end
--- @param module string
--- @return string? path to detected python, if any; nil if not found
--- @return string? error message if python can't be detected by {module}; nil if success
function M.detect_by_module(module)
local python_exe = vim.fn.expand(vim.g.python3_host_prog or '', true)
if python_exe ~= '' then
return vim.fn.exepath(vim.fn.expand(python_exe, true)), nil
end
local errors = {}
for _, exe in ipairs(python_candidates) do
local error = check_for_module(exe, module)
if not error then
return exe, error
end
-- Accumulate errors in case we don't find any suitable Python executable.
table.insert(errors, error)
end
-- No suitable Python executable found.
return nil, 'Could not load Python :\n' .. table.concat(errors, '\n')
end
function M.require(host)
-- Python host arguments
local prog = M.detect_by_module('neovim')
local args = {
prog,
'-c',
'import sys; sys.path = [p for p in sys.path if p != ""]; import neovim; neovim.start_host()',
}
-- Collect registered Python plugins into args
local python_plugins = vim.fn['remote#host#PluginsForHost'](host.name) ---@type any
---@param plugin any
for _, plugin in ipairs(python_plugins) do
table.insert(args, plugin.path)
end
return vim.fn['provider#Poll'](
args,
host.orig_name,
'$NVIM_PYTHON_LOG_FILE',
{ ['overlapped'] = true }
)
end
function M.call(method, args)
if s_err then
return
end
if not s_host then
-- Ensure that we can load the Python3 host before bootstrapping
local ok, result = pcall(vim.fn['remote#host#Require'], 'legacy-python3-provider') ---@type any, any
if not ok then
s_err = result
vim.api.nvim_echo({ { result, 'WarningMsg' } }, true, {})
return
end
s_host = result
end
return vim.fn.rpcrequest(s_host, 'python_' .. method, unpack(args))
end
function M.start()
-- The Python3 provider plugin will run in a separate instance of the Python3 host.
vim.fn['remote#host#RegisterClone']('legacy-python3-provider', 'python3')
vim.fn['remote#host#RegisterPlugin']('legacy-python3-provider', 'script_host.py', {})
end
return M
|