diff --git a/pylint/checkers/python3.py b/pylint/checkers/python3.py new file mode 100644 index 00000000000..f566b122719 --- /dev/null +++ b/pylint/checkers/python3.py @@ -0,0 +1,39 @@ +# 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 + +"""This is the remnant of the python3 checker. It was removed because +the transition from python 2 to python3 is behind us, but some checks +are still useful in python3 after all. +See https://github.com/PyCQA/pylint/issues/5025 +""" + + +from pylint import checkers, interfaces +from pylint.checkers import utils + + +class Python3Checker(checkers.BaseChecker): + + __implements__ = interfaces.IAstroidChecker + enabled = True + name = "python3" + + msgs = { + "W1641": ( + "Implementing __eq__ without also implementing __hash__", + "eq-without-hash", + "Used when a class implements __eq__ but not __hash__. In Python 2, objects " + "get object.__hash__ as the default implementation, in Python 3 objects get " + "None as their default __hash__ implementation if they also implement __eq__.", + ), + } + + @utils.check_messages("eq-without-hash") + def visit_classdef(self, node): + locals_and_methods = set(node.locals).union(x.name for x in node.mymethods()) + if "__eq__" in locals_and_methods and "__hash__" not in locals_and_methods: + self.add_message("eq-without-hash", node=node) + + +def register(linter): + linter.register_checker(Python3Checker(linter)) diff --git a/pylint/constants.py b/pylint/constants.py index c3b94863dc5..eb0a6183ad6 100644 --- a/pylint/constants.py +++ b/pylint/constants.py @@ -80,8 +80,9 @@ class DeletedMessage(NamedTuple): old_names: List[Tuple[str, str]] = [] -DELETED_MSGID_PREFIXES = [ - 16, # the PY3K+ checker, see https://github.com/PyCQA/pylint/pull/4942 +DELETED_MSGID_PREFIXES: List[int] = [ + # The PY3K+ checker was deleted see https://github.com/PyCQA/pylint/pull/4942 + # And then reinstated see https://github.com/PyCQA/pylint/issues/5025 ] DELETED_MESSAGES = [ @@ -139,7 +140,6 @@ class DeletedMessage(NamedTuple): DeletedMessage("W1638", "range-builtin-not-iterating"), DeletedMessage("W1639", "filter-builtin-not-iterating"), DeletedMessage("W1640", "using-cmp-argument"), - DeletedMessage("W1641", "eq-without-hash"), DeletedMessage("W1642", "div-method"), DeletedMessage("W1643", "idiv-method"), DeletedMessage("W1644", "rdiv-method"), diff --git a/tests/functional/a/access/access_to_protected_members.py b/tests/functional/a/access/access_to_protected_members.py index 7d732dcc562..f5118b667b2 100644 --- a/tests/functional/a/access/access_to_protected_members.py +++ b/tests/functional/a/access/access_to_protected_members.py @@ -59,7 +59,7 @@ def incorrect_access(self): return None -class Issue1802(object): +class Issue1802(object): # [eq-without-hash] """Test for GitHub issue 1802""" def __init__(self, value): self._foo = value diff --git a/tests/functional/a/access/access_to_protected_members.txt b/tests/functional/a/access/access_to_protected_members.txt index d9dfd96ce11..ab00aea3a3e 100644 --- a/tests/functional/a/access/access_to_protected_members.txt +++ b/tests/functional/a/access/access_to_protected_members.txt @@ -4,6 +4,7 @@ protected-access:42:6:42:21::Access to a protected member _protected of a client protected-access:43:0:43:19::Access to a protected member _cls_protected of a client class:UNDEFINED protected-access:44:6:44:25::Access to a protected member _cls_protected of a client class:UNDEFINED protected-access:58:19:58:40:Issue1031.incorrect_access:Access to a protected member _protected of a client class:UNDEFINED +eq-without-hash:62:0:101:20:Issue1802:Implementing __eq__ without also implementing __hash__:UNDEFINED protected-access:72:48:72:63:Issue1802.__eq__:Access to a protected member __private of a client class:UNDEFINED protected-access:80:32:80:42:Issue1802.not_in_special:Access to a protected member _foo of a client class:UNDEFINED protected-access:100:32:100:42:Issue1802.__fake_special__:Access to a protected member _foo of a client class:UNDEFINED diff --git a/tests/functional/e/eq_without_hash.py b/tests/functional/e/eq_without_hash.py new file mode 100644 index 00000000000..62682b01927 --- /dev/null +++ b/tests/functional/e/eq_without_hash.py @@ -0,0 +1,11 @@ +"""Regression test for #5025""" + +# pylint: disable=invalid-name,missing-docstring, too-few-public-methods + + +class AClass: # [eq-without-hash] + def __init__(self) -> None: + self.x = 5 + + def __eq__(self, other: object) -> bool: + return isinstance(other, AClass) and other.x == self.x diff --git a/tests/functional/e/eq_without_hash.txt b/tests/functional/e/eq_without_hash.txt new file mode 100644 index 00000000000..b56a1218ad8 --- /dev/null +++ b/tests/functional/e/eq_without_hash.txt @@ -0,0 +1 @@ +eq-without-hash:6:0:11:62:AClass:Implementing __eq__ without also implementing __hash__:UNDEFINED diff --git a/tests/functional/m/missing/missing_function_docstring_rgx.py b/tests/functional/m/missing/missing_function_docstring_rgx.py index 3c910d1a99b..56ab7cf4848 100644 --- a/tests/functional/m/missing/missing_function_docstring_rgx.py +++ b/tests/functional/m/missing/missing_function_docstring_rgx.py @@ -6,6 +6,6 @@ class MyClass: pass -class Child(MyClass): +class Child(MyClass): # [eq-without-hash] def __eq__(self, other): # [missing-function-docstring] return True diff --git a/tests/functional/m/missing/missing_function_docstring_rgx.txt b/tests/functional/m/missing/missing_function_docstring_rgx.txt index ee085ec067b..e0ee03af551 100644 --- a/tests/functional/m/missing/missing_function_docstring_rgx.txt +++ b/tests/functional/m/missing/missing_function_docstring_rgx.txt @@ -1 +1,2 @@ +eq-without-hash:9:0:11:19:Child:Implementing __eq__ without also implementing __hash__:UNDEFINED missing-function-docstring:10:4:11:19:Child.__eq__:Missing function or method docstring:INFERENCE diff --git a/tests/functional/n/name/name_preset_snake_case.py b/tests/functional/n/name/name_preset_snake_case.py index 75350497e4f..26e9dad5806 100644 --- a/tests/functional/n/name/name_preset_snake_case.py +++ b/tests/functional/n/name/name_preset_snake_case.py @@ -10,7 +10,7 @@ def say_hello(some_argument): return [some_argument * some_value for some_value in range(10)] -class MyClass: # [invalid-name] +class MyClass: # [invalid-name, eq-without-hash]] def __init__(self, arg_x): self._my_secret_x = arg_x diff --git a/tests/functional/n/name/name_preset_snake_case.txt b/tests/functional/n/name/name_preset_snake_case.txt index 664b2db90e9..2ae7b8744a6 100644 --- a/tests/functional/n/name/name_preset_snake_case.txt +++ b/tests/functional/n/name/name_preset_snake_case.txt @@ -1,4 +1,5 @@ invalid-name:6:0:6:13::"Constant name ""SOME_CONSTANT"" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]*|__.*__)$' pattern)":HIGH +eq-without-hash:13:0:22:83:MyClass:Implementing __eq__ without also implementing __hash__:UNDEFINED invalid-name:13:0:22:83:MyClass:"Class name ""MyClass"" doesn't conform to snake_case naming style ('[^\\W\\dA-Z][^\\WA-Z]+$' pattern)":HIGH invalid-name:25:0:26:8:sayHello:"Function name ""sayHello"" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]{2,}|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)":HIGH invalid-name:29:0:31:22:FooEnum:"Class name ""FooEnum"" doesn't conform to snake_case naming style ('[^\\W\\dA-Z][^\\WA-Z]+$' pattern)":HIGH diff --git a/tests/functional/n/namePresetCamelCase.py b/tests/functional/n/namePresetCamelCase.py index e048ec4d8a8..a442be76ca4 100644 --- a/tests/functional/n/namePresetCamelCase.py +++ b/tests/functional/n/namePresetCamelCase.py @@ -7,7 +7,7 @@ def sayHello(someArgument): return [someArgument * someValue for someValue in range(10)] -class MyClass: # [invalid-name] +class MyClass: # [invalid-name, eq-without-hash] def __init__(self, argX): self._mySecretX = argX diff --git a/tests/functional/n/namePresetCamelCase.txt b/tests/functional/n/namePresetCamelCase.txt index 7b9ec862317..bf4f7f52795 100644 --- a/tests/functional/n/namePresetCamelCase.txt +++ b/tests/functional/n/namePresetCamelCase.txt @@ -1,3 +1,4 @@ invalid-name:3:0:3:13::"Constant name ""SOME_CONSTANT"" doesn't conform to camelCase naming style ('([^\\W\\dA-Z][^\\W_]*|__.*__)$' pattern)":HIGH +eq-without-hash:10:0:19:79:MyClass:Implementing __eq__ without also implementing __hash__:UNDEFINED invalid-name:10:0:19:79:MyClass:"Class name ""MyClass"" doesn't conform to camelCase naming style ('[^\\W\\dA-Z][^\\W_]+$' pattern)":HIGH invalid-name:22:0:23:8:say_hello:"Function name ""say_hello"" doesn't conform to camelCase naming style ('([^\\W\\dA-Z][^\\W_]{2,}|__[^\\W\\dA-Z_]\\w+__)$' pattern)":HIGH diff --git a/tests/functional/p/protected_access_special_methods_off.py b/tests/functional/p/protected_access_special_methods_off.py index 8747b425e9d..714bcec5e3c 100644 --- a/tests/functional/p/protected_access_special_methods_off.py +++ b/tests/functional/p/protected_access_special_methods_off.py @@ -6,7 +6,7 @@ # pylint: disable=too-few-public-methods -class Protected: +class Protected: # [eq-without-hash] """A class""" def __init__(self): diff --git a/tests/functional/p/protected_access_special_methods_off.txt b/tests/functional/p/protected_access_special_methods_off.txt index ac9f6c66e3d..1f27d7a91c0 100644 --- a/tests/functional/p/protected_access_special_methods_off.txt +++ b/tests/functional/p/protected_access_special_methods_off.txt @@ -1,3 +1,4 @@ +eq-without-hash:9:0:23:40:Protected:Implementing __eq__ without also implementing __hash__:UNDEFINED unused-private-member:15:8:15:22:Protected.__init__:Unused private member `Protected.__private`:UNDEFINED protected-access:22:22:22:38:Protected._fake_special_:Access to a protected member _protected of a client class:UNDEFINED protected-access:23:25:23:40:Protected._fake_special_:Access to a protected member __private of a client class:UNDEFINED diff --git a/tests/functional/p/protected_access_special_methods_on.py b/tests/functional/p/protected_access_special_methods_on.py index 84488f44231..659fbe60270 100644 --- a/tests/functional/p/protected_access_special_methods_on.py +++ b/tests/functional/p/protected_access_special_methods_on.py @@ -6,7 +6,7 @@ # pylint: disable=too-few-public-methods -class Protected: +class Protected: # [eq-without-hash] """A class""" def __init__(self): diff --git a/tests/functional/p/protected_access_special_methods_on.txt b/tests/functional/p/protected_access_special_methods_on.txt index 0784dfbea6c..f7969457d58 100644 --- a/tests/functional/p/protected_access_special_methods_on.txt +++ b/tests/functional/p/protected_access_special_methods_on.txt @@ -1,3 +1,4 @@ +eq-without-hash:9:0:23:40:Protected:Implementing __eq__ without also implementing __hash__:UNDEFINED unused-private-member:15:8:15:22:Protected.__init__:Unused private member `Protected.__private`:UNDEFINED protected-access:18:26:18:42:Protected.__eq__:Access to a protected member _protected of a client class:UNDEFINED protected-access:22:22:22:38:Protected._fake_special_:Access to a protected member _protected of a client class:UNDEFINED