# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html # For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE # Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt from __future__ import annotations try: import isort.api import isort.settings HAS_ISORT_5 = True except ImportError: # isort < 5 import isort HAS_ISORT_5 = False import argparse import codecs import os import re import sys import textwrap import tokenize import warnings from collections import deque from collections.abc import Iterable, Sequence from io import BufferedReader, BytesIO from typing import ( TYPE_CHECKING, Any, List, Literal, Pattern, TextIO, Tuple, TypeVar, Union, ) from astroid import Module, modutils, nodes from pylint.constants import PY_EXTS from pylint.typing import OptionDict if TYPE_CHECKING: from pylint.lint import PyLinter DEFAULT_LINE_LENGTH = 79 # These are types used to overload get_global_option() and refer to the options type GLOBAL_OPTION_BOOL = Literal[ "suggestion-mode", "analyse-fallback-blocks", "allow-global-unused-variables", ] GLOBAL_OPTION_INT = Literal["max-line-length", "docstring-min-length"] GLOBAL_OPTION_LIST = Literal["ignored-modules"] GLOBAL_OPTION_PATTERN = Literal[ "no-docstring-rgx", "dummy-variables-rgx", "ignored-argument-names", "mixin-class-rgx", ] GLOBAL_OPTION_PATTERN_LIST = Literal["exclude-too-few-public-methods", "ignore-paths"] GLOBAL_OPTION_TUPLE_INT = Literal["py-version"] GLOBAL_OPTION_NAMES = Union[ GLOBAL_OPTION_BOOL, GLOBAL_OPTION_INT, GLOBAL_OPTION_LIST, GLOBAL_OPTION_PATTERN, GLOBAL_OPTION_PATTERN_LIST, GLOBAL_OPTION_TUPLE_INT, ] T_GlobalOptionReturnTypes = TypeVar( "T_GlobalOptionReturnTypes", bool, int, List[str], Pattern[str], List[Pattern[str]], Tuple[int, ...], ) def normalize_text( text: str, line_len: int = DEFAULT_LINE_LENGTH, indent: str = "" ) -> str: """Wrap the text on the given line length.""" return "\n".join( textwrap.wrap( text, width=line_len, initial_indent=indent, subsequent_indent=indent ) ) CMPS = ["=", "-", "+"] # py3k has no more cmp builtin def cmp(a: int | float, b: int | float) -> int: return (a > b) - (a < b) def diff_string(old: int | float, new: int | float) -> str: """Given an old and new int value, return a string representing the difference. """ diff = abs(old - new) diff_str = f"{CMPS[cmp(old, new)]}{diff and f'{diff:.2f}' or ''}" return diff_str def get_module_and_frameid(node: nodes.NodeNG) -> tuple[str, str]: """Return the module name and the frame id in the module.""" frame = node.frame() module, obj = "", [] while frame: if isinstance(frame, Module): module = frame.name else: obj.append(getattr(frame, "name", "")) try: frame = frame.parent.frame() except AttributeError: break obj.reverse() return module, ".".join(obj) def get_rst_title(title: str, character: str) -> str: """Permit to get a title formatted as ReStructuredText test (underlined with a chosen character). """ return f"{title}\n{character * len(title)}\n" def get_rst_section( section: str | None, options: list[tuple[str, OptionDict, Any]], doc: str | None = None, ) -> str: """Format an option's section using as a ReStructuredText formatted output.""" result = "" if section: result += get_rst_title(section, "'") if doc: formatted_doc = normalize_text(doc) result += f"{formatted_doc}\n\n" for optname, optdict, value in options: help_opt = optdict.get("help") result += f":{optname}:\n" if help_opt: assert isinstance(help_opt, str) formatted_help = normalize_text(help_opt, indent=" ") result += f"{formatted_help}\n" if value and optname != "py-version": value = str(_format_option_value(optdict, value)) result += f"\n Default: ``{value.replace('`` ', '```` ``')}``\n" return result def decoding_stream( stream: BufferedReader | BytesIO, encoding: str, errors: Literal["strict"] = "strict", ) -> codecs.StreamReader: try: reader_cls = codecs.getreader(encoding or sys.getdefaultencoding()) except LookupError: reader_cls = codecs.getreader(sys.getdefaultencoding()) return reader_cls(stream, errors) def tokenize_module(node: nodes.Module) -> list[tokenize.TokenInfo]: with node.stream() as stream: readline = stream.readline return list(tokenize.tokenize(readline)) def register_plugins(linter: PyLinter, directory: str) -> None: """Load all module and package in the given directory, looking for a 'register' function in each one, used to register pylint checkers. """ imported = {} for filename in os.listdir(directory): base, extension = os.path.splitext(filename) if base in imported or base == "__pycache__": continue if ( extension in PY_EXTS and base != "__init__" or ( not extension and os.path.isdir(os.path.join(directory, base)) and not filename.startswith(".") ) ): try: module = modutils.load_module_from_file( os.path.join(directory, filename) ) except ValueError: # empty module name (usually Emacs auto-save files) continue except ImportError as exc: print(f"Problem importing module {filename}: {exc}", file=sys.stderr) else: if hasattr(module, "register"): module.register(linter) imported[base] = 1 def _splitstrip(string: str, sep: str = ",") -> list[str]: """Return a list of stripped string by splitting the string given as argument on `sep` (',' by default), empty strings are discarded. >>> _splitstrip('a, b, c , 4,,') ['a', 'b', 'c', '4'] >>> _splitstrip('a') ['a'] >>> _splitstrip('a,\nb,\nc,') ['a', 'b', 'c'] :type string: str or unicode :param string: a csv line :type sep: str or unicode :param sep: field separator, default to the comma (',') :rtype: str or unicode :return: the unquoted string (or the input string if it wasn't quoted) """ return [word.strip() for word in string.split(sep) if word.strip()] def _unquote(string: str) -> str: """Remove optional quotes (simple or double) from the string. :param string: an optionally quoted string :return: the unquoted string (or the input string if it wasn't quoted) """ if not string: return string if string[0] in "\"'": string = string[1:] if string[-1] in "\"'": string = string[:-1] return string def _check_csv(value: list[str] | tuple[str] | str) -> Sequence[str]: if isinstance(value, (list, tuple)): return value return _splitstrip(value) def _check_regexp_csv(value: list[str] | tuple[str] | str) -> Iterable[str]: r"""Split a comma-separated list of regexps, taking care to avoid splitting a regex employing a comma as quantifier, as in `\d{1,2}`.""" if isinstance(value, (list, tuple)): yield from value else: # None is a sentinel value here regexps: deque[deque[str] | None] = deque([None]) open_braces = False for char in value: if char == "{": open_braces = True elif char == "}" and open_braces: open_braces = False if char == "," and not open_braces: regexps.append(None) elif regexps[-1] is None: regexps.pop() regexps.append(deque([char])) else: regexps[-1].append(char) yield from ("".join(regexp).strip() for regexp in regexps if regexp is not None) def _comment(string: str) -> str: """Return string as a comment.""" lines = [line.strip() for line in string.splitlines()] sep = "\n" return "# " + f"{sep}# ".join(lines) def _format_option_value(optdict: OptionDict, value: Any) -> str: """Return the user input's value from a 'compiled' value. TODO: Refactor the code to not use this deprecated function """ if optdict.get("type", None) == "py_version": value = ".".join(str(item) for item in value) elif isinstance(value, (list, tuple)): value = ",".join(_format_option_value(optdict, item) for item in value) elif isinstance(value, dict): value = ",".join(f"{k}:{v}" for k, v in value.items()) elif hasattr(value, "match"): # optdict.get('type') == 'regexp' # compiled regexp value = value.pattern elif optdict.get("type") == "yn": value = "yes" if value else "no" elif isinstance(value, str) and value.isspace(): value = f"'{value}'" return str(value) def format_section( stream: TextIO, section: str, options: list[tuple[str, OptionDict, Any]], doc: str | None = None, ) -> None: """Format an option's section using the INI format.""" warnings.warn( "format_section has been deprecated. It will be removed in pylint 3.0.", DeprecationWarning, stacklevel=2, ) if doc: print(_comment(doc), file=stream) print(f"[{section}]", file=stream) with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=DeprecationWarning) _ini_format(stream, options) def _ini_format(stream: TextIO, options: list[tuple[str, OptionDict, Any]]) -> None: """Format options using the INI format.""" warnings.warn( "_ini_format has been deprecated. It will be removed in pylint 3.0.", DeprecationWarning, stacklevel=2, ) for optname, optdict, value in options: # Skip deprecated option if "kwargs" in optdict: assert isinstance(optdict["kwargs"], dict) if "new_names" in optdict["kwargs"]: continue value = _format_option_value(optdict, value) help_opt = optdict.get("help") if help_opt: assert isinstance(help_opt, str) help_opt = normalize_text(help_opt, indent="# ") print(file=stream) print(help_opt, file=stream) else: print(file=stream) if value in {"None", "False"}: print(f"#{optname}=", file=stream) else: value = str(value).strip() if re.match(r"^([\w-]+,)+[\w-]+$", str(value)): separator = "\n " + " " * len(optname) value = separator.join(x + "," for x in str(value).split(",")) # remove trailing ',' from last element of the list value = value[:-1] print(f"{optname}={value}", file=stream) class IsortDriver: """A wrapper around isort API that changed between versions 4 and 5.""" def __init__(self, config: argparse.Namespace) -> None: if HAS_ISORT_5: self.isort5_config = isort.settings.Config( # There is no typo here. EXTRA_standard_library is # what most users want. The option has been named # KNOWN_standard_library for ages in pylint, and we # don't want to break compatibility. extra_standard_library=config.known_standard_library, known_third_party=config.known_third_party, ) else: # pylint: disable-next=no-member self.isort4_obj = isort.SortImports( # type: ignore[attr-defined] file_contents="", known_standard_library=config.known_standard_library, known_third_party=config.known_third_party, ) def place_module(self, package: str) -> str: if HAS_ISORT_5: return isort.api.place_module(package, self.isort5_config) return self.isort4_obj.place_module(package) # type: ignore[no-any-return]