Support pylama for python (#2266)

* Add pylama for python
* Consolidate python traceback handling
This commit is contained in:
Kevin Locke 2019-02-08 21:44:34 +00:00 committed by w0rp
parent 422908a572
commit a24f0b4d5f
13 changed files with 577 additions and 22 deletions

View File

@ -74,15 +74,11 @@ let s:end_col_pattern_map = {
\}
function! ale_linters#python#flake8#Handle(buffer, lines) abort
for l:line in a:lines[:10]
if match(l:line, '^Traceback') >= 0
return [{
\ 'lnum': 1,
\ 'text': 'An exception was thrown. See :ALEDetail',
\ 'detail': join(a:lines, "\n"),
\}]
endif
endfor
let l:output = ale#python#HandleTraceback(a:lines, 10)
if !empty(l:output)
return l:output
endif
" Matches patterns line the following:
"
@ -90,7 +86,6 @@ function! ale_linters#python#flake8#Handle(buffer, lines) abort
"
" stdin:6:6: E111 indentation is not a multiple of four
let l:pattern = '\v^[a-zA-Z]?:?[^:]+:(\d+):?(\d+)?: ([[:alnum:]]+):? (.*)$'
let l:output = []
for l:match in ale#util#GetMatches(a:lines, l:pattern)
let l:code = l:match[3]

View File

@ -0,0 +1,94 @@
" Author: Kevin Locke <kevin@kevinlocke.name>
" Description: pylama for python files
call ale#Set('python_pylama_executable', 'pylama')
call ale#Set('python_pylama_options', '')
call ale#Set('python_pylama_use_global', get(g:, 'ale_use_global_executables', 0))
call ale#Set('python_pylama_auto_pipenv', 0)
call ale#Set('python_pylama_change_directory', 1)
function! ale_linters#python#pylama#GetExecutable(buffer) abort
if (ale#Var(a:buffer, 'python_auto_pipenv') || ale#Var(a:buffer, 'python_pylama_auto_pipenv'))
\ && ale#python#PipenvPresent(a:buffer)
return 'pipenv'
endif
return ale#python#FindExecutable(a:buffer, 'python_pylama', ['pylama'])
endfunction
function! ale_linters#python#pylama#GetCommand(buffer) abort
let l:cd_string = ''
if ale#Var(a:buffer, 'python_pylama_change_directory')
" Pylama loads its configuration from the current directory only, and
" applies file masks using paths relative to the current directory.
" Run from project root, if found, otherwise buffer dir.
let l:project_root = ale#python#FindProjectRoot(a:buffer)
let l:cd_string = l:project_root isnot# ''
\ ? ale#path#CdString(l:project_root)
\ : ale#path#BufferCdString(a:buffer)
endif
let l:executable = ale_linters#python#pylama#GetExecutable(a:buffer)
let l:exec_args = l:executable =~? 'pipenv$'
\ ? ' run pylama'
\ : ''
return l:cd_string
\ . ale#Escape(l:executable) . l:exec_args
\ . ale#Pad(ale#Var(a:buffer, 'python_pylama_options'))
\ . ' %t'
endfunction
function! ale_linters#python#pylama#Handle(buffer, lines) abort
if empty(a:lines)
return []
endif
let l:output = ale#python#HandleTraceback(a:lines, 1)
let l:pattern = '\v^.{-}:([0-9]+):([0-9]+): +%(([A-Z][0-9]+):? +)?(.*)$'
" First letter of error code is a pylint-compatible message type
" http://pylint.pycqa.org/en/latest/user_guide/output.html#source-code-analysis-section
" D is for Documentation (pydocstyle)
let l:pylint_type_to_ale_type = {
\ 'I': 'I',
\ 'R': 'W',
\ 'C': 'W',
\ 'W': 'W',
\ 'E': 'E',
\ 'F': 'E',
\ 'D': 'W',
\}
let l:pylint_type_to_ale_sub_type = {
\ 'R': 'style',
\ 'C': 'style',
\ 'D': 'style',
\}
for l:match in ale#util#GetMatches(a:lines, l:pattern)
" Ignore C0103 for module name from temporary path (%t) which may not
" comply with module-rgx.
if l:match[3] is# 'C0103' && l:match[4] =~# '^Module name '
continue
endif
call add(l:output, {
\ 'lnum': str2nr(l:match[1]),
\ 'col': str2nr(l:match[2]),
\ 'code': l:match[3],
\ 'type': get(l:pylint_type_to_ale_type, l:match[3][0], 'W'),
\ 'sub_type': get(l:pylint_type_to_ale_sub_type, l:match[3][0], ''),
\ 'text': l:match[4],
\})
endfor
return l:output
endfunction
call ale#linter#Define('python', {
\ 'name': 'pylama',
\ 'executable_callback': 'ale_linters#python#pylama#GetExecutable',
\ 'command_callback': 'ale_linters#python#pylama#GetCommand',
\ 'callback': 'ale_linters#python#pylama#Handle',
\})

