refactor: rewrite python provider healthcheck in Lua

This is required to remove the vimscript checkhealth functions.
This commit is contained in:
dundargoc 2023-04-09 22:28:00 +02:00 committed by GitHub
parent 41b7586cbb
commit 71225228fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 529 additions and 355 deletions

View File

@ -1,24 +1,10 @@
let s:shell_error = 0
function! s:is_bad_response(s) abort
return a:s =~? '\v(^unable)|(^error)|(^outdated)'
endfunction
function! s:trim(s) abort
return substitute(a:s, '^\_s*\|\_s*$', '', 'g')
endfunction
" Convert '\' to '/'. Collapse '//' and '/./'.
function! s:normalize_path(s) abort
return substitute(substitute(a:s, '\', '/', 'g'), '/\./\|/\+', '/', 'g')
endfunction
" Returns TRUE if `cmd` exits with success, else FALSE.
function! s:cmd_ok(cmd) abort
call system(a:cmd)
return v:shell_error == 0
endfunction
" Handler for s:system() function.
function! s:system_handler(jobid, data, event) dict abort
if a:event ==# 'stderr'
@ -88,143 +74,6 @@ function! s:system(cmd, ...) abort
return opts.output
endfunction
function! s:systemlist(cmd, ...) abort
let stdout = split(s:system(a:cmd, a:0 ? a:1 : ''), "\n")
if a:0 > 1 && !empty(a:2)
return filter(stdout, '!empty(v:val)')
endif
return stdout
endfunction
" Fetch the contents of a URL.
function! s:download(url) abort
let has_curl = executable('curl')
if has_curl && system(['curl', '-V']) =~# 'Protocols:.*https'
let rv = s:system(['curl', '-sL', a:url], '', 1, 1)
return s:shell_error ? 'curl error with '.a:url.': '.s:shell_error : rv
elseif executable('python')
let script = "
\try:\n
\ from urllib.request import urlopen\n
\except ImportError:\n
\ from urllib2 import urlopen\n
\\n
\response = urlopen('".a:url."')\n
\print(response.read().decode('utf8'))\n
\"
let rv = s:system(['python', '-c', script])
return empty(rv) && s:shell_error
\ ? 'python urllib.request error: '.s:shell_error
\ : rv
endif
return 'missing `curl` '
\ .(has_curl ? '(with HTTPS support) ' : '')
\ .'and `python`, cannot make web request'
endfunction
" Get the latest Nvim Python client (pynvim) version from PyPI.
function! s:latest_pypi_version() abort
let pypi_version = 'unable to get pypi response'
let pypi_response = s:download('https://pypi.python.org/pypi/pynvim/json')
if !empty(pypi_response)
try
let pypi_data = json_decode(pypi_response)
catch /E474/
return 'error: '.pypi_response
endtry
let pypi_version = get(get(pypi_data, 'info', {}), 'version', 'unable to parse')
endif
return pypi_version
endfunction
" Get version information using the specified interpreter. The interpreter is
" used directly in case breaking changes were introduced since the last time
" Nvim's Python client was updated.
"
" Returns: [
" {python executable version},
" {current nvim version},
" {current pypi nvim status},
" {installed version status}
" ]
function! s:version_info(python) abort
let pypi_version = s:latest_pypi_version()
let python_version = s:trim(s:system([
\ a:python,
\ '-c',
\ 'import sys; print(".".join(str(x) for x in sys.version_info[:3]))',
\ ]))
if empty(python_version)
let python_version = 'unable to parse '.a:python.' response'
endif
let nvim_path = s:trim(s:system([
\ a:python, '-c',
\ 'import sys; ' .
\ 'sys.path = [p for p in sys.path if p != ""]; ' .
\ 'import neovim; print(neovim.__file__)']))
if s:shell_error || empty(nvim_path)
return [python_version, 'unable to load neovim Python module', pypi_version,
\ nvim_path]
endif
" Assuming that multiple versions of a package are installed, sort them
" numerically in descending order.
function! s:compare(metapath1, metapath2) abort
let a = matchstr(fnamemodify(a:metapath1, ':p:h:t'), '[0-9.]\+')
let b = matchstr(fnamemodify(a:metapath2, ':p:h:t'), '[0-9.]\+')
return a == b ? 0 : a > b ? 1 : -1
endfunction
" Try to get neovim.VERSION (added in 0.1.11dev).
let nvim_version = s:system([a:python, '-c',
\ 'from neovim import VERSION as v; '.
\ 'print("{}.{}.{}{}".format(v.major, v.minor, v.patch, v.prerelease))'],
\ '', 1, 1)
if empty(nvim_version)
let nvim_version = 'unable to find pynvim module version'
let base = fnamemodify(nvim_path, ':h')
let metas = glob(base.'-*/METADATA', 1, 1)
\ + glob(base.'-*/PKG-INFO', 1, 1)
\ + glob(base.'.egg-info/PKG-INFO', 1, 1)
let metas = sort(metas, 's:compare')
if !empty(metas)
for meta_line in readfile(metas[0])
if meta_line =~# '^Version:'
let nvim_version = matchstr(meta_line, '^Version: \zs\S\+')
break
endif
endfor
endif
endif
let nvim_path_base = fnamemodify(nvim_path, ':~:h')
let version_status = 'unknown; '.nvim_path_base
if !s:is_bad_response(nvim_version) && !s:is_bad_response(pypi_version)
if v:lua.vim.version.lt(nvim_version, pypi_version)
let version_status = 'outdated; from '.nvim_path_base
else
let version_status = 'up to date'
endif
endif
return [python_version, nvim_version, pypi_version, version_status]
endfunction
" Check the Python interpreter's usability.
function! s:check_bin(bin) abort
if !filereadable(a:bin) && (!has('win32') || !filereadable(a:bin.'.exe'))
call health#report_error(printf('"%s" was not found.', a:bin))
return 0
elseif executable(a:bin) != 1
call health#report_error(printf('"%s" is not executable.', a:bin))
return 0
endif
return 1
endfunction
" Check "loaded" var for given a:provider.
" Returns 1 if the caller should return (skip checks).
function! s:disabled_via_loaded_var(provider) abort
@ -241,204 +90,6 @@ function! s:disabled_via_loaded_var(provider) abort
return 0
endfunction
function! s:check_python() abort
call health#report_start('Python 3 provider (optional)')
let pyname = 'python3'
let python_exe = ''
let venv = exists('$VIRTUAL_ENV') ? resolve($VIRTUAL_ENV) : ''
let host_prog_var = pyname.'_host_prog'
let python_multiple = []
if s:disabled_via_loaded_var(pyname)
return
endif
let [pyenv, pyenv_root] = s:check_for_pyenv()
if exists('g:'.host_prog_var)
call health#report_info(printf('Using: g:%s = "%s"', host_prog_var, get(g:, host_prog_var)))
endif
let [pyname, pythonx_warnings] = provider#pythonx#Detect(3)
if empty(pyname)
call health#report_warn('No Python executable found that can `import neovim`. '
\ . 'Using the first available executable for diagnostics.')
elseif exists('g:'.host_prog_var)
let python_exe = pyname
endif
" No Python executable could `import neovim`, or host_prog_var was used.
if !empty(pythonx_warnings)
call health#report_warn(pythonx_warnings, ['See :help provider-python for more information.',
\ 'You may disable this provider (and warning) by adding `let g:loaded_python3_provider = 0` to your init.vim'])
elseif !empty(pyname) && empty(python_exe)
if !exists('g:'.host_prog_var)
call health#report_info(printf('`g:%s` is not set. Searching for '
\ . '%s in the environment.', host_prog_var, pyname))
endif
if !empty(pyenv)
let python_exe = s:trim(s:system([pyenv, 'which', pyname], '', 1))
if empty(python_exe)
call health#report_warn(printf('pyenv could not find %s.', pyname))
endif
endif
if empty(python_exe)
let python_exe = exepath(pyname)
if exists('$PATH')
for path in split($PATH, has('win32') ? ';' : ':')
let path_bin = s:normalize_path(path.'/'.pyname)
if path_bin != s:normalize_path(python_exe)
\ && index(python_multiple, path_bin) == -1
\ && executable(path_bin)
call add(python_multiple, path_bin)
endif
endfor
if len(python_multiple)
" This is worth noting since the user may install something
" that changes $PATH, like homebrew.
call health#report_info(printf('Multiple %s executables found. '
\ . 'Set `g:%s` to avoid surprises.', pyname, host_prog_var))
endif
if python_exe =~# '\<shims\>'
call health#report_warn(printf('`%s` appears to be a pyenv shim.', python_exe), [
\ '`pyenv` is not in $PATH, your pyenv installation is broken. '
\ .'Set `g:'.host_prog_var.'` to avoid surprises.',
\ ])
endif
endif
endif
endif
if !empty(python_exe) && !exists('g:'.host_prog_var)
if empty(venv) && !empty(pyenv)
\ && !empty(pyenv_root) && resolve(python_exe) !~# '^'.pyenv_root.'/'
call health#report_warn('pyenv is not set up optimally.', [
\ printf('Create a virtualenv specifically '
\ . 'for Nvim using pyenv, and set `g:%s`. This will avoid '
\ . 'the need to install the pynvim module in each '
\ . 'version/virtualenv.', host_prog_var)
\ ])
elseif !empty(venv)
if !empty(pyenv_root)
let venv_root = pyenv_root
else
let venv_root = fnamemodify(venv, ':h')
endif
if resolve(python_exe) !~# '^'.venv_root.'/'
call health#report_warn('Your virtualenv is not set up optimally.', [
\ printf('Create a virtualenv specifically '
\ . 'for Nvim and use `g:%s`. This will avoid '
\ . 'the need to install the pynvim module in each '
\ . 'virtualenv.', host_prog_var)
\ ])
endif
endif
endif
if empty(python_exe) && !empty(pyname)
" An error message should have already printed.
call health#report_error(printf('`%s` was not found.', pyname))
elseif !empty(python_exe) && !s:check_bin(python_exe)
let python_exe = ''
endif
" Diagnostic output
call health#report_info('Executable: ' . (empty(python_exe) ? 'Not found' : python_exe))
if len(python_multiple)
for path_bin in python_multiple
call health#report_info('Other python executable: ' . path_bin)
endfor
endif
if empty(python_exe)
" No Python executable can import 'neovim'. Check if any Python executable
" can import 'pynvim'. If so, that Python failed to import 'neovim' as
" well, which is most probably due to a failed pip upgrade:
" https://github.com/neovim/neovim/wiki/Following-HEAD#20181118
let [pynvim_exe, errors] = provider#pythonx#DetectByModule('pynvim', 3)
if !empty(pynvim_exe)
call health#report_error(
\ 'Detected pip upgrade failure: Python executable can import "pynvim" but '
\ . 'not "neovim": '. pynvim_exe,
\ "Use that Python version to reinstall \"pynvim\" and optionally \"neovim\".\n"
\ . pynvim_exe ." -m pip uninstall pynvim neovim\n"
\ . pynvim_exe ." -m pip install pynvim\n"
\ . pynvim_exe ." -m pip install neovim # only if needed by third-party software")
endif
else
let [majorpyversion, current, latest, status] = s:version_info(python_exe)
if 3 != str2nr(majorpyversion)
call health#report_warn('Unexpected Python version.' .
\ ' This could lead to confusing error messages.')
endif
call health#report_info('Python version: ' . majorpyversion)
if s:is_bad_response(status)
call health#report_info(printf('pynvim version: %s (%s)', current, status))
else
call health#report_info(printf('pynvim version: %s', current))
endif
if s:is_bad_response(current)
call health#report_error(
\ "pynvim is not installed.\nError: ".current,
\ ['Run in shell: '. python_exe .' -m pip install pynvim'])
endif
if s:is_bad_response(latest)
call health#report_warn('Could not contact PyPI to get latest version.')
call health#report_error('HTTP request failed: '.latest)
elseif s:is_bad_response(status)
call health#report_warn(printf('Latest pynvim is NOT installed: %s', latest))
elseif !s:is_bad_response(current)
call health#report_ok(printf('Latest pynvim is installed.'))
endif
endif
endfunction
" Check if pyenv is available and a valid pyenv root can be found, then return
" their respective paths. If either of those is invalid, return two empty
" strings, effectively ignoring pyenv.
function! s:check_for_pyenv() abort
let pyenv_path = resolve(exepath('pyenv'))
if empty(pyenv_path)
return ['', '']
endif
call health#report_info('pyenv: Path: '. pyenv_path)
let pyenv_root = exists('$PYENV_ROOT') ? resolve($PYENV_ROOT) : ''
if empty(pyenv_root)
let pyenv_root = s:trim(s:system([pyenv_path, 'root']))
call health#report_info('pyenv: $PYENV_ROOT is not set. Infer from `pyenv root`.')
endif
if !isdirectory(pyenv_root)
call health#report_warn(
\ printf('pyenv: Root does not exist: %s. '
\ . 'Ignoring pyenv for all following checks.', pyenv_root))
return ['', '']
endif
call health#report_info('pyenv: Root: '.pyenv_root)
return [pyenv_path, pyenv_root]
endfunction
" Resolves Python executable path by invoking and checking `sys.executable`.
function! s:python_exepath(invocation) abort
return s:normalize_path(system(fnameescape(a:invocation)
@ -721,7 +372,6 @@ function! s:check_perl() abort
endfunction
function! health#provider2#check() abort
call s:check_python()
call s:check_virtualenv()
call s:check_ruby()
call s:check_node()

View File

@ -1,5 +1,17 @@
local M = {}
local start = vim.health.report_start
local ok = vim.health.report_ok
local info = vim.health.report_info
local warn = vim.health.report_warn
local error = vim.health.report_error
local iswin = vim.loop.os_uname().sysname == 'Windows_NT'
local shell_error_code = 0
local function shell_error()
return shell_error_code ~= 0
end
-- Returns true if `cmd` exits with success, else false.
local function cmd_ok(cmd)
vim.fn.system(cmd)
@ -14,8 +26,122 @@ local function is_blank(s)
return s:find('^%s*$') ~= nil
end
local function isdir(path)
if not path then
return false
end
local stat = vim.loop.fs_stat(path)
if not stat then
return false
end
return stat.type == 'directory'
end
local function isfile(path)
if not path then
return false
end
local stat = vim.loop.fs_stat(path)
if not stat then
return false
end
return stat.type == 'file'
end
-- Handler for s:system() function.
local function system_handler(self, _, data, event)
if event == 'stderr' then
if self.add_stderr_to_output then
self.output = self.output .. vim.fn.join(data, '')
else
self.stderr = self.stderr .. vim.fn.join(data, '')
end
elseif event == 'stdout' then
self.output = self.output .. vim.fn.join(data, '')
elseif event == 'exit' then
shell_error_code = data
end
end
-- Attempts to construct a shell command from an args list.
-- Only for display, to help users debug a failed command.
local function shellify(cmd)
if type(cmd) ~= 'table' then
return cmd
end
return vim.fn.join(
vim.fn.map(vim.fn.copy(cmd), [[v:val =~# ''\m[^\-.a-zA-Z_/]'' ? shellescape(v:val) : v:val]]),
' '
)
end
-- Run a system command and timeout after 30 seconds.
local function system(cmd, ...)
local args = { ... }
local args_count = vim.tbl_count(args)
local stdin = (args_count > 0 and args[1] or '')
local stderr = (args_count > 1 and args[2] or false)
local ignore_error = (args_count > 2 and args[3] or false)
local opts = {
add_stderr_to_output = stderr,
output = '',
stderr = '',
on_stdout = system_handler,
on_stderr = system_handler,
on_exit = system_handler,
}
local jobid = vim.fn.jobstart(cmd, opts)
if jobid < 1 then
local message = 'Command error (job='
.. jobid
.. '): `'
.. shellify(cmd)
.. '` (in '
.. vim.fn.string(vim.fn.getcwd())
.. ')'
error(message)
shell_error_code = 1
return opts.output
end
if not is_blank(stdin) then
vim.cmd([[call jobsend(jobid, stdin)]])
end
local res = vim.fn.jobwait({ jobid }, 30000)
if res[1] == -1 then
error('Command timed out: ' .. shellify(cmd))
vim.cmd([[call jobstop(jobid)]])
elseif shell_error() and not ignore_error then
local emsg = 'Command error (job='
.. jobid
.. ', exit code '
.. shell_error_code
.. '): `'
.. shellify(cmd)
.. '` (in '
.. vim.fn.string(vim.fn.getcwd())
.. ')'
if not is_blank(opts.output) then
emsg = emsg .. '\noutput: ' .. opts.output
end
if not is_blank(opts.stderr) then
emsg = emsg .. '\nstderr: ' .. opts.stderr
end
error(emsg)
end
-- return opts.output
local _ = ...
return vim.fn.system(cmd)
end
local function clipboard()
vim.health.report_start('Clipboard (optional)')
start('Clipboard (optional)')
if
os.getenv('TMUX')
@ -28,28 +154,426 @@ local function clipboard()
'Install tmux 2.6+. https://superuser.com/q/231130',
'or use tmux with reattach-to-user-namespace. https://superuser.com/a/413233',
}
vim.health.report_error('pbcopy does not work with tmux version: ' .. tmux_version, advice)
error('pbcopy does not work with tmux version: ' .. tmux_version, advice)
end
local clipboard_tool = vim.fn['provider#clipboard#Executable']()
if vim.g.clipboard and is_blank(clipboard_tool) then
local error_message = vim.fn['provider#clipboard#Error']()
vim.health.report_error(
error(
error_message,
"Use the example in :help g:clipboard as a template, or don't set g:clipboard at all."
)
elseif is_blank(clipboard_tool) then
vim.health.report_warn(
warn(
'No clipboard tool found. Clipboard registers (`"+` and `"*`) will not work.',
':help clipboard'
)
else
vim.health.report_ok('Clipboard tool found: ' .. clipboard_tool)
ok('Clipboard tool found: ' .. clipboard_tool)
end
end
local function disabled_via_loaded_var(provider)
local loaded_var = 'loaded_' .. provider .. '_provider'
local v = vim.g[loaded_var]
if v == 0 then
info('Disabled (' .. loaded_var .. '=' .. v .. ').')
return true
end
return false
end
-- Check if pyenv is available and a valid pyenv root can be found, then return
-- their respective paths. If either of those is invalid, return two empty
-- strings, effectively ignoring pyenv.
local function check_for_pyenv()
local pyenv_path = vim.fn.resolve(vim.fn.exepath('pyenv'))
if is_blank(pyenv_path) then
return { '', '' }
end
info('pyenv: Path: ' .. pyenv_path)
local pyenv_root = os.getenv('PYENV_ROOT') and vim.fn.resolve('$PYENV_ROOT') or ''
if is_blank(pyenv_root) then
pyenv_root = vim.trim(system({ pyenv_path, 'root' }))
info('pyenv: $PYENV_ROOT is not set. Infer from `pyenv root`.')
end
if not isdir(pyenv_root) then
local message = 'pyenv: Root does not exist: '
.. pyenv_root
.. '. Ignoring pyenv for all following checks.'
warn(message)
return { '', '' }
end
info('pyenv: Root: ' .. pyenv_root)
return { pyenv_path, pyenv_root }
end
-- Check the Python interpreter's usability.
local function check_bin(bin)
if not isfile(bin) and (not iswin or not isfile(bin .. '.exe')) then
error('"' .. bin .. '" was not found.')
return false
elseif not executable(bin) then
error('"' .. bin .. '" is not executable.')
return false
end
return true
end
-- Fetch the contents of a URL.
local function download(url)
local has_curl = executable('curl')
if has_curl and vim.fn.system({ 'curl', '-V' }):find('Protocols:.*https') then
local rv = system({ 'curl', '-sL', url }, '', 1, 1)
if shell_error() then
return 'curl error with ' .. url .. ': ' .. shell_error_code
else
return rv
end
elseif executable('python') then
local script = "try:\n\
from urllib.request import urlopen\n\
except ImportError:\n\
from urllib2 import urlopen\n\
response = urlopen('" .. url .. "')\n\
print(response.read().decode('utf8'))\n"
local rv = system({ 'python', '-c', script })
if is_blank(rv) and shell_error() then
return 'python urllib.request error: ' .. shell_error_code
else
return rv
end
end
local message = 'missing `curl` '
if has_curl then
message = message .. '(with HTTPS support) '
end
message = message .. 'and `python`, cannot make web request'
return message
end
-- Get the latest Nvim Python client (pynvim) version from PyPI.
local function latest_pypi_version()
local pypi_version = 'unable to get pypi response'
local pypi_response = download('https://pypi.python.org/pypi/pynvim/json')
if not is_blank(pypi_response) then
local pcall_ok, output = pcall(vim.fn.json_decode, pypi_response)
local pypi_data
if pcall_ok then
pypi_data = output
else
return 'error: ' .. pypi_response
end
local pypi_element = pypi_data['info'] or {}
pypi_version = pypi_element['version'] or 'unable to parse'
end
return pypi_version
end
local function is_bad_response(s)
local lower = s:lower()
return vim.startswith(lower, 'unable')
or vim.startswith(lower, 'error')
or vim.startswith(lower, 'outdated')
end
-- Get version information using the specified interpreter. The interpreter is
-- used directly in case breaking changes were introduced since the last time
-- Nvim's Python client was updated.
--
-- Returns: {
-- {python executable version},
-- {current nvim version},
-- {current pypi nvim status},
-- {installed version status}
-- }
local function version_info(python)
local pypi_version = latest_pypi_version()
local python_version = vim.trim(system({
python,
'-c',
'import sys; print(".".join(str(x) for x in sys.version_info[:3]))',
}))
if is_blank(python_version) then
python_version = 'unable to parse ' .. python .. ' response'
end
local nvim_path = vim.trim(system({
python,
'-c',
'import sys; sys.path = [p for p in sys.path if p != ""]; import neovim; print(neovim.__file__)',
}))
if shell_error() or is_blank(nvim_path) then
return { python_version, 'unable to load neovim Python module', pypi_version, nvim_path }
end
-- Assuming that multiple versions of a package are installed, sort them
-- numerically in descending order.
local function compare(metapath1, metapath2)
local a = vim.fn.matchstr(vim.fn.fnamemodify(metapath1, ':p:h:t'), [[[0-9.]\+]])
local b = vim.fn.matchstr(vim.fn.fnamemodify(metapath2, ':p:h:t'), [[[0-9.]\+]])
if a == b then
return 0
elseif a > b then
return 1
else
return -1
end
end
-- Try to get neovim.VERSION (added in 0.1.11dev).
local nvim_version = system({
python,
'-c',
'from neovim import VERSION as v; print("{}.{}.{}{}".format(v.major, v.minor, v.patch, v.prerelease))',
}, '', 1, 1)
if is_blank(nvim_version) then
nvim_version = 'unable to find pynvim module version'
local base = vim.fs.basename(nvim_path, ':h')
local metas = vim.fn.glob(base .. '-*/METADATA', 1, 1)
vim.list_extend(metas, vim.fn.glob(base .. '-*/PKG-INFO', 1, 1))
vim.list_extend(metas, vim.fn.glob(base .. '.egg-info/PKG-INFO', 1, 1))
metas = table.sort(metas, compare)
if metas and next(metas) ~= nil then
for _, meta_line in ipairs(vim.fn.readfile(metas[1])) do
if vim.startswith(meta_line, 'Version:') then
nvim_version = vim.fn.matchstr(meta_line, [[^Version: \zs\S\+]])
break
end
end
end
end
local nvim_path_base = vim.fn.fnamemodify(nvim_path, [[:~:h]])
local version_status = 'unknown; ' .. nvim_path_base
if is_bad_response(nvim_version) and is_bad_response(pypi_version) then
if vim.version.lt(nvim_version, pypi_version) then
version_status = 'outdated; from ' .. nvim_path_base
else
version_status = 'up to date'
end
end
return { python_version, nvim_version, pypi_version, version_status }
end
local function python()
start('Python 3 provider (optional)')
local pyname = 'python3'
local python_exe = ''
local virtual_env = os.getenv('VIRTUAL_ENV')
local venv = virtual_env and vim.fn.resolve(virtual_env) or ''
local host_prog_var = pyname .. '_host_prog'
local python_multiple = {}
if disabled_via_loaded_var(pyname) then
return
end
local pyenv_table = check_for_pyenv()
local pyenv = pyenv_table[1]
local pyenv_root = pyenv_table[2]
if vim.g['host_prog_var'] then
local message = 'Using: g:' .. host_prog_var .. ' = "' .. vim.g['host_prog_var'] .. '"'
info(message)
end
local python_table = vim.fn['provider#pythonx#Detect'](3)
pyname = python_table[1]
local pythonx_warnings = python_table[2]
if is_blank(pyname) then
warn(
'No Python executable found that can `import neovim`. '
.. 'Using the first available executable for diagnostics.'
)
elseif vim.g['host_prog_var'] then
python_exe = pyname
end
-- No Python executable could `import neovim`, or host_prog_var was used.
if not is_blank(pythonx_warnings) then
warn(pythonx_warnings, {
'See :help provider-python for more information.',
'You may disable this provider (and warning) by adding `let g:loaded_python3_provider = 0` to your init.vim',
})
elseif not is_blank(pyname) and is_blank(python_exe) then
if not vim.g['host_prog_var'] then
local message = '`g:'
.. host_prog_var
.. '` is not set. Searching for '
.. pyname
.. ' in the environment.'
info(message)
end
if not is_blank(pyenv) then
python_exe = vim.trim(system({ pyenv, 'which', pyname }, '', 1))
if is_blank(python_exe) then
warn('pyenv could not find ' .. pyname .. '.')
end
end
if is_blank(python_exe) then
python_exe = vim.fn.exepath(pyname)
if os.getenv('PATH') then
local path_sep = iswin and ';' or ':'
local paths = vim.split(os.getenv('PATH') or '', path_sep)
for _, path in ipairs(paths) do
local path_bin = vim.fs.normalize(path .. '/' .. pyname)
if
path_bin ~= vim.fs.normalize(python_exe)
and vim.tbl_contains(python_multiple, path_bin)
and executable(path_bin)
then
python_multiple[#python_multiple + 1] = path_bin
end
end
if vim.tbl_count(python_multiple) > 0 then
-- This is worth noting since the user may install something
-- that changes $PATH, like homebrew.
local message = 'Multiple '
.. pyname
.. ' executables found. '
.. 'Set `g:'
.. host_prog_var
.. '` to avoid surprises.'
info(message)
end
if python_exe:find('shims') then
local message = '`' .. python_exe .. '` appears to be a pyenv shim.'
local advice = '`pyenv` is not in $PATH, your pyenv installation is broken. Set `g:'
.. host_prog_var
.. '` to avoid surprises.'
warn(message, advice)
end
end
end
end
if not is_blank(python_exe) and not vim.g[host_prog_var] then
if
is_blank(venv)
and not is_blank(pyenv)
and not is_blank(pyenv_root)
and vim.startswith(vim.fn.resolve(python_exe), pyenv_root .. '/')
then
local advice = 'Create a virtualenv specifically for Nvim using pyenv, and set `g:'
.. host_prog_var
.. '`. This will avoid the need to install the pynvim module in each version/virtualenv.'
warn('pyenv is not set up optimally.', advice)
elseif not is_blank(venv) then
local venv_root
if not is_blank(pyenv_root) then
venv_root = pyenv_root
else
venv_root = vim.fs.basename(venv)
end
if vim.startswith(vim.fn.resolve(python_exe), venv_root .. '/') then
local advice = 'Create a virtualenv specifically for Nvim and use `g:'
.. host_prog_var
.. '`. This will avoid the need to install the pynvim module in each virtualenv.'
warn('Your virtualenv is not set up optimally.', advice)
end
end
end
if is_blank(python_exe) and not is_blank(pyname) then
-- An error message should have already printed.
error('`' .. pyname .. '` was not found.')
elseif not is_blank(python_exe) and not check_bin(python_exe) then
python_exe = ''
end
-- Diagnostic output
info('Executable: ' .. (is_blank(python_exe) and 'Not found' or python_exe))
if vim.tbl_count(python_multiple) > 0 then
for _, path_bin in ipairs(python_multiple) do
info('Other python executable: ' .. path_bin)
end
end
if is_blank(python_exe) then
-- No Python executable can import 'neovim'. Check if any Python executable
-- can import 'pynvim'. If so, that Python failed to import 'neovim' as
-- well, which is most probably due to a failed pip upgrade:
-- https://github.com/neovim/neovim/wiki/Following-HEAD#20181118
local pynvim_table = vim.fn['provider#pythonx#DetectByModule']('pynvim', 3)
local pynvim_exe = pynvim_table[1]
if not is_blank(pynvim_exe) then
local message = 'Detected pip upgrade failure: Python executable can import "pynvim" but not "neovim": '
.. pynvim_exe
local advice = {
'Use that Python version to reinstall "pynvim" and optionally "neovim".',
pynvim_exe .. ' -m pip uninstall pynvim neovim',
pynvim_exe .. ' -m pip install pynvim',
pynvim_exe .. ' -m pip install neovim # only if needed by third-party software',
}
error(message, advice)
end
else
local version_info_table = version_info(python_exe)
local majorpyversion = version_info_table[1]
local current = version_info_table[2]
local latest = version_info_table[3]
local status = version_info_table[4]
if vim.fn.str2nr(majorpyversion) ~= 3 then
warn('Unexpected Python version. This could lead to confusing error messages.')
end
info('Python version: ' .. majorpyversion)
if is_bad_response(status) then
info('pynvim version: ' .. current .. ' (' .. status .. ')')
else
info('pynvim version: ' .. current)
end
if is_bad_response(current) then
error(
'pynvim is not installed.\nError: ' .. current,
'Run in shell: ' .. python_exe .. ' -m pip install pynvim'
)
end
if is_bad_response(latest) then
warn('Could not contact PyPI to get latest version.')
error('HTTP request failed: ' .. latest)
elseif is_bad_response(status) then
warn('Latest pynvim is NOT installed: ' .. latest)
elseif not is_bad_response(current) then
ok('Latest pynvim is installed.')
end
end
end
function M.check()
clipboard()
python()
end
return M