From 53c1d7ab407a5123eea7fa45f54f490539aec441 Mon Sep 17 00:00:00 2001 From: Stefan Scherfke Date: Sun, 14 Feb 2021 22:03:30 +0100 Subject: [PATCH 1/3] Recursively evolve nested attrs classes Fixes: #634 --- changelog.d/759.change.rst | 1 + docs/examples.rst | 21 +++++++++++++++++++++ src/attr/_funcs.py | 10 ++++++++-- tests/test_funcs.py | 24 ++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 changelog.d/759.change.rst diff --git a/changelog.d/759.change.rst b/changelog.d/759.change.rst new file mode 100644 index 000000000..81febca6d --- /dev/null +++ b/changelog.d/759.change.rst @@ -0,0 +1 @@ +Let ``evolve()`` work with nested ``attrs`` classes. #634 diff --git a/docs/examples.rst b/docs/examples.rst index 0fac312a0..196e93534 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -618,6 +618,27 @@ In Clojure that function is called `assoc >> 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 ------------- diff --git a/src/attr/_funcs.py b/src/attr/_funcs.py index e6c930cbd..44c1f20f2 100644 --- a/src/attr/_funcs.py +++ b/src/attr/_funcs.py @@ -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. @@ -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) diff --git a/tests/test_funcs.py b/tests/test_funcs.py index 2fc73dced..13b1c6c79 100644 --- a/tests/test_funcs.py +++ b/tests/test_funcs.py @@ -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) From c94600ed3740b93a87779323014db561d8432803 Mon Sep 17 00:00:00 2001 From: Stefan Scherfke Date: Thu, 18 Feb 2021 20:30:54 +0100 Subject: [PATCH 2/3] Apply suggestions from code review Co-authored-by: Hynek Schlawack --- changelog.d/759.change.rst | 2 +- docs/examples.rst | 2 +- src/attr/_funcs.py | 4 ++-- tests/test_funcs.py | 1 + 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/changelog.d/759.change.rst b/changelog.d/759.change.rst index 81febca6d..e627f5ea3 100644 --- a/changelog.d/759.change.rst +++ b/changelog.d/759.change.rst @@ -1 +1 @@ -Let ``evolve()`` work with nested ``attrs`` classes. #634 +``attrs.evolve()`` now works recursively with nested ``attrs`` classes. diff --git a/docs/examples.rst b/docs/examples.rst index 196e93534..83810f034 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -619,7 +619,7 @@ In Clojure that function is called `assoc Date: Thu, 18 Feb 2021 20:36:57 +0100 Subject: [PATCH 3/3] Update tests for recursive evolve() --- tests/test_funcs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_funcs.py b/tests/test_funcs.py index de2afb755..6b9052acb 100644 --- a/tests/test_funcs.py +++ b/tests/test_funcs.py @@ -619,6 +619,6 @@ class C(object): b = attr.ib(type=int) c1 = C(N1(N2(1), 2), 3) - c2 = evolve(c1, a={"c": {"e": 23}}) + c2 = evolve(c1, a={"c": {"e": 23}}, b=42) - assert c2 == C(N1(N2(23), 2), 3) + assert c2 == C(N1(N2(23), 2), 42)