Skip to content

Commit

Permalink
Allow usage of Pylint via stdin (#831)
Browse files Browse the repository at this point in the history
  • Loading branch information
mpanarin authored Sep 10, 2020
1 parent dfdfb09 commit 9bc744d
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 7 deletions.
141 changes: 140 additions & 1 deletion pyls/plugins/pylint_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import collections
import logging
import sys
import re
from subprocess import Popen, PIPE

from pylint.epylint import py_run
from pyls import hookimpl, lsp
Expand Down Expand Up @@ -154,12 +156,149 @@ def _build_pylint_flags(settings):
def pyls_settings():
# Default pylint to disabled because it requires a config
# file to be useful.
return {'plugins': {'pylint': {'enabled': False, 'args': []}}}
return {'plugins': {'pylint': {
'enabled': False,
'args': [],
# disabled by default as it can slow down the workflow
'executable': None,
}}}


@hookimpl
def pyls_lint(config, document, is_saved):
"""Run pylint linter."""
settings = config.plugin_settings('pylint')
log.debug("Got pylint settings: %s", settings)
# pylint >= 2.5.0 is required for working through stdin and only
# available with python3
if settings.get('executable') and sys.version_info[0] >= 3:
flags = build_args_stdio(settings)
pylint_executable = settings.get('executable', 'pylint')
return pylint_lint_stdin(pylint_executable, document, flags)
flags = _build_pylint_flags(settings)
return PylintLinter.lint(document, is_saved, flags=flags)


def build_args_stdio(settings):
"""Build arguments for calling pylint.
:param settings: client settings
:type settings: dict
:return: arguments to path to pylint
:rtype: list
"""
pylint_args = settings.get('args')
if pylint_args is None:
return []
return pylint_args


def pylint_lint_stdin(pylint_executable, document, flags):
"""Run pylint linter from stdin.
This runs pylint in a subprocess with popen.
This allows passing the file from stdin and as a result
run pylint on unsaved files. Can slowdown the workflow.
:param pylint_executable: path to pylint executable
:type pylint_executable: string
:param document: document to run pylint on
:type document: pyls.workspace.Document
:param flags: arguments to path to pylint
:type flags: list
:return: linting diagnostics
:rtype: list
"""
pylint_result = _run_pylint_stdio(pylint_executable, document, flags)
return _parse_pylint_stdio_result(document, pylint_result)


def _run_pylint_stdio(pylint_executable, document, flags):
"""Run pylint in popen.
:param pylint_executable: path to pylint executable
:type pylint_executable: string
:param document: document to run pylint on
:type document: pyls.workspace.Document
:param flags: arguments to path to pylint
:type flags: list
:return: result of calling pylint
:rtype: string
"""
log.debug("Calling %s with args: '%s'", pylint_executable, flags)
try:
cmd = [pylint_executable]
cmd.extend(flags)
cmd.extend(['--from-stdin', document.path])
p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
except IOError:
log.debug("Can't execute %s. Trying with 'python -m pylint'", pylint_executable)
cmd = ['python', '-m', 'pylint']
cmd.extend(flags)
cmd.extend(['--from-stdin', document.path])
p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
(stdout, stderr) = p.communicate(document.source.encode())
if stderr:
log.error("Error while running pylint '%s'", stderr.decode())
return stdout.decode()


def _parse_pylint_stdio_result(document, stdout):
"""Parse pylint results.
:param document: document to run pylint on
:type document: pyls.workspace.Document
:param stdout: pylint results to parse
:type stdout: string
:return: linting diagnostics
:rtype: list
"""
diagnostics = []
lines = stdout.splitlines()
for raw_line in lines:
parsed_line = re.match(r'(.*):(\d*):(\d*): (\w*): (.*)', raw_line)
if not parsed_line:
log.debug("Pylint output parser can't parse line '%s'", raw_line)
continue

parsed_line = parsed_line.groups()
if len(parsed_line) != 5:
log.debug("Pylint output parser can't parse line '%s'", raw_line)
continue

_, line, character, code, msg = parsed_line
line = int(line) - 1
character = int(character)
severity_map = {
'C': lsp.DiagnosticSeverity.Information,
'E': lsp.DiagnosticSeverity.Error,
'F': lsp.DiagnosticSeverity.Error,
'R': lsp.DiagnosticSeverity.Hint,
'W': lsp.DiagnosticSeverity.Warning,
}
severity = severity_map[code[0]]
diagnostics.append(
{
'source': 'pylint',
'code': code,
'range': {
'start': {
'line': line,
'character': character
},
'end': {
'line': line,
# no way to determine the column
'character': len(document.lines[line]) - 1
}
},
'message': msg,
'severity': severity,
}
)

return diagnostics
15 changes: 10 additions & 5 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/usr/bin/env python
import sys
from setuptools import find_packages, setup
import versioneer
import sys
Expand Down Expand Up @@ -59,7 +60,9 @@
'pycodestyle>=2.6.0,<2.7.0',
'pydocstyle>=2.0.0',
'pyflakes>=2.2.0,<2.3.0',
'pylint',
# pylint >= 2.5.0 is required for working through stdin and only
# available with python3
'pylint>=2.5.0' if sys.version_info.major >= 3 else 'pylint',
'rope>=0.10.5',
'yapf',
],
Expand All @@ -69,12 +72,14 @@
'pycodestyle': ['pycodestyle>=2.6.0,<2.7.0'],
'pydocstyle': ['pydocstyle>=2.0.0'],
'pyflakes': ['pyflakes>=2.2.0,<2.3.0'],
'pylint': ['pylint'],
'pylint': [
'pylint>=2.5.0' if sys.version_info.major >= 3 else 'pylint'],
'rope': ['rope>0.10.5'],
'yapf': ['yapf'],
'test': ['versioneer', 'pylint', 'pytest', 'mock', 'pytest-cov',
'coverage', 'numpy', 'pandas', 'matplotlib',
'pyqt5;python_version>="3"', 'flaky'],
'test': ['versioneer',
'pylint>=2.5.0' if sys.version_info.major >= 3 else 'pylint',
'pytest', 'mock', 'pytest-cov', 'coverage', 'numpy', 'pandas',
'matplotlib', 'pyqt5;python_version>="3"', 'flaky'],
},

