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
|