diff options
author | Ramon Saraiva <ramonsaraiva@gmail.com> | 2022-10-31 16:26:57 -0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-10-31 20:26:57 +0100 |
commit | 6c81cc93e6b79a2564e4ce02088a34a7ec2bbfaa (patch) | |
tree | 440b679f5d72cb647423c9174d3b04f1d7e3812c | |
parent | 35ce253048ce58e20601c4ad7e96fde8a10d1c2f (diff) | |
download | pylint-git-6c81cc93e6b79a2564e4ce02088a34a7ec2bbfaa.tar.gz |
@singledispatch and @singledispatchmethod checks for methods and functions (#7575)
Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
22 files changed, 304 insertions, 4 deletions
diff --git a/.pyenchant_pylint_custom_dict.txt b/.pyenchant_pylint_custom_dict.txt index f223005f6..ff95b085c 100644 --- a/.pyenchant_pylint_custom_dict.txt +++ b/.pyenchant_pylint_custom_dict.txt @@ -282,6 +282,7 @@ sep setcomp shortstrings singledispatch +singledispatchmethod spammy sqlalchemy src diff --git a/doc/data/messages/s/singledispatch-method/bad.py b/doc/data/messages/s/singledispatch-method/bad.py new file mode 100644 index 000000000..49e545b92 --- /dev/null +++ b/doc/data/messages/s/singledispatch-method/bad.py @@ -0,0 +1,19 @@ +from functools import singledispatch + + +class Board: + @singledispatch # [singledispatch-method] + @classmethod + def convert_position(cls, position): + pass + + @convert_position.register # [singledispatch-method] + @classmethod + def _(cls, position: str) -> tuple: + position_a, position_b = position.split(",") + return (int(position_a), int(position_b)) + + @convert_position.register # [singledispatch-method] + @classmethod + def _(cls, position: tuple) -> str: + return f"{position[0]},{position[1]}" diff --git a/doc/data/messages/s/singledispatch-method/details.rst b/doc/data/messages/s/singledispatch-method/details.rst new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/doc/data/messages/s/singledispatch-method/details.rst diff --git a/doc/data/messages/s/singledispatch-method/good.py b/doc/data/messages/s/singledispatch-method/good.py new file mode 100644 index 000000000..f38047cd1 --- /dev/null +++ b/doc/data/messages/s/singledispatch-method/good.py @@ -0,0 +1,19 @@ +from functools import singledispatch + + +class Board: + @singledispatch + @staticmethod + def convert_position(position): + pass + + @convert_position.register + @staticmethod + def _(position: str) -> tuple: + position_a, position_b = position.split(",") + return (int(position_a), int(position_b)) + + @convert_position.register + @staticmethod + def _(position: tuple) -> str: + return f"{position[0]},{position[1]}" diff --git a/doc/data/messages/s/singledispatchmethod-function/bad.py b/doc/data/messages/s/singledispatchmethod-function/bad.py new file mode 100644 index 000000000..d2255f865 --- /dev/null +++ b/doc/data/messages/s/singledispatchmethod-function/bad.py @@ -0,0 +1,19 @@ +from functools import singledispatchmethod + + +class Board: + @singledispatchmethod # [singledispatchmethod-function] + @staticmethod + def convert_position(position): + pass + + @convert_position.register # [singledispatchmethod-function] + @staticmethod + def _(position: str) -> tuple: + position_a, position_b = position.split(",") + return (int(position_a), int(position_b)) + + @convert_position.register # [singledispatchmethod-function] + @staticmethod + def _(position: tuple) -> str: + return f"{position[0]},{position[1]}" diff --git a/doc/data/messages/s/singledispatchmethod-function/details.rst b/doc/data/messages/s/singledispatchmethod-function/details.rst new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/doc/data/messages/s/singledispatchmethod-function/details.rst diff --git a/doc/data/messages/s/singledispatchmethod-function/good.py b/doc/data/messages/s/singledispatchmethod-function/good.py new file mode 100644 index 000000000..1bc3570b5 --- /dev/null +++ b/doc/data/messages/s/singledispatchmethod-function/good.py @@ -0,0 +1,18 @@ +from functools import singledispatchmethod + + +class Board: + @singledispatchmethod + def convert_position(cls, position): + pass + + @singledispatchmethod + @classmethod + def _(cls, position: str) -> tuple: + position_a, position_b = position.split(",") + return (int(position_a), int(position_b)) + + @singledispatchmethod + @classmethod + def _(cls, position: tuple) -> str: + return f"{position[0]},{position[1]}" diff --git a/doc/user_guide/checkers/features.rst b/doc/user_guide/checkers/features.rst index 72db6cdf4..baaf5a1cd 100644 --- a/doc/user_guide/checkers/features.rst +++ b/doc/user_guide/checkers/features.rst @@ -930,6 +930,12 @@ Stdlib checker Messages :invalid-envvar-value (E1507): *%s does not support %s type argument* Env manipulation functions support only string type arguments. See https://docs.python.org/3/library/os.html#os.getenv. +:singledispatch-method (E1519): *singledispatch decorator should not be used with methods, use singledispatchmethod instead.* + singledispatch should decorate functions and not class/instance methods. Use + singledispatchmethod for those cases. +:singledispatchmethod-function (E1520): *singledispatchmethod decorator should not be used with functions, use singledispatch instead.* + singledispatchmethod should decorate class/instance methods and not + functions. Use singledispatch for those cases. :bad-open-mode (W1501): *"%s" is not a valid mode for open.* Python supports: r, w, a[, x] modes with b, +, and U (only with r) options. See https://docs.python.org/3/library/functions.html#open diff --git a/doc/user_guide/messages/messages_overview.rst b/doc/user_guide/messages/messages_overview.rst index 92497dab9..c7720453f 100644 --- a/doc/user_guide/messages/messages_overview.rst +++ b/doc/user_guide/messages/messages_overview.rst @@ -145,6 +145,8 @@ All messages in the error category: error/return-arg-in-generator error/return-in-init error/return-outside-function + error/singledispatch-method + error/singledispatchmethod-function error/star-needs-assignment-target error/syntax-error error/too-few-format-args diff --git a/doc/whatsnew/fragments/6917.new_check b/doc/whatsnew/fragments/6917.new_check new file mode 100644 index 000000000..d36aa2c59 --- /dev/null +++ b/doc/whatsnew/fragments/6917.new_check @@ -0,0 +1,4 @@ +Added ``singledispatch-method`` which informs that ``@singledispatch`` should decorate functions and not class/instance methods. +Added ``singledispatchmethod-function`` which informs that ``@singledispatchmethod`` should decorate class/instance methods and not functions. + +Closes #6917 diff --git a/pylint/checkers/stdlib.py b/pylint/checkers/stdlib.py index 04658b654..e7ef763c4 100644 --- a/pylint/checkers/stdlib.py +++ b/pylint/checkers/stdlib.py @@ -397,6 +397,20 @@ class StdlibChecker(DeprecatedMixin, BaseChecker): "Env manipulation functions support only string type arguments. " "See https://docs.python.org/3/library/os.html#os.getenv.", ), + "E1519": ( + "singledispatch decorator should not be used with methods, " + "use singledispatchmethod instead.", + "singledispatch-method", + "singledispatch should decorate functions and not class/instance methods. " + "Use singledispatchmethod for those cases.", + ), + "E1520": ( + "singledispatchmethod decorator should not be used with functions, " + "use singledispatch instead.", + "singledispatchmethod-function", + "singledispatchmethod should decorate class/instance methods and not functions. " + "Use singledispatch for those cases.", + ), "W1508": ( "%s default type is %s. Expected str or None.", "invalid-envvar-default", @@ -565,10 +579,15 @@ class StdlibChecker(DeprecatedMixin, BaseChecker): for value in node.values: self._check_datetime(value) - @utils.only_required_for_messages("method-cache-max-size-none") + @utils.only_required_for_messages( + "method-cache-max-size-none", + "singledispatch-method", + "singledispatchmethod-function", + ) def visit_functiondef(self, node: nodes.FunctionDef) -> None: if node.decorators and isinstance(node.parent, nodes.ClassDef): self._check_lru_cache_decorators(node.decorators) + self._check_dispatch_decorators(node) def _check_lru_cache_decorators(self, decorators: nodes.Decorators) -> None: """Check if instance methods are decorated with functools.lru_cache.""" @@ -607,6 +626,36 @@ class StdlibChecker(DeprecatedMixin, BaseChecker): confidence=interfaces.INFERENCE, ) + def _check_dispatch_decorators(self, node: nodes.FunctionDef) -> None: + decorators_map: dict[str, tuple[nodes.NodeNG, interfaces.Confidence]] = {} + + for decorator in node.decorators.nodes: + if isinstance(decorator, nodes.Name) and decorator.name: + decorators_map[decorator.name] = (decorator, interfaces.HIGH) + elif utils.is_registered_in_singledispatch_function(node): + decorators_map["singledispatch"] = (decorator, interfaces.INFERENCE) + elif utils.is_registered_in_singledispatchmethod_function(node): + decorators_map["singledispatchmethod"] = ( + decorator, + interfaces.INFERENCE, + ) + + if "singledispatch" in decorators_map and "classmethod" in decorators_map: + self.add_message( + "singledispatch-method", + node=decorators_map["singledispatch"][0], + confidence=decorators_map["singledispatch"][1], + ) + elif ( + "singledispatchmethod" in decorators_map + and "staticmethod" in decorators_map + ): + self.add_message( + "singledispatchmethod-function", + node=decorators_map["singledispatchmethod"][0], + confidence=decorators_map["singledispatchmethod"][1], + ) + def _check_redundant_assert(self, node: nodes.Call, infer: InferenceResult) -> None: if ( isinstance(infer, astroid.BoundMethod) diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index b49496af2..b3e376ffe 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -1474,11 +1474,15 @@ def is_registered_in_singledispatch_function(node: nodes.FunctionDef) -> bool: decorators = node.decorators.nodes if node.decorators else [] for decorator in decorators: - # func.register are function calls - if not isinstance(decorator, nodes.Call): + # func.register are function calls or register attributes + # when the function is annotated with types + if isinstance(decorator, nodes.Call): + func = decorator.func + elif isinstance(decorator, nodes.Attribute): + func = decorator + else: continue - func = decorator.func if not isinstance(func, nodes.Attribute) or func.attrname != "register": continue @@ -1493,6 +1497,43 @@ def is_registered_in_singledispatch_function(node: nodes.FunctionDef) -> bool: return False +def find_inferred_fn_from_register(node: nodes.NodeNG) -> nodes.FunctionDef | None: + # func.register are function calls or register attributes + # when the function is annotated with types + if isinstance(node, nodes.Call): + func = node.func + elif isinstance(node, nodes.Attribute): + func = node + else: + return None + + if not isinstance(func, nodes.Attribute) or func.attrname != "register": + return None + + func_def = safe_infer(func.expr) + if not isinstance(func_def, nodes.FunctionDef): + return None + + return func_def + + +def is_registered_in_singledispatchmethod_function(node: nodes.FunctionDef) -> bool: + """Check if the given function node is a singledispatchmethod function.""" + + singledispatchmethod_qnames = ( + "functools.singledispatchmethod", + "singledispatch.singledispatchmethod", + ) + + decorators = node.decorators.nodes if node.decorators else [] + for decorator in decorators: + func_def = find_inferred_fn_from_register(decorator) + if func_def: + return decorated_with(func_def, singledispatchmethod_qnames) + + return False + + def get_node_last_lineno(node: nodes.NodeNG) -> int: """Get the last lineno of the given node. diff --git a/tests/functional/s/singledispatch_method.txt b/tests/functional/s/singledispatch_method.txt new file mode 100644 index 000000000..c747fb6a8 --- /dev/null +++ b/tests/functional/s/singledispatch_method.txt @@ -0,0 +1,3 @@ +singledispatch-method:26:5:26:19:Board.convert_position:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:HIGH +singledispatch-method:31:5:31:30:Board._:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:INFERENCE +singledispatch-method:37:5:37:30:Board._:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:INFERENCE diff --git a/tests/functional/s/singledispatch_method_py37.py b/tests/functional/s/singledispatch_method_py37.py new file mode 100644 index 000000000..c9269f7bf --- /dev/null +++ b/tests/functional/s/singledispatch_method_py37.py @@ -0,0 +1,23 @@ +"""Tests for singledispatch-method""" +# pylint: disable=missing-class-docstring, missing-function-docstring,too-few-public-methods + + +from functools import singledispatch + + +class Board: + @singledispatch # [singledispatch-method] + @classmethod + def convert_position(cls, position): + pass + + @convert_position.register # [singledispatch-method] + @classmethod + def _(cls, position: str) -> tuple: + position_a, position_b = position.split(",") + return (int(position_a), int(position_b)) + + @convert_position.register # [singledispatch-method] + @classmethod + def _(cls, position: tuple) -> str: + return f"{position[0]},{position[1]}" diff --git a/tests/functional/s/singledispatch_method_py37.rc b/tests/functional/s/singledispatch_method_py37.rc new file mode 100644 index 000000000..67a28a36a --- /dev/null +++ b/tests/functional/s/singledispatch_method_py37.rc @@ -0,0 +1,2 @@ +[testoptions] +max_pyver=3.8 diff --git a/tests/functional/s/singledispatch_method_py37.txt b/tests/functional/s/singledispatch_method_py37.txt new file mode 100644 index 000000000..111bc4722 --- /dev/null +++ b/tests/functional/s/singledispatch_method_py37.txt @@ -0,0 +1,3 @@ +singledispatch-method:9:5:9:19:Board.convert_position:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:HIGH +singledispatch-method:14:5:14:30:Board._:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:INFERENCE +singledispatch-method:20:5:20:30:Board._:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:INFERENCE diff --git a/tests/functional/s/singledispatch_method_py38.py b/tests/functional/s/singledispatch_method_py38.py new file mode 100644 index 000000000..ad8eea1dd --- /dev/null +++ b/tests/functional/s/singledispatch_method_py38.py @@ -0,0 +1,40 @@ +"""Tests for singledispatch-method""" +# pylint: disable=missing-class-docstring, missing-function-docstring,too-few-public-methods + + +from functools import singledispatch, singledispatchmethod + + +class BoardRight: + @singledispatchmethod + @classmethod + def convert_position(cls, position): + pass + + @convert_position.register + @classmethod + def _(cls, position: str) -> tuple: + position_a, position_b = position.split(",") + return (int(position_a), int(position_b)) + + @convert_position.register + def _(self, position: tuple) -> str: + return f"{position[0]},{position[1]}" + + +class Board: + @singledispatch # [singledispatch-method] + @classmethod + def convert_position(cls, position): + pass + + @convert_position.register # [singledispatch-method] + @classmethod + def _(cls, position: str) -> tuple: + position_a, position_b = position.split(",") + return (int(position_a), int(position_b)) + + @convert_position.register # [singledispatch-method] + @classmethod + def _(cls, position: tuple) -> str: + return f"{position[0]},{position[1]}" diff --git a/tests/functional/s/singledispatch_method_py38.rc b/tests/functional/s/singledispatch_method_py38.rc new file mode 100644 index 000000000..85fc502b3 --- /dev/null +++ b/tests/functional/s/singledispatch_method_py38.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.8 diff --git a/tests/functional/s/singledispatch_method_py38.txt b/tests/functional/s/singledispatch_method_py38.txt new file mode 100644 index 000000000..c747fb6a8 --- /dev/null +++ b/tests/functional/s/singledispatch_method_py38.txt @@ -0,0 +1,3 @@ +singledispatch-method:26:5:26:19:Board.convert_position:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:HIGH +singledispatch-method:31:5:31:30:Board._:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:INFERENCE +singledispatch-method:37:5:37:30:Board._:singledispatch decorator should not be used with methods, use singledispatchmethod instead.:INFERENCE diff --git a/tests/functional/s/singledispatchmethod_function_py38.py b/tests/functional/s/singledispatchmethod_function_py38.py new file mode 100644 index 000000000..ef44f71c1 --- /dev/null +++ b/tests/functional/s/singledispatchmethod_function_py38.py @@ -0,0 +1,41 @@ +"""Tests for singledispatchmethod-function""" +# pylint: disable=missing-class-docstring, missing-function-docstring,too-few-public-methods + + +from functools import singledispatch, singledispatchmethod + + +class BoardRight: + @singledispatch + @staticmethod + def convert_position(position): + pass + + @convert_position.register + @staticmethod + def _(position: str) -> tuple: + position_a, position_b = position.split(",") + return (int(position_a), int(position_b)) + + @convert_position.register + @staticmethod + def _(position: tuple) -> str: + return f"{position[0]},{position[1]}" + + +class Board: + @singledispatchmethod # [singledispatchmethod-function] + @staticmethod + def convert_position(position): + pass + + @convert_position.register # [singledispatchmethod-function] + @staticmethod + def _(position: str) -> tuple: + position_a, position_b = position.split(",") + return (int(position_a), int(position_b)) + + @convert_position.register # [singledispatchmethod-function] + @staticmethod + def _(position: tuple) -> str: + return f"{position[0]},{position[1]}" diff --git a/tests/functional/s/singledispatchmethod_function_py38.rc b/tests/functional/s/singledispatchmethod_function_py38.rc new file mode 100644 index 000000000..85fc502b3 --- /dev/null +++ b/tests/functional/s/singledispatchmethod_function_py38.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.8 diff --git a/tests/functional/s/singledispatchmethod_function_py38.txt b/tests/functional/s/singledispatchmethod_function_py38.txt new file mode 100644 index 000000000..4c236b346 --- /dev/null +++ b/tests/functional/s/singledispatchmethod_function_py38.txt @@ -0,0 +1,3 @@ +singledispatchmethod-function:27:5:27:25:Board.convert_position:singledispatchmethod decorator should not be used with functions, use singledispatch instead.:HIGH +singledispatchmethod-function:32:5:32:30:Board._:singledispatchmethod decorator should not be used with functions, use singledispatch instead.:INFERENCE +singledispatchmethod-function:38:5:38:30:Board._:singledispatchmethod decorator should not be used with functions, use singledispatch instead.:INFERENCE |