# To provide executable scripts, use entry points in preference to the
Expand Down
25 changes: 24 additions & 1 deletion test/plugins/test_pylint_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import os
import tempfile

from test import py2_only, py3_only
from test import py2_only, py3_only, IS_PY3
from pyls import lsp, uris
from pyls.workspace import Document
from pyls.plugins import pylint_lint
Expand Down Expand Up @@ -49,6 +49,20 @@ def test_pylint(config, workspace):
assert unused_import['range']['start'] == {'line': 0, 'character': 0}
assert unused_import['severity'] == lsp.DiagnosticSeverity.Warning

if IS_PY3:
# test running pylint in stdin
config.plugin_settings('pylint')['executable'] = 'pylint'
diags = pylint_lint.pyls_lint(config, doc, True)

msg = 'Unused import sys (unused-import)'
unused_import = [d for d in diags if d['message'] == msg][0]

assert unused_import['range']['start'] == {
'line': 0,
'character': 0,
}
assert unused_import['severity'] == lsp.DiagnosticSeverity.Warning


@py3_only
def test_syntax_error_pylint_py3(config, workspace):
Expand All @@ -60,6 +74,15 @@ def test_syntax_error_pylint_py3(config, workspace):
assert diag['range']['start'] == {'line': 0, 'character': 12}
assert diag['severity'] == lsp.DiagnosticSeverity.Error

# test running pylint in stdin
config.plugin_settings('pylint')['executable'] = 'pylint'
diag = pylint_lint.pyls_lint(config, doc, True)[0]

assert diag['message'].startswith('invalid syntax')
# Pylint doesn't give column numbers for invalid syntax.
assert diag['range']['start'] == {'line': 0, 'character': 12}
assert diag['severity'] == lsp.DiagnosticSeverity.Error


@py2_only
def test_syntax_error_pylint_py2(config, workspace):
Expand Down
5 changes: 5 additions & 0 deletions vscode-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,11 @@
"uniqueItems": false,
"description": "Arguments to pass to pylint."
},
"pyls.plugins.pylint.executable": {
"type": "string",
"default": null,
"description": "Executable to run pylint with. Enabling this will run pylint on unsaved files via stdin. Can slow down workflow. Only works with python3."
},
"pyls.plugins.rope_completion.enabled": {
"type": "boolean",
"default": true,
Expand Down

0 comments on commit 9bc744d

Please sign in to comment.