View File

@ -46,19 +46,14 @@ endfunction
function! ale_linters#python#vulture#Handle(buffer, lines) abort
for l:line in a:lines[:10]
if match(l:line, '^Traceback') >= 0
return [{
\ 'lnum': 1,
\ 'text': 'An exception was thrown. See :ALEDetail',
\ 'detail': join(a:lines, "\n"),
\}]
endif
endfor
let l:output = ale#python#HandleTraceback(a:lines, 10)
if !empty(l:output)
return l:output
endif
" Matches patterns line the following:
let l:pattern = '\v^([a-zA-Z]?:?[^:]+):(\d+): (.*)$'
let l:output = []
let l:dir = s:GetDir(a:buffer)
for l:match in ale#util#GetMatches(a:lines, l:pattern)

View File

@ -27,6 +27,7 @@ function! ale#python#FindProjectRootIni(buffer) abort
\|| filereadable(l:path . '/pycodestyle.cfg')
\|| filereadable(l:path . '/flake8.cfg')
\|| filereadable(l:path . '/.flake8rc')
\|| filereadable(l:path . '/pylama.ini')
\|| filereadable(l:path . '/Pipfile')
\|| filereadable(l:path . '/Pipfile.lock')
return l:path
@ -110,6 +111,44 @@ function! ale#python#FindExecutable(buffer, base_var_name, path_list) abort
return ale#Var(a:buffer, a:base_var_name . '_executable')
endfunction
" Handle traceback.print_exception() output starting in the first a:limit lines.
function! ale#python#HandleTraceback(lines, limit) abort
let l:nlines = len(a:lines)
let l:limit = a:limit > l:nlines ? l:nlines : a:limit
let l:start = 0
while l:start < l:limit
if a:lines[l:start] is# 'Traceback (most recent call last):'
break
endif
let l:start += 1
endwhile
if l:start >= l:limit
return []
endif
let l:end = l:start + 1
" Traceback entries are always prefixed with 2 spaces.
" SyntaxError marker (if present) is prefixed with at least 4 spaces.
" Final exc line starts with exception class name (never a space).
while l:end < l:nlines && a:lines[l:end][0] is# ' '
let l:end += 1
endwhile
let l:exc_line = l:end < l:nlines
\ ? a:lines[l:end]
\ : 'An exception was thrown.'
return [{
\ 'lnum': 1,
\ 'text': l:exc_line . ' (See :ALEDetail)',
\ 'detail': join(a:lines[(l:start):(l:end)], "\n"),
\}]
endfunction
" Detects whether a pipenv environment is present.
function! ale#python#PipenvPresent(buffer) abort
return findfile('Pipfile.lock', expand('#' . a:buffer . ':p:h') . ';') isnot# ''

View File

@ -31,6 +31,7 @@ ALE will look for configuration files with the following filenames. >
pycodestyle.cfg
flake8.cfg
.flake8rc
pylama.ini
Pipfile
Pipfile.lock
<
@ -449,6 +450,60 @@ g:ale_python_pyflakes_auto_pipenv *g:ale_python_pyflakes_auto_pipenv*
if true. This is overridden by a manually-set executable.
===============================================================================
pylama *ale-python-pylama*
g:ale_python_pylama_change_directory *g:ale_python_pylama_change_directory*
*b:ale_python_pylama_change_directory*
Type: |Number|
Default: `1`
If set to `1`, `pylama` will be run from a detected project root, per
|ale-python-root|. This is useful because `pylama` only searches for
configuration files in its current directory and applies file masks using
paths relative to its current directory. This option can be turned off if
you want to control the directory in which `pylama` is executed.
g:ale_python_pylama_executable *g:ale_python_pylama_executable*
*b:ale_python_pylama_executable*
Type: |String|
Default: `'pylama'`
This variable can be changed to modify the executable used for pylama. Set
this to `'pipenv'` to invoke `'pipenv` `run` `pylama'`.
g:ale_python_pylama_options *g:ale_python_pylama_options*
*b:ale_python_pylama_options*
Type: |String|
Default: `''`
This variable can be changed to add command-line arguments to the pylama
invocation.
g:ale_python_pylama_use_global *g:ale_python_pylama_use_global*
*b:ale_python_pylama_use_global*
Type: |Number|
Default: `get(g:, 'ale_use_global_executables', 0)`
This variable controls whether or not ALE will search for pylama in a
virtualenv directory first. If this variable is set to `1`, then ALE will
always use |g:ale_python_pylama_executable| for the executable path.
Both variables can be set with `b:` buffer variables instead.
g:ale_python_pylama_auto_pipenv *g:ale_python_pylama_auto_pipenv*
*b:ale_python_pylama_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.
===============================================================================
pylint *ale-python-pylint*

