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