diff --git a/.gitignore b/.gitignore index 9b539d7f..ae9f65fb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ # Ignore all hidden files everywhere. # Use `git add -f` to add hidden files. .* +__pycache__ +*.pyc /doc/tags /init.vim /test/ale-info-test-file diff --git a/README.md b/README.md index bc0902f7..a35a62ab 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ features, including: * Diagnostics (via Language Server Protocol linters) * Go To Definition (`:ALEGoToDefinition`) -* Completion (`let g:ale_completion_enabled = 1` before ALE is loaded) +* Completion (Built in completion support, or with Deoplete) * Finding references (`:ALEFindReferences`) * Hover information (`:ALEHover`) * Symbol search (`:ALESymbolSearch`) @@ -159,6 +159,18 @@ ALE offers some support for completion via hijacking of omnicompletion while you type. All of ALE's completion information must come from Language Server Protocol linters, or from `tsserver` for TypeScript. +ALE integrates with [Deoplete](https://github.com/Shougo/deoplete.nvim) as a +completion source, named `'ale'`. You can configure Deoplete to only use ALE as +the source of completion information, or mix it with other sources. + +```vim +" Use ALE and also some plugin 'foobar' as completion sources for all code. +let g:deoplete#sources = {'_': ['ale', 'foobar']} +``` + +ALE also offers its own automatic completion support, which does not require any +other plugins, and can be enabled by changing a setting before ALE is loaded. + ```vim " Enable completion where available. " This setting must be set before ALE is loaded. diff --git a/autoload/ale/completion.vim b/autoload/ale/completion.vim index 682f4c43..1d42c489 100644 --- a/autoload/ale/completion.vim +++ b/autoload/ale/completion.vim @@ -159,18 +159,20 @@ function! ale#completion#Filter(buffer, filetype, suggestions, prefix) abort endfunction function! s:ReplaceCompletionOptions() abort - " Remember the old omnifunc value, if there is one. - " If we don't store an old one, we'll just never reset the option. - " This will stop some random exceptions from appearing. - if !exists('b:ale_old_omnifunc') && !empty(&l:omnifunc) - let b:ale_old_omnifunc = &l:omnifunc + let l:source = get(get(b:, 'ale_completion_info', {}), 'source', '') + + if l:source is# 'ale-automatic' || l:source is# 'ale-manual' + " Remember the old omnifunc value, if there is one. + " If we don't store an old one, we'll just never reset the option. + " This will stop some random exceptions from appearing. + if !exists('b:ale_old_omnifunc') && !empty(&l:omnifunc) + let b:ale_old_omnifunc = &l:omnifunc + endif + + let &l:omnifunc = 'ale#completion#OmniFunc' endif - let &l:omnifunc = 'ale#completion#OmniFunc' - - let l:info = get(b:, 'ale_completion_info', {}) - - if !get(l:info, 'manual') + if l:source is# 'ale-automatic' if !exists('b:ale_old_completeopt') let b:ale_old_completeopt = &l:completeopt endif @@ -199,31 +201,49 @@ function! ale#completion#RestoreCompletionOptions() abort endif endfunction +function! ale#completion#GetCompletionPosition() abort + if !exists('b:ale_completion_info') + return 0 + endif + + let l:line = b:ale_completion_info.line + let l:column = b:ale_completion_info.column + let l:regex = s:GetFiletypeValue(s:omni_start_map, &filetype) + let l:up_to_column = getline(l:line)[: l:column - 2] + let l:match = matchstr(l:up_to_column, l:regex) + + return l:column - len(l:match) - 1 +endfunction + +function! ale#completion#GetCompletionResult() abort + " Parse a new response if there is one. + if exists('b:ale_completion_response') + \&& exists('b:ale_completion_parser') + let l:response = b:ale_completion_response + let l:parser = b:ale_completion_parser + + unlet b:ale_completion_response + unlet b:ale_completion_parser + + let b:ale_completion_result = function(l:parser)(l:response) + endif + + if exists('b:ale_completion_result') + return b:ale_completion_result + endif + + return v:null +endfunction + function! ale#completion#OmniFunc(findstart, base) abort if a:findstart - let l:line = b:ale_completion_info.line - let l:column = b:ale_completion_info.column - let l:regex = s:GetFiletypeValue(s:omni_start_map, &filetype) - let l:up_to_column = getline(l:line)[: l:column - 2] - let l:match = matchstr(l:up_to_column, l:regex) - - return l:column - len(l:match) - 1 + return ale#completion#GetCompletionPosition() else - " Parse a new response if there is one. - if exists('b:ale_completion_response') - \&& exists('b:ale_completion_parser') - let l:response = b:ale_completion_response - let l:parser = b:ale_completion_parser - - unlet b:ale_completion_response - unlet b:ale_completion_parser - - let b:ale_completion_result = function(l:parser)(l:response) - endif + let l:result = ale#completion#GetCompletionResult() call s:ReplaceCompletionOptions() - return get(b:, 'ale_completion_result', []) + return l:result isnot v:null ? l:result : [] endif endfunction @@ -239,7 +259,14 @@ function! ale#completion#Show(response, completion_parser) abort " Replace completion options shortly before opening the menu. call s:ReplaceCompletionOptions() - call timer_start(0, {-> ale#util#FeedKeys("\(ale_show_completion_menu)")}) + let l:source = get(get(b:, 'ale_completion_info', {}), 'source', '') + + if l:source is# 'ale-automatic' || l:source is# 'ale-manual' + call timer_start( + \ 0, + \ {-> ale#util#FeedKeys("\(ale_show_completion_menu)")} + \) + endif endfunction function! s:CompletionStillValid(request_id) abort @@ -249,7 +276,10 @@ function! s:CompletionStillValid(request_id) abort \&& has_key(b:, 'ale_completion_info') \&& b:ale_completion_info.request_id == a:request_id \&& b:ale_completion_info.line == l:line - \&& b:ale_completion_info.column == l:column + \&& ( + \ b:ale_completion_info.column == l:column + \ || b:ale_completion_info.source is# 'deoplete' + \) endfunction function! ale#completion#ParseTSServerCompletions(response) abort @@ -519,12 +549,12 @@ endfunction " This function can be used to manually trigger autocomplete, even when " g:ale_completion_enabled is set to false -function! ale#completion#GetCompletions(manual) abort +function! ale#completion#GetCompletions(source) abort let [l:line, l:column] = getpos('.')[1:2] let l:prefix = ale#completion#GetPrefix(&filetype, l:line, l:column) - if !a:manual && empty(l:prefix) + if a:source is# 'ale-automatic' && empty(l:prefix) return endif @@ -537,8 +567,9 @@ function! ale#completion#GetCompletions(manual) abort \ 'prefix': l:prefix, \ 'conn_id': 0, \ 'request_id': 0, - \ 'manual': a:manual, + \ 'source': a:source, \} + unlet! b:ale_completion_result let l:buffer = bufnr('') let l:Callback = function('s:OnReady') @@ -562,7 +593,7 @@ function! s:TimerHandler(...) abort " When running the timer callback, we have to be sure that the cursor " hasn't moved from where it was when we requested completions by typing. if s:timer_pos == [l:line, l:column] && ale#util#Mode() is# 'i' - call ale#completion#GetCompletions(0) + call ale#completion#GetCompletions('ale-automatic') endif endfunction diff --git a/doc/ale.txt b/doc/ale.txt index 6e48de27..95128187 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -321,40 +321,44 @@ servers. LSP linters can be used in combination with any other linter, and will automatically connect to LSP servers when needed. ALE also supports `tsserver` for TypeScript, which uses a different but very similar protocol. -ALE supports the following LSP/tsserver features: - -1. Diagnostics/linting - Enabled via selecting linters as usual. -2. Completion -3. Go to definition - ------------------------------------------------------------------------------- 5.1 Completion *ale-completion* -ALE offers limited support for automatic completion of code while you type. +ALE offers support for automatic completion of code while you type. Completion is only supported while at least one LSP linter is enabled. ALE will only suggest symbols provided by the LSP servers. -Suggestions will be made while you type after completion is enabled. -Completion can be enabled by setting |g:ale_completion_enabled| to `1`. This -setting must be set to `1` before ALE is loaded. The delay for completion can -be configured with |g:ale_completion_delay|. ALE will only suggest so many -possible matches for completion. The maximum number of items can be controlled -with |g:ale_completion_max_suggestions|. + *ale-deoplete-integration* + +ALE integrates with Deoplete for offering automatic completion data. ALE's +completion source for Deoplete is named `'ale'`, and should enabled +automatically if Deoplete is enabled and configured correctly. Deoplete +integration should not be combined with ALE's own implementation. + +ALE also offers its own completion implementation, which does not require any +other plugins. Suggestions will be made while you type after completion is +enabled. Completion can be enabled by setting |g:ale_completion_enabled| to +`1`. This setting must be set to `1` before ALE is loaded. The delay for +completion can be configured with |g:ale_completion_delay|. + +ALE will only suggest so many possible matches for completion. The maximum +number of items can be controlled with |g:ale_completion_max_suggestions|. If you don't like some of the suggestions you see, you can filter them out with |g:ale_completion_excluded_words| or |b:ale_completion_excluded_words|. The |ALEComplete| command can be used to show completion suggestions manually, -even when |g:ale_completion_enabled| is set to `0`. +even when |g:ale_completion_enabled| is set to `0`. For manually requesting +completion information with Deoplete, consult Deoplete's documentation. *ale-completion-completeopt-bug* -Automatic completion replaces |completeopt| before opening the omnicomplete -menu with . In some versions of Vim, the value set for the option -will not be respected. If you experience issues with Vim automatically -inserting text while you type, set the following option in vimrc, and your -issues should go away. > +ALE Automatic completion implementation replaces |completeopt| before opening +the omnicomplete menu with . In some versions of Vim, the value set +for the option will not be respected. If you experience issues with Vim +automatically inserting text while you type, set the following option in +vimrc, and your issues should go away. > set completeopt=menu,menuone,preview,noselect,noinsert < diff --git a/plugin/ale.vim b/plugin/ale.vim index ad3d3e56..cf39d632 100644 --- a/plugin/ale.vim +++ b/plugin/ale.vim @@ -216,7 +216,7 @@ command! -bar ALEDocumentation :call ale#hover#ShowDocumentationAtCursor() " Search for appearances of a symbol, such as a type name or function name. command! -nargs=1 ALESymbolSearch :call ale#symbol#Search() -command! -bar ALEComplete :call ale#completion#GetCompletions(1) +command! -bar ALEComplete :call ale#completion#GetCompletions('ale-manual') " mappings for commands nnoremap (ale_previous) :ALEPrevious diff --git a/rplugin/python3/deoplete/sources/ale.py b/rplugin/python3/deoplete/sources/ale.py new file mode 100644 index 00000000..1addfae3 --- /dev/null +++ b/rplugin/python3/deoplete/sources/ale.py @@ -0,0 +1,50 @@ +""" +A Deoplete source for ALE completion via tsserver and LSP. +""" +__author__ = 'Joao Paulo, w0rp' + +try: + from deoplete.source.base import Base +except ImportError: + # Mock the Base class if deoplete isn't available, as mock isn't available + # in the Docker image. + class Base(object): + def __init__(self, vim): + pass + + +# Make sure this code is valid in Python 2, used for running unit tests. +class Source(Base): + + def __init__(self, vim): + super(Source, self).__init__(vim) + + self.name = 'ale' + self.mark = '[L]' + self.rank = 100 + self.is_bytepos = True + self.min_pattern_length = 1 + + # Returns an integer for the start position, as with omnifunc. + def get_completion_position(self): + return self.vim.call('ale#completion#GetCompletionPosition') + + def gather_candidates(self, context): + if context.get('is_refresh'): + context['is_async'] = False + + if context['is_async']: + # Result is the same as for omnifunc, or None. + result = self.vim.call('ale#completion#GetCompletionResult') + + if result is not None: + context['is_async'] = False + + return result + else: + context['is_async'] = True + + # Request some completion results. + self.vim.call('ale#completion#GetCompletions', 'deoplete') + + return [] diff --git a/test/completion/test_completion_events.vader b/test/completion/test_completion_events.vader index d04a8085..f79773cc 100644 --- a/test/completion/test_completion_events.vader +++ b/test/completion/test_completion_events.vader @@ -27,7 +27,7 @@ Before: let g:get_completions_called = 0 " We just want to check if the function is called. - function! ale#completion#GetCompletions(manual) + function! ale#completion#GetCompletions(source) let g:get_completions_called = 1 endfunction @@ -57,6 +57,7 @@ After: unlet! b:ale_completion_info unlet! b:ale_completion_response unlet! b:ale_completion_parser + unlet! b:ale_completion_result unlet! b:ale_complete_done_time delfunction CheckCompletionCalled @@ -86,7 +87,7 @@ Execute(ale#completion#GetCompletions should not be called when the cursor posit call setpos('.', [bufnr(''), 1, 2, 0]) " We just want to check if the function is called. - function! ale#completion#GetCompletions(manual) + function! ale#completion#GetCompletions(source) let g:get_completions_called = 1 endfunction @@ -105,7 +106,7 @@ Execute(ale#completion#GetCompletions should not be called if you switch to norm let g:fake_mode = 'n' " We just want to check if the function is called. - function! ale#completion#GetCompletions(manual) + function! ale#completion#GetCompletions(source) let g:get_completions_called = 1 endfunction @@ -124,6 +125,7 @@ Execute(Completion should not be done shortly after the CompleteDone function): Execute(ale#completion#Show() should remember the omnifunc setting and replace it): let &l:omnifunc = 'FooBar' + let b:ale_completion_info = {'source': 'ale-automatic'} call ale#completion#Show('Response', 'Parser') AssertEqual 'FooBar', b:ale_old_omnifunc @@ -136,6 +138,7 @@ Execute(ale#completion#Show() should remember the omnifunc setting and replace i Execute(ale#completion#Show() should remember the completeopt setting and replace it): let &l:completeopt = 'menu' + let b:ale_completion_info = {'source': 'ale-automatic'} call ale#completion#Show('Response', 'Parser') AssertEqual 'menu', b:ale_old_completeopt @@ -148,6 +151,7 @@ Execute(ale#completion#Show() should remember the completeopt setting and replac Execute(ale#completion#Show() should set the preview option if it's set): let &l:completeopt = 'menu,preview' + let b:ale_completion_info = {'source': 'ale-automatic'} call ale#completion#Show('Response', 'Parser') AssertEqual 'menu,preview', b:ale_old_completeopt @@ -158,7 +162,7 @@ Execute(ale#completion#Show() should set the preview option if it's set): AssertEqual [["\(ale_show_completion_menu)"]], g:feedkeys_calls Execute(ale#completion#Show() should not replace the completeopt setting for manual completion): - let b:ale_completion_info = {'manual': 1} + let b:ale_completion_info = {'source': 'ale-manual'} let &l:completeopt = 'menu,preview' @@ -173,6 +177,7 @@ Execute(ale#completion#Show() should not replace the completeopt setting for man Execute(ale#completion#OmniFunc() should also remember the completeopt setting and replace it): let &l:completeopt = 'menu' + let b:ale_completion_info = {'source': 'ale-automatic'} call ale#completion#OmniFunc(0, '') AssertEqual 'menu', b:ale_old_completeopt @@ -181,18 +186,35 @@ Execute(ale#completion#OmniFunc() should also remember the completeopt setting a Execute(ale#completion#OmniFunc() should set the preview option if it's set): let &l:completeopt = 'menu,preview' + let b:ale_completion_info = {'source': 'ale-automatic'} call ale#completion#OmniFunc(0, '') AssertEqual 'menu,preview', b:ale_old_completeopt AssertEqual 'menu,menuone,preview,noselect,noinsert', &l:completeopt -Execute(ale#completion#Show() should make the correct feedkeys() call): +Execute(ale#completion#Show() should make the correct feedkeys() call for automatic completion): + let b:ale_completion_info = {'source': 'ale-automatic'} call ale#completion#Show('Response', 'Parser') AssertEqual [], g:feedkeys_calls sleep 1ms AssertEqual [["\(ale_show_completion_menu)"]], g:feedkeys_calls +Execute(ale#completion#Show() should make the correct feedkeys() call for manual completion): + let b:ale_completion_info = {'source': 'ale-automatic'} + call ale#completion#Show('Response', 'Parser') + + AssertEqual [], g:feedkeys_calls + sleep 1ms + AssertEqual [["\(ale_show_completion_menu)"]], g:feedkeys_calls + +Execute(ale#completion#Show() should not call feedkeys() for other sources): + let b:ale_completion_info = {'source': 'deoplete'} + call ale#completion#Show('Response', 'Parser') + + sleep 1ms + AssertEqual [], g:feedkeys_calls + Execute(ale#completion#Show() shouldn't do anything if you switch back to normal mode): let &l:completeopt = 'menu,preview' let g:fake_mode = 'n' @@ -247,9 +269,10 @@ Execute(The completion request_id should be reset when queuing again): AssertEqual 0, b:ale_completion_info.request_id -Execute(b:ale_completion_info should be set up correctly when requesting completions): +Execute(b:ale_completion_info should be set up correctly when requesting completions automatically): + let b:ale_completion_result = [] call setpos('.', [bufnr(''), 3, 14, 0]) - call ale#completion#GetCompletions(0) + call ale#completion#GetCompletions('ale-automatic') AssertEqual \ { @@ -259,11 +282,13 @@ Execute(b:ale_completion_info should be set up correctly when requesting complet \ 'line_length': 14, \ 'line': 3, \ 'prefix': 'ab', - \ 'manual': 0, + \ 'source': 'ale-automatic', \ }, \ b:ale_completion_info + Assert !exists('b:ale_completion_result') -Execute(b:ale_completion_info should be set up correctly when requesting completions): +Execute(b:ale_completion_info should be set up correctly when requesting completions manually): + let b:ale_completion_result = [] call setpos('.', [bufnr(''), 3, 14, 0]) ALEComplete @@ -275,9 +300,28 @@ Execute(b:ale_completion_info should be set up correctly when requesting complet \ 'line_length': 14, \ 'line': 3, \ 'prefix': 'ab', - \ 'manual': 1, + \ 'source': 'ale-manual', \ }, \ b:ale_completion_info + Assert !exists('b:ale_completion_result') + +Execute(b:ale_completion_info should be set up correctly for other sources): + let b:ale_completion_result = [] + call setpos('.', [bufnr(''), 3, 14, 0]) + call ale#completion#GetCompletions('deoplete') + + AssertEqual + \ { + \ 'request_id': 0, + \ 'conn_id': 0, + \ 'column': 14, + \ 'line_length': 14, + \ 'line': 3, + \ 'prefix': 'ab', + \ 'source': 'deoplete', + \ }, + \ b:ale_completion_info + Assert !exists('b:ale_completion_result') Execute(The correct keybinds should be configured): redir => g:output diff --git a/test/completion/test_lsp_completion_messages.vader b/test/completion/test_lsp_completion_messages.vader index dce61e36..25536436 100644 --- a/test/completion/test_lsp_completion_messages.vader +++ b/test/completion/test_lsp_completion_messages.vader @@ -102,7 +102,7 @@ Execute(The right message should be sent for the initial tsserver request): " The cursor position needs to match what was saved before. call setpos('.', [bufnr(''), 1, 3, 0]) - call ale#completion#GetCompletions(0) + call ale#completion#GetCompletions('ale-automatic') " We shouldn't register the callback yet. AssertEqual '''''', string(g:Callback) @@ -129,7 +129,7 @@ Execute(The right message should be sent for the initial tsserver request): \ 'request_id': 1, \ 'line': 1, \ 'prefix': 'fo', - \ 'manual': 0, + \ 'source': 'ale-automatic', \ }, \ get(b:, 'ale_completion_info', {}) @@ -191,7 +191,7 @@ Execute(The right message should be sent for the initial LSP request): " The cursor position needs to match what was saved before. call setpos('.', [bufnr(''), 1, 5, 0]) - call ale#completion#GetCompletions(0) + call ale#completion#GetCompletions('ale-automatic') " We shouldn't register the callback yet. AssertEqual '''''', string(g:Callback) @@ -234,7 +234,7 @@ Execute(The right message should be sent for the initial LSP request): \ 'request_id': 1, \ 'line': 1, \ 'prefix': 'fo', - \ 'manual': 0, + \ 'source': 'ale-automatic', \ 'completion_filter': 'ale#completion#python#CompletionItemFilter', \ }, \ get(b:, 'ale_completion_info', {}) @@ -260,7 +260,7 @@ Execute(Two completion requests shouldn't be sent in a row): " The cursor position needs to match what was saved before. call setpos('.', [bufnr(''), 1, 5, 0]) - call ale#completion#GetCompletions(0) + call ale#completion#GetCompletions('ale-automatic') " We shouldn't register the callback yet. AssertEqual '''''', string(g:Callback) diff --git a/test/python/test_deoplete_source.py b/test/python/test_deoplete_source.py new file mode 100644 index 00000000..960abe3a --- /dev/null +++ b/test/python/test_deoplete_source.py @@ -0,0 +1,130 @@ +import unittest +import imp + +ale_module = imp.load_source( + 'deoplete.sources.ale', + '/testplugin/rplugin/python3/deoplete/sources/ale.py', +) + + +class VimMock(object): + def __init__(self, call_list, call_results): + self.__call_list = call_list + self.__call_results = call_results + + def call(self, function, *args): + self.__call_list.append((function, args)) + + return self.__call_results.get(function, 0) + + +class DeopleteSourceTest(unittest.TestCase): + def setUp(self): + super(DeopleteSourceTest, self).setUp() + + self.call_list = [] + self.call_results = {} + self.source = ale_module.Source('vim') + self.source.vim = VimMock(self.call_list, self.call_results) + + def test_attributes(self): + """ + Check all of the attributes we set. + """ + attributes = dict( + (key, getattr(self.source, key)) + for key in + dir(self.source) + if not key.startswith('__') + and key != 'vim' + and not hasattr(getattr(self.source, key), '__self__') + ) + + self.assertEqual(attributes, { + 'is_bytepos': True, + 'mark': '[L]', + 'min_pattern_length': 1, + 'name': 'ale', + 'rank': 100, + }) + + def test_completion_position(self): + self.call_results['ale#completion#GetCompletionPosition'] = 2 + + self.assertEqual(self.source.get_completion_position(), 2) + self.assertEqual(self.call_list, [ + ('ale#completion#GetCompletionPosition', ()), + ]) + + def test_request_completion_results(self): + context = {'is_async': False} + + self.assertEqual(self.source.gather_candidates(context), []) + self.assertEqual(context, {'is_async': True}) + self.assertEqual(self.call_list, [ + ('ale#completion#GetCompletions', ('deoplete',)), + ]) + + def test_refresh_completion_results(self): + context = {'is_async': False} + + self.assertEqual(self.source.gather_candidates(context), []) + self.assertEqual(context, {'is_async': True}) + self.assertEqual(self.call_list, [ + ('ale#completion#GetCompletions', ('deoplete',)), + ]) + + context = {'is_async': True, 'is_refresh': True} + + self.assertEqual(self.source.gather_candidates(context), []) + self.assertEqual(context, {'is_async': True, 'is_refresh': True}) + self.assertEqual(self.call_list, [ + ('ale#completion#GetCompletions', ('deoplete',)), + ('ale#completion#GetCompletions', ('deoplete',)), + ]) + + def test_poll_no_result(self): + context = {'is_async': True} + self.call_results['ale#completion#GetCompletionResult'] = None + + self.assertEqual(self.source.gather_candidates(context), []) + self.assertEqual(context, {'is_async': True}) + self.assertEqual(self.call_list, [ + ('ale#completion#GetCompletionResult', ()), + ]) + + def test_poll_empty_result_ready(self): + context = {'is_async': True} + self.call_results['ale#completion#GetCompletionResult'] = [] + + self.assertEqual(self.source.gather_candidates(context), []) + self.assertEqual(context, {'is_async': False}) + self.assertEqual(self.call_list, [ + ('ale#completion#GetCompletionResult', ()), + ]) + + def test_poll_non_empty_result_ready(self): + context = {'is_async': True} + self.call_results['ale#completion#GetCompletionResult'] = [ + { + 'word': 'foobar', + 'kind': 'v', + 'icase': 1, + 'menu': '', + 'info': '', + }, + ] + + self.assertEqual(self.source.gather_candidates(context), [ + { + 'word': 'foobar', + 'kind': 'v', + 'icase': 1, + 'menu': '', + 'info': '', + }, + ]) + self.assertEqual(context, {'is_async': False}) + self.assertEqual(self.call_list, [ + ('ale#completion#GetCompletionResult', ()), + ]) diff --git a/test/script/custom-checks b/test/script/custom-checks index d4027fec..20dbfb80 100755 --- a/test/script/custom-checks +++ b/test/script/custom-checks @@ -67,4 +67,14 @@ echo test/script/check-toc || exit_code=$? +echo '========================================' +echo 'Check Python code' +echo '========================================' +echo + +docker run --rm -v "$PWD:/testplugin" "$DOCKER_RUN_IMAGE" \ + python -W ignore -m unittest discover /testplugin/test/python \ + || exit_code=$? +echo + exit $exit_code