diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index 4f415dd34d..d47663a0da 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -12,16 +12,17 @@ import re import string import warnings -from collections.abc import Callable, Iterable +from collections.abc import Iterable from functools import lru_cache, partial from re import Match -from typing import TypeVar +from typing import Callable, TypeVar import _string import astroid.objects from astroid import TooManyLevelsError, nodes from astroid.context import InferenceContext +from pylint.checkers.base_checker import BaseChecker from pylint.constants import TYPING_TYPE_CHECKS_GUARDS COMP_NODE_TYPES = ( @@ -223,6 +224,8 @@ ) T_Node = TypeVar("T_Node", bound=nodes.NodeNG) +CheckerT = TypeVar("CheckerT", bound=BaseChecker) +AstCallback = Callable[[CheckerT, T_Node], None] class NoSuchArgumentError(Exception): @@ -424,8 +427,16 @@ def overrides_a_method(class_node: nodes.ClassDef, name: str) -> bool: return False -def check_messages(*messages: str) -> Callable: - """Decorator to store messages that are handled by a checker method.""" +def only_required_for_messages(*messages: str) -> Callable[[AstCallback], AstCallback]: + """Decorator to store messages that are handled by a checker method as an + attribute of the function object. + + This information is used by ``ASTWalker`` to decide whether to call the decorated + method or not. If none of the messages is enabled, the method will be skipped. + Therefore, the list of messages must be well maintained at all times! + This decorator only has an effect on ``visit_*`` and ``leave_*`` methods + of a class inheriting from ``BaseChecker`` and implementing ``IAstroidChecker``. + """ def store_messages(func): func.checks_msgs = messages @@ -434,6 +445,20 @@ def store_messages(func): return store_messages +def check_messages(*messages: str) -> Callable[[AstCallback], AstCallback]: + """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, + ) + + return only_required_for_messages(*messages) + + class IncompleteFormatString(Exception): """A format string ended in the middle of a format specifier.""" diff --git a/tests/checkers/unittest_utils.py b/tests/checkers/unittest_utils.py index 1ac4611412..178d5467b8 100644 --- a/tests/checkers/unittest_utils.py +++ b/tests/checkers/unittest_utils.py @@ -473,3 +473,18 @@ def test_deprecation_is_inside_lambda() -> None: with pytest.warns(DeprecationWarning) as records: utils.is_inside_lambda(nodes.NodeNG()) assert len(records) == 1 + + +def test_deprecation_check_messages() -> None: + with pytest.warns(DeprecationWarning) as records: + + class Checker: # pylint: disable=unused-variable + @utils.check_messages("my-message") + def visit_assname(self, node): + pass + + assert len(records) == 1 + assert ( + records[0].message.args[0] + == "utils.check_messages will be removed in favour of calling utils.only_required_for_messages in pylint 3.0" + ) diff --git a/tests/utils/unittest_ast_walker.py b/tests/utils/unittest_ast_walker.py index c0ec6b8e6f..917200b325 100644 --- a/tests/utils/unittest_ast_walker.py +++ b/tests/utils/unittest_ast_walker.py @@ -8,7 +8,7 @@ import astroid -from pylint.checkers.utils import check_messages +from pylint.checkers.utils import only_required_for_messages from pylint.utils import ASTWalker @@ -24,23 +24,23 @@ class Checker: def __init__(self) -> None: self.called: set[str] = set() - @check_messages("first-message") + @only_required_for_messages("first-message") def visit_module(self, module): # pylint: disable=unused-argument self.called.add("module") - @check_messages("second-message") + @only_required_for_messages("second-message") def visit_call(self, module): raise NotImplementedError - @check_messages("second-message", "third-message") + @only_required_for_messages("second-message", "third-message") def visit_assignname(self, module): # pylint: disable=unused-argument self.called.add("assignname") - @check_messages("second-message") + @only_required_for_messages("second-message") def leave_assignname(self, module): raise NotImplementedError - def test_check_messages(self) -> None: + def test_only_required_for_messages(self) -> None: linter = self.MockLinter( {"first-message": True, "second-message": False, "third-message": True} ) @@ -55,7 +55,7 @@ class Checker: def __init__(self) -> None: self.called = False - @check_messages("first-message") + @only_required_for_messages("first-message") def visit_assname(self, node): # pylint: disable=unused-argument self.called = True