diff options
author | DudeNr33 <3929834+DudeNr33@users.noreply.github.com> | 2021-04-23 20:32:40 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-04-23 20:32:40 +0200 |
commit | cdeae9cd36f08b1c6289ed9eaaa7ada4be04c538 (patch) | |
tree | f07369e608bf6e12b220bb20fc8907b56d102de9 | |
parent | 922f38969c326826344e2d25af499cf8c5f80d8c (diff) | |
download | pylint-git-cdeae9cd36f08b1c6289ed9eaaa7ada4be04c538.tar.gz |
Enhancement 1070 file output (#4339)
* Add ``--output-file`` option
-rw-r--r-- | ChangeLog | 6 | ||||
-rw-r--r-- | doc/user_guide/run.rst | 2 | ||||
-rw-r--r-- | doc/whatsnew/2.8.rst | 4 | ||||
-rw-r--r-- | pylint/lint/run.py | 31 | ||||
-rw-r--r-- | tests/test_self.py | 95 |
5 files changed, 135 insertions, 3 deletions
@@ -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: [35mUnused variable 'variable'[0m ([35munused-variable[0m)", + ), + ("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, + ) |