Skip to content

Commit

Permalink
Recursively evolve nested attrs classes
Browse files Browse the repository at this point in the history
  • Loading branch information
sscherfke committed Feb 14, 2021
1 parent c2712fd commit 53c1d7a
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 2 deletions.
1 change: 1 addition & 0 deletions changelog.d/759.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Let ``evolve()`` work with nested ``attrs`` classes. #634
21 changes: 21 additions & 0 deletions docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,27 @@ In Clojure that function is called `assoc <https://clojuredocs.org/clojure.core/
>>> i1 == i2
False

This functions also works for nested ``attrs`` classes.
You just pass a (possibly nested) dict with changes for an attribute:

.. doctest::

>>> @attr.s(frozen=True)
... class Child(object):
... x = attr.ib()
... y = attr.ib()
>>> @attr.s(frozen=True)
... class Parent(object):
... child = attr.ib()
>>> i1 = Parent(Child(1, 2))
>>> i1
Parent(child=Child(x=1, y=2))
>>> i2 = attr.evolve(i1, child={"y": 3})
>>> i2
Parent(child=Child(x=1, y=3))
>>> i1 == i2, i1.child == i2.child
(False, False)


Other Goodies
-------------
Expand Down
10 changes: 8 additions & 2 deletions src/attr/_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,8 @@ def evolve(inst, **changes):
Create a new instance, based on *inst* with *changes* applied.
:param inst: Instance of a class with ``attrs`` attributes.
:param changes: Keyword changes in the new copy.
:param changes: Keyword changes in the new copy. Nested attrs classes ca
be updated by passing (nested) dicts of values.
:return: A copy of inst with *changes* incorporated.
Expand All @@ -337,8 +338,13 @@ def evolve(inst, **changes):
continue
attr_name = a.name # To deal with private attributes.
init_name = attr_name if attr_name[0] != "_" else attr_name[1:]
value = getattr(inst, attr_name)
if init_name not in changes:
changes[init_name] = getattr(inst, attr_name)
# Add original value to changes
changes[init_name] = value
elif has(value):
# Evolve nested attrs classes
changes[init_name] = evolve(value, **changes[init_name])

return cls(**changes)

Expand Down
24 changes: 24 additions & 0 deletions tests/test_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -597,3 +597,27 @@ class C(object):
b = attr.ib(init=False, default=0)

assert evolve(C(1), a=2).a == 2

def test_recursive(self):
"""
evolve() recursively evolves nested attrs classes when a dict is
passed for an attribute.
"""

@attr.s
class N2(object):
e = attr.ib(type=int)

@attr.s
class N1(object):
c = attr.ib(type=N2)
d = attr.ib(type=int)

@attr.s
class C(object):
a = attr.ib(type=N1)
b = attr.ib(type=int)

c1 = C(N1(N2(1), 2), 3)
c2 = evolve(c1, a={"c": {"e": 23}})
assert c2 == C(N1(N2(23), 2), 3)

0 comments on commit 53c1d7a

Please sign in to comment.