summaryrefslogtreecommitdiff
path: root/autoload/ale/python.vim
blob: 92e48da8f8e559ece6e6c6a4c63f58766016d3bb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
" Author: w0rp <dev@w0rp.com>
" Description: Functions for integrating with Python linters.

call ale#Set('python_auto_pipenv', '0')
call ale#Set('python_auto_poetry', '0')

let s:sep = has('win32') ? '\' : '/'
" bin is used for Unix virtualenv directories, and Scripts is for Windows.
let s:bin_dir = has('unix') ? 'bin' : 'Scripts'
let g:ale_virtualenv_dir_names = get(g:, 'ale_virtualenv_dir_names', [
\   '.env',
\   '.venv',
\   'env',
\   've-py3',
\   've',
\   'virtualenv',
\   'venv',
\])

function! ale#python#FindProjectRootIni(buffer) abort
    for l:path in ale#path#Upwards(expand('#' . a:buffer . ':p:h'))
        " If you change this, update ale-python-root documentation.
        if filereadable(l:path . '/MANIFEST.in')
        \|| filereadable(l:path . '/setup.cfg')
        \|| filereadable(l:path . '/pytest.ini')
        \|| filereadable(l:path . '/tox.ini')
        \|| filereadable(l:path . '/.pyre_configuration.local')
        \|| filereadable(l:path . '/mypy.ini')
        \|| filereadable(l:path . '/.mypy.ini')
        \|| filereadable(l:path . '/pycodestyle.cfg')
        \|| filereadable(l:path . '/.flake8')
        \|| filereadable(l:path . '/.flake8rc')
        \|| filereadable(l:path . '/pylama.ini')
        \|| filereadable(l:path . '/pylintrc')
        \|| filereadable(l:path . '/.pylintrc')
        \|| filereadable(l:path . '/pyrightconfig.json')
        \|| filereadable(l:path . '/pyrightconfig.toml')
        \|| filereadable(l:path . '/Pipfile')
        \|| filereadable(l:path . '/Pipfile.lock')
        \|| filereadable(l:path . '/poetry.lock')
        \|| filereadable(l:path . '/pyproject.toml')
        \|| filereadable(l:path . '/.tool-versions')
            return l:path
        endif
    endfor

    return ''
endfunction

" Given a buffer number, find the project root directory for Python.
" The root directory is defined as the first directory found while searching
" upwards through paths, including the current directory, until a path
" containing an init file (one from MANIFEST.in, setup.cfg, pytest.ini,
" tox.ini) is found. If it is not possible to find the project root directory
" via init file, then it will be defined as the first directory found
" searching upwards through paths, including the current directory, until no
" __init__.py files is found.
function! ale#python#FindProjectRoot(buffer) abort
    let l:ini_root = ale#python#FindProjectRootIni(a:buffer)

    if !empty(l:ini_root)
        return l:ini_root
    endif

    for l:path in ale#path#Upwards(expand('#' . a:buffer . ':p:h'))
        if !filereadable(l:path . '/__init__.py')
            return l:path
        endif
    endfor

    return ''
endfunction

" Given a buffer number, find a virtualenv path for Python.
function! ale#python#FindVirtualenv(buffer) abort
    for l:path in ale#path#Upwards(expand('#' . a:buffer . ':p:h'))
        " Skip empty path components returned in MSYS.
        if empty(l:path)
            continue
        endif

        for l:dirname in ale#Var(a:buffer, 'virtualenv_dir_names')
            let l:venv_dir = ale#path#Simplify(
            \   join([l:path, l:dirname], s:sep)
            \)
            let l:script_filename = ale#path#Simplify(
            \   join([l:venv_dir, s:bin_dir, 'activate'], s:sep)
            \)

            if filereadable(l:script_filename)
                return l:venv_dir
            endif
        endfor
    endfor

    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
function! ale#python#FindExecutable(buffer, base_var_name, path_list) abort
    if ale#Var(a:buffer, a:base_var_name . '_use_global')
        return ale#Var(a:buffer, a:base_var_name . '_executable')
    endif

    let l:virtualenv = ale#python#FindVirtualenv(a:buffer)

    if !empty(l:virtualenv)
        for l:path in a:path_list
            let l:ve_executable = ale#path#Simplify(
            \   join([l:virtualenv, s:bin_dir, l:path], s:sep)
            \)

            if executable(l:ve_executable)
                return l:ve_executable
            endif
        endfor
    endif

    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# ''
endfunction

" Detects whether a poetry environment is present.
function! ale#python#PoetryPresent(buffer) abort
    return findfile('poetry.lock', expand('#' . a:buffer . ':p:h') . ';') isnot# ''
endfunction