Repository: python-attrs/attrs (python)

Issue:
`attr.evolve` no longer supports setting an attr object as attribute
Hi,

I don't know if this is an intentional side-effect of #759, but `attr.evolve` no longer works in the non-recursive recursive case if the argument is an attr class.

Here is a snippet to reproduce it:

```py
import attr

@attr.s
class Cls1:
    param1 = attr.ib()


@attr.s
class Cls2:
    param2 = attr.ib()


obj2a = Cls2(param2="a")
obj2b = Cls2(param2="b")

obj1a = Cls1(param1=obj2a)
obj1b = attr.evolve(obj1a, param1=obj2b)

print(obj1b)
```

Before fe6eb3120013833d1927374eb5e826b72815a775, it printed this: `Cls1(param1=Cls2(param2='b'))`

But after this commit, it raises this error:

```
Traceback (most recent call last):
  File "test.py", line 17, in <module>
    obj1b = attr.evolve(obj1a, param1=obj2b)
  File "/home/dev/.local/lib/python3.7/site-packages/attrs-21.2.0.dev0-py3.7.egg/attr/_funcs.py", line 347, in evolve
    changes[init_name] = evolve(value, **changes[init_name])
TypeError: evolve() argument after ** must be a mapping, not Cls2
```

Thanks!

Pull request description:
Revert recursive evolve
Seems like the current implementation breaks a non-trivial amount of use cases.

So this reverts it and adds a regression test – lmk if I got it right (also the description of the test).

I welcome suggestions how to do it properly in separate PRs.

@ProgVal @staaam @sscherfke 

Fixes #804 

Patch:
diff --git a/changelog.d/806.breaking.rst b/changelog.d/806.breaking.rst
new file mode 100644
index 0000000..baa4c02
--- /dev/null
+++ b/changelog.d/806.breaking.rst
@@ -0,0 +1,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 83810f0..0fac312 100644
--- a/docs/examples.rst
+++ b/docs/examples.rst
@@ -618,27 +618,6 @@ In Clojure that function is called `assoc <https://clojuredocs.org/clojure.core/
    >>> 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 241d4bf..fda508c 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)
 


Test patch:
diff --git a/tests/test_funcs.py b/tests/test_funcs.py
index 1706aea..e957d0e 100644
--- a/tests/test_funcs.py
+++ b/tests/test_funcs.py
@@ -598,43 +598,50 @@ class TestEvolve(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
+        )