aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/provider/python.lua
blob: 83221312383a44042b79658f21cbb991ff546976 (plain) (blame)
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