python.lua 4.2 KB

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