utils.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  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. from __future__ import annotations
  5. import codecs
  6. import os
  7. import re
  8. import sys
  9. import textwrap
  10. import tokenize
  11. import warnings
  12. from collections import deque
  13. from collections.abc import Iterable, Sequence
  14. from io import BufferedReader, BytesIO
  15. from re import Pattern
  16. from typing import TYPE_CHECKING, Any, Literal, TextIO, TypeVar
  17. from astroid import modutils, nodes
  18. from pylint.constants import PY_EXTS
  19. from pylint.typing import OptionDict
  20. if TYPE_CHECKING:
  21. from pylint.lint import PyLinter
  22. DEFAULT_LINE_LENGTH = 79
  23. # These are types used to overload get_global_option() and refer to the options type
  24. GLOBAL_OPTION_BOOL = Literal[
  25. "analyse-fallback-blocks",
  26. "allow-global-unused-variables",
  27. "prefer-stubs",
  28. ]
  29. GLOBAL_OPTION_INT = Literal["max-line-length", "docstring-min-length"]
  30. GLOBAL_OPTION_LIST = Literal["ignored-modules"]
  31. GLOBAL_OPTION_PATTERN = Literal[
  32. "no-docstring-rgx",
  33. "dummy-variables-rgx",
  34. "ignored-argument-names",
  35. "mixin-class-rgx",
  36. ]
  37. GLOBAL_OPTION_PATTERN_LIST = Literal["exclude-too-few-public-methods", "ignore-paths"]
  38. GLOBAL_OPTION_TUPLE_INT = Literal["py-version"]
  39. GLOBAL_OPTION_NAMES = (
  40. GLOBAL_OPTION_BOOL
  41. | GLOBAL_OPTION_INT
  42. | GLOBAL_OPTION_LIST
  43. | GLOBAL_OPTION_PATTERN
  44. | GLOBAL_OPTION_PATTERN_LIST
  45. | GLOBAL_OPTION_TUPLE_INT
  46. )
  47. T_GlobalOptionReturnTypes = TypeVar(
  48. "T_GlobalOptionReturnTypes",
  49. bool,
  50. int,
  51. list[str],
  52. Pattern[str],
  53. list[Pattern[str]],
  54. tuple[int, ...],
  55. )
  56. def normalize_text(
  57. text: str, line_len: int = DEFAULT_LINE_LENGTH, indent: str = ""
  58. ) -> str:
  59. """Wrap the text on the given line length."""
  60. return "\n".join(
  61. textwrap.wrap(
  62. text, width=line_len, initial_indent=indent, subsequent_indent=indent
  63. )
  64. )
  65. CMPS = ["=", "-", "+"]
  66. # py3k has no more cmp builtin
  67. def cmp(a: float, b: float) -> int:
  68. return (a > b) - (a < b)
  69. def diff_string(old: float, new: float) -> str:
  70. """Given an old and new value, return a string representing the difference."""
  71. diff = abs(old - new)
  72. diff_str = f"{CMPS[cmp(old, new)]}{(diff and f'{diff:.2f}') or ''}"
  73. return diff_str
  74. def get_module_and_frameid(node: nodes.NodeNG) -> tuple[str, str]:
  75. """Return the module name and the frame id in the module."""
  76. frame = node.frame()
  77. module, obj = "", []
  78. while frame:
  79. if isinstance(frame, nodes.Module):
  80. module = frame.name
  81. else:
  82. obj.append(getattr(frame, "name", "<lambda>"))
  83. try:
  84. frame = frame.parent.frame()
  85. except AttributeError:
  86. break
  87. return module, ".".join(reversed(obj))
  88. def get_rst_title(title: str, character: str) -> str:
  89. """Permit to get a title formatted as ReStructuredText test (underlined with a
  90. chosen character).
  91. """
  92. return f"{title}\n{character * len(title)}\n"
  93. def get_rst_section(
  94. section: str | None,
  95. options: list[tuple[str, OptionDict, Any]],
  96. doc: str | None = None,
  97. ) -> str:
  98. """Format an option's section using as a ReStructuredText formatted output."""
  99. result = ""
  100. if section:
  101. result += get_rst_title(section, "'")
  102. if doc:
  103. formatted_doc = normalize_text(doc)
  104. result += f"{formatted_doc}\n\n"
  105. for optname, optdict, value in options:
  106. help_opt = optdict.get("help")
  107. result += f":{optname}:\n"
  108. if help_opt:
  109. assert isinstance(help_opt, str)
  110. formatted_help = normalize_text(help_opt, indent=" ")
  111. result += f"{formatted_help}\n"
  112. if value and optname != "py-version":
  113. value = str(_format_option_value(optdict, value))
  114. result += f"\n Default: ``{value.replace('`` ', '```` ``')}``\n"
  115. return result
  116. def decoding_stream(
  117. stream: BufferedReader | BytesIO,
  118. encoding: str,
  119. errors: Literal["strict"] = "strict",
  120. ) -> codecs.StreamReader:
  121. try:
  122. reader_cls = codecs.getreader(encoding or sys.getdefaultencoding())
  123. except LookupError:
  124. reader_cls = codecs.getreader(sys.getdefaultencoding())
  125. return reader_cls(stream, errors)
  126. def tokenize_module(node: nodes.Module) -> list[tokenize.TokenInfo]:
  127. with node.stream() as stream:
  128. readline = stream.readline
  129. return list(tokenize.tokenize(readline))
  130. def register_plugins(linter: PyLinter, directory: str) -> None:
  131. """Load all module and package in the given directory, looking for a
  132. 'register' function in each one, used to register pylint checkers.
  133. """
  134. imported = {}
  135. for filename in os.listdir(directory):
  136. base, extension = os.path.splitext(filename)
  137. if base in imported or base == "__pycache__":
  138. continue
  139. if (extension in PY_EXTS and base != "__init__") or (
  140. not extension
  141. and os.path.isdir(os.path.join(directory, base))
  142. and not filename.startswith(".")
  143. ):
  144. try:
  145. module = modutils.load_module_from_file(
  146. os.path.join(directory, filename)
  147. )
  148. except ValueError:
  149. # empty module name (usually Emacs auto-save files)
  150. continue
  151. except ImportError as exc:
  152. print(f"Problem importing module {filename}: {exc}", file=sys.stderr)
  153. else:
  154. if hasattr(module, "register"):
  155. module.register(linter)
  156. imported[base] = 1
  157. def _splitstrip(string: str, sep: str = ",") -> list[str]:
  158. r"""Return a list of stripped string by splitting the string given as
  159. argument on `sep` (',' by default), empty strings are discarded.
  160. >>> _splitstrip('a, b, c , 4,,')
  161. ['a', 'b', 'c', '4']
  162. >>> _splitstrip('a')
  163. ['a']
  164. >>> _splitstrip('a,\nb,\nc,')
  165. ['a', 'b', 'c']
  166. :type string: str or unicode
  167. :param string: a csv line
  168. :type sep: str or unicode
  169. :param sep: field separator, default to the comma (',')
  170. :rtype: str or unicode
  171. :return: the unquoted string (or the input string if it wasn't quoted)
  172. """
  173. return [word.strip() for word in string.split(sep) if word.strip()]
  174. def _unquote(string: str) -> str:
  175. """Remove optional quotes (simple or double) from the string.
  176. :param string: an optionally quoted string
  177. :return: the unquoted string (or the input string if it wasn't quoted)
  178. """
  179. if not string:
  180. return string
  181. if string[0] in "\"'":
  182. string = string[1:]
  183. if string[-1] in "\"'":
  184. string = string[:-1]
  185. return string
  186. def _check_csv(value: list[str] | tuple[str] | str) -> Sequence[str]:
  187. if isinstance(value, (list, tuple)):
  188. return value
  189. return _splitstrip(value)
  190. def _check_regexp_csv(value: list[str] | tuple[str] | str) -> Iterable[str]:
  191. r"""Split a comma-separated list of regexps, taking care to avoid splitting
  192. a regex employing a comma as quantifier, as in `\d{1,2}`.
  193. """
  194. if isinstance(value, (list, tuple)):
  195. yield from value
  196. else:
  197. # None is a sentinel value here
  198. regexps: deque[deque[str] | None] = deque([None])
  199. open_braces = False
  200. for char in value:
  201. if char == "{":
  202. open_braces = True
  203. elif char == "}" and open_braces:
  204. open_braces = False
  205. if char == "," and not open_braces:
  206. regexps.append(None)
  207. elif regexps[-1] is None:
  208. regexps.pop()
  209. regexps.append(deque([char]))
  210. else:
  211. regexps[-1].append(char)
  212. yield from ("".join(regexp).strip() for regexp in regexps if regexp is not None)
  213. def _comment(string: str) -> str:
  214. """Return string as a comment."""
  215. lines = [line.strip() for line in string.splitlines()]
  216. sep = "\n"
  217. return "# " + f"{sep}# ".join(lines)
  218. def _format_option_value(optdict: OptionDict, value: Any) -> str:
  219. """Return the user input's value from a 'compiled' value.
  220. TODO: Refactor the code to not use this deprecated function
  221. """
  222. if optdict.get("type", None) == "py_version":
  223. value = ".".join(str(item) for item in value)
  224. elif isinstance(value, (list, tuple)):
  225. value = ",".join(_format_option_value(optdict, item) for item in value)
  226. elif isinstance(value, dict):
  227. value = ",".join(f"{k}:{v}" for k, v in value.items())
  228. elif hasattr(value, "match"): # optdict.get('type') == 'regexp'
  229. # compiled regexp
  230. value = value.pattern
  231. elif optdict.get("type") == "yn":
  232. value = "yes" if value else "no"
  233. elif isinstance(value, str) and value.isspace():
  234. value = f"'{value}'"
  235. return str(value)
  236. def format_section(
  237. stream: TextIO,
  238. section: str,
  239. options: list[tuple[str, OptionDict, Any]],
  240. doc: str | None = None,
  241. ) -> None:
  242. """Format an option's section using the INI format."""
  243. warnings.warn(
  244. "format_section has been deprecated. It will be removed in pylint 4.0.",
  245. DeprecationWarning,
  246. stacklevel=2,
  247. )
  248. if doc:
  249. print(_comment(doc), file=stream)
  250. print(f"[{section}]", file=stream)
  251. with warnings.catch_warnings():
  252. warnings.filterwarnings("ignore", category=DeprecationWarning)
  253. _ini_format(stream, options)
  254. def _ini_format(stream: TextIO, options: list[tuple[str, OptionDict, Any]]) -> None:
  255. """Format options using the INI format."""
  256. warnings.warn(
  257. "_ini_format has been deprecated. It will be removed in pylint 4.0.",
  258. DeprecationWarning,
  259. stacklevel=2,
  260. )
  261. for optname, optdict, value in options:
  262. # Skip deprecated option
  263. if "kwargs" in optdict:
  264. assert isinstance(optdict["kwargs"], dict)
  265. if "new_names" in optdict["kwargs"]:
  266. continue
  267. value = _format_option_value(optdict, value)
  268. help_opt = optdict.get("help")
  269. if help_opt:
  270. assert isinstance(help_opt, str)
  271. help_opt = normalize_text(help_opt, indent="# ")
  272. print(file=stream)
  273. print(help_opt, file=stream)
  274. else:
  275. print(file=stream)
  276. if value in {"None", "False"}:
  277. print(f"#{optname}=", file=stream)
  278. else:
  279. value = str(value).strip()
  280. if re.match(r"^([\w-]+,)+[\w-]+$", str(value)):
  281. separator = "\n " + " " * len(optname)
  282. value = separator.join(x + "," for x in str(value).split(","))
  283. # remove trailing ',' from last element of the list
  284. value = value[:-1]
  285. print(f"{optname}={value}", file=stream)