From fdd37acc1f381230fc2ed87303e98c7a4daafaad Mon Sep 17 00:00:00 2001 From: pmacosta Date: Thu, 6 Dec 2018 13:27:03 -0500 Subject: [PATCH] Add support for pydocstyle linter (#2085) The linter can correctly parse pydocstyle output with any of the following command-line options enabled: --explain, --source, --debug, and/or --verbose --- README.md | 2 +- ale_linters/python/pydocstyle.vim | 74 +++++++++++ doc/ale-python.txt | 40 ++++++ doc/ale.txt | 3 +- .../test_pydocstyle_command_callback.vader | 39 ++++++ test/handler/test_pydocstyle_handler.vader | 116 ++++++++++++++++++ 6 files changed, 272 insertions(+), 2 deletions(-) create mode 100644 ale_linters/python/pydocstyle.vim create mode 100644 test/command_callback/test_pydocstyle_command_callback.vader create mode 100644 test/handler/test_pydocstyle_handler.vader diff --git a/README.md b/README.md index defd3df9..6fbd3aa3 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ formatting. | proto | [protoc-gen-lint](https://github.com/ckaznocha/protoc-gen-lint) | | Pug | [pug-lint](https://github.com/pugjs/pug-lint) | | Puppet | [languageserver](https://github.com/lingua-pupuli/puppet-editor-services), [puppet](https://puppet.com), [puppet-lint](https://puppet-lint.com) | -| Python | [autopep8](https://github.com/hhatto/autopep8), [black](https://github.com/ambv/black), [flake8](http://flake8.pycqa.org/en/latest/), [isort](https://github.com/timothycrosley/isort), [mypy](http://mypy-lang.org/), [prospector](https://github.com/PyCQA/prospector), [pycodestyle](https://github.com/PyCQA/pycodestyle), [pyls](https://github.com/palantir/python-language-server), [pyre](https://github.com/facebook/pyre-check), [pylint](https://www.pylint.org/) !!, [vulture](https://github.com/jendrikseipp/vulture) !!, [yapf](https://github.com/google/yapf) | +| Python | [autopep8](https://github.com/hhatto/autopep8), [black](https://github.com/ambv/black), [flake8](http://flake8.pycqa.org/en/latest/), [isort](https://github.com/timothycrosley/isort), [mypy](http://mypy-lang.org/), [prospector](https://github.com/PyCQA/prospector), [pycodestyle](https://github.com/PyCQA/pycodestyle), [pydocstyle](https://www.pydocstyle.org/), [pyls](https://github.com/palantir/python-language-server), [pyre](https://github.com/facebook/pyre-check), [pylint](https://www.pylint.org/) !!, [vulture](https://github.com/jendrikseipp/vulture) !!, [yapf](https://github.com/google/yapf) | | QML | [qmlfmt](https://github.com/jesperhh/qmlfmt), [qmllint](https://github.com/qt/qtdeclarative/tree/5.11/tools/qmllint) | | R | [lintr](https://github.com/jimhester/lintr) | | ReasonML | [merlin](https://github.com/the-lambda-church/merlin) see `:help ale-reasonml-ols` for configuration instructions, [ols](https://github.com/freebroccolo/ocaml-language-server), [refmt](https://github.com/reasonml/reason-cli) | diff --git a/ale_linters/python/pydocstyle.vim b/ale_linters/python/pydocstyle.vim new file mode 100644 index 00000000..ebf92bf1 --- /dev/null +++ b/ale_linters/python/pydocstyle.vim @@ -0,0 +1,74 @@ +" Author: Pablo Acosta +" Description: pydocstyle for python files + +call ale#Set('python_pydocstyle_executable', 'pydocstyle') +call ale#Set('python_pydocstyle_options', '') +call ale#Set('python_pydocstyle_use_global', get(g:, 'ale_use_global_executables', 0)) +call ale#Set('python_pydocstyle_auto_pipenv', 0) + +function! ale_linters#python#pydocstyle#GetExecutable(buffer) abort + if (ale#Var(a:buffer, 'python_auto_pipenv') || ale#Var(a:buffer, 'python_pydocstyle_auto_pipenv')) + \ && ale#python#PipenvPresent(a:buffer) + return 'pipenv' + endif + + return ale#python#FindExecutable(a:buffer, 'python_pydocstyle', ['pydocstyle']) +endfunction + +function! ale_linters#python#pydocstyle#GetCommand(buffer) abort + let l:dir = fnamemodify(bufname(a:buffer), ':p:h') + let l:executable = ale_linters#python#pydocstyle#GetExecutable(a:buffer) + + let l:exec_args = l:executable =~? 'pipenv$' + \ ? ' run pydocstyle' + \ : '' + + return ale#path#CdString(l:dir) + \ . ale#Escape(l:executable) . l:exec_args + \ . ' ' . ale#Var(a:buffer, 'python_pydocstyle_options') + \ . ' ' . ale#Escape(fnamemodify(bufname(a:buffer), ':p:t')) +endfunction + +function! ale_linters#python#pydocstyle#Handle(buffer, lines) abort + " Matches patterns like the following: + " mydir/myfile.py:33 in public function `myfunction`: + " DXXX: Error description + let l:fname = ale#Escape(fnamemodify(bufname(a:buffer), ':p:t')) + let l:line1_pattern = '\v^' . l:fname . ':\s*(\d+)\s+.*$' + let l:line2_pattern = '\v^.*([a-zA-Z]\d+):\s*(.*)$' + let l:output = [] + + let l:num_lines = len(a:lines) + let l:index = 0 + + while l:index < l:num_lines + let l:lnum = matchlist(a:lines[l:index], l:line1_pattern) + + if !empty(l:lnum) && (l:index + 1 < l:num_lines) + let l:desc = matchlist(a:lines[l:index + 1], l:line2_pattern) + + if !empty(l:desc) + call add(l:output, { + \ 'lnum': l:lnum[1] + 0, + \ 'col': 1, + \ 'type': 'W', + \ 'text': l:desc[2], + \ 'code': l:desc[1], + \}) + endif + + let l:index = l:index + 2 + else + let l:index = l:index + 1 + endif + endwhile + + return l:output +endfunction + +call ale#linter#Define('python', { +\ 'name': 'pydocstyle', +\ 'executable_callback': 'ale_linters#python#pydocstyle#GetExecutable', +\ 'command_callback': 'ale_linters#python#pydocstyle#GetCommand', +\ 'callback': 'ale_linters#python#pydocstyle#Handle', +\}) diff --git a/doc/ale-python.txt b/doc/ale-python.txt index 0b8e1746..f3f2801a 100644 --- a/doc/ale-python.txt +++ b/doc/ale-python.txt @@ -328,6 +328,46 @@ g:ale_python_pycodestyle_auto_pipenv *g:ale_python_pycodestyle_auto_pipenv* if true. This is overridden by a manually-set executable. +=============================================================================== +pydocstyle *ale-python-pydocstyle* + + +g:ale_python_pydocstyle_executable *g:ale_python_pydocstyle_executable* + *b:ale_python_pydocstyle_executable* + Type: |String| + Default: `'pydocstyle'` + + See |ale-integrations-local-executables| + + Set this to `'pipenv'` to invoke `'pipenv` `run` `pydocstyle'`. + + +g:ale_python_pydocstyle_options *g:ale_python_pydocstyle_options* + *b:ale_python_pydocstyle_options* + Type: |String| + Default: `''` + + This variable can be changed to add command-line arguments to the + pydocstyle invocation. + + +g:ale_python_pydocstyle_use_global *g:ale_python_pydocstyle_use_global* + *b:ale_python_pydocstyle_use_global* + Type: |Number| + Default: `get(g:, 'ale_use_global_executables', 0)` + + See |ale-integrations-local-executables| + + +g:ale_python_pydocstyle_auto_pipenv *g:ale_python_pydocstyle_auto_pipenv* + *b:ale_python_pydocstyle_auto_pipenv* + Type: |Number| + Default: `0` + + Detect whether the file is inside a pipenv, and set the executable to `pipenv` + if true. This is overridden by a manually-set executable. + + =============================================================================== pyflakes *ale-python-pyflakes* diff --git a/doc/ale.txt b/doc/ale.txt index 92a100f9..370e0455 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -258,6 +258,7 @@ CONTENTS *ale-contents* mypy................................|ale-python-mypy| prospector..........................|ale-python-prospector| pycodestyle.........................|ale-python-pycodestyle| + pydocstyle..........................|ale-python-pydocstyle| pyflakes............................|ale-python-pyflakes| pylint..............................|ale-python-pylint| pyls................................|ale-python-pyls| @@ -475,7 +476,7 @@ Notes: * proto: `protoc-gen-lint` * Pug: `pug-lint` * Puppet: `languageserver`, `puppet`, `puppet-lint` -* Python: `autopep8`, `black`, `flake8`, `isort`, `mypy`, `prospector`, `pycodestyle`, `pyls`, `pyre`, `pylint`!!, `vulture`!!, `yapf` +* Python: `autopep8`, `black`, `flake8`, `isort`, `mypy`, `prospector`, `pycodestyle`, `pydocstyle`, `pyls`, `pyre`, `pylint`!!, `vulture`!!, `yapf` * QML: `qmlfmt`, `qmllint` * R: `lintr` * ReasonML: `merlin`, `ols`, `refmt` diff --git a/test/command_callback/test_pydocstyle_command_callback.vader b/test/command_callback/test_pydocstyle_command_callback.vader new file mode 100644 index 00000000..7e0df9ca --- /dev/null +++ b/test/command_callback/test_pydocstyle_command_callback.vader @@ -0,0 +1,39 @@ +Before: + call ale#assert#SetUpLinterTest('python', 'pydocstyle') + +After: + call ale#assert#TearDownLinterTest() + +Execute(The pydocstyle command callback should return default string): + AssertLinter 'pydocstyle', + \ ale#path#BufferCdString(bufnr('')) + \ . ale#Escape('pydocstyle') . ' ' . ale#Escape('dummy.txt') + +Execute(The pydocstyle command callback should allow options): + let g:ale_python_pydocstyle_options = '--verbose' + + AssertLinter 'pydocstyle', + \ ale#path#BufferCdString(bufnr('')) + \ . ale#Escape('pydocstyle') . ' --verbose ' . ale#Escape('dummy.txt') + +Execute(The pydocstyle executable should be configurable): + let g:ale_python_pydocstyle_executable = '~/.local/bin/pydocstyle' + + AssertLinter '~/.local/bin/pydocstyle', + \ ale#path#BufferCdString(bufnr('')) + \ . ale#Escape('~/.local/bin/pydocstyle') . ' ' . ale#Escape('dummy.txt') + +Execute(Setting executable to 'pipenv' appends 'run pydocstyle'): + let g:ale_python_pydocstyle_executable = 'path/to/pipenv' + + AssertLinter 'path/to/pipenv', + \ ale#path#BufferCdString(bufnr('')) + \ . ale#Escape('path/to/pipenv') . ' run pydocstyle ' . ale#Escape('dummy.txt') + +Execute(Pipenv is detected when python_pydocstyle_auto_pipenv is set): + let g:ale_python_pydocstyle_auto_pipenv = 1 + call ale#test#SetFilename('/testplugin/test/python_fixtures/pipenv/whatever.py') + + AssertLinter 'pipenv', + \ ale#path#BufferCdString(bufnr('')) + \ . ale#Escape('pipenv') . ' run pydocstyle ' . ale#Escape('whatever.py') diff --git a/test/handler/test_pydocstyle_handler.vader b/test/handler/test_pydocstyle_handler.vader new file mode 100644 index 00000000..d155dc9a --- /dev/null +++ b/test/handler/test_pydocstyle_handler.vader @@ -0,0 +1,116 @@ +Before: + Save g:ale_warn_about_trailing_whitespace + + let g:ale_warn_about_trailing_whitespace = 1 + + runtime ale_linters/python/pydocstyle.vim + +After: + Restore + + call ale#linter#Reset() + + silent file something_else.py + +" File sample.py +" # sample.py file +" +" def main(): +" """ +" This is a multi-line description that should produce multiple errors to be +" tested by the handler +" """ +" return Fales +" +" +" if __name__ == '__main__': +" main() +" +" The command to generate the handler input is: +" +" $ python -m pydocstyle --verbose --source --explain sample.py +" [...] +" $ + +Execute(Basic pydocstyle warnings should be handled): + AssertEqual + \ [ + \ { + \ 'lnum': 1, + \ 'col': 1, + \ 'text': 'Missing docstring in public module', + \ 'code': 'D100', + \ 'type': 'W', + \ }, + \ { + \ 'lnum': 4, + \ 'col': 1, + \ 'text': '1 blank line required between summary line and description (found 0)', + \ 'code': 'D205', + \ 'type': 'W', + \ }, + \ { + \ 'lnum': 4, + \ 'col': 1, + \ 'text': 'First line should end with a period (not ''e'')', + \ 'code': 'D400', + \ 'type': 'W', + \ }, + \ { + \ 'lnum': 4, + \ 'col': 1, + \ 'text': 'First line should be in imperative mood; try rephrasing (found ''This'')', + \ 'code': 'D401', + \ 'type': 'W', + \ }, + \ ], + \ ale_linters#python#pydocstyle#Handle(bufnr(''), [ + \ 'Checking file ' . fnamemodify(bufname(bufnr('')), ':p') . '.', + \ ale#Escape(fnamemodify(bufname(bufnr('')), ':t')) . ':1 at module level:', + \ ' D100: Missing docstring in public module', + \ '', + \ ' All modules should normally have docstrings. [...] all functions and', + \ ' classes exported by a module should also have docstrings. Public', + \ ' methods (including the __init__ constructor) should also have', + \ ' docstrings.', + \ ' Note: Public (exported) definitions are either those with names listed', + \ ' in __all__ variable (if present), or those that do not start', + \ ' with a single underscore.', + \ '', + \ ' 1: # 2: 3: s 4: a 5: m 6: p 7: l ...', + \ '', + \ '', + \ ale#Escape(fnamemodify(bufname(bufnr('')), ':t')) . ':4 in public function `main`:', + \ ' D205: 1 blank line required between summary line and description (found 0)', + \ '', + \ ' Multi-line docstrings consist of a summary line just like a one-line', + \ ' docstring, followed by a blank line, followed by a more elaborate', + \ ' description. The summary line may be used by automatic indexing tools;', + \ ' it is important that it fits on one line and is separated from the', + \ ' rest of the docstring by a blank line.', + \ '', + \ ' 3: d 4: e 5: f 6: 7: m 8: a 9: i ...', + \ '', + \ '', + \ ale#Escape(fnamemodify(bufname(bufnr('')), ':t')) . ':4 in public function `main`:', + \ ' D400: First line should end with a period (not ''e'')', + \ '', + \ ' The [first line of a] docstring is a phrase ending in a period.', + \ '', + \ ' 3: d 4: e 5: f 6: 7: m 8: a 9: i ...', + \ '', + \ '', + \ ale#Escape(fnamemodify(bufname(bufnr('')), ':t')) . ':4 in public function `main`:', + \ ' D401: First line should be in imperative mood; try rephrasing (found ''This'')', + \ '', + \ ' [Docstring] prescribes the function or method''s effect as a command:', + \ ' ("Do this", "Return that"), not as a description; e.g. don''t write', + \ ' "Returns the pathname ...".', + \ '', + \ ' 3: d 4: e 5: f 6: 7: m 8: a 9: i ...', + \ ]) + +Execute(Handler should handle empty output): + AssertEqual + \ [], + \ ale_linters#python#pydocstyle#Handle(bufnr(''), [])