View File

@ -269,6 +269,7 @@ CONTENTS *ale-contents*
pycodestyle.........................|ale-python-pycodestyle|
pydocstyle..........................|ale-python-pydocstyle|
pyflakes............................|ale-python-pyflakes|
pylama..............................|ale-python-pylama|
pylint..............................|ale-python-pylint|
pyls................................|ale-python-pyls|
pyre................................|ale-python-pyre|

View File

@ -0,0 +1,85 @@
Before:
call ale#assert#SetUpLinterTest('python', 'pylama')
call ale#test#SetFilename('test.py')
let b:bin_dir = has('win32') ? 'Scripts' : 'bin'
let b:command_tail = ' %t'
After:
unlet! b:bin_dir
unlet! b:executable
unlet! b:command_tail
call ale#assert#TearDownLinterTest()
Execute(The pylama command callback should return a default):
AssertLinter 'pylama',
\ ale#path#BufferCdString(bufnr(''))
\ . ale#Escape('pylama') . b:command_tail
Execute(The option for disabling changing directories should work):
let g:ale_python_pylama_change_directory = 0
AssertLinter 'pylama', ale#Escape('pylama') . b:command_tail
Execute(The pylama executable should be configurable, and escaped properly):
let g:ale_python_pylama_executable = 'executable with spaces'
AssertLinter 'executable with spaces',
\ ale#path#BufferCdString(bufnr(''))
\ . ale#Escape('executable with spaces') . b:command_tail
Execute(The pylama command callback should let you set options):
let g:ale_python_pylama_options = '--some-option'
AssertLinter 'pylama',
\ ale#path#BufferCdString(bufnr(''))
\ . ale#Escape('pylama') . ' --some-option' . b:command_tail
Execute(The pylama command callback should switch directories to the detected project root):
silent execute 'file ' . fnameescape(g:dir . '/python_paths/no_virtualenv/subdir/foo/bar.py')
AssertLinter 'pylama',
\ ale#path#CdString(ale#path#Simplify(g:dir . '/python_paths/no_virtualenv/subdir'))
\ . ale#Escape('pylama') . b:command_tail
Execute(The pylama command callback shouldn't detect virtualenv directories where they don't exist):
silent execute 'file ' . fnameescape(g:dir . '/python_paths/no_virtualenv/subdir/foo/bar.py')
AssertLinter 'pylama',
\ ale#path#CdString(ale#path#Simplify(g:dir . '/python_paths/no_virtualenv/subdir'))
\ . ale#Escape('pylama') . b:command_tail
Execute(The pylama command callback should detect virtualenv directories and switch to the project root):
silent execute 'file ' . fnameescape(g:dir . '/python_paths/with_virtualenv/subdir/foo/bar.py')
let b:executable = ale#path#Simplify(
\ g:dir . '/python_paths/with_virtualenv/env/' . b:bin_dir . '/pylama'
\)
AssertLinter b:executable,
\ ale#path#CdString(ale#path#Simplify(g:dir . '/python_paths/with_virtualenv/subdir'))
\ . ale#Escape(b:executable) . b:command_tail
Execute(You should able able to use the global pylama instead):
silent execute 'file ' . fnameescape(g:dir . '/python_paths/with_virtualenv/subdir/foo/bar.py')
let g:ale_python_pylama_use_global = 1
AssertLinter 'pylama',
\ ale#path#CdString(ale#path#Simplify(g:dir . '/python_paths/with_virtualenv/subdir'))
\ . ale#Escape('pylama') . b:command_tail
Execute(Setting executable to 'pipenv' appends 'run pylama'):
let g:ale_python_pylama_executable = 'path/to/pipenv'
AssertLinter 'path/to/pipenv',
\ ale#path#BufferCdString(bufnr(''))
\ . ale#Escape('path/to/pipenv') . ' run pylama' . b:command_tail
Execute(Pipenv is detected when python_pylama_auto_pipenv is set):
let g:ale_python_pylama_auto_pipenv = 1
call ale#test#SetFilename('/testplugin/test/python_fixtures/pipenv/whatever.py')
AssertLinter 'pipenv',
\ ale#path#BufferCdString(bufnr(''))
\ . ale#Escape('pipenv') . ' run pylama' . b:command_tail

