text.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  2. # For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE
  3. # Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt
  4. """Plain text reporters:.
  5. :text: the default one grouping messages by module
  6. :colorized: an ANSI colorized text reporter
  7. """
  8. from __future__ import annotations
  9. import os
  10. import re
  11. import sys
  12. import warnings
  13. from dataclasses import asdict, fields
  14. from typing import TYPE_CHECKING, Dict, NamedTuple, Optional, TextIO, cast, overload
  15. from pylint.message import Message
  16. from pylint.reporters import BaseReporter
  17. from pylint.reporters.ureports.text_writer import TextWriter
  18. from pylint.utils import _splitstrip
  19. if TYPE_CHECKING:
  20. from pylint.lint import PyLinter
  21. from pylint.reporters.ureports.nodes import Section
  22. class MessageStyle(NamedTuple):
  23. """Styling of a message."""
  24. color: str | None
  25. """The color name (see `ANSI_COLORS` for available values)
  26. or the color number when 256 colors are available.
  27. """
  28. style: tuple[str, ...] = ()
  29. """Tuple of style strings (see `ANSI_COLORS` for available values)."""
  30. ColorMappingDict = Dict[str, MessageStyle]
  31. TITLE_UNDERLINES = ["", "=", "-", "."]
  32. ANSI_PREFIX = "\033["
  33. ANSI_END = "m"
  34. ANSI_RESET = "\033[0m"
  35. ANSI_STYLES = {
  36. "reset": "0",
  37. "bold": "1",
  38. "italic": "3",
  39. "underline": "4",
  40. "blink": "5",
  41. "inverse": "7",
  42. "strike": "9",
  43. }
  44. ANSI_COLORS = {
  45. "reset": "0",
  46. "black": "30",
  47. "red": "31",
  48. "green": "32",
  49. "yellow": "33",
  50. "blue": "34",
  51. "magenta": "35",
  52. "cyan": "36",
  53. "white": "37",
  54. }
  55. MESSAGE_FIELDS = {i.name for i in fields(Message)}
  56. """All fields of the Message class."""
  57. def _get_ansi_code(msg_style: MessageStyle) -> str:
  58. """Return ANSI escape code corresponding to color and style.
  59. :param msg_style: the message style
  60. :raise KeyError: if a nonexistent color or style identifier is given
  61. :return: the built escape code
  62. """
  63. ansi_code = [ANSI_STYLES[effect] for effect in msg_style.style]
  64. if msg_style.color:
  65. if msg_style.color.isdigit():
  66. ansi_code.extend(["38", "5"])
  67. ansi_code.append(msg_style.color)
  68. else:
  69. ansi_code.append(ANSI_COLORS[msg_style.color])
  70. if ansi_code:
  71. return ANSI_PREFIX + ";".join(ansi_code) + ANSI_END
  72. return ""
  73. @overload
  74. def colorize_ansi(
  75. msg: str,
  76. msg_style: MessageStyle | None = ...,
  77. ) -> str:
  78. ...
  79. @overload
  80. def colorize_ansi(
  81. msg: str,
  82. msg_style: str | None = ...,
  83. style: str = ...,
  84. *,
  85. color: str | None = ...,
  86. ) -> str:
  87. # Remove for pylint 3.0
  88. ...
  89. def colorize_ansi(
  90. msg: str,
  91. msg_style: MessageStyle | str | None = None,
  92. style: str = "",
  93. **kwargs: str | None,
  94. ) -> str:
  95. r"""colorize message by wrapping it with ANSI escape codes
  96. :param msg: the message string to colorize
  97. :param msg_style: the message style
  98. or color (for backwards compatibility): the color of the message style
  99. :param style: the message's style elements, this will be deprecated
  100. :param \**kwargs: used to accept `color` parameter while it is being deprecated
  101. :return: the ANSI escaped string
  102. """
  103. # TODO: 3.0: Remove deprecated typing and only accept MessageStyle as parameter
  104. if not isinstance(msg_style, MessageStyle):
  105. warnings.warn(
  106. "In pylint 3.0, the colorize_ansi function of Text reporters will only accept a "
  107. "MessageStyle parameter",
  108. DeprecationWarning,
  109. stacklevel=2,
  110. )
  111. color = kwargs.get("color")
  112. style_attrs = tuple(_splitstrip(style))
  113. msg_style = MessageStyle(color or msg_style, style_attrs)
  114. # If both color and style are not defined, then leave the text as is
  115. if msg_style.color is None and len(msg_style.style) == 0:
  116. return msg
  117. escape_code = _get_ansi_code(msg_style)
  118. # If invalid (or unknown) color, don't wrap msg with ANSI codes
  119. if escape_code:
  120. return f"{escape_code}{msg}{ANSI_RESET}"
  121. return msg
  122. def make_header(msg: Message) -> str:
  123. return f"************* Module {msg.module}"
  124. class TextReporter(BaseReporter):
  125. """Reports messages and layouts in plain text."""
  126. name = "text"
  127. extension = "txt"
  128. line_format = "{path}:{line}:{column}: {msg_id}: {msg} ({symbol})"
  129. def __init__(self, output: TextIO | None = None) -> None:
  130. super().__init__(output)
  131. self._modules: set[str] = set()
  132. self._template = self.line_format
  133. self._fixed_template = self.line_format
  134. """The output format template with any unrecognized arguments removed."""
  135. def on_set_current_module(self, module: str, filepath: str | None) -> None:
  136. """Set the format template to be used and check for unrecognized arguments."""
  137. template = str(self.linter.config.msg_template or self._template)
  138. # Return early if the template is the same as the previous one
  139. if template == self._template:
  140. return
  141. # Set template to the currently selected template
  142. self._template = template
  143. # Check to see if all parameters in the template are attributes of the Message
  144. arguments = re.findall(r"\{(\w+?)(:.*)?\}", template)
  145. for argument in arguments:
  146. if argument[0] not in MESSAGE_FIELDS:
  147. warnings.warn(
  148. f"Don't recognize the argument '{argument[0]}' in the --msg-template. "
  149. "Are you sure it is supported on the current version of pylint?"
  150. )
  151. template = re.sub(r"\{" + argument[0] + r"(:.*?)?\}", "", template)
  152. self._fixed_template = template
  153. def write_message(self, msg: Message) -> None:
  154. """Convenience method to write a formatted message with class default
  155. template.
  156. """
  157. self_dict = asdict(msg)
  158. for key in ("end_line", "end_column"):
  159. self_dict[key] = self_dict[key] or ""
  160. self.writeln(self._fixed_template.format(**self_dict))
  161. def handle_message(self, msg: Message) -> None:
  162. """Manage message of different type and in the context of path."""
  163. if msg.module not in self._modules:
  164. self.writeln(make_header(msg))
  165. self._modules.add(msg.module)
  166. self.write_message(msg)
  167. def _display(self, layout: Section) -> None:
  168. """Launch layouts display."""
  169. print(file=self.out)
  170. TextWriter().format(layout, self.out)
  171. class NoHeaderReporter(TextReporter):
  172. """Reports messages and layouts in plain text without a module header."""
  173. name = "no-header"
  174. def handle_message(self, msg: Message) -> None:
  175. """Write message(s) without module header."""
  176. if msg.module not in self._modules:
  177. self._modules.add(msg.module)
  178. self.write_message(msg)
  179. class ParseableTextReporter(TextReporter):
  180. """A reporter very similar to TextReporter, but display messages in a form
  181. recognized by most text editors :
  182. <filename>:<linenum>:<msg>
  183. """
  184. name = "parseable"
  185. line_format = "{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}"
  186. def __init__(self, output: TextIO | None = None) -> None:
  187. warnings.warn(
  188. f"{self.name} output format is deprecated. This is equivalent to --msg-template={self.line_format}",
  189. DeprecationWarning,
  190. stacklevel=2,
  191. )
  192. super().__init__(output)
  193. class VSTextReporter(ParseableTextReporter):
  194. """Visual studio text reporter."""
  195. name = "msvs"
  196. line_format = "{path}({line}): [{msg_id}({symbol}){obj}] {msg}"
  197. class ColorizedTextReporter(TextReporter):
  198. """Simple TextReporter that colorizes text output."""
  199. name = "colorized"
  200. COLOR_MAPPING: ColorMappingDict = {
  201. "I": MessageStyle("green"),
  202. "C": MessageStyle(None, ("bold",)),
  203. "R": MessageStyle("magenta", ("bold", "italic")),
  204. "W": MessageStyle("magenta"),
  205. "E": MessageStyle("red", ("bold",)),
  206. "F": MessageStyle("red", ("bold", "underline")),
  207. "S": MessageStyle("yellow", ("inverse",)), # S stands for module Separator
  208. }
  209. def __init__(
  210. self,
  211. output: TextIO | None = None,
  212. color_mapping: (
  213. ColorMappingDict | dict[str, tuple[str | None, str]] | None
  214. ) = None,
  215. ) -> None:
  216. super().__init__(output)
  217. # TODO: 3.0: Remove deprecated typing and only accept ColorMappingDict as
  218. # color_mapping parameter
  219. if color_mapping and not isinstance(
  220. list(color_mapping.values())[0], MessageStyle
  221. ):
  222. warnings.warn(
  223. "In pylint 3.0, the ColorizedTextReporter will only accept ColorMappingDict as "
  224. "color_mapping parameter",
  225. DeprecationWarning,
  226. stacklevel=2,
  227. )
  228. temp_color_mapping: ColorMappingDict = {}
  229. for key, value in color_mapping.items():
  230. color = value[0]
  231. style_attrs = tuple(_splitstrip(value[1])) # type: ignore[arg-type]
  232. temp_color_mapping[key] = MessageStyle(color, style_attrs)
  233. color_mapping = temp_color_mapping
  234. else:
  235. color_mapping = cast(Optional[ColorMappingDict], color_mapping)
  236. self.color_mapping = color_mapping or ColorizedTextReporter.COLOR_MAPPING
  237. ansi_terms = ["xterm-16color", "xterm-256color"]
  238. if os.environ.get("TERM") not in ansi_terms:
  239. if sys.platform == "win32":
  240. # pylint: disable=import-outside-toplevel
  241. import colorama
  242. self.out = colorama.AnsiToWin32(self.out)
  243. def _get_decoration(self, msg_id: str) -> MessageStyle:
  244. """Returns the message style as defined in self.color_mapping."""
  245. return self.color_mapping.get(msg_id[0]) or MessageStyle(None)
  246. def handle_message(self, msg: Message) -> None:
  247. """Manage message of different types, and colorize output
  248. using ANSI escape codes.
  249. """
  250. if msg.module not in self._modules:
  251. msg_style = self._get_decoration("S")
  252. modsep = colorize_ansi(make_header(msg), msg_style)
  253. self.writeln(modsep)
  254. self._modules.add(msg.module)
  255. msg_style = self._get_decoration(msg.C)
  256. msg.msg = colorize_ansi(msg.msg, msg_style)
  257. msg.symbol = colorize_ansi(msg.symbol, msg_style)
  258. msg.category = colorize_ansi(msg.category, msg_style)
  259. msg.C = colorize_ansi(msg.C, msg_style)
  260. self.write_message(msg)
  261. def register(linter: PyLinter) -> None:
  262. linter.register_reporter(TextReporter)
  263. linter.register_reporter(NoHeaderReporter)
  264. linter.register_reporter(ParseableTextReporter)
  265. linter.register_reporter(VSTextReporter)
  266. linter.register_reporter(ColorizedTextReporter)