summaryrefslogtreecommitdiff
path: root/pylint
diff options
context:
space:
mode:
authorThomas Hisch <t.hisch@gmail.com>2019-03-02 14:28:31 +0100
committerClaudiu Popa <pcmanticore@gmail.com>2019-03-02 14:28:31 +0100
commitfb5a2a6dd70f4f8dae84c92c10d88dbe29c20dd6 (patch)
treef39bb615b84f22ba0c94b716c6ad0285d86caca2 /pylint
parent01b5dd649f2f84f6e3d5907065db0d7676948bd6 (diff)
downloadpylint-git-fb5a2a6dd70f4f8dae84c92c10d88dbe29c20dd6.tar.gz
Add support for reading from stdin (#2746)
pylint gained a new `--from-stdin` flag which activates stdin linting, useful for editors and similar use cases. Closes: #1187
Diffstat (limited to 'pylint')
-rw-r--r--pylint/exceptions.py4
-rw-r--r--pylint/lint.py101
-rw-r--r--pylint/test/test_self.py77
3 files changed, 160 insertions, 22 deletions
diff --git a/pylint/exceptions.py b/pylint/exceptions.py
index 57353a466..d5dd17fb2 100644
--- a/pylint/exceptions.py
+++ b/pylint/exceptions.py
@@ -23,3 +23,7 @@ class EmptyReportError(Exception):
class InvalidReporterError(Exception):
"""raised when selected reporter is invalid (e.g. not found)"""
+
+
+class InvalidArgsError(ValueError):
+ """raised when passed arguments are invalid, e.g., have the wrong length"""
diff --git a/pylint/lint.py b/pylint/lint.py
index a0c970a56..1b53b1658 100644
--- a/pylint/lint.py
+++ b/pylint/lint.py
@@ -62,6 +62,7 @@ from __future__ import print_function
import collections
import contextlib
+from io import TextIOWrapper
import operator
import os
@@ -75,6 +76,7 @@ import warnings
import astroid
from astroid.__pkginfo__ import version as astroid_version
+from astroid.builder import AstroidBuilder
from astroid import modutils
from pylint import checkers
from pylint import interfaces
@@ -89,6 +91,21 @@ from pylint.reporters.ureports import nodes as report_nodes
MANAGER = astroid.MANAGER
+def _ast_from_string(data, filepath, modname):
+ cached = MANAGER.astroid_cache.get(modname)
+ if cached and cached.file == filepath:
+ return cached
+
+ return AstroidBuilder(MANAGER).string_build(data, modname, filepath)
+
+
+def _read_stdin():
+ # https://mail.python.org/pipermail/python-list/2012-November/634424.html
+ # FIXME should this try to check the file's declared encoding?
+ sys.stdin = TextIOWrapper(sys.stdin.detach(), encoding="utf-8")
+ return sys.stdin.read()
+
+
def _get_new_args(message):
location = (
message.abspath,
@@ -556,6 +573,16 @@ class PyLinter(
),
},
),
+ (
+ "from-stdin",
+ {
+ "action": "store_true",
+ "help": (
+ "Interpret the stdin as a python script, whose filename "
+ "needs to be passed as the module_or_package argument."
+ ),
+ },
+ ),
)
option_groups = (
@@ -1054,31 +1081,61 @@ class PyLinter(
if interfaces.implements(checker, interfaces.IAstroidChecker):
walker.add_checker(checker)
# build ast and check modules or packages
- for descr in self.expand_files(files_or_modules):
- modname, filepath, is_arg = descr["name"], descr["path"], descr["isarg"]
- if not self.should_analyze_file(modname, filepath, is_argument=is_arg):
- continue
+ if self.config.from_stdin:
+ if len(files_or_modules) != 1:
+ raise exceptions.InvalidArgsError(
+ "Missing filename required for --from-stdin"
+ )
+
+ filepath = files_or_modules[0]
+ try:
+ # Note that this function does not really perform an
+ # __import__ but may raise an ImportError exception, which
+ # we want to catch here.
+ modname = ".".join(modutils.modpath_from_file(filepath))
+ except ImportError:
+ modname = os.path.splitext(os.path.basename(filepath))[0]
self.set_current_module(modname, filepath)
+
# get the module representation
- ast_node = self.get_ast(filepath, modname)
- if ast_node is None:
- continue
- # XXX to be correct we need to keep module_msgs_state for every
- # analyzed module (the problem stands with localized messages which
- # are only detected in the .close step)
- self.file_state = utils.FileState(descr["basename"])
- self._ignore_file = False
- # fix the current file (if the source file was not available or
- # if it's actually a c extension)
- self.current_file = ast_node.file # pylint: disable=maybe-no-member
- self.check_astroid_module(ast_node, walker, rawcheckers, tokencheckers)
- # warn about spurious inline messages handling
- spurious_messages = self.file_state.iter_spurious_suppression_messages(
- self.msgs_store
- )
- for msgid, line, args in spurious_messages:
- self.add_message(msgid, line, None, args)
+ ast_node = _ast_from_string(_read_stdin(), filepath, modname)
+
+ if ast_node is not None:
+ self.file_state = utils.FileState(filepath)
+ self.check_astroid_module(ast_node, walker, rawcheckers, tokencheckers)
+ # warn about spurious inline messages handling
+ spurious_messages = self.file_state.iter_spurious_suppression_messages(
+ self.msgs_store
+ )
+ for msgid, line, args in spurious_messages:
+ self.add_message(msgid, line, None, args)
+ else:
+ for descr in self.expand_files(files_or_modules):
+ modname, filepath, is_arg = descr["name"], descr["path"], descr["isarg"]
+ if not self.should_analyze_file(modname, filepath, is_argument=is_arg):
+ continue
+
+ self.set_current_module(modname, filepath)
+ # get the module representation
+ ast_node = self.get_ast(filepath, modname)
+ if ast_node is None:
+ continue
+ # XXX to be correct we need to keep module_msgs_state for every
+ # analyzed module (the problem stands with localized messages which
+ # are only detected in the .close step)
+ self.file_state = utils.FileState(descr["basename"])
+ self._ignore_file = False
+ # fix the current file (if the source file was not available or
+ # if it's actually a c extension)
+ self.current_file = ast_node.file # pylint: disable=maybe-no-member
+ self.check_astroid_module(ast_node, walker, rawcheckers, tokencheckers)
+ # warn about spurious inline messages handling
+ spurious_messages = self.file_state.iter_spurious_suppression_messages(
+ self.msgs_store
+ )
+ for msgid, line, args in spurious_messages:
+ self.add_message(msgid, line, None, args)
# notify global end
self.stats["statement"] = walker.nbstatements
for checker in reversed(_checkers):
diff --git a/pylint/test/test_self.py b/pylint/test/test_self.py
index d03566eeb..224e1e44d 100644
--- a/pylint/test/test_self.py
+++ b/pylint/test/test_self.py
@@ -30,6 +30,7 @@ import tempfile
import textwrap
import configparser
from io import StringIO
+from unittest import mock
from pylint.lint import Run
from pylint.reporters import BaseReporter
@@ -568,3 +569,79 @@ class TestRunTC(object):
finally:
os.remove(module)
os.removedirs(fake_path)
+
+ @pytest.mark.parametrize(
+ "input_path,module,expected_path",
+ [
+ (join(HERE, "mymodule.py"), "mymodule", "pylint/test/mymodule.py"),
+ ("mymodule.py", "mymodule", "mymodule.py"),
+ ],
+ )
+ def test_stdin(self, input_path, module, expected_path):
+ expected_output = (
+ "************* Module {module}\n"
+ "{path}:1:0: C0111: Missing module docstring (missing-docstring)\n"
+ "{path}:1:0: W0611: Unused import os (unused-import)\n\n"
+ ).format(path=expected_path, module=module)
+
+ with mock.patch(
+ "pylint.lint._read_stdin", return_value="import os\n"
+ ) as mock_stdin:
+ self._test_output(
+ ["--from-stdin", input_path], expected_output=expected_output
+ )
+ assert mock_stdin.call_count == 1
+
+ def test_stdin_missing_modulename(self):
+ self._runtest(["--from-stdin"], code=32)
+
+ @pytest.mark.parametrize("write_bpy_to_disk", [False, True])
+ def test_relative_imports(self, write_bpy_to_disk, tmpdir):
+ a = tmpdir.join("a")
+
+ b_code = textwrap.dedent(
+ """
+ from .c import foobar
+ from .d import bla # module does not exist
+
+ foobar('hello')
+ bla()
+ """
+ )
+
+ c_code = textwrap.dedent(
+ """
+ def foobar(arg):
+ pass
+ """
+ )
+
+ a.mkdir()
+ a.join("__init__.py").write("")
+ if write_bpy_to_disk:
+ a.join("b.py").write(b_code)
+ a.join("c.py").write(c_code)
+
+ curdir = os.getcwd()
+ try:
+ # why don't we start pylint in a subprocess?
+ os.chdir(str(tmpdir))
+ expected = (
+ "************* Module a.b\n"
+ "a/b.py:1:0: C0111: Missing module docstring (missing-docstring)\n"
+ "a/b.py:3:0: E0401: Unable to import 'a.d' (import-error)\n\n"
+ )
+
+ if write_bpy_to_disk:
+ # --from-stdin is not used here
+ self._test_output(["a/b.py"], expected_output=expected)
+
+ # this code needs to work w/ and w/o a file named a/b.py on the
+ # harddisk.
+ with mock.patch("pylint.lint._read_stdin", return_value=b_code):
+ self._test_output(
+ ["--from-stdin", join("a", "b.py")], expected_output=expected
+ )
+
+ finally:
+ os.chdir(curdir)