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