Repository: python-attrs/attrs (python)

Issue:
attr.asdict(retain_collection_types=False) doesn't support frozenset
Hey, I ran into some surprising behavior when using `attr.asdict()` with a frozen set.
I was expecting `retain_collection_types=False` to convert a frozenset to a list, as it does with a tuple or list, but it keeps it as a frozenset

Example
```py3
import attr

@attr.s
class Foo:
    bar = attr.ib()

DATA = (1, 2, 3)
print("set:", attr.asdict(Foo(set(DATA))))
print("frozenset", attr.asdict(Foo(frozenset(DATA))))
```

Output:
```sh
$ python attr_demo.py
set: {'bar': [1, 2, 3]}
frozenset {'bar': frozenset({1, 2, 3})}
```

Expected Output
```sh
$ python attr_demo.py
set: {'bar': [1, 2, 3]}
frozenset  {'bar': [1, 2, 3]}
```


it looks like the code change is adding `frozenset` to the `isinstance` check on https://github.com/python-attrs/attrs/blob/master/src/attr/_funcs.py#L55


Pull request:
Treat frozensets like sets in asdict/astuple
Fixes #657


Patch:
diff --git a/changelog.d/704.change.rst b/changelog.d/704.change.rst
new file mode 100644
index 0000000..26b08e2
--- /dev/null
+++ b/changelog.d/704.change.rst
@@ -0,0 +1,1 @@
+``attr.asdict()`` and ``attr.astuple()`` now treat ``frozenset``\ s like ``set``\ s with regards to the *retain_collection_types* argument.
diff --git a/src/attr/_funcs.py b/src/attr/_funcs.py
index 56e3fcf..e6c930c 100644
--- a/src/attr/_funcs.py
+++ b/src/attr/_funcs.py
@@ -53,8 +53,10 @@ def asdict(
         v = getattr(inst, a.name)
         if filter is not None and not filter(a, v):
             continue
+
         if value_serializer is not None:
             v = value_serializer(inst, a, v)
+
         if recurse is True:
             if has(v.__class__):
                 rv[a.name] = asdict(
@@ -65,7 +67,7 @@ def asdict(
                     retain_collection_types,
                     value_serializer,
                 )
-            elif isinstance(v, (tuple, list, set)):
+            elif isinstance(v, (tuple, list, set, frozenset)):
                 cf = v.__class__ if retain_collection_types is True else list
                 rv[a.name] = cf(
                     [
@@ -127,7 +129,7 @@ def _asdict_anything(
             retain_collection_types,
             value_serializer,
         )
-    elif isinstance(val, (tuple, list, set)):
+    elif isinstance(val, (tuple, list, set, frozenset)):
         cf = val.__class__ if retain_collection_types is True else list
         rv = cf(
             [
@@ -158,6 +160,7 @@ def _asdict_anything(
         rv = val
         if value_serializer is not None:
             rv = value_serializer(None, None, rv)
+
     return rv
 
 
@@ -212,7 +215,7 @@ def astuple(
                         retain_collection_types=retain,
                     )
                 )
-            elif isinstance(v, (tuple, list, set)):
+            elif isinstance(v, (tuple, list, set, frozenset)):
                 cf = v.__class__ if retain is True else list
                 rv.append(
                     cf(
@@ -257,6 +260,7 @@ def astuple(
                 rv.append(v)
         else:
             rv.append(v)
+
     return rv if tuple_factory is list else tuple_factory(rv)
 
 


Test patch:
diff --git a/tests/test_funcs.py b/tests/test_funcs.py
index 4d99e06..20e2747 100644
--- a/tests/test_funcs.py
+++ b/tests/test_funcs.py
@@ -149,12 +149,26 @@ class TestAsDict(object):
             retain_collection_types=True,
         )
 
+    @given(set_type=st.sampled_from((set, frozenset)))
+    def test_sets_no_retain(self, C, set_type):
+        """
+        Set types are converted to lists if retain_collection_types=False.
+        """
+        d = asdict(
+            C(1, set_type((1, 2, 3))),
+            retain_collection_types=False,
+            recurse=True,
+        )
+
+        assert {"x": 1, "y": [1, 2, 3]} == d
+
     @given(st.sampled_from(MAPPING_TYPES))
     def test_dicts(self, C, dict_factory):
         """
         If recurse is True, also recurse into dicts.
         """
         res = asdict(C(1, {"a": C(4, 5)}), dict_factory=dict_factory)
+
         assert {"x": 1, "y": {"a": {"x": 4, "y": 5}}} == res
         assert isinstance(res, dict_factory)
 
@@ -330,6 +344,19 @@ class TestAsTuple(object):
 
         assert instance == roundtrip_instance
 
+    @given(set_type=st.sampled_from((set, frozenset)))
+    def test_sets_no_retain(self, C, set_type):
+        """
+        Set types are converted to lists if retain_collection_types=False.
+        """
+        d = astuple(
+            C(1, set_type((1, 2, 3))),
+            retain_collection_types=False,
+            recurse=True,
+        )
+
+        assert (1, [1, 2, 3]) == d
+
 
 class TestHas(object):
     """