From 4874c68b044b71704e5921e3e23060963bf3ebb5 Mon Sep 17 00:00:00 2001 From: ruro Date: Sun, 23 May 2021 15:58:38 +0300 Subject: [PATCH 01/14] implement MultiReporter --- pylint/reporters/__init__.py | 9 ++- pylint/reporters/multi_reporter.py | 89 ++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 pylint/reporters/multi_reporter.py diff --git a/pylint/reporters/__init__.py b/pylint/reporters/__init__.py index d5433f2275..1721be7b6f 100644 --- a/pylint/reporters/__init__.py +++ b/pylint/reporters/__init__.py @@ -26,6 +26,7 @@ from pylint.reporters.base_reporter import BaseReporter from pylint.reporters.collecting_reporter import CollectingReporter from pylint.reporters.json_reporter import JSONReporter +from pylint.reporters.multi_reporter import MultiReporter from pylint.reporters.reports_handler_mix_in import ReportsHandlerMixIn @@ -34,4 +35,10 @@ def initialize(linter): utils.register_plugins(linter, __path__[0]) -__all__ = ["BaseReporter", "ReportsHandlerMixIn", "JSONReporter", "CollectingReporter"] +__all__ = [ + "BaseReporter", + "ReportsHandlerMixIn", + "JSONReporter", + "CollectingReporter", + "MultiReporter", +] diff --git a/pylint/reporters/multi_reporter.py b/pylint/reporters/multi_reporter.py new file mode 100644 index 0000000000..031df68697 --- /dev/null +++ b/pylint/reporters/multi_reporter.py @@ -0,0 +1,89 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/PyCQA/pylint/blob/master/LICENSE + + +import os + +from pylint.interfaces import IReporter + + +class MultiReporter: + """Reports messages and layouts in plain text""" + + __implements__ = IReporter + name = "_internal_multi_reporter" + # Note: do not register this reporter with linter.register_reporter as it is + # not intended to be used directly like a regular reporter, but is + # instead used to implement the + # `--output-format=json:somefile.json,colorized` + # multiple output formats feature + + extension = "" + + def __init__(self, sub_reporters, close_output_files, output=None): + self._sub_reporters = sub_reporters + self.close_output_files = close_output_files + + self._path_strip_prefix = os.getcwd() + os.sep + self._linter = None + + self.set_output(output) + + def __del__(self): + self.close_output_files() + + @property + def path_strip_prefix(self): + return self._path_strip_prefix + + @path_strip_prefix.setter + def path_strip_prefix(self, value): + self._path_strip_prefix = value + for rep in self._sub_reporters: + rep.path_strip_prefix = value + + @property + def linter(self): + return self._linter + + @linter.setter + def linter(self, value): + self._linter = value + for rep in self._sub_reporters: + rep.linter = value + + def handle_message(self, msg): + """Handle a new message triggered on the current file.""" + for rep in self._sub_reporters: + rep.handle_message(msg) + + # pylint: disable=no-self-use + def set_output(self, output=None): + """set output stream""" + if output is not None: + raise NotImplementedError("MultiReporter does not support direct output.") + + def writeln(self, string=""): + """write a line in the output buffer""" + for rep in self._sub_reporters: + rep.writeln(string) + + def display_reports(self, layout): + """display results encapsulated in the layout tree""" + for rep in self._sub_reporters: + rep.display_reports(layout) + + def display_messages(self, layout): + """hook for displaying the messages of the reporter""" + for rep in self._sub_reporters: + rep.display_messages(layout) + + def on_set_current_module(self, module, filepath): + """hook called when a module starts to be analysed""" + for rep in self._sub_reporters: + rep.on_set_current_module(module, filepath) + + def on_close(self, stats, previous_stats): + """hook called when a module finished analyzing""" + for rep in self._sub_reporters: + rep.on_close(stats, previous_stats) From b294ade94f6909f9a84edffade6c224b6e5ac89d Mon Sep 17 00:00:00 2001 From: ruro Date: Sun, 23 May 2021 16:03:05 +0300 Subject: [PATCH 02/14] implement ,-separated output-format with :outputs --- pylint/lint/pylinter.py | 75 +++++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 21 deletions(-) diff --git a/pylint/lint/pylinter.py b/pylint/lint/pylinter.py index 392b188b43..784ebca73b 100644 --- a/pylint/lint/pylinter.py +++ b/pylint/lint/pylinter.py @@ -43,6 +43,15 @@ def _read_stdin(): return sys.stdin.read() +def _load_reporter_by_class(reporter_class): + qname = reporter_class + module_part = astroid.modutils.get_module_part(qname) + module = astroid.modutils.load_module_from_name(module_part) + class_name = qname.split(".")[-1] + reporter_class = getattr(module, class_name) + return reporter_class + + # Python Linter class ######################################################### MSGS = { @@ -451,7 +460,7 @@ def __init__(self, options=(), reporter=None, option_groups=(), pylintrc=None): messages store / checkers / reporter / astroid manager""" self.msgs_store = MessageDefinitionStore() self.reporter = None - self._reporter_name = None + self._reporter_names = None self._reporters = {} self._checkers = collections.defaultdict(list) self._pragma_lineno = {} @@ -502,7 +511,7 @@ def load_default_plugins(self): # Make sure to load the default reporter, because # the option has been set before the plugins had been loaded. if not self.reporter: - self._load_reporter() + self._load_reporters() def load_plugin_modules(self, modnames): """take a list of module names which are pylint plugins and load @@ -527,25 +536,49 @@ def load_plugin_configuration(self): if hasattr(module, "load_configuration"): module.load_configuration(self) - def _load_reporter(self): - name = self._reporter_name.lower() - if name in self._reporters: - self.set_reporter(self._reporters[name]()) + def _load_reporters(self): + sub_reporters = [] + output_files = [] + with contextlib.ExitStack() as stack: + for reporter_name in self._reporter_names.split(","): + reporter_name, *reporter_output = reporter_name.split(":") + + reporter = self._load_reporter_by_name(reporter_name) + sub_reporters.append(reporter) + + if reporter_output: + (reporter_output,) = reporter_output + + # pylint: disable=consider-using-with + output_file = stack.enter_context(open(reporter_output, "w")) + + reporter.set_output(output_file) + output_files.append(output_file) + + # Extend the lifetime of all opened output files + close_output_files = stack.pop_all().close + + if len(sub_reporters) > 1 or output_files: + self.set_reporter( + reporters.MultiReporter( + sub_reporters, + close_output_files, + ) + ) else: - try: - reporter_class = self._load_reporter_class() - except (ImportError, AttributeError) as e: - raise exceptions.InvalidReporterError(name) from e - else: - self.set_reporter(reporter_class()) + self.set_reporter(sub_reporters[0]) - def _load_reporter_class(self): - qname = self._reporter_name - module_part = astroid.modutils.get_module_part(qname) - module = astroid.modutils.load_module_from_name(module_part) - class_name = qname.split(".")[-1] - reporter_class = getattr(module, class_name) - return reporter_class + def _load_reporter_by_name(self, reporter_name): + name = reporter_name.lower() + if name in self._reporters: + return self._reporters[name]() + + try: + reporter_class = _load_reporter_by_class(reporter_name) + except (ImportError, AttributeError) as e: + raise exceptions.InvalidReporterError(name) from e + else: + return reporter_class() def set_reporter(self, reporter): """set the reporter used to display messages and reports""" @@ -575,11 +608,11 @@ def set_option(self, optname, value, action=None, optdict=None): meth(value) return # no need to call set_option, disable/enable methods do it elif optname == "output-format": - self._reporter_name = value + self._reporter_names = value # If the reporters are already available, load # the reporter class. if self._reporters: - self._load_reporter() + self._load_reporters() try: checkers.BaseTokenChecker.set_option(self, optname, value, action, optdict) From 0fd8b4bb71cba112d1382c8b65c151c1649d51a4 Mon Sep 17 00:00:00 2001 From: ruro Date: Sun, 23 May 2021 18:00:11 +0300 Subject: [PATCH 03/14] add tests for new output-format --- tests/unittest_reporting.py | 52 +++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/unittest_reporting.py b/tests/unittest_reporting.py index abd912b27d..f72709d2cf 100644 --- a/tests/unittest_reporting.py +++ b/tests/unittest_reporting.py @@ -14,8 +14,11 @@ # For details: https://github.com/PyCQA/pylint/blob/master/LICENSE # pylint: disable=redefined-outer-name +import os import warnings +from contextlib import redirect_stdout from io import StringIO +from tempfile import mktemp import pytest @@ -73,6 +76,55 @@ def test_parseable_output_regression(): ) +def test_multi_format_output(): + text = StringIO() + json = mktemp() + formats = ",".join( + [ + "json:" + json, + "text", + ] + ) + + try: + with redirect_stdout(text): + linter = PyLinter() + linter.set_option("output-format", formats) + linter.load_default_plugins() + linter.open() + linter.set_current_module("0123") + linter.add_message("line-too-long", line=1, args=(1, 2)) + linter.generate_reports() + linter.reporter.close_output_files() + + with open(json) as f: + assert ( + f.read() == "[\n" + " {\n" + ' "type": "convention",\n' + ' "module": "0123",\n' + ' "obj": "",\n' + ' "line": 1,\n' + ' "column": 0,\n' + ' "path": "0123",\n' + ' "symbol": "line-too-long",\n' + ' "message": "Line too long (1/2)",\n' + ' "message-id": "C0301"\n' + " }\n" + "]\n" + ) + finally: + try: + os.remove(json) + except OSError: + pass + + assert ( + text.getvalue() == "************* Module 0123\n" + "0123:1:0: C0301: Line too long (1/2) (line-too-long)\n" + ) + + def test_display_results_is_renamed(): class CustomReporter(TextReporter): def _display(self, layout): From 60ff2c6f81a482f4b722172bfef59fa6b70f5af3 Mon Sep 17 00:00:00 2001 From: ruro Date: Sun, 23 May 2021 16:03:28 +0300 Subject: [PATCH 04/14] add docs, changelog, whatsnew, contributor entry --- CONTRIBUTORS.txt | 2 ++ ChangeLog | 5 +++++ doc/user_guide/output.rst | 9 +++++++++ doc/whatsnew/2.9.rst | 3 +++ 4 files changed, 19 insertions(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 0e6a1d4d4a..d4ebc24431 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -491,3 +491,5 @@ contributors: - Added ignore_signatures to duplicate checker * Jacob Walls: contributor + +* ruro: contributor diff --git a/ChangeLog b/ChangeLog index 905873ebfd..858d0e117a 100644 --- a/ChangeLog +++ b/ChangeLog @@ -72,6 +72,11 @@ modules are added. Closes #2309 +* Allow comma-separated list in ``output-format`` and separate output files for + each specified format. + + Closes #1798 + What's New in Pylint 2.8.2? =========================== diff --git a/doc/user_guide/output.rst b/doc/user_guide/output.rst index b329d30572..4d375fdc7a 100644 --- a/doc/user_guide/output.rst +++ b/doc/user_guide/output.rst @@ -6,6 +6,15 @@ The default format for the output is raw text. You can change this by passing pylint the ``--output-format=`` option. Possible values are: json, parseable, colorized and msvs (visual studio). +Multiple output formats can be used at the same time by passing +``--output-format`` a comma-separated list of formats. To change the output file +for an individual format, specify it after a semicolon. For example, you can +save a json report to ``somefile`` and print a colorized report to stdout at the +same time with : +:: + + --output-format=json:somefile,colorized + Moreover you can customize the exact way information are displayed using the `--msg-template=` option. The `format string` uses the `Python new format syntax`_ and the following fields are available : diff --git a/doc/whatsnew/2.9.rst b/doc/whatsnew/2.9.rst index 644e9a648d..c2c1dcf5ea 100644 --- a/doc/whatsnew/2.9.rst +++ b/doc/whatsnew/2.9.rst @@ -41,3 +41,6 @@ Other Changes of overridden functions. It aims to separate the functionality of ``arguments-differ``. * Fix incompatibility with Python 3.6.0 caused by ``typing.Counter`` and ``typing.NoReturn`` usage + +* Allow comma-separated list in ``output-format`` and separate output files for + each specified format. From f21b932c7e9401f2ff240a44904235c07a792f80 Mon Sep 17 00:00:00 2001 From: ruro Date: Sun, 23 May 2021 20:00:53 +0300 Subject: [PATCH 05/14] fix minor test nitpicks --- tests/unittest_reporting.py | 73 +++++++++++++++---------------------- 1 file changed, 30 insertions(+), 43 deletions(-) diff --git a/tests/unittest_reporting.py b/tests/unittest_reporting.py index f72709d2cf..71b45ba88a 100644 --- a/tests/unittest_reporting.py +++ b/tests/unittest_reporting.py @@ -14,11 +14,9 @@ # For details: https://github.com/PyCQA/pylint/blob/master/LICENSE # pylint: disable=redefined-outer-name -import os import warnings from contextlib import redirect_stdout from io import StringIO -from tempfile import mktemp import pytest @@ -76,48 +74,37 @@ def test_parseable_output_regression(): ) -def test_multi_format_output(): +def test_multi_format_output(tmp_path): text = StringIO() - json = mktemp() - formats = ",".join( - [ - "json:" + json, - "text", - ] - ) - - try: - with redirect_stdout(text): - linter = PyLinter() - linter.set_option("output-format", formats) - linter.load_default_plugins() - linter.open() - linter.set_current_module("0123") - linter.add_message("line-too-long", line=1, args=(1, 2)) - linter.generate_reports() - linter.reporter.close_output_files() - - with open(json) as f: - assert ( - f.read() == "[\n" - " {\n" - ' "type": "convention",\n' - ' "module": "0123",\n' - ' "obj": "",\n' - ' "line": 1,\n' - ' "column": 0,\n' - ' "path": "0123",\n' - ' "symbol": "line-too-long",\n' - ' "message": "Line too long (1/2)",\n' - ' "message-id": "C0301"\n' - " }\n" - "]\n" - ) - finally: - try: - os.remove(json) - except OSError: - pass + json = tmp_path / "somefile.json" + formats = ",".join(["json:" + str(json), "text"]) + + with redirect_stdout(text): + linter = PyLinter() + linter.set_option("output-format", formats) + linter.load_default_plugins() + linter.open() + linter.set_current_module("0123") + linter.add_message("line-too-long", line=1, args=(1, 2)) + linter.generate_reports() + linter.reporter.close_output_files() + + with open(json) as f: + assert ( + f.read() == "[\n" + " {\n" + ' "type": "convention",\n' + ' "module": "0123",\n' + ' "obj": "",\n' + ' "line": 1,\n' + ' "column": 0,\n' + ' "path": "0123",\n' + ' "symbol": "line-too-long",\n' + ' "message": "Line too long (1/2)",\n' + ' "message-id": "C0301"\n' + " }\n" + "]\n" + ) assert ( text.getvalue() == "************* Module 0123\n" From 13011f9fc80bb392993c5ecd7b4e90340c451fcf Mon Sep 17 00:00:00 2001 From: ruro Date: Sun, 23 May 2021 20:04:49 +0300 Subject: [PATCH 06/14] add a bunch of type hints --- pylint/lint/pylinter.py | 9 +++---- pylint/reporters/multi_reporter.py | 42 +++++++++++++++++++++--------- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/pylint/lint/pylinter.py b/pylint/lint/pylinter.py index 784ebca73b..2cc319c832 100644 --- a/pylint/lint/pylinter.py +++ b/pylint/lint/pylinter.py @@ -43,13 +43,12 @@ def _read_stdin(): return sys.stdin.read() -def _load_reporter_by_class(reporter_class): +def _load_reporter_by_class(reporter_class: str) -> type: qname = reporter_class module_part = astroid.modutils.get_module_part(qname) module = astroid.modutils.load_module_from_name(module_part) class_name = qname.split(".")[-1] - reporter_class = getattr(module, class_name) - return reporter_class + return getattr(module, class_name) # Python Linter class ######################################################### @@ -536,7 +535,7 @@ def load_plugin_configuration(self): if hasattr(module, "load_configuration"): module.load_configuration(self) - def _load_reporters(self): + def _load_reporters(self) -> None: sub_reporters = [] output_files = [] with contextlib.ExitStack() as stack: @@ -568,7 +567,7 @@ def _load_reporters(self): else: self.set_reporter(sub_reporters[0]) - def _load_reporter_by_name(self, reporter_name): + def _load_reporter_by_name(self, reporter_name: str) -> reporters.BaseReporter: name = reporter_name.lower() if name in self._reporters: return self._reporters[name]() diff --git a/pylint/reporters/multi_reporter.py b/pylint/reporters/multi_reporter.py index 031df68697..3757d40668 100644 --- a/pylint/reporters/multi_reporter.py +++ b/pylint/reporters/multi_reporter.py @@ -3,8 +3,15 @@ import os +from typing import IO, Any, AnyStr, Callable, List, Mapping, Optional, Union from pylint.interfaces import IReporter +from pylint.reporters.base_reporter import BaseReporter +from pylint.reporters.ureports.nodes import BaseLayout + +AnyFile = IO[AnyStr] +AnyPath = Union[str, bytes, os.PathLike] +PyLinter = Any class MultiReporter: @@ -20,12 +27,17 @@ class MultiReporter: extension = "" - def __init__(self, sub_reporters, close_output_files, output=None): + def __init__( + self, + sub_reporters: List[BaseReporter], + close_output_files: Callable[[], None], + output: Optional[AnyFile] = None, + ): self._sub_reporters = sub_reporters self.close_output_files = close_output_files self._path_strip_prefix = os.getcwd() + os.sep - self._linter = None + self._linter: Optional[PyLinter] = None self.set_output(output) @@ -33,57 +45,61 @@ def __del__(self): self.close_output_files() @property - def path_strip_prefix(self): + def path_strip_prefix(self) -> str: return self._path_strip_prefix @path_strip_prefix.setter - def path_strip_prefix(self, value): + def path_strip_prefix(self, value: str) -> None: self._path_strip_prefix = value for rep in self._sub_reporters: rep.path_strip_prefix = value @property - def linter(self): + def linter(self) -> Optional[PyLinter]: return self._linter @linter.setter - def linter(self, value): + def linter(self, value: PyLinter) -> None: self._linter = value for rep in self._sub_reporters: rep.linter = value - def handle_message(self, msg): + def handle_message(self, msg: str) -> None: """Handle a new message triggered on the current file.""" for rep in self._sub_reporters: rep.handle_message(msg) # pylint: disable=no-self-use - def set_output(self, output=None): + def set_output(self, output: Optional[AnyFile] = None) -> None: """set output stream""" if output is not None: raise NotImplementedError("MultiReporter does not support direct output.") - def writeln(self, string=""): + def writeln(self, string: str = "") -> None: """write a line in the output buffer""" for rep in self._sub_reporters: rep.writeln(string) - def display_reports(self, layout): + def display_reports(self, layout: BaseLayout) -> None: """display results encapsulated in the layout tree""" for rep in self._sub_reporters: rep.display_reports(layout) - def display_messages(self, layout): + def display_messages(self, layout: BaseLayout) -> None: """hook for displaying the messages of the reporter""" for rep in self._sub_reporters: rep.display_messages(layout) - def on_set_current_module(self, module, filepath): + def on_set_current_module(self, module: str, filepath: Optional[AnyPath]) -> None: """hook called when a module starts to be analysed""" for rep in self._sub_reporters: rep.on_set_current_module(module, filepath) - def on_close(self, stats, previous_stats): + def on_close( + self, + stats: Mapping[Any, Any], + previous_stats: Mapping[Any, Any], + ) -> None: """hook called when a module finished analyzing""" for rep in self._sub_reporters: rep.on_close(stats, previous_stats) From c8c9ce0377db1911939534e8385c0035ae93e5eb Mon Sep 17 00:00:00 2001 From: ruro Date: Sun, 23 May 2021 20:45:57 +0300 Subject: [PATCH 07/14] fix split for windows paths containing semicolons --- pylint/lint/pylinter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylint/lint/pylinter.py b/pylint/lint/pylinter.py index 2cc319c832..fc7cee346d 100644 --- a/pylint/lint/pylinter.py +++ b/pylint/lint/pylinter.py @@ -540,7 +540,7 @@ def _load_reporters(self) -> None: output_files = [] with contextlib.ExitStack() as stack: for reporter_name in self._reporter_names.split(","): - reporter_name, *reporter_output = reporter_name.split(":") + reporter_name, *reporter_output = reporter_name.split(":", 1) reporter = self._load_reporter_by_name(reporter_name) sub_reporters.append(reporter) From f1456ce39daec4063ba410010b1e4194abb925be Mon Sep 17 00:00:00 2001 From: ruro Date: Sun, 23 May 2021 21:54:14 +0300 Subject: [PATCH 08/14] make MultiReporter.path_strip_prefix readonly --- pylint/reporters/multi_reporter.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pylint/reporters/multi_reporter.py b/pylint/reporters/multi_reporter.py index 3757d40668..5723dc1601 100644 --- a/pylint/reporters/multi_reporter.py +++ b/pylint/reporters/multi_reporter.py @@ -48,12 +48,6 @@ def __del__(self): def path_strip_prefix(self) -> str: return self._path_strip_prefix - @path_strip_prefix.setter - def path_strip_prefix(self, value: str) -> None: - self._path_strip_prefix = value - for rep in self._sub_reporters: - rep.path_strip_prefix = value - @property def linter(self) -> Optional[PyLinter]: return self._linter @@ -72,6 +66,9 @@ def handle_message(self, msg: str) -> None: # pylint: disable=no-self-use def set_output(self, output: Optional[AnyFile] = None) -> None: """set output stream""" + # MultiReporter doesn't have it's own output. This method is only + # provided for API parity with BaseReporter and should not be called + # with non-None values for 'output'. if output is not None: raise NotImplementedError("MultiReporter does not support direct output.") From 087396d346225a0fc8d719c880cdf01d8b050239 Mon Sep 17 00:00:00 2001 From: ruro Date: Sun, 23 May 2021 21:54:56 +0300 Subject: [PATCH 09/14] improve MultiReporter coverage --- tests/unittest_reporting.py | 124 ++++++++++++++++++++++++++++++++++-- 1 file changed, 118 insertions(+), 6 deletions(-) diff --git a/tests/unittest_reporting.py b/tests/unittest_reporting.py index 71b45ba88a..09f7725627 100644 --- a/tests/unittest_reporting.py +++ b/tests/unittest_reporting.py @@ -77,38 +77,150 @@ def test_parseable_output_regression(): def test_multi_format_output(tmp_path): text = StringIO() json = tmp_path / "somefile.json" + source_file = tmp_path / "somemodule.py" + source_file.write_text('NOT_EMPTY = "This module is not empty"\n') formats = ",".join(["json:" + str(json), "text"]) with redirect_stdout(text): linter = PyLinter() + linter.set_option("persistent", False) linter.set_option("output-format", formats) + linter.set_option("reports", True) + linter.set_option("score", True) linter.load_default_plugins() + + assert linter.reporter.linter is linter + with pytest.raises(NotImplementedError): + linter.reporter.set_output(text) + linter.open() - linter.set_current_module("0123") + linter.check_single_file("somemodule", source_file, "somemodule") linter.add_message("line-too-long", line=1, args=(1, 2)) linter.generate_reports() - linter.reporter.close_output_files() + linter.reporter.writeln("direct output") + del linter.reporter # Ensure the output files are flushed and closed with open(json) as f: assert ( f.read() == "[\n" " {\n" ' "type": "convention",\n' - ' "module": "0123",\n' + ' "module": "somemodule",\n' + ' "obj": "",\n' + ' "line": 1,\n' + ' "column": 0,\n' + f' "path": "{source_file}",\n' + ' "symbol": "missing-module-docstring",\n' + ' "message": "Missing module docstring",\n' + ' "message-id": "C0114"\n' + " },\n" + " {\n" + ' "type": "convention",\n' + ' "module": "somemodule",\n' ' "obj": "",\n' ' "line": 1,\n' ' "column": 0,\n' - ' "path": "0123",\n' + f' "path": "{source_file}",\n' ' "symbol": "line-too-long",\n' ' "message": "Line too long (1/2)",\n' ' "message-id": "C0301"\n' " }\n" "]\n" + "direct output\n" ) assert ( - text.getvalue() == "************* Module 0123\n" - "0123:1:0: C0301: Line too long (1/2) (line-too-long)\n" + text.getvalue() == "************* Module somemodule\n" + f"{source_file}:1:0: C0114: Missing module docstring (missing-module-docstring)\n" + f"{source_file}:1:0: C0301: Line too long (1/2) (line-too-long)\n" + "\n" + "\n" + "Report\n" + "======\n" + "1 statements analysed.\n" + "\n" + "Statistics by type\n" + "------------------\n" + "\n" + "+---------+-------+-----------+-----------+------------+---------+\n" + "|type |number |old number |difference |%documented |%badname |\n" + "+=========+=======+===========+===========+============+=========+\n" + "|module |1 |NC |NC |0.00 |0.00 |\n" + "+---------+-------+-----------+-----------+------------+---------+\n" + "|class |0 |NC |NC |0 |0 |\n" + "+---------+-------+-----------+-----------+------------+---------+\n" + "|method |0 |NC |NC |0 |0 |\n" + "+---------+-------+-----------+-----------+------------+---------+\n" + "|function |0 |NC |NC |0 |0 |\n" + "+---------+-------+-----------+-----------+------------+---------+\n" + "\n" + "\n" + "\n" + "Raw metrics\n" + "-----------\n" + "\n" + "+----------+-------+------+---------+-----------+\n" + "|type |number |% |previous |difference |\n" + "+==========+=======+======+=========+===========+\n" + "|code |2 |66.67 |NC |NC |\n" + "+----------+-------+------+---------+-----------+\n" + "|docstring |0 |0.00 |NC |NC |\n" + "+----------+-------+------+---------+-----------+\n" + "|comment |0 |0.00 |NC |NC |\n" + "+----------+-------+------+---------+-----------+\n" + "|empty |1 |33.33 |NC |NC |\n" + "+----------+-------+------+---------+-----------+\n" + "\n" + "\n" + "\n" + "Duplication\n" + "-----------\n" + "\n" + "+-------------------------+------+---------+-----------+\n" + "| |now |previous |difference |\n" + "+=========================+======+=========+===========+\n" + "|nb duplicated lines |0 |NC |NC |\n" + "+-------------------------+------+---------+-----------+\n" + "|percent duplicated lines |0.000 |NC |NC |\n" + "+-------------------------+------+---------+-----------+\n" + "\n" + "\n" + "\n" + "Messages by category\n" + "--------------------\n" + "\n" + "+-----------+-------+---------+-----------+\n" + "|type |number |previous |difference |\n" + "+===========+=======+=========+===========+\n" + "|convention |2 |NC |NC |\n" + "+-----------+-------+---------+-----------+\n" + "|refactor |0 |NC |NC |\n" + "+-----------+-------+---------+-----------+\n" + "|warning |0 |NC |NC |\n" + "+-----------+-------+---------+-----------+\n" + "|error |0 |NC |NC |\n" + "+-----------+-------+---------+-----------+\n" + "\n" + "\n" + "\n" + "Messages\n" + "--------\n" + "\n" + "+-------------------------+------------+\n" + "|message id |occurrences |\n" + "+=========================+============+\n" + "|missing-module-docstring |1 |\n" + "+-------------------------+------------+\n" + "|line-too-long |1 |\n" + "+-------------------------+------------+\n" + "\n" + "\n" + "\n" + "\n" + "-------------------------------------\n" + "Your code has been rated at -10.00/10\n" + "\n" + "direct output\n" ) From 2c737ec891c60fa14df8ea7c916c42bf60e12a87 Mon Sep 17 00:00:00 2001 From: ruro Date: Sun, 23 May 2021 22:33:32 +0300 Subject: [PATCH 10/14] explicitly close the files for pypy delayed gc --- tests/unittest_reporting.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/unittest_reporting.py b/tests/unittest_reporting.py index 09f7725627..c2e711af97 100644 --- a/tests/unittest_reporting.py +++ b/tests/unittest_reporting.py @@ -98,7 +98,10 @@ def test_multi_format_output(tmp_path): linter.add_message("line-too-long", line=1, args=(1, 2)) linter.generate_reports() linter.reporter.writeln("direct output") - del linter.reporter # Ensure the output files are flushed and closed + + # Ensure the output files are flushed and closed + linter.reporter.close_output_files() + del linter.reporter with open(json) as f: assert ( From 03619cbdf202ee5b4407ff8a90c1e32a2f8af616 Mon Sep 17 00:00:00 2001 From: ruro Date: Sun, 23 May 2021 22:43:34 +0300 Subject: [PATCH 11/14] properly escape paths for json output test --- tests/unittest_reporting.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/unittest_reporting.py b/tests/unittest_reporting.py index c2e711af97..455dbca143 100644 --- a/tests/unittest_reporting.py +++ b/tests/unittest_reporting.py @@ -17,6 +17,7 @@ import warnings from contextlib import redirect_stdout from io import StringIO +from json import dumps import pytest @@ -79,6 +80,7 @@ def test_multi_format_output(tmp_path): json = tmp_path / "somefile.json" source_file = tmp_path / "somemodule.py" source_file.write_text('NOT_EMPTY = "This module is not empty"\n') + escaped_source_file = dumps(str(source_file)) formats = ",".join(["json:" + str(json), "text"]) with redirect_stdout(text): @@ -112,7 +114,7 @@ def test_multi_format_output(tmp_path): ' "obj": "",\n' ' "line": 1,\n' ' "column": 0,\n' - f' "path": "{source_file}",\n' + f' "path": {escaped_source_file},\n' ' "symbol": "missing-module-docstring",\n' ' "message": "Missing module docstring",\n' ' "message-id": "C0114"\n' @@ -123,7 +125,7 @@ def test_multi_format_output(tmp_path): ' "obj": "",\n' ' "line": 1,\n' ' "column": 0,\n' - f' "path": "{source_file}",\n' + f' "path": {escaped_source_file},\n' ' "symbol": "line-too-long",\n' ' "message": "Line too long (1/2)",\n' ' "message-id": "C0301"\n' From afd18e4f135df1973b204be29bd802e23e31446a Mon Sep 17 00:00:00 2001 From: ruro Date: Mon, 24 May 2021 00:06:07 +0300 Subject: [PATCH 12/14] cover the FQN name class format loading code --- tests/unittest_reporting.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/tests/unittest_reporting.py b/tests/unittest_reporting.py index 455dbca143..2022deefaf 100644 --- a/tests/unittest_reporting.py +++ b/tests/unittest_reporting.py @@ -22,7 +22,9 @@ import pytest from pylint import checkers +from pylint.interfaces import IReporter from pylint.lint import PyLinter +from pylint.reporters import BaseReporter from pylint.reporters.text import ParseableTextReporter, TextReporter @@ -75,13 +77,32 @@ def test_parseable_output_regression(): ) +class NopReporter(BaseReporter): + __implements__ = IReporter + name = "nop-reporter" + extension = "" + + def __init__(self, output=None): + super().__init__(output) + print("A NopReporter was initialized.", file=self.out) + + def writeln(self, string=""): + pass + + def _display(self, layout): + pass + + def test_multi_format_output(tmp_path): text = StringIO() json = tmp_path / "somefile.json" + source_file = tmp_path / "somemodule.py" source_file.write_text('NOT_EMPTY = "This module is not empty"\n') escaped_source_file = dumps(str(source_file)) - formats = ",".join(["json:" + str(json), "text"]) + + nop_format = NopReporter.__module__ + "." + NopReporter.__name__ + formats = ",".join(["json:" + str(json), "text", nop_format]) with redirect_stdout(text): linter = PyLinter() @@ -135,7 +156,8 @@ def test_multi_format_output(tmp_path): ) assert ( - text.getvalue() == "************* Module somemodule\n" + text.getvalue() == "A NopReporter was initialized.\n" + "************* Module somemodule\n" f"{source_file}:1:0: C0114: Missing module docstring (missing-module-docstring)\n" f"{source_file}:1:0: C0301: Line too long (1/2) (line-too-long)\n" "\n" From 2deb605973cc8177e93bba48746030b5617bf925 Mon Sep 17 00:00:00 2001 From: ruro Date: Mon, 24 May 2021 00:41:55 +0300 Subject: [PATCH 13/14] enable newline translation in tests for windows --- tests/unittest_reporting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unittest_reporting.py b/tests/unittest_reporting.py index 2022deefaf..554f325cfd 100644 --- a/tests/unittest_reporting.py +++ b/tests/unittest_reporting.py @@ -94,7 +94,7 @@ def _display(self, layout): def test_multi_format_output(tmp_path): - text = StringIO() + text = StringIO(newline=None) json = tmp_path / "somefile.json" source_file = tmp_path / "somemodule.py" From 399d0d3547cfb029c72f800de62dcbe8a2b279a3 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Mon, 24 May 2021 20:03:28 +0200 Subject: [PATCH 14/14] Update doc/whatsnew/2.9.rst --- doc/whatsnew/2.9.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/whatsnew/2.9.rst b/doc/whatsnew/2.9.rst index c2c1dcf5ea..ad91ed5d82 100644 --- a/doc/whatsnew/2.9.rst +++ b/doc/whatsnew/2.9.rst @@ -43,4 +43,4 @@ Other Changes * Fix incompatibility with Python 3.6.0 caused by ``typing.Counter`` and ``typing.NoReturn`` usage * Allow comma-separated list in ``output-format`` and separate output files for - each specified format. + each specified format. Each output file can be defined after a semicolon for example : ``--output-format=json:myfile.json,colorized``