Skip to content

Commit 9ef0fe6

Browse files
authored
Detect __eq__ and __ne__ methods that use Any instead of object (#163)
1 parent 3c9a136 commit 9ef0fe6

File tree

4 files changed

+44
-14
lines changed

4 files changed

+44
-14
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ Bugfixes:
1010
* fix bug where `TypeVar`s were erroneously flagged as unused if they were only used in
1111
a `typing.Union` subscript.
1212

13+
Features:
14+
* introduce Y032 (prefer `object` to `Any` for the second argument in `__eq__` and
15+
`__ne__` methods).
16+
1317
## 22.1.0
1418

1519
* extend Y001 to cover `ParamSpec` and `TypeVarTuple` in addition to `TypeVar`

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ currently emitted:
6363
| Y029 | It is almost always redundant to define `__str__` or `__repr__` in a stub file, as the signatures are almost always identical to `object.__str__` and `object.__repr__`.
6464
| Y030 | Union expressions should never have more than one `Literal` member, as `Literal[1] \| Literal[2]` is semantically identical to `Literal[1, 2]`.
6565
| Y031 | `TypedDict`s should use class-based syntax instead of assignment-based syntax wherever possible. (In situations where this is not possible, such as if a field is a Python keyword or an invalid identifier, this error will not be raised.)
66+
| Y032 | The second argument of an `__eq__` or `__ne__` method should usually be annotated with `object` rather than `Any`.
6667

6768
Many error codes enforce modern conventions, and some cannot yet be used in
6869
all cases:

pyi.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ def _is_object(node: ast.expr, name: str, *, from_: Container[str]) -> bool:
283283
_is_TypedDict = partial(_is_object, name="TypedDict", from_=_TYPING_MODULES)
284284
_is_Literal = partial(_is_object, name="Literal", from_=_TYPING_MODULES)
285285
_is_abstractmethod = partial(_is_object, name="abstractmethod", from_={"abc"})
286+
_is_Any = partial(_is_object, name="Any", from_={"typing"})
286287

287288

288289
def _unparse_assign_node(node: ast.Assign | ast.AnnAssign) -> str:
@@ -806,25 +807,36 @@ def visit_ClassDef(self, node: ast.ClassDef) -> None:
806807
):
807808
self.error(statement, Y013)
808809

809-
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
810+
def _visit_method(self, node: ast.FunctionDef) -> None:
811+
method_name = node.name
812+
all_args = node.args
813+
814+
if all_args.kwonlyargs:
815+
return
816+
817+
# pos-only args don't exist on 3.7
818+
pos_only_args: list[ast.arg] = getattr(all_args, "posonlyargs", [])
819+
pos_or_kwd_args = all_args.args
820+
non_kw_only_args = pos_only_args + pos_or_kwd_args
821+
810822
# Raise an error for defining __str__ or __repr__ on a class, but only if:
811823
# 1). The method is not decorated with @abstractmethod
812824
# 2). The method has the exact same signature as object.__str__/object.__repr__
813-
if (
814-
self.in_class.active
815-
and node.name in {"__repr__", "__str__"}
816-
and _is_name(node.returns, "str")
817-
and not any(_is_abstractmethod(deco) for deco in node.decorator_list)
818-
):
819-
all_args = node.args
820-
# pos-only args don't exist on 3.7
821-
pos_only_args: list[ast.arg] = getattr(all_args, "posonlyargs", [])
822-
pos_or_kwd_args = all_args.args
823-
kwd_only_args = all_args.kwonlyargs
824-
825-
if ((len(pos_only_args) + len(pos_or_kwd_args)) == 1) and not kwd_only_args:
825+
if method_name in {"__repr__", "__str__"}:
826+
if (
827+
len(non_kw_only_args) == 1
828+
and _is_name(node.returns, "str")
829+
and not any(_is_abstractmethod(deco) for deco in node.decorator_list)
830+
):
826831
self.error(node, Y029)
827832

833+
elif method_name in {"__eq__", "__ne__"}:
834+
if len(non_kw_only_args) == 2 and _is_Any(non_kw_only_args[1].annotation):
835+
self.error(node, Y032.format(method_name=method_name))
836+
837+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
838+
if self.in_class.active:
839+
self._visit_method(node)
828840
self._visit_function(node)
829841

830842
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
@@ -1064,3 +1076,6 @@ def parse_options(cls, optmanager, options, extra_args) -> None:
10641076
Y029 = "Y029 Defining __repr__ or __str__ in a stub is almost always redundant"
10651077
Y030 = "Y030 Multiple Literal members in a union. {suggestion}"
10661078
Y031 = "Y031 Use class-based syntax for TypedDicts where possible"
1079+
Y032 = (
1080+
'Y032 Prefer "object" to "Any" for the second parameter in "{method_name}" methods'
1081+
)

tests/classdefs.pyi

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
11
import abc
2+
import typing
23
from abc import abstractmethod
4+
from typing import Any
35

46
class Bad:
57
def __repr__(self) -> str: ... # Y029 Defining __repr__ or __str__ in a stub is almost always redundant
68
def __str__(self) -> str: ... # Y029 Defining __repr__ or __str__ in a stub is almost always redundant
9+
def __eq__(self, other: Any) -> bool: ... # Y032 Prefer "object" to "Any" for the second parameter in "__eq__" methods
10+
def __ne__(self, other: typing.Any) -> typing.Any: ... # Y032 Prefer "object" to "Any" for the second parameter in "__ne__" methods
711

812
class Good:
913
@abstractmethod
1014
def __str__(self) -> str: ...
1115
@abc.abstractmethod
1216
def __repr__(self) -> str: ...
17+
def __eq__(self, other: object) -> bool: ...
18+
def __ne__(self, obj: object) -> int: ...
1319

1420
class Fine:
1521
@abc.abstractmethod
1622
def __str__(self) -> str: ...
1723
@abc.abstractmethod
1824
def __repr__(self) -> str: ...
25+
def __eq__(self, other: Any, strange_extra_arg: list[str]) -> Any: ...
26+
def __ne__(self, *, kw_only_other: Any) -> bool: ...
1927

2028
class AlsoGood(str):
2129
def __str__(self) -> AlsoGood: ...
@@ -27,3 +35,5 @@ class FineAndDandy:
2735

2836
def __repr__(self) -> str: ...
2937
def __str__(self) -> str: ...
38+
def __eq__(self, other: Any) -> bool: ...
39+
def __ne__(self, other: Any) -> bool: ...

0 commit comments

Comments
 (0)