Skip to content

Commit cec0035

Browse files
authored
Add unstructuring and structuring support for deque in standard lib (#355)
* Implement `_structure_deque` and related tests, docs * Adjust location of `test_seqs_deque` * Fix compatibility issue for py3.7/py3.8 and improve docs * Fix typo in docstring of `deques_of_primitives` * Fix incorrect doctest output for example of deques
1 parent 27e9c0d commit cec0035

9 files changed

+184
-10
lines changed

HISTORY.md

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
([#350](https://github.com/python-attrs/cattrs/issues/350) [#353](https://github.com/python-attrs/cattrs/pull/353))
1919
- Subclasses structuring and unstructuring is now supported via a custom `include_subclasses` strategy.
2020
([#312](https://github.com/python-attrs/cattrs/pull/312))
21+
- Add unstructuring and structuring support to `deque` in standard lib.
22+
([#355](https://github.com/python-attrs/cattrs/issues/355))
2123

2224
## 22.2.0 (2022-10-03)
2325

docs/structuring.md

+27
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,33 @@ These generic types are composable with all other converters.
146146
['1', None, '3']
147147
```
148148

149+
### Deques
150+
151+
Deques can be produced from any iterable object. Types converting
152+
to deques are:
153+
154+
- `Deque[T]`
155+
- `deque[T]`
156+
157+
In all cases, a new **unbounded** deque (`maxlen=None`) will be returned,
158+
so this operation can be used to copy an iterable into a deque.
159+
If you want to convert into bounded `deque`, registering a custom structuring hook is a good approach.
160+
161+
```{doctest}
162+
>>> cattrs.structure((1, 2, 3), deque[int])
163+
deque([1, 2, 3])
164+
```
165+
166+
These generic types are composable with all other converters.
167+
168+
```{doctest}
169+
>>> cattrs.structure((1, None, 3), deque[Optional[str]])
170+
deque(['1', None, '3'])
171+
```
172+
173+
```{versionadded} 23.1.0
174+
```
175+
149176
### Sets and Frozensets
150177

151178
Sets and frozensets can be produced from any iterable object. Types converting

docs/unstructuring.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ unstructure all sets into lists, try the following:
8080
Going even further, the Converter contains heuristics to support the
8181
following Python types, in order of decreasing generality:
8282

83-
- `Sequence`, `MutableSequence`, `list`, `tuple`
83+
- `Sequence`, `MutableSequence`, `list`, `deque`, `tuple`
8484
- `Set`, `frozenset`, `MutableSet`, `set`
85-
- `Mapping`, `MutableMapping`, `dict`, `Counter`
85+
- `Mapping`, `MutableMapping`, `dict`, `defaultdict`, `OrderedDict`, `Counter`
8686

8787
For example, if you override the unstructure type for `Sequence`, but not for
8888
`MutableSequence`, `list` or `tuple`, the override will also affect those

src/cattrs/_compat.py

+21-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import builtins
22
import sys
3+
from collections import deque
34
from collections.abc import MutableSet as AbcMutableSet
45
from collections.abc import Set as AbcSet
56
from dataclasses import MISSING
67
from dataclasses import fields as dataclass_fields
78
from dataclasses import is_dataclass
89
from typing import AbstractSet as TypingAbstractSet
9-
from typing import Any, Dict, FrozenSet, List
10+
from typing import Any, Deque, Dict, FrozenSet, List
1011
from typing import Mapping as TypingMapping
1112
from typing import MutableMapping as TypingMutableMapping
1213
from typing import MutableSequence as TypingMutableSequence
@@ -177,6 +178,13 @@ def is_sequence(type: Any) -> bool:
177178
or (type.__origin__ in (Tuple, tuple) and type.__args__[1] is ...)
178179
)
179180

181+
def is_deque(type: Any) -> bool:
182+
return (
183+
type in (deque, Deque)
184+
or (type.__class__ is _GenericAlias and issubclass(type.__origin__, deque))
185+
or type.__origin__ is deque
186+
)
187+
180188
def is_mutable_set(type):
181189
return type is set or (
182190
type.__class__ is _GenericAlias and issubclass(type.__origin__, MutableSet)
@@ -327,8 +335,10 @@ def is_sequence(type: Any) -> bool:
327335
TypingSequence,
328336
TypingMutableSequence,
329337
AbcMutableSequence,
330-
Tuple,
331338
tuple,
339+
Tuple,
340+
deque,
341+
Deque,
332342
)
333343
or (
334344
type.__class__ is _GenericAlias
@@ -339,10 +349,17 @@ def is_sequence(type: Any) -> bool:
339349
and type.__args__[1] is ...
340350
)
341351
)
342-
or (origin in (list, AbcMutableSequence, AbcSequence))
352+
or (origin in (list, deque, AbcMutableSequence, AbcSequence))
343353
or (origin is tuple and type.__args__[1] is ...)
344354
)
345355

356+
def is_deque(type):
357+
return (
358+
type in (deque, Deque)
359+
or (type.__class__ is _GenericAlias and issubclass(type.__origin__, deque))
360+
or (getattr(type, "__origin__", None) is deque)
361+
)
362+
346363
def is_mutable_set(type):
347364
return (
348365
type in (TypingSet, TypingMutableSet, set)
@@ -370,7 +387,7 @@ def is_bare(type):
370387

371388
def is_mapping(type):
372389
return (
373-
type in (TypingMapping, Dict, TypingMutableMapping, dict, AbcMutableMapping)
390+
type in (dict, Dict, TypingMapping, TypingMutableMapping, AbcMutableMapping)
374391
or (
375392
type.__class__ is _GenericAlias
376393
and issubclass(type.__origin__, TypingMapping)

src/cattrs/converters.py

+36-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from collections import Counter
1+
from collections import Counter, deque
22
from collections.abc import MutableSet as AbcMutableSet
33
from dataclasses import Field
44
from enum import Enum
@@ -7,6 +7,7 @@
77
from typing import (
88
Any,
99
Callable,
10+
Deque,
1011
Dict,
1112
Iterable,
1213
List,
@@ -46,6 +47,7 @@
4647
is_annotated,
4748
is_bare,
4849
is_counter,
50+
is_deque,
4951
is_frozenset,
5052
is_generic,
5153
is_generic_attrs,
@@ -193,6 +195,7 @@ def __init__(
193195
(is_literal, self._structure_simple_literal),
194196
(is_literal_containing_enums, self._structure_enum_literal),
195197
(is_sequence, self._structure_list),
198+
(is_deque, self._structure_deque),
196199
(is_mutable_set, self._structure_set),
197200
(is_frozenset, self._structure_frozenset),
198201
(is_tuple, self._structure_tuple),
@@ -326,7 +329,6 @@ def register_structure_hook_factory(
326329

327330
def structure(self, obj: Any, cl: Type[T]) -> T:
328331
"""Convert unstructured Python data structures to structured data."""
329-
330332
return self._structure_func.dispatch(cl)(obj, cl)
331333

332334
# Classes to Python primitives.
@@ -545,6 +547,36 @@ def _structure_list(self, obj: Iterable[T], cl: Any) -> List[T]:
545547
res = [handler(e, elem_type) for e in obj]
546548
return res
547549

550+
def _structure_deque(self, obj: Iterable[T], cl: Any) -> Deque[T]:
551+
"""Convert an iterable to a potentially generic deque."""
552+
if is_bare(cl) or cl.__args__[0] is Any:
553+
res = deque(e for e in obj)
554+
else:
555+
elem_type = cl.__args__[0]
556+
handler = self._structure_func.dispatch(elem_type)
557+
if self.detailed_validation:
558+
errors = []
559+
res = deque()
560+
ix = 0 # Avoid `enumerate` for performance.
561+
for e in obj:
562+
try:
563+
res.append(handler(e, elem_type))
564+
except Exception as e:
565+
msg = IterableValidationNote(
566+
f"Structuring {cl} @ index {ix}", ix, elem_type
567+
)
568+
e.__notes__ = getattr(e, "__notes__", []) + [msg]
569+
errors.append(e)
570+
finally:
571+
ix += 1
572+
if errors:
573+
raise IterableValidationError(
574+
f"While structuring {cl!r}", errors, cl
575+
)
576+
else:
577+
res = deque(handler(e, elem_type) for e in obj)
578+
return res
579+
548580
def _structure_set(
549581
self, obj: Iterable[T], cl: Any, structure_to: type = set
550582
) -> Set[T]:
@@ -823,6 +855,8 @@ def __init__(
823855
if MutableSequence in co:
824856
if list not in co:
825857
co[list] = co[MutableSequence]
858+
if deque not in co:
859+
co[deque] = co[MutableSequence]
826860

827861
# abc.Mapping overrides, if defined, can apply to MutableMappings
828862
if Mapping in co:

tests/test_converter.py

+57
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Test both structuring and unstructuring."""
2+
from collections import deque
23
from typing import (
4+
Deque,
35
FrozenSet,
46
List,
57
MutableSequence,
@@ -524,20 +526,24 @@ class Outer:
524526
(tuple, tuple),
525527
(list, list),
526528
(list, List),
529+
(deque, Deque),
527530
(set, Set),
528531
(set, set),
529532
(frozenset, frozenset),
530533
(frozenset, FrozenSet),
531534
(list, MutableSequence),
535+
(deque, MutableSequence),
532536
(tuple, Sequence),
533537
]
534538
if is_py39_plus
535539
else [
536540
(tuple, Tuple),
537541
(list, List),
542+
(deque, Deque),
538543
(set, Set),
539544
(frozenset, FrozenSet),
540545
(list, MutableSequence),
546+
(deque, MutableSequence),
541547
(tuple, Sequence),
542548
]
543549
),
@@ -563,6 +569,57 @@ def test_seq_of_simple_classes_unstructure(cls_and_vals, seq_type_and_annotation
563569
assert all(e == test_val for e in outputs)
564570

565571

572+
@given(
573+
sampled_from(
574+
[
575+
(tuple, Tuple),
576+
(tuple, tuple),
577+
(list, list),
578+
(list, List),
579+
(deque, deque),
580+
(deque, Deque),
581+
(set, Set),
582+
(set, set),
583+
(frozenset, frozenset),
584+
(frozenset, FrozenSet),
585+
]
586+
if is_py39_plus
587+
else [
588+
(tuple, Tuple),
589+
(list, List),
590+
(deque, Deque),
591+
(set, Set),
592+
(frozenset, FrozenSet),
593+
]
594+
)
595+
)
596+
def test_seq_of_bare_classes_structure(seq_type_and_annotation):
597+
"""Structure iterable of values to a sequence of primitives."""
598+
converter = Converter()
599+
600+
bare_classes = ((int, (1,)), (float, (1.0,)), (str, ("test",)), (bool, (True,)))
601+
seq_type, annotation = seq_type_and_annotation
602+
603+
for cl, vals in bare_classes:
604+
605+
@define(frozen=True)
606+
class C:
607+
a: cl
608+
b: cl
609+
610+
inputs = [{"a": cl(*vals), "b": cl(*vals)} for _ in range(5)]
611+
outputs = converter.structure(
612+
inputs,
613+
cl=annotation[C]
614+
if annotation not in (Tuple, tuple)
615+
else annotation[C, ...],
616+
)
617+
expected = seq_type(C(a=cl(*vals), b=cl(*vals)) for _ in range(5))
618+
619+
assert type(outputs) == seq_type
620+
assert outputs == expected
621+
622+
566623
@pytest.mark.skipif(not is_py39_plus, reason="3.9+ only")
567624
def test_annotated_attrs():
568625
"""Annotation support works for attrs classes."""

tests/test_generics.py

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from typing import Dict, Generic, List, Optional, TypeVar, Union
1+
from collections import deque
2+
from typing import Deque, Dict, Generic, List, Optional, TypeVar, Union
23

34
import pytest
45
from attr import asdict, attrs, define
@@ -145,6 +146,18 @@ class TClass2(Generic[T]):
145146
assert res == data
146147

147148

149+
def test_structure_deque_of_generic_unions(converter):
150+
@attrs(auto_attribs=True)
151+
class TClass2(Generic[T]):
152+
c: T
153+
154+
data = deque((TClass2(c="string"), TClass(1, 2)))
155+
res = converter.structure(
156+
[asdict(x) for x in data], Deque[Union[TClass[int, int], TClass2[str]]]
157+
)
158+
assert res == data
159+
160+
148161
def test_raises_if_no_generic_params_supplied(
149162
converter: Union[Converter, BaseConverter]
150163
):

tests/test_structure.py

+11
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
lists_of_primitives,
3131
primitive_strategies,
3232
seqs_of_primitives,
33+
deque_seqs_of_primitives,
3334
)
3435

3536
NoneType = type(None)
@@ -85,6 +86,16 @@ def test_structuring_seqs(seq_and_type):
8586
assert x == y
8687

8788

89+
@given(deque_seqs_of_primitives)
90+
def test_structuring_seqs_to_deque(seq_and_type):
91+
"""Test structuring sequence generic types."""
92+
converter = BaseConverter()
93+
iterable, t = seq_and_type
94+
converted = converter.structure(iterable, t)
95+
for x, y in zip(iterable, converted):
96+
assert x == y
97+
98+
8899
@given(sets_of_primitives, set_types)
89100
def test_structuring_sets(set_and_type, set_type):
90101
"""Test structuring generic sets."""

tests/untyped.py

+14-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from enum import Enum
66
from typing import (
77
Any,
8+
Deque,
89
Dict,
910
List,
1011
Mapping,
@@ -57,6 +58,7 @@ def enums_of_primitives(draw):
5758

5859

5960
list_types = st.sampled_from([List, Sequence, MutableSequence])
61+
deque_types = st.sampled_from([Deque, Sequence, MutableSequence])
6062
set_types = st.sampled_from([Set, MutableSet])
6163

6264

@@ -71,6 +73,17 @@ def lists_of_primitives(draw):
7173
return draw(st.lists(prim_strat)), list_t
7274

7375

76+
@st.composite
77+
def deques_of_primitives(draw):
78+
"""Generate a strategy that yields tuples of list of primitives and types.
79+
80+
For example, a sample value might be ([1,2], Deque[int]).
81+
"""
82+
prim_strat, t = draw(primitive_strategies)
83+
deque_t = draw(deque_types.map(lambda deque_t: deque_t[t]) | deque_types)
84+
return draw(st.lists(prim_strat)), deque_t
85+
86+
7487
@st.composite
7588
def mut_sets_of_primitives(draw):
7689
"""A strategy that generates mutable sets of primitives."""
@@ -98,7 +111,7 @@ def frozen_sets_of_primitives(draw):
98111
dict_types = st.sampled_from([Dict, MutableMapping, Mapping])
99112

100113
seqs_of_primitives = st.one_of(lists_of_primitives(), h_tuples_of_primitives)
101-
114+
deque_seqs_of_primitives = st.one_of(deques_of_primitives(), h_tuples_of_primitives)
102115
sets_of_primitives = st.one_of(mut_sets_of_primitives(), frozen_sets_of_primitives())
103116

104117

0 commit comments

Comments
 (0)