From f10d05086417b2e2c3f88ae61384ca4c282c0acc Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 6 May 2021 15:26:27 +0200 Subject: [PATCH] Revert recursive evolve (#806) * Revert "Recursively evolve nested attrs classes (#759)" * Add regression test * lol legacy python * Add newsfragment * Add a test to prevent inst -> dict replacement breaking --- changelog.d/806.breaking.rst | 1 + docs/examples.rst | 21 -------------- src/attr/_funcs.py | 10 ++----- tests/test_funcs.py | 55 ++++++++++++++++++++---------------- 4 files changed, 34 insertions(+), 53 deletions(-) create mode 100644 changelog.d/806.breaking.rst diff --git a/changelog.d/806.breaking.rst b/changelog.d/806.breaking.rst new file mode 100644 index 000000000..baa4c02fe --- /dev/null +++ b/changelog.d/806.breaking.rst @@ -0,0 +1 @@ +We had to revert the recursive feature for ``attr.evolve()`` because it broke some use-cases -- sorry! diff --git a/docs/examples.rst b/docs/examples.rst index 83810f034..0fac312a0 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -618,27 +618,6 @@ In Clojure that function is called `assoc >> i1 == i2 False -This functions also works for nested ``attrs`` classes. -Pass a (possibly nested) dictionary 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 241d4bfc7..fda508c5c 100644 --- a/src/attr/_funcs.py +++ b/src/attr/_funcs.py @@ -319,8 +319,7 @@ 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. Nested ``attrs`` classes - can be updated by passing (nested) dicts of values. + :param changes: Keyword changes in the new copy. :return: A copy of inst with *changes* incorporated. @@ -338,13 +337,8 @@ 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: - # Add original value to changes - changes[init_name] = value - elif has(value): - # Evolve nested attrs classes - changes[init_name] = evolve(value, **changes[init_name]) + changes[init_name] = getattr(inst, attr_name) return cls(**changes) diff --git a/tests/test_funcs.py b/tests/test_funcs.py index 1706aea4b..e957d0e38 100644 --- a/tests/test_funcs.py +++ b/tests/test_funcs.py @@ -598,43 +598,50 @@ class C(object): assert evolve(C(1), a=2).a == 2 - def test_recursive(self): + def test_regression_attrs_classes(self): """ - evolve() recursively evolves nested attrs classes when a dict is - passed for an attribute. + evolve() can evolve fields that are instances of attrs classes. + + Regression test for #804 """ @attr.s - class N2(object): - e = attr.ib(type=int) + class Cls1(object): + param1 = attr.ib() @attr.s - class N1(object): - c = attr.ib(type=N2) - d = attr.ib(type=int) + class Cls2(object): + param2 = attr.ib() - @attr.s - class C(object): - a = attr.ib(type=N1) - b = attr.ib(type=int) + obj2a = Cls2(param2="a") + obj2b = Cls2(param2="b") - c1 = C(N1(N2(1), 2), 3) - c2 = evolve(c1, a={"c": {"e": 23}}, b=42) + obj1a = Cls1(param1=obj2a) - assert c2 == C(N1(N2(23), 2), 42) + assert Cls1(param1=Cls2(param2="b")) == attr.evolve( + obj1a, param1=obj2b + ) - def test_recursive_dict_val(self): + def test_dicts(self): """ - evolve() only attempts recursion when the current value is an ``attrs`` - class. Dictionaries as values can be replaced like any other value. + evolve() can replace an attrs class instance with a dict. + + See #806 """ @attr.s - class C(object): - a = attr.ib(type=dict) - b = attr.ib(type=int) + class Cls1(object): + param1 = attr.ib() + + @attr.s + class Cls2(object): + param2 = attr.ib() - c1 = C({"spam": 1}, 2) - c2 = evolve(c1, a={"eggs": 2}, b=42) + obj2a = Cls2(param2="a") + obj2b = {"foo": 42, "param2": 42} - assert c2 == C({"eggs": 2}, 42) + obj1a = Cls1(param1=obj2a) + + assert Cls1({"foo": 42, "param2": 42}) == attr.evolve( + obj1a, param1=obj2b + )