From 6721cd1cf2da0294124b75d382a042e23ec27d47 Mon Sep 17 00:00:00 2001 From: Matus Valo Date: Mon, 22 Feb 2021 00:26:53 +0100 Subject: Introduced deprecated attributes --- ChangeLog | 2 + doc/whatsnew/2.8.rst | 1 + examples/deprecation_checker.py | 62 ++++--- pylint/checkers/deprecated.py | 75 ++++++++- tests/checkers/unittest_deprecated.py | 302 +++++++++++++++++++++++++++++++--- 5 files changed, 393 insertions(+), 49 deletions(-) diff --git a/ChangeLog b/ChangeLog index 4211c8814..5558d0157 100644 --- a/ChangeLog +++ b/ChangeLog @@ -9,6 +9,8 @@ Release date: TBA .. Put new features here +* Introduce logic for checking deprecated attributes in DeprecationMixin. + What's New in Pylint 2.7.3? =========================== diff --git a/doc/whatsnew/2.8.rst b/doc/whatsnew/2.8.rst index f7806b1a9..7768edd86 100644 --- a/doc/whatsnew/2.8.rst +++ b/doc/whatsnew/2.8.rst @@ -12,6 +12,7 @@ Summary -- Release highlights New checkers ============ +* Add ``deprecated-argument`` check for deprecated arguments. Other Changes ============= diff --git a/examples/deprecation_checker.py b/examples/deprecation_checker.py index 382f76bb3..03b8e3e36 100644 --- a/examples/deprecation_checker.py +++ b/examples/deprecation_checker.py @@ -8,28 +8,39 @@ from module mymodule: def deprecated_function(): pass + def myfunction(arg0, arg1, deprecated_arg1=None, arg2='foo', arg3='bar', deprecated_arg2='spam'): + pass + class MyClass: def deprecated_method(self): pass + def mymethod(self, arg0, arg1, deprecated1=None, arg2='foo', deprecated2='bar', arg3='spam'): + pass + $ cat mymain.py - from mymodule import deprecated_function, MyClass + from mymodule import deprecated_function, myfunction, MyClass deprecated_function() + myfunction(0, 1, 'deprecated_arg1', deprecated_arg2=None) MyClass().deprecated_method() + MyClass().mymethod(0, 1, deprecated1=None, deprecated2=None) $ pylint --load-plugins=deprecation_checker mymain.py ************* Module mymain mymain.py:3:0: W1505: Using deprecated method deprecated_function() (deprecated-method) - mymain.py:4:0: W1505: Using deprecated method deprecated_method() (deprecated-method) + mymain.py:4:0: W1511: Using deprecated argument deprecated_arg1 of method myfunction() (deprecated-argument) + mymain.py:4:0: W1511: Using deprecated argument deprecated_arg2 of method myfunction() (deprecated-argument) + mymain.py:5:0: W1505: Using deprecated method deprecated_method() (deprecated-method) + mymain.py:6:0: W1511: Using deprecated argument deprecated1 of method mymethod() (deprecated-argument) + mymain.py:6:0: W1511: Using deprecated argument deprecated2 of method mymethod() (deprecated-argument) ------------------------------------------------------------------ - Your code has been rated at 3.33/10 (previous run: 3.33/10, +0.00) + Your code has been rated at 2.00/10 (previous run: 2.00/10, +0.00) """ -import astroid -from pylint.checkers import BaseChecker, DeprecatedMixin, utils +from pylint.checkers import BaseChecker, DeprecatedMixin from pylint.interfaces import IAstroidChecker @@ -45,24 +56,6 @@ class DeprecationChecker(DeprecatedMixin, BaseChecker): # The name defines a custom section of the config for this checker. name = "deprecated" - @utils.check_messages( - "deprecated-method", - ) - def visit_call(self, node): - """Called when a :class:`.astroid.node_classes.Call` node is visited. - - See :mod:`astroid` for the description of available nodes. - - :param node: The node to check. - :type node: astroid.node_classes.Call - """ - try: - for inferred in node.func.infer(): - # Calling entry point for deprecation check logic. - self.check_deprecated_method(node, inferred) - except astroid.InferenceError: - return - def deprecated_methods(self): """Callback method called by DeprecatedMixin for every method/function found in the code. @@ -71,6 +64,29 @@ class DeprecationChecker(DeprecatedMixin, BaseChecker): """ return {"mymodule.deprecated_function", "mymodule.MyClass.deprecated_method"} + def deprecated_arguments(self, method: str): + """Callback returning the deprecated arguments of method/function. + + Returns: + collections.abc.Iterable in form: + ((POSITION1, PARAM1), (POSITION2: PARAM2) ...) + where + * POSITIONX - position of deprecated argument PARAMX in function definition. + If argument is keyword-only, POSITIONX should be None. + * PARAMX - name of the deprecated argument. + """ + if method == "mymodule.myfunction": + # myfunction() has two deprecated arguments: + # * deprecated_arg1 defined at 2nd position and + # * deprecated_arg2 defined at 5th position. + return ((2, "deprecated_arg1"), (5, "deprecated_arg2")) + if method == "mymodule.MyClass.mymethod": + # mymethod() has two deprecated arguments: + # * deprecated1 defined at 2nd position and + # * deprecated2 defined at 4th position. + return ((2, "deprecated1"), (4, "deprecated2")) + return () + def register(linter): """This required method auto registers the checker. diff --git a/pylint/checkers/deprecated.py b/pylint/checkers/deprecated.py index 54254340d..358c88ee4 100644 --- a/pylint/checkers/deprecated.py +++ b/pylint/checkers/deprecated.py @@ -3,11 +3,13 @@ """Checker mixin for deprecated functionality.""" -import abc +from itertools import chain from typing import Any import astroid +from pylint.checkers import utils + ACCEPTABLE_NODES = ( astroid.BoundMethod, astroid.UnboundMethod, @@ -15,9 +17,9 @@ ACCEPTABLE_NODES = ( ) -class DeprecatedMixin(metaclass=abc.ABCMeta): +class DeprecatedMixin: """A mixin implementing logic for checking deprecated symbols. - A class imlementing mixin must define "deprecated-method" Message. + A class implementing mixin must define "deprecated-method" Message. """ msgs: Any = { @@ -26,15 +28,65 @@ class DeprecatedMixin(metaclass=abc.ABCMeta): "deprecated-method", "The method is marked as deprecated and will be removed in the future.", ), + "W1511": ( + "Using deprecated argument %s of method %s()", + "deprecated-argument", + "The argument is marked as deprecated and will be removed in the future.", + ), } - @abc.abstractmethod + @utils.check_messages( + "deprecated-method", + "deprecated-argument", + ) + def visit_call(self, node): + """Called when a :class:`.astroid.node_classes.Call` node is visited. + + Args: + node (astroid.node_classes.Call): The node to check. + """ + try: + for inferred in node.func.infer(): + # Calling entry point for deprecation check logic. + self.check_deprecated_method(node, inferred) + except astroid.InferenceError: + return + def deprecated_methods(self): """Callback returning the deprecated methods/functions. Returns: collections.abc.Container of deprecated function/method names. """ + # pylint: disable=no-self-use + return () + + def deprecated_arguments(self, method: str): + """Callback returning the deprecated arguments of method/function. + + Args: + method (str): name of function/method checked for deprecated arguments + + Returns: + collections.abc.Iterable in form: + ((POSITION1, PARAM1), (POSITION2: PARAM2) ...) + where + * POSITIONX - position of deprecated argument PARAMX in function definition. + If argument is keyword-only, POSITIONX should be None. + * PARAMX - name of the deprecated argument. + E.g. suppose function: + + .. code-block:: python + def bar(arg1, arg2, arg3, arg4, arg5='spam') + + with deprecated arguments `arg2` and `arg4`. `deprecated_arguments` should return: + + .. code-block:: python + ((1, 'arg2'), (3, 'arg4')) + """ + # pylint: disable=no-self-use + # pylint: disable=unused-argument + return () def check_deprecated_method(self, node, inferred): """Executes the checker for the given node. This method should @@ -56,3 +108,18 @@ class DeprecatedMixin(metaclass=abc.ABCMeta): qname = inferred.qname() if any(name in self.deprecated_methods() for name in (qname, func_name)): self.add_message("deprecated-method", node=node, args=(func_name,)) + num_of_args = len(node.args) + kwargs = {kw.arg for kw in node.keywords} if node.keywords else {} + for position, arg_name in chain( + self.deprecated_arguments(func_name), self.deprecated_arguments(qname) + ): + if arg_name in kwargs: + # function was called with deprecated argument as keyword argument + self.add_message( + "deprecated-argument", node=node, args=(arg_name, func_name) + ) + elif position is not None and position < num_of_args: + # function was called with deprecated argument as positional argument + self.add_message( + "deprecated-argument", node=node, args=(arg_name, func_name) + ) diff --git a/tests/checkers/unittest_deprecated.py b/tests/checkers/unittest_deprecated.py index 42e78a605..f7e8c1309 100644 --- a/tests/checkers/unittest_deprecated.py +++ b/tests/checkers/unittest_deprecated.py @@ -1,6 +1,6 @@ import astroid -from pylint.checkers import BaseChecker, DeprecatedMixin, utils +from pylint.checkers import BaseChecker, DeprecatedMixin from pylint.interfaces import UNDEFINED, IAstroidChecker from pylint.testutils import CheckerTestCase, Message @@ -9,30 +9,30 @@ class _DeprecatedChecker(DeprecatedMixin, BaseChecker): __implements__ = (IAstroidChecker,) name = "deprecated" - msgs = { - "W1505": ( - "Using deprecated method %s()", - "deprecated-method", - "The method is marked as deprecated and will be removed in " - "a future version of Python. Consider looking for an " - "alternative in the documentation.", - ) - } - - @utils.check_messages( - "deprecated-method", - ) - def visit_call(self, node): - """Visit a Call node.""" - try: - for inferred in node.func.infer(): - self.check_deprecated_method(node, inferred) - except astroid.InferenceError: - return - def deprecated_methods(self): return {"deprecated_func", ".Deprecated.deprecated_method"} + def deprecated_arguments(self, method): + if method == "myfunction1": + # def myfunction1(arg1, deprecated_arg1='spam') + return ((1, "deprecated_arg1"),) + if method == "myfunction2": + # def myfunction2(arg1, deprecated_arg1, arg2='foo', deprecated_arg2='spam')) + return ((1, "deprecated_arg1"), (3, "deprecated_arg2")) + if method == "myfunction3": + # def myfunction1(arg1, *, deprecated_arg1='spam') + return ((None, "deprecated_arg1"),) + if method == ".MyClass.mymethod1": + # def mymethod1(self, arg1, deprecated_arg1=None) + return ((1, "deprecated_arg1"),) + if method == ".MyClass.mymethod2": + # def mymethod2(self, arg1, deprecated_arg1='bar', arg2='foo', deprecated_arg2='spam')) + return ((1, "deprecated_arg1"), (3, "deprecated_arg2")) + if method == ".MyClass.mymethod3": + # def mymethod1(self, arg1, *, deprecated_arg1=None) + return ((None, "deprecated_arg1"),) + return () + class TestDeprecatedChecker(CheckerTestCase): CHECKER_CLASS = _DeprecatedChecker @@ -97,3 +97,261 @@ class TestDeprecatedChecker(CheckerTestCase): ) with self.assertNoMessages(): self.checker.visit_call(node) + + def test_function_deprecated_arg(self): + # Tests raising error when calling function with deprecated argument + node = astroid.extract_node( + """ + def myfunction1(arg1, deprecated_arg1='spam'): + pass + + myfunction1(None, 'deprecated') + """ + ) + with self.assertAddsMessages( + Message( + msg_id="deprecated-argument", + args=("deprecated_arg1", "myfunction1"), + node=node, + confidence=UNDEFINED, + ) + ): + self.checker.visit_call(node) + + def test_function_deprecated_kwarg(self): + # Tests raising error when calling function with deprecated keyword argument + node = astroid.extract_node( + """ + def myfunction1(arg1, deprecated_arg1='spam'): + pass + + myfunction1(None, deprecated_arg1='deprecated') + """ + ) + with self.assertAddsMessages( + Message( + msg_id="deprecated-argument", + args=("deprecated_arg1", "myfunction1"), + node=node, + confidence=UNDEFINED, + ) + ): + self.checker.visit_call(node) + + def test_function_deprecated_not_used(self): + # Tests raising error when calling function without deprecated argument + node = astroid.extract_node( + """ + def myfunction1(arg1, deprecated_arg1='spam'): + pass + + myfunction1(None) + """ + ) + with self.assertNoMessages(): + self.checker.visit_call(node) + + def test_function_deprecated_kwarg_only(self): + # Tests raising error when calling function with deprecated keyword only argument + node = astroid.extract_node( + """ + def myfunction3(arg1, *, deprecated_arg1='spam'): + pass + + myfunction3(None, deprecated_arg1='deprecated') + """ + ) + with self.assertAddsMessages( + Message( + msg_id="deprecated-argument", + args=("deprecated_arg1", "myfunction3"), + node=node, + confidence=UNDEFINED, + ) + ): + self.checker.visit_call(node) + + def test_method_deprecated_arg(self): + # Tests raising error when calling method with deprecated argument + node = astroid.extract_node( + """ + class MyClass: + def mymethod1(self, arg1, deprecated_arg1): + pass + + MyClass().mymethod1(None, 'deprecated') + """ + ) + with self.assertAddsMessages( + Message( + msg_id="deprecated-argument", + args=("deprecated_arg1", "mymethod1"), + node=node, + confidence=UNDEFINED, + ) + ): + self.checker.visit_call(node) + + def test_method_deprecated_kwarg(self): + # Tests raising error when calling method with deprecated keyword argument + node = astroid.extract_node( + """ + class MyClass: + def mymethod1(self, arg1, deprecated_arg1): + pass + + MyClass().mymethod1(None, deprecated_arg1='deprecated') + """ + ) + with self.assertAddsMessages( + Message( + msg_id="deprecated-argument", + args=("deprecated_arg1", "mymethod1"), + node=node, + confidence=UNDEFINED, + ) + ): + self.checker.visit_call(node) + + def test_method_deprecated_not_used(self): + # Tests raising error when calling method without deprecated argument + node = astroid.extract_node( + """ + class MyClass: + def mymethod1(self, arg1, deprecated_arg1): + pass + + MyClass().mymethod1(None) + """ + ) + with self.assertNoMessages(): + self.checker.visit_call(node) + + def test_method_deprecated_kwarg_only(self): + # Tests raising error when calling method with deprecated keyword only argument + node = astroid.extract_node( + """ + class MyClass: + def mymethod3(self, arg1, *, deprecated_arg1): + pass + + MyClass().mymethod3(None, deprecated_arg1='deprecated') + """ + ) + with self.assertAddsMessages( + Message( + msg_id="deprecated-argument", + args=("deprecated_arg1", "mymethod3"), + node=node, + confidence=UNDEFINED, + ) + ): + self.checker.visit_call(node) + + def test_function_deprecated_arg_kwargs(self): + # Tests raising error when calling function with deprecated argument + # and keyword argument + node = astroid.extract_node( + """ + def myfunction2(arg1, deprecated_arg1, arg2='foo', deprecated_arg2='spam'): + pass + + myfunction2(None, 'deprecated', deprecated_arg2='deprecated') + """ + ) + with self.assertAddsMessages( + Message( + msg_id="deprecated-argument", + args=("deprecated_arg1", "myfunction2"), + node=node, + confidence=UNDEFINED, + ), + Message( + msg_id="deprecated-argument", + args=("deprecated_arg2", "myfunction2"), + node=node, + confidence=UNDEFINED, + ), + ): + self.checker.visit_call(node) + + def test_function_deprecated_kwarg_kwarg(self): + # Tests raising error when calling function with deprecated keyword arguments + node = astroid.extract_node( + """ + def myfunction2(arg1, deprecated_arg1, arg2='foo', deprecated_arg2='spam'): + pass + + myfunction2(None, deprecated_arg1='deprecated', deprecated_arg2='deprecated') + """ + ) + with self.assertAddsMessages( + Message( + msg_id="deprecated-argument", + args=("deprecated_arg1", "myfunction2"), + node=node, + confidence=UNDEFINED, + ), + Message( + msg_id="deprecated-argument", + args=("deprecated_arg2", "myfunction2"), + node=node, + confidence=UNDEFINED, + ), + ): + self.checker.visit_call(node) + + def test_method_deprecated_arg_kwargs(self): + # Tests raising error when calling method with deprecated argument + # and keyword argument + node = astroid.extract_node( + """ + class MyClass: + def mymethod2(self, arg1, deprecated_arg1, arg2='foo', deprecated_arg2='spam'): + pass + + MyClass().mymethod2(None, 'deprecated', deprecated_arg2='deprecated') + """ + ) + with self.assertAddsMessages( + Message( + msg_id="deprecated-argument", + args=("deprecated_arg1", "mymethod2"), + node=node, + confidence=UNDEFINED, + ), + Message( + msg_id="deprecated-argument", + args=("deprecated_arg2", "mymethod2"), + node=node, + confidence=UNDEFINED, + ), + ): + self.checker.visit_call(node) + + def test_method_deprecated_kwarg_kwarg(self): + # Tests raising error when calling method with deprecated keyword arguments + node = astroid.extract_node( + """ + class MyClass: + def mymethod2(self, arg1, deprecated_arg1, arg2='foo', deprecated_arg2='spam'): + pass + + MyClass().mymethod2(None, deprecated_arg1='deprecated', deprecated_arg2='deprecated') + """ + ) + with self.assertAddsMessages( + Message( + msg_id="deprecated-argument", + args=("deprecated_arg1", "mymethod2"), + node=node, + confidence=UNDEFINED, + ), + Message( + msg_id="deprecated-argument", + args=("deprecated_arg2", "mymethod2"), + node=node, + confidence=UNDEFINED, + ), + ): + self.checker.visit_call(node) -- cgit v1.2.1