|
22 | 22 | from ._compat import ( |
23 | 23 | PY_3_10_PLUS, |
24 | 24 | PY_3_11_PLUS, |
| 25 | + PY_3_13_PLUS, |
25 | 26 | _AnnotationExtractor, |
26 | 27 | _get_annotations, |
27 | 28 | get_generic_base, |
@@ -565,6 +566,60 @@ def _frozen_delattrs(self, name): |
565 | 566 | raise FrozenInstanceError |
566 | 567 |
|
567 | 568 |
|
| 569 | +def evolve(*args, **changes): |
| 570 | + """ |
| 571 | + Create a new instance, based on the first positional argument with |
| 572 | + *changes* applied. |
| 573 | +
|
| 574 | + Args: |
| 575 | +
|
| 576 | + inst: |
| 577 | + Instance of a class with *attrs* attributes. *inst* must be passed |
| 578 | + as a positional argument. |
| 579 | +
|
| 580 | + changes: |
| 581 | + Keyword changes in the new copy. |
| 582 | +
|
| 583 | + Returns: |
| 584 | + A copy of inst with *changes* incorporated. |
| 585 | +
|
| 586 | + Raises: |
| 587 | + TypeError: |
| 588 | + If *attr_name* couldn't be found in the class ``__init__``. |
| 589 | +
|
| 590 | + attrs.exceptions.NotAnAttrsClassError: |
| 591 | + If *cls* is not an *attrs* class. |
| 592 | +
|
| 593 | + .. versionadded:: 17.1.0 |
| 594 | + .. deprecated:: 23.1.0 |
| 595 | + It is now deprecated to pass the instance using the keyword argument |
| 596 | + *inst*. It will raise a warning until at least April 2024, after which |
| 597 | + it will become an error. Always pass the instance as a positional |
| 598 | + argument. |
| 599 | + .. versionchanged:: 24.1.0 |
| 600 | + *inst* can't be passed as a keyword argument anymore. |
| 601 | + """ |
| 602 | + try: |
| 603 | + (inst,) = args |
| 604 | + except ValueError: |
| 605 | + msg = ( |
| 606 | + f"evolve() takes 1 positional argument, but {len(args)} were given" |
| 607 | + ) |
| 608 | + raise TypeError(msg) from None |
| 609 | + |
| 610 | + cls = inst.__class__ |
| 611 | + attrs = fields(cls) |
| 612 | + for a in attrs: |
| 613 | + if not a.init: |
| 614 | + continue |
| 615 | + attr_name = a.name # To deal with private attributes. |
| 616 | + init_name = a.alias |
| 617 | + if init_name not in changes: |
| 618 | + changes[init_name] = getattr(inst, attr_name) |
| 619 | + |
| 620 | + return cls(**changes) |
| 621 | + |
| 622 | + |
568 | 623 | class _ClassBuilder: |
569 | 624 | """ |
570 | 625 | Iteratively build *one* class. |
@@ -979,6 +1034,12 @@ def add_init(self): |
979 | 1034 |
|
980 | 1035 | return self |
981 | 1036 |
|
| 1037 | + def add_replace(self): |
| 1038 | + self._cls_dict["__replace__"] = self._add_method_dunders( |
| 1039 | + lambda self, **changes: evolve(self, **changes) |
| 1040 | + ) |
| 1041 | + return self |
| 1042 | + |
982 | 1043 | def add_match_args(self): |
983 | 1044 | self._cls_dict["__match_args__"] = tuple( |
984 | 1045 | field.name |
@@ -1381,6 +1442,9 @@ def wrap(cls): |
1381 | 1442 | msg = "Invalid value for cache_hash. To use hash caching, init must be True." |
1382 | 1443 | raise TypeError(msg) |
1383 | 1444 |
|
| 1445 | + if PY_3_13_PLUS and not _has_own_attribute(cls, "__replace__"): |
| 1446 | + builder.add_replace() |
| 1447 | + |
1384 | 1448 | if ( |
1385 | 1449 | PY_3_10_PLUS |
1386 | 1450 | and match_args |
|
0 commit comments