text.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. # Copyright (c) 2006-2007, 2010-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
  2. # Copyright (c) 2012-2014 Google, Inc.
  3. # Copyright (c) 2014 Brett Cannon <brett@python.org>
  4. # Copyright (c) 2014 Arun Persaud <arun@nubati.net>
  5. # Copyright (c) 2015-2018, 2020 Claudiu Popa <pcmanticore@gmail.com>
  6. # Copyright (c) 2015 Florian Bruhin <me@the-compiler.org>
  7. # Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
  8. # Copyright (c) 2016 y2kbugger <y2kbugger@users.noreply.github.com>
  9. # Copyright (c) 2018-2019 Nick Drozd <nicholasdrozd@gmail.com>
  10. # Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com>
  11. # Copyright (c) 2018 Jace Browning <jacebrowning@gmail.com>
  12. # Copyright (c) 2019-2021 Pierre Sassoulas <pierre.sassoulas@gmail.com>
  13. # Copyright (c) 2019 Hugo van Kemenade <hugovk@users.noreply.github.com>
  14. # Copyright (c) 2020 hippo91 <guillaume.peillex@gmail.com>
  15. # Copyright (c) 2021 Daniël van Noord <13665637+DanielNoord@users.noreply.github.com>
  16. # Copyright (c) 2021 Marc Mueller <30130371+cdce8p@users.noreply.github.com>
  17. # Copyright (c) 2021 bot <bot@noreply.github.com>
  18. # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  19. # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
  20. """Plain text reporters:
  21. :text: the default one grouping messages by module
  22. :colorized: an ANSI colorized text reporter
  23. """
  24. import os
  25. import re
  26. import sys
  27. import warnings
  28. from typing import (
  29. TYPE_CHECKING,
  30. Dict,
  31. NamedTuple,
  32. Optional,
  33. Set,
  34. TextIO,
  35. Tuple,
  36. Union,
  37. cast,
  38. overload,
  39. )
  40. from pylint.interfaces import IReporter
  41. from pylint.message import Message
  42. from pylint.reporters import BaseReporter
  43. from pylint.reporters.ureports.text_writer import TextWriter
  44. from pylint.utils import _splitstrip
  45. if TYPE_CHECKING:
  46. from pylint.lint import PyLinter
  47. from pylint.reporters.ureports.nodes import Section
  48. class MessageStyle(NamedTuple):
  49. """Styling of a message"""
  50. color: Optional[str]
  51. """The color name (see `ANSI_COLORS` for available values)
  52. or the color number when 256 colors are available
  53. """
  54. style: Tuple[str, ...] = ()
  55. """Tuple of style strings (see `ANSI_COLORS` for available values).
  56. """
  57. ColorMappingDict = Dict[str, MessageStyle]
  58. TITLE_UNDERLINES = ["", "=", "-", "."]
  59. ANSI_PREFIX = "\033["
  60. ANSI_END = "m"
  61. ANSI_RESET = "\033[0m"
  62. ANSI_STYLES = {
  63. "reset": "0",
  64. "bold": "1",
  65. "italic": "3",
  66. "underline": "4",
  67. "blink": "5",
  68. "inverse": "7",
  69. "strike": "9",
  70. }
  71. ANSI_COLORS = {
  72. "reset": "0",
  73. "black": "30",
  74. "red": "31",
  75. "green": "32",
  76. "yellow": "33",
  77. "blue": "34",
  78. "magenta": "35",
  79. "cyan": "36",
  80. "white": "37",
  81. }
  82. def _get_ansi_code(msg_style: MessageStyle) -> str:
  83. """return ansi escape code corresponding to color and style
  84. :param msg_style: the message style
  85. :raise KeyError: if an unexistent color or style identifier is given
  86. :return: the built escape code
  87. """
  88. ansi_code = [ANSI_STYLES[effect] for effect in msg_style.style]
  89. if msg_style.color:
  90. if msg_style.color.isdigit():
  91. ansi_code.extend(["38", "5"])
  92. ansi_code.append(msg_style.color)
  93. else:
  94. ansi_code.append(ANSI_COLORS[msg_style.color])
  95. if ansi_code:
  96. return ANSI_PREFIX + ";".join(ansi_code) + ANSI_END
  97. return ""
  98. @overload
  99. def colorize_ansi(
  100. msg: str,
  101. msg_style: Optional[MessageStyle] = None,
  102. ) -> str:
  103. ...
  104. @overload
  105. def colorize_ansi(
  106. msg: str,
  107. msg_style: Optional[str] = None,
  108. style: Optional[str] = None,
  109. *,
  110. color: Optional[str] = None,
  111. ) -> str:
  112. # Remove for pylint 3.0
  113. ...
  114. def colorize_ansi(
  115. msg: str,
  116. msg_style: Union[MessageStyle, str, None] = None,
  117. style: Optional[str] = None,
  118. **kwargs: Optional[str],
  119. ) -> str:
  120. r"""colorize message by wrapping it with ansi escape codes
  121. :param msg: the message string to colorize
  122. :param msg_style: the message style
  123. or color (for backwards compatibility): the color of the message style
  124. :param style: the message's style elements, this will be deprecated
  125. :param \**kwargs: used to accept `color` parameter while it is being deprecated
  126. :return: the ansi escaped string
  127. """
  128. # pylint: disable-next=fixme
  129. # TODO: Remove DeprecationWarning and only accept MessageStyle as parameter
  130. if not isinstance(msg_style, MessageStyle):
  131. warnings.warn(
  132. "In pylint 3.0, the colorize_ansi function of Text reporters will only accept a MessageStyle parameter",
  133. DeprecationWarning,
  134. )
  135. color = kwargs.get("color")
  136. style_attrs = tuple(_splitstrip(style))
  137. msg_style = MessageStyle(color or msg_style, style_attrs)
  138. # If both color and style are not defined, then leave the text as is
  139. if msg_style.color is None and len(msg_style.style) == 0:
  140. return msg
  141. escape_code = _get_ansi_code(msg_style)
  142. # If invalid (or unknown) color, don't wrap msg with ansi codes
  143. if escape_code:
  144. return f"{escape_code}{msg}{ANSI_RESET}"
  145. return msg
  146. class TextReporter(BaseReporter):
  147. """Reports messages and layouts in plain text"""
  148. __implements__ = IReporter
  149. name = "text"
  150. extension = "txt"
  151. line_format = "{path}:{line}:{column}: {msg_id}: {msg} ({symbol})"
  152. def __init__(self, output: Optional[TextIO] = None) -> None:
  153. super().__init__(output)
  154. self._modules: Set[str] = set()
  155. self._template = self.line_format
  156. self._fixed_template = self.line_format
  157. """The output format template with any unrecognized arguments removed"""
  158. def on_set_current_module(self, module: str, filepath: Optional[str]) -> None:
  159. """Set the format template to be used and check for unrecognized arguments."""
  160. template = str(self.linter.config.msg_template or self._template)
  161. # Return early if the template is the same as the previous one
  162. if template == self._template:
  163. return
  164. # Set template to the currently selected template
  165. self._template = template
  166. # Check to see if all parameters in the template are attributes of the Message
  167. arguments = re.findall(r"\{(.+?)(:.*)?\}", template)
  168. for argument in arguments:
  169. if argument[0] not in Message._fields:
  170. warnings.warn(
  171. f"Don't recognize the argument '{argument[0]}' in the --msg-template. "
  172. "Are you sure it is supported on the current version of pylint?"
  173. )
  174. template = re.sub(r"\{" + argument[0] + r"(:.*?)?\}", "", template)
  175. self._fixed_template = template
  176. def write_message(self, msg: Message) -> None:
  177. """Convenience method to write a formatted message with class default template"""
  178. self_dict = msg._asdict()
  179. for key in ("end_line", "end_column"):
  180. self_dict[key] = self_dict[key] or ""
  181. self.writeln(self._fixed_template.format(**self_dict))
  182. def handle_message(self, msg: Message) -> None:
  183. """manage message of different type and in the context of path"""
  184. if msg.module not in self._modules:
  185. if msg.module:
  186. self.writeln(f"************* Module {msg.module}")
  187. self._modules.add(msg.module)
  188. else:
  189. self.writeln("************* ")
  190. self.write_message(msg)
  191. def _display(self, layout: "Section") -> None:
  192. """launch layouts display"""
  193. print(file=self.out)
  194. TextWriter().format(layout, self.out)
  195. class ParseableTextReporter(TextReporter):
  196. """a reporter very similar to TextReporter, but display messages in a form
  197. recognized by most text editors :
  198. <filename>:<linenum>:<msg>
  199. """
  200. name = "parseable"
  201. line_format = "{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}"
  202. def __init__(self, output: Optional[TextIO] = None) -> None:
  203. warnings.warn(
  204. f"{self.name} output format is deprecated. This is equivalent to --msg-template={self.line_format}",
  205. DeprecationWarning,
  206. )
  207. super().__init__(output)
  208. class VSTextReporter(ParseableTextReporter):
  209. """Visual studio text reporter"""
  210. name = "msvs"
  211. line_format = "{path}({line}): [{msg_id}({symbol}){obj}] {msg}"
  212. class ColorizedTextReporter(TextReporter):
  213. """Simple TextReporter that colorizes text output"""
  214. name = "colorized"
  215. COLOR_MAPPING: ColorMappingDict = {
  216. "I": MessageStyle("green"),
  217. "C": MessageStyle(None, ("bold",)),
  218. "R": MessageStyle("magenta", ("bold", "italic")),
  219. "W": MessageStyle("magenta"),
  220. "E": MessageStyle("red", ("bold",)),
  221. "F": MessageStyle("red", ("bold", "underline")),
  222. "S": MessageStyle("yellow", ("inverse",)), # S stands for module Separator
  223. }
  224. @overload
  225. def __init__(
  226. self,
  227. output: Optional[TextIO] = None,
  228. color_mapping: Optional[ColorMappingDict] = None,
  229. ) -> None:
  230. ...
  231. @overload
  232. def __init__(
  233. self,
  234. output: Optional[TextIO] = None,
  235. color_mapping: Optional[Dict[str, Tuple[Optional[str], Optional[str]]]] = None,
  236. ) -> None:
  237. # Remove for pylint 3.0
  238. ...
  239. def __init__(
  240. self,
  241. output: Optional[TextIO] = None,
  242. color_mapping: Union[
  243. ColorMappingDict, Dict[str, Tuple[Optional[str], Optional[str]]], None
  244. ] = None,
  245. ) -> None:
  246. super().__init__(output)
  247. # pylint: disable-next=fixme
  248. # TODO: Remove DeprecationWarning and only accept ColorMappingDict as color_mapping parameter
  249. if color_mapping and not isinstance(
  250. list(color_mapping.values())[0], MessageStyle
  251. ):
  252. warnings.warn(
  253. "In pylint 3.0, the ColoreziedTextReporter will only accept ColorMappingDict as color_mapping parameter",
  254. DeprecationWarning,
  255. )
  256. temp_color_mapping: ColorMappingDict = {}
  257. for key, value in color_mapping.items():
  258. color = value[0]
  259. style_attrs = tuple(_splitstrip(value[1]))
  260. temp_color_mapping[key] = MessageStyle(color, style_attrs)
  261. color_mapping = temp_color_mapping
  262. else:
  263. color_mapping = cast(Optional[ColorMappingDict], color_mapping)
  264. self.color_mapping = color_mapping or ColorizedTextReporter.COLOR_MAPPING
  265. ansi_terms = ["xterm-16color", "xterm-256color"]
  266. if os.environ.get("TERM") not in ansi_terms:
  267. if sys.platform == "win32":
  268. # pylint: disable=import-outside-toplevel
  269. import colorama
  270. self.out = colorama.AnsiToWin32(self.out)
  271. def _get_decoration(self, msg_id: str) -> MessageStyle:
  272. """Returns the message style as defined in self.color_mapping"""
  273. return self.color_mapping.get(msg_id[0]) or MessageStyle(None)
  274. def handle_message(self, msg: Message) -> None:
  275. """manage message of different types, and colorize output
  276. using ansi escape codes
  277. """
  278. if msg.module not in self._modules:
  279. msg_style = self._get_decoration("S")
  280. if msg.module:
  281. modsep = colorize_ansi(f"************* Module {msg.module}", msg_style)
  282. else:
  283. modsep = colorize_ansi(f"************* {msg.module}", msg_style)
  284. self.writeln(modsep)
  285. self._modules.add(msg.module)
  286. msg_style = self._get_decoration(msg.C)
  287. msg = msg._replace(
  288. **{
  289. attr: colorize_ansi(getattr(msg, attr), msg_style)
  290. for attr in ("msg", "symbol", "category", "C")
  291. }
  292. )
  293. self.write_message(msg)
  294. def register(linter: "PyLinter") -> None:
  295. """Register the reporter classes with the linter."""
  296. linter.register_reporter(TextReporter)
  297. linter.register_reporter(ParseableTextReporter)
  298. linter.register_reporter(VSTextReporter)
  299. linter.register_reporter(ColorizedTextReporter)