From 41fcdb3ddf4063c50be51233cad8446420713676 Mon Sep 17 00:00:00 2001 From: Eric Langlois Date: Wed, 7 Oct 2015 23:27:05 -0400 Subject: [PATCH] Added g:pymode_lint_external_python When set, linting is performed using the given external Python binary. Allows linting to be performed against a different version of Python than what Vim was compiled with. --- doc/pymode.txt | 10 ++- plugin/pymode.vim | 6 +- pymode/lint.py | 165 ++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 150 insertions(+), 31 deletions(-) diff --git a/doc/pymode.txt b/doc/pymode.txt index 63047f58..82ab92a3 100644 --- a/doc/pymode.txt +++ b/doc/pymode.txt @@ -289,7 +289,7 @@ Check code on every save (every) *'g:pymode_lint_unmodified'* > let g:pymode_lint_unmodified = 0 -Check code when editing (on the fly) *'g:pymode_lint_on_fly'* +Check code when editing (on the fly) *'g:pymode_lint_on_fly'* > let g:pymode_lint_on_fly = 0 @@ -326,6 +326,14 @@ Auto open cwindow (quickfix) if any errors have been found > let g:pymode_lint_cwindow = 1 +External python binary to use while linting. *'g:pymode_lint_external_python'* +If empty, linter runs within Vim's python interpreter. The version of Python +runnning the linter determines the version of Python for which checks are run. +Setting this allows lint checks to be performed against a different version of +Python than what Vim was compiled with. +> + let g:pymode_lint_external_python = "/usr/bin/python3" + Place error |signs| *'g:pymode_signs'* > let g:pymode_lint_signs = 1 diff --git a/plugin/pymode.vim b/plugin/pymode.vim index 26541ae2..9eaf7b2d 100644 --- a/plugin/pymode.vim +++ b/plugin/pymode.vim @@ -117,11 +117,15 @@ call pymode#default("g:pymode_lint_select", "") " Auto open cwindow if any errors has been finded call pymode#default("g:pymode_lint_cwindow", 1) -" If not emply, errors will be sort by defined relevance +" If not empty, errors will be sort by defined relevance " E.g. let g:pymode_lint_sort = ['E', 'C', 'I'] " Errors first 'E', " after them 'C' and ... call pymode#default("g:pymode_lint_sort", []) +" If not empty, run lint using the given Python binary +" instead of vim's built-in Python. +call pymode#default("g:pymode_lint_external_python", "") + " Place error signs call pymode#default("g:pymode_lint_signs", 1) diff --git a/pymode/lint.py b/pymode/lint.py index 57c97f7a..1e64a349 100644 --- a/pymode/lint.py +++ b/pymode/lint.py @@ -3,9 +3,13 @@ from .environment import env from .utils import silence_stderr +import os import os.path +import re +import subprocess +import tempfile - +from pylama.errors import Error as LintError from pylama.lint.extensions import LINTERS try: @@ -15,6 +19,118 @@ pass +def __maybe_list_to_str(x): + """Convert list or string to str.""" + if isinstance(x, str): + return x + else: + return ','.join(x) + +__PARSE_PYLAMA_MESSSAGE_RE = re.compile( + '^(?P.*?):(?P\d*):(((?P\d*):)| (\[(?P.*?)\])) (?P.*)$') +__PARSE_PYLAMA_TEXT_RE = re.compile( + '^(?P.*) \[(?P.*?)\]$') + + +def _external_python_code_check(python_binary, linters, ignore, select, linter_options): + path = os.path.relpath(env.curbuf.name, env.curdir) + env.debug("Start code check: ", path) + + check_path_command_args = [ + python_binary, '-m', 'pylama', path, + '--linters', __maybe_list_to_str(linters), + '--force', + '--ignore', __maybe_list_to_str(ignore), + '--select', __maybe_list_to_str(select), + ] + env.debug("Linter options: ", linter_options) + + options_file = None + try: + if linter_options: + with tempfile.NamedTemporaryFile('w', delete=False) as f: + options_file = f.name + for linter, opts in linter_options.items(): + f.write('[pylama:{linter!s}]\n'.format(linter=linter)) + for param, value in opts.items(): + f.write('{param!s}={value!s}\n'.format( + param=param, value=value)) + + check_path_command_args += ['--options', options_file] + + env.debug("Check path args: ", check_path_command_args) + check_path_process = subprocess.Popen( + check_path_command_args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + stdout, stderr = check_path_process.communicate() + finally: + if options_file is not None: + os.remove(options_file) + + errors = [] + for line in stdout.splitlines(): + message_match = __PARSE_PYLAMA_MESSSAGE_RE.match(line) + if message_match is not None: + full_message_text = message_match.group('text') + error_kwargs = { + 'filename': message_match.group('filename'), + } + if message_match.group('lnum'): + error_kwargs['lnum'] = int(message_match.group('lnum')) + if message_match.group('col'): + error_kwargs['col'] = int(message_match.group('col')) + if message_match.group('type') is not None: + error_kwargs['type'] = message_match.group('type') + + linter_match = __PARSE_PYLAMA_TEXT_RE.match(full_message_text) + if linter_match is None: + error_kwargs['text'] = full_message_text + else: + error_kwargs['text'] = linter_match.group('text') + error_kwargs['linter'] = linter_match.group('linter') + + errors.append(LintError(**error_kwargs)) + return errors + + +def _internal_python_code_check(linters, ignore, select, linter_options): + from pylama.core import run + from pylama.main import parse_options + + if not env.curbuf.name: + return env.stop() + + options = parse_options( + linters=linters, force=1, + ignore=ignore, + select=select, + ) + + for linter, opts in linter_options.items(): + if opts: + options.linters_params[linter] = options.linters_params.get(linter, {}) + options.linters_params[linter].update(opts) + + env.debug(options) + + path = os.path.relpath(env.curbuf.name, env.curdir) + env.debug("Start code check: ", path) + + if getattr(options, 'skip', None) and any(p.match(path) for p in options.skip): # noqa + env.message('Skip code checking.') + env.debug("Skipped") + return env.stop() + + if env.options.get('debug'): + from pylama.core import LOGGER, logging + LOGGER.setLevel(logging.DEBUG) + + errors = run(path, code='\n'.join(env.curbuf) + '\n', options=options) + return errors + + def code_check(): """Run pylama and check current file. @@ -22,43 +138,34 @@ def code_check(): """ with silence_stderr(): - - from pylama.core import run - from pylama.main import parse_options - if not env.curbuf.name: return env.stop() linters = env.var('g:pymode_lint_checkers') env.debug(linters) + ignore = env.var('g:pymode_lint_ignore') + select = env.var('g:pymode_lint_select') - options = parse_options( - linters=linters, force=1, - ignore=env.var('g:pymode_lint_ignore'), - select=env.var('g:pymode_lint_select'), - ) - + linter_options = {} for linter in linters: opts = env.var('g:pymode_lint_options_%s' % linter, silence=True) if opts: - options.linters_params[linter] = options.linters_params.get(linter, {}) - options.linters_params[linter].update(opts) - - env.debug(options) - - path = os.path.relpath(env.curbuf.name, env.curdir) - env.debug("Start code check: ", path) - - if getattr(options, 'skip', None) and any(p.match(path) for p in options.skip): # noqa - env.message('Skip code checking.') - env.debug("Skipped") - return env.stop() - - if env.options.get('debug'): - from pylama.core import LOGGER, logging - LOGGER.setLevel(logging.DEBUG) - - errors = run(path, code='\n'.join(env.curbuf) + '\n', options=options) + linter_options[linter] = opts + + python_binary = env.var('g:pymode_lint_external_python', silence=True) + if python_binary: + errors = _external_python_code_check( + python_binary=python_binary, + linters=linters, + ignore=ignore, + select=select, + linter_options=linter_options) + else: + errors = _internal_python_code_check( + linters=linters, + ignore=ignore, + select=select, + linter_options=linter_options) env.debug("Find errors: ", len(errors)) sort_rules = env.var('g:pymode_lint_sort')