utils.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  2. # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
  3. # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt
  4. from __future__ import annotations
  5. try:
  6. import isort.api
  7. import isort.settings
  8. HAS_ISORT_5 = True
  9. except ImportError: # isort < 5
  10. import isort
  11. HAS_ISORT_5 = False
  12. import argparse
  13. import codecs
  14. import os
  15. import re
  16. import sys
  17. import textwrap
  18. import tokenize
  19. import warnings
  20. from collections.abc import Sequence
  21. from io import BufferedReader, BytesIO
  22. from typing import (
  23. TYPE_CHECKING,
  24. Any,
  25. List,
  26. Pattern,
  27. TextIO,
  28. Tuple,
  29. TypeVar,
  30. Union,
  31. overload,
  32. )
  33. from astroid import Module, modutils, nodes
  34. from pylint.constants import PY_EXTS
  35. from pylint.typing import OptionDict
  36. if sys.version_info >= (3, 8):
  37. from typing import Literal
  38. else:
  39. from typing_extensions import Literal
  40. if TYPE_CHECKING:
  41. from pylint.checkers.base_checker import BaseChecker
  42. from pylint.lint import PyLinter
  43. DEFAULT_LINE_LENGTH = 79
  44. # These are types used to overload get_global_option() and refer to the options type
  45. GLOBAL_OPTION_BOOL = Literal[
  46. "suggestion-mode",
  47. "analyse-fallback-blocks",
  48. "allow-global-unused-variables",
  49. ]
  50. GLOBAL_OPTION_INT = Literal["max-line-length", "docstring-min-length"]
  51. GLOBAL_OPTION_LIST = Literal["ignored-modules"]
  52. GLOBAL_OPTION_PATTERN = Literal[
  53. "no-docstring-rgx",
  54. "dummy-variables-rgx",
  55. "ignored-argument-names",
  56. "mixin-class-rgx",
  57. ]
  58. GLOBAL_OPTION_PATTERN_LIST = Literal["exclude-too-few-public-methods", "ignore-paths"]
  59. GLOBAL_OPTION_TUPLE_INT = Literal["py-version"]
  60. GLOBAL_OPTION_NAMES = Union[
  61. GLOBAL_OPTION_BOOL,
  62. GLOBAL_OPTION_INT,
  63. GLOBAL_OPTION_LIST,
  64. GLOBAL_OPTION_PATTERN,
  65. GLOBAL_OPTION_PATTERN_LIST,
  66. GLOBAL_OPTION_TUPLE_INT,
  67. ]
  68. T_GlobalOptionReturnTypes = TypeVar(
  69. "T_GlobalOptionReturnTypes",
  70. bool,
  71. int,
  72. List[str],
  73. Pattern[str],
  74. List[Pattern[str]],
  75. Tuple[int, ...],
  76. )
  77. def normalize_text(
  78. text: str, line_len: int = DEFAULT_LINE_LENGTH, indent: str = ""
  79. ) -> str:
  80. """Wrap the text on the given line length."""
  81. return "\n".join(
  82. textwrap.wrap(
  83. text, width=line_len, initial_indent=indent, subsequent_indent=indent
  84. )
  85. )
  86. CMPS = ["=", "-", "+"]
  87. # py3k has no more cmp builtin
  88. def cmp(a: int | float, b: int | float) -> int:
  89. return (a > b) - (a < b)
  90. def diff_string(old: int | float, new: int | float) -> str:
  91. """Given an old and new int value, return a string representing the
  92. difference.
  93. """
  94. diff = abs(old - new)
  95. diff_str = f"{CMPS[cmp(old, new)]}{diff and f'{diff:.2f}' or ''}"
  96. return diff_str
  97. def get_module_and_frameid(node: nodes.NodeNG) -> tuple[str, str]:
  98. """Return the module name and the frame id in the module."""
  99. frame = node.frame(future=True)
  100. module, obj = "", []
  101. while frame:
  102. if isinstance(frame, Module):
  103. module = frame.name
  104. else:
  105. obj.append(getattr(frame, "name", "<lambda>"))
  106. try:
  107. frame = frame.parent.frame(future=True)
  108. except AttributeError:
  109. break
  110. obj.reverse()
  111. return module, ".".join(obj)
  112. def get_rst_title(title: str, character: str) -> str:
  113. """Permit to get a title formatted as ReStructuredText test (underlined with a
  114. chosen character).
  115. """
  116. return f"{title}\n{character * len(title)}\n"
  117. def get_rst_section(
  118. section: str | None,
  119. options: list[tuple[str, OptionDict, Any]],
  120. doc: str | None = None,
  121. ) -> str:
  122. """Format an option's section using as a ReStructuredText formatted output."""
  123. result = ""
  124. if section:
  125. result += get_rst_title(section, "'")
  126. if doc:
  127. formatted_doc = normalize_text(doc)
  128. result += f"{formatted_doc}\n\n"
  129. for optname, optdict, value in options:
  130. help_opt = optdict.get("help")
  131. result += f":{optname}:\n"
  132. if help_opt:
  133. assert isinstance(help_opt, str)
  134. formatted_help = normalize_text(help_opt, indent=" ")
  135. result += f"{formatted_help}\n"
  136. if value and optname != "py-version":
  137. value = str(_format_option_value(optdict, value))
  138. result += f"\n Default: ``{value.replace('`` ', '```` ``')}``\n"
  139. return result
  140. def decoding_stream(
  141. stream: BufferedReader | BytesIO,
  142. encoding: str,
  143. errors: Literal["strict"] = "strict",
  144. ) -> codecs.StreamReader:
  145. try:
  146. reader_cls = codecs.getreader(encoding or sys.getdefaultencoding())
  147. except LookupError:
  148. reader_cls = codecs.getreader(sys.getdefaultencoding())
  149. return reader_cls(stream, errors)
  150. def tokenize_module(node: nodes.Module) -> list[tokenize.TokenInfo]:
  151. with node.stream() as stream:
  152. readline = stream.readline
  153. return list(tokenize.tokenize(readline))
  154. def register_plugins(linter: PyLinter, directory: str) -> None:
  155. """Load all module and package in the given directory, looking for a
  156. 'register' function in each one, used to register pylint checkers.
  157. """
  158. imported = {}
  159. for filename in os.listdir(directory):
  160. base, extension = os.path.splitext(filename)
  161. if base in imported or base == "__pycache__":
  162. continue
  163. if (
  164. extension in PY_EXTS
  165. and base != "__init__"
  166. or (
  167. not extension
  168. and os.path.isdir(os.path.join(directory, base))
  169. and not filename.startswith(".")
  170. )
  171. ):
  172. try:
  173. module = modutils.load_module_from_file(
  174. os.path.join(directory, filename)
  175. )
  176. except ValueError:
  177. # empty module name (usually Emacs auto-save files)
  178. continue
  179. except ImportError as exc:
  180. print(f"Problem importing module {filename}: {exc}", file=sys.stderr)
  181. else:
  182. if hasattr(module, "register"):
  183. module.register(linter)
  184. imported[base] = 1
  185. @overload
  186. def get_global_option(
  187. checker: BaseChecker, option: GLOBAL_OPTION_BOOL, default: bool | None = ...
  188. ) -> bool:
  189. ...
  190. @overload
  191. def get_global_option(
  192. checker: BaseChecker, option: GLOBAL_OPTION_INT, default: int | None = ...
  193. ) -> int:
  194. ...
  195. @overload
  196. def get_global_option(
  197. checker: BaseChecker,
  198. option: GLOBAL_OPTION_LIST,
  199. default: list[str] | None = ...,
  200. ) -> list[str]:
  201. ...
  202. @overload
  203. def get_global_option(
  204. checker: BaseChecker,
  205. option: GLOBAL_OPTION_PATTERN,
  206. default: Pattern[str] | None = ...,
  207. ) -> Pattern[str]:
  208. ...
  209. @overload
  210. def get_global_option(
  211. checker: BaseChecker,
  212. option: GLOBAL_OPTION_PATTERN_LIST,
  213. default: list[Pattern[str]] | None = ...,
  214. ) -> list[Pattern[str]]:
  215. ...
  216. @overload
  217. def get_global_option(
  218. checker: BaseChecker,
  219. option: GLOBAL_OPTION_TUPLE_INT,
  220. default: tuple[int, ...] | None = ...,
  221. ) -> tuple[int, ...]:
  222. ...
  223. def get_global_option(
  224. checker: BaseChecker,
  225. option: GLOBAL_OPTION_NAMES,
  226. default: T_GlobalOptionReturnTypes | None = None, # pylint: disable=unused-argument
  227. ) -> T_GlobalOptionReturnTypes | None | Any:
  228. """DEPRECATED: Retrieve an option defined by the given *checker* or
  229. by all known option providers.
  230. It will look in the list of all options providers
  231. until the given *option* will be found.
  232. If the option wasn't found, the *default* value will be returned.
  233. """
  234. warnings.warn(
  235. "get_global_option has been deprecated. You can use "
  236. "checker.linter.config to get all global options instead.",
  237. DeprecationWarning,
  238. stacklevel=2,
  239. )
  240. return getattr(checker.linter.config, option.replace("-", "_"))
  241. def _splitstrip(string: str, sep: str = ",") -> list[str]:
  242. """Return a list of stripped string by splitting the string given as
  243. argument on `sep` (',' by default), empty strings are discarded.
  244. >>> _splitstrip('a, b, c , 4,,')
  245. ['a', 'b', 'c', '4']
  246. >>> _splitstrip('a')
  247. ['a']
  248. >>> _splitstrip('a,\nb,\nc,')
  249. ['a', 'b', 'c']
  250. :type string: str or unicode
  251. :param string: a csv line
  252. :type sep: str or unicode
  253. :param sep: field separator, default to the comma (',')
  254. :rtype: str or unicode
  255. :return: the unquoted string (or the input string if it wasn't quoted)
  256. """
  257. return [word.strip() for word in string.split(sep) if word.strip()]
  258. def _unquote(string: str) -> str:
  259. """Remove optional quotes (simple or double) from the string.
  260. :param string: an optionally quoted string
  261. :return: the unquoted string (or the input string if it wasn't quoted)
  262. """
  263. if not string:
  264. return string
  265. if string[0] in "\"'":
  266. string = string[1:]
  267. if string[-1] in "\"'":
  268. string = string[:-1]
  269. return string
  270. def _check_csv(value: list[str] | tuple[str] | str) -> Sequence[str]:
  271. if isinstance(value, (list, tuple)):
  272. return value
  273. return _splitstrip(value)
  274. def _comment(string: str) -> str:
  275. """Return string as a comment."""
  276. lines = [line.strip() for line in string.splitlines()]
  277. sep = "\n"
  278. return "# " + f"{sep}# ".join(lines)
  279. def _format_option_value(optdict: OptionDict, value: Any) -> str:
  280. """Return the user input's value from a 'compiled' value.
  281. TODO: 3.0: Remove deprecated function
  282. """
  283. if optdict.get("type", None) == "py_version":
  284. value = ".".join(str(item) for item in value)
  285. elif isinstance(value, (list, tuple)):
  286. value = ",".join(_format_option_value(optdict, item) for item in value)
  287. elif isinstance(value, dict):
  288. value = ",".join(f"{k}:{v}" for k, v in value.items())
  289. elif hasattr(value, "match"): # optdict.get('type') == 'regexp'
  290. # compiled regexp
  291. value = value.pattern
  292. elif optdict.get("type") == "yn":
  293. value = "yes" if value else "no"
  294. elif isinstance(value, str) and value.isspace():
  295. value = f"'{value}'"
  296. return str(value)
  297. def format_section(
  298. stream: TextIO,
  299. section: str,
  300. options: list[tuple[str, OptionDict, Any]],
  301. doc: str | None = None,
  302. ) -> None:
  303. """Format an option's section using the INI format."""
  304. warnings.warn(
  305. "format_section has been deprecated. It will be removed in pylint 3.0.",
  306. DeprecationWarning,
  307. stacklevel=2,
  308. )
  309. if doc:
  310. print(_comment(doc), file=stream)
  311. print(f"[{section}]", file=stream)
  312. with warnings.catch_warnings():
  313. warnings.filterwarnings("ignore", category=DeprecationWarning)
  314. _ini_format(stream, options)
  315. def _ini_format(stream: TextIO, options: list[tuple[str, OptionDict, Any]]) -> None:
  316. """Format options using the INI format."""
  317. warnings.warn(
  318. "_ini_format has been deprecated. It will be removed in pylint 3.0.",
  319. DeprecationWarning,
  320. stacklevel=2,
  321. )
  322. for optname, optdict, value in options:
  323. # Skip deprecated option
  324. if "kwargs" in optdict:
  325. assert isinstance(optdict["kwargs"], dict)
  326. if "new_names" in optdict["kwargs"]:
  327. continue
  328. value = _format_option_value(optdict, value)
  329. help_opt = optdict.get("help")
  330. if help_opt:
  331. assert isinstance(help_opt, str)
  332. help_opt = normalize_text(help_opt, indent="# ")
  333. print(file=stream)
  334. print(help_opt, file=stream)
  335. else:
  336. print(file=stream)
  337. if value in {"None", "False"}:
  338. print(f"#{optname}=", file=stream)
  339. else:
  340. value = str(value).strip()
  341. if re.match(r"^([\w-]+,)+[\w-]+$", str(value)):
  342. separator = "\n " + " " * len(optname)
  343. value = separator.join(x + "," for x in str(value).split(","))
  344. # remove trailing ',' from last element of the list
  345. value = value[:-1]
  346. print(f"{optname}={value}", file=stream)
  347. class IsortDriver:
  348. """A wrapper around isort API that changed between versions 4 and 5."""
  349. def __init__(self, config: argparse.Namespace) -> None:
  350. if HAS_ISORT_5:
  351. self.isort5_config = isort.settings.Config(
  352. # There is no typo here. EXTRA_standard_library is
  353. # what most users want. The option has been named
  354. # KNOWN_standard_library for ages in pylint, and we
  355. # don't want to break compatibility.
  356. extra_standard_library=config.known_standard_library,
  357. known_third_party=config.known_third_party,
  358. )
  359. else:
  360. # pylint: disable-next=no-member
  361. self.isort4_obj = isort.SortImports( # type: ignore[attr-defined]
  362. file_contents="",
  363. known_standard_library=config.known_standard_library,
  364. known_third_party=config.known_third_party,
  365. )
  366. def place_module(self, package: str) -> str:
  367. if HAS_ISORT_5:
  368. return isort.api.place_module(package, self.isort5_config)
  369. return self.isort4_obj.place_module(package) # type: ignore[no-any-return]