diff options
author | Joe Young <80432516+jpy-git@users.noreply.github.com> | 2022-03-30 11:47:30 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-03-30 12:47:30 +0200 |
commit | 1f0b406d3e7b7e670ff7f887c9ed25228da220d2 (patch) | |
tree | c3ef11d8056bd4245bd37d93aab8180792cb91b8 | |
parent | 5324fecb32b0c86a70c88fc196360d721085fdfc (diff) | |
download | pylint-git-1f0b406d3e7b7e670ff7f887c9ed25228da220d2.tar.gz |
C2801: New check for manual __dunder__ methods (#5938)
Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
Co-authored-by: Daniƫl van Noord <13665637+DanielNoord@users.noreply.github.com>
-rw-r--r-- | ChangeLog | 4 | ||||
-rw-r--r-- | doc/data/messages/u/unnecessary-dunder-call/bad.py | 6 | ||||
-rw-r--r-- | doc/data/messages/u/unnecessary-dunder-call/good.py | 6 | ||||
-rw-r--r-- | doc/whatsnew/2.14.rst | 4 | ||||
-rw-r--r-- | pylint/checkers/base_checker.py | 2 | ||||
-rw-r--r-- | pylint/checkers/dunder_methods.py | 151 | ||||
-rw-r--r-- | tests/functional/a/assigning/assigning_non_slot.py | 2 | ||||
-rw-r--r-- | tests/functional/c/class_members_py30.py | 2 | ||||
-rw-r--r-- | tests/functional/g/generic_alias/generic_alias_typing.py | 2 | ||||
-rw-r--r-- | tests/functional/i/inner_classes.py | 2 | ||||
-rw-r--r-- | tests/functional/n/non/non_init_parent_called.py | 2 | ||||
-rw-r--r-- | tests/functional/t/too/too_many_arguments.py | 2 | ||||
-rw-r--r-- | tests/functional/u/unnecessary/unnecessary_dunder_call.py | 46 | ||||
-rw-r--r-- | tests/functional/u/unnecessary/unnecessary_dunder_call.txt | 3 |
14 files changed, 227 insertions, 7 deletions
@@ -10,6 +10,10 @@ Release date: TBA Put new features here and also in 'doc/whatsnew/2.14.rst' +* Add new check ``unnecessary-dunder-call`` for unnecessary dunder method calls. + + Closes #5936 + * ``potential-index-error``: Emitted when the index of a list or tuple exceeds its length. This checker is currently quite conservative to avoid false positives. We welcome suggestions for improvements. diff --git a/doc/data/messages/u/unnecessary-dunder-call/bad.py b/doc/data/messages/u/unnecessary-dunder-call/bad.py new file mode 100644 index 000000000..66149d772 --- /dev/null +++ b/doc/data/messages/u/unnecessary-dunder-call/bad.py @@ -0,0 +1,6 @@ +three = 3.0.__str__() # [unnecessary-dunder-call] +twelve = "1".__add__("2") # [unnecessary-dunder-call] + + +def is_bigger_than_two(x): + return x.__gt__(2) # [unnecessary-dunder-call] diff --git a/doc/data/messages/u/unnecessary-dunder-call/good.py b/doc/data/messages/u/unnecessary-dunder-call/good.py new file mode 100644 index 000000000..fe41db776 --- /dev/null +++ b/doc/data/messages/u/unnecessary-dunder-call/good.py @@ -0,0 +1,6 @@ +three = str(3.0) +twelve = "1" + "2" + + +def is_bigger_than_two(x): + return x > 2 diff --git a/doc/whatsnew/2.14.rst b/doc/whatsnew/2.14.rst index ad38200e7..ecb7388de 100644 --- a/doc/whatsnew/2.14.rst +++ b/doc/whatsnew/2.14.rst @@ -12,6 +12,10 @@ Summary -- Release highlights New checkers ============ +* Add new check ``unnecessary-dunder-call`` for unnecessary dunder method calls. + + Closes #5936 + * ``potential-index-error``: Emitted when the index of a list or tuple exceeds its length. This checker is currently quite conservative to avoid false positives. We welcome suggestions for improvements. diff --git a/pylint/checkers/base_checker.py b/pylint/checkers/base_checker.py index 21c63364a..31e178b32 100644 --- a/pylint/checkers/base_checker.py +++ b/pylint/checkers/base_checker.py @@ -61,7 +61,7 @@ class BaseChecker(OptionsProviderMixIn): def __gt__(self, other): """Permit to sort a list of Checker by name.""" - return f"{self.name}{self.msgs}".__gt__(f"{other.name}{other.msgs}") + return f"{self.name}{self.msgs}" > (f"{other.name}{other.msgs}") def __repr__(self): status = "Checker" if self.enabled else "Disabled checker" diff --git a/pylint/checkers/dunder_methods.py b/pylint/checkers/dunder_methods.py new file mode 100644 index 000000000..4c260c34a --- /dev/null +++ b/pylint/checkers/dunder_methods.py @@ -0,0 +1,151 @@ +# 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 typing import TYPE_CHECKING + +from astroid import nodes + +from pylint.checkers import BaseChecker +from pylint.interfaces import HIGH, IAstroidChecker + +if TYPE_CHECKING: + from pylint.lint import PyLinter + + +class DunderCallChecker(BaseChecker): + """Check for unnecessary dunder method calls. + + Docs: https://docs.python.org/3/reference/datamodel.html#basic-customization + We exclude __init__, __new__, __subclasses__, __init_subclass__, + __set_name__, __class_getitem__, __missing__, __exit__, __await__, + __del__, __aexit__, __getnewargs_ex__, __getnewargs__, __getstate__, + __setstate__, __reduce__, __reduce_ex__ + since these either have no alternative method of being called or + have a genuine use case for being called manually. + """ + + __implements__ = IAstroidChecker + + includedict = { + "__repr__": "Use repr built-in function", + "__str__": "Use str built-in function", + "__bytes__": "Use bytes built-in function", + "__format__": "Use format built-in function, format string method, or f-string", + "__lt__": "Use < operator", + "__le__": "Use <= operator", + "__eq__": "Use == operator", + "__ne__": "Use != operator", + "__gt__": "Use > operator", + "__ge__": "Use >= operator", + "__hash__": "Use hash built-in function", + "__bool__": "Use bool built-in function", + "__getattr__": "Access attribute directly or use getattr built-in function", + "__getattribute__": "Access attribute directly or use getattr built-in function", + "__setattr__": "Set attribute directly or use setattr built-in function", + "__delattr__": "Use del keyword", + "__dir__": "Use dir built-in function", + "__get__": "Use get method", + "__set__": "Use set method", + "__delete__": "Use del keyword", + "__instancecheck__": "Use isinstance built-in function", + "__subclasscheck__": "Use issubclass built-in function", + "__call__": "Invoke instance directly", + "__len__": "Use len built-in function", + "__length_hint__": "Use length_hint method", + "__getitem__": "Access item via subscript", + "__setitem__": "Set item via subscript", + "__delitem__": "Use del keyword", + "__iter__": "Use iter built-in function", + "__next__": "Use next built-in function", + "__reversed__": "Use reversed built-in funciton", + "__contains__": "Use in keyword", + "__add__": "Use + operator", + "__sub__": "Use - operator", + "__mul__": "Use * operator", + "__matmul__": "Use @ operator", + "__truediv__": "Use / operator", + "__floordiv__": "Use // operator", + "__mod__": "Use % operator", + "__divmod__": "Use divmod built-in function", + "__pow__": "Use ** operator or pow built-in function", + "__lshift__": "Use << operator", + "__rshift__": "Use >> operator", + "__and__": "Use & operator", + "__xor__": "Use ^ operator", + "__or__": "Use | operator", + "__radd__": "Use + operator", + "__rsub__": "Use - operator", + "__rmul__": "Use * operator", + "__rmatmul__": "Use @ operator", + "__rtruediv__": "Use / operator", + "__rfloordiv__": "Use // operator", + "__rmod__": "Use % operator", + "__rdivmod__": "Use divmod built-in function", + "__rpow__": "Use ** operator or pow built-in function", + "__rlshift__": "Use << operator", + "__rrshift__": "Use >> operator", + "__rand__": "Use & operator", + "__rxor__": "Use ^ operator", + "__ror__": "Use | operator", + "__iadd__": "Use += operator", + "__isub__": "Use -= operator", + "__imul__": "Use *= operator", + "__imatmul__": "Use @= operator", + "__itruediv__": "Use /= operator", + "__ifloordiv__": "Use //= operator", + "__imod__": "Use %= operator", + "__ipow__": "Use **= operator", + "__ilshift__": "Use <<= operator", + "__irshift__": "Use >>= operator", + "__iand__": "Use &= operator", + "__ixor__": "Use ^= operator", + "__ior__": "Use |= operator", + "__neg__": "Multiply by -1 instead", + "__pos__": "Multiply by +1 instead", + "__abs__": "Use abs built-in function", + "__invert__": "Use ~ operator", + "__complex__": "Use complex built-in function", + "__int__": "Use int built-in function", + "__float__": "Use float built-in function", + "__index__": "Use index method", + "__round__": "Use round built-in function", + "__trunc__": "Use math.trunc function", + "__floor__": "Use math.floor function", + "__ceil__": "Use math.ceil function", + "__enter__": "Invoke context manager directly", + "__aiter__": "Use iter built-in function", + "__anext__": "Use next built-in function", + "__aenter__": "Invoke context manager directly", + "__copy__": "Use copy.copy function", + "__deepcopy__": "Use copy.deepcopy function", + "__fspath__": "Use os.fspath function instead", + } + name = "unnecessary-dunder-call" + priority = -1 + msgs = { + "C2801": ( + "Unnecessarily calls dunder method %s. %s.", + "unnecessary-dunder-call", + "Used when a dunder method is manually called instead " + "of using the corresponding function/method/operator.", + ), + } + options = () + + def visit_call(self, node: nodes.Call) -> None: + """Check if method being called is an unnecessary dunder method.""" + if ( + isinstance(node.func, nodes.Attribute) + and node.func.attrname in self.includedict + ): + self.add_message( + "unnecessary-dunder-call", + node=node, + args=(node.func.attrname, self.includedict[node.func.attrname]), + confidence=HIGH, + ) + + +def register(linter: "PyLinter") -> None: + linter.register_checker(DunderCallChecker(linter)) diff --git a/tests/functional/a/assigning/assigning_non_slot.py b/tests/functional/a/assigning/assigning_non_slot.py index 2cd1483e0..0a0c0427c 100644 --- a/tests/functional/a/assigning/assigning_non_slot.py +++ b/tests/functional/a/assigning/assigning_non_slot.py @@ -1,7 +1,7 @@ """ Checks assigning attributes not found in class slots will trigger assigning-non-slot warning. """ -# pylint: disable=too-few-public-methods, no-init, missing-docstring, import-error, useless-object-inheritance, redundant-u-string-prefix +# pylint: disable=too-few-public-methods, no-init, missing-docstring, import-error, useless-object-inheritance, redundant-u-string-prefix, unnecessary-dunder-call from collections import deque from missing import Unknown diff --git a/tests/functional/c/class_members_py30.py b/tests/functional/c/class_members_py30.py index cb7267ce5..2e34127e3 100644 --- a/tests/functional/c/class_members_py30.py +++ b/tests/functional/c/class_members_py30.py @@ -1,5 +1,5 @@ """ Various tests for class members access. """
-# pylint: disable=too-few-public-methods,import-error,no-init,missing-docstring, wrong-import-position,wrong-import-order, useless-object-inheritance
+# pylint: disable=too-few-public-methods,import-error,no-init,missing-docstring, wrong-import-position,wrong-import-order, useless-object-inheritance, unnecessary-dunder-call
from missing import Missing
class MyClass(object):
"""class docstring"""
diff --git a/tests/functional/g/generic_alias/generic_alias_typing.py b/tests/functional/g/generic_alias/generic_alias_typing.py index 2ce05deaf..6d114739a 100644 --- a/tests/functional/g/generic_alias/generic_alias_typing.py +++ b/tests/functional/g/generic_alias/generic_alias_typing.py @@ -1,7 +1,7 @@ """Test generic alias support for typing.py types.""" # flake8: noqa # pylint: disable=missing-docstring,pointless-statement -# pylint: disable=too-few-public-methods,multiple-statements,line-too-long +# pylint: disable=too-few-public-methods,multiple-statements,line-too-long, unnecessary-dunder-call import abc import typing diff --git a/tests/functional/i/inner_classes.py b/tests/functional/i/inner_classes.py index 7ce8101f0..cabae5734 100644 --- a/tests/functional/i/inner_classes.py +++ b/tests/functional/i/inner_classes.py @@ -1,4 +1,4 @@ -# pylint: disable=too-few-public-methods, useless-object-inheritance, unnecessary-pass +# pylint: disable=too-few-public-methods, useless-object-inheritance, unnecessary-pass, unnecessary-dunder-call """Backend Base Classes for the schwelm user DB""" __revision__ = "alpha" diff --git a/tests/functional/n/non/non_init_parent_called.py b/tests/functional/n/non/non_init_parent_called.py index 97489accf..7ad3f1932 100644 --- a/tests/functional/n/non/non_init_parent_called.py +++ b/tests/functional/n/non/non_init_parent_called.py @@ -1,5 +1,5 @@ # pylint: disable=protected-access,import-self,too-few-public-methods,line-too-long -# pylint: disable=wrong-import-order, useless-object-inheritance, +# pylint: disable=wrong-import-order, useless-object-inheritance, unnecessary-dunder-call """test for call to __init__ from a non ancestor class """ from __future__ import print_function diff --git a/tests/functional/t/too/too_many_arguments.py b/tests/functional/t/too/too_many_arguments.py index 689745e27..8d26902af 100644 --- a/tests/functional/t/too/too_many_arguments.py +++ b/tests/functional/t/too/too_many_arguments.py @@ -1,4 +1,4 @@ -# pylint: disable=missing-docstring,wrong-import-position +# pylint: disable=missing-docstring,wrong-import-position,unnecessary-dunder-call def stupid_function(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9): # [too-many-arguments] return arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9 diff --git a/tests/functional/u/unnecessary/unnecessary_dunder_call.py b/tests/functional/u/unnecessary/unnecessary_dunder_call.py new file mode 100644 index 000000000..341353a82 --- /dev/null +++ b/tests/functional/u/unnecessary/unnecessary_dunder_call.py @@ -0,0 +1,46 @@ +"""Checks for unnecessary-dunder-call.""" +# pylint: disable=too-few-public-methods, undefined-variable, useless-object-inheritance +# pylint: disable=missing-class-docstring, missing-function-docstring + +# Test includelisted dunder methods raise lint when manually called. +num_str = some_num.__str__() # [unnecessary-dunder-call] +num_repr = some_num.__add__(2) # [unnecessary-dunder-call] +my_repr = my_module.my_object.__repr__() # [unnecessary-dunder-call] + +# Test unknown/user-defined dunder methods don't raise lint. +my_woohoo = my_object.__woohoo__() + +# Test allowed dunder methods don't raise lint. +class Foo1(object): + def __init__(self): + object.__init__(self) + +class Foo2(object): + def __init__(self): + super().__init__(self) + +class Bar1(object): + def __new__(cls): + object.__new__(cls) + +class Bar2(object): + def __new__(cls): + super().__new__(cls) + +class Base: + @classmethod + def get_first_subclass(cls): + for subklass in cls.__subclasses__(): + return subklass + return object + +class PluginBase(object): + subclasses = [] + + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + cls.subclasses.append(cls) + +# Test no lint raised for attributes. +my_instance_name = x.__class__.__name__ +my_pkg_version = pkg.__version__ diff --git a/tests/functional/u/unnecessary/unnecessary_dunder_call.txt b/tests/functional/u/unnecessary/unnecessary_dunder_call.txt new file mode 100644 index 000000000..208ad96bb --- /dev/null +++ b/tests/functional/u/unnecessary/unnecessary_dunder_call.txt @@ -0,0 +1,3 @@ +unnecessary-dunder-call:6:10:6:28::Unnecessarily calls dunder method __str__. Use str built-in function.:HIGH +unnecessary-dunder-call:7:11:7:30::Unnecessarily calls dunder method __add__. Use + operator.:HIGH +unnecessary-dunder-call:8:10:8:40::Unnecessarily calls dunder method __repr__. Use repr built-in function.:HIGH |