123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406 |
- # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
- # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
- try:
- import isort.api
- HAS_ISORT_5 = True
- except ImportError: # isort < 5
- import isort
- HAS_ISORT_5 = False
- import codecs
- import os
- import re
- import sys
- import textwrap
- import tokenize
- from io import BufferedReader, BytesIO
- from typing import (
- TYPE_CHECKING,
- List,
- Optional,
- Pattern,
- TextIO,
- Tuple,
- TypeVar,
- Union,
- overload,
- )
- from astroid import Module, modutils, nodes
- from pylint.constants import PY_EXTS
- if sys.version_info >= (3, 8):
- from typing import Literal
- else:
- from typing_extensions import Literal
- if TYPE_CHECKING:
- from pylint.checkers.base_checker import BaseChecker
- DEFAULT_LINE_LENGTH = 79
- # These are types used to overload get_global_option() and refer to the options type
- GLOBAL_OPTION_BOOL = Literal[
- "ignore-mixin-members",
- "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, line_len=DEFAULT_LINE_LENGTH, indent=""):
- """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, b):
- return (a > b) - (a < b)
- def diff_string(old, new):
- """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):
- """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", "<lambda>"))
- try:
- frame = frame.parent.frame()
- except AttributeError:
- break
- obj.reverse()
- return module, ".".join(obj)
- def get_rst_title(title, character):
- """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, options, doc=None):
- """format an options 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:
- 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: Union[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, directory):
- """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
- @overload
- def get_global_option(
- checker: "BaseChecker", option: GLOBAL_OPTION_BOOL, default: Optional[bool] = None
- ) -> bool:
- ...
- @overload
- def get_global_option(
- checker: "BaseChecker", option: GLOBAL_OPTION_INT, default: Optional[int] = None
- ) -> int:
- ...
- @overload
- def get_global_option(
- checker: "BaseChecker",
- option: GLOBAL_OPTION_LIST,
- default: Optional[List[str]] = None,
- ) -> List[str]:
- ...
- @overload
- def get_global_option(
- checker: "BaseChecker",
- option: GLOBAL_OPTION_PATTERN,
- default: Optional[Pattern[str]] = None,
- ) -> Pattern[str]:
- ...
- @overload
- def get_global_option(
- checker: "BaseChecker",
- option: GLOBAL_OPTION_PATTERN_LIST,
- default: Optional[List[Pattern[str]]] = None,
- ) -> List[Pattern[str]]:
- ...
- @overload
- def get_global_option(
- checker: "BaseChecker",
- option: GLOBAL_OPTION_TUPLE_INT,
- default: Optional[Tuple[int, ...]] = None,
- ) -> Tuple[int, ...]:
- ...
- def get_global_option(
- checker: "BaseChecker",
- option: GLOBAL_OPTION_NAMES,
- default: Optional[T_GlobalOptionReturnTypes] = None,
- ) -> Optional[T_GlobalOptionReturnTypes]:
- """Retrieve an option defined by the given *checker* or
- by all known option providers.
- It will look in the list of all options providers
- until the given *option* will be found.
- If the option wasn't found, the *default* value will be returned.
- """
- # First, try in the given checker's config.
- # After that, look in the options providers.
- try:
- return getattr(checker.config, option.replace("-", "_"))
- except AttributeError:
- pass
- for provider in checker.linter.options_providers:
- for options in provider.options:
- if options[0] == option:
- return getattr(provider.config, option.replace("-", "_"))
- return default
- def _splitstrip(string, sep=","):
- """return a list of stripped string by splitting the string given as
- argument on `sep` (',' by default). Empty string 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):
- """remove optional quotes (simple or double) from the string
- :type string: str or unicode
- :param string: an optionally quoted string
- :rtype: str or unicode
- :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):
- if isinstance(value, (list, tuple)):
- return value
- return _splitstrip(value)
- 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, value):
- """return the user input's value from a 'compiled' value"""
- 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 value
- def format_section(
- stream: TextIO, section: str, options: List[Tuple], doc: Optional[str] = None
- ) -> None:
- """format an options section using the INI format"""
- if doc:
- print(_comment(doc), file=stream)
- print(f"[{section}]", file=stream)
- _ini_format(stream, options)
- def _ini_format(stream: TextIO, options: List[Tuple]) -> None:
- """format options using the INI format"""
- for optname, optdict, value in options:
- value = _format_option_value(optdict, value)
- help_opt = optdict.get("help")
- if help_opt:
- help_opt = normalize_text(help_opt, indent="# ")
- print(file=stream)
- print(help_opt, file=stream)
- else:
- print(file=stream)
- if value is None:
- 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):
- if HAS_ISORT_5:
- self.isort5_config = isort.api.Config(
- # There is not 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:
- self.isort4_obj = isort.SortImports( # pylint: disable=no-member
- file_contents="",
- known_standard_library=config.known_standard_library,
- known_third_party=config.known_third_party,
- )
- def place_module(self, package):
- if HAS_ISORT_5:
- return isort.api.place_module(package, self.isort5_config)
- return self.isort4_obj.place_module(package)
|