From 4c162877e2943ac8f6b29bc79ccf313f8eb88ba6 Mon Sep 17 00:00:00 2001 From: w0rp Date: Wed, 8 Feb 2023 09:11:31 +0000 Subject: #2172 Auto PATH with ale_python_auto_virtualenv Automatically set `PATH` for some Python linters that seem to need it when g:ale_python_auto_virtualenv or b:ale_python_auto_virtualenv is `1`. --- ale_linters/python/jedils.vim | 8 +- ale_linters/python/pylsp.vim | 8 +- ale_linters/python/pyright.vim | 8 +- autoload/ale/python.vim | 20 +++- doc/ale-python.txt | 105 ++++++++++++--------- plugin/ale.vim | 4 + test/linter/test_jedils.vader | 47 +++++++++ test/linter/test_pylsp.vader | 14 +++ test/linter/test_pyright.vader | 14 +++ .../with_virtualenv/env/bin/jedi-language-server | 0 10 files changed, 174 insertions(+), 54 deletions(-) create mode 100644 test/linter/test_jedils.vader create mode 100755 test/test-files/python/with_virtualenv/env/bin/jedi-language-server diff --git a/ale_linters/python/jedils.vim b/ale_linters/python/jedils.vim index eae5fb07..d3e15bf2 100644 --- a/ale_linters/python/jedils.vim +++ b/ale_linters/python/jedils.vim @@ -16,12 +16,16 @@ endfunction function! ale_linters#python#jedils#GetCommand(buffer) abort let l:executable = ale_linters#python#jedils#GetExecutable(a:buffer) - let l:exec_args = l:executable =~? 'pipenv$' \ ? ' run jedi-language-server' \ : '' + let l:env_string = '' + + if ale#Var(a:buffer, 'python_auto_virtualenv') + let l:env_string = ale#python#AutoVirtualenvEnvString(a:buffer) + endif - return ale#Escape(l:executable) . l:exec_args + return l:env_string . ale#Escape(l:executable) . l:exec_args endfunction call ale#linter#Define('python', { diff --git a/ale_linters/python/pylsp.vim b/ale_linters/python/pylsp.vim index a699e4f6..a1c31018 100644 --- a/ale_linters/python/pylsp.vim +++ b/ale_linters/python/pylsp.vim @@ -37,12 +37,16 @@ endfunction function! ale_linters#python#pylsp#GetCommand(buffer) abort let l:executable = ale_linters#python#pylsp#GetExecutable(a:buffer) - let l:exec_args = l:executable =~? 'pipenv\|poetry$' \ ? ' run pylsp' \ : '' + let l:env_string = '' + + if ale#Var(a:buffer, 'python_auto_virtualenv') + let l:env_string = ale#python#AutoVirtualenvEnvString(a:buffer) + endif - return ale#Escape(l:executable) . l:exec_args . ale#Pad(ale#Var(a:buffer, 'python_pylsp_options')) + return l:env_string . ale#Escape(l:executable) . l:exec_args . ale#Pad(ale#Var(a:buffer, 'python_pylsp_options')) endfunction call ale#linter#Define('python', { diff --git a/ale_linters/python/pyright.vim b/ale_linters/python/pyright.vim index f16af84c..ccade0c9 100644 --- a/ale_linters/python/pyright.vim +++ b/ale_linters/python/pyright.vim @@ -64,12 +64,16 @@ endfunction function! ale_linters#python#pyright#GetCommand(buffer) abort let l:executable = ale_linters#python#pyright#GetExecutable(a:buffer) - let l:exec_args = l:executable =~? 'pipenv\|poetry$' \ ? ' run pyright' \ : '' + let l:env_string = '' + + if ale#Var(a:buffer, 'python_auto_virtualenv') + let l:env_string = ale#python#AutoVirtualenvEnvString(a:buffer) + endif - return ale#Escape(l:executable) . l:exec_args . ' --stdio' + return l:env_string . ale#Escape(l:executable) . l:exec_args . ' --stdio' endfunction call ale#linter#Define('python', { diff --git a/autoload/ale/python.vim b/autoload/ale/python.vim index 7a998414..92e48da8 100644 --- a/autoload/ale/python.vim +++ b/autoload/ale/python.vim @@ -1,4 +1,4 @@ -" Author: w0rp +" Author: w0rp " Description: Functions for integrating with Python linters. call ale#Set('python_auto_pipenv', '0') @@ -96,6 +96,24 @@ function! ale#python#FindVirtualenv(buffer) abort return $VIRTUAL_ENV endfunction +" Automatically determine virtualenv environment variables and build +" a string of them to prefix linter commands with. +function! ale#python#AutoVirtualenvEnvString(buffer) abort + let l:venv_dir = ale#python#FindVirtualenv(a:buffer) + let l:sep = has('win32') ? ';' : ':' + + if !empty(l:venv_dir) + let l:vars = [ + \ ['PATH', ale#path#Simplify(l:venv_dir . '/bin') . l:sep . $PATH], + \] + + " We don't need a space between var as ale#Env adds one. + return join(map(l:vars, 'ale#Env(v:val[0], v:val[1])'), '') + endif + + return '' +endfunction + " Given a buffer number and a command name, find the path to the executable. " First search on a virtualenv for Python, if nothing is found, try the global " command. Returns an empty string if cannot find the executable diff --git a/doc/ale-python.txt b/doc/ale-python.txt index aad64b1d..d7c5cacc 100644 --- a/doc/ale-python.txt +++ b/doc/ale-python.txt @@ -20,6 +20,17 @@ g:ale_python_auto_poetry *g:ale_python_auto_poetry* if true. This is overridden by a manually-set executable. +g:ale_python_auto_virtualenv *g:ale_python_auto_virtualenv* + *b:ale_python_auto_virtualenv* + Type: |Number| + Default: `0` + + If set to `1`, ALE will automatically set environment variables for commands + such as `PATH` to attempt to make the experience of running Python linters + via virtualenv easier, without the need for another plugin or some + specialised setup. + + =============================================================================== ALE Python Project Root Behavior *ale-python-root* @@ -88,24 +99,24 @@ g:ale_python_autoflake_use_global *g:ale_python_autoflake_use_global* =============================================================================== autoimport *ale-python-autoimport* -g:ale_python_autoimport_executable *g:ale_python_autoimport_executable* - *b:ale_python_autoimport_executable* +g:ale_python_autoimport_executable *g:ale_python_autoimport_executable* + *b:ale_python_autoimport_executable* Type: |String| Default: `'autoimport'` See |ale-integrations-local-executables| -g:ale_python_autoimport_options *g:ale_python_autoimport_options* - *b:ale_python_autoimport_options* +g:ale_python_autoimport_options *g:ale_python_autoimport_options* + *b:ale_python_autoimport_options* Type: |String| Default: `''` This variable can be set to pass extra options to autoimport. -g:ale_python_autoimport_use_global *g:ale_python_autoimport_use_global* - *b:ale_python_autoimport_use_global* +g:ale_python_autoimport_use_global *g:ale_python_autoimport_use_global* + *b:ale_python_autoimport_use_global* Type: |Number| Default: `get(g:, 'ale_use_global_executables', 0)` @@ -338,7 +349,7 @@ g:ale_python_flake8_auto_poetry *g:ale_python_flake8_auto_poetry* flakehell *ale-python-flakehell* g:ale_python_flakehell_change_directory*g:ale_python_flakehell_change_directory* - *b:ale_python_flakehell_change_directory* + *b:ale_python_flakehell_change_directory* Type: |String| Default: `project` @@ -349,8 +360,8 @@ g:ale_python_flakehell_change_directory*g:ale_python_flakehell_change_directory* Python is executed from yourself. -g:ale_python_flakehell_executable *g:ale_python_flakehell_executable* - *b:ale_python_flakehell_executable* +g:ale_python_flakehell_executable *g:ale_python_flakehell_executable* + *b:ale_python_flakehell_executable* Type: |String| Default: `'flakehell'` @@ -360,8 +371,8 @@ g:ale_python_flakehell_executable *g:ale_python_flakehell_executable* invoke `'python` `-m` `flakehell'`. -g:ale_python_flakehell_options *g:ale_python_flakehell_options* - *b:ale_python_flakehell_options* +g:ale_python_flakehell_options *g:ale_python_flakehell_options* + *b:ale_python_flakehell_options* Type: |String| Default: `''` @@ -369,8 +380,8 @@ g:ale_python_flakehell_options *g:ale_python_flakehell_options* lint invocation. -g:ale_python_flakehell_use_global *g:ale_python_flakehell_use_global* - *b:ale_python_flakehell_use_global* +g:ale_python_flakehell_use_global *g:ale_python_flakehell_use_global* + *b:ale_python_flakehell_use_global* Type: |Number| Default: `get(g:, 'ale_use_global_executables', 0)` @@ -381,8 +392,8 @@ g:ale_python_flakehell_use_global *g:ale_python_flakehell_use_global* Both variables can be set with `b:` buffer variables instead. -g:ale_python_flakehell_auto_pipenv *g:ale_python_flakehell_auto_pipenv* - *b:ale_python_flakehell_auto_pipenv* +g:ale_python_flakehell_auto_pipenv *g:ale_python_flakehell_auto_pipenv* + *b:ale_python_flakehell_auto_pipenv* Type: |Number| Default: `0` @@ -975,13 +986,13 @@ g:ale_python_pylint_use_msg_id *g:ale_python_pylint_use_msg_id* =============================================================================== -pylsp *ale-python-pylsp* +pylsp *ale-python-pylsp* `pylsp` will be run from a detected project root, per |ale-python-root|. -g:ale_python_pylsp_executable *g:ale_python_pylsp_executable* - *b:ale_python_pylsp_executable* +g:ale_python_pylsp_executable *g:ale_python_pylsp_executable* + *b:ale_python_pylsp_executable* Type: |String| Default: `'pylsp'` @@ -991,16 +1002,16 @@ g:ale_python_pylsp_executable *g:ale_python_pylsp_executable Set this to `'poetry'` to invoke `'poetry` `run` `pyls'`. -g:ale_python_pylsp_use_global *g:ale_python_pylsp_use_global* - *b:ale_python_pylsp_use_global* +g:ale_python_pylsp_use_global *g:ale_python_pylsp_use_global* + *b:ale_python_pylsp_use_global* Type: |Number| Default: `get(g:, 'ale_use_global_executables', 0)` See |ale-integrations-local-executables| -g:ale_python_pylsp_auto_pipenv *g:ale_python_pylsp_auto_pipenv* - *b:ale_python_pylsp_auto_pipenv* +g:ale_python_pylsp_auto_pipenv *g:ale_python_pylsp_auto_pipenv* + *b:ale_python_pylsp_auto_pipenv* Type: |Number| Default: `0` @@ -1008,8 +1019,8 @@ g:ale_python_pylsp_auto_pipenv *g:ale_python_pylsp_auto_pipenv if true. This is overridden by a manually-set executable. -g:ale_python_pylsp_auto_poetry *g:ale_python_pylsp_auto_poetry* - *b:ale_python_pylsp_auto_poetry* +g:ale_python_pylsp_auto_poetry *g:ale_python_pylsp_auto_poetry* + *b:ale_python_pylsp_auto_poetry* Type: |Number| Default: `0` @@ -1017,8 +1028,8 @@ g:ale_python_pylsp_auto_poetry *g:ale_python_pylsp_auto_poetry if true. This is overridden by a manually-set executable. -g:ale_python_pylsp_config *g:ale_python_pylsp_config* - *b:ale_python_pylsp_config* +g:ale_python_pylsp_config *g:ale_python_pylsp_config* + *b:ale_python_pylsp_config* Type: |Dictionary| Default: `{}` @@ -1035,8 +1046,8 @@ g:ale_python_pylsp_config *g:ale_python_pylsp_config \ } < -g:ale_python_pylsp_options *g:ale_python_pylsp_options* - *b:ale_python_pylsp_options* +g:ale_python_pylsp_options *g:ale_python_pylsp_options* + *b:ale_python_pylsp_options* Type: |String| Default: `''` @@ -1158,10 +1169,10 @@ g:ale_python_pyright_config *g:ale_python_pyright_config* < =============================================================================== -refurb *ale-python-refurb* +refurb *ale-python-refurb* -g:ale_python_refurb_change_directory *g:ale_python_refurb_change_directory* - *b:ale_python_refurb_change_directory* +g:ale_python_refurb_change_directory *g:ale_python_refurb_change_directory* + *b:ale_python_refurb_change_directory* Type: |Number| Default: `1` @@ -1170,8 +1181,8 @@ g:ale_python_refurb_change_directory *g:ale_python_refurb_change_directory `refurb` will be run from the buffer's directory. -g:ale_python_refurb_executable *g:ale_python_refurb_executable* - *b:ale_python_refurb_executable* +g:ale_python_refurb_executable *g:ale_python_refurb_executable* + *b:ale_python_refurb_executable* Type: |String| Default: `'refurb'` @@ -1181,8 +1192,8 @@ g:ale_python_refurb_executable *g:ale_python_refurb_executable Set this to `'poetry'` to invoke `'poetry` `run` `refurb'`. -g:ale_python_refurb_options *g:ale_python_refurb_options* - *b:ale_python_refurb_options* +g:ale_python_refurb_options *g:ale_python_refurb_options* + *b:ale_python_refurb_options* Type: |String| Default: `''` @@ -1192,16 +1203,16 @@ g:ale_python_refurb_options *g:ale_python_refurb_options For example, to select/enable and/or disable some error codes, you may want to set > let g:ale_python_refurb_options = '--ignore 100' -g:ale_python_refurb_use_global *g:ale_python_refurb_use_global* - *b:ale_python_refurb_use_global* +g:ale_python_refurb_use_global *g:ale_python_refurb_use_global* + *b:ale_python_refurb_use_global* Type: |Number| Default: `get(g:, 'ale_use_global_executables', 0)` See |ale-integrations-local-executables| -g:ale_python_refurb_auto_pipenv *g:ale_python_refurb_auto_pipenv* - *b:ale_python_refurb_auto_pipenv* +g:ale_python_refurb_auto_pipenv *g:ale_python_refurb_auto_pipenv* + *b:ale_python_refurb_auto_pipenv* Type: |Number| Default: `0` @@ -1209,8 +1220,8 @@ g:ale_python_refurb_auto_pipenv *g:ale_python_refurb_auto_pipenv if true. This is overridden by a manually-set executable. -g:ale_python_refurb_auto_poetry *g:ale_python_refurb_auto_poetry* - *b:ale_python_refurb_auto_poetry* +g:ale_python_refurb_auto_poetry *g:ale_python_refurb_auto_poetry* + *b:ale_python_refurb_auto_poetry* Type: |Number| Default: `0` @@ -1335,8 +1346,8 @@ g:ale_python_unimport_auto_poetry *g:ale_python_unimport_auto_poetry* if true. This is overridden by a manually-set executable. -g:ale_python_unimport_executable *g:ale_python_unimport_executable* - *b:ale_python_unimport_executable* +g:ale_python_unimport_executable *g:ale_python_unimport_executable* + *b:ale_python_unimport_executable* Type: |String| Default: `'unimport'` @@ -1346,8 +1357,8 @@ g:ale_python_unimport_executable *g:ale_python_unimport_executable* Set this to `'poetry'` to invoke `'poetry` `run` `unimport'`. -g:ale_python_unimport_options *g:ale_python_unimport_options* - *b:ale_python_unimport_options* +g:ale_python_unimport_options *g:ale_python_unimport_options* + *b:ale_python_unimport_options* Type: |String| Default: `''` @@ -1355,8 +1366,8 @@ g:ale_python_unimport_options *g:ale_python_unimport_options* invocation. -g:ale_python_unimport_use_global *g:ale_python_unimport_use_global* - *b:ale_python_unimport_use_global* +g:ale_python_unimport_use_global *g:ale_python_unimport_use_global* + *b:ale_python_unimport_use_global* Type: |Number| Default: `get(g:, 'ale_use_global_executables', 0)` diff --git a/plugin/ale.vim b/plugin/ale.vim index b9786bb0..f9b6cab8 100644 --- a/plugin/ale.vim +++ b/plugin/ale.vim @@ -178,6 +178,10 @@ let g:ale_python_auto_pipenv = get(g:, 'ale_python_auto_pipenv', 0) " Enable automatic detection of poetry for Python linters. let g:ale_python_auto_poetry = get(g:, 'ale_python_auto_poetry', 0) +" Enable automatic adjustment of environment variables for Python linters. +" The variables are set based on ALE's virtualenv detection. +let g:ale_python_auto_virtualenv = get(g:, 'ale_python_auto_virtualenv', 0) + " This variable can be overridden to set the GO111MODULE environment variable. let g:ale_go_go111module = get(g:, 'ale_go_go111module', '') diff --git a/test/linter/test_jedils.vader b/test/linter/test_jedils.vader new file mode 100644 index 00000000..0c2781bd --- /dev/null +++ b/test/linter/test_jedils.vader @@ -0,0 +1,47 @@ +Before: + call ale#assert#SetUpLinterTest('python', 'jedils') + Save b:ale_python_auto_virtualenv + + let b:bin_dir = has('win32') ? 'Scripts' : 'bin' + +After: + unlet! b:bin_dir + unlet! b:venv_bin + unlet! b:sep + unlet! b:executable + + call ale#assert#TearDownLinterTest() + +Execute(The jedi-language-server command callback should return default string): + call ale#test#SetFilename('./foo.py') + + AssertLinter 'jedi-language-server', ale#Escape('jedi-language-server') + +Execute(The jedi-language-server executable should be configurable): + let g:ale_python_jedils_executable = '~/.local/bin/jedi-language-server' + + AssertLinter '~/.local/bin/jedi-language-server' , ale#Escape('~/.local/bin/jedi-language-server') + +Execute(virtualenv vars should be used when ale_python_auto_virtualenv = 1): + let b:ale_python_auto_virtualenv = 1 + call ale#test#SetFilename('../test-files/python/with_virtualenv/subdir/foo/bar.py') + + let b:venv_bin = ale#path#Simplify(g:dir . '/../test-files/python/with_virtualenv/env/' . b:bin_dir) + let b:sep = has('win32') ? ';' : ':' + let b:executable = ale#path#Simplify(b:venv_bin . '/jedi-language-server') + + AssertLinter b:executable, ale#Env('PATH', b:venv_bin . b:sep . $PATH) + \ . ale#Escape(b:executable) + +Execute(You should be able to override the jedi-language-server virtualenv lookup): + call ale#test#SetFilename('../test-files/python/with_virtualenv/subdir/foo/bar.py') + + let g:ale_python_jedils_use_global = 1 + + AssertLinter 'jedi-language-server', ale#Escape('jedi-language-server') + +Execute(Setting executable to 'pipenv' appends 'run jedi-language-server'): + let g:ale_python_jedils_executable = 'path/to/pipenv' + call ale#test#SetFilename('../test-files/dummy') + + AssertLinter 'path/to/pipenv', ale#Escape('path/to/pipenv') . ' run jedi-language-server' diff --git a/test/linter/test_pylsp.vader b/test/linter/test_pylsp.vader index 34cc30c6..d1490978 100644 --- a/test/linter/test_pylsp.vader +++ b/test/linter/test_pylsp.vader @@ -1,10 +1,13 @@ Before: call ale#assert#SetUpLinterTest('python', 'pylsp') + Save b:ale_python_auto_virtualenv let b:bin_dir = has('win32') ? 'Scripts' : 'bin' After: unlet! b:bin_dir + unlet! b:venv_bin + unlet! b:sep unlet! b:executable call ale#assert#TearDownLinterTest() @@ -40,6 +43,17 @@ Execute(The pylsp executable should be run from the virtualenv path): AssertEqual ale#Escape(b:executable), \ ale_linters#python#pylsp#GetCommand(bufnr('')) +Execute(virtualenv vars should be used when ale_python_auto_virtualenv = 1): + let b:ale_python_auto_virtualenv = 1 + call ale#test#SetFilename('../test-files/python/with_virtualenv/subdir/foo/bar.py') + + let b:venv_bin = ale#path#Simplify(g:dir . '/../test-files/python/with_virtualenv/env/' . b:bin_dir) + let b:sep = has('win32') ? ';' : ':' + let b:executable = ale#path#Simplify(b:venv_bin . '/pylsp') + + AssertLinter b:executable, ale#Env('PATH', b:venv_bin . b:sep . $PATH) + \ . ale#Escape(b:executable) + Execute(You should be able to override the pylsp virtualenv lookup): call ale#test#SetFilename('../test-files/python/with_virtualenv/subdir/foo/bar.py') diff --git a/test/linter/test_pyright.vader b/test/linter/test_pyright.vader index 303efa24..c81ab4b4 100644 --- a/test/linter/test_pyright.vader +++ b/test/linter/test_pyright.vader @@ -1,10 +1,13 @@ Before: call ale#assert#SetUpLinterTest('python', 'pyright') + Save b:ale_python_auto_virtualenv let b:bin_dir = has('win32') ? 'Scripts' : 'bin' After: unlet! b:bin_dir + unlet! b:venv_bin + unlet! b:sep unlet! b:executable call ale#assert#TearDownLinterTest() @@ -132,6 +135,17 @@ Execute(The pyright callbacks should detect virtualenv directories): AssertLinter b:executable, ale#Escape(b:executable) . ' --stdio' +Execute(virtualenv vars should be used when ale_python_auto_virtualenv = 1): + let b:ale_python_auto_virtualenv = 1 + call ale#test#SetFilename('../test-files/python/with_virtualenv/subdir/foo/bar.py') + + let b:venv_bin = ale#path#Simplify(g:dir . '/../test-files/python/with_virtualenv/env/' . b:bin_dir) + let b:sep = has('win32') ? ';' : ':' + let b:executable = ale#path#Simplify(b:venv_bin . '/pyright-langserver') + + AssertLinter b:executable, ale#Env('PATH', b:venv_bin . b:sep . $PATH) + \ . ale#Escape(b:executable) . ' --stdio' + Execute(Setting executable to 'pipenv' should append 'run pyright'): call ale#test#SetFilename('../test-files') diff --git a/test/test-files/python/with_virtualenv/env/bin/jedi-language-server b/test/test-files/python/with_virtualenv/env/bin/jedi-language-server new file mode 100755 index 00000000..e69de29b -- cgit v1.2.3