summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDudeNr33 <3929834+DudeNr33@users.noreply.github.com>2021-04-23 20:32:40 +0200
committerGitHub <noreply@github.com>2021-04-23 20:32:40 +0200
commitcdeae9cd36f08b1c6289ed9eaaa7ada4be04c538 (patch)
treef07369e608bf6e12b220bb20fc8907b56d102de9
parent922f38969c326826344e2d25af499cf8c5f80d8c (diff)
downloadpylint-git-cdeae9cd36f08b1c6289ed9eaaa7ada4be04c538.tar.gz
Enhancement 1070 file output (#4339)
* Add ``--output-file`` option
-rw-r--r--ChangeLog6
-rw-r--r--doc/user_guide/run.rst2
-rw-r--r--doc/whatsnew/2.8.rst4
-rw-r--r--pylint/lint/run.py31
-rw-r--r--tests/test_self.py95
5 files changed, 135 insertions, 3 deletions
diff --git a/ChangeLog b/ChangeLog
index 200656717..30c91a51a 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -22,6 +22,10 @@ Release date: Undefined
* Add new extension ``ConfusingConsecutiveElifChecker``. This optional checker emits a refactoring message (R5601 ``confusing-consecutive-elif``)
if if/elif statements with different indentation levels follow directly one after the other.
+* New option ``--output=<file>`` to output result to a file rather than printing to stdout.
+
+ Closes #1070
+
* Use a prescriptive message for ``unidiomatic-typecheck``
Closes #3891
@@ -67,7 +71,7 @@ Release date: Undefined
Closes #4019
-* Run will not fail if score exactly equals ``config.fail_under`.
+* Run will not fail if score exactly equals ``config.fail_under``.
* Functions that never returns may declare ``NoReturn`` as type hints, so that
``inconsistent-return-statements`` is not emitted.
diff --git a/doc/user_guide/run.rst b/doc/user_guide/run.rst
index e020e7f2e..0e5cce4be 100644
--- a/doc/user_guide/run.rst
+++ b/doc/user_guide/run.rst
@@ -180,4 +180,6 @@ exit code meaning stderr stream message
- "<return of linter.help()>"
- "Jobs number <#> should be greater \
than 0"
+ - "<IOError message when trying to open \
+ output file>"
========= ========================= ==========================================
diff --git a/doc/whatsnew/2.8.rst b/doc/whatsnew/2.8.rst
index 60c24e4ec..4f498268e 100644
--- a/doc/whatsnew/2.8.rst
+++ b/doc/whatsnew/2.8.rst
@@ -26,6 +26,10 @@ New checkers
Other Changes
=============
+* New option ``--output=<file>`` to output result to a file rather than printing to stdout.
+
+ Closes #1070
+
* Reduce usage of blacklist/whitelist terminology. Notably, ``extension-pkg-allow-list`` is an
alternative to ``extension-pkg-whitelist`` and the message ``blacklisted-name`` is now emitted as
``disallowed-name``. The previous names are accepted to maintain backward compatibility.
diff --git a/pylint/lint/run.py b/pylint/lint/run.py
index 020faf781..073af8560 100644
--- a/pylint/lint/run.py
+++ b/pylint/lint/run.py
@@ -79,6 +79,7 @@ group are mutually exclusive.",
do_exit=UNUSED_PARAM_SENTINEL,
): # pylint: disable=redefined-builtin
self._rcfile = None
+ self._output = None
self._version_asked = False
self._plugins = []
self.verbose = None
@@ -92,6 +93,7 @@ group are mutually exclusive.",
"rcfile": (self.cb_set_rcfile, True),
"load-plugins": (self.cb_add_plugins, True),
"verbose": (self.cb_verbose_mode, False),
+ "output": (self.cb_set_output, True),
},
)
except ArgumentPreprocessingError as ex:
@@ -112,6 +114,17 @@ group are mutually exclusive.",
},
),
(
+ "output",
+ {
+ "action": "callback",
+ "callback": Run._return_one,
+ "group": "Commands",
+ "type": "string",
+ "metavar": "<file>",
+ "help": "Specify an output file.",
+ },
+ ),
+ (
"init-hook",
{
"action": "callback",
@@ -355,8 +368,18 @@ group are mutually exclusive.",
# load plugin specific configuration.
linter.load_plugin_configuration()
- linter.check(args)
- score_value = linter.generate_reports()
+ if self._output:
+ try:
+ with open(self._output, "w") as output:
+ linter.reporter.set_output(output)
+ linter.check(args)
+ score_value = linter.generate_reports()
+ except OSError as ex:
+ print(ex, file=sys.stderr)
+ sys.exit(32)
+ else:
+ linter.check(args)
+ score_value = linter.generate_reports()
if do_exit is not UNUSED_PARAM_SENTINEL:
warnings.warn(
@@ -381,6 +404,10 @@ group are mutually exclusive.",
"""callback for option preprocessing (i.e. before option parsing)"""
self._rcfile = value
+ def cb_set_output(self, name, value):
+ """callback for option preprocessing (i.e. before option parsing)"""
+ self._output = value
+
def cb_add_plugins(self, name, value):
"""callback for option preprocessing (i.e. before option parsing)"""
self._plugins.extend(utils._splitstrip(value))
diff --git a/tests/test_self.py b/tests/test_self.py
index b5664aea1..a42ed1d07 100644
--- a/tests/test_self.py
+++ b/tests/test_self.py
@@ -45,6 +45,7 @@ import warnings
from copy import copy
from io import StringIO
from os.path import abspath, dirname, join
+from pathlib import Path
from typing import Generator, Optional
from unittest import mock
from unittest.mock import patch
@@ -162,6 +163,21 @@ class TestRunTC:
expected_output = self._clean_paths(expected_output)
assert expected_output.strip() in actual_output.strip()
+ def _test_output_file(self, args, filename, expected_output):
+ """
+ Run Pylint with the ``output`` option set (must be included in
+ the ``args`` passed to this method!) and check the file content afterwards.
+ """
+ out = StringIO()
+ self._run_pylint(args, out=out)
+ cmdline_output = out.getvalue()
+ file_output = self._clean_paths(Path(filename).read_text(encoding="utf-8"))
+ expected_output = self._clean_paths(expected_output)
+ assert (
+ cmdline_output == ""
+ ), "Unexpected output to stdout/stderr while output option was set"
+ assert expected_output.strip() in file_output.strip()
+
def test_pkginfo(self):
"""Make pylint check itself."""
self._runtest(["pylint.__pkginfo__"], reporter=TextReporter(StringIO()), code=0)
@@ -1031,3 +1047,82 @@ class TestRunTC:
HERE, "regrtest_data", "regression_missing_init_3564", "subdirectory/"
)
self._test_output([path, "-j2"], expected_output="No such file or directory")
+
+ def test_output_file_valid_path(self, tmpdir):
+ path = join(HERE, "regrtest_data", "unused_variable.py")
+ output_file = tmpdir / "output.txt"
+ expected = "Your code has been rated at 7.50/10"
+ self._test_output_file(
+ [path, f"--output={output_file}"],
+ output_file,
+ expected_output=expected,
+ )
+
+ def test_output_file_invalid_path_exits_with_code_32(self):
+ path = join(HERE, "regrtest_data", "unused_variable.py")
+ output_file = "thisdirectorydoesnotexit/output.txt"
+ self._runtest([path, f"--output={output_file}"], code=32)
+
+ @pytest.mark.parametrize(
+ "output_format, expected_output",
+ [
+ (
+ "text",
+ "tests/regrtest_data/unused_variable.py:4:4: W0612: Unused variable 'variable' (unused-variable)",
+ ),
+ (
+ "parseable",
+ "tests/regrtest_data/unused_variable.py:4: [W0612(unused-variable), test] Unused variable 'variable'",
+ ),
+ (
+ "msvs",
+ "tests/regrtest_data/unused_variable.py(4): [W0612(unused-variable)test] Unused variable 'variable'",
+ ),
+ (
+ "colorized",
+ "tests/regrtest_data/unused_variable.py:4:4: W0612: Unused variable 'variable' (unused-variable)",
+ ),
+ ("json", '"message": "Unused variable \'variable\'",'),
+ ],
+ )
+ def test_output_file_can_be_combined_with_output_format_option(
+ self, tmpdir, output_format, expected_output
+ ):
+ path = join(HERE, "regrtest_data", "unused_variable.py")
+ output_file = tmpdir / "output.txt"
+ self._test_output_file(
+ [path, f"--output={output_file}", f"--output-format={output_format}"],
+ output_file,
+ expected_output,
+ )
+
+ def test_output_file_can_be_combined_with_custom_reporter(self, tmpdir):
+ path = join(HERE, "regrtest_data", "unused_variable.py")
+ output_file = tmpdir / "output.txt"
+ # It does not really have to be a truly custom reporter.
+ # It is only important that it is being passed explicitly to ``Run``.
+ myreporter = TextReporter()
+ self._run_pylint(
+ [path, f"--output={output_file}"],
+ out=sys.stdout,
+ reporter=myreporter,
+ )
+ assert output_file.exists()
+
+ def test_output_file_specified_in_rcfile(self, tmpdir):
+ output_file = tmpdir / "output.txt"
+ rcfile = tmpdir / "pylintrc"
+ rcfile_contents = textwrap.dedent(
+ f"""
+ [MASTER]
+ output={output_file}
+ """
+ )
+ rcfile.write_text(rcfile_contents, encoding="utf-8")
+ path = join(HERE, "regrtest_data", "unused_variable.py")
+ expected = "Your code has been rated at 7.50/10"
+ self._test_output_file(
+ [path, f"--output={output_file}", f"--rcfile={rcfile}"],
+ output_file,
+ expected_output=expected,
+ )