summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRamon Saraiva <ramonsaraiva@gmail.com>2022-10-31 16:26:57 -0300
committerGitHub <noreply@github.com>2022-10-31 20:26:57 +0100
commit6c81cc93e6b79a2564e4ce02088a34a7ec2bbfaa (patch)
tree440b679f5d72cb647423c9174d3b04f1d7e3812c
parent35ce253048ce58e20601c4ad7e96fde8a10d1c2f (diff)
downloadpylint-git-6c81cc93e6b79a2564e4ce02088a34a7ec2bbfaa.tar.gz
@singledispatch and @singledispatchmethod checks for methods and functions (#7575)
Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
-rw-r--r--.pyenchant_pylint_custom_dict.txt1
-rw-r--r--doc/data/messages/s/singledispatch-method/bad.py19
-rw-r--r--doc/data/messages/s/singledispatch-method/details.rst0
-rw-r--r--doc/data/messages/s/singledispatch-method/good.py19
-rw-r--r--doc/data/messages/s/singledispatchmethod-function/bad.py19
-rw-r--r--doc/data/messages/s/singledispatchmethod-function/details.rst0
-rw-r--r--doc/data/messages/s/singledispatchmethod-function/good.py18
-rw-r--r--doc/user_guide/checkers/features.rst6
-rw-r--r--doc/user_guide/messages/messages_overview.rst2
-rw-r--r--doc/whatsnew/fragments/6917.new_check4
-rw-r--r--pylint/checkers/stdlib.py51
-rw-r--r--pylint/checkers/utils.py47
-rw-r--r--tests/functional/s/singledispatch_method.txt3
-rw-r--r--tests/functional/s/singledispatch_method_py37.py23
-rw-r--r--tests/functional/s/singledispatch_method_py37.rc2
-rw-r--r--tests/functional/s/singledispatch_method_py37.txt3
-rw-r--r--tests/functional/s/singledispatch_method_py38.py40
-rw-r--r--tests/functional/s/singledispatch_method_py38.rc2
-rw-r--r--tests/functional/s/singledispatch_method_py38.txt3
-rw-r--r--tests/functional/s/singledispatchmethod_function_py38.py41
-rw-r--r--tests/functional/s/singledispatchmethod_function_py38.rc2
-rw-r--r--tests/functional/s/singledispatchmethod_function_py38.txt3
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