View File

@ -113,7 +113,7 @@ Execute(The flake8 handler should handle stack traces):
\ [
\ {
\ 'lnum': 1,
\ 'text': 'An exception was thrown. See :ALEDetail',
\ 'text': 'ImportError: No module named parser (See :ALEDetail)',
\ 'detail': join([
\ 'Traceback (most recent call last):',
\ ' File "/usr/local/bin/flake8", line 7, in <module>',

View File

@ -0,0 +1,212 @@
Before:
Save g:ale_warn_about_trailing_whitespace
let g:ale_warn_about_trailing_whitespace = 1
runtime ale_linters/python/pylama.vim
After:
Restore
call ale#linter#Reset()
silent file something_else.py
Execute(The pylama handler should handle no messages):
AssertEqual [], ale_linters#python#pylama#Handle(bufnr(''), [])
Execute(The pylama handler should handle basic warnings and syntax errors):
AssertEqual
\ [
\ {
\ 'lnum': 8,
\ 'col': 1,
\ 'code': 'W0611',
\ 'type': 'W',
\ 'sub_type': '',
\ 'text': '''foo'' imported but unused [pyflakes]',
\ },
\ {
\ 'lnum': 8,
\ 'col': 0,
\ 'code': 'E0401',
\ 'type': 'E',
\ 'sub_type': '',
\ 'text': 'Unable to import ''foo'' [pylint]',
\ },
\ {
\ 'lnum': 10,
\ 'col': 1,
\ 'code': 'E302',
\ 'type': 'E',
\ 'sub_type': '',
\ 'text': 'expected 2 blank lines, found 1 [pycodestyle]',
\ },
\ {
\ 'lnum': 11,
\ 'col': 1,
\ 'code': 'D401',
\ 'type': 'W',
\ 'sub_type': 'style',
\ 'text': 'First line should be in imperative mood (''Get'', not ''Gets'') [pydocstyle]',
\ },
\ {
\ 'lnum': 15,
\ 'col': 81,
\ 'code': 'E501',
\ 'type': 'E',
\ 'sub_type': '',
\ 'text': 'line too long (96 > 80 characters) [pycodestyle]',
\ },
\ {
\ 'lnum': 16,
\ 'col': 1,
\ 'code': 'D203',
\ 'type': 'W',
\ 'sub_type': 'style',
\ 'text': '1 blank line required before class docstring (found 0) [pydocstyle]',
\ },
\ {
\ 'lnum': 18,
\ 'col': 1,
\ 'code': 'D107',
\ 'type': 'W',
\ 'sub_type': 'style',
\ 'text': 'Missing docstring in __init__ [pydocstyle]',
\ },
\ {
\ 'lnum': 20,
\ 'col': 0,
\ 'code': 'C4001',
\ 'type': 'W',
\ 'sub_type': 'style',
\ 'text': 'Invalid string quote ", should be '' [pylint]',
\ },
\ ],
\ ale_linters#python#pylama#Handle(bufnr(''), [
\ 'No config file found, using default configuration',
\ 'index.py:8:1: W0611 ''foo'' imported but unused [pyflakes]',
\ 'index.py:8:0: E0401 Unable to import ''foo'' [pylint]',
\ 'index.py:10:1: E302 expected 2 blank lines, found 1 [pycodestyle]',
\ 'index.py:11:1: D401 First line should be in imperative mood (''Get'', not ''Gets'') [pydocstyle]',
\ 'index.py:15:81: E501 line too long (96 > 80 characters) [pycodestyle]',
\ 'index.py:16:1: D203 1 blank line required before class docstring (found 0) [pydocstyle]',
\ 'index.py:18:1: D107 Missing docstring in __init__ [pydocstyle]',
\ 'index.py:20:0: C4001 Invalid string quote ", should be '' [pylint]',
\ ])
Execute(The pylama handler should handle tracebacks with parsable messages):
AssertEqual
\ [
\ {
\ 'lnum': 1,
\ 'text': 'ParseError: Cannot parse file. (See :ALEDetail)',
\ 'detail': join([
\ 'Traceback (most recent call last):',
\ ' File "/usr/local/lib/python2.7/site-packages/pylama/core.py", line 66, in run',
\ ' path, code=code, ignore=ignore, select=select, params=lparams)',
\ ' File "/usr/local/lib/python2.7/site-packages/pylama/lint/pylama_pydocstyle.py", line 37, in run',
\ ' } for e in PyDocChecker().check_source(*check_source_args)]',
\ ' File "/usr/local/lib/python2.7/site-packages/pydocstyle/checker.py", line 64, in check_source',
\ ' module = parse(StringIO(source), filename)',
\ ' File "/usr/local/lib/python2.7/site-packages/pydocstyle/parser.py", line 340, in __call__',
\ ' return self.parse(*args, **kwargs)',
\ ' File "/usr/local/lib/python2.7/site-packages/pydocstyle/parser.py", line 328, in parse',
\ ' six.raise_from(ParseError(), error)',
\ ' File "/usr/local/lib/python2.7/site-packages/six.py", line 737, in raise_from',
\ ' raise value',
\ 'ParseError: Cannot parse file.',
\ ], "\n"),
\ },
\ {
\ 'lnum': 11,
\ 'col': 1,
\ 'code': 'E302',
\ 'type': 'E',
\ 'sub_type': '',
\ 'text': 'expected 2 blank lines, found 1 [pycodestyle]',
\ },
\ {
\ 'lnum': 16,
\ 'col': 81,
\ 'code': 'E501',
\ 'type': 'E',
\ 'sub_type': '',
\ 'text': 'line too long (96 > 80 characters) [pycodestyle]',
\ },
\ ],
\ ale_linters#python#pylama#Handle(bufnr(''), [
\ 'Traceback (most recent call last):',
\ ' File "/usr/local/lib/python2.7/site-packages/pylama/core.py", line 66, in run',
\ ' path, code=code, ignore=ignore, select=select, params=lparams)',
\ ' File "/usr/local/lib/python2.7/site-packages/pylama/lint/pylama_pydocstyle.py", line 37, in run',
\ ' } for e in PyDocChecker().check_source(*check_source_args)]',
\ ' File "/usr/local/lib/python2.7/site-packages/pydocstyle/checker.py", line 64, in check_source',
\ ' module = parse(StringIO(source), filename)',
\ ' File "/usr/local/lib/python2.7/site-packages/pydocstyle/parser.py", line 340, in __call__',
\ ' return self.parse(*args, **kwargs)',
\ ' File "/usr/local/lib/python2.7/site-packages/pydocstyle/parser.py", line 328, in parse',
\ ' six.raise_from(ParseError(), error)',
\ ' File "/usr/local/lib/python2.7/site-packages/six.py", line 737, in raise_from',
\ ' raise value',
\ 'ParseError: Cannot parse file.',
\ '',
\ 'index.py:11:1: E302 expected 2 blank lines, found 1 [pycodestyle]',
\ 'index.py:16:81: E501 line too long (96 > 80 characters) [pycodestyle]',
\ ])
" Note: This is probably a bug, since all pylama plugins produce codes, but
" should be handled for compatibility.
" Note: The pylama isort plugin is distributed in the isort package.
Execute(The pylama handler should handle messages without codes):
AssertEqual
\ [
\ {
\ 'lnum': 0,
\ 'col': 0,
\ 'code': '',
\ 'type': 'W',
\ 'sub_type': '',
\ 'text': 'Incorrectly sorted imports. [isort]'
\ },
\ ],
\ ale_linters#python#pylama#Handle(bufnr(''), [
\ 'index.py:0:0: Incorrectly sorted imports. [isort]',
\ ])
" Note: This is a pylama bug, but should be handled for compatibility.
" See https://github.com/klen/pylama/pull/146
Execute(The pylama handler should handle message codes followed by a colon):
AssertEqual
\ [
\ {
\ 'lnum': 31,
\ 'col': 1,
\ 'code': 'E800',
\ 'type': 'E',
\ 'sub_type': '',
\ 'text': 'Found commented out code: # needs_sphinx = ''1.0'' [eradicate]',
\ },
\ ],
\ ale_linters#python#pylama#Handle(bufnr(''), [
\ 'index.py:31:1: E800: Found commented out code: # needs_sphinx = ''1.0'' [eradicate]',
\ ])
" The directory created for %t may not comply with pylint module name config.
" This should not be reported to users.
Execute(The pylama handler should ignore C0103 from temp dir, not others):
AssertEqual
\ [
\ {
\ 'lnum': 29,
\ 'col': 0,
\ 'code': 'C0103',
\ 'type': 'W',
\ 'sub_type': 'style',
\ 'text': 'Constant name "badname" doesn''t conform to UPPER_CASE naming style [pylint]',
\ },
\ ],
\ ale_linters#python#pylama#Handle(bufnr(''), [
\ '../../../../tmp/vmynR33/456/__init__.py:1:0: C0103 Module name "456" doesn''t conform to snake_case naming style [pylint]',
\ '../../../../tmp/vmynR33/456/__init__.py:29:0: C0103 Constant name "badname" doesn''t conform to UPPER_CASE naming style [pylint]',
\ ])

View File

@ -70,7 +70,7 @@ Execute(Vulture exception should be handled):
\ [
\ {
\ 'lnum': 1,
\ 'text': 'An exception was thrown. See :ALEDetail',
\ 'text': 'BaddestException: Everything gone wrong (See :ALEDetail)',
\ 'detail': join([
\ 'Traceback (most recent call last):',
\ ' File "/usr/lib/python3.6/site-packages/vulture/__init__.py", line 13, in <module>',

View File

@ -0,0 +1,79 @@
Execute(ale#python#HandleTraceback returns empty List for empty lines):
AssertEqual
\ [],
\ ale#python#HandleTraceback([], 10)
Execute(ale#python#HandleTraceback returns traceback, when present):
AssertEqual
\ [{
\ 'lnum': 1,
\ 'text': 'Exception: Example error (See :ALEDetail)',
\ 'detail': join([
\ 'Traceback (most recent call last):',
\ ' File "./example.py", line 5, in <module>',
\ ' raise Exception(''Example message'')',
\ 'Exception: Example error',
\ ], "\n"),
\ }],
\ ale#python#HandleTraceback([
\ 'Traceback (most recent call last):',
\ ' File "./example.py", line 5, in <module>',
\ ' raise Exception(''Example message'')',
\ 'Exception: Example error',
\ ], 1)
" SyntaxError has extra output lines about the source
Execute(ale#python#HandleTraceback returns SyntaxError traceback):
AssertEqual
\ [{
\ 'lnum': 1,
\ 'text': 'SyntaxError: invalid syntax (See :ALEDetail)',
\ 'detail': join([
\ 'Traceback (most recent call last):',
\ ' File "<string>", line 1, in <module>',
\ ' File "example.py", line 5',
\ ' +',
\ ' ^',
\ 'SyntaxError: invalid syntax',
\ ], "\n"),
\ }],
\ ale#python#HandleTraceback([
\ 'Traceback (most recent call last):',
\ ' File "<string>", line 1, in <module>',
\ ' File "example.py", line 5',
\ ' +',
\ ' ^',
\ 'SyntaxError: invalid syntax',
\ ], 1)
Execute(ale#python#HandleTraceback ignores traceback after line limit):
AssertEqual
\ [],
\ ale#python#HandleTraceback([
\ '',
\ 'Traceback (most recent call last):',
\ ' File "./example.py", line 5, in <module>',
\ ' raise Exception(''Example message'')',
\ 'Exception: Example error',
\ ], 1)
Execute(ale#python#HandleTraceback doesn't include later lines in detail):
AssertEqual
\ [{
\ 'lnum': 1,
\ 'text': 'Exception: Example error (See :ALEDetail)',
\ 'detail': join([
\ 'Traceback (most recent call last):',
\ ' File "./example.py", line 5, in <module>',
\ ' raise Exception(''Example message'')',
\ 'Exception: Example error',
\ ], "\n"),
\ }],
\ ale#python#HandleTraceback([
\ 'Traceback (most recent call last):',
\ ' File "./example.py", line 5, in <module>',
\ ' raise Exception(''Example message'')',
\ 'Exception: Example error',
\ 'file:1:2: Style issue',
\ 'file:3:4: Non-style issue',
\ ], 1)