diff --git a/mypy/checker.py b/mypy/checker.py index fc9733117a0a..f9e9b9c0e95c 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2234,6 +2234,9 @@ def check_method_override_for_base_with_name( # Will always fail to typecheck below, since we know the node is a method original_type = NoneType() + if isinstance(original_node, Var) and original_node.allow_incompatible_override: + return False + always_allow_covariant = False if is_settable_property(defn) and ( is_settable_property(original_node) or isinstance(original_node, Var) @@ -3435,18 +3438,30 @@ def check_compatibility_all_supers(self, lvalue: RefExpr, rvalue: Expression) -> return for base in lvalue_node.info.mro[1:]: - # The type of "__slots__" and some other attributes usually doesn't need to - # be compatible with a base class. We'll still check the type of "__slots__" - # against "object" as an exception. - if lvalue_node.allow_incompatible_override and not ( - lvalue_node.name == "__slots__" and base.fullname == "builtins.object" + if ( + lvalue_node.name == "__hash__" + and base.fullname == "builtins.object" + and isinstance(get_proper_type(lvalue_type), NoneType) ): + # allow `__hash__ = None` if the overridden `__hash__` comes from object + # This isn't LSP-compliant, but too common in real code. continue if is_private(lvalue_node.name): continue base_type, base_node = self.node_type_from_base(lvalue_node.name, base, lvalue) + # The type of "__slots__" and some other attributes usually doesn't need to + # be compatible with a base class. We'll still check the type of "__slots__" + # against "object" as an exception. + if ( + isinstance(base_node, Var) + and base_node.allow_incompatible_override + and not ( + lvalue_node.name == "__slots__" and base.fullname == "builtins.object" + ) + ): + continue custom_setter = is_custom_settable_property(base_node) if isinstance(base_type, PartialType): base_type = None diff --git a/mypy/semanal.py b/mypy/semanal.py index d70abe911fea..9f678d6543b9 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -2018,6 +2018,27 @@ def analyze_class_body_common(self, defn: ClassDef) -> None: self.setup_self_type() defn.defs.accept(self) self.apply_class_plugin_hooks(defn) + + if ( + "__eq__" in defn.info.names + and "__hash__" not in defn.info.names + and not defn.info.is_protocol + ): + # If a class defines `__eq__` without `__hash__`, it's no longer hashable. + # Excludes Protocol from consideration as we don't want to enforce unhashability + # of their instances. + hash_none = Var("__hash__", NoneType()) + hash_none.info = defn.info + hash_none.set_line(defn) + hash_none.is_classvar = True + # Making a class hashable is allowed even if its parents weren't. + # The only possible consequence of this LSP violation would be + # `assert child.__hash__ is None` no longer passing, probably nobody + # cares about that - it's impossible to statically restrict to non-hashable + # anyway. + hash_none.allow_incompatible_override = True + self.add_symbol("__hash__", hash_none, defn) + self.leave_class() def analyze_typeddict_classdef(self, defn: ClassDef) -> bool: @@ -3237,6 +3258,22 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: self.process__all__(s) self.process__deletable__(s) self.process__slots__(s) + self.process__hash__(s) + + def process__hash__(self, s: AssignmentStmt) -> None: + # Allow overriding `__hash__ = None` in subclasses. + if ( + isinstance(self.type, TypeInfo) + and len(s.lvalues) == 1 + and isinstance(s.lvalues[0], NameExpr) + and s.lvalues[0].name == "__hash__" + and s.lvalues[0].kind == MDEF + and isinstance(s.rvalue, NameExpr) + and s.rvalue.name == "None" + ): + var = s.lvalues[0].node + if isinstance(var, Var): + var.allow_incompatible_override = True def analyze_identity_global_assignment(self, s: AssignmentStmt) -> bool: """Special case 'X = X' in global scope. diff --git a/mypyc/test-data/fixtures/ir.py b/mypyc/test-data/fixtures/ir.py index 1b92590a5fd4..5a5c65900bf4 100644 --- a/mypyc/test-data/fixtures/ir.py +++ b/mypyc/test-data/fixtures/ir.py @@ -90,6 +90,7 @@ def __init__(self, x: object) -> None: pass def __add__(self, x: str) -> str: pass def __mul__(self, x: int) -> str: pass def __rmul__(self, x: int) -> str: pass + def __hash__(self) -> int: pass def __eq__(self, x: object) -> bool: pass def __ne__(self, x: object) -> bool: pass def __lt__(self, x: str) -> bool: ... diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index f4bbaf41dc47..74a17f157b6f 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -116,11 +116,7 @@ class Base: __hash__: None = None class Derived(Base): - def __hash__(self) -> int: # E: Signature of "__hash__" incompatible with supertype "Base" \ - # N: Superclass: \ - # N: None \ - # N: Subclass: \ - # N: def __hash__(self) -> int + def __hash__(self) -> int: pass # Correct: @@ -147,6 +143,63 @@ class Base: class Derived(Base): __hash__ = 1 # E: Incompatible types in assignment (expression has type "int", base class "Base" defined the type as "Callable[[], int]") +[case testEqWithoutHash] +class A: + def __eq__(self, other) -> bool: ... + +reveal_type(A.__hash__) # N: Revealed type is "None" +[builtins fixtures/primitives.pyi] + +[case testHashNoneOverride] +class A: + __hash__ = None + +class B(A): + def __hash__(self) -> int: + return 0 + +class C(A): + __hash__ = object.__hash__ + +class D(A): + def __hash__(self, x: int) -> str: # E: Signature of "__hash__" incompatible with supertype "builtins.object" \ + # N: Superclass: \ + # N: def __hash__(self) -> int \ + # N: Subclass: \ + # N: def __hash__(self, x: int) -> str + return '' + +def bad_hash(x: E, y: str) -> str: + return y + +class E(A): + __hash__ = bad_hash # E: Incompatible types in assignment (expression has type "Callable[[str], str]", base class "object" defined the type as "Callable[[], int]") + + + + +[builtins fixtures/primitives.pyi] + +[case testHashNoneBadOverride] +class A: + def __hash__(self) -> int: return 0 + +class B(A): + __hash__ = None # E: Incompatible types in assignment (expression has type "None", base class "A" defined the type as "Callable[[], int]") +[builtins fixtures/primitives.pyi] + +[case testHashOverrideInMultipleInheritance] +class Foo: + def __eq__(self, other: "Foo") -> bool: ... + +class Bar: + def __hash__(self) -> int: ... + +class BadChild(Foo, Bar): ... # E: Definition of "__hash__" in base class "Foo" is incompatible with definition in base class "Bar" +class GoodChild(Bar, Foo): ... +[builtins fixtures/tuple.pyi] + + [case testOverridePartialAttributeWithMethod] # This was crashing: https://github.com/python/mypy/issues/11686. class Base: diff --git a/test-data/unit/fine-grained-inspect.test b/test-data/unit/fine-grained-inspect.test index 5caa1a94387b..b40669ae82a3 100644 --- a/test-data/unit/fine-grained-inspect.test +++ b/test-data/unit/fine-grained-inspect.test @@ -122,10 +122,10 @@ None [builtins fixtures/args.pyi] [out] == -{"C": ["x"], "object": ["__eq__", "__init__", "__ne__"]} +{"C": ["x"], "object": ["__eq__", "__hash__", "__init__", "__ne__"]} {"Iterable": ["__iter__"]} NameExpr -> {} -NameExpr -> {"object": ["__eq__", "__init__", "__ne__"]} +NameExpr -> {"object": ["__eq__", "__hash__", "__init__", "__ne__"]} [case testInspectTypeVarBoundAttrs] # inspect2: --show=attrs tmp/foo.py:8:13 diff --git a/test-data/unit/fixtures/primitives.pyi b/test-data/unit/fixtures/primitives.pyi index 2f8623c79b9f..22e1b2bb8302 100644 --- a/test-data/unit/fixtures/primitives.pyi +++ b/test-data/unit/fixtures/primitives.pyi @@ -10,6 +10,7 @@ class object: def __str__(self) -> str: pass def __eq__(self, other: object) -> bool: pass def __ne__(self, other: object) -> bool: pass + def __hash__(self) -> int: pass class type: def __init__(self, x: object) -> None: pass