Skip to content

Commit a663346

Browse files
HnasarTinche
andcommitted
attrs: Fix emulating hash method logic
This commit fixes a couple regressions in 1.9.0 from 91be285. Attrs' logic for hashability is slightly complex: * https://www.attrs.org/en/stable/hashing.html * https://github.com/python-attrs/attrs/blob/9e443b18527dc96b194e92805fa751cbf8434ba9/src/attr/_make.py#L1660-L1689 Mypy now properly emulates attrs' logic so that custom `__hash__` implementations are preserved, `@frozen` subclasses are always hashable, and classes are only made unhashable based on the values of `eq` and `unsafe_hash`. Fixes python#17015 Fixes python#16556 (comment) Based on a patch in python#17012 Co-Authored-By: Tin Tvrtkovic <[email protected]>
1 parent ea49e1f commit a663346

File tree

4 files changed

+110
-14
lines changed

4 files changed

+110
-14
lines changed

mypy/plugins/attrs.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -325,9 +325,6 @@ def attr_class_maker_callback(
325325
frozen = _get_frozen(ctx, frozen_default)
326326
order = _determine_eq_order(ctx)
327327
slots = _get_decorator_bool_argument(ctx, "slots", slots_default)
328-
hashable = _get_decorator_bool_argument(ctx, "hash", False) or _get_decorator_bool_argument(
329-
ctx, "unsafe_hash", False
330-
)
331328

332329
auto_attribs = _get_decorator_optional_bool_argument(ctx, "auto_attribs", auto_attribs_default)
333330
kw_only = _get_decorator_bool_argument(ctx, "kw_only", False)
@@ -371,7 +368,24 @@ def attr_class_maker_callback(
371368
_add_order(ctx, adder)
372369
if frozen:
373370
_make_frozen(ctx, attributes)
374-
elif not hashable:
371+
# Frozen classes are hashable by default, even if inheriting from non-frozen ones.
372+
hashable = _get_decorator_bool_argument(
373+
ctx, "hash", True
374+
) and _get_decorator_bool_argument(ctx, "unsafe_hash", True)
375+
else:
376+
hashable = _get_decorator_optional_bool_argument(ctx, "unsafe_hash")
377+
if hashable is None: # unspecified
378+
hashable = _get_decorator_optional_bool_argument(ctx, "hash")
379+
380+
eq = _get_decorator_optional_bool_argument(ctx, "eq")
381+
has_own_hash = "__hash__" in ctx.cls.info.names
382+
383+
if has_own_hash or (hashable is None and eq is False):
384+
pass # Do nothing.
385+
elif hashable:
386+
# We copy the `__hash__` signature from `object` to make them hashable.
387+
ctx.cls.info.names["__hash__"] = ctx.cls.info.mro[-1].names["__hash__"]
388+
else:
375389
_remove_hashability(ctx)
376390

377391
return True

test-data/unit/check-incremental.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3015,7 +3015,7 @@ class NoInit:
30153015
class NoCmp:
30163016
x: int
30173017

3018-
[builtins fixtures/list.pyi]
3018+
[builtins fixtures/plugin_attrs.pyi]
30193019
[rechecked]
30203020
[stale]
30213021
[out1]

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

Lines changed: 89 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
@@ -2380,3 +2381,82 @@ class B(A):
23802381
reveal_type(B.__hash__) # N: Revealed type is "None"
23812382

23822383
[builtins fixtures/plugin_attrs.pyi]
2384+
2385+
[case testManualOwnHashability]
2386+
from attrs import define, frozen
2387+
2388+
@define
2389+
class A:
2390+
a: int
2391+
def __hash__(self) -> int:
2392+
...
2393+
2394+
reveal_type(A.__hash__) # N: Revealed type is "def (self: __main__.A) -> builtins.int"
2395+
2396+
[builtins fixtures/plugin_attrs.pyi]
2397+
2398+
[case testSubclassDefaultLosesHashability]
2399+
from attrs import define, frozen
2400+
2401+
@define
2402+
class A:
2403+
a: int
2404+
def __hash__(self) -> int:
2405+
...
2406+
2407+
@define
2408+
class B(A):
2409+
pass
2410+
2411+
reveal_type(B.__hash__) # N: Revealed type is "None"
2412+
2413+
[builtins fixtures/plugin_attrs.pyi]
2414+
2415+
[case testSubclassEqFalseKeepsHashability]
2416+
from attrs import define, frozen
2417+
2418+
@define
2419+
class A:
2420+
a: int
2421+
def __hash__(self) -> int:
2422+
...
2423+
2424+
@define(eq=False)
2425+
class B(A):
2426+
pass
2427+
2428+
reveal_type(B.__hash__) # N: Revealed type is "def (self: __main__.A) -> builtins.int"
2429+
2430+
[builtins fixtures/plugin_attrs.pyi]
2431+
2432+
[case testSubclassingFrozenHashability]
2433+
from attrs import define, frozen
2434+
2435+
@define
2436+
class A:
2437+
a: int
2438+
2439+
@frozen
2440+
class B(A):
2441+
pass
2442+
2443+
reveal_type(B.__hash__) # N: Revealed type is "def (self: builtins.object) -> builtins.int"
2444+
2445+
[builtins fixtures/plugin_attrs.pyi]
2446+
2447+
[case testSubclassingFrozenHashOffHashability]
2448+
from attrs import define, frozen
2449+
2450+
@define
2451+
class A:
2452+
a: int
2453+
def __hash__(self) -> int:
2454+
...
2455+
2456+
@frozen(unsafe_hash=False)
2457+
class B(A):
2458+
pass
2459+
2460+
reveal_type(B.__hash__) # N: Revealed type is "None"
2461+
2462+
[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)