From b3336c77378f88eb1428d6ff5b6e78bba1177f40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Tue, 21 Sep 2021 00:04:48 +0200 Subject: Add typing and refactor text reporter (#5041) --- pylint/reporters/base_reporter.py | 2 +- pylint/reporters/text.py | 206 ++++++++++++++++++++++++++------------ 2 files changed, 142 insertions(+), 66 deletions(-) diff --git a/pylint/reporters/base_reporter.py b/pylint/reporters/base_reporter.py index 6747a47fb..d2efc24eb 100644 --- a/pylint/reporters/base_reporter.py +++ b/pylint/reporters/base_reporter.py @@ -23,7 +23,7 @@ class BaseReporter: extension = "" - def __init__(self, output: TextIO = sys.stdout): + def __init__(self, output: Optional[TextIO] = None): self.linter: "PyLinter" self.section = 0 self.out: TextIO = output or sys.stdout diff --git a/pylint/reporters/text.py b/pylint/reporters/text.py index e941ba60d..6caa9b913 100644 --- a/pylint/reporters/text.py +++ b/pylint/reporters/text.py @@ -26,17 +26,44 @@ import os import sys import warnings -from typing import TYPE_CHECKING, Optional +from typing import ( + TYPE_CHECKING, + Dict, + NamedTuple, + Optional, + Set, + TextIO, + Tuple, + Union, + cast, + overload, +) -from pylint import utils from pylint.interfaces import IReporter from pylint.message import Message from pylint.reporters import BaseReporter from pylint.reporters.ureports.text_writer import TextWriter +from pylint.utils import _splitstrip if TYPE_CHECKING: + from pylint.lint import PyLinter from pylint.reporters.ureports.nodes import Section + +class MessageStyle(NamedTuple): + """Styling of a message""" + + color: Optional[str] + """The color name (see `ANSI_COLORS` for available values) + or the color number when 256 colors are available + """ + style: Tuple[str, ...] + """Tuple of style strings (see `ANSI_COLORS` for available values). + """ + + +ColorMappingDict = Dict[str, MessageStyle] + TITLE_UNDERLINES = ["", "=", "-", "."] ANSI_PREFIX = "\033[" @@ -64,64 +91,80 @@ ANSI_COLORS = { } -def _get_ansi_code(color=None, style=None): +def _get_ansi_code(msg_style: MessageStyle) -> str: """return ansi escape code corresponding to color and style - :type color: str or None - :param color: - the color name (see `ANSI_COLORS` for available values) - or the color number when 256 colors are available - - :type style: str or None - :param style: - style string (see `ANSI_COLORS` for available values). To get - several style effects at the same time, use a coma as separator. + :param msg_style: the message style :raise KeyError: if an unexistent color or style identifier is given - :rtype: str :return: the built escape code """ - ansi_code = [] - if style: - style_attrs = utils._splitstrip(style) - for effect in style_attrs: - ansi_code.append(ANSI_STYLES[effect]) - if color: - if color.isdigit(): + ansi_code = [ANSI_STYLES[effect] for effect in msg_style.style] + if msg_style.color: + if msg_style.color.isdigit(): ansi_code.extend(["38", "5"]) - ansi_code.append(color) + ansi_code.append(msg_style.color) else: - ansi_code.append(ANSI_COLORS[color]) + ansi_code.append(ANSI_COLORS[msg_style.color]) if ansi_code: return ANSI_PREFIX + ";".join(ansi_code) + ANSI_END return "" -def colorize_ansi(msg, color=None, style=None): +@overload +def colorize_ansi( + msg: str, + msg_style: Optional[MessageStyle] = None, +) -> str: + ... + + +@overload +def colorize_ansi( + msg: str, + msg_style: Optional[str] = None, + style: Optional[str] = None, + *, + color: Optional[str] = None, +) -> str: + # Remove for pylint 3.0 + ... + + +def colorize_ansi( + msg: str, + msg_style: Union[MessageStyle, str, None] = None, + style: Optional[str] = None, + **kwargs: Optional[str], +) -> str: """colorize message by wrapping it with ansi escape codes - :type msg: str or unicode :param msg: the message string to colorize - :type color: str or None - :param color: - the color identifier (see `ANSI_COLORS` for available values) + :param msg_style: the message style + or color (for backwards compatibility): the color of the message style - :type style: str or None - :param style: - style string (see `ANSI_COLORS` for available values). To get - several style effects at the same time, use a coma as separator. + :param style: the message's style elements, this will be deprecated - :raise KeyError: if an unexistent color or style identifier is given + :param kwargs: used to accept `color` parameter while it is being deprecated - :rtype: str or unicode :return: the ansi escaped string """ + # pylint: disable-next=fixme + # TODO: Remove DeprecationWarning and only accept MessageStyle as parameter + if not isinstance(msg_style, MessageStyle): + warnings.warn( + "In pylint 3.0, the colorize_ansi function of Text reporters will only accept a MessageStyle parameter", + DeprecationWarning, + ) + color = kwargs.get("color") + style_attrs = tuple(_splitstrip(style)) + msg_style = MessageStyle(color or msg_style, style_attrs) # If both color and style are not defined, then leave the text as is - if color is None and style is None: + if msg_style.color is None and len(msg_style.style) == 0: return msg - escape_code = _get_ansi_code(color, style) + escape_code = _get_ansi_code(msg_style) # If invalid (or unknown) color, don't wrap msg with ansi codes if escape_code: return f"{escape_code}{msg}{ANSI_RESET}" @@ -136,15 +179,15 @@ class TextReporter(BaseReporter): extension = "txt" line_format = "{path}:{line}:{column}: {msg_id}: {msg} ({symbol})" - def __init__(self, output=None): + def __init__(self, output: Optional[TextIO] = None) -> None: BaseReporter.__init__(self, output) - self._modules = set() + self._modules: Set[str] = set() self._template = self.line_format def on_set_current_module(self, module: str, filepath: Optional[str]) -> None: self._template = str(self.linter.config.msg_template or self._template) - def write_message(self, msg): + def write_message(self, msg: Message) -> None: """Convenience method to write a formatted message with class default template""" self.writeln(msg.format(self._template)) @@ -174,7 +217,7 @@ class ParseableTextReporter(TextReporter): name = "parseable" line_format = "{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}" - def __init__(self, output=None): + def __init__(self, output: Optional[TextIO] = None) -> None: warnings.warn( f"{self.name} output format is deprecated. This is equivalent to --msg-template={self.line_format}", DeprecationWarning, @@ -193,19 +236,59 @@ class ColorizedTextReporter(TextReporter): """Simple TextReporter that colorizes text output""" name = "colorized" - COLOR_MAPPING = { - "I": ("green", None), - "C": (None, "bold"), - "R": ("magenta", "bold, italic"), - "W": ("magenta", None), - "E": ("red", "bold"), - "F": ("red", "bold, underline"), - "S": ("yellow", "inverse"), # S stands for module Separator + COLOR_MAPPING: ColorMappingDict = { + "I": MessageStyle("green", ()), + "C": MessageStyle(None, ("bold",)), + "R": MessageStyle("magenta", ("bold", "italic")), + "W": MessageStyle("magenta", ()), + "E": MessageStyle("red", ("bold",)), + "F": MessageStyle("red", ("bold", "underline")), + "S": MessageStyle("yellow", ("inverse",)), # S stands for module Separator } - def __init__(self, output=None, color_mapping=None): + @overload + def __init__( + self, + output: Optional[TextIO] = None, + color_mapping: Optional[ColorMappingDict] = None, + ) -> None: + ... + + @overload + def __init__( + self, + output: Optional[TextIO] = None, + color_mapping: Optional[Dict[str, Tuple[Optional[str], Optional[str]]]] = None, + ) -> None: + # Remove for pylint 3.0 + ... + + def __init__( + self, + output: Optional[TextIO] = None, + color_mapping: Union[ + ColorMappingDict, Dict[str, Tuple[Optional[str], Optional[str]]], None + ] = None, + ) -> None: TextReporter.__init__(self, output) - self.color_mapping = color_mapping or dict(ColorizedTextReporter.COLOR_MAPPING) + # pylint: disable-next=fixme + # TODO: Remove DeprecationWarning and only accept ColorMappingDict as color_mapping parameter + if color_mapping and not isinstance( + list(color_mapping.values())[0], MessageStyle + ): + warnings.warn( + "In pylint 3.0, the ColoreziedTextReporter will only accept ColorMappingDict as color_mapping parameter", + DeprecationWarning, + ) + temp_color_mapping: ColorMappingDict = {} + for key, value in color_mapping.items(): + color = value[0] + style_attrs = tuple(_splitstrip(value[1])) + temp_color_mapping[key] = MessageStyle(color, style_attrs) + color_mapping = temp_color_mapping + else: + color_mapping = cast(Optional[ColorMappingDict], color_mapping) + self.color_mapping = color_mapping or ColorizedTextReporter.COLOR_MAPPING ansi_terms = ["xterm-16color", "xterm-256color"] if os.environ.get("TERM") not in ansi_terms: if sys.platform == "win32": @@ -214,41 +297,34 @@ class ColorizedTextReporter(TextReporter): self.out = colorama.AnsiToWin32(self.out) - def _get_decoration(self, msg_id): - """Returns the tuple color, style associated with msg_id as defined - in self.color_mapping - """ - try: - return self.color_mapping[msg_id[0]] - except KeyError: - return None, None + def _get_decoration(self, msg_id: str) -> MessageStyle: + """Returns the message style as defined in self.color_mapping""" + return self.color_mapping.get(msg_id[0]) or MessageStyle(None, ()) def handle_message(self, msg: Message) -> None: """manage message of different types, and colorize output using ansi escape codes """ if msg.module not in self._modules: - color, style = self._get_decoration("S") + msg_style = self._get_decoration("S") if msg.module: - modsep = colorize_ansi( - f"************* Module {msg.module}", color, style - ) + modsep = colorize_ansi(f"************* Module {msg.module}", msg_style) else: - modsep = colorize_ansi(f"************* {msg.module}", color, style) + modsep = colorize_ansi(f"************* {msg.module}", msg_style) self.writeln(modsep) self._modules.add(msg.module) - color, style = self._get_decoration(msg.C) + msg_style = self._get_decoration(msg.C) msg = msg._replace( **{ - attr: colorize_ansi(getattr(msg, attr), color, style) + attr: colorize_ansi(getattr(msg, attr), msg_style) for attr in ("msg", "symbol", "category", "C") } ) self.write_message(msg) -def register(linter): +def register(linter: "PyLinter") -> None: """Register the reporter classes with the linter.""" linter.register_reporter(TextReporter) linter.register_reporter(ParseableTextReporter) -- cgit v1.2.1