diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 345ea822ed940..6b90308c42e4e 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -371,7 +371,11 @@ def attr_class_maker_callback( _add_order(ctx, adder) if frozen: _make_frozen(ctx, attributes) - elif not hashable: + hashable = _get_decorator_bool_argument( + ctx, "hash", True + ) and _get_decorator_bool_argument(ctx, "unsafe_hash", True) + + if not hashable: _remove_hashability(ctx) return True @@ -836,6 +840,12 @@ def _make_frozen(ctx: mypy.plugin.ClassDefContext, attributes: list[Attribute]) ctx.cls.info.names[var.name] = SymbolTableNode(MDEF, var) var.is_property = True + # Frozen classes are hashable by default, even if inheriting from non-frozen ones. + # We copy the `__hash__` signature from `object` to make them hashable. + # If the frozen class was created using `hash=False`, the hashability + # will be removed later. + ctx.cls.info.names["__hash__"] = ctx.cls.info.mro[-1].names["__hash__"] + def _add_init( ctx: mypy.plugin.ClassDefContext, diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 42faa8c627ba5..a7f4fafc579e7 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -3015,7 +3015,7 @@ class NoInit: class NoCmp: x: int -[builtins fixtures/list.pyi] +[builtins fixtures/plugin_attrs.pyi] [rechecked] [stale] [out1] diff --git a/test-data/unit/check-plugin-attrs.test b/test-data/unit/check-plugin-attrs.test index 0f379724553aa..216241b8b9ce4 100644 --- a/test-data/unit/check-plugin-attrs.test +++ b/test-data/unit/check-plugin-attrs.test @@ -360,7 +360,8 @@ class A: a = A(5) a.a = 16 # E: Property "a" defined in "A" is read-only -[builtins fixtures/bool.pyi] +[builtins fixtures/plugin_attrs.pyi] + [case testAttrsNextGenFrozen] from attr import frozen, field @@ -370,7 +371,7 @@ class A: a = A(5) a.a = 16 # E: Property "a" defined in "A" is read-only -[builtins fixtures/bool.pyi] +[builtins fixtures/plugin_attrs.pyi] [case testAttrsNextGenDetect] from attr import define, field @@ -420,7 +421,7 @@ reveal_type(A) # N: Revealed type is "def (a: builtins.int, b: builtins.bool) - reveal_type(B) # N: Revealed type is "def (a: builtins.bool, b: builtins.int) -> __main__.B" reveal_type(C) # N: Revealed type is "def (a: builtins.int) -> __main__.C" -[builtins fixtures/bool.pyi] +[builtins fixtures/plugin_attrs.pyi] [case testAttrsDataClass] import attr @@ -1155,7 +1156,7 @@ c = NonFrozenFrozen(1, 2) c.a = 17 # E: Property "a" defined in "NonFrozenFrozen" is read-only c.b = 17 # E: Property "b" defined in "NonFrozenFrozen" is read-only -[builtins fixtures/bool.pyi] +[builtins fixtures/plugin_attrs.pyi] [case testAttrsCallableAttributes] from typing import Callable import attr @@ -1178,7 +1179,7 @@ class G: class FFrozen(F): def bar(self) -> bool: return self._cb(5, 6) -[builtins fixtures/callable.pyi] +[builtins fixtures/plugin_attrs.pyi] [case testAttrsWithFactory] from typing import List @@ -1450,7 +1451,7 @@ class C: total = attr.ib(type=Bad) # E: Name "Bad" is not defined C(0).total = 1 # E: Property "total" defined in "C" is read-only -[builtins fixtures/bool.pyi] +[builtins fixtures/plugin_attrs.pyi] [case testTypeInAttrDeferredStar] import lib @@ -1941,7 +1942,7 @@ class C: default=None, converter=default_if_none(factory=dict) \ # E: Unsupported converter, only named functions, types and lambdas are currently supported ) -[builtins fixtures/dict.pyi] +[builtins fixtures/plugin_attrs.pyi] [case testAttrsUnannotatedConverter] import attr @@ -2012,7 +2013,7 @@ class Sub(Base): @property def name(self) -> str: ... -[builtins fixtures/property.pyi] +[builtins fixtures/plugin_attrs.pyi] [case testOverrideWithPropertyInFrozenClassChecked] from attrs import frozen @@ -2035,7 +2036,7 @@ class Sub(Base): # This matches runtime semantics reveal_type(Sub) # N: Revealed type is "def (*, name: builtins.str, first_name: builtins.str, last_name: builtins.str) -> __main__.Sub" -[builtins fixtures/property.pyi] +[builtins fixtures/plugin_attrs.pyi] [case testFinalInstanceAttribute] from attrs import define @@ -2342,6 +2343,12 @@ class A: reveal_type(A.__hash__) # N: Revealed type is "def (self: builtins.object) -> builtins.int" +@frozen(hash=False) +class B: + b: int + +reveal_type(B.__hash__) # N: Revealed type is "None" + [builtins fixtures/plugin_attrs.pyi] [case testManualHashHashability] @@ -2380,3 +2387,18 @@ class B(A): reveal_type(B.__hash__) # N: Revealed type is "None" [builtins fixtures/plugin_attrs.pyi] + +[case testSubclassingFrozenHashability] +from attrs import define, frozen + +@define +class A: + a: int + +@frozen +class B(A): + pass + +reveal_type(B.__hash__) # N: Revealed type is "def (self: builtins.object) -> builtins.int" + +[builtins fixtures/plugin_attrs.pyi] diff --git a/test-data/unit/fixtures/plugin_attrs.pyi b/test-data/unit/fixtures/plugin_attrs.pyi index 5b87c47b5bc88..7fd641727253e 100644 --- a/test-data/unit/fixtures/plugin_attrs.pyi +++ b/test-data/unit/fixtures/plugin_attrs.pyi @@ -35,3 +35,5 @@ class tuple(Sequence[Tco], Generic[Tco]): def __iter__(self) -> Iterator[Tco]: pass def __contains__(self, item: object) -> bool: pass def __getitem__(self, x: int) -> Tco: pass + +property = object() # Dummy definition