From dc0c7e97b1e92beffa36f76c5c56164f69b81a2a Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Thu, 16 Sep 2021 11:03:37 +0200 Subject: [PATCH] Add typing in ``pylint.reporters`` (#5004) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add typing and fix small issue in pylint.reporters Fix typing error in pylint/checkers/imports.py. Add typing of report related code outside of pylint.reporters. * Remove unused argument in pylint.reporters.VNode constructor * Simplify and specify the typing in reporters nodes Co-authored-by: Daniƫl van Noord <13665637+DanielNoord@users.noreply.github.com> --- pylint/checkers/__init__.py | 2 +- pylint/checkers/imports.py | 2 +- pylint/interfaces.py | 5 +- pylint/lint/report_functions.py | 10 ++-- pylint/reporters/__init__.py | 9 ++- pylint/reporters/collecting_reporter.py | 4 +- pylint/reporters/multi_reporter.py | 2 +- pylint/reporters/reports_handler_mix_in.py | 23 +++++--- pylint/reporters/ureports/base_writer.py | 26 ++++++--- pylint/reporters/ureports/nodes.py | 64 +++++++++++++--------- pylint/reporters/ureports/text_writer.py | 44 ++++++++++----- 11 files changed, 118 insertions(+), 73 deletions(-) diff --git a/pylint/checkers/__init__.py b/pylint/checkers/__init__.py index 584f476fc0..d15c876897 100644 --- a/pylint/checkers/__init__.py +++ b/pylint/checkers/__init__.py @@ -61,7 +61,7 @@ def table_lines_from_stats( columns: Iterable[str], ) -> List[str]: """get values listed in from and , - and return a formated list of values, designed to be given to a + and return a formatted list of values, designed to be given to a ureport.Table object """ lines: List[str] = [] diff --git a/pylint/checkers/imports.py b/pylint/checkers/imports.py index b8609e9624..4c75b59283 100644 --- a/pylint/checkers/imports.py +++ b/pylint/checkers/imports.py @@ -193,7 +193,7 @@ def _make_graph( report's section """ outputfile = _dependencies_graph(filename, dep_info) - sect.append(Paragraph(f"{gtype}imports graph has been written to {outputfile}")) + sect.append(Paragraph((f"{gtype}imports graph has been written to {outputfile}",))) # the import checker itself ################################################### diff --git a/pylint/interfaces.py b/pylint/interfaces.py index cac7e76c36..2e1ca6da5f 100644 --- a/pylint/interfaces.py +++ b/pylint/interfaces.py @@ -17,6 +17,7 @@ """Interfaces for Pylint objects""" from collections import namedtuple +from typing import Tuple from astroid import nodes @@ -40,7 +41,7 @@ def is_implemented_by(cls, instance): return implements(instance, cls) -def implements(obj, interface): +def implements(obj: "Interface", interface: Tuple[type, type]) -> bool: """Return true if the give object (maybe an instance or class) implements the interface. """ @@ -101,4 +102,4 @@ def display_reports(self, layout): """display results encapsulated in the layout tree""" -__all__ = ("IRawChecker", "IAstroidChecker", "ITokenChecker", "IReporter") +__all__ = ("IRawChecker", "IAstroidChecker", "ITokenChecker", "IReporter", "IChecker") diff --git a/pylint/lint/report_functions.py b/pylint/lint/report_functions.py index 12c4b03352..201b28b7bb 100644 --- a/pylint/lint/report_functions.py +++ b/pylint/lint/report_functions.py @@ -5,7 +5,7 @@ from typing import DefaultDict, Dict, List, Tuple, Union from pylint import checkers, exceptions -from pylint.reporters.ureports import nodes as report_nodes +from pylint.reporters.ureports.nodes import Table from pylint.typing import CheckerStats @@ -19,7 +19,7 @@ def report_total_messages_stats( lines += checkers.table_lines_from_stats( stats, previous_stats, ("convention", "refactor", "warning", "error") ) - sect.append(report_nodes.Table(children=lines, cols=4, rheaders=1)) + sect.append(Table(children=lines, cols=4, rheaders=1)) def report_messages_stats( @@ -41,7 +41,7 @@ def report_messages_stats( lines = ["message id", "occurrences"] for value, msg_id in in_order: lines += [msg_id, str(value)] - sect.append(report_nodes.Table(children=lines, cols=2, rheaders=1)) + sect.append(Table(children=lines, cols=2, rheaders=1)) def report_messages_by_module_stats( @@ -61,7 +61,7 @@ def report_messages_by_module_stats( total: int = stats[m_type] # type: ignore for module in module_stats.keys(): mod_total = module_stats[module][m_type] - percent = 0 if total == 0 else float((mod_total) * 100) / total + percent = 0 if total == 0 else float(mod_total * 100) / total by_mod[module][m_type] = percent sorted_result = [] for module, mod_info in by_mod.items(): @@ -86,4 +86,4 @@ def report_messages_by_module_stats( lines.append(f"{val:.2f}") if len(lines) == 5: raise exceptions.EmptyReportError() - sect.append(report_nodes.Table(children=lines, cols=5, rheaders=1)) + sect.append(Table(children=lines, cols=5, rheaders=1)) diff --git a/pylint/reporters/__init__.py b/pylint/reporters/__init__.py index 79b13e083b..39cf5fb0a7 100644 --- a/pylint/reporters/__init__.py +++ b/pylint/reporters/__init__.py @@ -21,7 +21,7 @@ # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE """utilities methods and classes for reporters""" - +from typing import TYPE_CHECKING from pylint import utils from pylint.reporters.base_reporter import BaseReporter @@ -30,10 +30,13 @@ from pylint.reporters.multi_reporter import MultiReporter from pylint.reporters.reports_handler_mix_in import ReportsHandlerMixIn +if TYPE_CHECKING: + from pylint.lint.pylinter import PyLinter + -def initialize(linter): +def initialize(linter: "PyLinter") -> None: """initialize linter with reporters in this package""" - utils.register_plugins(linter, __path__[0]) + utils.register_plugins(linter, __path__[0]) # type: ignore # Fixed in https://github.com/python/mypy/pull/9454 __all__ = [ diff --git a/pylint/reporters/collecting_reporter.py b/pylint/reporters/collecting_reporter.py index 309c604052..145c3c81b3 100644 --- a/pylint/reporters/collecting_reporter.py +++ b/pylint/reporters/collecting_reporter.py @@ -8,11 +8,11 @@ class CollectingReporter(BaseReporter): name = "collector" - def __init__(self): + def __init__(self) -> None: BaseReporter.__init__(self) self.messages = [] - def reset(self): + def reset(self) -> None: self.messages = [] _display = None diff --git a/pylint/reporters/multi_reporter.py b/pylint/reporters/multi_reporter.py index 245c10f79c..2ac361b381 100644 --- a/pylint/reporters/multi_reporter.py +++ b/pylint/reporters/multi_reporter.py @@ -43,7 +43,7 @@ def __init__( self.set_output(output) - def __del__(self): + def __del__(self) -> None: self.close_output_files() @property diff --git a/pylint/reporters/reports_handler_mix_in.py b/pylint/reporters/reports_handler_mix_in.py index 450d383f51..de3bbe225a 100644 --- a/pylint/reporters/reports_handler_mix_in.py +++ b/pylint/reporters/reports_handler_mix_in.py @@ -2,9 +2,10 @@ # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE import collections -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Callable, DefaultDict, Dict, List, Tuple from pylint.exceptions import EmptyReportError +from pylint.interfaces import IChecker from pylint.reporters.ureports.nodes import Section from pylint.typing import CheckerStats @@ -17,9 +18,11 @@ class ReportsHandlerMixIn: related methods for the main lint class """ - def __init__(self): - self._reports = collections.defaultdict(list) - self._reports_state = {} + def __init__(self) -> None: + self._reports: DefaultDict[ + IChecker, List[Tuple[str, str, Callable]] + ] = collections.defaultdict(list) + self._reports_state: Dict[str, bool] = {} def report_order(self): """Return a list of reports, sorted in the order @@ -27,7 +30,9 @@ def report_order(self): """ return list(self._reports) - def register_report(self, reportid, r_title, r_cb, checker): + def register_report( + self, reportid: str, r_title: str, r_cb: Callable, checker: IChecker + ) -> None: """register a report reportid is the unique identifier for the report @@ -38,17 +43,17 @@ def register_report(self, reportid, r_title, r_cb, checker): reportid = reportid.upper() self._reports[checker].append((reportid, r_title, r_cb)) - def enable_report(self, reportid): + def enable_report(self, reportid: str) -> None: """disable the report of the given id""" reportid = reportid.upper() self._reports_state[reportid] = True - def disable_report(self, reportid): + def disable_report(self, reportid: str) -> None: """disable the report of the given id""" reportid = reportid.upper() self._reports_state[reportid] = False - def report_is_enabled(self, reportid): + def report_is_enabled(self, reportid: str) -> bool: """return true if the report associated to the given identifier is enabled """ @@ -58,7 +63,7 @@ def make_reports( # type: ignore # ReportsHandlerMixIn is always mixed with PyL self: "PyLinter", stats: CheckerStats, old_stats: CheckerStats, - ): + ) -> Section: """render registered reports""" sect = Section("Report", f"{self.stats['statement']} statements analysed.") for checker in self.report_order(): diff --git a/pylint/reporters/ureports/base_writer.py b/pylint/reporters/ureports/base_writer.py index f0651f98cd..a7fd167079 100644 --- a/pylint/reporters/ureports/base_writer.py +++ b/pylint/reporters/ureports/base_writer.py @@ -18,7 +18,15 @@ import os import sys from io import StringIO -from typing import Iterator, TextIO +from typing import TYPE_CHECKING, Iterator, List, TextIO, Union + +if TYPE_CHECKING: + from pylint.reporters.ureports.nodes import ( + EvaluationSection, + Paragraph, + Section, + Table, + ) class BaseWriter: @@ -39,34 +47,36 @@ def format(self, layout, stream: TextIO = sys.stdout, encoding=None) -> None: layout.accept(self) self.end_format() - def format_children(self, layout): + def format_children( + self, layout: Union["EvaluationSection", "Paragraph", "Section"] + ) -> None: """recurse on the layout children and call their accept method (see the Visitor pattern) """ for child in getattr(layout, "children", ()): child.accept(self) - def writeln(self, string=""): + def writeln(self, string: str = "") -> None: """write a line in the output buffer""" self.write(string + os.linesep) - def write(self, string): + def write(self, string: str) -> None: """write a string in the output buffer""" self.out.write(string) - def begin_format(self): + def begin_format(self) -> None: """begin to format a layout""" self.section = 0 - def end_format(self): + def end_format(self) -> None: """finished to format a layout""" - def get_table_content(self, table): + def get_table_content(self, table: "Table") -> List[List[str]]: """trick to get table content without actually writing it return an aligned list of lists containing table cells values as string """ - result = [[]] + result: List[List[str]] = [[]] cols = table.cols for cell in self.compute_content(table): if cols == 0: diff --git a/pylint/reporters/ureports/nodes.py b/pylint/reporters/ureports/nodes.py index 02887612e1..1a0405777e 100644 --- a/pylint/reporters/ureports/nodes.py +++ b/pylint/reporters/ureports/nodes.py @@ -14,21 +14,21 @@ A micro report is a tree of layout and content objects. """ -from typing import Optional +from typing import Any, Iterable, Iterator, List, Optional, Union + +from pylint.reporters.ureports.text_writer import TextWriter class VNode: - def __init__(self, nid=None): - self.id = nid - # navigation - self.parent = None - self.children = [] - self.visitor_name = self.__class__.__name__.lower() - - def __iter__(self): + def __init__(self) -> None: + self.parent: Optional["BaseLayout"] = None + self.children: List["VNode"] = [] + self.visitor_name: str = self.__class__.__name__.lower() + + def __iter__(self) -> Iterator["VNode"]: return iter(self.children) - def accept(self, visitor, *args, **kwargs): + def accept(self, visitor: TextWriter, *args: Any, **kwargs: Any) -> None: func = getattr(visitor, f"visit_{self.visitor_name}") return func(self, *args, **kwargs) @@ -44,8 +44,8 @@ class BaseLayout(VNode): * children : components in this table (i.e. the table's cells) """ - def __init__(self, children=(), **kwargs): - super().__init__(**kwargs) + def __init__(self, children: Iterable[Union["Text", str]] = ()) -> None: + super().__init__() for child in children: if isinstance(child, VNode): self.append(child) @@ -63,14 +63,14 @@ def insert(self, index: int, child: VNode) -> None: self.children.insert(index, child) child.parent = self - def parents(self): + def parents(self) -> List["BaseLayout"]: """return the ancestor nodes""" assert self.parent is not self if self.parent is None: return [] return [self.parent] + self.parent.parents() - def add_text(self, text): + def add_text(self, text: str) -> None: """shortcut to add text data""" self.children.append(Text(text)) @@ -85,11 +85,8 @@ class Text(VNode): * data : the text value as an encoded or unicode string """ - def __init__(self, data, escaped=True, **kwargs): - super().__init__(**kwargs) - # if isinstance(data, unicode): - # data = data.encode('ascii') - assert isinstance(data, str), data.__class__ + def __init__(self, data: str, escaped: bool = True) -> None: + super().__init__() self.escaped = escaped self.data = data @@ -117,22 +114,28 @@ class Section(BaseLayout): as a first paragraph """ - def __init__(self, title=None, description=None, **kwargs): - super().__init__(**kwargs) + def __init__( + self, + title: Optional[str] = None, + description: Optional[str] = None, + children: Iterable[Union["Text", str]] = (), + ) -> None: + super().__init__(children=children) if description: self.insert(0, Paragraph([Text(description)])) if title: self.insert(0, Title(children=(title,))) - self.report_id: Optional[str] = None + self.report_id: str = "" # Used in ReportHandlerMixin.make_reports class EvaluationSection(Section): - def __init__(self, message, **kwargs): - super().__init__(**kwargs) + def __init__( + self, message: str, children: Iterable[Union["Text", str]] = () + ) -> None: + super().__init__(children=children) title = Paragraph() title.append(Text("-" * len(message))) self.append(title) - message_body = Paragraph() message_body.append(Text(message)) self.append(message_body) @@ -169,8 +172,15 @@ class Table(BaseLayout): * title : the table's optional title """ - def __init__(self, cols, title=None, rheaders=0, cheaders=0, **kwargs): - super().__init__(**kwargs) + def __init__( + self, + cols: int, + title: Optional[str] = None, + rheaders: int = 0, + cheaders: int = 0, + children: Iterable[Union["Text", str]] = (), + ) -> None: + super().__init__(children=children) assert isinstance(cols, int) self.cols = cols self.title = title diff --git a/pylint/reporters/ureports/text_writer.py b/pylint/reporters/ureports/text_writer.py index a48d73aac7..a00392bfe8 100644 --- a/pylint/reporters/ureports/text_writer.py +++ b/pylint/reporters/ureports/text_writer.py @@ -11,7 +11,20 @@ """Text formatting drivers for ureports""" -from pylint.reporters.ureports import BaseWriter +from typing import TYPE_CHECKING, List + +from pylint.reporters.ureports.base_writer import BaseWriter + +if TYPE_CHECKING: + from pylint.reporters.ureports.nodes import ( + EvaluationSection, + Paragraph, + Section, + Table, + Text, + Title, + VerbatimText, + ) TITLE_UNDERLINES = ["", "=", "-", "`", ".", "~", "^"] BULLETS = ["*", "-"] @@ -22,11 +35,11 @@ class TextWriter(BaseWriter): (ReStructured inspiration but not totally handled yet) """ - def begin_format(self): - super().begin_format() + def __init__(self): + super().__init__() self.list_level = 0 - def visit_section(self, layout): + def visit_section(self, layout: "Section") -> None: """display a section as text""" self.section += 1 self.writeln() @@ -34,14 +47,14 @@ def visit_section(self, layout): self.section -= 1 self.writeln() - def visit_evaluationsection(self, layout): + def visit_evaluationsection(self, layout: "EvaluationSection") -> None: """Display an evaluation section as a text.""" self.section += 1 self.format_children(layout) self.section -= 1 self.writeln() - def visit_title(self, layout): + def visit_title(self, layout: "Title") -> None: title = "".join(list(self.compute_content(layout))) self.writeln(title) try: @@ -49,12 +62,12 @@ def visit_title(self, layout): except IndexError: print("FIXME TITLE TOO DEEP. TURNING TITLE INTO TEXT") - def visit_paragraph(self, layout): + def visit_paragraph(self, layout: "Paragraph") -> None: """enter a paragraph""" self.format_children(layout) self.writeln() - def visit_table(self, layout): + def visit_table(self, layout: "Table") -> None: """display a table as text""" table_content = self.get_table_content(layout) # get columns width @@ -65,33 +78,36 @@ def visit_table(self, layout): self.default_table(layout, table_content, cols_width) self.writeln() - def default_table(self, layout, table_content, cols_width): + def default_table( + self, layout: "Table", table_content: List[List[str]], cols_width: List[int] + ) -> None: """format a table""" cols_width = [size + 1 for size in cols_width] format_strings = " ".join(["%%-%ss"] * len(cols_width)) - format_strings = format_strings % tuple(cols_width) - format_strings = format_strings.split(" ") + format_strings %= tuple(cols_width) + table_linesep = "\n+" + "+".join("-" * w for w in cols_width) + "+\n" headsep = "\n+" + "+".join("=" * w for w in cols_width) + "+\n" self.write(table_linesep) + split_strings = format_strings.split(" ") for index, line in enumerate(table_content): self.write("|") for line_index, at_index in enumerate(line): - self.write(format_strings[line_index] % at_index) + self.write(split_strings[line_index] % at_index) self.write("|") if index == 0 and layout.rheaders: self.write(headsep) else: self.write(table_linesep) - def visit_verbatimtext(self, layout): + def visit_verbatimtext(self, layout: "VerbatimText") -> None: """display a verbatim layout as text (so difficult ;)""" self.writeln("::\n") for line in layout.data.splitlines(): self.writeln(" " + line) self.writeln() - def visit_text(self, layout): + def visit_text(self, layout: "Text") -> None: """add some text""" self.write(f"{layout.data}")