utils.py 12 KB

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