Skip to content

Commit bde3f58

Browse files
authored
Deprecate hash for unsafe_hash (#1323)
* Deprecate hash for unsafe_hash It's the standard -- what are we gonna do. * Add deprecation tests * Add news fragment
1 parent dbb25ce commit bde3f58

File tree

9 files changed

+119
-61
lines changed

9 files changed

+119
-61
lines changed

changelog.d/1323.deprecation.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The *hash* argument to `@attr.s`, `@attrs.define`, and `make_class()` is now deprecated in favor of *unsafe_hash*, as defined by PEP 681.

src/attr/_make.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1270,20 +1270,30 @@ def attrs(
12701270
.. versionadded:: 24.1.0
12711271
If a class has an *inherited* classmethod called
12721272
``__attrs_init_subclass__``, it is executed after the class is created.
1273+
.. deprecated:: 24.1.0 *hash* is deprecated in favor of *unsafe_hash*.
12731274
"""
12741275
if repr_ns is not None:
12751276
import warnings
12761277

12771278
warnings.warn(
12781279
DeprecationWarning(
1279-
"The `repr_ns` argument is deprecated and will be removed in or after April 2025."
1280+
"The `repr_ns` argument is deprecated and will be removed in or after August 2025."
12801281
),
12811282
stacklevel=2,
12821283
)
12831284

12841285
eq_, order_ = _determine_attrs_eq_order(cmp, eq, order, None)
12851286

1286-
# unsafe_hash takes precedence due to PEP 681.
1287+
# hash is deprecated & unsafe_hash takes precedence due to PEP 681.
1288+
if hash is not None:
1289+
import warnings
1290+
1291+
warnings.warn(
1292+
DeprecationWarning(
1293+
"The `hash` argument is deprecated in favor of `unsafe_hash` and will be removed in or after August 2025."
1294+
),
1295+
stacklevel=2,
1296+
)
12871297
if unsafe_hash is not None:
12881298
hash = unsafe_hash
12891299

@@ -2854,6 +2864,19 @@ def make_class(
28542864
True,
28552865
)
28562866

2867+
hash = attributes_arguments.pop("hash", _SENTINEL)
2868+
if hash is not _SENTINEL:
2869+
import warnings
2870+
2871+
warnings.warn(
2872+
DeprecationWarning(
2873+
"The `hash` argument is deprecated in favor of `unsafe_hash` and will be removed in or after August 2025."
2874+
),
2875+
stacklevel=2,
2876+
)
2877+
2878+
attributes_arguments["unsafe_hash"] = hash
2879+
28572880
cls = _attrs(these=cls_dict, **attributes_arguments)(type_)
28582881
# Only add type annotations now or "_attrs()" will complain:
28592882
cls.__annotations__ = {
@@ -2866,7 +2889,7 @@ def make_class(
28662889
# import into .validators / .converters.
28672890

28682891

2869-
@attrs(slots=True, hash=True)
2892+
@attrs(slots=True, unsafe_hash=True)
28702893
class _AndValidator:
28712894
"""
28722895
Compose many validators to a single one.

src/attr/_next_gen.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ def define(
184184
details.
185185
186186
hash (bool | None):
187-
Alias for *unsafe_hash*. *unsafe_hash* takes precedence.
187+
Deprecated alias for *unsafe_hash*. *unsafe_hash* takes precedence.
188188
189189
cache_hash (bool):
190190
Ensure that the object's hash code is computed only once and stored
@@ -320,6 +320,7 @@ def define(
320320
.. versionadded:: 24.1.0
321321
If a class has an *inherited* classmethod called
322322
``__attrs_init_subclass__``, it is executed after the class is created.
323+
.. deprecated:: 24.1.0 *hash* is deprecated in favor of *unsafe_hash*.
323324
324325
.. note::
325326

src/attr/validators.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def disabled():
8888
set_run_validators(True)
8989

9090

91-
@attrs(repr=False, slots=True, hash=True)
91+
@attrs(repr=False, slots=True, unsafe_hash=True)
9292
class _InstanceOfValidator:
9393
type = attrib()
9494

@@ -196,7 +196,7 @@ def matches_re(regex, flags=0, func=None):
196196
return _MatchesReValidator(pattern, match_func)
197197

198198

199-
@attrs(repr=False, slots=True, hash=True)
199+
@attrs(repr=False, slots=True, unsafe_hash=True)
200200
class _OptionalValidator:
201201
validator = attrib()
202202

@@ -231,7 +231,7 @@ def optional(validator):
231231
return _OptionalValidator(validator)
232232

233233

234-
@attrs(repr=False, slots=True, hash=True)
234+
@attrs(repr=False, slots=True, unsafe_hash=True)
235235
class _InValidator:
236236
options = attrib()
237237
_original_options = attrib(hash=False)
@@ -290,7 +290,7 @@ def in_(options):
290290
return _InValidator(options, repr_options)
291291

292292

293-
@attrs(repr=False, slots=False, hash=True)
293+
@attrs(repr=False, slots=False, unsafe_hash=True)
294294
class _IsCallableValidator:
295295
def __call__(self, inst, attr, value):
296296
"""
@@ -328,7 +328,7 @@ def is_callable():
328328
return _IsCallableValidator()
329329

330330

331-
@attrs(repr=False, slots=True, hash=True)
331+
@attrs(repr=False, slots=True, unsafe_hash=True)
332332
class _DeepIterable:
333333
member_validator = attrib(validator=is_callable())
334334
iterable_validator = attrib(
@@ -377,7 +377,7 @@ def deep_iterable(member_validator, iterable_validator=None):
377377
return _DeepIterable(member_validator, iterable_validator)
378378

379379

380-
@attrs(repr=False, slots=True, hash=True)
380+
@attrs(repr=False, slots=True, unsafe_hash=True)
381381
class _DeepMapping:
382382
key_validator = attrib(validator=is_callable())
383383
value_validator = attrib(validator=is_callable())
@@ -554,7 +554,7 @@ def min_len(length):
554554
return _MinLengthValidator(length)
555555

556556

557-
@attrs(repr=False, slots=True, hash=True)
557+
@attrs(repr=False, slots=True, unsafe_hash=True)
558558
class _SubclassOfValidator:
559559
type = attrib()
560560

@@ -592,7 +592,7 @@ def _subclass_of(type):
592592
return _SubclassOfValidator(type)
593593

594594

595-
@attrs(repr=False, slots=True, hash=True)
595+
@attrs(repr=False, slots=True, unsafe_hash=True)
596596
class _NotValidator:
597597
validator = attrib()
598598
msg = attrib(
@@ -665,7 +665,7 @@ def not_(validator, *, msg=None, exc_types=(ValueError, TypeError)):
665665
return _NotValidator(validator, msg, exc_types)
666666

667667

668-
@attrs(repr=False, slots=True, hash=True)
668+
@attrs(repr=False, slots=True, unsafe_hash=True)
669669
class _OrValidator:
670670
validators = attrib()
671671

tests/test_dunders.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,16 @@ class OrderCallableCSlots:
6464
# HashC is hashable by explicit definition while HashCSlots is hashable
6565
# implicitly. The "Cached" versions are the same, except with hash code
6666
# caching enabled
67-
HashC = simple_class(hash=True)
68-
HashCSlots = simple_class(hash=None, eq=True, frozen=True, slots=True)
69-
HashCCached = simple_class(hash=True, cache_hash=True)
67+
HashC = simple_class(unsafe_hash=True)
68+
HashCSlots = simple_class(unsafe_hash=None, eq=True, frozen=True, slots=True)
69+
HashCCached = simple_class(unsafe_hash=True, cache_hash=True)
7070
HashCSlotsCached = simple_class(
71-
hash=None, eq=True, frozen=True, slots=True, cache_hash=True
71+
unsafe_hash=None, eq=True, frozen=True, slots=True, cache_hash=True
7272
)
7373
# the cached hash code is stored slightly differently in this case
7474
# so it needs to be tested separately
7575
HashCFrozenNotSlotsCached = simple_class(
76-
frozen=True, slots=False, hash=True, cache_hash=True
76+
frozen=True, slots=False, unsafe_hash=True, cache_hash=True
7777
)
7878

7979

@@ -443,17 +443,17 @@ def test_str_no_repr(self):
443443

444444
# these are for use in TestAddHash.test_cache_hash_serialization
445445
# they need to be out here so they can be un-pickled
446-
@attr.attrs(hash=True, cache_hash=False)
446+
@attr.attrs(unsafe_hash=True, cache_hash=False)
447447
class HashCacheSerializationTestUncached:
448448
foo_value = attr.ib()
449449

450450

451-
@attr.attrs(hash=True, cache_hash=True)
451+
@attr.attrs(unsafe_hash=True, cache_hash=True)
452452
class HashCacheSerializationTestCached:
453453
foo_value = attr.ib()
454454

455455

456-
@attr.attrs(slots=True, hash=True, cache_hash=True)
456+
@attr.attrs(slots=True, unsafe_hash=True, cache_hash=True)
457457
class HashCacheSerializationTestCachedSlots:
458458
foo_value = attr.ib()
459459

@@ -660,7 +660,7 @@ def test_copy_hash_cleared(self, cache_hash, frozen, slots):
660660

661661
# Give it an explicit hash if we don't have an implicit one
662662
if not frozen:
663-
kwargs["hash"] = True
663+
kwargs["unsafe_hash"] = True
664664

665665
@attr.s(**kwargs)
666666
class C:
@@ -711,7 +711,7 @@ def test_copy_two_arg_reduce(self, frozen):
711711
__reduce__ generated when cache_hash=True works in that case.
712712
"""
713713

714-
@attr.s(frozen=frozen, cache_hash=True, hash=True)
714+
@attr.s(frozen=frozen, cache_hash=True, unsafe_hash=True)
715715
class C:
716716
x = attr.ib()
717717

@@ -965,7 +965,7 @@ def test_false(self):
965965
assert False is bool(NOTHING)
966966

967967

968-
@attr.s(hash=True, order=True)
968+
@attr.s(unsafe_hash=True, order=True)
969969
class C:
970970
pass
971971

@@ -974,17 +974,19 @@ class C:
974974
OriginalC = C
975975

976976

977-
@attr.s(hash=True, order=True)
977+
@attr.s(unsafe_hash=True, order=True)
978978
class C:
979979
pass
980980

981981

982982
CopyC = C
983983

984984

985-
@attr.s(hash=True, order=True)
985+
@attr.s(unsafe_hash=True, order=True)
986986
class C:
987-
"""A different class, to generate different methods."""
987+
"""
988+
A different class, to generate different methods.
989+
"""
988990

989991
a = attr.ib()
990992

tests/test_functional.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -379,26 +379,26 @@ class C:
379379

380380
def test_hash_by_id(self):
381381
"""
382-
With dict classes, hashing by ID is active for hash=False even on
383-
Python 3. This is incorrect behavior but we have to retain it for
384-
backward compatibility.
382+
With dict classes, hashing by ID is active for hash=False. This is
383+
incorrect behavior but we have to retain it for
384+
backwards-compatibility.
385385
"""
386386

387-
@attr.s(hash=False)
387+
@attr.s(unsafe_hash=False)
388388
class HashByIDBackwardCompat:
389389
x = attr.ib()
390390

391391
assert hash(HashByIDBackwardCompat(1)) != hash(
392392
HashByIDBackwardCompat(1)
393393
)
394394

395-
@attr.s(hash=False, eq=False)
395+
@attr.s(unsafe_hash=False, eq=False)
396396
class HashByID:
397397
x = attr.ib()
398398

399399
assert hash(HashByID(1)) != hash(HashByID(1))
400400

401-
@attr.s(hash=True)
401+
@attr.s(unsafe_hash=True)
402402
class HashByValues:
403403
x = attr.ib()
404404

@@ -421,17 +421,22 @@ class C:
421421
class D(C):
422422
pass
423423

424-
def test_hash_false_eq_false(self, slots):
424+
def test_unsafe_hash_false_eq_false(self, slots):
425425
"""
426-
hash=False and eq=False make a class hashable by ID.
426+
unsafe_hash=False and eq=False make a class hashable by ID.
427427
"""
428428

429-
@attr.s(hash=False, eq=False, slots=slots)
429+
@attr.s(unsafe_hash=False, eq=False, slots=slots)
430430
class C:
431431
pass
432432

433433
assert hash(C()) != hash(C())
434434

435+
def test_hash_deprecated(self):
436+
"""
437+
Using the hash argument is deprecated.
438+
"""
439+
435440
def test_eq_false(self, slots):
436441
"""
437442
eq=False makes a class hashable by ID.

0 commit comments

Comments
 (0)