neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

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