Skip to content

Commit d8a7f0f

Browse files
committed
Fix attrs hashability detection when inheriting from mutable
1 parent ea49e1f commit d8a7f0f

File tree

3 files changed

+44
-10
lines changed

3 files changed

+44
-10
lines changed

mypy/plugins/attrs.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,11 @@ def attr_class_maker_callback(
371371
_add_order(ctx, adder)
372372
if frozen:
373373
_make_frozen(ctx, attributes)
374-
elif not hashable:
374+
hashable = _get_decorator_bool_argument(
375+
ctx, "hash", True
376+
) and _get_decorator_bool_argument(ctx, "unsafe_hash", True)
377+
378+
if not hashable:
375379
_remove_hashability(ctx)
376380

377381
return True
@@ -836,6 +840,12 @@ def _make_frozen(ctx: mypy.plugin.ClassDefContext, attributes: list[Attribute])
836840
ctx.cls.info.names[var.name] = SymbolTableNode(MDEF, var)
837841
var.is_property = True
838842

843+
# Frozen classes are hashable by default, even if inheriting from non-frozen ones.
844+
# We copy the `__hash__` signature from `object` to make them hashable.
845+
# If the frozen class was created using `hash=False`, the hashability
846+
# will be removed later.
847+
ctx.cls.info.names["__hash__"] = ctx.cls.info.mro[-1].names["__hash__"]
848+
839849

840850
def _add_init(
841851
ctx: mypy.plugin.ClassDefContext,

test-data/unit/check-plugin-attrs.test

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,8 @@ class A:
360360

361361
a = A(5)
362362
a.a = 16 # E: Property "a" defined in "A" is read-only
363-
[builtins fixtures/bool.pyi]
363+
[builtins fixtures/plugin_attrs.pyi]
364+
364365
[case testAttrsNextGenFrozen]
365366
from attr import frozen, field
366367

@@ -370,7 +371,7 @@ class A:
370371

371372
a = A(5)
372373
a.a = 16 # E: Property "a" defined in "A" is read-only
373-
[builtins fixtures/bool.pyi]
374+
[builtins fixtures/plugin_attrs.pyi]
374375

375376
[case testAttrsNextGenDetect]
376377
from attr import define, field
@@ -420,7 +421,7 @@ reveal_type(A) # N: Revealed type is "def (a: builtins.int, b: builtins.bool) -
420421
reveal_type(B) # N: Revealed type is "def (a: builtins.bool, b: builtins.int) -> __main__.B"
421422
reveal_type(C) # N: Revealed type is "def (a: builtins.int) -> __main__.C"
422423

423-
[builtins fixtures/bool.pyi]
424+
[builtins fixtures/plugin_attrs.pyi]
424425

425426
[case testAttrsDataClass]
426427
import attr
@@ -1155,7 +1156,7 @@ c = NonFrozenFrozen(1, 2)
11551156
c.a = 17 # E: Property "a" defined in "NonFrozenFrozen" is read-only
11561157
c.b = 17 # E: Property "b" defined in "NonFrozenFrozen" is read-only
11571158

1158-
[builtins fixtures/bool.pyi]
1159+
[builtins fixtures/plugin_attrs.pyi]
11591160
[case testAttrsCallableAttributes]
11601161
from typing import Callable
11611162
import attr
@@ -1178,7 +1179,7 @@ class G:
11781179
class FFrozen(F):
11791180
def bar(self) -> bool:
11801181
return self._cb(5, 6)
1181-
[builtins fixtures/callable.pyi]
1182+
[builtins fixtures/plugin_attrs.pyi]
11821183

11831184
[case testAttrsWithFactory]
11841185
from typing import List
@@ -1450,7 +1451,7 @@ class C:
14501451
total = attr.ib(type=Bad) # E: Name "Bad" is not defined
14511452

14521453
C(0).total = 1 # E: Property "total" defined in "C" is read-only
1453-
[builtins fixtures/bool.pyi]
1454+
[builtins fixtures/plugin_attrs.pyi]
14541455

14551456
[case testTypeInAttrDeferredStar]
14561457
import lib
@@ -1941,7 +1942,7 @@ class C:
19411942
default=None, converter=default_if_none(factory=dict) \
19421943
# E: Unsupported converter, only named functions, types and lambdas are currently supported
19431944
)
1944-
[builtins fixtures/dict.pyi]
1945+
[builtins fixtures/plugin_attrs.pyi]
19451946

19461947
[case testAttrsUnannotatedConverter]
19471948
import attr
@@ -2012,7 +2013,7 @@ class Sub(Base):
20122013

20132014
@property
20142015
def name(self) -> str: ...
2015-
[builtins fixtures/property.pyi]
2016+
[builtins fixtures/plugin_attrs.pyi]
20162017

20172018
[case testOverrideWithPropertyInFrozenClassChecked]
20182019
from attrs import frozen
@@ -2035,7 +2036,7 @@ class Sub(Base):
20352036

20362037
# This matches runtime semantics
20372038
reveal_type(Sub) # N: Revealed type is "def (*, name: builtins.str, first_name: builtins.str, last_name: builtins.str) -> __main__.Sub"
2038-
[builtins fixtures/property.pyi]
2039+
[builtins fixtures/plugin_attrs.pyi]
20392040

20402041
[case testFinalInstanceAttribute]
20412042
from attrs import define
@@ -2342,6 +2343,12 @@ class A:
23422343

23432344
reveal_type(A.__hash__) # N: Revealed type is "def (self: builtins.object) -> builtins.int"
23442345

2346+
@frozen(hash=False)
2347+
class B:
2348+
b: int
2349+
2350+
reveal_type(B.__hash__) # N: Revealed type is "None"
2351+
23452352
[builtins fixtures/plugin_attrs.pyi]
23462353

23472354
[case testManualHashHashability]
@@ -2380,3 +2387,18 @@ class B(A):
23802387
reveal_type(B.__hash__) # N: Revealed type is "None"
23812388

23822389
[builtins fixtures/plugin_attrs.pyi]
2390+
2391+
[case testSubclassingFrozenHashability]
2392+
from attrs import define, frozen
2393+
2394+
@define
2395+
class A:
2396+
a: int
2397+
2398+
@frozen
2399+
class B(A):
2400+
pass
2401+
2402+
reveal_type(B.__hash__) # N: Revealed type is "def (self: builtins.object) -> builtins.int"
2403+
2404+
[builtins fixtures/plugin_attrs.pyi]

test-data/unit/fixtures/plugin_attrs.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,5 @@ class tuple(Sequence[Tco], Generic[Tco]):
3535
def __iter__(self) -> Iterator[Tco]: pass
3636
def __contains__(self, item: object) -> bool: pass
3737
def __getitem__(self, x: int) -> Tco: pass
38+
39+
property = object() # Dummy definition

0 commit comments

Comments
 (0)