diff options
Diffstat (limited to 'pylint')
186 files changed, 1256 insertions, 4219 deletions
diff --git a/pylint/__init__.py b/pylint/__init__.py index 2cc7edadb..cc0f609aa 100644 --- a/pylint/__init__.py +++ b/pylint/__init__.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 @@ -9,14 +9,12 @@ __all__ = [ "version", "modify_sys_path", "run_pylint", - "run_epylint", "run_symilar", "run_pyreverse", ] import os import sys -import warnings from collections.abc import Sequence from typing import NoReturn @@ -48,22 +46,6 @@ def _run_pylint_config(argv: Sequence[str] | None = None) -> None: _PylintConfigRun(argv or sys.argv[1:]) -def run_epylint(argv: Sequence[str] | None = None) -> NoReturn: - """Run epylint. - - argv can be a list of strings normally supplied as arguments on the command line - """ - from pylint.epylint import Run as EpylintRun - - warnings.warn( - "'run_epylint' will be removed in pylint 3.0, use " - "https://github.com/emacsorphanage/pylint instead.", - DeprecationWarning, - stacklevel=1, - ) - EpylintRun(argv) - - def run_pyreverse(argv: Sequence[str] | None = None) -> NoReturn: """Run pyreverse. @@ -96,12 +78,12 @@ def modify_sys_path() -> None: - Remove the first entry. This will always be either "" or the working directory - Remove the working directory from the second and third entries if PYTHONPATH includes a ":" at the beginning or the end. - https://github.com/PyCQA/pylint/issues/3636 + https://github.com/pylint-dev/pylint/issues/3636 Don't remove it if PYTHONPATH contains the cwd or '.' as the entry will only be added once. - Don't remove the working directory from the rest. It will be included if pylint is installed in an editable configuration (as the last item). - https://github.com/PyCQA/pylint/issues/4161 + https://github.com/pylint-dev/pylint/issues/4161 """ cwd = os.getcwd() if sys.path[0] in ("", ".", cwd): diff --git a/pylint/__main__.py b/pylint/__main__.py index 7df5805f9..448ac55b6 100644 --- a/pylint/__main__.py +++ b/pylint/__main__.py @@ -1,8 +1,8 @@ #!/usr/bin/env python # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt import pylint diff --git a/pylint/__pkginfo__.py b/pylint/__pkginfo__.py index 3566c348f..5a86f05b0 100644 --- a/pylint/__pkginfo__.py +++ b/pylint/__pkginfo__.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """This module exists for compatibility reasons. @@ -9,14 +9,14 @@ It's updated via tbump, do not modify. from __future__ import annotations -__version__ = "2.17.4" +__version__ = "3.0.0b1" def get_numversion_from_version(v: str) -> tuple[int, int, int]: """Kept for compatibility reason. - See https://github.com/PyCQA/pylint/issues/4399 - https://github.com/PyCQA/pylint/issues/4420, + See https://github.com/pylint-dev/pylint/issues/4399 + https://github.com/pylint-dev/pylint/issues/4420, """ version = v.replace("pylint-", "") result_version = [] diff --git a/pylint/checkers/__init__.py b/pylint/checkers/__init__.py index ed641d8e5..0f9985918 100644 --- a/pylint/checkers/__init__.py +++ b/pylint/checkers/__init__.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Utilities methods and classes for checkers. @@ -42,8 +42,7 @@ messages nor reports. XXX not true, emit a 07 report ! from __future__ import annotations -import sys -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Literal from pylint.checkers.base_checker import ( BaseChecker, @@ -51,14 +50,8 @@ from pylint.checkers.base_checker import ( BaseTokenChecker, ) from pylint.checkers.deprecated import DeprecatedMixin -from pylint.checkers.mapreduce_checker import MapReduceMixin from pylint.utils import LinterStats, diff_string, register_plugins -if sys.version_info >= (3, 8): - from typing import Literal -else: - from typing_extensions import Literal - if TYPE_CHECKING: from pylint.lint import PyLinter @@ -141,7 +134,6 @@ __all__ = [ "BaseTokenChecker", "BaseRawFileChecker", "initialize", - "MapReduceMixin", "DeprecatedMixin", "register_plugins", ] diff --git a/pylint/checkers/async.py b/pylint/checkers/async.py index b06ec60c8..a8ee77302 100644 --- a/pylint/checkers/async.py +++ b/pylint/checkers/async.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Checker for anything related to the async protocol (PEP 492).""" diff --git a/pylint/checkers/bad_chained_comparison.py b/pylint/checkers/bad_chained_comparison.py index 8c3aeb9cc..2e1912160 100644 --- a/pylint/checkers/bad_chained_comparison.py +++ b/pylint/checkers/bad_chained_comparison.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 diff --git a/pylint/checkers/base/__init__.py b/pylint/checkers/base/__init__.py index f427cbf21..c9067b405 100644 --- a/pylint/checkers/base/__init__.py +++ b/pylint/checkers/base/__init__.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt # pylint: disable=duplicate-code # This is similar to the __init__ of .name_checker diff --git a/pylint/checkers/base/basic_checker.py b/pylint/checkers/base/basic_checker.py index 062e67a97..18de86142 100644 --- a/pylint/checkers/base/basic_checker.py +++ b/pylint/checkers/base/basic_checker.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Basic checker for Python code.""" @@ -8,9 +8,8 @@ from __future__ import annotations import collections import itertools -import sys from collections.abc import Iterator -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, Literal, cast import astroid from astroid import nodes, objects, util @@ -24,11 +23,6 @@ from pylint.utils import LinterStats if TYPE_CHECKING: from pylint.lint.pylinter import PyLinter -if sys.version_info >= (3, 8): - from typing import Literal -else: - from typing_extensions import Literal - class _BasicChecker(BaseChecker): """Permits separating multiple checks with the same checker name into @@ -329,7 +323,7 @@ class BasicChecker(_BasicChecker): nodes.Subscript, ) inferred = None - emit = isinstance(test, (nodes.Const,) + structs + const_nodes) + emit = isinstance(test, (nodes.Const, *structs, *const_nodes)) maybe_generator_call = None if not isinstance(test, except_nodes): inferred = utils.safe_infer(test) @@ -367,9 +361,9 @@ class BasicChecker(_BasicChecker): try: # Just forcing the generator to infer all elements. # astroid.exceptions.InferenceError are false positives - # see https://github.com/PyCQA/pylint/pull/8185 + # see https://github.com/pylint-dev/pylint/pull/8185 if isinstance(inferred, nodes.FunctionDef): - call_inferred = list(inferred.infer_call_result()) + call_inferred = list(inferred.infer_call_result(node)) elif isinstance(inferred, nodes.Lambda): call_inferred = list(inferred.infer_call_result(node)) except astroid.InferenceError: @@ -462,7 +456,7 @@ class BasicChecker(_BasicChecker): # Heuristic: only run inference for names that begin with an uppercase char # This reduces W0133's coverage, but retains acceptable runtime performance - # For more details, see: https://github.com/PyCQA/pylint/issues/8073 + # For more details, see: https://github.com/pylint-dev/pylint/issues/8073 inferred = utils.safe_infer(expr) if name[:1].isupper() else None if isinstance(inferred, objects.ExceptionInstance): self.add_message( diff --git a/pylint/checkers/base/basic_error_checker.py b/pylint/checkers/base/basic_error_checker.py index 25038247c..ccf9773a7 100644 --- a/pylint/checkers/base/basic_error_checker.py +++ b/pylint/checkers/base/basic_error_checker.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Basic Error checker from the basic checker.""" diff --git a/pylint/checkers/base/comparison_checker.py b/pylint/checkers/base/comparison_checker.py index ffbd27374..14d40c7d6 100644 --- a/pylint/checkers/base/comparison_checker.py +++ b/pylint/checkers/base/comparison_checker.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Comparison checker from the basic checker.""" diff --git a/pylint/checkers/base/docstring_checker.py b/pylint/checkers/base/docstring_checker.py index 791b085b5..91b3e7d4a 100644 --- a/pylint/checkers/base/docstring_checker.py +++ b/pylint/checkers/base/docstring_checker.py @@ -1,13 +1,13 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Docstring checker from the basic checker.""" from __future__ import annotations import re -import sys +from typing import Literal import astroid from astroid import nodes @@ -21,11 +21,6 @@ from pylint.checkers.utils import ( is_property_setter, ) -if sys.version_info >= (3, 8): - from typing import Literal -else: - from typing_extensions import Literal - # do not require a doc string on private/system methods NO_REQUIRED_DOC_RGX = re.compile("^_") diff --git a/pylint/checkers/base/name_checker/__init__.py b/pylint/checkers/base/name_checker/__init__.py index 3d6818b7a..dec4335f5 100644 --- a/pylint/checkers/base/name_checker/__init__.py +++ b/pylint/checkers/base/name_checker/__init__.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt __all__ = [ "NameChecker", diff --git a/pylint/checkers/base/name_checker/checker.py b/pylint/checkers/base/name_checker/checker.py index 58f7198ef..56ae925fb 100644 --- a/pylint/checkers/base/name_checker/checker.py +++ b/pylint/checkers/base/name_checker/checker.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Basic checker for Python code.""" @@ -476,7 +476,12 @@ class NameChecker(_BasicChecker): # global introduced variable aren't in the function locals if node.name in frame and node.name not in frame.argnames(): if not _redefines_import(node): - self._check_name("variable", node.name, node) + if isinstance( + assign_type, nodes.AnnAssign + ) and self._assigns_typealias(assign_type.annotation): + self._check_name("typealias", node.name, node) + else: + self._check_name("variable", node.name, node) # Check names defined in class scopes elif isinstance(frame, nodes.ClassDef): diff --git a/pylint/checkers/base/name_checker/naming_style.py b/pylint/checkers/base/name_checker/naming_style.py index 8ccec4289..6410f9181 100644 --- a/pylint/checkers/base/name_checker/naming_style.py +++ b/pylint/checkers/base/name_checker/naming_style.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 diff --git a/pylint/checkers/base/pass_checker.py b/pylint/checkers/base/pass_checker.py index 120d4c5a0..a9351ba75 100644 --- a/pylint/checkers/base/pass_checker.py +++ b/pylint/checkers/base/pass_checker.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt from astroid import nodes diff --git a/pylint/checkers/base_checker.py b/pylint/checkers/base_checker.py index dd7a0222f..9785f9c4a 100644 --- a/pylint/checkers/base_checker.py +++ b/pylint/checkers/base_checker.py @@ -1,12 +1,11 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 import abc import functools -import warnings from collections.abc import Iterable, Sequence from inspect import cleandoc from tokenize import TokenInfo @@ -17,7 +16,7 @@ from astroid import nodes from pylint.config.arguments_provider import _ArgumentsProvider from pylint.constants import _MSG_ORDER, MAIN_CHECKER_NAME, WarningScope from pylint.exceptions import InvalidMessageError -from pylint.interfaces import Confidence, IRawChecker, ITokenChecker, implements +from pylint.interfaces import Confidence from pylint.message.message_definition import MessageDefinition from pylint.typing import ( ExtraMessageOptions, @@ -47,18 +46,9 @@ class BaseChecker(_ArgumentsProvider): def __init__(self, linter: PyLinter) -> None: """Checker instances should have the linter as argument.""" - if getattr(self, "__implements__", None): - warnings.warn( - "Using the __implements__ inheritance pattern for BaseChecker is no " - "longer supported. Child classes should only inherit BaseChecker or any " - "of the other checker types from pylint.checkers.", - DeprecationWarning, - stacklevel=2, - ) if self.name is not None: self.name = self.name.lower() self.linter = linter - _ArgumentsProvider.__init__(self, linter) def __gt__(self, other: Any) -> bool: @@ -96,11 +86,9 @@ class BaseChecker(_ArgumentsProvider): See: MessageHandlerMixIn.get_full_documentation() """ - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - return self.get_full_documentation( - msgs=self.msgs, options=self.options_and_values(), reports=self.reports - ) + return self.get_full_documentation( + msgs=self.msgs, options=self._options_and_values(), reports=self.reports + ) def get_full_documentation( self, @@ -193,21 +181,10 @@ class BaseChecker(_ArgumentsProvider): def create_message_definition_from_tuple( self, msgid: str, msg_tuple: MessageDefinitionTuple ) -> MessageDefinition: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - if isinstance(self, (BaseTokenChecker, BaseRawFileChecker)): - default_scope = WarningScope.LINE - # TODO: 3.0: Remove deprecated if-statement - elif implements(self, (IRawChecker, ITokenChecker)): - warnings.warn( # pragma: no cover - "Checkers should subclass BaseTokenChecker or BaseRawFileChecker " - "instead of using the __implements__ mechanism. Use of __implements__ " - "will no longer be supported in pylint 3.0", - DeprecationWarning, - ) - default_scope = WarningScope.LINE # pragma: no cover - else: - default_scope = WarningScope.NODE + if isinstance(self, (BaseTokenChecker, BaseRawFileChecker)): + default_scope = WarningScope.LINE + else: + default_scope = WarningScope.NODE options: ExtraMessageOptions = {} if len(msg_tuple) == 4: (msg, symbol, descr, options) = msg_tuple # type: ignore[misc] @@ -234,20 +211,6 @@ class BaseChecker(_ArgumentsProvider): for msgid, msg_tuple in sorted(self.msgs.items()) ] - def get_message_definition(self, msgid: str) -> MessageDefinition: - # TODO: 3.0: Remove deprecated method - warnings.warn( - "'get_message_definition' is deprecated and will be removed in 3.0.", - DeprecationWarning, - stacklevel=2, - ) - for message_definition in self.messages: - if message_definition.msgid == msgid: - return message_definition - error_msg = f"MessageDefinition for '{msgid}' does not exists. " - error_msg += f"Choose from {[m.msgid for m in self.messages]}." - raise InvalidMessageError(error_msg) - def open(self) -> None: """Called before visiting project (i.e. set of modules).""" diff --git a/pylint/checkers/classes/__init__.py b/pylint/checkers/classes/__init__.py index e51194439..422fae2ee 100644 --- a/pylint/checkers/classes/__init__.py +++ b/pylint/checkers/classes/__init__.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 diff --git a/pylint/checkers/classes/class_checker.py b/pylint/checkers/classes/class_checker.py index e09df1706..77a795bcb 100644 --- a/pylint/checkers/classes/class_checker.py +++ b/pylint/checkers/classes/class_checker.py @@ -1,15 +1,15 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Classes checker for Python code.""" from __future__ import annotations import collections -import sys from collections import defaultdict from collections.abc import Callable, Sequence +from functools import cached_property from itertools import chain, zip_longest from re import Pattern from typing import TYPE_CHECKING, Any, Union @@ -47,11 +47,6 @@ if TYPE_CHECKING: from pylint.lint.pylinter import PyLinter -if sys.version_info >= (3, 8): - from functools import cached_property -else: - from astroid.decorators import cachedproperty as cached_property - _AccessNodes = Union[nodes.Attribute, nodes.AssignAttr] INVALID_BASE_CLASSES = {"bool", "range", "slice", "memoryview"} diff --git a/pylint/checkers/classes/special_methods_checker.py b/pylint/checkers/classes/special_methods_checker.py index 9de5e619f..025f28562 100644 --- a/pylint/checkers/classes/special_methods_checker.py +++ b/pylint/checkers/classes/special_methods_checker.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Special methods checker and helper function's module.""" diff --git a/pylint/checkers/deprecated.py b/pylint/checkers/deprecated.py index f6a82b1da..821a9836c 100644 --- a/pylint/checkers/deprecated.py +++ b/pylint/checkers/deprecated.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Checker mixin for deprecated functionality.""" diff --git a/pylint/checkers/design_analysis.py b/pylint/checkers/design_analysis.py index 11ff7a5a1..701615d89 100644 --- a/pylint/checkers/design_analysis.py +++ b/pylint/checkers/design_analysis.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Check for signs of poor design.""" diff --git a/pylint/checkers/dunder_methods.py b/pylint/checkers/dunder_methods.py index b66844901..535dbb168 100644 --- a/pylint/checkers/dunder_methods.py +++ b/pylint/checkers/dunder_methods.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 @@ -23,7 +23,7 @@ class DunderCallChecker(BaseChecker): Docs: https://docs.python.org/3/reference/datamodel.html#basic-customization We exclude names in list pylint.constants.EXTRA_DUNDER_METHODS such as - __index__ (see https://github.com/PyCQA/pylint/issues/6795) + __index__ (see https://github.com/pylint-dev/pylint/issues/6795) since these either have no alternative method of being called or have a genuine use case for being called manually. diff --git a/pylint/checkers/ellipsis_checker.py b/pylint/checkers/ellipsis_checker.py index 52b15c702..4e7e3bd35 100644 --- a/pylint/checkers/ellipsis_checker.py +++ b/pylint/checkers/ellipsis_checker.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Ellipsis checker for Python code.""" diff --git a/pylint/checkers/exceptions.py b/pylint/checkers/exceptions.py index c2bd246d7..5757b8d55 100644 --- a/pylint/checkers/exceptions.py +++ b/pylint/checkers/exceptions.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Checks for various exception related errors.""" @@ -210,16 +210,17 @@ class ExceptionRaiseRefVisitor(BaseVisitor): "notimplemented-raised", node=self._node, confidence=HIGH ) return - try: - exceptions = list(_annotated_unpack_infer(node)) + exceptions = [ + c + for _, c in _annotated_unpack_infer(node) + if isinstance(c, nodes.ClassDef) + ] except astroid.InferenceError: return - for _, exception in exceptions: - if isinstance( - exception, nodes.ClassDef - ) and self._checker._is_overgeneral_exception(exception): + for exception in exceptions: + if self._checker._is_overgeneral_exception(exception): self._checker.add_message( "broad-exception-raised", args=exception.name, @@ -306,13 +307,13 @@ class ExceptionsChecker(checkers.BaseChecker): def open(self) -> None: self._builtin_exceptions = _builtin_exceptions() + # TODO 3.1: Remove this check and put it elsewhere for exc_name in self.linter.config.overgeneral_exceptions: if "." not in exc_name: warnings.warn_explicit( - "Specifying exception names in the overgeneral-exceptions option" - " without module name is deprecated and support for it" - " will be removed in pylint 3.0." - f" Use fully qualified name (maybe 'builtins.{exc_name}' ?) instead.", + f"'{exc_name}' is not a proper value for the 'overgeneral-exceptions' option. " + f"Use fully qualified name (maybe 'builtins.{exc_name}' ?) instead. " + "This will cease to be checked at runtime in 3.1.0.", category=UserWarning, filename="pylint: Command line or configuration file", lineno=1, @@ -646,13 +647,7 @@ class ExceptionsChecker(checkers.BaseChecker): exceptions_classes += [exc for _, exc in exceptions] def _is_overgeneral_exception(self, exception: nodes.ClassDef) -> bool: - return ( - exception.qname() in self.linter.config.overgeneral_exceptions - # TODO: 3.0: not a qualified name, deprecated - or "." not in exception.name - and exception.name in self.linter.config.overgeneral_exceptions - and exception.root().name == utils.EXCEPTIONS_MODULE - ) + return exception.qname() in self.linter.config.overgeneral_exceptions def register(linter: PyLinter) -> None: diff --git a/pylint/checkers/format.py b/pylint/checkers/format.py index 562c48f5d..3e3cd3227 100644 --- a/pylint/checkers/format.py +++ b/pylint/checkers/format.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Python code format's checker. @@ -13,11 +13,10 @@ Some parts of the process_token method is based from The Tab Nanny std module. from __future__ import annotations -import sys import tokenize from functools import reduce from re import Match -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Literal from astroid import nodes @@ -31,10 +30,6 @@ from pylint.utils.pragma_parser import OPTION_PO, PragmaParserError, parse_pragm if TYPE_CHECKING: from pylint.lint import PyLinter -if sys.version_info >= (3, 8): - from typing import Literal -else: - from typing_extensions import Literal _KEYWORD_TOKENS = { "assert", @@ -346,7 +341,7 @@ class FormatChecker(BaseTokenChecker, BaseRawFileChecker): if found_and_or: return if keyword_token == "in": - # This special case was added in https://github.com/PyCQA/pylint/pull/4948 + # This special case was added in https://github.com/pylint-dev/pylint/pull/4948 # but it could be removed in the future. Avoid churn for now. return self.add_message( @@ -667,8 +662,8 @@ class FormatChecker(BaseTokenChecker, BaseRawFileChecker): self.add_message("missing-final-newline", line=lineno + offset) continue # We don't test for trailing whitespaces in strings - # See https://github.com/PyCQA/pylint/issues/6936 - # and https://github.com/PyCQA/pylint/issues/3822 + # See https://github.com/pylint-dev/pylint/issues/6936 + # and https://github.com/pylint-dev/pylint/issues/3822 if tokens.type(line_start) != tokenize.STRING: self.check_trailing_whitespace_ending(line, lineno + offset) diff --git a/pylint/checkers/imports.py b/pylint/checkers/imports.py index ef7fe50cc..42649f3d9 100644 --- a/pylint/checkers/imports.py +++ b/pylint/checkers/imports.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Imports checkers for Python code.""" @@ -12,6 +12,7 @@ import os import sys from collections import defaultdict from collections.abc import ItemsView, Sequence +from functools import cached_property from typing import TYPE_CHECKING, Any, Dict, List, Union import astroid @@ -23,6 +24,7 @@ from pylint.checkers.utils import ( get_import_name, in_type_checking_block, is_from_fallback_block, + is_module_ignored, is_sys_guard, node_ignores_exception, ) @@ -37,11 +39,6 @@ from pylint.utils.linterstats import LinterStats if TYPE_CHECKING: from pylint.lint import PyLinter -if sys.version_info >= (3, 8): - from functools import cached_property -else: - from astroid.decorators import cachedproperty as cached_property - # The dictionary with Any should actually be a _ImportTree again # but mypy doesn't support recursive types yet @@ -84,18 +81,6 @@ DEPRECATED_MODULES = { } -def _qualified_names(modname: str | None) -> list[str]: - """Split the names of the given module into subparts. - - For example, - _qualified_names('pylint.checkers.ImportsChecker') - returns - ['pylint', 'pylint.checkers', 'pylint.checkers.ImportsChecker'] - """ - names = modname.split(".") if modname is not None else "" - return [".".join(names[0 : i + 1]) for i in range(len(names))] - - def _get_first_import( node: ImportNode, context: nodes.LocalsDictNodeNG, @@ -153,12 +138,11 @@ def _get_first_import( def _ignore_import_failure( node: ImportNode, - modname: str | None, + modname: str, ignored_modules: Sequence[str], ) -> bool: - for submodule in _qualified_names(modname): - if submodule in ignored_modules: - return True + if is_module_ignored(modname, ignored_modules): + return True # Ignore import failure if part of guarded import block # I.e. `sys.version_info` or `typing.TYPE_CHECKING` @@ -852,7 +836,7 @@ class ImportsChecker(DeprecatedMixin, BaseChecker): return std_imports, external_imports, local_imports def _get_imported_module( - self, importnode: ImportNode, modname: str | None + self, importnode: ImportNode, modname: str ) -> nodes.Module | None: try: return importnode.do_import_module(modname) diff --git a/pylint/checkers/lambda_expressions.py b/pylint/checkers/lambda_expressions.py index 69c5b738e..18c03060d 100644 --- a/pylint/checkers/lambda_expressions.py +++ b/pylint/checkers/lambda_expressions.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 diff --git a/pylint/checkers/logging.py b/pylint/checkers/logging.py index 3064dc926..461047bdf 100644 --- a/pylint/checkers/logging.py +++ b/pylint/checkers/logging.py @@ -1,14 +1,13 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Checker for use of Python logging.""" from __future__ import annotations import string -import sys -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Literal import astroid from astroid import bases, nodes @@ -19,11 +18,6 @@ from pylint.checkers import utils from pylint.checkers.utils import infer_all from pylint.typing import MessageDefinitionTuple -if sys.version_info >= (3, 8): - from typing import Literal -else: - from typing_extensions import Literal - if TYPE_CHECKING: from pylint.lint import PyLinter diff --git a/pylint/checkers/mapreduce_checker.py b/pylint/checkers/mapreduce_checker.py deleted file mode 100644 index 96e86d7c0..000000000 --- a/pylint/checkers/mapreduce_checker.py +++ /dev/null @@ -1,32 +0,0 @@ -# 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt - -from __future__ import annotations - -import abc -import warnings -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: - from pylint.lint import PyLinter - - -class MapReduceMixin(metaclass=abc.ABCMeta): - """A mixin design to allow multi-process/threaded runs of a Checker.""" - - def __init__(self) -> None: - warnings.warn( - "MapReduceMixin has been deprecated and will be removed in pylint 3.0. " - "To make a checker reduce map data simply implement get_map_data and reduce_map_data.", - DeprecationWarning, - stacklevel=2, - ) - - @abc.abstractmethod - def get_map_data(self) -> Any: - """Returns merge-able/reducible data that will be examined.""" - - @abc.abstractmethod - def reduce_map_data(self, linter: PyLinter, data: list[Any]) -> None: - """For a given Checker, receives data for all mapped runs.""" diff --git a/pylint/checkers/method_args.py b/pylint/checkers/method_args.py index 8862328e3..59083fa25 100644 --- a/pylint/checkers/method_args.py +++ b/pylint/checkers/method_args.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Variables checkers for Python code.""" diff --git a/pylint/checkers/misc.py b/pylint/checkers/misc.py index 8f6495735..f81dc43e7 100644 --- a/pylint/checkers/misc.py +++ b/pylint/checkers/misc.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Check source code is ascii only or has an encoding declaration (PEP 263).""" @@ -29,6 +29,7 @@ class ByIdManagedMessagesChecker(BaseRawFileChecker): "%s", "use-symbolic-message-instead", "Used when a message is enabled or disabled by id.", + {"default_enabled": False}, ) } options = () diff --git a/pylint/checkers/modified_iterating_checker.py b/pylint/checkers/modified_iterating_checker.py index bdc8fff7f..9d89f7255 100644 --- a/pylint/checkers/modified_iterating_checker.py +++ b/pylint/checkers/modified_iterating_checker.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 diff --git a/pylint/checkers/nested_min_max.py b/pylint/checkers/nested_min_max.py index e9aa409f0..219382ff5 100644 --- a/pylint/checkers/nested_min_max.py +++ b/pylint/checkers/nested_min_max.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Check for use of nested min/max functions.""" @@ -10,6 +10,7 @@ import copy from typing import TYPE_CHECKING from astroid import nodes, objects +from astroid.const import Context from pylint.checkers import BaseChecker from pylint.checkers.utils import only_required_for_messages, safe_infer @@ -96,7 +97,20 @@ class NestedMinMaxChecker(BaseChecker): if isinstance( inferred, (nodes.List, nodes.Tuple, nodes.Set, *DICT_TYPES) ): - splat_node = nodes.Starred(lineno=inferred.lineno) + splat_node = nodes.Starred( + ctx=Context.Load, + lineno=inferred.lineno, + col_offset=0, + parent=nodes.NodeNG( + lineno=None, + col_offset=None, + end_lineno=None, + end_col_offset=None, + parent=None, + ), + end_lineno=0, + end_col_offset=0, + ) splat_node.value = arg fixed_node.args = ( fixed_node.args[:idx] diff --git a/pylint/checkers/newstyle.py b/pylint/checkers/newstyle.py index 0aed97183..392faeaea 100644 --- a/pylint/checkers/newstyle.py +++ b/pylint/checkers/newstyle.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Check for new / old style related problems.""" diff --git a/pylint/checkers/non_ascii_names.py b/pylint/checkers/non_ascii_names.py index 455e575ad..825db1b11 100644 --- a/pylint/checkers/non_ascii_names.py +++ b/pylint/checkers/non_ascii_names.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """All alphanumeric unicode character are allowed in Python but due to similarities in how they look they can be confused. diff --git a/pylint/checkers/raw_metrics.py b/pylint/checkers/raw_metrics.py index 3469dc15e..ef4535345 100644 --- a/pylint/checkers/raw_metrics.py +++ b/pylint/checkers/raw_metrics.py @@ -1,22 +1,16 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 -import sys import tokenize -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any, Literal, cast from pylint.checkers import BaseTokenChecker from pylint.reporters.ureports.nodes import Paragraph, Section, Table, Text from pylint.utils import LinterStats, diff_string -if sys.version_info >= (3, 8): - from typing import Literal -else: - from typing_extensions import Literal - if TYPE_CHECKING: from pylint.lint import PyLinter diff --git a/pylint/checkers/refactoring/__init__.py b/pylint/checkers/refactoring/__init__.py index d215dcee2..785ce3f96 100644 --- a/pylint/checkers/refactoring/__init__.py +++ b/pylint/checkers/refactoring/__init__.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Looks for code which can be refactored.""" diff --git a/pylint/checkers/refactoring/implicit_booleaness_checker.py b/pylint/checkers/refactoring/implicit_booleaness_checker.py index 73503f8e6..5b91990b0 100644 --- a/pylint/checkers/refactoring/implicit_booleaness_checker.py +++ b/pylint/checkers/refactoring/implicit_booleaness_checker.py @@ -1,9 +1,11 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 +import itertools + import astroid from astroid import bases, nodes, util @@ -12,6 +14,14 @@ from pylint.checkers import utils from pylint.interfaces import HIGH, INFERENCE +def _is_constant_zero(node: str | nodes.NodeNG) -> bool: + # We have to check that node.value is not False because node.value == 0 is True + # when node.value is False + return ( + isinstance(node, astroid.Const) and node.value == 0 and node.value is not False + ) + + class ImplicitBooleanessChecker(checkers.BaseChecker): """Checks for incorrect usage of comparisons or len() inside conditions. @@ -56,23 +66,47 @@ class ImplicitBooleanessChecker(checkers.BaseChecker): "C1802": ( "Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty", "use-implicit-booleaness-not-len", - "Used when Pylint detects that len(sequence) is being used " - "without explicit comparison inside a condition to determine if a sequence is empty. " - "Instead of coercing the length to a boolean, either " - "rely on the fact that empty sequences are false or " - "compare the length against a scalar.", + "Empty sequences are considered false in a boolean context. You can either" + " remove the call to 'len' (``if not x``) or compare the length against a" + "scalar (``if len(x) > 1``).", {"old_names": [("C1801", "len-as-condition")]}, ), "C1803": ( - "'%s' can be simplified to '%s' as an empty %s is falsey", + '"%s" can be simplified to "%s", if it is strictly a sequence, as an empty %s is falsey', "use-implicit-booleaness-not-comparison", - "Used when Pylint detects that collection literal comparison is being " - "used to check for emptiness; Use implicit booleaness instead " - "of a collection classes; empty collections are considered as false", + "Empty sequences are considered false in a boolean context. Following this" + " check blindly in weakly typed code base can create hard to debug issues." + " If the value can be something else that is falsey but not a sequence (for" + " example ``None``, an empty string, or ``0``) the code will not be " + "equivalent.", + ), + "C1804": ( + '"%s" can be simplified to "%s", if it is striclty a string, as an empty string is falsey', + "use-implicit-booleaness-not-comparison-to-string", + "Empty string are considered false in a boolean context. Following this" + " check blindly in weakly typed code base can create hard to debug issues." + " If the value can be something else that is falsey but not a string (for" + " example ``None``, an empty sequence, or ``0``) the code will not be " + "equivalent.", + { + "default_enabled": False, + "old_names": [("C1901", "compare-to-empty-string")], + }, + ), + "C1805": ( + '"%s" can be simplified to "%s", if it is strictly an int, as 0 is falsey', + "use-implicit-booleaness-not-comparison-to-zero", + "0 is considered false in a boolean context. Following this" + " check blindly in weakly typed code base can create hard to debug issues." + " If the value can be something else that is falsey but not an int (for" + " example ``None``, an empty string, or an empty sequence) the code will not be " + "equivalent.", + {"default_enabled": False, "old_names": [("C2001", "compare-to-zero")]}, ), } options = () + _operators = {"!=", "==", "is not", "is"} @utils.only_required_for_messages("use-implicit-booleaness-not-len") def visit_call(self, node: nodes.Call) -> None: @@ -146,9 +180,83 @@ class ImplicitBooleanessChecker(checkers.BaseChecker): "use-implicit-booleaness-not-len", node=node, confidence=HIGH ) - @utils.only_required_for_messages("use-implicit-booleaness-not-comparison") + @utils.only_required_for_messages( + "use-implicit-booleaness-not-comparison", + "use-implicit-booleaness-not-comparison-to-string", + "use-implicit-booleaness-not-comparison-to-zero", + ) def visit_compare(self, node: nodes.Compare) -> None: - self._check_use_implicit_booleaness_not_comparison(node) + if self.linter.is_message_enabled("use-implicit-booleaness-not-comparison"): + self._check_use_implicit_booleaness_not_comparison(node) + if self.linter.is_message_enabled( + "use-implicit-booleaness-not-comparison-to-zero" + ) or self.linter.is_message_enabled( + "use-implicit-booleaness-not-comparison-to-str" + ): + self._check_compare_to_str_or_zero(node) + + def _check_compare_to_str_or_zero(self, node: nodes.Compare) -> None: + # note: astroid.Compare has the left most operand in node.left + # while the rest are a list of tuples in node.ops + # the format of the tuple is ('compare operator sign', node) + # here we squash everything into `ops` to make it easier for processing later + ops: list[tuple[str, nodes.NodeNG]] = [("", node.left), *node.ops] + iter_ops = iter(ops) + all_ops = list(itertools.chain(*iter_ops)) + for ops_idx in range(len(all_ops) - 2): + op_2 = all_ops[ops_idx + 1] + if op_2 not in self._operators: + continue + op_1 = all_ops[ops_idx] + op_3 = all_ops[ops_idx + 2] + error_detected = False + if self.linter.is_message_enabled( + "use-implicit-booleaness-not-comparison-to-zero" + ): + # 0 ?? X + if _is_constant_zero(op_1): + error_detected = True + op = op_3 + # X ?? 0 + elif _is_constant_zero(op_3): + error_detected = True + op = op_1 + if error_detected: + original = f"{op_1.as_string()} {op_2} {op_3.as_string()}" + suggestion = ( + op.as_string() + if op_2 in {"!=", "is not"} + else f"not {op.as_string()}" + ) + self.add_message( + "use-implicit-booleaness-not-comparison-to-zero", + args=(original, suggestion), + node=node, + confidence=HIGH, + ) + error_detected = False + if self.linter.is_message_enabled( + "use-implicit-booleaness-not-comparison-to-str" + ): + node_name = "" + # x ?? "" + if utils.is_empty_str_literal(op_1): + error_detected = True + node_name = op_3.as_string() + # '' ?? X + elif utils.is_empty_str_literal(op_3): + error_detected = True + node_name = op_1.as_string() + if error_detected: + suggestion = ( + f"not {node_name}" if op_2 in {"==", "is"} else node_name + ) + self.add_message( + "use-implicit-booleaness-not-comparison-to-string", + args=(node.as_string(), suggestion), + node=node, + confidence=HIGH, + ) def _check_use_implicit_booleaness_not_comparison( self, node: nodes.Compare diff --git a/pylint/checkers/refactoring/not_checker.py b/pylint/checkers/refactoring/not_checker.py index e356a70e1..555e21099 100644 --- a/pylint/checkers/refactoring/not_checker.py +++ b/pylint/checkers/refactoring/not_checker.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt import astroid from astroid import nodes diff --git a/pylint/checkers/refactoring/recommendation_checker.py b/pylint/checkers/refactoring/recommendation_checker.py index 3a6d1033c..db2e2f604 100644 --- a/pylint/checkers/refactoring/recommendation_checker.py +++ b/pylint/checkers/refactoring/recommendation_checker.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 @@ -54,10 +54,10 @@ class RecommendationChecker(checkers.BaseChecker): "are more efficient than ``sets``.", ), "C0209": ( - "Formatting a regular string which could be a f-string", + "Formatting a regular string which could be an f-string", "consider-using-f-string", "Used when we detect a string that is being formatted with format() or % " - "which could potentially be a f-string. The use of f-strings is preferred. " + "which could potentially be an f-string. The use of f-strings is preferred. " "Requires Python 3.6 and ``py-version >= 3.6``.", ), } diff --git a/pylint/checkers/refactoring/refactoring_checker.py b/pylint/checkers/refactoring/refactoring_checker.py index ec4d3d71e..5d0a2dd7a 100644 --- a/pylint/checkers/refactoring/refactoring_checker.py +++ b/pylint/checkers/refactoring/refactoring_checker.py @@ -1,16 +1,15 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 import collections import copy import itertools -import sys import tokenize from collections.abc import Iterator -from functools import reduce +from functools import cached_property, reduce from re import Pattern from typing import TYPE_CHECKING, Any, NamedTuple, Union, cast @@ -27,10 +26,6 @@ from pylint.interfaces import HIGH, INFERENCE, Confidence if TYPE_CHECKING: from pylint.lint import PyLinter -if sys.version_info >= (3, 8): - from functools import cached_property -else: - from astroid.decorators import cachedproperty as cached_property NodesWithNestedBlocks = Union[ nodes.TryExcept, nodes.TryFinally, nodes.While, nodes.For, nodes.If @@ -98,7 +93,7 @@ def _is_trailing_comma(tokens: list[tokenize.TokenInfo], index: int) -> bool: if token.exact_type != tokenize.COMMA: return False # Must have remaining tokens on the same line such as NEWLINE - left_tokens = list(itertools.islice(tokens, index + 1, None)) + left_tokens = itertools.islice(tokens, index + 1, None) more_tokens_on_line = False for remaining_token in left_tokens: @@ -644,9 +639,10 @@ class RefactoringChecker(checkers.BaseTokenChecker): # token[2] is the actual position and also is # reported by IronPython. self._elifs.extend([token[2], tokens[index + 1][2]]) - elif _is_trailing_comma(tokens, index): - if self.linter.is_message_enabled("trailing-comma-tuple"): - self.add_message("trailing-comma-tuple", line=token.start[0]) + elif self.linter.is_message_enabled( + "trailing-comma-tuple" + ) and _is_trailing_comma(tokens, index): + self.add_message("trailing-comma-tuple", line=token.start[0]) @utils.only_required_for_messages("consider-using-with") def leave_module(self, _: nodes.Module) -> None: @@ -1060,8 +1056,8 @@ class RefactoringChecker(checkers.BaseTokenChecker): def _check_consider_using_generator(self, node: nodes.Call) -> None: # 'any', 'all', definitely should use generator, while 'list', 'tuple', # 'sum', 'max', and 'min' need to be considered first - # See https://github.com/PyCQA/pylint/pull/3309#discussion_r576683109 - # https://github.com/PyCQA/pylint/pull/6595#issuecomment-1125704244 + # See https://github.com/pylint-dev/pylint/pull/3309#discussion_r576683109 + # https://github.com/pylint-dev/pylint/pull/6595#issuecomment-1125704244 # and https://peps.python.org/pep-0289/ checked_call = ["any", "all", "sum", "max", "min", "list", "tuple"] if ( @@ -1175,7 +1171,7 @@ class RefactoringChecker(checkers.BaseTokenChecker): if len(node.args) == 0: # handle case when builtin.next is called without args. - # see https://github.com/PyCQA/pylint/issues/7828 + # see https://github.com/pylint-dev/pylint/issues/7828 return inferred = utils.safe_infer(node.func) diff --git a/pylint/checkers/similar.py b/pylint/checkers/similar.py index 2cfba16bf..2b08204f2 100644 --- a/pylint/checkers/similar.py +++ b/pylint/checkers/similar.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """A similarities / code duplication command line tool and pylint checker. @@ -848,15 +848,17 @@ class SimilarChecker(BaseRawFileChecker, Similar): stream must implement the readlines method """ if self.linter.current_name is None: + # TODO: 3.0 Fix current_name warnings.warn( ( "In pylint 3.0 the current_name attribute of the linter object should be a string. " "If unknown it should be initialized as an empty string." ), DeprecationWarning, + stacklevel=2, ) with node.stream() as stream: - self.append_stream(self.linter.current_name, stream, node.file_encoding) # type: ignore[arg-type] + self.append_stream(self.linter.current_name, stream, node.file_encoding) def close(self) -> None: """Compute and display similarities on closing (i.e. end of parsing).""" diff --git a/pylint/checkers/spelling.py b/pylint/checkers/spelling.py index 3fbd017f8..91161c60d 100644 --- a/pylint/checkers/spelling.py +++ b/pylint/checkers/spelling.py @@ -1,27 +1,21 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Checker for spelling errors in comments and docstrings.""" from __future__ import annotations import re -import sys import tokenize from re import Pattern -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Literal from astroid import nodes from pylint.checkers import BaseTokenChecker from pylint.checkers.utils import only_required_for_messages -if sys.version_info >= (3, 8): - from typing import Literal -else: - from typing_extensions import Literal - if TYPE_CHECKING: from pylint.lint import PyLinter diff --git a/pylint/checkers/stdlib.py b/pylint/checkers/stdlib.py index 96b273c74..edc79867f 100644 --- a/pylint/checkers/stdlib.py +++ b/pylint/checkers/stdlib.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Checkers for various standard library functions.""" @@ -25,7 +25,7 @@ if TYPE_CHECKING: DeprecationDict = Dict[Tuple[int, int, int], Set[str]] OPEN_FILES_MODE = ("open", "file") -OPEN_FILES_FUNCS = OPEN_FILES_MODE + ("read_text", "write_text") +OPEN_FILES_FUNCS = (*OPEN_FILES_MODE, "read_text", "write_text") UNITTEST_CASE = "unittest.case" THREADING_THREAD = "threading.Thread" COPY_COPY = "copy.copy" diff --git a/pylint/checkers/strings.py b/pylint/checkers/strings.py index 380d87fb0..2cc780da5 100644 --- a/pylint/checkers/strings.py +++ b/pylint/checkers/strings.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Checker for string formatting operations.""" @@ -8,11 +8,10 @@ from __future__ import annotations import collections import re -import sys import tokenize from collections import Counter from collections.abc import Iterable, Sequence -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Literal import astroid from astroid import bases, nodes, util @@ -26,11 +25,6 @@ from pylint.typing import MessageDefinitionTuple if TYPE_CHECKING: from pylint.lint import PyLinter -if sys.version_info >= (3, 8): - from typing import Literal -else: - from typing_extensions import Literal - _AST_NODE_STR_TYPES = ("__builtin__.unicode", "__builtin__.str", "builtins.str") # Prefixes for both strings and bytes literals per @@ -816,7 +810,7 @@ class StringConstantChecker(BaseTokenChecker, BaseRawFileChecker): token_index = (elt.lineno, elt.col_offset) if token_index not in self.string_tokens: # This may happen with Latin1 encoding - # cf. https://github.com/PyCQA/pylint/issues/2610 + # cf. https://github.com/pylint-dev/pylint/issues/2610 continue matching_token, next_token = self.string_tokens[token_index] # We detect string concatenation: the AST Const is the diff --git a/pylint/checkers/threading_checker.py b/pylint/checkers/threading_checker.py index df0dfe7cf..b289d6707 100644 --- a/pylint/checkers/threading_checker.py +++ b/pylint/checkers/threading_checker.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index 1a628f231..97a7460a8 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Try to find more bugs in the code using astroid inference capabilities.""" @@ -14,9 +14,9 @@ import shlex import sys import types from collections.abc import Callable, Iterable, Iterator, Sequence -from functools import singledispatch +from functools import cached_property, singledispatch from re import Pattern -from typing import TYPE_CHECKING, Any, TypeVar, Union +from typing import TYPE_CHECKING, Any, Literal, TypeVar, Union import astroid import astroid.exceptions @@ -53,13 +53,6 @@ from pylint.constants import PY310_PLUS from pylint.interfaces import HIGH, INFERENCE from pylint.typing import MessageDefinitionTuple -if sys.version_info >= (3, 8): - from functools import cached_property - from typing import Literal -else: - from astroid.decorators import cachedproperty as cached_property - from typing_extensions import Literal - if TYPE_CHECKING: from pylint.lint import PyLinter @@ -125,7 +118,7 @@ def _is_owner_ignored( matches any name from the *ignored_classes* or if its qualified name can be found in *ignored_classes*. """ - if is_module_ignored(owner.root(), ignored_modules): + if is_module_ignored(owner.root().qname(), ignored_modules): return True # Match against ignored classes. @@ -158,7 +151,7 @@ def _(node: nodes.ClassDef | bases.Instance) -> Iterable[str]: def _string_distance(seq1: str, seq2: str) -> int: seq2_length = len(seq2) - row = list(range(1, seq2_length + 1)) + [0] + row = [*list(range(1, seq2_length + 1)), 0] for seq1_index, seq1_char in enumerate(seq1): last_row = row row = [0] * seq2_length + [seq1_index + 1] @@ -733,7 +726,9 @@ def _no_context_variadic( else: inferred_statement = inferred.statement(future=True) - if not length and isinstance(inferred_statement, nodes.Lambda): + if not length and isinstance( + inferred_statement, (nodes.Lambda, nodes.FunctionDef) + ): is_in_starred_context = _has_parent_of_type(node, variadic_type, statement) used_as_starred_argument = any( variadic.value == name or variadic.value.parent_of(name) @@ -1569,6 +1564,11 @@ accessed. Python regular expressions are accepted.", node=node, args=(keyword, callable_name), ) + elif ( + keyword in [arg.name for arg in called.args.posonlyargs] + and called.args.kwarg + ): + pass else: parameters[i] = (parameters[i][0], True) elif keyword in kwparams: @@ -1654,7 +1654,7 @@ accessed. Python regular expressions are accepted.", if not isinstance(inferred, nodes.FunctionDef): return False - for return_value in inferred.infer_call_result(): + for return_value in inferred.infer_call_result(caller=None): # infer_call_result() returns nodes.Const.None for None return values # so this also catches non-returning decorators if not isinstance(return_value, nodes.FunctionDef): @@ -1685,9 +1685,9 @@ accessed. Python regular expressions are accepted.", # Determine what method on the parent this index will use # The parent of this node will be a Subscript, and the parent of that # node determines if the Subscript is a get, set, or delete operation. - if subscript.ctx is astroid.Store: + if subscript.ctx is astroid.Context.Store: methodname = "__setitem__" - elif subscript.ctx is astroid.Del: + elif subscript.ctx is astroid.Context.Del: methodname = "__delitem__" else: methodname = "__getitem__" @@ -2015,7 +2015,7 @@ accessed. Python regular expressions are accepted.", # TODO: This check was disabled (by adding the leading underscore) # due to false positives several years ago - can we re-enable it? - # https://github.com/PyCQA/pylint/issues/6359 + # https://github.com/pylint-dev/pylint/issues/6359 @only_required_for_messages("unsupported-binary-operation") def _visit_binop(self, node: nodes.BinOp) -> None: """Detect TypeErrors for binary arithmetic operands.""" @@ -2023,7 +2023,7 @@ accessed. Python regular expressions are accepted.", # TODO: This check was disabled (by adding the leading underscore) # due to false positives several years ago - can we re-enable it? - # https://github.com/PyCQA/pylint/issues/6359 + # https://github.com/pylint-dev/pylint/issues/6359 @only_required_for_messages("unsupported-binary-operation") def _visit_augassign(self, node: nodes.AugAssign) -> None: """Detect TypeErrors for augmented binary arithmetic operands.""" @@ -2109,13 +2109,13 @@ accessed. Python regular expressions are accepted.", confidence=INFERENCE, ) - if node.ctx == astroid.Load: + if node.ctx == astroid.Context.Load: supported_protocol = supports_getitem msg = "unsubscriptable-object" - elif node.ctx == astroid.Store: + elif node.ctx == astroid.Context.Store: supported_protocol = supports_setitem msg = "unsupported-assignment-operation" - elif node.ctx == astroid.Del: + elif node.ctx == astroid.Context.Del: supported_protocol = supports_delitem msg = "unsupported-delete-operation" diff --git a/pylint/checkers/unicode.py b/pylint/checkers/unicode.py index c7e011b6f..ab8410c3e 100644 --- a/pylint/checkers/unicode.py +++ b/pylint/checkers/unicode.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Unicode and some other ASCII characters can be used to create programs that run much different compared to what a human reader would expect from them. diff --git a/pylint/checkers/unsupported_version.py b/pylint/checkers/unsupported_version.py index 0c4a7c5bb..64f2630d8 100644 --- a/pylint/checkers/unsupported_version.py +++ b/pylint/checkers/unsupported_version.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Checker for features used that are not supported by all python versions indicated by the py-version setting. @@ -34,7 +34,7 @@ class UnsupportedVersionChecker(BaseChecker): "F-strings are not supported by all versions included in the py-version setting", "using-f-string-in-unsupported-version", "Used when the py-version set by the user is lower than 3.6 and pylint encounters " - "a f-string.", + "an f-string.", ), "W2602": ( "typing.final is not supported by all versions included in the py-version setting", diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index 771b21fff..6b66ad620 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Some functions that may be useful for various checkers.""" @@ -12,8 +12,6 @@ import itertools import numbers import re import string -import warnings -from collections import deque from collections.abc import Iterable, Iterator from functools import lru_cache, partial from re import Match @@ -249,17 +247,6 @@ class InferredTypeError(Exception): pass -def is_inside_lambda(node: nodes.NodeNG) -> bool: - """Return whether the given node is inside a lambda.""" - warnings.warn( - "utils.is_inside_lambda will be removed in favour of calling " - "utils.get_node_first_ancestor_of_type(x, nodes.Lambda) in pylint 3.0", - DeprecationWarning, - stacklevel=2, - ) - return any(isinstance(parent, nodes.Lambda) for parent in node.node_ancestors()) - - def get_all_elements( node: nodes.NodeNG, ) -> Iterable[nodes.NodeNG]: @@ -369,9 +356,10 @@ def is_defined_before(var_node: nodes.Name) -> bool: if defnode is None: continue defnode_scope = defnode.scope() - if isinstance(defnode_scope, COMP_NODE_TYPES + (nodes.Lambda,)): + if isinstance( + defnode_scope, (*COMP_NODE_TYPES, nodes.Lambda, nodes.FunctionDef) + ): # Avoid the case where var_node_scope is a nested function - # FunctionDef is a Lambda until https://github.com/PyCQA/astroid/issues/291 if isinstance(defnode_scope, nodes.FunctionDef): var_node_scope = var_node.scope() if var_node_scope is not defnode_scope and isinstance( @@ -505,25 +493,6 @@ def only_required_for_messages( return store_messages -def check_messages( - *messages: str, -) -> Callable[ - [AstCallbackMethod[_CheckerT, _NodeT]], AstCallbackMethod[_CheckerT, _NodeT] -]: - """Kept for backwards compatibility, deprecated. - - Use only_required_for_messages instead, which conveys the intent of the decorator much clearer. - """ - warnings.warn( - "utils.check_messages will be removed in favour of calling " - "utils.only_required_for_messages in pylint 3.0", - DeprecationWarning, - stacklevel=2, - ) - - return only_required_for_messages(*messages) - - class IncompleteFormatString(Exception): """A format string ended in the middle of a format specifier.""" @@ -1162,6 +1131,7 @@ def node_ignores_exception( return any(get_contextlib_suppressors(node, exception)) +@lru_cache(maxsize=1024) def class_is_abstract(node: nodes.ClassDef) -> bool: """Return true if the given class node should be considered as an abstract class. @@ -1572,27 +1542,6 @@ def is_postponed_evaluation_enabled(node: nodes.NodeNG) -> bool: return "annotations" in module.future_imports -def is_class_subscriptable_pep585_with_postponed_evaluation_enabled( - value: nodes.ClassDef, node: nodes.NodeNG -) -> bool: - """Check if class is subscriptable with PEP 585 and - postponed evaluation enabled. - """ - warnings.warn( - "'is_class_subscriptable_pep585_with_postponed_evaluation_enabled' has been " - "deprecated and will be removed in pylint 3.0. " - "Use 'is_postponed_evaluation_enabled(node) and " - "is_node_in_type_annotation_context(node)' instead.", - DeprecationWarning, - stacklevel=2, - ) - return ( - is_postponed_evaluation_enabled(node) - and value.qname() in SUBSCRIPTABLE_CLASSES_PEP585 - and is_node_in_type_annotation_context(node) - ) - - def is_node_in_type_annotation_context(node: nodes.NodeNG) -> bool: """Check if node is in type annotation context. @@ -1843,48 +1792,6 @@ def is_sys_guard(node: nodes.If) -> bool: return False -def is_typing_guard(node: nodes.If) -> bool: - """Return True if IF stmt is a typing guard. - - >>> from typing import TYPE_CHECKING - >>> if TYPE_CHECKING: - >>> from xyz import a - """ - warnings.warn( - "This method will be removed in pylint 3.0; use in_type_checking_block() instead.", - DeprecationWarning, - stacklevel=2, - ) # pragma: no cover - return isinstance( - node.test, (nodes.Name, nodes.Attribute) - ) and node.test.as_string().endswith("TYPE_CHECKING") - - -def is_node_in_typing_guarded_import_block(node: nodes.NodeNG) -> bool: - """Return True if node is part for guarded `typing.TYPE_CHECKING` if block.""" - warnings.warn( - "This method will be removed in pylint 3.0; use in_type_checking_block() instead.", - DeprecationWarning, - stacklevel=2, - ) # pragma: no cover - return isinstance(node.parent, nodes.If) and is_typing_guard(node.parent) - - -def is_node_in_guarded_import_block(node: nodes.NodeNG) -> bool: - """Return True if node is part for guarded if block. - - I.e. `sys.version_info` or `typing.TYPE_CHECKING` - """ - warnings.warn( - "This method will be removed in pylint 3.0; use in_type_checking_block() instead.", - DeprecationWarning, - stacklevel=2, - ) # pragma: no cover - return isinstance(node.parent, nodes.If) and ( - is_sys_guard(node.parent) or is_typing_guard(node.parent) - ) - - def is_reassigned_after_current(node: nodes.NodeNG, varname: str) -> bool: """Check if the given variable name is reassigned in the same scope after the current node. @@ -2035,7 +1942,7 @@ def is_typing_member(node: nodes.NodeNG, names_to_check: tuple[str, ...]) -> boo return False -@lru_cache() +@lru_cache def in_for_else_branch(parent: nodes.NodeNG, stmt: nodes.Statement) -> bool: """Returns True if stmt is inside the else branch for a parent For stmt.""" return isinstance(parent, nodes.For) and any( @@ -2144,37 +2051,30 @@ def is_augmented_assign(node: nodes.Assign) -> tuple[bool, str]: return False, "" +def _qualified_name_parts(qualified_module_name: str) -> list[str]: + """Split the names of the given module into subparts. + + For example, + _qualified_name_parts('pylint.checkers.ImportsChecker') + returns + ['pylint', 'pylint.checkers', 'pylint.checkers.ImportsChecker'] + """ + names = qualified_module_name.split(".") + return [".".join(names[0 : i + 1]) for i in range(len(names))] + + def is_module_ignored( - module: nodes.Module, - ignored_modules: Iterable[str], + qualified_module_name: str, ignored_modules: Iterable[str] ) -> bool: ignored_modules = set(ignored_modules) - module_name = module.name - module_qname = module.qname() - - for ignore in ignored_modules: - # Try to match the module name / fully qualified name directly - if module_qname in ignored_modules or module_name in ignored_modules: - return True - - # Try to see if the ignores pattern match against the module name. - if fnmatch.fnmatch(module_qname, ignore): + for current_module in _qualified_name_parts(qualified_module_name): + # Try to match the module name directly + if current_module in ignored_modules: return True - - # Otherwise, we might have a root module name being ignored, - # and the qualified owner has more levels of depth. - parts = deque(module_name.split(".")) - current_module = "" - - while parts: - part = parts.popleft() - if not current_module: - current_module = part - else: - current_module += f".{part}" - if current_module in ignored_modules: + for ignore in ignored_modules: + # Try to see if the ignores pattern match against the module name. + if fnmatch.fnmatch(current_module, ignore): return True - return False @@ -2214,53 +2114,6 @@ def is_class_attr(name: str, klass: nodes.ClassDef) -> bool: return False -def is_defined(name: str, node: nodes.NodeNG) -> bool: - """Searches for a tree node that defines the given variable name.""" - is_defined_so_far = False - - if isinstance(node, nodes.NamedExpr): - is_defined_so_far = node.target.name == name - - if isinstance(node, (nodes.Import, nodes.ImportFrom)): - is_defined_so_far = any(node_name[0] == name for node_name in node.names) - - if isinstance(node, nodes.With): - is_defined_so_far = any( - isinstance(item[1], nodes.AssignName) and item[1].name == name - for item in node.items - ) - - if isinstance(node, (nodes.ClassDef, nodes.FunctionDef)): - is_defined_so_far = node.name == name - - if isinstance(node, nodes.AnnAssign): - is_defined_so_far = ( - node.value - and isinstance(node.target, nodes.AssignName) - and node.target.name == name - ) - - if isinstance(node, nodes.Assign): - is_defined_so_far = any( - any( - ( - ( - isinstance(elt, nodes.Starred) - and isinstance(elt.value, nodes.AssignName) - and elt.value.name == name - ) - or (isinstance(elt, nodes.AssignName) and elt.name == name) - ) - for elt in get_all_elements(target) - ) - for target in node.targets - ) - - return is_defined_so_far or any( - is_defined(name, child) for child in node.get_children() - ) - - def get_inverse_comparator(op: str) -> str: """Returns the inverse comparator given a comparator. @@ -2317,12 +2170,30 @@ def not_condition_as_string( return msg +@lru_cache(maxsize=1000) +def overridden_method( + klass: nodes.LocalsDictNodeNG, name: str | None +) -> nodes.FunctionDef | None: + """Get overridden method if any.""" + try: + parent = next(klass.local_attr_ancestors(name)) + except (StopIteration, KeyError): + return None + try: + meth_node = parent[name] + except KeyError: # pragma: no cover + # We have found an ancestor defining <name> but it's not in the local + # dictionary. This may happen with astroid built from living objects. + return None + if isinstance(meth_node, nodes.FunctionDef): + return meth_node + return None # pragma: no cover + + def clear_lru_caches() -> None: """Clear caches holding references to AST nodes.""" - # pylint: disable-next=import-outside-toplevel - from pylint.checkers.variables import overridden_method - caches_holding_node_references: list[_lru_cache_wrapper[Any]] = [ + class_is_abstract, in_for_else_branch, infer_all, is_overload_stub, diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index 8cd1ed83c..2504c0c84 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Variables checkers for Python code.""" @@ -11,11 +11,10 @@ import copy import itertools import os import re -import sys from collections import defaultdict from collections.abc import Generator, Iterable, Iterator from enum import Enum -from functools import lru_cache +from functools import cached_property from typing import TYPE_CHECKING, Any, NamedTuple import astroid @@ -26,18 +25,15 @@ from astroid.typing import InferenceResult from pylint.checkers import BaseChecker, utils from pylint.checkers.utils import ( in_type_checking_block, + is_module_ignored, is_postponed_evaluation_enabled, is_sys_guard, + overridden_method, ) from pylint.constants import PY39_PLUS, TYPING_NEVER, TYPING_NORETURN from pylint.interfaces import CONTROL_FLOW, HIGH, INFERENCE, INFERENCE_FAILURE from pylint.typing import MessageDefinitionTuple -if sys.version_info >= (3, 8): - from functools import cached_property -else: - from astroid.decorators import cachedproperty as cached_property - if TYPE_CHECKING: from pylint.lint import PyLinter @@ -151,26 +147,6 @@ def _is_from_future_import(stmt: nodes.ImportFrom, name: str) -> bool | None: return None -@lru_cache(maxsize=1000) -def overridden_method( - klass: nodes.LocalsDictNodeNG, name: str | None -) -> nodes.FunctionDef | None: - """Get overridden method if any.""" - try: - parent = next(klass.local_attr_ancestors(name)) - except (StopIteration, KeyError): - return None - try: - meth_node = parent[name] - except KeyError: - # We have found an ancestor defining <name> but it's not in the local - # dictionary. This may happen with astroid built from living objects. - return None - if isinstance(meth_node, nodes.FunctionDef): - return meth_node - return None - - def _get_unpacking_extra_info(node: nodes.Assign, inferred: InferenceResult) -> str: """Return extra information to add to the message for unpacking-non-sequence and unbalanced-tuple/dict-unpacking errors. @@ -242,17 +218,11 @@ def _detect_global_scope( return node.lineno < defframe.lineno # type: ignore[no-any-return] if not isinstance(node.parent, (nodes.FunctionDef, nodes.Arguments)): return False - elif any( - not isinstance(f, (nodes.ClassDef, nodes.Module)) for f in (frame, defframe) - ): - # Not interested in other frames, since they are already - # not in a global scope. - return False break_scopes = [] - for current_scope in (scope, def_scope): + for current_scope in (scope or frame, def_scope): # Look for parent scopes. If there is anything different - # than a module or a class scope, then they frames don't + # than a module or a class scope, then the frames don't # share a global scope. parent_scope = current_scope while parent_scope: @@ -263,7 +233,7 @@ def _detect_global_scope( parent_scope = parent_scope.parent.scope() else: break - if break_scopes and len(set(break_scopes)) != 1: + if len(set(break_scopes)) > 1: # Store different scopes than expected. # If the stored scopes are, in fact, the very same, then it means # that the two frames (frame and defframe) share the same scope, @@ -564,7 +534,6 @@ class NamesConsumer: copy.copy(node.locals), {}, collections.defaultdict(list), scope_type ) self.node = node - self._if_nodes_deemed_uncertain: set[nodes.If] = set() def __repr__(self) -> str: _to_consumes = [f"{k}->{v}" for k, v in self._atomic.to_consume.items()] @@ -715,26 +684,28 @@ scope_type : {self._atomic.scope_type} return found_nodes @staticmethod - def _exhaustively_define_name_raise_or_return( - name: str, node: nodes.NodeNG - ) -> bool: - """Return True if there is a collectively exhaustive set of paths under - this `if_node` that define `name`, raise, or return. + def _inferred_to_define_name_raise_or_return(name: str, node: nodes.NodeNG) -> bool: + """Return True if there is a path under this `if_node` + that is inferred to define `name`, raise, or return. """ # Handle try and with if isinstance(node, (nodes.TryExcept, nodes.TryFinally)): # Allow either a path through try/else/finally OR a path through ALL except handlers - return ( - NamesConsumer._defines_name_raises_or_returns_recursive(name, node) - or isinstance(node, nodes.TryExcept) - and all( - NamesConsumer._defines_name_raises_or_returns_recursive( - name, handler - ) - for handler in node.handlers + try_except_node = node + if isinstance(node, nodes.TryFinally): + try_except_node = next( + (child for child in node.nodes_of_class(nodes.TryExcept)), + None, ) + handlers = try_except_node.handlers if try_except_node else [] + return NamesConsumer._defines_name_raises_or_returns_recursive( + name, node + ) or all( + NamesConsumer._defines_name_raises_or_returns_recursive(name, handler) + for handler in handlers ) - if isinstance(node, nodes.With): + + if isinstance(node, (nodes.With, nodes.For, nodes.While)): return NamesConsumer._defines_name_raises_or_returns_recursive(name, node) if not isinstance(node, nodes.If): @@ -748,13 +719,29 @@ scope_type : {self._atomic.scope_type} if NamesConsumer._defines_name_raises_or_returns(name, node): return True - # If there is no else, then there is no collectively exhaustive set of paths - if not node.orelse: - return False + test = node.test.value if isinstance(node.test, nodes.NamedExpr) else node.test + all_inferred = utils.infer_all(test) + only_search_if = False + only_search_else = True + for inferred in all_inferred: + if not isinstance(inferred, nodes.Const): + only_search_else = False + continue + val = inferred.value + only_search_if = only_search_if or (val != NotImplemented and val) + only_search_else = only_search_else and not val + + # Only search else branch when test condition is inferred to be false + if all_inferred and only_search_else: + return NamesConsumer._branch_handles_name(name, node.orelse) + # Only search if branch when test condition is inferred to be true + if all_inferred and only_search_if: + return NamesConsumer._branch_handles_name(name, node.body) + # Search both if and else branches return NamesConsumer._branch_handles_name( name, node.body - ) and NamesConsumer._branch_handles_name(name, node.orelse) + ) or NamesConsumer._branch_handles_name(name, node.orelse) @staticmethod def _branch_handles_name(name: str, body: Iterable[nodes.NodeNG]) -> bool: @@ -762,9 +749,16 @@ scope_type : {self._atomic.scope_type} NamesConsumer._defines_name_raises_or_returns(name, if_body_stmt) or isinstance( if_body_stmt, - (nodes.If, nodes.TryExcept, nodes.TryFinally, nodes.With), + ( + nodes.If, + nodes.TryExcept, + nodes.TryFinally, + nodes.With, + nodes.For, + nodes.While, + ), ) - and NamesConsumer._exhaustively_define_name_raise_or_return( + and NamesConsumer._inferred_to_define_name_raise_or_return( name, if_body_stmt ) for if_body_stmt in body @@ -776,57 +770,80 @@ scope_type : {self._atomic.scope_type} """Identify nodes of uncertain execution because they are defined under tests that evaluate false. - Don't identify a node if there is a collectively exhaustive set of paths - that define the name, raise, or return (e.g. every if/else branch). + Don't identify a node if there is a path that is inferred to + define the name, raise, or return (e.g. any executed if/elif/else branch). """ uncertain_nodes = [] for other_node in found_nodes: - if in_type_checking_block(other_node): - continue - - if not isinstance(other_node, nodes.AssignName): + if isinstance(other_node, nodes.AssignName): + name = other_node.name + elif isinstance(other_node, (nodes.Import, nodes.ImportFrom)): + name = node.name + else: continue - closest_if = utils.get_node_first_ancestor_of_type(other_node, nodes.If) - if closest_if is None: - continue - if node.frame() is not closest_if.frame(): - continue - if closest_if is not None and closest_if.parent_of(node): + all_if = [ + n + for n in other_node.node_ancestors() + if isinstance(n, nodes.If) and not n.parent_of(node) + ] + if not all_if: continue - # Name defined in every if/else branch - if NamesConsumer._exhaustively_define_name_raise_or_return( - other_node.name, closest_if + closest_if = all_if[0] + if ( + isinstance(node, nodes.AssignName) + and node.frame() is not closest_if.frame() ): continue + if closest_if.parent_of(node): + continue - # Higher-level if already determined to be always false - if any( - if_node.parent_of(closest_if) - for if_node in self._if_nodes_deemed_uncertain - ): - uncertain_nodes.append(other_node) + outer_if = all_if[-1] + if NamesConsumer._node_guarded_by_same_test(node, outer_if): continue - # All inferred values must test false - if isinstance(closest_if.test, nodes.NamedExpr): - test = closest_if.test.value - else: - test = closest_if.test - all_inferred = utils.infer_all(test) - if not all_inferred or not all( - isinstance(inferred, nodes.Const) and not inferred.value - for inferred in all_inferred - ): + # Name defined in the if/else control flow + if NamesConsumer._inferred_to_define_name_raise_or_return(name, outer_if): continue uncertain_nodes.append(other_node) - self._if_nodes_deemed_uncertain.add(closest_if) return uncertain_nodes @staticmethod + def _node_guarded_by_same_test(node: nodes.NodeNG, other_if: nodes.If) -> bool: + """Identify if `node` is guarded by an equivalent test as `other_if`. + + Two tests are equivalent if their string representations are identical + or if their inferred values consist only of constants and those constants + are identical, and the if test guarding `node` is not a Name. + """ + other_if_test_as_string = other_if.test.as_string() + other_if_test_all_inferred = utils.infer_all(other_if.test) + for ancestor in node.node_ancestors(): + if not isinstance(ancestor, nodes.If): + continue + if ancestor.test.as_string() == other_if_test_as_string: + return True + if isinstance(ancestor.test, nodes.Name): + continue + all_inferred = utils.infer_all(ancestor.test) + if len(all_inferred) == len(other_if_test_all_inferred): + if any( + not isinstance(test, nodes.Const) + for test in (*all_inferred, *other_if_test_all_inferred) + ): + continue + if {test.value for test in all_inferred} != { + test.value for test in other_if_test_all_inferred + }: + continue + return True + + return False + + @staticmethod def _uncertain_nodes_in_except_blocks( found_nodes: list[nodes.NodeNG], node: nodes.NodeNG, @@ -921,6 +938,18 @@ scope_type : {self._atomic.scope_type} for child_named_expr in node.nodes_of_class(nodes.NamedExpr) ): return True + if isinstance(node, (nodes.Import, nodes.ImportFrom)) and any( + (node_name[1] and node_name[1] == name) or (node_name[0] == name) + for node_name in node.names + ): + return True + if isinstance(node, nodes.With) and any( + isinstance(item[1], nodes.AssignName) and item[1].name == name + for item in node.items + ): + return True + if isinstance(node, (nodes.ClassDef, nodes.FunctionDef)) and node.name == name: + return True return False @staticmethod @@ -939,18 +968,23 @@ scope_type : {self._atomic.scope_type} for nested_stmt in stmt.get_children() ): return True + if isinstance( + stmt, nodes.TryExcept + ) and NamesConsumer._defines_name_raises_or_returns_recursive(name, stmt): + return True return False @staticmethod def _check_loop_finishes_via_except( node: nodes.NodeNG, other_node_try_except: nodes.TryExcept ) -> bool: - """Check for a case described in https://github.com/PyCQA/pylint/issues/5683. + """Check for a specific control flow scenario. - It consists of a specific control flow scenario where the only - non-break exit from a loop consists of the very except handler we are - examining, such that code in the `else` branch of the loop can depend on it - being assigned. + Described in https://github.com/pylint-dev/pylint/issues/5683. + + A scenario where the only non-break exit from a loop consists of the very + except handler we are examining, such that code in the `else` branch of + the loop can depend on it being assigned. Example: @@ -1247,6 +1281,9 @@ class VariablesChecker(BaseChecker): tuple[nodes.ExceptHandler, nodes.AssignName] ] = [] """This is a queue, last in first out.""" + self._evaluated_type_checking_scopes: dict[ + str, list[nodes.LocalsDictNodeNG] + ] = {} self._postponed_evaluation_enabled = False @utils.only_required_for_messages( @@ -1707,21 +1744,16 @@ class VariablesChecker(BaseChecker): if found_nodes is None: return (VariableVisitConsumerAction.CONTINUE, None) if not found_nodes: - if node.name in current_consumer.consumed_uncertain: - confidence = CONTROL_FLOW - else: - confidence = HIGH - self.add_message( - "used-before-assignment", - args=node.name, - node=node, - confidence=confidence, - ) + self._report_unfound_name_definition(node, current_consumer) # Mark for consumption any nodes added to consumed_uncertain by # get_next_to_consume() because they might not have executed. + nodes_to_consume = current_consumer.consumed_uncertain[node.name] + nodes_to_consume = self._filter_type_checking_import_from_consumption( + node, nodes_to_consume + ) return ( VariableVisitConsumerAction.RETURN, - current_consumer.consumed_uncertain[node.name], + nodes_to_consume, ) self._check_late_binding_closure(node) @@ -1858,7 +1890,9 @@ class VariablesChecker(BaseChecker): confidence=HIGH, ) - elif self._is_only_type_assignment(node, defstmt): + elif not self._is_builtin(node.name) and self._is_only_type_assignment( + node, defstmt + ): if node.scope().locals.get(node.name): self.add_message( "used-before-assignment", args=node.name, node=node, confidence=HIGH @@ -1885,6 +1919,61 @@ class VariablesChecker(BaseChecker): return (VariableVisitConsumerAction.RETURN, found_nodes) + def _report_unfound_name_definition( + self, node: nodes.NodeNG, current_consumer: NamesConsumer + ) -> None: + """Reports used-before-assignment when all name definition nodes + get filtered out by NamesConsumer. + """ + if ( + self._postponed_evaluation_enabled + and utils.is_node_in_type_annotation_context(node) + ): + return + if self._is_builtin(node.name): + return + if self._is_variable_annotation_in_function(node): + return + if ( + node.name in self._evaluated_type_checking_scopes + and node.scope() in self._evaluated_type_checking_scopes[node.name] + ): + return + + confidence = ( + CONTROL_FLOW if node.name in current_consumer.consumed_uncertain else HIGH + ) + self.add_message( + "used-before-assignment", + args=node.name, + node=node, + confidence=confidence, + ) + + def _filter_type_checking_import_from_consumption( + self, node: nodes.NodeNG, nodes_to_consume: list[nodes.NodeNG] + ) -> list[nodes.NodeNG]: + """Do not consume type-checking import node as used-before-assignment + may invoke in different scopes. + """ + type_checking_import = next( + ( + n + for n in nodes_to_consume + if isinstance(n, (nodes.Import, nodes.ImportFrom)) + and in_type_checking_block(n) + ), + None, + ) + # If used-before-assignment reported for usage of type checking import + # keep track of its scope + if type_checking_import and not self._is_variable_annotation_in_function(node): + self._evaluated_type_checking_scopes.setdefault(node.name, []).append( + node.scope() + ) + nodes_to_consume = [n for n in nodes_to_consume if n != type_checking_import] + return nodes_to_consume + @utils.only_required_for_messages("no-name-in-module") def visit_import(self, node: nodes.Import) -> None: """Check modules attribute accesses.""" @@ -2049,7 +2138,6 @@ class VariablesChecker(BaseChecker): parent = parent.parent return False - # pylint: disable = too-many-branches @staticmethod def _is_variable_violation( node: nodes.Name, @@ -2203,42 +2291,6 @@ class VariablesChecker(BaseChecker): anc is defnode.value for anc in node.node_ancestors() ) - # Look for type checking definitions inside a type checking guard. - # Relevant for function annotations only, not variable annotations (AnnAssign) - if ( - isinstance(defstmt, (nodes.Import, nodes.ImportFrom)) - and isinstance(defstmt.parent, nodes.If) - and in_type_checking_block(defstmt) - and not in_type_checking_block(node) - ): - defstmt_parent = defstmt.parent - - maybe_annotation = utils.get_node_first_ancestor_of_type( - node, nodes.AnnAssign - ) - if not ( - maybe_annotation - and utils.get_node_first_ancestor_of_type( - maybe_annotation, nodes.FunctionDef - ) - ): - # Exempt those definitions that are used inside the type checking - # guard or that are defined in any elif/else type checking guard branches. - used_in_branch = defstmt_parent.parent_of(node) - if not used_in_branch: - if defstmt_parent.has_elif_block(): - defined_in_or_else = utils.is_defined( - node.name, defstmt_parent.orelse[0] - ) - else: - defined_in_or_else = any( - utils.is_defined(node.name, content) - for content in defstmt_parent.orelse - ) - - if not defined_in_or_else: - maybe_before_assign = True - return maybe_before_assign, annotation_return, use_outer_definition @staticmethod @@ -2254,7 +2306,7 @@ class VariablesChecker(BaseChecker): return any( VariablesChecker._maybe_used_and_assigned_at_once(elt) for elt in defstmt.value.elts - if isinstance(elt, NODES_WITH_VALUE_ATTR + (nodes.IfExp, nodes.Match)) + if isinstance(elt, (*NODES_WITH_VALUE_ATTR, nodes.IfExp, nodes.Match)) ) value = defstmt.value if isinstance(value, nodes.IfExp): @@ -2278,18 +2330,15 @@ class VariablesChecker(BaseChecker): for call in value.nodes_of_class(klass=nodes.Call) ) - def _is_only_type_assignment( - self, node: nodes.Name, defstmt: nodes.Statement - ) -> bool: + def _is_builtin(self, name: str) -> bool: + return name in self.linter.config.additional_builtins or utils.is_builtin(name) + + @staticmethod + def _is_only_type_assignment(node: nodes.Name, defstmt: nodes.Statement) -> bool: """Check if variable only gets assigned a type and never a value.""" if not isinstance(defstmt, nodes.AnnAssign) or defstmt.value: return False - if node.name in self.linter.config.additional_builtins or utils.is_builtin( - node.name - ): - return False - defstmt_frame = defstmt.frame(future=True) node_frame = node.frame(future=True) @@ -2378,6 +2427,16 @@ class VariablesChecker(BaseChecker): return True return False + @staticmethod + def _is_variable_annotation_in_function(node: nodes.NodeNG) -> bool: + is_annotation = utils.get_node_first_ancestor_of_type(node, nodes.AnnAssign) + return ( + is_annotation + and utils.get_node_first_ancestor_of_type( # type: ignore[return-value] + is_annotation, nodes.FunctionDef + ) + ) + def _ignore_class_scope(self, node: nodes.NodeNG) -> bool: """Return True if the node is in a local class scope, as an assignment. @@ -2427,10 +2486,7 @@ class VariablesChecker(BaseChecker): # the usage is safe because the function will not be defined either if # the variable is not defined. scope = node.scope() - # FunctionDef subclasses Lambda due to a curious ontology. Check both. - # See https://github.com/PyCQA/astroid/issues/291 - # TODO: Revisit when astroid 3.0 includes the change - if isinstance(scope, nodes.Lambda) and any( + if isinstance(scope, (nodes.Lambda, nodes.FunctionDef)) and any( asmt.scope().parent_of(scope) for asmt in astmts ): return @@ -2481,7 +2537,7 @@ class VariablesChecker(BaseChecker): else_stmt, (nodes.Return, nodes.Raise, nodes.Break, nodes.Continue) ): return - # TODO: 2.16: Consider using RefactoringChecker._is_function_def_never_returning + # TODO: 3.0: Consider using RefactoringChecker._is_function_def_never_returning if isinstance(else_stmt, nodes.Expr) and isinstance( else_stmt.value, nodes.Call ): @@ -2906,7 +2962,7 @@ class VariablesChecker(BaseChecker): @staticmethod def _nodes_to_unpack(node: nodes.NodeNG) -> list[nodes.NodeNG] | None: """Return the list of values of the `Assign` node.""" - if isinstance(node, (nodes.Tuple, nodes.List) + DICT_TYPES): + if isinstance(node, (nodes.Tuple, nodes.List, *DICT_TYPES)): return node.itered() # type: ignore[no-any-return] if isinstance(node, astroid.Instance) and any( ancestor.qname() == "typing.NamedTuple" for ancestor in node.ancestors() @@ -2961,7 +3017,9 @@ class VariablesChecker(BaseChecker): if not isinstance(module, nodes.Module): return None except astroid.NotFoundError: - if module.name in self._ignored_modules: + # Unable to import `name` from `module`. Since `name` may itself be a + # module, we first check if it matches the ignored modules. + if is_module_ignored(f"{module.qname()}.{name}", self._ignored_modules): return None self.add_message( "no-name-in-module", args=(name, module.name), node=node diff --git a/pylint/config/__init__.py b/pylint/config/__init__.py index 5f90bbae0..5dbda321c 100644 --- a/pylint/config/__init__.py +++ b/pylint/config/__init__.py @@ -1,67 +1,9 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 -__all__ = [ - "ConfigurationMixIn", # Deprecated - "find_default_config_files", - "find_pylintrc", # Deprecated - "Option", # Deprecated - "OptionsManagerMixIn", # Deprecated - "OptionParser", # Deprecated - "OptionsProviderMixIn", # Deprecated - "UnsupportedAction", # Deprecated - "PYLINTRC", - "USER_HOME", # Compatibility with the old API - "PYLINT_HOME", # Compatibility with the old API - "save_results", # Compatibility with the old API # Deprecated - "load_results", # Compatibility with the old API # Deprecated -] +__all__ = ["find_default_config_files"] -import warnings - -from pylint.config.arguments_provider import UnsupportedAction -from pylint.config.configuration_mixin import ConfigurationMixIn -from pylint.config.environment_variable import PYLINTRC -from pylint.config.find_default_config_files import ( - find_default_config_files, - find_pylintrc, -) -from pylint.config.option import Option -from pylint.config.option_manager_mixin import OptionsManagerMixIn -from pylint.config.option_parser import OptionParser # type: ignore[attr-defined] -from pylint.config.options_provider_mixin import ( # type: ignore[attr-defined] - OptionsProviderMixIn, -) -from pylint.constants import PYLINT_HOME, USER_HOME -from pylint.utils import LinterStats - - -def load_results(base: str) -> LinterStats | None: - # TODO: 3.0: Remove deprecated function - # pylint: disable=import-outside-toplevel - from pylint.lint.caching import load_results as _real_load_results - - warnings.warn( - "'pylint.config.load_results' is deprecated, please use " - "'pylint.lint.load_results' instead. This will be removed in 3.0.", - DeprecationWarning, - stacklevel=2, - ) - return _real_load_results(base, PYLINT_HOME) - - -def save_results(results: LinterStats, base: str) -> None: - # TODO: 3.0: Remove deprecated function - # pylint: disable=import-outside-toplevel - from pylint.lint.caching import save_results as _real_save_results - - warnings.warn( - "'pylint.config.save_results' is deprecated, please use " - "'pylint.lint.save_results' instead. This will be removed in 3.0.", - DeprecationWarning, - stacklevel=2, - ) - return _real_save_results(results, base, PYLINT_HOME) +from pylint.config.find_default_config_files import find_default_config_files diff --git a/pylint/config/_pylint_config/__init__.py b/pylint/config/_pylint_config/__init__.py index 622d0dfe3..8d36bd88b 100644 --- a/pylint/config/_pylint_config/__init__.py +++ b/pylint/config/_pylint_config/__init__.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Everything related to the 'pylint-config' command. diff --git a/pylint/config/_pylint_config/generate_command.py b/pylint/config/_pylint_config/generate_command.py index 110069b90..d1b73c99b 100644 --- a/pylint/config/_pylint_config/generate_command.py +++ b/pylint/config/_pylint_config/generate_command.py @@ -1,13 +1,12 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Everything related to the 'pylint-config generate' command.""" from __future__ import annotations -import warnings from io import StringIO from typing import TYPE_CHECKING @@ -29,10 +28,8 @@ def generate_interactive_config(linter: PyLinter) -> None: config_string = linter._generate_config_file(minimal=minimal) else: output_stream = StringIO() - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - linter.generate_config(stream=output_stream, skipsections=("Commands",)) - config_string = output_stream.getvalue() + linter._generate_config(stream=output_stream, skipsections=("Commands",)) + config_string = output_stream.getvalue() if to_file: with open(output_file_name, "w", encoding="utf-8") as f: diff --git a/pylint/config/_pylint_config/help_message.py b/pylint/config/_pylint_config/help_message.py index 15c9803e7..7ba947429 100644 --- a/pylint/config/_pylint_config/help_message.py +++ b/pylint/config/_pylint_config/help_message.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Everything related to the 'pylint-config -h' command and subcommands.""" diff --git a/pylint/config/_pylint_config/main.py b/pylint/config/_pylint_config/main.py index 716702df5..e562da2ef 100644 --- a/pylint/config/_pylint_config/main.py +++ b/pylint/config/_pylint_config/main.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Everything related to the 'pylint-config' command.""" diff --git a/pylint/config/_pylint_config/setup.py b/pylint/config/_pylint_config/setup.py index ae52b99ba..211f9bc6d 100644 --- a/pylint/config/_pylint_config/setup.py +++ b/pylint/config/_pylint_config/setup.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Everything related to the setup of the 'pylint-config' command.""" diff --git a/pylint/config/_pylint_config/utils.py b/pylint/config/_pylint_config/utils.py index cd5f8affe..f9185e8b1 100644 --- a/pylint/config/_pylint_config/utils.py +++ b/pylint/config/_pylint_config/utils.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Utils for the 'pylint-config' command.""" @@ -9,12 +9,7 @@ from __future__ import annotations import sys from collections.abc import Callable from pathlib import Path -from typing import TypeVar - -if sys.version_info >= (3, 8): - from typing import Literal -else: - from typing_extensions import Literal +from typing import Literal, TypeVar if sys.version_info >= (3, 10): from typing import ParamSpec diff --git a/pylint/config/argument.py b/pylint/config/argument.py index d648ae7e4..d826cbd3e 100644 --- a/pylint/config/argument.py +++ b/pylint/config/argument.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Definition of an Argument class and transformers for various argument types. @@ -13,10 +13,9 @@ import argparse import os import pathlib import re -import sys from collections.abc import Callable from glob import glob -from typing import Any, Pattern, Sequence, Tuple, Union +from typing import Any, Literal, Pattern, Sequence, Tuple, Union from pylint import interfaces from pylint import utils as pylint_utils @@ -24,12 +23,6 @@ from pylint.config.callback_actions import _CallbackAction, _ExtendAction from pylint.config.deprecation_actions import _NewNamesAction, _OldNamesAction from pylint.constants import PY38_PLUS -if sys.version_info >= (3, 8): - from typing import Literal -else: - from typing_extensions import Literal - - _ArgumentTypes = Union[ str, int, diff --git a/pylint/config/arguments_manager.py b/pylint/config/arguments_manager.py index ef1d0cc72..b99c9476f 100644 --- a/pylint/config/arguments_manager.py +++ b/pylint/config/arguments_manager.py @@ -1,24 +1,18 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Arguments manager class used to handle command-line arguments and options.""" from __future__ import annotations import argparse -import configparser -import copy -import optparse # pylint: disable=deprecated-module -import os import re import sys import textwrap import warnings -from collections import OrderedDict from collections.abc import Sequence -from pathlib import Path -from typing import TYPE_CHECKING, Any, TextIO, Union +from typing import TYPE_CHECKING, Any, TextIO import tomlkit @@ -37,11 +31,6 @@ from pylint.config.exceptions import ( _UnrecognizedOptionError, ) from pylint.config.help_formatter import _HelpFormatter -from pylint.config.option import Option -from pylint.config.option_parser import OptionParser # type: ignore[attr-defined] -from pylint.config.options_provider_mixin import ( # type: ignore[attr-defined] - OptionsProviderMixIn, -) from pylint.config.utils import _convert_option_to_argument, _parse_rich_type_value from pylint.constants import MAIN_CHECKER_NAME from pylint.typing import DirectoryNamespaceDict, OptionDict @@ -55,10 +44,7 @@ else: if TYPE_CHECKING: from pylint.config.arguments_provider import _ArgumentsProvider -ConfigProvider = Union["_ArgumentsProvider", OptionsProviderMixIn] - -# pylint: disable-next=too-many-instance-attributes class _ArgumentsManager: """Arguments manager class used to handle command-line arguments and options.""" @@ -95,21 +81,6 @@ class _ArgumentsManager: self._directory_namespaces: DirectoryNamespaceDict = {} """Mapping of directories and their respective namespace objects.""" - # TODO: 3.0: Remove deprecated attributes introduced to keep API - # parity with optparse. Until '_maxlevel' - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - self.reset_parsers(usage or "") - # list of registered options providers - self._options_providers: list[ConfigProvider] = [] - # dictionary associating option name to checker - self._all_options: OrderedDict[str, ConfigProvider] = OrderedDict() - self._short_options: dict[str, str] = {} - self._nocallback_options: dict[ConfigProvider, str] = {} - self._mygroups: dict[str, optparse.OptionGroup] = {} - # verbosity - self._maxlevel: int = 0 - @property def config(self) -> argparse.Namespace: """Namespace for all options.""" @@ -119,25 +90,6 @@ class _ArgumentsManager: def config(self, value: argparse.Namespace) -> None: self._config = value - @property - def options_providers(self) -> list[ConfigProvider]: - # TODO: 3.0: Remove deprecated attribute. - warnings.warn( - "options_providers has been deprecated. It will be removed in pylint 3.0.", - DeprecationWarning, - stacklevel=2, - ) - return self._options_providers - - @options_providers.setter - def options_providers(self, value: list[ConfigProvider]) -> None: - warnings.warn( - "Setting options_providers has been deprecated. It will be removed in pylint 3.0.", - DeprecationWarning, - stacklevel=2, - ) - self._options_providers = value - def _register_options_provider(self, provider: _ArgumentsProvider) -> None: """Register an options provider and load its defaults.""" for opt, optdict in provider.options: @@ -281,173 +233,12 @@ class _ArgumentsManager: return parsed_args - def reset_parsers(self, usage: str = "") -> None: # pragma: no cover - """DEPRECATED.""" - warnings.warn( - "reset_parsers has been deprecated. Parsers should be instantiated " - "once during initialization and do not need to be reset.", - DeprecationWarning, - stacklevel=2, - ) - # configuration file parser - self.cfgfile_parser = configparser.ConfigParser( - inline_comment_prefixes=("#", ";") - ) - # command line parser - self.cmdline_parser = OptionParser(Option, usage=usage) - self.cmdline_parser.options_manager = self - self._optik_option_attrs = set(self.cmdline_parser.option_class.ATTRS) - - def register_options_provider( - self, provider: ConfigProvider, own_group: bool = True - ) -> None: # pragma: no cover - """DEPRECATED: Register an options provider.""" - warnings.warn( - "register_options_provider has been deprecated. Options providers and " - "arguments providers should be registered by initializing ArgumentsProvider. " - "This automatically registers the provider on the ArgumentsManager.", - DeprecationWarning, - stacklevel=2, - ) - self.options_providers.append(provider) - non_group_spec_options = [ - option for option in provider.options if "group" not in option[1] - ] - groups = getattr(provider, "option_groups", ()) - if own_group and non_group_spec_options: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - self.add_option_group( - provider.name.upper(), - provider.__doc__, - non_group_spec_options, - provider, - ) - else: - for opt, optdict in non_group_spec_options: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - self.add_optik_option(provider, self.cmdline_parser, opt, optdict) - for gname, gdoc in groups: - gname = gname.upper() - goptions = [ - option - for option in provider.options - if option[1].get("group", "").upper() == gname # type: ignore[union-attr] - ] - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - self.add_option_group(gname, gdoc, goptions, provider) - - def add_option_group( - self, - group_name: str, - _: str | None, - options: list[tuple[str, OptionDict]], - provider: ConfigProvider, - ) -> None: # pragma: no cover - """DEPRECATED.""" - warnings.warn( - "add_option_group has been deprecated. Option groups should be " - "registered by initializing ArgumentsProvider. " - "This automatically registers the group on the ArgumentsManager.", - DeprecationWarning, - stacklevel=2, - ) - # add option group to the command line parser - if group_name in self._mygroups: - group = self._mygroups[group_name] - else: - group = optparse.OptionGroup( - self.cmdline_parser, title=group_name.capitalize() - ) - self.cmdline_parser.add_option_group(group) - self._mygroups[group_name] = group - # add section to the config file - if ( - group_name != "DEFAULT" - and group_name not in self.cfgfile_parser._sections # type: ignore[attr-defined] - ): - self.cfgfile_parser.add_section(group_name) - # add provider's specific options - for opt, optdict in options: - if not isinstance(optdict.get("action", "store"), str): - optdict["action"] = "callback" - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - self.add_optik_option(provider, group, opt, optdict) - - def add_optik_option( - self, - provider: ConfigProvider, - optikcontainer: optparse.OptionParser | optparse.OptionGroup, - opt: str, - optdict: OptionDict, - ) -> None: # pragma: no cover - """DEPRECATED.""" - warnings.warn( - "add_optik_option has been deprecated. Options should be automatically " - "added by initializing an ArgumentsProvider.", - DeprecationWarning, - stacklevel=2, - ) - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - args, optdict = self.optik_option(provider, opt, optdict) - option = optikcontainer.add_option(*args, **optdict) - self._all_options[opt] = provider - self._maxlevel = max(self._maxlevel, option.level or 0) - - def optik_option( - self, provider: ConfigProvider, opt: str, optdict: OptionDict - ) -> tuple[list[str], OptionDict]: # pragma: no cover - """DEPRECATED: Get our personal option definition and return a suitable form for - use with optik/optparse. - """ - warnings.warn( - "optik_option has been deprecated. Parsing of option dictionaries should be done " - "automatically by initializing an ArgumentsProvider.", - DeprecationWarning, - stacklevel=2, - ) - optdict = copy.copy(optdict) - if "action" in optdict: - self._nocallback_options[provider] = opt - else: - optdict["action"] = "callback" - optdict["callback"] = self.cb_set_provider_option - # default is handled here and *must not* be given to optik if you - # want the whole machinery to work - if "default" in optdict: - if ( - "help" in optdict - and optdict.get("default") is not None - and optdict["action"] not in ("store_true", "store_false") - ): - optdict["help"] += " [current: %default]" # type: ignore[operator] - del optdict["default"] - args = ["--" + str(opt)] - if "short" in optdict: - self._short_options[optdict["short"]] = opt # type: ignore[index] - args.append("-" + optdict["short"]) # type: ignore[operator] - del optdict["short"] - # cleanup option definition dict before giving it to optik - for key in list(optdict.keys()): - if key not in self._optik_option_attrs: - optdict.pop(key) - return args, optdict - - def generate_config( + def _generate_config( self, stream: TextIO | None = None, skipsections: tuple[str, ...] = () - ) -> None: # pragma: no cover - """DEPRECATED: Write a configuration file according to the current configuration + ) -> None: + """Write a configuration file according to the current configuration into the given stream or stdout. """ - warnings.warn( - "generate_config has been deprecated. It will be removed in pylint 3.0.", - DeprecationWarning, - stacklevel=2, - ) options_by_section = {} sections = [] for group in sorted( @@ -503,202 +294,10 @@ class _ArgumentsManager: ) printed = True - def load_provider_defaults(self) -> None: # pragma: no cover - """DEPRECATED: Initialize configuration using default values.""" - warnings.warn( - "load_provider_defaults has been deprecated. Parsing of option defaults should be done " - "automatically by initializing an ArgumentsProvider.", - DeprecationWarning, - stacklevel=2, - ) - for provider in self.options_providers: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - provider.load_defaults() - - def read_config_file( - self, config_file: Path | None = None, verbose: bool = False - ) -> None: # pragma: no cover - """DEPRECATED: Read the configuration file but do not load it (i.e. dispatching - values to each option's provider). - - :raises OSError: When the specified config file doesn't exist - """ - warnings.warn( - "read_config_file has been deprecated. It will be removed in pylint 3.0.", - DeprecationWarning, - stacklevel=2, - ) - if not config_file: - if verbose: - print( - "No config file found, using default configuration", file=sys.stderr - ) - return - config_file = Path(os.path.expandvars(config_file)).expanduser() - if not config_file.exists(): - raise OSError(f"The config file {str(config_file)} doesn't exist!") - parser = self.cfgfile_parser - if config_file.suffix == ".toml": - try: - self._parse_toml(config_file, parser) - except tomllib.TOMLDecodeError: - pass - else: - # Use this encoding in order to strip the BOM marker, if any. - with open(config_file, encoding="utf_8_sig") as fp: - parser.read_file(fp) - # normalize each section's title - for sect, values in list(parser._sections.items()): # type: ignore[attr-defined] - if sect.startswith("pylint."): - sect = sect[len("pylint.") :] - if not sect.isupper() and values: - parser._sections[sect.upper()] = values # type: ignore[attr-defined] - - if verbose: - print(f"Using config file '{config_file}'", file=sys.stderr) - - @staticmethod - def _parse_toml( - config_file: Path, parser: configparser.ConfigParser - ) -> None: # pragma: no cover - """DEPRECATED: Parse and handle errors of a toml configuration file. - - TODO: 3.0: Remove deprecated method. - """ - with open(config_file, mode="rb") as fp: - content = tomllib.load(fp) - try: - sections_values = content["tool"]["pylint"] - except KeyError: - return - for section, values in sections_values.items(): - section_name = section.upper() - # TOML has rich types, convert values to - # strings as ConfigParser expects. - if not isinstance(values, dict): - continue - for option, value in values.items(): - if isinstance(value, bool): - values[option] = "yes" if value else "no" - elif isinstance(value, list): - values[option] = ",".join(value) - else: - values[option] = str(value) - for option, value in values.items(): - try: - parser.set(section_name, option, value=value) - except configparser.NoSectionError: - parser.add_section(section_name) - parser.set(section_name, option, value=value) - - def load_config_file(self) -> None: # pragma: no cover - """DEPRECATED: Dispatch values previously read from a configuration file to each - option's provider. - """ - warnings.warn( - "load_config_file has been deprecated. It will be removed in pylint 3.0.", - DeprecationWarning, - stacklevel=2, - ) - parser = self.cfgfile_parser - for section in parser.sections(): - for option, value in parser.items(section): - try: - self.global_set_option(option, value) - except (KeyError, optparse.OptionError): - continue - - def load_configuration(self, **kwargs: Any) -> None: # pragma: no cover - """DEPRECATED: Override configuration according to given parameters.""" - warnings.warn( - "load_configuration has been deprecated. It will be removed in pylint 3.0.", - DeprecationWarning, - stacklevel=2, - ) - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - return self.load_configuration_from_config(kwargs) - - def load_configuration_from_config( - self, config: dict[str, Any] - ) -> None: # pragma: no cover - warnings.warn( - "DEPRECATED: load_configuration_from_config has been deprecated. It will be removed in pylint 3.0.", - DeprecationWarning, - stacklevel=2, - ) - for opt, opt_value in config.items(): - opt = opt.replace("_", "-") - provider = self._all_options[opt] - provider.set_option(opt, opt_value) - - def load_command_line_configuration( - self, args: list[str] | None = None - ) -> list[str]: # pragma: no cover - """DEPRECATED: Override configuration according to command line parameters. - - return additional arguments - """ - warnings.warn( - "load_command_line_configuration has been deprecated. It will be removed in pylint 3.0.", - DeprecationWarning, - stacklevel=2, - ) - args = sys.argv[1:] if args is None else list(args) - (options, args) = self.cmdline_parser.parse_args(args=args) - for provider in self._nocallback_options: - config = provider.config - for attr in config.__dict__.keys(): - value = getattr(options, attr, None) - if value is None: - continue - setattr(config, attr, value) - return args # type: ignore[return-value] - - def help(self, level: int | None = None) -> str: + def help(self) -> str: """Return the usage string based on the available options.""" - if level is not None: - warnings.warn( - "Supplying a 'level' argument to help() has been deprecated." - "You can call help() without any arguments.", - DeprecationWarning, - stacklevel=2, - ) return self._arg_parser.format_help() - def cb_set_provider_option( # pragma: no cover - self, option: Any, opt: Any, value: Any, parser: Any - ) -> None: - """DEPRECATED: Optik callback for option setting.""" - # TODO: 3.0: Remove deprecated method. - warnings.warn( - "cb_set_provider_option has been deprecated. It will be removed in pylint 3.0.", - DeprecationWarning, - stacklevel=2, - ) - if opt.startswith("--"): - # remove -- on long option - opt = opt[2:] - else: - # short option, get its long equivalent - opt = self._short_options[opt[1:]] - # trick since we can't set action='store_true' on options - if value is None: - value = 1 - self.set_option(opt, value) - - def global_set_option(self, opt: str, value: Any) -> None: # pragma: no cover - """DEPRECATED: Set option on the correct option provider.""" - # TODO: 3.0: Remove deprecated method. - warnings.warn( - "global_set_option has been deprecated. You can use _arguments_manager.set_option " - "or linter.set_option to set options on the global configuration object.", - DeprecationWarning, - stacklevel=2, - ) - self.set_option(opt, value) - def _generate_config_file(self, *, minimal: bool = False) -> str: """Write a configuration file according to the current configuration into stdout. @@ -795,30 +394,8 @@ class _ArgumentsManager: return str(toml_string) - def set_option( - self, - optname: str, - value: Any, - action: str | None = "default_value", - optdict: None | str | OptionDict = "default_value", - ) -> None: + def set_option(self, optname: str, value: Any) -> None: """Set an option on the namespace object.""" - # TODO: 3.0: Remove deprecated arguments. - if action != "default_value": - warnings.warn( - "The 'action' argument has been deprecated. You can use set_option " - "without the 'action' or 'optdict' arguments.", - DeprecationWarning, - stacklevel=2, - ) - if optdict != "default_value": - warnings.warn( - "The 'optdict' argument has been deprecated. You can use set_option " - "without the 'action' or 'optdict' arguments.", - DeprecationWarning, - stacklevel=2, - ) - self.config = self._arg_parser.parse_known_args( [f"--{optname.replace('_', '-')}", _parse_rich_type_value(value)], self.config, diff --git a/pylint/config/arguments_provider.py b/pylint/config/arguments_provider.py index f5aac2f1d..7f75718ca 100644 --- a/pylint/config/arguments_provider.py +++ b/pylint/config/arguments_provider.py @@ -1,14 +1,11 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Arguments provider class used to expose options.""" from __future__ import annotations -import argparse -import optparse # pylint: disable=deprecated-module -import warnings from collections.abc import Iterator from typing import Any @@ -16,19 +13,6 @@ from pylint.config.arguments_manager import _ArgumentsManager from pylint.typing import OptionDict, Options -class UnsupportedAction(Exception): - """Raised by set_option when it doesn't know what to do for an action.""" - - def __init__(self, *args: object) -> None: - # TODO: 3.0: Remove deprecated exception - warnings.warn( - "UnsupportedAction has been deprecated and will be removed in pylint 3.0", - DeprecationWarning, - stacklevel=2, - ) - super().__init__(*args) - - class _ArgumentsProvider: """Base class for classes that provide arguments.""" @@ -47,170 +31,35 @@ class _ArgumentsProvider: self._arguments_manager._register_options_provider(self) - self._level = 0 - - @property - def level(self) -> int: - # TODO: 3.0: Remove deprecated attribute - warnings.warn( - "The level attribute has been deprecated. It was used to display the checker in the help or not," - " and everything is displayed in the help now. It will be removed in pylint 3.0.", - DeprecationWarning, - stacklevel=2, - ) - return self._level - - @level.setter - def level(self, value: int) -> None: - # TODO: 3.0: Remove deprecated attribute - warnings.warn( - "Setting the level attribute has been deprecated. It was used to display the checker " - "in the help or not, and everything is displayed in the help now. It will be removed " - "in pylint 3.0.", - DeprecationWarning, - stacklevel=2, - ) - self._level = value - - @property - def config(self) -> argparse.Namespace: - # TODO: 3.0: Remove deprecated attribute - warnings.warn( - "The checker-specific config attribute has been deprecated. Please use " - "'linter.config' to access the global configuration object.", - DeprecationWarning, - stacklevel=2, - ) - return self._arguments_manager.config - - def load_defaults(self) -> None: # pragma: no cover - """DEPRECATED: Initialize the provider using default values.""" - warnings.warn( - "load_defaults has been deprecated. Option groups should be " - "registered by initializing an ArgumentsProvider. " - "This automatically registers the group on the ArgumentsManager.", - DeprecationWarning, - stacklevel=2, - ) - for opt, optdict in self.options: - action = optdict.get("action") - if action != "callback": - # callback action have no default - if optdict is None: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - optdict = self.get_option_def(opt) - default = optdict.get("default") - self.set_option(opt, default, action, optdict) - - def option_attrname( - self, opt: str, optdict: OptionDict | None = None - ) -> str: # pragma: no cover - """DEPRECATED: Get the config attribute corresponding to opt.""" - warnings.warn( - "option_attrname has been deprecated. It will be removed " - "in a future release.", - DeprecationWarning, - stacklevel=2, - ) - if optdict is None: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - optdict = self.get_option_def(opt) - return optdict.get("dest", opt.replace("-", "_")) # type: ignore[return-value] - - def option_value(self, opt: str) -> Any: # pragma: no cover - """DEPRECATED: Get the current value for the given option.""" - warnings.warn( - "option_value has been deprecated. It will be removed " - "in a future release.", - DeprecationWarning, - stacklevel=2, - ) + def _option_value(self, opt: str) -> Any: + """Get the current value for the given option.""" return getattr(self._arguments_manager.config, opt.replace("-", "_"), None) - def set_option( # pragma: no cover - self, - optname: Any, - value: Any, - action: Any = None, # pylint: disable=unused-argument - optdict: Any = None, # pylint: disable=unused-argument - ) -> None: - """DEPRECATED: Method called to set an option (registered in the options - list). - """ - # TODO: 3.0: Remove deprecated method. - warnings.warn( - "set_option has been deprecated. You can use _arguments_manager.set_option " - "or linter.set_option to set options on the global configuration object.", - DeprecationWarning, - stacklevel=2, - ) - self._arguments_manager.set_option(optname, value) - - def get_option_def(self, opt: str) -> OptionDict: # pragma: no cover - """DEPRECATED: Return the dictionary defining an option given its name. - - :raises OptionError: If the option isn't found. - """ - warnings.warn( - "get_option_def has been deprecated. It will be removed " - "in a future release.", - DeprecationWarning, - stacklevel=2, - ) - assert self.options - for option in self.options: - if option[0] == opt: - return option[1] - raise optparse.OptionError( - f"no such option {opt} in section {self.name!r}", opt # type: ignore[arg-type] - ) - - def options_by_section( + def _options_by_section( self, ) -> Iterator[ tuple[str, list[tuple[str, OptionDict, Any]]] | tuple[None, dict[str, list[tuple[str, OptionDict, Any]]]] - ]: # pragma: no cover - """DEPRECATED: Return an iterator on options grouped by section. + ]: + """Return an iterator on options grouped by section. (section, [list of (optname, optdict, optvalue)]) """ - # TODO 3.0: Make this function private see - # https://github.com/PyCQA/pylint/pull/6665#discussion_r880143229 - # It's only used in '_get_global_options_documentation' - warnings.warn( - "options_by_section has been deprecated. It will be removed " - "in a future release.", - DeprecationWarning, - stacklevel=2, - ) sections: dict[str, list[tuple[str, OptionDict, Any]]] = {} for optname, optdict in self.options: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - sections.setdefault(optdict.get("group"), []).append( # type: ignore[arg-type] - (optname, optdict, self.option_value(optname)) - ) + sections.setdefault(optdict.get("group"), []).append( # type: ignore[arg-type] + (optname, optdict, self._option_value(optname)) + ) if None in sections: yield None, sections.pop(None) # type: ignore[call-overload] for section, options in sorted(sections.items()): yield section.upper(), options - def options_and_values( + def _options_and_values( self, options: Options | None = None - ) -> Iterator[tuple[str, OptionDict, Any]]: # pragma: no cover + ) -> Iterator[tuple[str, OptionDict, Any]]: """DEPRECATED.""" - warnings.warn( - "options_and_values has been deprecated. It will be removed " - "in a future release.", - DeprecationWarning, - stacklevel=2, - ) if options is None: options = self.options for optname, optdict in options: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - yield optname, optdict, self.option_value(optname) + yield optname, optdict, self._option_value(optname) diff --git a/pylint/config/callback_actions.py b/pylint/config/callback_actions.py index a4c633464..7c93f2553 100644 --- a/pylint/config/callback_actions.py +++ b/pylint/config/callback_actions.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt # pylint: disable=too-many-arguments, redefined-builtin, duplicate-code @@ -11,7 +11,6 @@ from __future__ import annotations import abc import argparse import sys -import warnings from collections.abc import Callable, Sequence from pathlib import Path from typing import TYPE_CHECKING, Any @@ -265,11 +264,9 @@ class _GenerateRCFileAction(_AccessRunObjectAction): values: str | Sequence[Any] | None, option_string: str | None = "--generate-rcfile", ) -> None: - # TODO: 2.x: Deprecate this after the auto-upgrade functionality of + # TODO: 3.x: Deprecate this after the auto-upgrade functionality of # pylint-config is sufficient. - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - self.run.linter.generate_config(skipsections=("Commands",)) + self.run.linter._generate_config(skipsections=("Commands",)) sys.exit(0) diff --git a/pylint/config/config_file_parser.py b/pylint/config/config_file_parser.py index 019f9b738..047df5889 100644 --- a/pylint/config/config_file_parser.py +++ b/pylint/config/config_file_parser.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Configuration file parser class.""" @@ -9,7 +9,6 @@ from __future__ import annotations import configparser import os import sys -import warnings from pathlib import Path from typing import TYPE_CHECKING @@ -41,23 +40,13 @@ class _ConfigurationFileParser: config_content: dict[str, str] = {} options: list[str] = [] + ini_file_with_sections = self._ini_file_with_sections(file_path) for section in parser.sections(): - if self._ini_file_with_sections(file_path) and not section.startswith( - "pylint" - ): - if section.lower() == "master": - # TODO: 3.0: Remove deprecated handling of master, only allow 'pylint.' sections - warnings.warn( - "The use of 'MASTER' or 'master' as configuration section for pylint " - "has been deprecated, as it's bad practice to not start sections titles " - "with the tool name. Please use 'pylint.main' instead.", - UserWarning, - ) - else: - continue - for opt, value in parser[section].items(): - config_content[opt] = value - options += [f"--{opt}", value] + if ini_file_with_sections and not section.startswith("pylint"): + continue + for option, value in parser[section].items(): + config_content[option] = value + options += [f"--{option}", value] return config_content, options @staticmethod diff --git a/pylint/config/config_initialization.py b/pylint/config/config_initialization.py index 3c9c4e22a..4f7b614ee 100644 --- a/pylint/config/config_initialization.py +++ b/pylint/config/config_initialization.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 diff --git a/pylint/config/configuration_mixin.py b/pylint/config/configuration_mixin.py deleted file mode 100644 index 55857224a..000000000 --- a/pylint/config/configuration_mixin.py +++ /dev/null @@ -1,41 +0,0 @@ -# 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt - -from __future__ import annotations - -import warnings -from typing import Any - -from pylint.config.option_manager_mixin import OptionsManagerMixIn -from pylint.config.options_provider_mixin import ( # type: ignore[attr-defined] - OptionsProviderMixIn, -) - - -class ConfigurationMixIn(OptionsManagerMixIn, OptionsProviderMixIn): # type: ignore[misc] - """Basic mixin for simple configurations which don't need the - manager / providers model. - """ - - def __init__(self, *args: Any, **kwargs: Any) -> None: - # TODO: 3.0: Remove deprecated class - warnings.warn( - "ConfigurationMixIn has been deprecated and will be removed in pylint 3.0", - DeprecationWarning, - stacklevel=2, - ) - if not args: - kwargs.setdefault("usage", "") - OptionsManagerMixIn.__init__(self, *args, **kwargs) - OptionsProviderMixIn.__init__(self) - if not getattr(self, "option_groups", None): - self.option_groups: list[tuple[str, str]] = [] - for _, optdict in self.options: - try: - gdef = (optdict["group"].upper(), "") - except KeyError: - continue - if gdef not in self.option_groups: - self.option_groups.append(gdef) - self.register_options_provider(self, own_group=False) diff --git a/pylint/config/deprecation_actions.py b/pylint/config/deprecation_actions.py index ceef200a7..85a77cc78 100644 --- a/pylint/config/deprecation_actions.py +++ b/pylint/config/deprecation_actions.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt # pylint: disable=too-many-arguments, redefined-builtin @@ -104,4 +104,5 @@ class _NewNamesAction(argparse._StoreAction): f"{self.option_strings[0]} has been deprecated. Please look into " f"using any of the following options: {', '.join(self.new_names)}.", DeprecationWarning, + stacklevel=2, ) diff --git a/pylint/config/environment_variable.py b/pylint/config/environment_variable.py deleted file mode 100644 index 291b1651c..000000000 --- a/pylint/config/environment_variable.py +++ /dev/null @@ -1,11 +0,0 @@ -# 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt - -import warnings - -from pylint.config.find_default_config_files import find_pylintrc - -with warnings.catch_warnings(): - warnings.simplefilter("ignore") - PYLINTRC = find_pylintrc() diff --git a/pylint/config/exceptions.py b/pylint/config/exceptions.py index cb4fc1f8f..982e3f494 100644 --- a/pylint/config/exceptions.py +++ b/pylint/config/exceptions.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 diff --git a/pylint/config/find_default_config_files.py b/pylint/config/find_default_config_files.py index 43e682a58..3b03f6357 100644 --- a/pylint/config/find_default_config_files.py +++ b/pylint/config/find_default_config_files.py @@ -1,13 +1,12 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 import configparser import os import sys -import warnings from collections.abc import Iterator from pathlib import Path @@ -17,7 +16,28 @@ else: import tomli as tomllib RC_NAMES = (Path("pylintrc"), Path(".pylintrc")) -CONFIG_NAMES = RC_NAMES + (Path("pyproject.toml"), Path("setup.cfg")) +PYPROJECT_NAME = Path("pyproject.toml") +CONFIG_NAMES = (*RC_NAMES, PYPROJECT_NAME, Path("setup.cfg")) + + +def _find_pyproject() -> Path: + """Search for file pyproject.toml in the parent directories recursively. + + It resolves symlinks, so if there is any symlink up in the tree, it does not respect them + """ + current_dir = Path.cwd().resolve() + is_root = False + while not is_root: + if (current_dir / PYPROJECT_NAME).is_file(): + return current_dir / PYPROJECT_NAME + is_root = ( + current_dir == current_dir.parent + or (current_dir / ".git").is_dir() + or (current_dir / ".hg").is_dir() + ) + current_dir = current_dir.parent + + return current_dir def _toml_has_config(path: Path | str) -> bool: @@ -101,6 +121,13 @@ def find_default_config_files() -> Iterator[Path]: pass try: + parent_pyproject = _find_pyproject() + if parent_pyproject.is_file() and _toml_has_config(parent_pyproject): + yield parent_pyproject.resolve() + except OSError: + pass + + try: yield from _find_config_in_home_or_environment() except OSError: pass @@ -110,21 +137,3 @@ def find_default_config_files() -> Iterator[Path]: yield Path("/etc/pylintrc").resolve() except OSError: pass - - -def find_pylintrc() -> str | None: - """Search the pylint rc file and return its path if it finds it, else return - None. - """ - # TODO: 3.0: Remove deprecated function - warnings.warn( - "find_pylintrc and the PYLINTRC constant have been deprecated. " - "Use find_default_config_files if you want access to pylint's configuration file " - "finding logic.", - DeprecationWarning, - stacklevel=2, - ) - for config_file in find_default_config_files(): - if str(config_file).endswith("pylintrc"): - return str(config_file) - return None diff --git a/pylint/config/help_formatter.py b/pylint/config/help_formatter.py index 5e517d944..78d43d178 100644 --- a/pylint/config/help_formatter.py +++ b/pylint/config/help_formatter.py @@ -1,13 +1,13 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 import argparse from pylint.config.callback_actions import _CallbackAction -from pylint.constants import DEFAULT_PYLINT_HOME, OLD_DEFAULT_PYLINT_HOME +from pylint.constants import DEFAULT_PYLINT_HOME class _HelpFormatter(argparse.RawDescriptionHelpFormatter): @@ -35,8 +35,7 @@ class _HelpFormatter(argparse.RawDescriptionHelpFormatter): Environment variables: The following environment variables are used: * PYLINTHOME Path to the directory where persistent data for the run will - be stored. If not found, it defaults to '{DEFAULT_PYLINT_HOME}' - or '{OLD_DEFAULT_PYLINT_HOME}' (in the current working directory). + be stored. If not found, it defaults to '{DEFAULT_PYLINT_HOME}'. * PYLINTRC Path to the configuration file. See the documentation for the method used to search for configuration file. diff --git a/pylint/config/option.py b/pylint/config/option.py deleted file mode 100644 index 74e4d45d8..000000000 --- a/pylint/config/option.py +++ /dev/null @@ -1,239 +0,0 @@ -# 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt - -from __future__ import annotations - -import copy -import optparse # pylint: disable=deprecated-module -import pathlib -import re -import warnings -from collections.abc import Callable, Sequence -from re import Pattern -from typing import Any - -from pylint import utils - - -# pylint: disable=unused-argument -def _csv_validator( - _: Any, name: str, value: str | list[str] | tuple[str] -) -> Sequence[str]: - return utils._check_csv(value) - - -# pylint: disable=unused-argument -def _regexp_validator( - _: Any, name: str, value: str | re.Pattern[str] -) -> re.Pattern[str]: - if hasattr(value, "pattern"): - return value # type: ignore[return-value] - return re.compile(value) - - -# pylint: disable=unused-argument -def _regexp_csv_validator( - _: Any, name: str, value: str | list[str] -) -> list[re.Pattern[str]]: - return [_regexp_validator(_, name, val) for val in _csv_validator(_, name, value)] - - -def _regexp_paths_csv_validator( - _: Any, name: str, value: str | list[Pattern[str]] -) -> list[Pattern[str]]: - if isinstance(value, list): - return value - patterns = [] - for val in _csv_validator(_, name, value): - patterns.append( - re.compile( - str(pathlib.PureWindowsPath(val)).replace("\\", "\\\\") - + "|" - + pathlib.PureWindowsPath(val).as_posix() - ) - ) - return patterns - - -def _choice_validator(choices: list[Any], name: str, value: Any) -> Any: - if value not in choices: - msg = "option %s: invalid value: %r, should be in %s" - raise optparse.OptionValueError(msg % (name, value, choices)) - return value - - -def _yn_validator(opt: str, _: str, value: Any) -> bool: - if isinstance(value, int): - return bool(value) - if isinstance(value, str): - value = value.lower() - if value in {"y", "yes", "true"}: - return True - if value in {"n", "no", "false"}: - return False - msg = "option %s: invalid yn value %r, should be in (y, yes, true, n, no, false)" - raise optparse.OptionValueError(msg % (opt, value)) - - -def _multiple_choice_validator(choices: list[Any], name: str, value: Any) -> Any: - values = utils._check_csv(value) - for csv_value in values: - if csv_value not in choices: - msg = "option %s: invalid value: %r, should be in %s" - raise optparse.OptionValueError(msg % (name, csv_value, choices)) - return values - - -def _non_empty_string_validator( # pragma: no cover # Unused - opt: Any, _: str, value: str -) -> str: - if not value: - msg = "indent string can't be empty." - raise optparse.OptionValueError(msg) - return utils._unquote(value) - - -def _multiple_choices_validating_option( # pragma: no cover # Unused - opt: optparse.Option, name: str, value: Any -) -> Any: - return _multiple_choice_validator( - opt.choices, name, value # type: ignore[attr-defined] - ) - - -def _py_version_validator(_: Any, name: str, value: Any) -> tuple[int, int, int]: - if not isinstance(value, tuple): - try: - value = tuple(int(val) for val in value.split(".")) - except (ValueError, AttributeError): - raise optparse.OptionValueError( - f"Invalid format for {name}, should be version string. E.g., '3.8'" - ) from None - return value # type: ignore[no-any-return] - - -VALIDATORS: dict[str, Callable[[Any, str, Any], Any] | Callable[[Any], Any]] = { - "string": utils._unquote, - "int": int, - "float": float, - "glob_paths_csv": _csv_validator, - "regexp": lambda pattern: re.compile(pattern or ""), - "regexp_csv": _regexp_csv_validator, - "regexp_paths_csv": _regexp_paths_csv_validator, - "csv": _csv_validator, - "yn": _yn_validator, - "choice": lambda opt, name, value: _choice_validator(opt["choices"], name, value), - "confidence": lambda opt, name, value: _multiple_choice_validator( - opt["choices"], name, value - ), - "multiple_choice": lambda opt, name, value: _multiple_choice_validator( - opt["choices"], name, value - ), - "non_empty_string": _non_empty_string_validator, - "py_version": _py_version_validator, -} - - -def _call_validator(opttype: str, optdict: Any, option: str, value: Any) -> Any: - if opttype not in VALIDATORS: - raise TypeError(f'Unsupported type "{opttype}"') - try: - return VALIDATORS[opttype](optdict, option, value) # type: ignore[call-arg] - except TypeError: - try: - return VALIDATORS[opttype](value) # type: ignore[call-arg] - except Exception as e: - raise optparse.OptionValueError( - f"{option} value ({value!r}) should be of type {opttype}" - ) from e - - -def _validate(value: Any, optdict: Any, name: str = "") -> Any: - """Return a validated value for an option according to its type. - - optional argument name is only used for error message formatting - """ - try: - _type = optdict["type"] - except KeyError: - return value - return _call_validator(_type, optdict, name, value) - - -# pylint: disable=no-member -class Option(optparse.Option): - TYPES = optparse.Option.TYPES + ( - "glob_paths_csv", - "regexp", - "regexp_csv", - "regexp_paths_csv", - "csv", - "yn", - "confidence", - "multiple_choice", - "non_empty_string", - "py_version", - ) - ATTRS = optparse.Option.ATTRS + ["hide", "level"] - TYPE_CHECKER = copy.copy(optparse.Option.TYPE_CHECKER) - TYPE_CHECKER["glob_paths_csv"] = _csv_validator - TYPE_CHECKER["regexp"] = _regexp_validator - TYPE_CHECKER["regexp_csv"] = _regexp_csv_validator - TYPE_CHECKER["regexp_paths_csv"] = _regexp_paths_csv_validator - TYPE_CHECKER["csv"] = _csv_validator - TYPE_CHECKER["yn"] = _yn_validator - TYPE_CHECKER["confidence"] = _multiple_choices_validating_option - TYPE_CHECKER["multiple_choice"] = _multiple_choices_validating_option - TYPE_CHECKER["non_empty_string"] = _non_empty_string_validator - TYPE_CHECKER["py_version"] = _py_version_validator - - def __init__(self, *opts: Any, **attrs: Any) -> None: - # TODO: 3.0: Remove deprecated class - warnings.warn( - "Option has been deprecated and will be removed in pylint 3.0", - DeprecationWarning, - stacklevel=2, - ) - super().__init__(*opts, **attrs) - if hasattr(self, "hide") and self.hide: - self.help = optparse.SUPPRESS_HELP - - def _check_choice(self) -> None: - if self.type in {"choice", "multiple_choice", "confidence"}: - if self.choices is None: # type: ignore[attr-defined] - raise optparse.OptionError( - "must supply a list of choices for type 'choice'", self - ) - if not isinstance(self.choices, (tuple, list)): # type: ignore[attr-defined] - raise optparse.OptionError( - # pylint: disable-next=consider-using-f-string - "choices must be a list of strings ('%s' supplied)" - % str(type(self.choices)).split("'")[1], # type: ignore[attr-defined] - self, - ) - elif self.choices is not None: # type: ignore[attr-defined] - raise optparse.OptionError( - f"must not supply choices for type {self.type!r}", self - ) - - optparse.Option.CHECK_METHODS[2] = _check_choice # type: ignore[index] - - def process( # pragma: no cover # Argparse - self, opt: Any, value: Any, values: Any, parser: Any - ) -> int: - assert isinstance(self.dest, str) - if self.callback and self.callback.__module__ == "pylint.lint.run": - return 1 - # First, convert the value(s) to the right type. Howl if any - # value(s) are bogus. - value = self.convert_value(opt, value) - if self.type == "named": - existent = getattr(values, self.dest) - if existent: - existent.update(value) - value = existent - # And then take whatever action is expected of us. - # This is a separate method to make life easier for - # subclasses to add new actions. - return self.take_action(self.action, self.dest, opt, value, values, parser) diff --git a/pylint/config/option_manager_mixin.py b/pylint/config/option_manager_mixin.py deleted file mode 100644 index c468f494f..000000000 --- a/pylint/config/option_manager_mixin.py +++ /dev/null @@ -1,372 +0,0 @@ -# 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt - - -# pylint: disable=duplicate-code - -from __future__ import annotations - -import collections -import configparser -import contextlib -import copy -import optparse # pylint: disable=deprecated-module -import os -import sys -import warnings -from collections.abc import Iterator -from pathlib import Path -from typing import TYPE_CHECKING, Any, TextIO - -from pylint import utils -from pylint.config.option import Option -from pylint.config.option_parser import OptionParser # type: ignore[attr-defined] -from pylint.typing import OptionDict - -if TYPE_CHECKING: - from pylint.config.options_provider_mixin import ( # type: ignore[attr-defined] - OptionsProviderMixin, - ) - -if sys.version_info >= (3, 11): - import tomllib -else: - import tomli as tomllib - - -def _expand_default(self: optparse.HelpFormatter, option: Option) -> str: - """Patch OptionParser.expand_default with custom behaviour. - - This will handle defaults to avoid overriding values in the - configuration file. - """ - if self.parser is None or not self.default_tag: - return str(option.help) - optname = option._long_opts[0][2:] - try: - provider = self.parser.options_manager._all_options[optname] # type: ignore[attr-defined] - except KeyError: - value = None - else: - optdict = provider.get_option_def(optname) - optname = provider.option_attrname(optname, optdict) - value = getattr(provider.config, optname, optdict) - value = utils._format_option_value(optdict, value) - if value is optparse.NO_DEFAULT or not value: - value = self.NO_DEFAULT_VALUE - return option.help.replace(self.default_tag, str(value)) # type: ignore[union-attr] - - -@contextlib.contextmanager -def _patch_optparse() -> Iterator[None]: - # pylint: disable = redefined-variable-type - orig_default = optparse.HelpFormatter - try: - optparse.HelpFormatter.expand_default = _expand_default # type: ignore[assignment] - yield - finally: - optparse.HelpFormatter.expand_default = orig_default # type: ignore[assignment] - - -class OptionsManagerMixIn: - """Handle configuration from both a configuration file and command line options.""" - - def __init__(self, usage: str) -> None: - # TODO: 3.0: Remove deprecated class - warnings.warn( - "OptionsManagerMixIn has been deprecated and will be removed in pylint 3.0", - DeprecationWarning, - stacklevel=2, - ) - self.reset_parsers(usage) - # list of registered options providers - self.options_providers: list[OptionsProviderMixin] = [] - # dictionary associating option name to checker - self._all_options: collections.OrderedDict[Any, Any] = collections.OrderedDict() - self._short_options: dict[Any, Any] = {} - self._nocallback_options: dict[Any, Any] = {} - self._mygroups: dict[Any, Any] = {} - # verbosity - self._maxlevel = 0 - - def reset_parsers(self, usage: str = "") -> None: - # configuration file parser - self.cfgfile_parser = configparser.ConfigParser( - inline_comment_prefixes=("#", ";") - ) - # command line parser - self.cmdline_parser = OptionParser(Option, usage=usage) - self.cmdline_parser.options_manager = self - self._optik_option_attrs = set(self.cmdline_parser.option_class.ATTRS) - - def register_options_provider( - self, provider: OptionsProviderMixin, own_group: bool = True - ) -> None: - """Register an options provider.""" - self.options_providers.append(provider) - non_group_spec_options = [ - option for option in provider.options if "group" not in option[1] - ] - groups = getattr(provider, "option_groups", ()) - if own_group and non_group_spec_options: - self.add_option_group( - provider.name.upper(), - provider.__doc__, - non_group_spec_options, - provider, - ) - else: - for opt, optdict in non_group_spec_options: - self.add_optik_option(provider, self.cmdline_parser, opt, optdict) - for gname, gdoc in groups: - gname = gname.upper() - goptions = [ - option - for option in provider.options - if option[1].get("group", "").upper() == gname - ] - self.add_option_group(gname, gdoc, goptions, provider) - - def add_option_group( - self, group_name: str, _: Any, options: Any, provider: OptionsProviderMixin - ) -> None: - # add option group to the command line parser - if group_name in self._mygroups: - group = self._mygroups[group_name] - else: - group = optparse.OptionGroup( - self.cmdline_parser, title=group_name.capitalize() - ) - self.cmdline_parser.add_option_group(group) - self._mygroups[group_name] = group - # add section to the config file - if ( - group_name != "DEFAULT" - and group_name not in self.cfgfile_parser._sections # type: ignore[attr-defined] - ): - self.cfgfile_parser.add_section(group_name) - # add provider's specific options - for opt, optdict in options: - if not isinstance(optdict.get("action", "store"), str): - optdict["action"] = "callback" - self.add_optik_option(provider, group, opt, optdict) - - def add_optik_option( - self, - provider: OptionsProviderMixin, - optikcontainer: Any, - opt: str, - optdict: OptionDict, - ) -> None: - args, optdict = self.optik_option(provider, opt, optdict) - option = optikcontainer.add_option(*args, **optdict) - self._all_options[opt] = provider - self._maxlevel = max(self._maxlevel, option.level or 0) - - def optik_option( - self, provider: OptionsProviderMixin, opt: str, optdict: OptionDict - ) -> tuple[list[str], OptionDict]: - """Get our personal option definition and return a suitable form for - use with optik/optparse. - """ - optdict = copy.copy(optdict) - if "action" in optdict: - self._nocallback_options[provider] = opt - else: - optdict["action"] = "callback" - optdict["callback"] = self.cb_set_provider_option - # default is handled here and *must not* be given to optik if you - # want the whole machinery to work - if "default" in optdict: - if ( - "help" in optdict - and optdict.get("default") is not None - and optdict["action"] not in ("store_true", "store_false") - ): - optdict["help"] += " [current: %default]" # type: ignore[operator] - del optdict["default"] - args = ["--" + str(opt)] - if "short" in optdict: - self._short_options[optdict["short"]] = opt - args.append("-" + optdict["short"]) # type: ignore[operator] - del optdict["short"] - # cleanup option definition dict before giving it to optik - for key in list(optdict.keys()): - if key not in self._optik_option_attrs: - optdict.pop(key) - return args, optdict - - def cb_set_provider_option( - self, option: Option, opt: str, value: Any, parser: Any - ) -> None: - """Optik callback for option setting.""" - if opt.startswith("--"): - # remove -- on long option - opt = opt[2:] - else: - # short option, get its long equivalent - opt = self._short_options[opt[1:]] - # trick since we can't set action='store_true' on options - if value is None: - value = 1 - self.global_set_option(opt, value) - - def global_set_option(self, opt: str, value: Any) -> None: - """Set option on the correct option provider.""" - self._all_options[opt].set_option(opt, value) - - def generate_config( - self, stream: TextIO | None = None, skipsections: tuple[str, ...] = () - ) -> None: - """Write a configuration file according to the current configuration - into the given stream or stdout. - """ - options_by_section: dict[str, list[tuple[str, OptionDict, Any]]] = {} - sections = [] - for provider in self.options_providers: - for section, options in provider.options_by_section(): - if section is None: - section = provider.name - if section in skipsections: - continue - options = [ - (n, d, v) - for (n, d, v) in options - if d.get("type") is not None and not d.get("deprecated") - ] - if not options: - continue - if section not in sections: - sections.append(section) - all_options = options_by_section.setdefault(section, []) - all_options += options - stream = stream or sys.stdout - printed = False - for section in sections: - if printed: - print("\n", file=stream) - utils.format_section( - stream, section.upper(), sorted(options_by_section[section]) - ) - printed = True - - def load_provider_defaults(self) -> None: - """Initialize configuration using default values.""" - for provider in self.options_providers: - provider.load_defaults() - - def read_config_file( - self, config_file: Path | None = None, verbose: bool = False - ) -> None: - """Read the configuration file but do not load it (i.e. dispatching - values to each option's provider). - """ - if config_file: - config_file = Path(os.path.expandvars(config_file)).expanduser() - if not config_file.exists(): - raise OSError(f"The config file {str(config_file)} doesn't exist!") - - parser = self.cfgfile_parser - if config_file.suffix == ".toml": - try: - self._parse_toml(config_file, parser) - except tomllib.TOMLDecodeError: - pass - else: - # Use this encoding in order to strip the BOM marker, if any. - with open(config_file, encoding="utf_8_sig") as fp: - parser.read_file(fp) - # normalize each section's title - for sect, values in list(parser._sections.items()): # type: ignore[attr-defined] - if sect.startswith("pylint."): - sect = sect[len("pylint.") :] - if not sect.isupper() and values: - parser._sections[sect.upper()] = values # type: ignore[attr-defined] - - if not verbose: - return - if config_file and config_file.exists(): - msg = f"Using config file '{config_file}'" - else: - msg = "No config file found, using default configuration" - print(msg, file=sys.stderr) - - def _parse_toml(self, config_file: Path, parser: configparser.ConfigParser) -> None: - """Parse and handle errors of a toml configuration file.""" - with open(config_file, mode="rb") as fp: - content = tomllib.load(fp) - try: - sections_values = content["tool"]["pylint"] - except KeyError: - return - for section, values in sections_values.items(): - section_name = section.upper() - # TOML has rich types, convert values to - # strings as ConfigParser expects. - if not isinstance(values, dict): - # This class is a mixin: add_message comes from the `PyLinter` class - self.add_message( # type: ignore[attr-defined] - "bad-configuration-section", line=0, args=(section, values) - ) - continue - for option, value in values.items(): - if isinstance(value, bool): - values[option] = "yes" if value else "no" - elif isinstance(value, list): - values[option] = ",".join(value) - else: - values[option] = str(value) - for option, value in values.items(): - try: - parser.set(section_name, option, value=value) - except configparser.NoSectionError: - parser.add_section(section_name) - parser.set(section_name, option, value=value) - - def load_config_file(self) -> None: - """Dispatch values previously read from a configuration file to each - option's provider. - """ - parser = self.cfgfile_parser - for section in parser.sections(): - for option, value in parser.items(section): - try: - self.global_set_option(option, value) - except (KeyError, optparse.OptionError): - continue - - def load_configuration(self, **kwargs: Any) -> None: - """Override configuration according to given parameters.""" - return self.load_configuration_from_config(kwargs) - - def load_configuration_from_config(self, config: dict[str, Any]) -> None: - for opt, opt_value in config.items(): - opt = opt.replace("_", "-") - provider = self._all_options[opt] - provider.set_option(opt, opt_value) - - def load_command_line_configuration( - self, args: list[str] | None = None - ) -> list[str]: - """Override configuration according to command line parameters. - - return additional arguments - """ - with _patch_optparse(): - args = sys.argv[1:] if args is None else list(args) - (options, args) = self.cmdline_parser.parse_args(args=args) - for provider in self._nocallback_options: - config = provider.config - for attr in config.__dict__.keys(): - value = getattr(options, attr, None) - if value is None: - continue - setattr(config, attr, value) - return args # type: ignore[return-value] - - def help(self, level: int = 0) -> str: - """Return the usage string for available options.""" - self.cmdline_parser.formatter.output_level = level - with _patch_optparse(): - return str(self.cmdline_parser.format_help()) diff --git a/pylint/config/option_parser.py b/pylint/config/option_parser.py deleted file mode 100644 index 9ffb80116..000000000 --- a/pylint/config/option_parser.py +++ /dev/null @@ -1,56 +0,0 @@ -# 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt - -# type: ignore # Deprecated module. - -import optparse # pylint: disable=deprecated-module -import warnings - -from pylint.config.option import Option - - -def _level_options(group, outputlevel): - return [ - option - for option in group.option_list - if (getattr(option, "level", 0) or 0) <= outputlevel - and option.help is not optparse.SUPPRESS_HELP - ] - - -class OptionParser(optparse.OptionParser): - def __init__(self, option_class, *args, **kwargs): - # TODO: 3.0: Remove deprecated class - warnings.warn( - "OptionParser has been deprecated and will be removed in pylint 3.0", - DeprecationWarning, - stacklevel=2, - ) - super().__init__(option_class=Option, *args, **kwargs) # noqa: B026 - - def format_option_help(self, formatter=None): - if formatter is None: - formatter = self.formatter - outputlevel = getattr(formatter, "output_level", 0) - formatter.store_option_strings(self) - result = [formatter.format_heading("Options")] - formatter.indent() - if self.option_list: - result.append(optparse.OptionContainer.format_option_help(self, formatter)) - result.append("\n") - for group in self.option_groups: - if group.level <= outputlevel and ( - group.description or _level_options(group, outputlevel) - ): - result.append(group.format_help(formatter)) - result.append("\n") - formatter.dedent() - # Drop the last "\n", or the header if no options or option groups: - return "".join(result[:-1]) - - def _match_long_opt(self, opt): # pragma: no cover # Unused - """Disable abbreviations.""" - if opt not in self._long_opt: - raise optparse.BadOptionError(opt) - return opt diff --git a/pylint/config/options_provider_mixin.py b/pylint/config/options_provider_mixin.py deleted file mode 100644 index 67f64ee0a..000000000 --- a/pylint/config/options_provider_mixin.py +++ /dev/null @@ -1,123 +0,0 @@ -# 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt - -# type: ignore # Deprecated module. - -import optparse # pylint: disable=deprecated-module -import warnings - -from pylint.config.callback_actions import _CallbackAction -from pylint.config.option import _validate -from pylint.typing import Options - - -class UnsupportedAction(Exception): - """Raised by set_option when it doesn't know what to do for an action.""" - - -class OptionsProviderMixIn: - """Mixin to provide options to an OptionsManager.""" - - # those attributes should be overridden - name = "default" - options: Options = () - level = 0 - - def __init__(self): - # TODO: 3.0: Remove deprecated class - warnings.warn( - "OptionsProviderMixIn has been deprecated and will be removed in pylint 3.0", - DeprecationWarning, - stacklevel=2, - ) - self.config = optparse.Values() - self.load_defaults() - - def load_defaults(self): - """Initialize the provider using default values.""" - for opt, optdict in self.options: - action = optdict.get("action") - if action != "callback": - # callback action have no default - if optdict is None: - optdict = self.get_option_def(opt) - default = optdict.get("default") - self.set_option(opt, default, action, optdict) - - def option_attrname(self, opt, optdict=None): - """Get the config attribute corresponding to opt.""" - if optdict is None: - optdict = self.get_option_def(opt) - return optdict.get("dest", opt.replace("-", "_")) - - def option_value(self, opt): - """Get the current value for the given option.""" - return getattr(self.config, self.option_attrname(opt), None) - - def set_option(self, optname, value, action=None, optdict=None): - """Method called to set an option (registered in the options list).""" - if optdict is None: - optdict = self.get_option_def(optname) - if value is not None: - value = _validate(value, optdict, optname) - if action is None: - action = optdict.get("action", "store") - if action == "store": - setattr(self.config, self.option_attrname(optname, optdict), value) - elif action in {"store_true", "count"}: - setattr(self.config, self.option_attrname(optname, optdict), value) - elif action == "store_false": - setattr(self.config, self.option_attrname(optname, optdict), value) - elif action == "append": - optname = self.option_attrname(optname, optdict) - _list = getattr(self.config, optname, None) - if _list is None: - if isinstance(value, (list, tuple)): - _list = value - elif value is not None: - _list = [value] - setattr(self.config, optname, _list) - elif isinstance(_list, tuple): - setattr(self.config, optname, _list + (value,)) - else: - _list.append(value) - elif ( - action == "callback" - or (not isinstance(action, str)) - and issubclass(action, _CallbackAction) - ): - return - else: - raise UnsupportedAction(action) - - def get_option_def(self, opt): - """Return the dictionary defining an option given its name.""" - assert self.options - for option in self.options: - if option[0] == opt: - return option[1] - raise optparse.OptionError( - f"no such option {opt} in section {self.name!r}", opt - ) - - def options_by_section(self): - """Return an iterator on options grouped by section. - - (section, [list of (optname, optdict, optvalue)]) - """ - sections = {} - for optname, optdict in self.options: - sections.setdefault(optdict.get("group"), []).append( - (optname, optdict, self.option_value(optname)) - ) - if None in sections: - yield None, sections.pop(None) - for section, options in sorted(sections.items()): - yield section.upper(), options - - def options_and_values(self, options=None): - if options is None: - options = self.options - for optname, optdict in options: - yield optname, optdict, self.option_value(optname) diff --git a/pylint/config/utils.py b/pylint/config/utils.py index d7cbd7c07..91e4ff86f 100644 --- a/pylint/config/utils.py +++ b/pylint/config/utils.py @@ -1,13 +1,12 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Utils for arguments/options parsing and handling.""" from __future__ import annotations import re -import warnings from collections.abc import Callable, Sequence from pathlib import Path from typing import TYPE_CHECKING, Any @@ -39,14 +38,6 @@ def _convert_option_to_argument( | _ExtendArgument ): """Convert an optdict to an Argument class instance.""" - if "level" in optdict and "hide" not in optdict: - warnings.warn( - "The 'level' key in optdicts has been deprecated. " - "Use 'hide' with a boolean to hide an option from the help message. " - f"optdict={optdict}", - DeprecationWarning, - ) - # Get the long and short flags flags = [f"--{opt}"] if "short" in optdict: @@ -74,17 +65,9 @@ def _convert_option_to_argument( section=optdict.get("group", None), metavar=optdict.get("metavar", None), ) - try: - default = optdict["default"] - except KeyError: - warnings.warn( - "An option dictionary should have a 'default' key to specify " - "the option's default value. This key will be required in pylint " - "3.0. It is not required for 'store_true' and callable actions. " - f"optdict={optdict}", - DeprecationWarning, - ) - default = None + + default = optdict["default"] + if action == "extend": return _ExtendArgument( flags=flags, diff --git a/pylint/constants.py b/pylint/constants.py index de29fb9b8..5bf4abf5c 100644 --- a/pylint/constants.py +++ b/pylint/constants.py @@ -1,14 +1,12 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 import os -import pathlib import platform import sys -from datetime import datetime import astroid import platformdirs @@ -49,9 +47,6 @@ MSG_TYPES_STATUS = {"I": 0, "C": 16, "R": 8, "W": 4, "E": 2, "F": 1} # on all project using [MAIN] in their rcfile. MAIN_CHECKER_NAME = "main" -USER_HOME = os.path.expanduser("~") -# TODO: 3.0: Remove in 3.0 with all the surrounding code -OLD_DEFAULT_PYLINT_HOME = ".pylint.d" DEFAULT_PYLINT_HOME = platformdirs.user_cache_dir("pylint") DEFAULT_IGNORE_LIST = ("CVS",) @@ -101,55 +96,10 @@ INCOMPATIBLE_WITH_USELESS_SUPPRESSION = frozenset( ) -def _warn_about_old_home(pylint_home: pathlib.Path) -> None: - """Warn users about the old pylint home being deprecated. - - The spam prevention mechanism is due to pylint being used in parallel by - pre-commit, and the message being spammy in this context - Also if you work with an old version of pylint that recreates the - old pylint home, you can get the old message for a long time. - """ - prefix_spam_prevention = "pylint_warned_about_old_cache_already" - spam_prevention_file = pathlib.Path(pylint_home) / datetime.now().strftime( - prefix_spam_prevention + "_%Y-%m-%d.temp" - ) - old_home = pathlib.Path(USER_HOME) / OLD_DEFAULT_PYLINT_HOME - - if old_home.exists() and not spam_prevention_file.exists(): - print( - f"PYLINTHOME is now '{pylint_home}' but obsolescent '{old_home}' is found; " - "you can safely remove the latter", - file=sys.stderr, - ) - - # Remove old spam prevention file - if pylint_home.exists(): - for filename in pylint_home.iterdir(): - if prefix_spam_prevention in str(filename): - try: - os.remove(pylint_home / filename) - except OSError: # pragma: no cover - pass - - # Create spam prevention file for today - try: - pylint_home.mkdir(parents=True, exist_ok=True) - with open(spam_prevention_file, "w", encoding="utf8") as f: - f.write("") - except Exception as exc: # pragma: no cover # pylint: disable=broad-except - print( - "Can't write the file that was supposed to " - f"prevent 'pylint.d' deprecation spam in {pylint_home} because of {exc}." - ) - - def _get_pylint_home() -> str: """Return the pylint home.""" if "PYLINTHOME" in os.environ: return os.environ["PYLINTHOME"] - - _warn_about_old_home(pathlib.Path(DEFAULT_PYLINT_HOME)) - return DEFAULT_PYLINT_HOME diff --git a/pylint/epylint.py b/pylint/epylint.py deleted file mode 100755 index dd23b450b..000000000 --- a/pylint/epylint.py +++ /dev/null @@ -1,224 +0,0 @@ -# mode: python; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -# -*- vim:fenc=utf-8:ft=python:et:sw=4:ts=4:sts=4 - -# 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt - -"""Emacs and Flymake compatible Pylint. - -This script is for integration with Emacs and is compatible with Flymake mode. - -epylint walks out of python packages before invoking pylint. This avoids -reporting import errors that occur when a module within a package uses the -absolute import path to get another module within this package. - -For example: - - Suppose a package is structured as - - a/__init__.py - a/b/x.py - a/c/y.py - - - Then if y.py imports x as "from a.b import x" the following produces pylint - errors - - cd a/c; pylint y.py - - - The following obviously doesn't - - pylint a/c/y.py - - - As this script will be invoked by Emacs within the directory of the file - we are checking we need to go out of it to avoid these false positives. - -You may also use py_run to run pylint with desired options and get back (or not) -its output. -""" - -from __future__ import annotations - -import os -import shlex -import sys -import warnings -from collections.abc import Sequence -from io import StringIO -from subprocess import PIPE, Popen -from typing import NoReturn, TextIO, overload - -if sys.version_info >= (3, 8): - from typing import Literal -else: - from typing_extensions import Literal - - -def _get_env() -> dict[str, str]: - """Extracts the environment PYTHONPATH and appends the current 'sys.path' - to it. - """ - env = dict(os.environ) - env["PYTHONPATH"] = os.pathsep.join(sys.path) - return env - - -def lint(filename: str, options: Sequence[str] = ()) -> int: - """Pylint the given file. - - When run from Emacs we will be in the directory of a file, and passed its - filename. If this file is part of a package and is trying to import other - modules from within its own package or another package rooted in a directory - below it, pylint will classify it as a failed import. - - To get around this, we traverse down the directory tree to find the root of - the package this module is in. We then invoke pylint from this directory. - - Finally, we must correct the filenames in the output generated by pylint so - Emacs doesn't become confused (it will expect just the original filename, - while pylint may extend it with extra directories if we've traversed down - the tree) - """ - # traverse downwards until we are out of a python package - full_path = os.path.abspath(filename) - parent_path = os.path.dirname(full_path) - child_path = os.path.basename(full_path) - - while parent_path != "/" and os.path.exists( - os.path.join(parent_path, "__init__.py") - ): - child_path = os.path.join(os.path.basename(parent_path), child_path) - parent_path = os.path.dirname(parent_path) - - # Start pylint - # Ensure we use the python and pylint associated with the running epylint - run_cmd = "import sys; from pylint.lint import Run; Run(sys.argv[1:])" - cmd = ( - [sys.executable, "-c", run_cmd] - + [ - "--msg-template", - "{path}:{line}: {category} ({msg_id}, {symbol}, {obj}) {msg}", - "-r", - "n", - child_path, - ] - + list(options) - ) - - with Popen( - cmd, stdout=PIPE, cwd=parent_path, env=_get_env(), universal_newlines=True - ) as process: - for line in process.stdout: # type: ignore[union-attr] - # remove pylintrc warning - if line.startswith("No config file found"): - continue - - # modify the file name that's put out to reverse the path traversal we made - parts = line.split(":") - if parts and parts[0] == child_path: - line = ":".join([filename] + parts[1:]) - print(line, end=" ") - - process.wait() - return process.returncode - - -@overload -def py_run( - command_options: str = ..., - return_std: Literal[False] = ..., - stdout: TextIO | int | None = ..., - stderr: TextIO | int | None = ..., -) -> None: - ... - - -@overload -def py_run( - command_options: str, - return_std: Literal[True], - stdout: TextIO | int | None = ..., - stderr: TextIO | int | None = ..., -) -> tuple[StringIO, StringIO]: - ... - - -def py_run( - command_options: str = "", - return_std: bool = False, - stdout: TextIO | int | None = None, - stderr: TextIO | int | None = None, -) -> tuple[StringIO, StringIO] | None: - """Run pylint from python. - - ``command_options`` is a string containing ``pylint`` command line options; - ``return_std`` (boolean) indicates return of created standard output - and error (see below); - ``stdout`` and ``stderr`` are 'file-like' objects in which standard output - could be written. - - Calling agent is responsible for stdout/err management (creation, close). - Default standard output and error are those from sys, - or standalone ones (``subprocess.PIPE``) are used - if they are not set and ``return_std``. - - If ``return_std`` is set to ``True``, this function returns a 2-uple - containing standard output and error related to created process, - as follows: ``(stdout, stderr)``. - - To silently run Pylint on a module, and get its standard output and error: - >>> (pylint_stdout, pylint_stderr) = py_run( 'module_name.py', True) - """ - warnings.warn( - "'epylint' will be removed in pylint 3.0, use https://github.com/emacsorphanage/pylint instead.", - DeprecationWarning, - stacklevel=2, - ) - # Detect if we use Python as executable or not, else default to `python` - executable = sys.executable if "python" in sys.executable else "python" - - # Create command line to call pylint - epylint_part = [executable, "-c", "from pylint import epylint;epylint.Run()"] - options = shlex.split(command_options, posix=not sys.platform.startswith("win")) - cli = epylint_part + options - - # Providing standard output and/or error if not set - if stdout is None: - stdout = PIPE if return_std else sys.stdout - if stderr is None: - stderr = PIPE if return_std else sys.stderr - # Call pylint in a sub-process - with Popen( - cli, - shell=False, - stdout=stdout, - stderr=stderr, - env=_get_env(), - universal_newlines=True, - ) as process: - proc_stdout, proc_stderr = process.communicate() - # Return standard output and error - if return_std: - return StringIO(proc_stdout), StringIO(proc_stderr) - return None - - -def Run(argv: Sequence[str] | None = None) -> NoReturn: - warnings.warn( - "'epylint' will be removed in pylint 3.0, use https://github.com/emacsorphanage/pylint instead.", - DeprecationWarning, - stacklevel=2, - ) - if not argv and len(sys.argv) == 1: - print(f"Usage: {sys.argv[0]} <filename> [options]") - sys.exit(1) - - argv = argv or sys.argv[1:] - if not os.path.exists(argv[0]): - print(f"{argv[0]} does not exist") - sys.exit(1) - else: - sys.exit(lint(argv[0], argv[1:])) - - -if __name__ == "__main__": - Run() diff --git a/pylint/exceptions.py b/pylint/exceptions.py index 766eb9327..2bfbfa8cc 100644 --- a/pylint/exceptions.py +++ b/pylint/exceptions.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Exception classes raised by various operations within pylint.""" diff --git a/pylint/extensions/__init__.py b/pylint/extensions/__init__.py index 01b978e4b..e9e2b0d1b 100644 --- a/pylint/extensions/__init__.py +++ b/pylint/extensions/__init__.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 diff --git a/pylint/extensions/_check_docs_utils.py b/pylint/extensions/_check_docs_utils.py index 33edc95e9..dddc577d6 100644 --- a/pylint/extensions/_check_docs_utils.py +++ b/pylint/extensions/_check_docs_utils.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Utility methods for docstring checking.""" diff --git a/pylint/extensions/bad_builtin.py b/pylint/extensions/bad_builtin.py index 904b2a394..22ef2b95e 100644 --- a/pylint/extensions/bad_builtin.py +++ b/pylint/extensions/bad_builtin.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Checker for deprecated builtins.""" diff --git a/pylint/extensions/broad_try_clause.py b/pylint/extensions/broad_try_clause.py index 2291d32d4..762055a25 100644 --- a/pylint/extensions/broad_try_clause.py +++ b/pylint/extensions/broad_try_clause.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Looks for try/except statements with too much code in the try clause.""" diff --git a/pylint/extensions/check_elif.py b/pylint/extensions/check_elif.py index b584ea35e..de20ed8ec 100644 --- a/pylint/extensions/check_elif.py +++ b/pylint/extensions/check_elif.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 diff --git a/pylint/extensions/code_style.py b/pylint/extensions/code_style.py index 262a7f0c4..5ce1ae476 100644 --- a/pylint/extensions/code_style.py +++ b/pylint/extensions/code_style.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 diff --git a/pylint/extensions/comparetozero.py b/pylint/extensions/comparetozero.py deleted file mode 100644 index 116bf229a..000000000 --- a/pylint/extensions/comparetozero.py +++ /dev/null @@ -1,95 +0,0 @@ -# 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt - -"""Looks for comparisons to zero.""" - -from __future__ import annotations - -import itertools -from typing import TYPE_CHECKING - -import astroid -from astroid import nodes - -from pylint import checkers -from pylint.checkers import utils -from pylint.interfaces import HIGH - -if TYPE_CHECKING: - from pylint.lint import PyLinter - - -def _is_constant_zero(node: str | nodes.NodeNG) -> bool: - # We have to check that node.value is not False because node.value == 0 is True - # when node.value is False - return ( - isinstance(node, astroid.Const) and node.value == 0 and node.value is not False - ) - - -class CompareToZeroChecker(checkers.BaseChecker): - """Checks for comparisons to zero. - - Most of the time you should use the fact that integers with a value of 0 are false. - An exception to this rule is when 0 is allowed in the program and has a - different meaning than None! - """ - - # configuration section name - name = "compare-to-zero" - msgs = { - "C2001": ( - '"%s" can be simplified to "%s" as 0 is falsey', - "compare-to-zero", - "Used when Pylint detects comparison to a 0 constant.", - ) - } - - options = () - - @utils.only_required_for_messages("compare-to-zero") - def visit_compare(self, node: nodes.Compare) -> None: - # pylint: disable=duplicate-code - _operators = ["!=", "==", "is not", "is"] - # note: astroid.Compare has the left most operand in node.left - # while the rest are a list of tuples in node.ops - # the format of the tuple is ('compare operator sign', node) - # here we squash everything into `ops` to make it easier for processing later - ops: list[tuple[str, nodes.NodeNG]] = [("", node.left)] - ops.extend(node.ops) - iter_ops = iter(ops) - all_ops = list(itertools.chain(*iter_ops)) - - for ops_idx in range(len(all_ops) - 2): - op_1 = all_ops[ops_idx] - op_2 = all_ops[ops_idx + 1] - op_3 = all_ops[ops_idx + 2] - error_detected = False - - # 0 ?? X - if _is_constant_zero(op_1) and op_2 in _operators: - error_detected = True - op = op_3 - # X ?? 0 - elif op_2 in _operators and _is_constant_zero(op_3): - error_detected = True - op = op_1 - - if error_detected: - original = f"{op_1.as_string()} {op_2} {op_3.as_string()}" - suggestion = ( - op.as_string() - if op_2 in {"!=", "is not"} - else f"not {op.as_string()}" - ) - self.add_message( - "compare-to-zero", - args=(original, suggestion), - node=node, - confidence=HIGH, - ) - - -def register(linter: PyLinter) -> None: - linter.register_checker(CompareToZeroChecker(linter)) diff --git a/pylint/extensions/comparison_placement.py b/pylint/extensions/comparison_placement.py index df7cc9890..f7ecceae3 100644 --- a/pylint/extensions/comparison_placement.py +++ b/pylint/extensions/comparison_placement.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Checks for yoda comparisons (variable before constant) See https://en.wikipedia.org/wiki/Yoda_conditions. diff --git a/pylint/extensions/confusing_elif.py b/pylint/extensions/confusing_elif.py index ada8b5ab1..546b644b3 100644 --- a/pylint/extensions/confusing_elif.py +++ b/pylint/extensions/confusing_elif.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 diff --git a/pylint/extensions/consider_refactoring_into_while_condition.py b/pylint/extensions/consider_refactoring_into_while_condition.py index b4b53d8fa..b7e905e8a 100644 --- a/pylint/extensions/consider_refactoring_into_while_condition.py +++ b/pylint/extensions/consider_refactoring_into_while_condition.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Looks for try/except statements with too much code in the try clause.""" diff --git a/pylint/extensions/consider_ternary_expression.py b/pylint/extensions/consider_ternary_expression.py index 0e9444662..83046ce38 100644 --- a/pylint/extensions/consider_ternary_expression.py +++ b/pylint/extensions/consider_ternary_expression.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Check for if / assign blocks that can be rewritten with if-expressions.""" diff --git a/pylint/extensions/dict_init_mutate.py b/pylint/extensions/dict_init_mutate.py index fb4c83647..4977e234b 100644 --- a/pylint/extensions/dict_init_mutate.py +++ b/pylint/extensions/dict_init_mutate.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Check for use of dictionary mutation after initialization.""" from __future__ import annotations diff --git a/pylint/extensions/docparams.py b/pylint/extensions/docparams.py index 0c2e4e9e3..6e173c39e 100644 --- a/pylint/extensions/docparams.py +++ b/pylint/extensions/docparams.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Pylint plugin for checking in Sphinx, Google, or Numpy style docstrings.""" diff --git a/pylint/extensions/docstyle.py b/pylint/extensions/docstyle.py index 1ca2885e9..c54ab93b2 100644 --- a/pylint/extensions/docstyle.py +++ b/pylint/extensions/docstyle.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 diff --git a/pylint/extensions/dunder.py b/pylint/extensions/dunder.py index e0e9af316..ad3e272aa 100644 --- a/pylint/extensions/dunder.py +++ b/pylint/extensions/dunder.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 diff --git a/pylint/extensions/empty_comment.py b/pylint/extensions/empty_comment.py index e8a914708..61e257ffd 100644 --- a/pylint/extensions/empty_comment.py +++ b/pylint/extensions/empty_comment.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 diff --git a/pylint/extensions/emptystring.py b/pylint/extensions/emptystring.py deleted file mode 100644 index f96a980f5..000000000 --- a/pylint/extensions/emptystring.py +++ /dev/null @@ -1,78 +0,0 @@ -# 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt - -"""Looks for comparisons to empty string.""" - -from __future__ import annotations - -import itertools -from typing import TYPE_CHECKING - -from astroid import nodes - -from pylint import checkers -from pylint.checkers import utils -from pylint.interfaces import HIGH - -if TYPE_CHECKING: - from pylint.lint import PyLinter - - -class CompareToEmptyStringChecker(checkers.BaseChecker): - name = "compare-to-empty-string" - msgs = { - "C1901": ( - '"%s" can be simplified to "%s" as an empty string is falsey', - "compare-to-empty-string", - "Used when Pylint detects comparison to an empty string constant.", - ) - } - - options = () - - @utils.only_required_for_messages("compare-to-empty-string") - def visit_compare(self, node: nodes.Compare) -> None: - """Checks for comparisons to empty string. - - Most of the time you should use the fact that empty strings are false. - An exception to this rule is when an empty string value is allowed in the program - and has a different meaning than None! - """ - _operators = {"!=", "==", "is not", "is"} - # note: astroid.Compare has the left most operand in node.left while the rest - # are a list of tuples in node.ops the format of the tuple is - # ('compare operator sign', node) here we squash everything into `ops` - # to make it easier for processing later - ops: list[tuple[str, nodes.NodeNG | None]] = [("", node.left)] - ops.extend(node.ops) - iter_ops = iter(ops) - ops = list(itertools.chain(*iter_ops)) # type: ignore[arg-type] - for ops_idx in range(len(ops) - 2): - op_1: nodes.NodeNG | None = ops[ops_idx] - op_2: str = ops[ops_idx + 1] # type: ignore[assignment] - op_3: nodes.NodeNG | None = ops[ops_idx + 2] - error_detected = False - if op_1 is None or op_3 is None or op_2 not in _operators: - continue - node_name = "" - # x ?? "" - if utils.is_empty_str_literal(op_1): - error_detected = True - node_name = op_3.as_string() - # '' ?? X - elif utils.is_empty_str_literal(op_3): - error_detected = True - node_name = op_1.as_string() - if error_detected: - suggestion = f"not {node_name}" if op_2 in {"==", "is"} else node_name - self.add_message( - "compare-to-empty-string", - args=(node.as_string(), suggestion), - node=node, - confidence=HIGH, - ) - - -def register(linter: PyLinter) -> None: - linter.register_checker(CompareToEmptyStringChecker(linter)) diff --git a/pylint/extensions/eq_without_hash.py b/pylint/extensions/eq_without_hash.py index b0d0f01bd..5f39dfa3e 100644 --- a/pylint/extensions/eq_without_hash.py +++ b/pylint/extensions/eq_without_hash.py @@ -1,12 +1,12 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """This is the remnant of the python3 checker. It was removed because the transition from python 2 to python3 is behind us, but some checks are still useful in python3 after all. -See https://github.com/PyCQA/pylint/issues/5025 +See https://github.com/pylint-dev/pylint/issues/5025 """ from astroid import nodes diff --git a/pylint/extensions/for_any_all.py b/pylint/extensions/for_any_all.py index bc7dd9c48..2369a595d 100644 --- a/pylint/extensions/for_any_all.py +++ b/pylint/extensions/for_any_all.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Check for use of for loops that only check for a condition.""" diff --git a/pylint/extensions/magic_value.py b/pylint/extensions/magic_value.py index 7cfb410ae..c69711f85 100644 --- a/pylint/extensions/magic_value.py +++ b/pylint/extensions/magic_value.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Checks for magic values instead of literals.""" diff --git a/pylint/extensions/mccabe.py b/pylint/extensions/mccabe.py index ea64d2ebf..604360fb7 100644 --- a/pylint/extensions/mccabe.py +++ b/pylint/extensions/mccabe.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Module to add McCabe checker class for pylint.""" diff --git a/pylint/extensions/no_self_use.py b/pylint/extensions/no_self_use.py index 0fd38877f..8b9b8aa9b 100644 --- a/pylint/extensions/no_self_use.py +++ b/pylint/extensions/no_self_use.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 diff --git a/pylint/extensions/overlapping_exceptions.py b/pylint/extensions/overlapping_exceptions.py index 93d225137..ddcf24e7c 100644 --- a/pylint/extensions/overlapping_exceptions.py +++ b/pylint/extensions/overlapping_exceptions.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Looks for overlapping exceptions.""" diff --git a/pylint/extensions/private_import.py b/pylint/extensions/private_import.py index df08c7116..a9e537c1b 100644 --- a/pylint/extensions/private_import.py +++ b/pylint/extensions/private_import.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Check for imports on private external modules and names.""" diff --git a/pylint/extensions/redefined_loop_name.py b/pylint/extensions/redefined_loop_name.py index df333fab9..d03b80be3 100644 --- a/pylint/extensions/redefined_loop_name.py +++ b/pylint/extensions/redefined_loop_name.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Optional checker to warn when loop variables are overwritten in the loop's body.""" diff --git a/pylint/extensions/redefined_variable_type.py b/pylint/extensions/redefined_variable_type.py index 8d88d856e..ba5af3136 100644 --- a/pylint/extensions/redefined_variable_type.py +++ b/pylint/extensions/redefined_variable_type.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 diff --git a/pylint/extensions/set_membership.py b/pylint/extensions/set_membership.py index f267e046f..b72f5aa18 100644 --- a/pylint/extensions/set_membership.py +++ b/pylint/extensions/set_membership.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 diff --git a/pylint/extensions/typing.py b/pylint/extensions/typing.py index 264d0c383..2956465cf 100644 --- a/pylint/extensions/typing.py +++ b/pylint/extensions/typing.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 diff --git a/pylint/extensions/while_used.py b/pylint/extensions/while_used.py index 6f9612196..da1f9d59c 100644 --- a/pylint/extensions/while_used.py +++ b/pylint/extensions/while_used.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Check for use of while loops.""" diff --git a/pylint/graph.py b/pylint/graph.py index 5cffca615..4112fadfa 100644 --- a/pylint/graph.py +++ b/pylint/graph.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Graph manipulation utilities. diff --git a/pylint/interfaces.py b/pylint/interfaces.py index 221084fab..a93a40ceb 100644 --- a/pylint/interfaces.py +++ b/pylint/interfaces.py @@ -1,28 +1,12 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt - -"""Interfaces for Pylint objects.""" +# 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 -import warnings -from tokenize import TokenInfo -from typing import TYPE_CHECKING, NamedTuple - -from astroid import nodes - -if TYPE_CHECKING: - from pylint.checkers import BaseChecker - from pylint.message import Message - from pylint.reporters.ureports.nodes import Section +from typing import NamedTuple __all__ = ( - "IRawChecker", - "IAstroidChecker", - "ITokenChecker", - "IReporter", - "IChecker", "HIGH", "CONTROL_FLOW", "INFERENCE", @@ -51,87 +35,3 @@ UNDEFINED = Confidence("UNDEFINED", "Warning without any associated confidence l CONFIDENCE_LEVELS = [HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, UNDEFINED] CONFIDENCE_LEVEL_NAMES = [i.name for i in CONFIDENCE_LEVELS] - - -class Interface: - """Base class for interfaces.""" - - def __init__(self) -> None: - warnings.warn( - "Interface and all of its subclasses have been deprecated " - "and will be removed in pylint 3.0.", - DeprecationWarning, - stacklevel=2, - ) - - @classmethod - def is_implemented_by( - cls: type[Interface] | tuple[type[Interface], ...], instance: BaseChecker - ) -> bool: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - return implements(instance, cls) - - -def implements( - obj: BaseChecker, - interface: type[Interface] | tuple[type[Interface], ...], -) -> bool: - """Does the given object (maybe an instance or class) implement the interface.""" - # TODO: 3.0: Remove deprecated function - warnings.warn( - "implements has been deprecated in favour of using basic " - "inheritance patterns without using __implements__.", - DeprecationWarning, - stacklevel=2, - ) - implements_ = getattr(obj, "__implements__", ()) - if not isinstance(implements_, (list, tuple)): - implements_ = (implements_,) - return any(issubclass(i, interface) for i in implements_) - - -class IChecker(Interface): - """Base interface, to be used only for sub interfaces definition.""" - - def open(self) -> None: - """Called before visiting project (i.e. set of modules).""" - - def close(self) -> None: - """Called after visiting project (i.e. set of modules).""" - - -class IRawChecker(IChecker): - """Interface for checker which need to parse the raw file.""" - - def process_module(self, node: nodes.Module) -> None: - """Process a module. - - The module's content is accessible via ``astroid.stream`` - """ - - -class ITokenChecker(IChecker): - """Interface for checkers that need access to the token list.""" - - def process_tokens(self, tokens: list[TokenInfo]) -> None: - """Process a module. - - Tokens is a list of all source code tokens in the file. - """ - - -class IAstroidChecker(IChecker): - """Interface for checker which prefers receive events according to - statement type. - """ - - -class IReporter(Interface): - """Reporter collect messages and display results encapsulated in a layout.""" - - def handle_message(self, msg: Message) -> None: - """Handle the given message object.""" - - def display_reports(self, layout: Section) -> None: - """Display results encapsulated in the layout tree.""" diff --git a/pylint/lint/__init__.py b/pylint/lint/__init__.py index 573d9c262..adc920708 100644 --- a/pylint/lint/__init__.py +++ b/pylint/lint/__init__.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Pylint [options] modules_or_packages. @@ -27,12 +27,7 @@ from pylint.lint.report_functions import ( report_total_messages_stats, ) from pylint.lint.run import Run -from pylint.lint.utils import ( - _augment_sys_path, - _patch_sys_path, - augmented_sys_path, - fix_import_path, -) +from pylint.lint.utils import _augment_sys_path, augmented_sys_path __all__ = [ "check_parallel", @@ -42,8 +37,6 @@ __all__ = [ "report_total_messages_stats", "Run", "ArgumentPreprocessingError", - "_patch_sys_path", - "fix_import_path", "_augment_sys_path", "augmented_sys_path", "discover_package_path", diff --git a/pylint/lint/base_options.py b/pylint/lint/base_options.py index b7c81991a..ee811db4e 100644 --- a/pylint/lint/base_options.py +++ b/pylint/lint/base_options.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Functions that creates the basic options for the Run and PyLinter classes.""" diff --git a/pylint/lint/caching.py b/pylint/lint/caching.py index 8ea8a2236..97c4503d1 100644 --- a/pylint/lint/caching.py +++ b/pylint/lint/caching.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 @@ -45,6 +45,7 @@ def load_results( "You're using an old pylint cache with invalid data following " f"an upgrade, please delete '{data_file}'.", UserWarning, + stacklevel=2, ) raise TypeError return data diff --git a/pylint/lint/expand_modules.py b/pylint/lint/expand_modules.py index bb25986e4..1e8fd032f 100644 --- a/pylint/lint/expand_modules.py +++ b/pylint/lint/expand_modules.py @@ -1,12 +1,11 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 import os import sys -import warnings from collections.abc import Sequence from re import Pattern @@ -24,18 +23,6 @@ def _modpath_from_file(filename: str, is_namespace: bool, path: list[str]) -> li ) -def get_python_path(filepath: str) -> str: - # TODO: Remove deprecated function - warnings.warn( - "get_python_path has been deprecated because assumption that there's always an __init__.py " - "is not true since python 3.3 and is causing problems, particularly with PEP 420." - "Use discover_package_path and pass source root(s).", - DeprecationWarning, - stacklevel=2, - ) - return discover_package_path(filepath, []) - - def discover_package_path(modulepath: str, source_roots: Sequence[str]) -> str: """Discover package path from one its modules and source roots.""" dirname = os.path.realpath(os.path.expanduser(modulepath)) @@ -101,7 +88,7 @@ def expand_modules( ): continue module_package_path = discover_package_path(something, source_roots) - additional_search_path = [".", module_package_path] + path + additional_search_path = [".", module_package_path, *path] if os.path.exists(something): # this is a file or a directory try: diff --git a/pylint/lint/message_state_handler.py b/pylint/lint/message_state_handler.py index ddeeaa7bc..26028f0fa 100644 --- a/pylint/lint/message_state_handler.py +++ b/pylint/lint/message_state_handler.py @@ -1,13 +1,12 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 -import sys import tokenize from collections import defaultdict -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Literal from pylint import exceptions, interfaces from pylint.constants import ( @@ -27,12 +26,6 @@ from pylint.utils.pragma_parser import ( parse_pragma, ) -if sys.version_info >= (3, 8): - from typing import Literal -else: - from typing_extensions import Literal - - if TYPE_CHECKING: from pylint.lint.pylinter import PyLinter @@ -55,9 +48,8 @@ class _MessageStateHandler: "enable-msg": self._options_methods["enable"], } self._pragma_lineno: dict[str, int] = {} - # TODO: 3.0: Update key type to str when current_name is always str self._stashed_messages: defaultdict[ - tuple[str | None, str], list[tuple[str | None, str]] + tuple[str, str], list[tuple[str | None, str]] ] = defaultdict(list) """Some messages in the options (for --enable and --disable) are encountered too early to warn about them. diff --git a/pylint/lint/parallel.py b/pylint/lint/parallel.py index 544a256d3..e57c7e8ab 100644 --- a/pylint/lint/parallel.py +++ b/pylint/lint/parallel.py @@ -1,11 +1,10 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 import functools -import warnings from collections import defaultdict from collections.abc import Iterable, Sequence from typing import TYPE_CHECKING, Any @@ -42,7 +41,7 @@ def _worker_initialize( """Function called to initialize a worker for a Process within a concurrent Pool. :param linter: A linter-class (PyLinter) instance pickled with dill - :param extra_packages_paths: Extra entries to be added to sys.path + :param extra_packages_paths: Extra entries to be added to `sys.path` """ global _worker_linter # pylint: disable=global-statement _worker_linter = dill.loads(linter) @@ -61,10 +60,9 @@ def _worker_check_single_file( file_item: FileItem, ) -> tuple[ int, - # TODO: 3.0: Make this only str after deprecation has been removed - str | None, str, - str | None, + str, + str, list[Message], LinterStats, int, @@ -82,14 +80,6 @@ def _worker_check_single_file( msgs = _worker_linter.reporter.messages assert isinstance(_worker_linter.reporter, reporters.CollectingReporter) _worker_linter.reporter.reset() - if _worker_linter.current_name is None: - warnings.warn( - ( - "In pylint 3.0 the current_name attribute of the linter object should be a string. " - "If unknown it should be initialized as an empty string." - ), - DeprecationWarning, - ) return ( id(multiprocessing.current_process()), _worker_linter.current_name, @@ -175,4 +165,4 @@ def check_parallel( linter.msg_status |= msg_status _merge_mapreduce_data(linter, all_mapreduce_data) - linter.stats = merge_stats([linter.stats] + all_stats) + linter.stats = merge_stats([linter.stats, *all_stats]) diff --git a/pylint/lint/pylinter.py b/pylint/lint/pylinter.py index 863076f8f..ed607aca5 100644 --- a/pylint/lint/pylinter.py +++ b/pylint/lint/pylinter.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 @@ -12,14 +12,13 @@ import os import sys import tokenize import traceback -import warnings from collections import defaultdict from collections.abc import Callable, Iterator, Sequence from io import TextIOWrapper from pathlib import Path from re import Pattern from types import ModuleType -from typing import Any +from typing import Any, Protocol import astroid from astroid import nodes @@ -69,12 +68,6 @@ from pylint.typing import ( ) from pylint.utils import ASTWalker, FileState, LinterStats, utils -if sys.version_info >= (3, 8): - from typing import Protocol -else: - from typing_extensions import Protocol - - MANAGER = astroid.MANAGER @@ -139,26 +132,38 @@ MSGS: dict[str, MessageDefinitionTuple] = { "raw-checker-failed", "Used to inform that a built-in module has not been checked " "using the raw checkers.", - {"scope": WarningScope.LINE}, + { + "scope": WarningScope.LINE, + "default_enabled": False, + }, ), "I0010": ( "Unable to consider inline option %r", "bad-inline-option", "Used when an inline option is either badly formatted or can't " "be used inside modules.", - {"scope": WarningScope.LINE}, + { + "scope": WarningScope.LINE, + "default_enabled": False, + }, ), "I0011": ( "Locally disabling %s (%s)", "locally-disabled", "Used when an inline option disables a message or a messages category.", - {"scope": WarningScope.LINE}, + { + "scope": WarningScope.LINE, + "default_enabled": False, + }, ), "I0013": ( "Ignoring entire file", "file-ignored", "Used to inform that the file will not be checked", - {"scope": WarningScope.LINE}, + { + "scope": WarningScope.LINE, + "default_enabled": False, + }, ), "I0020": ( "Suppressed %s (from line %d)", @@ -167,14 +172,20 @@ MSGS: dict[str, MessageDefinitionTuple] = { "by a disable= comment in the file. This message is not " "generated for messages that are ignored due to configuration " "settings.", - {"scope": WarningScope.LINE}, + { + "scope": WarningScope.LINE, + "default_enabled": False, + }, ), "I0021": ( "Useless suppression of %s", "useless-suppression", "Reported when a message is explicitly disabled for a line or " "a block of code, but never triggered.", - {"scope": WarningScope.LINE}, + { + "scope": WarningScope.LINE, + "default_enabled": False, + }, ), "I0022": ( 'Pragma "%s" is deprecated, use "%s" instead', @@ -185,6 +196,7 @@ MSGS: dict[str, MessageDefinitionTuple] = { { "old_names": [("I0014", "deprecated-disable-all")], "scope": WarningScope.LINE, + "default_enabled": False, }, ), "E0001": ( @@ -311,7 +323,8 @@ class PyLinter( self.options: Options = options + _make_linter_options(self) for opt_group in option_groups: self.option_groups_descs[opt_group[0]] = opt_group[1] - self._option_groups: tuple[tuple[str, str], ...] = option_groups + ( + self._option_groups: tuple[tuple[str, str], ...] = ( + *option_groups, ("Messages control", "Options controlling analysis messages"), ("Reports", "Options related to output formatting and reporting"), ) @@ -339,32 +352,13 @@ class PyLinter( # Attributes related to visiting files self.file_state = FileState("", self.msgs_store, is_base_filestate=True) - self.current_name: str | None = None + self.current_name: str = "" self.current_file: str | None = None self._ignore_file = False self._ignore_paths: list[Pattern[str]] = [] self.register_checker(self) - @property - def option_groups(self) -> tuple[tuple[str, str], ...]: - # TODO: 3.0: Remove deprecated attribute - warnings.warn( - "The option_groups attribute has been deprecated and will be removed in pylint 3.0", - DeprecationWarning, - stacklevel=2, - ) - return self._option_groups - - @option_groups.setter - def option_groups(self, value: tuple[tuple[str, str], ...]) -> None: - warnings.warn( - "The option_groups attribute has been deprecated and will be removed in pylint 3.0", - DeprecationWarning, - stacklevel=2, - ) - self._option_groups = value - def load_default_plugins(self) -> None: checkers.initialize(self) reporters.initialize(self) @@ -655,23 +649,12 @@ class PyLinter( else: yield something - def check(self, files_or_modules: Sequence[str] | str) -> None: + def check(self, files_or_modules: Sequence[str]) -> None: """Main checking entry: check a list of files or modules from their name. files_or_modules is either a string or list of strings presenting modules to check. """ - # 1) Initialize self.initialize() - - # 2) Gather all files - if not isinstance(files_or_modules, (list, tuple)): - # TODO: 3.0: Remove deprecated typing and update docstring - warnings.warn( - "In pylint 3.0, the checkers check function will only accept sequence of string", - DeprecationWarning, - stacklevel=2, - ) - files_or_modules = (files_or_modules,) # type: ignore[assignment] if self.config.recursive: files_or_modules = tuple(self._discover_files(files_or_modules)) if self.config.from_stdin: @@ -687,7 +670,7 @@ class PyLinter( } ) - # TODO: Move the parallel invocation into step 5 of the checking process + # TODO: Move the parallel invocation into step 3 of the checking process if not self.config.from_stdin and self.config.jobs > 1: original_sys_path = sys.path[:] check_parallel( @@ -699,7 +682,7 @@ class PyLinter( sys.path = original_sys_path return - # 3) Get all FileItems + # 1) Get all FileItems with augmented_sys_path(extra_packages_paths): if self.config.from_stdin: fileitems = self._get_file_descr_from_stdin(files_or_modules[0]) @@ -711,10 +694,10 @@ class PyLinter( # The contextmanager also opens all checkers and sets up the PyLinter class with augmented_sys_path(extra_packages_paths): with self._astroid_module_checker() as check_astroid_module: - # 4) Get the AST for each FileItem + # 2) Get the AST for each FileItem ast_per_fileitem = self._get_asts(fileitems, data) - # 5) Lint each ast + # 3) Lint each ast self._lint_files(ast_per_fileitem, check_astroid_module) def _get_asts( @@ -743,15 +726,6 @@ class PyLinter( return ast_per_fileitem - def check_single_file(self, name: str, filepath: str, modname: str) -> None: - warnings.warn( - "In pylint 3.0, the checkers check_single_file function will be removed. " - "Use check_single_file_item instead.", - DeprecationWarning, - stacklevel=2, - ) - self.check_single_file_item(FileItem(name, filepath, modname)) - def check_single_file_item(self, file: FileItem) -> None: """Check single file item. @@ -919,26 +893,13 @@ class PyLinter( self.add_message(key, args=message) return result - def set_current_module( - self, modname: str | None, filepath: str | None = None - ) -> None: + def set_current_module(self, modname: str, filepath: str | None = None) -> None: """Set the name of the currently analyzed module and init statistics for it. """ if not modname and filepath is None: return self.reporter.on_set_current_module(modname or "", filepath) - if modname is None: - # TODO: 3.0: Remove all modname or ""'s in this method - warnings.warn( - ( - "In pylint 3.0 modname should be a string so that it can be used to " - "correctly set the current_name attribute of the linter instance. " - "If unknown it should be initialized as an empty string." - ), - DeprecationWarning, - stacklevel=2, - ) self.current_name = modname self.current_file = filepath or modname self.stats.init_single_module(modname or "") @@ -976,41 +937,9 @@ class PyLinter( tokencheckers = [ c for c in _checkers if isinstance(c, checkers.BaseTokenChecker) ] - # TODO: 3.0: Remove deprecated for-loop - for c in _checkers: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - if ( - interfaces.implements(c, interfaces.ITokenChecker) - and c not in tokencheckers - and c is not self - ): - tokencheckers.append(c) # type: ignore[arg-type] # pragma: no cover - warnings.warn( # pragma: no cover - "Checkers should subclass BaseTokenChecker " - "instead of using the __implements__ mechanism. Use of __implements__ " - "will no longer be supported in pylint 3.0", - DeprecationWarning, - ) rawcheckers = [ c for c in _checkers if isinstance(c, checkers.BaseRawFileChecker) ] - # TODO: 3.0: Remove deprecated if-statement - for c in _checkers: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - if ( - interfaces.implements(c, interfaces.IRawChecker) - and c not in rawcheckers - ): - rawcheckers.append(c) # type: ignore[arg-type] # pragma: no cover - warnings.warn( # pragma: no cover - "Checkers should subclass BaseRawFileChecker " - "instead of using the __implements__ mechanism. Use of __implements__ " - "will no longer be supported in pylint 3.0", - DeprecationWarning, - ) - # notify global begin for checker in _checkers: checker.open() walker.add_checker(checker) @@ -1085,10 +1014,6 @@ class PyLinter( retval = self._check_astroid_module( ast_node, walker, rawcheckers, tokencheckers ) - - # TODO: 3.0: Remove unnecessary assertion - assert self.current_name - self.stats.by_module[self.current_name]["statement"] = ( walker.nbstatements - before_check_statements ) @@ -1154,12 +1079,7 @@ class PyLinter( """ # Display whatever messages are left on the reporter. self.reporter.display_messages(report_nodes.Section()) - - # TODO: 3.0: Remove second half of if-statement - if ( - not self.file_state._is_base_filestate - and self.file_state.base_name is not None - ): + if not self.file_state._is_base_filestate: # load previous results if any previous_stats = load_results(self.file_state.base_name) self.reporter.on_close(self.stats, previous_stats) @@ -1181,11 +1101,9 @@ class PyLinter( def _report_evaluation(self) -> int | None: """Make the global evaluation report.""" - # check with at least check 1 statements (usually 0 when there is a + # check with at least a statement (usually 0 when there is a # syntax error preventing pylint from further processing) note = None - # TODO: 3.0: Remove assertion - assert self.file_state.base_name is not None previous_stats = load_results(self.file_state.base_name) if self.stats.statement == 0: return note @@ -1270,12 +1188,7 @@ class PyLinter( msg_cat = MSG_TYPES[message_definition.msgid[0]] self.msg_status |= MSG_TYPES_STATUS[message_definition.msgid[0]] self.stats.increase_single_message_count(msg_cat, 1) - # TODO: 3.0 Should be removable after https://github.com/PyCQA/pylint/pull/5580 - self.stats.increase_single_module_message_count( - self.current_name, # type: ignore[arg-type] - msg_cat, - 1, - ) + self.stats.increase_single_module_message_count(self.current_name, msg_cat, 1) try: self.stats.by_msg[message_definition.symbol] += 1 except KeyError: diff --git a/pylint/lint/report_functions.py b/pylint/lint/report_functions.py index 7d1674977..da7ab5fbc 100644 --- a/pylint/lint/report_functions.py +++ b/pylint/lint/report_functions.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 diff --git a/pylint/lint/run.py b/pylint/lint/run.py index 49b807f87..776803f2e 100644 --- a/pylint/lint/run.py +++ b/pylint/lint/run.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 @@ -9,7 +9,7 @@ import sys import warnings from collections.abc import Sequence from pathlib import Path -from typing import Any, ClassVar +from typing import ClassVar from pylint import config from pylint.checkers.utils import clear_lru_caches @@ -97,9 +97,6 @@ def _cpu_count() -> int: return cpu_count -UNUSED_PARAM_SENTINEL = object() - - class Run: """Helper class to use as main for pylint with 'run(*sys.argv[1:])'.""" @@ -123,7 +120,6 @@ group are mutually exclusive.", args: Sequence[str], reporter: BaseReporter | None = None, exit: bool = True, # pylint: disable=redefined-builtin - do_exit: Any = UNUSED_PARAM_SENTINEL, ) -> None: # Immediately exit if user asks for version if "--version" in args: @@ -158,9 +154,6 @@ group are mutually exclusive.", # load command line plugins linter.load_plugin_modules(self._plugins) - linter.disable("I") - linter.enable("c-extension-no-member") - # Register the options needed for 'pylint-config' # By not registering them by default they don't show up in the normal usage message if self._is_pylint_config: @@ -175,6 +168,7 @@ group are mutually exclusive.", warnings.warn( "NOTE: The 'pylint-config' command is experimental and usage can change", UserWarning, + stacklevel=2, ) code = _handle_pylint_config_commands(linter) if exit: @@ -214,14 +208,6 @@ group are mutually exclusive.", else: linter.check(args) score_value = linter.generate_reports() - - if do_exit is not UNUSED_PARAM_SENTINEL: - warnings.warn( - "do_exit is deprecated and it is going to be removed in a future version.", - DeprecationWarning, - ) - exit = do_exit - if linter.config.clear_cache_post_run: clear_lru_caches() MANAGER.clear_cache() diff --git a/pylint/lint/utils.py b/pylint/lint/utils.py index 98fb8087a..a7fbfd0bc 100644 --- a/pylint/lint/utils.py +++ b/pylint/lint/utils.py @@ -1,19 +1,18 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 import contextlib +import platform import sys import traceback -import warnings from collections.abc import Iterator, Sequence from datetime import datetime from pathlib import Path -from pylint.config import PYLINT_HOME -from pylint.lint.expand_modules import discover_package_path +from pylint.constants import PYLINT_HOME, full_version def prepare_crash_report(ex: Exception, filepath: str, crash_file_path: str) -> Path: @@ -26,18 +25,20 @@ def prepare_crash_report(ex: Exception, filepath: str, crash_file_path: str) -> if not issue_template_path.exists(): template = """\ First, please verify that the bug is not already filled: -https://github.com/PyCQA/pylint/issues/ +https://github.com/pylint-dev/pylint/issues/ -Then create a new crash issue: -https://github.com/PyCQA/pylint/issues/new?assignees=&labels=crash%2Cneeds+triage&template=BUG-REPORT.yml +Then create a new issue: +https://github.com/pylint-dev/pylint/issues/new?labels=Crash 💥%2CNeeds triage 📥 -""" - template += f"""\ +""" + template += f""" Issue title: Crash ``{ex}`` (if possible, be more specific about what made pylint crash) -Content: -When parsing the following file: + +### Bug description + +When parsing the following ``a.py``: <!-- If sharing the code is not an option, please state so, @@ -48,11 +49,49 @@ When parsing the following file: {file_content} ``` -pylint crashed with a ``{ex.__class__.__name__}`` and with the following stacktrace: +### Command used + +```shell +pylint a.py ``` + +### Pylint output + +<details open> + <summary> + pylint crashed with a ``{ex.__class__.__name__}`` and with the following stacktrace: + </summary> + +```python """ template += traceback.format_exc() - template += "```\n" + template += f""" +``` + + +</details> + +### Expected behavior + +No crash. + +### Pylint version + +```shell +{full_version} +``` + +### OS / Environment + +{sys.platform} ({platform.system()}) + +### Additional dependencies + +<!-- +Please remove this part if you're not using any of +your dependencies in the example. + --> +""" try: with open(issue_template_path, "a", encoding="utf8") as f: f.write(template) @@ -73,19 +112,6 @@ def get_fatal_error_message(filepath: str, issue_template_path: Path) -> str: ) -def _patch_sys_path(args: Sequence[str]) -> list[str]: - # TODO: Remove deprecated function - warnings.warn( - "_patch_sys_path has been deprecated because it relies on auto-magic package path " - "discovery which is implemented by get_python_path that is deprecated. " - "Use _augment_sys_path and pass additional sys.path entries as an argument obtained from " - "discover_package_path.", - DeprecationWarning, - stacklevel=2, - ) - return _augment_sys_path([discover_package_path(arg, []) for arg in args]) - - def _augment_sys_path(additional_paths: Sequence[str]) -> list[str]: original = list(sys.path) changes = [] @@ -100,28 +126,6 @@ def _augment_sys_path(additional_paths: Sequence[str]) -> list[str]: @contextlib.contextmanager -def fix_import_path(args: Sequence[str]) -> Iterator[None]: - """Prepare 'sys.path' for running the linter checks. - - Within this context, each of the given arguments is importable. - Paths are added to 'sys.path' in corresponding order to the arguments. - We avoid adding duplicate directories to sys.path. - `sys.path` is reset to its original value upon exiting this context. - """ - # TODO: Remove deprecated function - warnings.warn( - "fix_import_path has been deprecated because it relies on auto-magic package path " - "discovery which is implemented by get_python_path that is deprecated. " - "Use augmented_sys_path and pass additional sys.path entries as an argument obtained from " - "discover_package_path.", - DeprecationWarning, - stacklevel=2, - ) - with augmented_sys_path([discover_package_path(arg, []) for arg in args]): - yield - - -@contextlib.contextmanager def augmented_sys_path(additional_paths: Sequence[str]) -> Iterator[None]: """Augment 'sys.path' by adding non-existent entries from additional_paths.""" original = _augment_sys_path(additional_paths) diff --git a/pylint/message/__init__.py b/pylint/message/__init__.py index 11d2b17dc..6fa8e44b7 100644 --- a/pylint/message/__init__.py +++ b/pylint/message/__init__.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """All the classes related to Message handling.""" diff --git a/pylint/message/_deleted_message_ids.py b/pylint/message/_deleted_message_ids.py index 4f361c96a..60289e805 100644 --- a/pylint/message/_deleted_message_ids.py +++ b/pylint/message/_deleted_message_ids.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 @@ -18,7 +18,7 @@ DELETED_MSGID_PREFIXES: list[int] = [] DELETED_MESSAGES_IDS = { # Everything until the next comment is from the PY3K+ checker - "https://github.com/PyCQA/pylint/pull/4942": [ + "https://github.com/pylint-dev/pylint/pull/4942": [ DeletedMessage("W1601", "apply-builtin"), DeletedMessage("E1601", "print-statement"), DeletedMessage("E1602", "parameter-unpacking"), @@ -95,10 +95,10 @@ DELETED_MESSAGES_IDS = { DeletedMessage("W1661", "exception-escape"), DeletedMessage("W1662", "comprehension-escape"), ], - "https://github.com/PyCQA/pylint/pull/3578": [ + "https://github.com/pylint-dev/pylint/pull/3578": [ DeletedMessage("W0312", "mixed-indentation"), ], - "https://github.com/PyCQA/pylint/pull/3577": [ + "https://github.com/pylint-dev/pylint/pull/3577": [ DeletedMessage( "C0326", "bad-whitespace", @@ -109,7 +109,7 @@ DELETED_MESSAGES_IDS = { ], ), ], - "https://github.com/PyCQA/pylint/pull/3571": [ + "https://github.com/pylint-dev/pylint/pull/3571": [ DeletedMessage("C0330", "bad-continuation") ], "https://pylint.readthedocs.io/en/latest/whatsnew/1/1.4.html#what-s-new-in-pylint-1-4-3": [ @@ -117,10 +117,10 @@ DELETED_MESSAGES_IDS = { DeletedMessage("R0922", "abstract-class-little-used"), DeletedMessage("W0142", "star-args"), ], - "https://github.com/PyCQA/pylint/issues/2409": [ + "https://github.com/pylint-dev/pylint/issues/2409": [ DeletedMessage("W0232", "no-init"), ], - "https://github.com/PyCQA/pylint/pull/6421": [ + "https://github.com/pylint-dev/pylint/pull/6421": [ DeletedMessage("W0111", "assign-to-new-keyword"), ], } diff --git a/pylint/message/message.py b/pylint/message/message.py index 23dd6c082..6ee8c5f78 100644 --- a/pylint/message/message.py +++ b/pylint/message/message.py @@ -1,11 +1,10 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 from dataclasses import asdict, dataclass -from warnings import warn from pylint.constants import MSG_TYPES from pylint.interfaces import UNDEFINED, Confidence @@ -35,27 +34,10 @@ class Message: # pylint: disable=too-many-instance-attributes self, msg_id: str, symbol: str, - location: tuple[str, str, str, str, int, int] | MessageLocationTuple, + location: MessageLocationTuple, msg: str, confidence: Confidence | None, ) -> None: - if not isinstance(location, MessageLocationTuple): - warn( - "In pylint 3.0, Messages will only accept a MessageLocationTuple as location parameter", - DeprecationWarning, - stacklevel=2, - ) - location = MessageLocationTuple( - location[0], - location[1], - location[2], - location[3], - location[4], - location[5], - None, - None, - ) - self.msg_id = msg_id self.symbol = symbol self.msg = msg diff --git a/pylint/message/message_definition.py b/pylint/message/message_definition.py index 25aa87d92..29a8780cb 100644 --- a/pylint/message/message_definition.py +++ b/pylint/message/message_definition.py @@ -1,11 +1,10 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 import sys -import warnings from typing import TYPE_CHECKING, Any from astroid import nodes @@ -73,21 +72,8 @@ class MessageDefinition: def __str__(self) -> str: return f"{repr(self)}:\n{self.msg} {self.description}" - def may_be_emitted( - self, - py_version: tuple[int, ...] | sys._version_info | None = None, - ) -> bool: - """Return True if message may be emitted using the configured py_version.""" - if py_version is None: - py_version = sys.version_info - warnings.warn( - "'py_version' will be a required parameter of " - "'MessageDefinition.may_be_emitted' in pylint 3.0. The most likely " - "solution is to use 'linter.config.py_version' if you need to keep " - "using this function, or to use 'MessageDefinition.is_message_enabled'" - " instead.", - DeprecationWarning, - ) + def may_be_emitted(self, py_version: tuple[int, ...] | sys._version_info) -> bool: + """May the message be emitted using the configured py_version?""" if self.minversion is not None and self.minversion > py_version: return False if self.maxversion is not None and self.maxversion <= py_version: diff --git a/pylint/message/message_definition_store.py b/pylint/message/message_definition_store.py index 7bbc70a51..4921be212 100644 --- a/pylint/message/message_definition_store.py +++ b/pylint/message/message_definition_store.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 @@ -58,7 +58,7 @@ class MessageDefinitionStore: # Since MessageDefinitionStore is only initialized once # and the arguments are relatively small we do not run the # risk of creating a large memory leak. - # See discussion in: https://github.com/PyCQA/pylint/pull/5673 + # See discussion in: https://github.com/pylint-dev/pylint/pull/5673 @functools.lru_cache( # pylint: disable=method-cache-max-size-none # noqa: B019 maxsize=None ) diff --git a/pylint/message/message_id_store.py b/pylint/message/message_id_store.py index d1810bd2b..273522d3d 100644 --- a/pylint/message/message_id_store.py +++ b/pylint/message/message_id_store.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 diff --git a/pylint/py.typed b/pylint/py.typed new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/pylint/py.typed diff --git a/pylint/pyreverse/__init__.py b/pylint/pyreverse/__init__.py index 458c0f35d..175e9cb67 100644 --- a/pylint/pyreverse/__init__.py +++ b/pylint/pyreverse/__init__.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Pyreverse.extensions.""" diff --git a/pylint/pyreverse/diadefslib.py b/pylint/pyreverse/diadefslib.py index 85b23052e..3b7694823 100644 --- a/pylint/pyreverse/diadefslib.py +++ b/pylint/pyreverse/diadefslib.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Handle diagram generation options for class diagram or default diagrams.""" @@ -12,6 +12,7 @@ from typing import Any import astroid from astroid import nodes +from astroid.modutils import is_stdlib_module from pylint.pyreverse.diagrams import ClassDiagram, PackageDiagram from pylint.pyreverse.inspector import Linker, Project @@ -67,10 +68,14 @@ class DiaDefGenerator: return self.anc_level, self.association_level def show_node(self, node: nodes.ClassDef) -> bool: - """True if builtins and not show_builtins.""" - if self.config.show_builtin: - return True - return node.root().name != "builtins" # type: ignore[no-any-return] + """Determine if node should be shown based on config.""" + if node.root().name == "builtins": + return self.config.show_builtin # type: ignore[no-any-return] + + if is_stdlib_module(node.root().name): + return self.config.show_stdlib # type: ignore[no-any-return] + + return True def add_class(self, node: nodes.ClassDef) -> None: """Visit one class and add it to diagram.""" diff --git a/pylint/pyreverse/diagrams.py b/pylint/pyreverse/diagrams.py index 4437d3c4e..01bce7dc3 100644 --- a/pylint/pyreverse/diagrams.py +++ b/pylint/pyreverse/diagrams.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Diagram objects.""" @@ -13,7 +13,7 @@ import astroid from astroid import nodes, util from pylint.checkers.utils import decorated_with_property -from pylint.pyreverse.utils import FilterMixIn, is_interface +from pylint.pyreverse.utils import FilterMixIn class Figure: @@ -50,7 +50,13 @@ class DiagramEntity(Figure): ) -> None: super().__init__() self.title = title - self.node: nodes.NodeNG = node if node else nodes.NodeNG() + self.node: nodes.NodeNG = node or nodes.NodeNG( + lineno=None, + col_offset=None, + end_lineno=None, + end_col_offset=None, + parent=None, + ) self.shape = self.default_shape @@ -195,11 +201,7 @@ class ClassDiagram(Figure, FilterMixIn): node = obj.node obj.attrs = self.get_attrs(node) obj.methods = self.get_methods(node) - # shape - if is_interface(node): - obj.shape = "interface" - else: - obj.shape = "class" + obj.shape = "class" # inheritance link for par_node in node.ancestors(recurs=False): try: @@ -207,13 +209,6 @@ class ClassDiagram(Figure, FilterMixIn): self.add_relationship(obj, par_obj, "specialization") except KeyError: continue - # implements link - for impl_node in node.implements: - try: - impl_obj = self.object_from_node(impl_node) - self.add_relationship(obj, impl_obj, "implements") - except KeyError: - continue # associations & aggregations links for name, values in list(node.aggregations_type.items()): diff --git a/pylint/pyreverse/dot_printer.py b/pylint/pyreverse/dot_printer.py index 99cb17e97..edaea2384 100644 --- a/pylint/pyreverse/dot_printer.py +++ b/pylint/pyreverse/dot_printer.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Class to generate files in dot format and image formats supported by Graphviz.""" @@ -25,13 +25,11 @@ class HTMLLabels(Enum): ALLOWED_CHARSETS: frozenset[str] = frozenset(("utf-8", "iso-8859-1", "latin1")) SHAPES: dict[NodeType, str] = { NodeType.PACKAGE: "box", - NodeType.INTERFACE: "record", NodeType.CLASS: "record", } # pylint: disable-next=consider-using-namedtuple-or-dataclass ARROWS: dict[EdgeType, dict[str, str]] = { EdgeType.INHERITS: {"arrowtail": "none", "arrowhead": "empty"}, - EdgeType.IMPLEMENTS: {"arrowtail": "node", "arrowhead": "empty", "style": "dashed"}, EdgeType.ASSOCIATION: { "fontcolor": "green", "arrowtail": "none", diff --git a/pylint/pyreverse/inspector.py b/pylint/pyreverse/inspector.py index 523ff8171..0cabe9473 100644 --- a/pylint/pyreverse/inspector.py +++ b/pylint/pyreverse/inspector.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Visitor doing some post-processing on the astroid tree. @@ -12,13 +12,11 @@ from __future__ import annotations import collections import os import traceback -import warnings from abc import ABC, abstractmethod -from collections.abc import Generator -from typing import Any, Callable, Optional +from typing import Callable, Optional import astroid -from astroid import nodes, util +from astroid import nodes from pylint import constants from pylint.pyreverse import utils @@ -39,27 +37,6 @@ def _astroid_wrapper( return None -def interfaces(node: nodes.ClassDef) -> Generator[Any, None, None]: - """Return an iterator on interfaces implemented by the given class node.""" - try: - implements = astroid.bases.Instance(node).getattr("__implements__")[0] - except astroid.exceptions.NotFoundError: - return - if implements.frame(future=True) is not node: - return - found = set() - missing = False - for iface in nodes.unpack_infer(implements): - if isinstance(iface, util.UninferableBase): - missing = True - continue - if iface not in found: - found.add(iface) - yield iface - if missing: - raise astroid.exceptions.InferenceError() - - class IdGeneratorMixIn: """Mixin adding the ability to generate integer uid.""" @@ -129,9 +106,6 @@ class Linker(IdGeneratorMixIn, utils.LocalsVisitor): * aggregations_type as instance_attrs_type but for aggregations relationships - - * implements, - list of implemented interface _objects_ (only on astroid.Class nodes) """ def __init__(self, project: Project, tag: bool = False) -> None: @@ -172,7 +146,6 @@ class Linker(IdGeneratorMixIn, utils.LocalsVisitor): """Visit an astroid.Class node. * set the locals_type and instance_attrs_type mappings - * set the implements list and build it * optionally tag the node with a unique id """ if hasattr(node, "locals_type"): @@ -194,24 +167,6 @@ class Linker(IdGeneratorMixIn, utils.LocalsVisitor): if not isinstance(assignattr, nodes.Unknown): self.associations_handler.handle(assignattr, node) self.handle_assignattr_type(assignattr, node) - # resolve implemented interface - try: - ifaces = interfaces(node) - if ifaces is not None: - node.implements = list(ifaces) - if node.implements: - # TODO: 3.0: Remove support for __implements__ - warnings.warn( - "pyreverse will drop support for resolving and displaying " - "implemented interfaces in pylint 3.0. The implementation " - "relies on the '__implements__' attribute proposed in PEP 245" - ", which was rejected in 2006.", - DeprecationWarning, - ) - else: - node.implements = [] - except astroid.InferenceError: - node.implements = [] def visit_functiondef(self, node: nodes.FunctionDef) -> None: """Visit an astroid.Function node. diff --git a/pylint/pyreverse/main.py b/pylint/pyreverse/main.py index a2dc94734..58128bb57 100644 --- a/pylint/pyreverse/main.py +++ b/pylint/pyreverse/main.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Create UML diagrams for classes and modules in <packages>.""" @@ -27,7 +27,6 @@ from pylint.typing import Options DIRECTLY_SUPPORTED_FORMATS = ( "dot", - "vcg", "puml", "plantuml", "mmd", @@ -35,23 +34,16 @@ DIRECTLY_SUPPORTED_FORMATS = ( ) DEFAULT_COLOR_PALETTE = ( - "aliceblue", - "antiquewhite", - "aquamarine", - "burlywood", - "cadetblue", - "chartreuse", - "chocolate", - "coral", - "cornflowerblue", - "cyan", - "darkgoldenrod", - "darkseagreen", - "dodgerblue", - "forestgreen", - "gold", - "hotpink", - "mediumspringgreen", + # colorblind scheme taken from https://personal.sron.nl/~pault/ + "#77AADD", # light blue + "#99DDFF", # light cyan + "#44BB99", # mint + "#BBCC33", # pear + "#AAAA00", # olive + "#EEDD88", # light yellow + "#EE8866", # orange + "#FFAABB", # pink + "#DDDDDD", # pale grey ) OPTIONS: Options = ( @@ -138,6 +130,15 @@ OPTIONS: Options = ( }, ), ( + "show-stdlib", + { + "short": "L", + "action": "store_true", + "default": False, + "help": "include standard library objects in representation of classes", + }, + ), + ( "module-names", { "short": "m", @@ -157,6 +158,14 @@ OPTIONS: Options = ( }, ), ( + "no-standalone", + { + "action": "store_true", + "default": False, + "help": "only show nodes with connections", + }, + ), + ( "output", { "short": "o", diff --git a/pylint/pyreverse/mermaidjs_printer.py b/pylint/pyreverse/mermaidjs_printer.py index a8f3c576b..61d0d7948 100644 --- a/pylint/pyreverse/mermaidjs_printer.py +++ b/pylint/pyreverse/mermaidjs_printer.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Class to generate files in mermaidjs format.""" @@ -17,12 +17,10 @@ class MermaidJSPrinter(Printer): NODES: dict[NodeType, str] = { NodeType.CLASS: "class", - NodeType.INTERFACE: "class", NodeType.PACKAGE: "class", } ARROWS: dict[EdgeType, str] = { EdgeType.INHERITS: "--|>", - EdgeType.IMPLEMENTS: "..|>", EdgeType.ASSOCIATION: "--*", EdgeType.AGGREGATION: "--o", EdgeType.USES: "-->", @@ -46,7 +44,6 @@ class MermaidJSPrinter(Printer): # pylint: disable=duplicate-code if properties is None: properties = NodeProperties(label=name) - stereotype = "~~Interface~~" if type_ is NodeType.INTERFACE else "" nodetype = self.NODES[type_] body = [] if properties.attrs: @@ -60,7 +57,7 @@ class MermaidJSPrinter(Printer): line += f" {get_annotation_label(func.returns)}" body.append(line) name = name.split(".")[-1] - self.emit(f"{nodetype} {name}{stereotype} {{") + self.emit(f"{nodetype} {name} {{") self._inc_indent() for line in body: self.emit(line) diff --git a/pylint/pyreverse/plantuml_printer.py b/pylint/pyreverse/plantuml_printer.py index de3f983b7..5f703b62e 100644 --- a/pylint/pyreverse/plantuml_printer.py +++ b/pylint/pyreverse/plantuml_printer.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Class to generate files in dot format and image formats supported by Graphviz.""" @@ -17,12 +17,10 @@ class PlantUmlPrinter(Printer): NODES: dict[NodeType, str] = { NodeType.CLASS: "class", - NodeType.INTERFACE: "class", NodeType.PACKAGE: "package", } ARROWS: dict[EdgeType, str] = { EdgeType.INHERITS: "--|>", - EdgeType.IMPLEMENTS: "..|>", EdgeType.ASSOCIATION: "--*", EdgeType.AGGREGATION: "--o", EdgeType.USES: "-->", @@ -56,10 +54,9 @@ class PlantUmlPrinter(Printer): """ if properties is None: properties = NodeProperties(label=name) - stereotype = " << interface >>" if type_ is NodeType.INTERFACE else "" nodetype = self.NODES[type_] if properties.color and properties.color != self.DEFAULT_COLOR: - color = f" #{properties.color}" + color = f" #{properties.color.lstrip('#')}" else: color = "" body = [] @@ -76,7 +73,7 @@ class PlantUmlPrinter(Printer): label = properties.label if properties.label is not None else name if properties.fontcolor and properties.fontcolor != self.DEFAULT_COLOR: label = f"<color:{properties.fontcolor}>{label}</color>" - self.emit(f'{nodetype} "{label}" as {name}{stereotype}{color} {{') + self.emit(f'{nodetype} "{label}" as {name}{color} {{') self._inc_indent() for line in body: self.emit(line) diff --git a/pylint/pyreverse/printer.py b/pylint/pyreverse/printer.py index cdbf7e3c8..f08c74602 100644 --- a/pylint/pyreverse/printer.py +++ b/pylint/pyreverse/printer.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Base class defining the interface for a printer.""" @@ -17,13 +17,11 @@ from pylint.pyreverse.utils import get_annotation_label class NodeType(Enum): CLASS = "class" - INTERFACE = "interface" PACKAGE = "package" class EdgeType(Enum): INHERITS = "inherits" - IMPLEMENTS = "implements" ASSOCIATION = "association" AGGREGATION = "aggregation" USES = "uses" diff --git a/pylint/pyreverse/printer_factory.py b/pylint/pyreverse/printer_factory.py index 41e8b46c8..fdbe480ed 100644 --- a/pylint/pyreverse/printer_factory.py +++ b/pylint/pyreverse/printer_factory.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 @@ -8,10 +8,8 @@ from pylint.pyreverse.dot_printer import DotPrinter from pylint.pyreverse.mermaidjs_printer import HTMLMermaidJSPrinter, MermaidJSPrinter from pylint.pyreverse.plantuml_printer import PlantUmlPrinter from pylint.pyreverse.printer import Printer -from pylint.pyreverse.vcg_printer import VCGPrinter filetype_to_printer: dict[str, type[Printer]] = { - "vcg": VCGPrinter, "plantuml": PlantUmlPrinter, "puml": PlantUmlPrinter, "mmd": MermaidJSPrinter, diff --git a/pylint/pyreverse/utils.py b/pylint/pyreverse/utils.py index 078bc1b7e..6294773b2 100644 --- a/pylint/pyreverse/utils.py +++ b/pylint/pyreverse/utils.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Generic classes/functions for pyreverse core/extensions.""" @@ -72,11 +72,6 @@ def get_visibility(name: str) -> str: return visibility -def is_interface(node: nodes.ClassDef) -> bool: - # bw compatibility - return node.type == "interface" # type: ignore[no-any-return] - - def is_exception(node: nodes.ClassDef) -> bool: # bw compatibility return node.type == "exception" # type: ignore[no-any-return] diff --git a/pylint/pyreverse/vcg_printer.py b/pylint/pyreverse/vcg_printer.py deleted file mode 100644 index b9e2e94f3..000000000 --- a/pylint/pyreverse/vcg_printer.py +++ /dev/null @@ -1,303 +0,0 @@ -# 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt - -"""Functions to generate files readable with George Sander's vcg -(Visualization of Compiler Graphs). - -You can download vcg at https://rw4.cs.uni-sb.de/~sander/html/gshome.html -Note that vcg exists as a debian package. -See vcg's documentation for explanation about the different values that -maybe used for the functions parameters. -""" - -from __future__ import annotations - -from collections.abc import Mapping -from typing import Any - -from pylint.pyreverse.printer import EdgeType, Layout, NodeProperties, NodeType, Printer - -ATTRS_VAL = { - "algos": ( - "dfs", - "tree", - "minbackward", - "left_to_right", - "right_to_left", - "top_to_bottom", - "bottom_to_top", - "maxdepth", - "maxdepthslow", - "mindepth", - "mindepthslow", - "mindegree", - "minindegree", - "minoutdegree", - "maxdegree", - "maxindegree", - "maxoutdegree", - ), - "booleans": ("yes", "no"), - "colors": ( - "black", - "white", - "blue", - "red", - "green", - "yellow", - "magenta", - "lightgrey", - "cyan", - "darkgrey", - "darkblue", - "darkred", - "darkgreen", - "darkyellow", - "darkmagenta", - "darkcyan", - "gold", - "lightblue", - "lightred", - "lightgreen", - "lightyellow", - "lightmagenta", - "lightcyan", - "lilac", - "turquoise", - "aquamarine", - "khaki", - "purple", - "yellowgreen", - "pink", - "orange", - "orchid", - ), - "shapes": ("box", "ellipse", "rhomb", "triangle"), - "textmodes": ("center", "left_justify", "right_justify"), - "arrowstyles": ("solid", "line", "none"), - "linestyles": ("continuous", "dashed", "dotted", "invisible"), -} - -# meaning of possible values: -# O -> string -# 1 -> int -# list -> value in list -GRAPH_ATTRS = { - "title": 0, - "label": 0, - "color": ATTRS_VAL["colors"], - "textcolor": ATTRS_VAL["colors"], - "bordercolor": ATTRS_VAL["colors"], - "width": 1, - "height": 1, - "borderwidth": 1, - "textmode": ATTRS_VAL["textmodes"], - "shape": ATTRS_VAL["shapes"], - "shrink": 1, - "stretch": 1, - "orientation": ATTRS_VAL["algos"], - "vertical_order": 1, - "horizontal_order": 1, - "xspace": 1, - "yspace": 1, - "layoutalgorithm": ATTRS_VAL["algos"], - "late_edge_labels": ATTRS_VAL["booleans"], - "display_edge_labels": ATTRS_VAL["booleans"], - "dirty_edge_labels": ATTRS_VAL["booleans"], - "finetuning": ATTRS_VAL["booleans"], - "manhattan_edges": ATTRS_VAL["booleans"], - "smanhattan_edges": ATTRS_VAL["booleans"], - "port_sharing": ATTRS_VAL["booleans"], - "edges": ATTRS_VAL["booleans"], - "nodes": ATTRS_VAL["booleans"], - "splines": ATTRS_VAL["booleans"], -} -NODE_ATTRS = { - "title": 0, - "label": 0, - "color": ATTRS_VAL["colors"], - "textcolor": ATTRS_VAL["colors"], - "bordercolor": ATTRS_VAL["colors"], - "width": 1, - "height": 1, - "borderwidth": 1, - "textmode": ATTRS_VAL["textmodes"], - "shape": ATTRS_VAL["shapes"], - "shrink": 1, - "stretch": 1, - "vertical_order": 1, - "horizontal_order": 1, -} -EDGE_ATTRS = { - "sourcename": 0, - "targetname": 0, - "label": 0, - "linestyle": ATTRS_VAL["linestyles"], - "class": 1, - "thickness": 0, - "color": ATTRS_VAL["colors"], - "textcolor": ATTRS_VAL["colors"], - "arrowcolor": ATTRS_VAL["colors"], - "backarrowcolor": ATTRS_VAL["colors"], - "arrowsize": 1, - "backarrowsize": 1, - "arrowstyle": ATTRS_VAL["arrowstyles"], - "backarrowstyle": ATTRS_VAL["arrowstyles"], - "textmode": ATTRS_VAL["textmodes"], - "priority": 1, - "anchor": 1, - "horizontal_order": 1, -} -SHAPES: dict[NodeType, str] = { - NodeType.PACKAGE: "box", - NodeType.CLASS: "box", - NodeType.INTERFACE: "ellipse", -} -# pylint: disable-next=consider-using-namedtuple-or-dataclass -ARROWS: dict[EdgeType, dict[str, str | int]] = { - EdgeType.USES: { - "arrowstyle": "solid", - "backarrowstyle": "none", - "backarrowsize": 0, - }, - EdgeType.INHERITS: { - "arrowstyle": "solid", - "backarrowstyle": "none", - "backarrowsize": 10, - }, - EdgeType.IMPLEMENTS: { - "arrowstyle": "solid", - "backarrowstyle": "none", - "linestyle": "dotted", - "backarrowsize": 10, - }, - EdgeType.ASSOCIATION: { - "arrowstyle": "solid", - "backarrowstyle": "none", - "textcolor": "green", - }, - EdgeType.AGGREGATION: { - "arrowstyle": "solid", - "backarrowstyle": "none", - "textcolor": "green", - }, -} -ORIENTATION: dict[Layout, str] = { - Layout.LEFT_TO_RIGHT: "left_to_right", - Layout.RIGHT_TO_LEFT: "right_to_left", - Layout.TOP_TO_BOTTOM: "top_to_bottom", - Layout.BOTTOM_TO_TOP: "bottom_to_top", -} - -# Misc utilities ############################################################### - - -class VCGPrinter(Printer): - def _open_graph(self) -> None: - """Emit the header lines.""" - self.emit("graph:{\n") - self._inc_indent() - self._write_attributes( - GRAPH_ATTRS, - title=self.title, - layoutalgorithm="dfs", - late_edge_labels="yes", - port_sharing="no", - manhattan_edges="yes", - ) - if self.layout: - self._write_attributes(GRAPH_ATTRS, orientation=ORIENTATION[self.layout]) - - def _close_graph(self) -> None: - """Emit the lines needed to properly close the graph.""" - self._dec_indent() - self.emit("}") - - def emit_node( - self, - name: str, - type_: NodeType, - properties: NodeProperties | None = None, - ) -> None: - """Create a new node. - - Nodes can be classes, packages, participants etc. - """ - if properties is None: - properties = NodeProperties(label=name) - elif properties.label is None: - properties.label = name - self.emit(f'node: {{title:"{name}"', force_newline=False) - self._write_attributes( - NODE_ATTRS, - label=self._build_label_for_node(properties), - shape=SHAPES[type_], - ) - self.emit("}") - - @staticmethod - def _build_label_for_node(properties: NodeProperties) -> str: - fontcolor = "\f09" if properties.fontcolor == "red" else "" - label = rf"\fb{fontcolor}{properties.label}\fn" - if properties.attrs is None and properties.methods is None: - # return a compact form which only displays the classname in a box - return label - attrs = properties.attrs or [] - methods = properties.methods or [] - method_names = [func.name for func in methods] - # box width for UML like diagram - maxlen = max(len(name) for name in [properties.label] + method_names + attrs) - line = "_" * (maxlen + 2) - label = rf"{label}\n\f{line}" - for attr in attrs: - label = rf"{label}\n\f08{attr}" - if attrs: - label = rf"{label}\n\f{line}" - for func in method_names: - label = rf"{label}\n\f10{func}()" - return label - - def emit_edge( - self, - from_node: str, - to_node: str, - type_: EdgeType, - label: str | None = None, - ) -> None: - """Create an edge from one node to another to display relationships.""" - self.emit( - f'edge: {{sourcename:"{from_node}" targetname:"{to_node}"', - force_newline=False, - ) - attributes = ARROWS[type_] - if label: - attributes["label"] = label - self._write_attributes( - EDGE_ATTRS, - **attributes, - ) - self.emit("}") - - def _write_attributes( - self, attributes_dict: Mapping[str, Any], **args: Any - ) -> None: - """Write graph, node or edge attributes.""" - for key, value in args.items(): - try: - _type = attributes_dict[key] - except KeyError as e: - raise AttributeError( - f"no such attribute {key}\npossible attributes are {attributes_dict.keys()}" - ) from e - - if not _type: - self.emit(f'{key}:"{value}"\n') - elif _type == 1: - self.emit(f"{key}:{int(value)}\n") - elif value in _type: - self.emit(f"{key}:{value}\n") - else: - raise ValueError( - f"value {value} isn't correct for attribute {key} correct values are {type}" - ) diff --git a/pylint/pyreverse/writer.py b/pylint/pyreverse/writer.py index d92f4e2e5..58e967115 100644 --- a/pylint/pyreverse/writer.py +++ b/pylint/pyreverse/writer.py @@ -1,8 +1,8 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt -"""Utilities for creating VCG and Dot diagrams.""" +"""Utilities for creating diagrams.""" from __future__ import annotations @@ -57,6 +57,12 @@ class DiagramWriter: # sorted to get predictable (hence testable) results for module in sorted(diagram.modules(), key=lambda x: x.title): module.fig_id = module.node.qname() + if self.config.no_standalone and not any( + module in (rel.from_object, rel.to_object) + for rel in diagram.get_relationships("depends") + ): + continue + self.printer.emit_node( module.fig_id, type_=NodeType.PACKAGE, @@ -75,9 +81,17 @@ class DiagramWriter: # sorted to get predictable (hence testable) results for obj in sorted(diagram.objects, key=lambda x: x.title): # type: ignore[no-any-return] obj.fig_id = obj.node.qname() - type_ = NodeType.INTERFACE if obj.shape == "interface" else NodeType.CLASS + if self.config.no_standalone and not any( + obj in (rel.from_object, rel.to_object) + for rel_type in ("specialization", "association", "aggregation") + for rel in diagram.get_relationships(rel_type) + ): + continue + self.printer.emit_node( - obj.fig_id, type_=type_, properties=self.get_class_properties(obj) + obj.fig_id, + type_=NodeType.CLASS, + properties=self.get_class_properties(obj), ) # inheritance links for rel in diagram.get_relationships("specialization"): @@ -86,13 +100,6 @@ class DiagramWriter: rel.to_object.fig_id, type_=EdgeType.INHERITS, ) - # implementation links - for rel in diagram.get_relationships("implements"): - self.printer.emit_edge( - rel.from_object.fig_id, - rel.to_object.fig_id, - type_=EdgeType.IMPLEMENTS, - ) # generate associations for rel in diagram.get_relationships("association"): self.printer.emit_edge( diff --git a/pylint/reporters/__init__.py b/pylint/reporters/__init__.py index f22530de1..cf7b57576 100644 --- a/pylint/reporters/__init__.py +++ b/pylint/reporters/__init__.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Utilities methods and classes for reporters.""" diff --git a/pylint/reporters/base_reporter.py b/pylint/reporters/base_reporter.py index 3df970d80..d370b1910 100644 --- a/pylint/reporters/base_reporter.py +++ b/pylint/reporters/base_reporter.py @@ -1,14 +1,12 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 import os import sys -import warnings from typing import TYPE_CHECKING, TextIO -from warnings import warn from pylint.message import Message from pylint.reporters.ureports.nodes import Text @@ -31,13 +29,6 @@ class BaseReporter: """Name of the reporter.""" def __init__(self, output: TextIO | None = None) -> None: - if getattr(self, "__implements__", None): - warnings.warn( - "Using the __implements__ inheritance pattern for BaseReporter is no " - "longer supported. Child classes should only inherit BaseReporter", - DeprecationWarning, - stacklevel=2, - ) self.linter: PyLinter self.section = 0 self.out: TextIO = output or sys.stdout @@ -49,16 +40,6 @@ class BaseReporter: """Handle a new message triggered on the current file.""" self.messages.append(msg) - def set_output(self, output: TextIO | None = None) -> None: - """Set output stream.""" - # TODO: 3.0: Remove deprecated method - warn( - "'set_output' will be removed in 3.0, please use 'reporter.out = stream' instead", - DeprecationWarning, - stacklevel=2, - ) - self.out = output or sys.stdout - def writeln(self, string: str = "") -> None: """Write a line in the output buffer.""" print(string, file=self.out) diff --git a/pylint/reporters/collecting_reporter.py b/pylint/reporters/collecting_reporter.py index ca9170253..943a74d55 100644 --- a/pylint/reporters/collecting_reporter.py +++ b/pylint/reporters/collecting_reporter.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 diff --git a/pylint/reporters/json_reporter.py b/pylint/reporters/json_reporter.py index 29df6ba07..176946e72 100644 --- a/pylint/reporters/json_reporter.py +++ b/pylint/reporters/json_reporter.py @@ -1,25 +1,19 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """JSON reporter.""" from __future__ import annotations import json -import sys -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, TypedDict from pylint.interfaces import UNDEFINED from pylint.message import Message from pylint.reporters.base_reporter import BaseReporter from pylint.typing import MessageLocationTuple -if sys.version_info >= (3, 8): - from typing import TypedDict -else: - from typing_extensions import TypedDict - if TYPE_CHECKING: from pylint.lint.pylinter import PyLinter from pylint.reporters.ureports.nodes import Section @@ -75,7 +69,7 @@ class JSONReporter(BaseJSONReporter): TODO: 3.0: Remove this JSONReporter in favor of the new one handling abs-path and confidence. - TODO: 2.16: Add a new JSONReporter handling abs-path, confidence and scores. + TODO: 3.0: Add a new JSONReporter handling abs-path, confidence and scores. (Ultimately all other breaking change related to json for 3.0). """ diff --git a/pylint/reporters/multi_reporter.py b/pylint/reporters/multi_reporter.py index 8bf0828c4..0c27293b7 100644 --- a/pylint/reporters/multi_reporter.py +++ b/pylint/reporters/multi_reporter.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 diff --git a/pylint/reporters/reports_handler_mix_in.py b/pylint/reporters/reports_handler_mix_in.py index 32a7190e7..95d45ba91 100644 --- a/pylint/reporters/reports_handler_mix_in.py +++ b/pylint/reporters/reports_handler_mix_in.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 diff --git a/pylint/reporters/text.py b/pylint/reporters/text.py index c25b31f7e..462ea48fe 100644 --- a/pylint/reporters/text.py +++ b/pylint/reporters/text.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Plain text reporters:. @@ -15,12 +15,11 @@ import re import sys import warnings from dataclasses import asdict, fields -from typing import TYPE_CHECKING, Dict, NamedTuple, Optional, TextIO, cast, overload +from typing import TYPE_CHECKING, Dict, NamedTuple, TextIO from pylint.message import Message from pylint.reporters import BaseReporter from pylint.reporters.ureports.text_writer import TextWriter -from pylint.utils import _splitstrip if TYPE_CHECKING: from pylint.lint import PyLinter @@ -37,6 +36,34 @@ class MessageStyle(NamedTuple): style: tuple[str, ...] = () """Tuple of style strings (see `ANSI_COLORS` for available values).""" + def __get_ansi_code(self) -> str: + """Return ANSI escape code corresponding to color and style. + + :raise KeyError: if a nonexistent color or style identifier is given + + :return: the built escape code + """ + ansi_code = [ANSI_STYLES[effect] for effect in self.style] + if self.color: + if self.color.isdigit(): + ansi_code.extend(["38", "5"]) + ansi_code.append(self.color) + else: + ansi_code.append(ANSI_COLORS[self.color]) + if ansi_code: + return ANSI_PREFIX + ";".join(ansi_code) + ANSI_END + return "" + + def _colorize_ansi(self, msg: str) -> str: + if self.color is None and len(self.style) == 0: + # If both color and style are not defined, then leave the text as is. + return msg + escape_code = self.__get_ansi_code() + # If invalid (or unknown) color, don't wrap msg with ANSI codes + if escape_code: + return f"{escape_code}{msg}{ANSI_RESET}" + return msg + ColorMappingDict = Dict[str, MessageStyle] @@ -70,85 +97,9 @@ MESSAGE_FIELDS = {i.name for i in fields(Message)} """All fields of the Message class.""" -def _get_ansi_code(msg_style: MessageStyle) -> str: - """Return ANSI escape code corresponding to color and style. - - :param msg_style: the message style - - :raise KeyError: if a nonexistent color or style identifier is given - - :return: the built escape code - """ - ansi_code = [ANSI_STYLES[effect] for effect in msg_style.style] - if msg_style.color: - if msg_style.color.isdigit(): - ansi_code.extend(["38", "5"]) - ansi_code.append(msg_style.color) - else: - ansi_code.append(ANSI_COLORS[msg_style.color]) - if ansi_code: - return ANSI_PREFIX + ";".join(ansi_code) + ANSI_END - return "" - - -@overload -def colorize_ansi( - msg: str, - msg_style: MessageStyle | None = ..., -) -> str: - ... - - -@overload -def colorize_ansi( - msg: str, - msg_style: str | None = ..., - style: str = ..., - *, - color: str | None = ..., -) -> str: - # Remove for pylint 3.0 - ... - - -def colorize_ansi( - msg: str, - msg_style: MessageStyle | str | None = None, - style: str = "", - **kwargs: str | None, -) -> str: - r"""colorize message by wrapping it with ANSI escape codes - - :param msg: the message string to colorize - - :param msg_style: the message style - or color (for backwards compatibility): the color of the message style - - :param style: the message's style elements, this will be deprecated - - :param \**kwargs: used to accept `color` parameter while it is being deprecated - - :return: the ANSI escaped string - """ - # TODO: 3.0: Remove deprecated typing and only accept MessageStyle as parameter - if not isinstance(msg_style, MessageStyle): - warnings.warn( - "In pylint 3.0, the colorize_ansi function of Text reporters will only accept a " - "MessageStyle parameter", - DeprecationWarning, - stacklevel=2, - ) - color = kwargs.get("color") - style_attrs = tuple(_splitstrip(style)) - msg_style = MessageStyle(color or msg_style, style_attrs) - # If both color and style are not defined, then leave the text as is - if msg_style.color is None and len(msg_style.style) == 0: - return msg - escape_code = _get_ansi_code(msg_style) - # If invalid (or unknown) color, don't wrap msg with ANSI codes - if escape_code: - return f"{escape_code}{msg}{ANSI_RESET}" - return msg +def colorize_ansi(msg: str, msg_style: MessageStyle) -> str: + """Colorize message by wrapping it with ANSI escape codes.""" + return msg_style._colorize_ansi(msg) def make_header(msg: Message) -> str: @@ -186,7 +137,8 @@ class TextReporter(BaseReporter): if argument[0] not in MESSAGE_FIELDS: warnings.warn( f"Don't recognize the argument '{argument[0]}' in the --msg-template. " - "Are you sure it is supported on the current version of pylint?" + "Are you sure it is supported on the current version of pylint?", + stacklevel=2, ) template = re.sub(r"\{" + argument[0] + r"(:.*?)?\}", "", template) self._fixed_template = template @@ -269,30 +221,9 @@ class ColorizedTextReporter(TextReporter): def __init__( self, output: TextIO | None = None, - color_mapping: ( - ColorMappingDict | dict[str, tuple[str | None, str]] | None - ) = None, + color_mapping: ColorMappingDict | None = None, ) -> None: super().__init__(output) - # TODO: 3.0: Remove deprecated typing and only accept ColorMappingDict as - # color_mapping parameter - if color_mapping and not isinstance( - list(color_mapping.values())[0], MessageStyle - ): - warnings.warn( - "In pylint 3.0, the ColorizedTextReporter will only accept ColorMappingDict as " - "color_mapping parameter", - DeprecationWarning, - stacklevel=2, - ) - temp_color_mapping: ColorMappingDict = {} - for key, value in color_mapping.items(): - color = value[0] - style_attrs = tuple(_splitstrip(value[1])) # type: ignore[arg-type] - temp_color_mapping[key] = MessageStyle(color, style_attrs) - color_mapping = temp_color_mapping - else: - color_mapping = cast(Optional[ColorMappingDict], color_mapping) self.color_mapping = color_mapping or ColorizedTextReporter.COLOR_MAPPING ansi_terms = ["xterm-16color", "xterm-256color"] if os.environ.get("TERM") not in ansi_terms: diff --git a/pylint/reporters/ureports/__init__.py b/pylint/reporters/ureports/__init__.py index a6a0946af..b87c3c319 100644 --- a/pylint/reporters/ureports/__init__.py +++ b/pylint/reporters/ureports/__init__.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt __all__ = ("BaseWriter",) diff --git a/pylint/reporters/ureports/base_writer.py b/pylint/reporters/ureports/base_writer.py index e4bd7e710..9a12123cb 100644 --- a/pylint/reporters/ureports/base_writer.py +++ b/pylint/reporters/ureports/base_writer.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Universal report objects and some formatting drivers. diff --git a/pylint/reporters/ureports/nodes.py b/pylint/reporters/ureports/nodes.py index 8b6bf32c6..59443996d 100644 --- a/pylint/reporters/ureports/nodes.py +++ b/pylint/reporters/ureports/nodes.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Micro reports objects. @@ -72,7 +72,7 @@ class BaseLayout(VNode): assert self.parent is not self if self.parent is None: return [] - return [self.parent] + self.parent.parents() + return [self.parent, *self.parent.parents()] def add_text(self, text: str) -> None: """Shortcut to add text data.""" diff --git a/pylint/reporters/ureports/text_writer.py b/pylint/reporters/ureports/text_writer.py index 13c675aab..5dd6a5d08 100644 --- a/pylint/reporters/ureports/text_writer.py +++ b/pylint/reporters/ureports/text_writer.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Text formatting drivers for ureports.""" diff --git a/pylint/testutils/__init__.py b/pylint/testutils/__init__.py index e9b8de1de..0ff9b773b 100644 --- a/pylint/testutils/__init__.py +++ b/pylint/testutils/__init__.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Functional/non regression tests for pylint.""" diff --git a/pylint/testutils/_primer/__init__.py b/pylint/testutils/_primer/__init__.py index 17c854572..2c40f561b 100644 --- a/pylint/testutils/_primer/__init__.py +++ b/pylint/testutils/_primer/__init__.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt __all__ = ["PackageToLint", "PRIMER_DIRECTORY_PATH"] diff --git a/pylint/testutils/_primer/package_to_lint.py b/pylint/testutils/_primer/package_to_lint.py index d59f93ed5..1f596f02b 100644 --- a/pylint/testutils/_primer/package_to_lint.py +++ b/pylint/testutils/_primer/package_to_lint.py @@ -1,22 +1,17 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 import logging -import sys from pathlib import Path +from typing import Literal from git import GitCommandError from git.cmd import Git from git.repo import Repo -if sys.version_info >= (3, 8): - from typing import Literal -else: - from typing_extensions import Literal - PRIMER_DIRECTORY_PATH = Path("tests") / ".pylint_primer_tests" diff --git a/pylint/testutils/_primer/primer.py b/pylint/testutils/_primer/primer.py index 7d08f1df5..abe6bc4dd 100644 --- a/pylint/testutils/_primer/primer.py +++ b/pylint/testutils/_primer/primer.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 diff --git a/pylint/testutils/_primer/primer_command.py b/pylint/testutils/_primer/primer_command.py index bbc930913..817c1a0d3 100644 --- a/pylint/testutils/_primer/primer_command.py +++ b/pylint/testutils/_primer/primer_command.py @@ -1,23 +1,17 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 import abc import argparse -import sys from pathlib import Path -from typing import Dict +from typing import Dict, TypedDict from pylint.reporters.json_reporter import OldJsonExport from pylint.testutils._primer import PackageToLint -if sys.version_info >= (3, 8): - from typing import TypedDict -else: - from typing_extensions import TypedDict - class PackageData(TypedDict): commit: str diff --git a/pylint/testutils/_primer/primer_compare_command.py b/pylint/testutils/_primer/primer_compare_command.py index acc1c9562..b405d7577 100644 --- a/pylint/testutils/_primer/primer_compare_command.py +++ b/pylint/testutils/_primer/primer_compare_command.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 import json diff --git a/pylint/testutils/_primer/primer_prepare_command.py b/pylint/testutils/_primer/primer_prepare_command.py index e69e55b95..27e216bd5 100644 --- a/pylint/testutils/_primer/primer_prepare_command.py +++ b/pylint/testutils/_primer/primer_prepare_command.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 import sys @@ -15,20 +15,21 @@ class PrepareCommand(PrimerCommand): def run(self) -> None: commit_string = "" version_string = ".".join(str(x) for x in sys.version_info[:2]) + # Shorten the SHA to avoid exceeding GitHub's 512 char ceiling if self.config.clone: for package, data in self.packages.items(): local_commit = data.lazy_clone() print(f"Cloned '{package}' at commit '{local_commit}'.") - commit_string += local_commit + "_" + commit_string += local_commit[:8] + "_" elif self.config.check: for package, data in self.packages.items(): local_commit = Repo(data.clone_directory).head.object.hexsha print(f"Found '{package}' at commit '{local_commit}'.") - commit_string += local_commit + "_" + commit_string += local_commit[:8] + "_" elif self.config.make_commit_string: for package, data in self.packages.items(): remote_sha1_commit = ( - Git().ls_remote(data.url, data.branch).split("\t")[0] + Git().ls_remote(data.url, data.branch).split("\t")[0][:8] ) print(f"'{package}' remote is at commit '{remote_sha1_commit}'.") commit_string += remote_sha1_commit + "_" diff --git a/pylint/testutils/_primer/primer_run_command.py b/pylint/testutils/_primer/primer_run_command.py index cd17d6b1d..7fb4a9ea2 100644 --- a/pylint/testutils/_primer/primer_run_command.py +++ b/pylint/testutils/_primer/primer_run_command.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 @@ -77,7 +77,8 @@ class RunCommand(PrimerCommand): # Duplicate code takes too long and is relatively safe # TODO: Find a way to allow cyclic-import and compare output correctly disables = ["--disable=duplicate-code,cyclic-import"] - arguments = data.pylint_args + enables + disables + additional = ["--clear-cache-post-run=y"] + arguments = data.pylint_args + enables + disables + additional output = StringIO() reporter = JSONReporter(output) print(f"Running 'pylint {', '.join(arguments)}'") @@ -96,6 +97,7 @@ class RunCommand(PrimerCommand): if fatal_msgs: warnings.warn( f"Encountered fatal errors while priming {package_name} !\n" - f"{self._print_msgs(fatal_msgs)}\n\n" + f"{self._print_msgs(fatal_msgs)}\n\n", + stacklevel=2, ) return messages, fatal_msgs diff --git a/pylint/testutils/_run.py b/pylint/testutils/_run.py index 0ad68868f..e0a19deca 100644 --- a/pylint/testutils/_run.py +++ b/pylint/testutils/_run.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Classes and functions used to mimic normal pylint runs. @@ -10,10 +10,8 @@ This module is considered private and can change at any time. from __future__ import annotations from collections.abc import Sequence -from typing import Any from pylint.lint import Run as LintRun -from pylint.lint.run import UNUSED_PARAM_SENTINEL from pylint.reporters.base_reporter import BaseReporter from pylint.testutils.lint_module_test import PYLINTRC @@ -39,7 +37,6 @@ class _Run(LintRun): args: Sequence[str], reporter: BaseReporter | None = None, exit: bool = True, # pylint: disable=redefined-builtin - do_exit: Any = UNUSED_PARAM_SENTINEL, ) -> None: args = _add_rcfile_default_pylintrc(list(args)) - super().__init__(args, reporter, exit, do_exit) + super().__init__(args, reporter, exit) diff --git a/pylint/testutils/checker_test_case.py b/pylint/testutils/checker_test_case.py index 291f52002..343eb8254 100644 --- a/pylint/testutils/checker_test_case.py +++ b/pylint/testutils/checker_test_case.py @@ -1,11 +1,10 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 import contextlib -import warnings from collections.abc import Generator, Iterator from typing import Any @@ -78,23 +77,8 @@ class CheckerTestCase: assert expected_msg.line == gotten_msg.line, msg assert expected_msg.col_offset == gotten_msg.col_offset, msg if PY38_PLUS and not IS_PYPY or PY39_PLUS: - # TODO: 3.0: Remove deprecated missing arguments and remove the warning - if not expected_msg.end_line == gotten_msg.end_line: - warnings.warn( # pragma: no cover - f"The end_line attribute of {gotten_msg} does not match " - f"the expected value in {expected_msg}. In pylint 3.0 correct end_line " - "attributes will be required for MessageTest.", - DeprecationWarning, - stacklevel=2, - ) - if not expected_msg.end_col_offset == gotten_msg.end_col_offset: - warnings.warn( # pragma: no cover - f"The end_col_offset attribute of {gotten_msg} does not match " - f"the expected value in {expected_msg}. In pylint 3.0 correct end_col_offset " - "attributes will be required for MessageTest.", - DeprecationWarning, - stacklevel=2, - ) + assert expected_msg.end_line == gotten_msg.end_line, msg + assert expected_msg.end_col_offset == gotten_msg.end_col_offset, msg def walk(self, node: nodes.NodeNG) -> None: """Recursive walk on the given node.""" diff --git a/pylint/testutils/configuration_test.py b/pylint/testutils/configuration_test.py index 8b921ceff..5ddb2c03b 100644 --- a/pylint/testutils/configuration_test.py +++ b/pylint/testutils/configuration_test.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Utility functions for configuration testing.""" diff --git a/pylint/testutils/constants.py b/pylint/testutils/constants.py index 1c3c69d79..0d93b7e87 100644 --- a/pylint/testutils/constants.py +++ b/pylint/testutils/constants.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt import operator import re diff --git a/pylint/testutils/decorator.py b/pylint/testutils/decorator.py index ab99cbdd8..c20692132 100644 --- a/pylint/testutils/decorator.py +++ b/pylint/testutils/decorator.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 diff --git a/pylint/testutils/functional/__init__.py b/pylint/testutils/functional/__init__.py index 4840e1ba6..c1c1c5139 100644 --- a/pylint/testutils/functional/__init__.py +++ b/pylint/testutils/functional/__init__.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt __all__ = [ "FunctionalTestFile", diff --git a/pylint/testutils/functional/find_functional_tests.py b/pylint/testutils/functional/find_functional_tests.py index 200cee7ec..f2e636687 100644 --- a/pylint/testutils/functional/find_functional_tests.py +++ b/pylint/testutils/functional/find_functional_tests.py @@ -1,18 +1,20 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 import os +from collections.abc import Iterator from pathlib import Path from pylint.testutils.functional.test_file import FunctionalTestFile -REASONABLY_DISPLAYABLE_VERTICALLY = 48 -"""'Wet finger' number of files that are reasonable to display by an IDE.""" -SHOULD_BE_IN_THE_SAME_DIRECTORY = 5 -"""'Wet finger' as in 'in my settings there are precisely this many'.""" +REASONABLY_DISPLAYABLE_VERTICALLY = 49 +"""'Wet finger' number of files that are reasonable to display by an IDE. + +'Wet finger' as in 'in my settings there are precisely this many'. +""" IGNORED_PARENT_DIRS = { "deprecated_relative_import", @@ -32,11 +34,12 @@ IGNORED_PARENT_PARENT_DIRS = { def get_functional_test_files_from_directory( input_dir: Path | str, + max_file_per_directory: int = REASONABLY_DISPLAYABLE_VERTICALLY, ) -> list[FunctionalTestFile]: """Get all functional tests in the input_dir.""" suite = [] - _check_functional_tests_structure(Path(input_dir)) + _check_functional_tests_structure(Path(input_dir), max_file_per_directory) for dirpath, dirnames, filenames in os.walk(input_dir): if dirpath.endswith("__pycache__"): @@ -49,39 +52,88 @@ def get_functional_test_files_from_directory( return suite -def _check_functional_tests_structure(directory: Path) -> None: - """Check if test directories follow correct file/folder structure.""" - # Ignore underscored directories +def _check_functional_tests_structure( + directory: Path, max_file_per_directory: int +) -> None: + """Check if test directories follow correct file/folder structure. + + Ignore underscored directories or files. + """ if Path(directory).stem.startswith("_"): return files: set[Path] = set() dirs: set[Path] = set() + def _get_files_from_dir( + path: Path, violations: list[tuple[Path, int]] + ) -> list[Path]: + """Return directories and files from a directory and handles violations.""" + files_without_leading_underscore = list( + p for p in path.iterdir() if not p.stem.startswith("_") + ) + if len(files_without_leading_underscore) > max_file_per_directory: + violations.append((path, len(files_without_leading_underscore))) + return files_without_leading_underscore + + def walk(path: Path) -> Iterator[Path]: + violations: list[tuple[Path, int]] = [] + violations_msgs: set[str] = set() + parent_dir_files = _get_files_from_dir(path, violations) + error_msg = ( + "The following directory contains too many functional tests files:\n" + ) + for _file_or_dir in parent_dir_files: + if _file_or_dir.is_dir(): + _files = _get_files_from_dir(_file_or_dir, violations) + yield _file_or_dir.resolve() + try: + yield from walk(_file_or_dir) + except AssertionError as e: + violations_msgs.add(str(e).replace(error_msg, "")) + else: + yield _file_or_dir.resolve() + if violations or violations_msgs: + _msg = error_msg + for offending_file, number in violations: + _msg += f"- {offending_file}: {number} when the max is {max_file_per_directory}\n" + for error_msg in violations_msgs: + _msg += error_msg + raise AssertionError(_msg) + # Collect all sub-directories and files in directory - for file_or_dir in directory.iterdir(): - if file_or_dir.is_file(): - if file_or_dir.suffix == ".py" and not file_or_dir.stem.startswith("_"): - files.add(file_or_dir) - elif file_or_dir.is_dir(): + for file_or_dir in walk(directory): + if file_or_dir.is_dir(): dirs.add(file_or_dir) - _check_functional_tests_structure(file_or_dir) - - assert len(files) <= REASONABLY_DISPLAYABLE_VERTICALLY, ( - f"{directory} contains too many functional tests files " - + f"({len(files)} > {REASONABLY_DISPLAYABLE_VERTICALLY})." - ) + elif file_or_dir.suffix == ".py": + files.add(file_or_dir) + directory_does_not_exists: list[tuple[Path, Path]] = [] + misplaced_file: list[Path] = [] for file in files: possible_dir = file.parent / file.stem.split("_")[0] - assert not possible_dir.exists(), f"{file} should go in {possible_dir}." - + if possible_dir.exists(): + directory_does_not_exists.append((file, possible_dir)) # Exclude some directories as they follow a different structure if ( not len(file.parent.stem) == 1 # First letter sub-directories and file.parent.stem not in IGNORED_PARENT_DIRS and file.parent.parent.stem not in IGNORED_PARENT_PARENT_DIRS ): - assert file.stem.startswith( - file.parent.stem - ), f"{file} should not go in {file.parent}" + if not file.stem.startswith(file.parent.stem): + misplaced_file.append(file) + + if directory_does_not_exists or misplaced_file: + msg = "The following functional tests are disorganized:\n" + for file, possible_dir in directory_does_not_exists: + msg += ( + f"- In '{directory}', '{file.relative_to(directory)}' " + f"should go in '{possible_dir.relative_to(directory)}'\n" + ) + for file in misplaced_file: + msg += ( + f"- In '{directory}', {file.relative_to(directory)} should go in a directory" + f" that starts with the first letters" + f" of '{file.stem}' (not '{file.parent.stem}')\n" + ) + raise AssertionError(msg) diff --git a/pylint/testutils/functional/lint_module_output_update.py b/pylint/testutils/functional/lint_module_output_update.py index e4f88b3ef..8855fd1aa 100644 --- a/pylint/testutils/functional/lint_module_output_update.py +++ b/pylint/testutils/functional/lint_module_output_update.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 diff --git a/pylint/testutils/functional/test_file.py b/pylint/testutils/functional/test_file.py index 85a72daa9..16593b5c4 100644 --- a/pylint/testutils/functional/test_file.py +++ b/pylint/testutils/functional/test_file.py @@ -1,13 +1,13 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 import configparser -import sys from collections.abc import Callable from os.path import basename, exists, join +from typing import TypedDict def parse_python_version(ver_str: str) -> tuple[int, ...]: @@ -19,12 +19,6 @@ class NoFileError(Exception): pass -if sys.version_info >= (3, 8): - from typing import TypedDict -else: - from typing_extensions import TypedDict - - class TestFileOptions(TypedDict): min_pyver: tuple[int, ...] max_pyver: tuple[int, ...] @@ -62,7 +56,7 @@ class FunctionalTestFile: def __init__(self, directory: str, filename: str) -> None: self._directory = directory self.base = filename.replace(".py", "") - # TODO: 2.x: Deprecate FunctionalTestFile.options and related code + # TODO: 3.0: Deprecate FunctionalTestFile.options and related code # We should just parse these options like a normal configuration file. self.options: TestFileOptions = { "min_pyver": (2, 5), diff --git a/pylint/testutils/functional_test_file.py b/pylint/testutils/functional_test_file.py deleted file mode 100644 index e2bd7f59b..000000000 --- a/pylint/testutils/functional_test_file.py +++ /dev/null @@ -1,24 +0,0 @@ -# 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt - -__all__ = [ - "FunctionalTestFile", - "NoFileError", - "parse_python_version", -] - -import warnings - -from pylint.testutils.functional import ( - FunctionalTestFile, - NoFileError, - parse_python_version, -) - -warnings.warn( - "'pylint.testutils.functional_test_file' will be accessible from" - " the 'pylint.testutils.functional' namespace in pylint 3.0.", - DeprecationWarning, - stacklevel=2, -) diff --git a/pylint/testutils/get_test_info.py b/pylint/testutils/get_test_info.py index a91b1ecd2..eb2c78cfd 100644 --- a/pylint/testutils/get_test_info.py +++ b/pylint/testutils/get_test_info.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 diff --git a/pylint/testutils/global_test_linter.py b/pylint/testutils/global_test_linter.py index d1ffac599..2e0d3d170 100644 --- a/pylint/testutils/global_test_linter.py +++ b/pylint/testutils/global_test_linter.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt from pylint import checkers from pylint.lint import PyLinter diff --git a/pylint/testutils/lint_module_test.py b/pylint/testutils/lint_module_test.py index e139af12b..b578e3162 100644 --- a/pylint/testutils/lint_module_test.py +++ b/pylint/testutils/lint_module_test.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 @@ -266,7 +266,7 @@ class LintModuleTest: expected_messages: MessageCounter, actual_output: list[OutputLine], ) -> str: - msg = [f'Wrong results for file "{self._test_file.base}":'] + msg = [f'Wrong message(s) raised for "{Path(self._test_file.source).name}":'] missing, unexpected = self.multiset_difference( expected_messages, actual_messages ) @@ -289,7 +289,7 @@ class LintModuleTest: ) -> str: missing = set(expected_lines) - set(received_lines) unexpected = set(received_lines) - set(expected_lines) - error_msg = f"Wrong output for '{self._test_file.base}.txt':" + error_msg = f'Wrong output for "{Path(self._test_file.expected_output).name}":' sort_by_line_number = operator.attrgetter("lineno") if missing: error_msg += "\n- Missing lines:\n" diff --git a/pylint/testutils/output_line.py b/pylint/testutils/output_line.py index 7465fce9d..95f24cc12 100644 --- a/pylint/testutils/output_line.py +++ b/pylint/testutils/output_line.py @@ -1,10 +1,9 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 -import warnings from collections.abc import Sequence from typing import Any, NamedTuple, TypeVar @@ -13,7 +12,6 @@ from astroid import nodes from pylint.constants import PY38_PLUS from pylint.interfaces import UNDEFINED, Confidence from pylint.message.message import Message -from pylint.testutils.constants import UPDATE_OPTION _T = TypeVar("_T") @@ -89,60 +87,22 @@ class OutputLine(NamedTuple): """ if isinstance(row, str): row = row.split(",") - # noinspection PyBroadException - # pylint: disable = too-many-try-statements try: + line = int(row[1]) column = cls._get_column(row[2]) - if len(row) == 5: - warnings.warn( - "In pylint 3.0 functional tests expected output should always include the " - "expected confidence level, expected end_line and expected end_column. " - "An OutputLine should thus have a length of 8.", - DeprecationWarning, - stacklevel=2, - ) - return cls( - row[0], - int(row[1]), - column, - None, - None, - row[3], - row[4], - UNDEFINED.name, - ) - if len(row) == 6: - warnings.warn( - "In pylint 3.0 functional tests expected output should always include the " - "expected end_line and expected end_column. An OutputLine should thus have " - "a length of 8.", - DeprecationWarning, - stacklevel=2, - ) - return cls( - row[0], int(row[1]), column, None, None, row[3], row[4], row[5] - ) - if len(row) == 8: - end_line = cls._get_py38_none_value(row[3], check_endline) - end_column = cls._get_py38_none_value(row[4], check_endline) - return cls( - row[0], - int(row[1]), - column, - cls._value_to_optional_int(end_line), - cls._value_to_optional_int(end_column), - row[5], - row[6], - row[7], - ) - raise IndexError - except Exception: # pylint: disable=broad-except - warnings.warn( - "Expected 'msg-symbolic-name:42:27:MyClass.my_function:The message:" - f"CONFIDENCE' but we got '{':'.join(row)}'. Try updating the expected" - f" output with:\npython tests/test_functional.py {UPDATE_OPTION}", - UserWarning, + end_line = cls._value_to_optional_int( + cls._get_py38_none_value(row[3], check_endline) + ) + end_column = cls._value_to_optional_int( + cls._get_py38_none_value(row[4], check_endline) ) + # symbol, line, column, end_line, end_column, node, msg, confidences + assert len(row) == 8 + return cls( + row[0], line, column, end_line, end_column, row[5], row[6], row[7] + ) + except Exception: # pylint: disable=broad-except + # We need this to not fail for the update script to work. return cls("", 0, 0, None, None, "", "", "") def to_csv(self) -> tuple[str, str, str, str, str, str, str, str]: diff --git a/pylint/testutils/pyreverse.py b/pylint/testutils/pyreverse.py index 20dce84dd..c621f9e7a 100644 --- a/pylint/testutils/pyreverse.py +++ b/pylint/testutils/pyreverse.py @@ -1,23 +1,17 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 import argparse import configparser import shlex -import sys from pathlib import Path -from typing import NamedTuple +from typing import NamedTuple, TypedDict from pylint.pyreverse.main import DEFAULT_COLOR_PALETTE -if sys.version_info >= (3, 8): - from typing import TypedDict -else: - from typing_extensions import TypedDict - # This class could and should be replaced with a simple dataclass when support for Python < 3.7 is dropped. # A NamedTuple is not possible as some tests need to modify attributes during the test. @@ -37,7 +31,9 @@ class PyreverseConfig( all_ancestors: bool | None = None, show_associated: int | None = None, all_associated: bool | None = None, + no_standalone: bool = False, show_builtin: bool = False, + show_stdlib: bool = False, module_names: bool | None = None, only_classnames: bool = False, output_format: str = "dot", @@ -58,7 +54,9 @@ class PyreverseConfig( self.all_ancestors = all_ancestors self.show_associated = show_associated self.all_associated = all_associated + self.no_standalone = no_standalone self.show_builtin = show_builtin + self.show_stdlib = show_stdlib self.module_names = module_names self.only_classnames = only_classnames self.output_format = output_format diff --git a/pylint/testutils/reporter_for_tests.py b/pylint/testutils/reporter_for_tests.py index 62d70a38d..d3c06eecd 100644 --- a/pylint/testutils/reporter_for_tests.py +++ b/pylint/testutils/reporter_for_tests.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 @@ -20,7 +20,7 @@ class GenericTestReporter(BaseReporter): out: StringIO - def __init__( # pylint: disable=super-init-not-called # See https://github.com/PyCQA/pylint/issues/4941 + def __init__( # pylint: disable=super-init-not-called # See https://github.com/pylint-dev/pylint/issues/4941 self, ) -> None: self.path_strip_prefix: str = getcwd() + sep diff --git a/pylint/testutils/testing_pylintrc b/pylint/testutils/testing_pylintrc index ba5318b51..9429b858f 100644 --- a/pylint/testutils/testing_pylintrc +++ b/pylint/testutils/testing_pylintrc @@ -7,3 +7,7 @@ disable= suppressed-message, locally-disabled, useless-suppression, + +enable= + deprecated-pragma, + use-symbolic-message-instead, diff --git a/pylint/testutils/tokenize_str.py b/pylint/testutils/tokenize_str.py index 13295fe51..dc9ada72a 100644 --- a/pylint/testutils/tokenize_str.py +++ b/pylint/testutils/tokenize_str.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 diff --git a/pylint/testutils/unittest_linter.py b/pylint/testutils/unittest_linter.py index a519680f1..a19afec56 100644 --- a/pylint/testutils/unittest_linter.py +++ b/pylint/testutils/unittest_linter.py @@ -1,13 +1,12 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt # pylint: disable=duplicate-code from __future__ import annotations -import sys -from typing import Any +from typing import Any, Literal from astroid import nodes @@ -15,11 +14,6 @@ from pylint.interfaces import UNDEFINED, Confidence from pylint.lint import PyLinter from pylint.testutils.output_line import MessageTest -if sys.version_info >= (3, 8): - from typing import Literal -else: - from typing_extensions import Literal - class UnittestLinter(PyLinter): """A fake linter class to capture checker messages.""" diff --git a/pylint/testutils/utils.py b/pylint/testutils/utils.py index 292e991c2..1ff999b28 100644 --- a/pylint/testutils/utils.py +++ b/pylint/testutils/utils.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 diff --git a/pylint/typing.py b/pylint/typing.py index d62618605..da11112b4 100644 --- a/pylint/typing.py +++ b/pylint/typing.py @@ -1,13 +1,12 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """A collection of typing utilities.""" from __future__ import annotations import argparse -import sys from pathlib import Path from typing import ( TYPE_CHECKING, @@ -15,19 +14,17 @@ from typing import ( Callable, Dict, Iterable, + Literal, NamedTuple, Optional, Pattern, + Protocol, Tuple, Type, + TypedDict, Union, ) -if sys.version_info >= (3, 8): - from typing import Literal, Protocol, TypedDict -else: - from typing_extensions import Literal, Protocol, TypedDict - if TYPE_CHECKING: from pylint.config.callback_actions import _CallbackAction from pylint.pyreverse.inspector import Project diff --git a/pylint/utils/__init__.py b/pylint/utils/__init__.py index bc5011db9..61833a598 100644 --- a/pylint/utils/__init__.py +++ b/pylint/utils/__init__.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Some various utilities and helper classes, most of them used in the main pylint class. @@ -14,13 +14,11 @@ from pylint.utils.utils import ( HAS_ISORT_5, IsortDriver, _check_csv, - _format_option_value, _splitstrip, _unquote, decoding_stream, diff_string, format_section, - get_global_option, get_module_and_frameid, get_rst_section, get_rst_title, @@ -34,14 +32,12 @@ __all__ = [ "HAS_ISORT_5", "IsortDriver", "_check_csv", - "_format_option_value", "_splitstrip", "_unquote", "decoding_stream", "diff_string", "FileState", "format_section", - "get_global_option", "get_module_and_frameid", "get_rst_section", "get_rst_title", diff --git a/pylint/utils/ast_walker.py b/pylint/utils/ast_walker.py index cf0f13fd1..338caf57a 100644 --- a/pylint/utils/ast_walker.py +++ b/pylint/utils/ast_walker.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 diff --git a/pylint/utils/docs.py b/pylint/utils/docs.py index ebd7cc8c3..ba592c4a4 100644 --- a/pylint/utils/docs.py +++ b/pylint/utils/docs.py @@ -1,13 +1,12 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Various helper functions to create the docs of a linter object.""" from __future__ import annotations import sys -import warnings from typing import TYPE_CHECKING, Any, TextIO from pylint.constants import MAIN_CHECKER_NAME @@ -25,20 +24,16 @@ def _get_checkers_infos(linter: PyLinter) -> dict[str, dict[str, Any]]: if name != MAIN_CHECKER_NAME: try: by_checker[name]["checker"] = checker - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - by_checker[name]["options"] += checker.options_and_values() + by_checker[name]["options"] += checker._options_and_values() by_checker[name]["msgs"].update(checker.msgs) by_checker[name]["reports"] += checker.reports except KeyError: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - by_checker[name] = { - "checker": checker, - "options": list(checker.options_and_values()), - "msgs": dict(checker.msgs), - "reports": list(checker.reports), - } + by_checker[name] = { + "checker": checker, + "options": list(checker._options_and_values()), + "msgs": dict(checker.msgs), + "reports": list(checker.reports), + } return by_checker @@ -51,16 +46,14 @@ Pylint provides global options and switches. """ for checker in linter.get_checkers(): if checker.name == MAIN_CHECKER_NAME and checker.options: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - for section, options in checker.options_by_section(): - if section is None: - title = "General options" - else: - title = f"{section.capitalize()} options" - result += get_rst_title(title, "~") - assert isinstance(options, list) - result += f"{get_rst_section(None, options)}\n" + for section, options in checker._options_by_section(): + if section is None: + title = "General options" + else: + title = f"{section.capitalize()} options" + result += get_rst_title(title, "~") + assert isinstance(options, list) + result += f"{get_rst_section(None, options)}\n" return result diff --git a/pylint/utils/file_state.py b/pylint/utils/file_state.py index 19122b373..9ae5fb077 100644 --- a/pylint/utils/file_state.py +++ b/pylint/utils/file_state.py @@ -1,15 +1,13 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 import collections -import sys -import warnings from collections import defaultdict from collections.abc import Iterator -from typing import TYPE_CHECKING, Dict +from typing import TYPE_CHECKING, Dict, Literal from astroid import nodes @@ -19,11 +17,6 @@ from pylint.constants import ( WarningScope, ) -if sys.version_info >= (3, 8): - from typing import Literal -else: - from typing_extensions import Literal - if TYPE_CHECKING: from pylint.message import MessageDefinition, MessageDefinitionStore @@ -36,26 +29,12 @@ class FileState: def __init__( self, - modname: str | None = None, - msg_store: MessageDefinitionStore | None = None, + modname: str, + msg_store: MessageDefinitionStore, node: nodes.Module | None = None, *, is_base_filestate: bool = False, ) -> None: - if modname is None: - warnings.warn( - "FileState needs a string as modname argument. " - "This argument will be required in pylint 3.0", - DeprecationWarning, - stacklevel=2, - ) - if msg_store is None: - warnings.warn( - "FileState needs a 'MessageDefinitionStore' as msg_store argument. " - "This argument will be required in pylint 3.0", - DeprecationWarning, - stacklevel=2, - ) self.base_name = modname self._module_msgs_state: MessageStateDict = {} self._raw_module_msgs_state: MessageStateDict = {} @@ -74,25 +53,6 @@ class FileState: PyLinter. """ - def collect_block_lines( - self, msgs_store: MessageDefinitionStore, module_node: nodes.Module - ) -> None: - """Walk the AST to collect block level options line numbers.""" - warnings.warn( - "'collect_block_lines' has been deprecated and will be removed in pylint 3.0.", - DeprecationWarning, - stacklevel=2, - ) - for msg, lines in self._module_msgs_state.items(): - self._raw_module_msgs_state[msg] = lines.copy() - orig_state = self._module_msgs_state.copy() - self._module_msgs_state = {} - self._suppression_mapping = {} - self._effective_max_line_number = module_node.tolineno - for msgid, lines in orig_state.items(): - for msgdef in msgs_store.get_message_definitions(msgid): - self._set_state_on_block_lines(msgs_store, module_node, msgdef, lines) - def _set_state_on_block_lines( self, msgs_store: MessageDefinitionStore, @@ -230,10 +190,6 @@ class FileState: ) -> None: """Set status (enabled/disable) for a given message at a given line.""" assert line > 0 - assert self._module - # TODO: 3.0: Remove unnecessary assertion - assert self._msgs_store - if scope != "line": # Expand the status to cover all relevant block lines self._set_state_on_block_lines( diff --git a/pylint/utils/linterstats.py b/pylint/utils/linterstats.py index f9a6c6c3c..e7a088b7b 100644 --- a/pylint/utils/linterstats.py +++ b/pylint/utils/linterstats.py @@ -1,19 +1,13 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 -import sys -from typing import cast +from typing import Literal, TypedDict, cast from pylint.typing import MessageTypesFullName -if sys.version_info >= (3, 8): - from typing import Literal, TypedDict -else: - from typing_extensions import Literal, TypedDict - class BadNames(TypedDict): """TypedDict to store counts of node types with bad names.""" diff --git a/pylint/utils/pragma_parser.py b/pylint/utils/pragma_parser.py index df3627380..12513e284 100644 --- a/pylint/utils/pragma_parser.py +++ b/pylint/utils/pragma_parser.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 diff --git a/pylint/utils/utils.py b/pylint/utils/utils.py index 054d307bc..543d7cbb3 100644 --- a/pylint/utils/utils.py +++ b/pylint/utils/utils.py @@ -1,6 +1,6 @@ # 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 -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt +# 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 @@ -28,12 +28,12 @@ from typing import ( TYPE_CHECKING, Any, List, + Literal, Pattern, TextIO, Tuple, TypeVar, Union, - overload, ) from astroid import Module, modutils, nodes @@ -41,13 +41,7 @@ from astroid import Module, modutils, nodes from pylint.constants import PY_EXTS from pylint.typing import OptionDict -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 from pylint.lint import PyLinter DEFAULT_LINE_LENGTH = 79 @@ -215,77 +209,6 @@ def register_plugins(linter: PyLinter, directory: str) -> None: imported[base] = 1 -@overload -def get_global_option( - checker: BaseChecker, option: GLOBAL_OPTION_BOOL, default: bool | None = ... -) -> bool: - ... - - -@overload -def get_global_option( - checker: BaseChecker, option: GLOBAL_OPTION_INT, default: int | None = ... -) -> int: - ... - - -@overload -def get_global_option( - checker: BaseChecker, - option: GLOBAL_OPTION_LIST, - default: list[str] | None = ..., -) -> list[str]: - ... - - -@overload -def get_global_option( - checker: BaseChecker, - option: GLOBAL_OPTION_PATTERN, - default: Pattern[str] | None = ..., -) -> Pattern[str]: - ... - - -@overload -def get_global_option( - checker: BaseChecker, - option: GLOBAL_OPTION_PATTERN_LIST, - default: list[Pattern[str]] | None = ..., -) -> list[Pattern[str]]: - ... - - -@overload -def get_global_option( - checker: BaseChecker, - option: GLOBAL_OPTION_TUPLE_INT, - default: tuple[int, ...] | None = ..., -) -> tuple[int, ...]: - ... - - -def get_global_option( - checker: BaseChecker, - option: GLOBAL_OPTION_NAMES, - default: T_GlobalOptionReturnTypes | None = None, # pylint: disable=unused-argument -) -> T_GlobalOptionReturnTypes | None | Any: - """DEPRECATED: 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. - """ - warnings.warn( - "get_global_option has been deprecated. You can use " - "checker.linter.config to get all global options instead.", - DeprecationWarning, - stacklevel=2, - ) - return getattr(checker.linter.config, option.replace("-", "_")) - - 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. @@ -340,7 +263,7 @@ def _comment(string: str) -> str: def _format_option_value(optdict: OptionDict, value: Any) -> str: """Return the user input's value from a 'compiled' value. - TODO: 3.0: Remove deprecated function + 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) |