|
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,64 @@ 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 | + .. tip:: |
| 575 | +
|
| 576 | + On Python 3.13 and later, you can also use `copy.replace` instead. |
| 577 | +
|
| 578 | + Args: |
| 579 | +
|
| 580 | + inst: |
| 581 | + Instance of a class with *attrs* attributes. *inst* must be passed |
| 582 | + as a positional argument. |
| 583 | +
|
| 584 | + changes: |
| 585 | + Keyword changes in the new copy. |
| 586 | +
|
| 587 | + Returns: |
| 588 | + A copy of inst with *changes* incorporated. |
| 589 | +
|
| 590 | + Raises: |
| 591 | + TypeError: |
| 592 | + If *attr_name* couldn't be found in the class ``__init__``. |
| 593 | +
|
| 594 | + attrs.exceptions.NotAnAttrsClassError: |
| 595 | + If *cls* is not an *attrs* class. |
| 596 | +
|
| 597 | + .. versionadded:: 17.1.0 |
| 598 | + .. deprecated:: 23.1.0 |
| 599 | + It is now deprecated to pass the instance using the keyword argument |
| 600 | + *inst*. It will raise a warning until at least April 2024, after which |
| 601 | + it will become an error. Always pass the instance as a positional |
| 602 | + argument. |
| 603 | + .. versionchanged:: 24.1.0 |
| 604 | + *inst* can't be passed as a keyword argument anymore. |
| 605 | + """ |
| 606 | + try: |
| 607 | + (inst,) = args |
| 608 | + except ValueError: |
| 609 | + msg = ( |
| 610 | + f"evolve() takes 1 positional argument, but {len(args)} were given" |
| 611 | + ) |
| 612 | + raise TypeError(msg) from None |
| 613 | + |
| 614 | + cls = inst.__class__ |
| 615 | + attrs = fields(cls) |
| 616 | + for a in attrs: |
| 617 | + if not a.init: |
| 618 | + continue |
| 619 | + attr_name = a.name # To deal with private attributes. |
| 620 | + init_name = a.alias |
| 621 | + if init_name not in changes: |
| 622 | + changes[init_name] = getattr(inst, attr_name) |
| 623 | + |
| 624 | + return cls(**changes) |
| 625 | + |
| 626 | + |
568 | 627 | class _ClassBuilder: |
569 | 628 | """ |
570 | 629 | Iteratively build *one* class. |
@@ -979,6 +1038,12 @@ def add_init(self): |
979 | 1038 |
|
980 | 1039 | return self |
981 | 1040 |
|
| 1041 | + def add_replace(self): |
| 1042 | + self._cls_dict["__replace__"] = self._add_method_dunders( |
| 1043 | + lambda self, **changes: evolve(self, **changes) |
| 1044 | + ) |
| 1045 | + return self |
| 1046 | + |
982 | 1047 | def add_match_args(self): |
983 | 1048 | self._cls_dict["__match_args__"] = tuple( |
984 | 1049 | field.name |
@@ -1381,6 +1446,9 @@ def wrap(cls): |
1381 | 1446 | msg = "Invalid value for cache_hash. To use hash caching, init must be True." |
1382 | 1447 | raise TypeError(msg) |
1383 | 1448 |
|
| 1449 | + if PY_3_13_PLUS and not _has_own_attribute(cls, "__replace__"): |
| 1450 | + builder.add_replace() |
| 1451 | + |
1384 | 1452 | if ( |
1385 | 1453 | PY_3_10_PLUS |
1386 | 1454 | and match_args |
@@ -2394,7 +2462,7 @@ def evolve(self, **changes): |
2394 | 2462 | Copy *self* and apply *changes*. |
2395 | 2463 |
|
2396 | 2464 | This works similarly to `attrs.evolve` but that function does not work |
2397 | | - with {class}`Attribute`. |
| 2465 | + with :class:`attrs.Attribute`. |
2398 | 2466 |
|
2399 | 2467 | It is mainly meant to be used for `transform-fields`. |
2400 | 2468 |
|
|
0 commit comments