Skip to content

Commit 536bb4b

Browse files
committed
Implement loads and dumps for preconf converters
1 parent fe21a1d commit 536bb4b

14 files changed

+236
-35
lines changed

HISTORY.rst

+3-2
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,16 @@ History
1111
(`#218 <https://github.com/python-attrs/cattrs/issues/218>`_)
1212
* Fix structuring bare ``typing.Tuple`` on Pythons lower than 3.9.
1313
(`#218 <https://github.com/python-attrs/cattrs/issues/218>`_)
14-
* Fix a wrong ``AttributeError`` of an missing ``__parameters__`` attribute. This could happen
15-
when inheriting certain generic classes – for example ``typing.*`` classes are affected.
14+
* Fix a wrong ``AttributeError`` of an missing ``__parameters__`` attribute. This could happen
15+
when inheriting certain generic classes – for example ``typing.*`` classes are affected.
1616
(`#217 <https://github.com/python-attrs/cattrs/issues/217>`_)
1717
* Fix structuring of ``enum.Enum`` instances in ``typing.Literal`` types.
1818
(`#231 <https://github.com/python-attrs/cattrs/pull/231>`_)
1919
* Fix unstructuring all tuples - unannotated, variable-length, homogenous and heterogenous - to `list`.
2020
(`#226 <https://github.com/python-attrs/cattrs/issues/226>`_)
2121
* For ``forbid_extra_keys`` raise custom ``ForbiddenExtraKeyError`` instead of generic ``Exception``.
2222
(`#255 <https://github.com/python-attrs/cattrs/pull/225>`_)
23+
* All preconf converters now support ``loads`` and ``dumps`` directly.
2324

2425
1.10.0 (2022-01-04)
2526
-------------------

docs/conf.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@
276276
# texinfo_no_detailmenu = False
277277

278278
doctest_global_setup = (
279-
"import attr, cattr;"
279+
"import attr, cattr, cattrs;"
280280
"from attr import Factory, define, field;"
281281
"from typing import *;"
282282
"from enum import Enum, unique"

docs/customizing.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ when fields have default values this may help with catching typos.
9797
creating structure hooks with ``make_dict_structure_fn``.
9898

9999
.. doctest::
100+
:options: +SKIP
100101

101102
>>> from cattr.gen import make_dict_structure_fn
102103
>>>
@@ -164,4 +165,3 @@ or unstructuring function.
164165
>>> c.register_unstructure_hook(ExampleClass, unst_hook)
165166
>>> c.unstructure(ExampleClass(1))
166167
{}
167-

docs/preconf.rst

+16
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,22 @@ These converters support the following classes and type annotations, both for st
2222
* sequences, mutable sequences, mappings, mutable mappings, sets, mutable sets
2323
* ``datetime.datetime``
2424

25+
.. versionadded:: 22.1.0
26+
All preconf converters now have ``loads`` and ``dumps`` methods, which combine un/structuring and the de/serialization logic from their underlying libraries.
27+
28+
.. doctest::
29+
30+
>>> from cattr.preconf.json import make_converter
31+
>>> converter = make_converter()
32+
33+
>>> @define
34+
... class Test:
35+
... a: int
36+
>>>
37+
38+
>>> converter.dumps(Test(1))
39+
'{"a": 1}'
40+
2541
Particular libraries may have additional constraints documented below.
2642

2743
Standard library ``json``

docs/structuring.rst

+6-6
Original file line numberDiff line numberDiff line change
@@ -461,19 +461,19 @@ Here's a small example showing how to use factory hooks to apply the `forbid_ext
461461
.. doctest::
462462
463463
>>> from attr import define, has
464-
>>> from cattr.gen import make_dict_structure_fn
465-
466-
>>> c = cattr.GenConverter()
467-
>>> c.register_structure_hook_factory(has, lambda cl: make_dict_structure_fn(cl, c, _cattrs_forbid_extra_keys=True))
464+
>>> from cattrs.gen import make_dict_structure_fn
465+
466+
>>> c = cattrs.GenConverter()
467+
>>> c.register_structure_hook_factory(has, lambda cl: make_dict_structure_fn(cl, c, _cattrs_forbid_extra_keys=True, _cattrs_detailed_validation=False))
468468
469469
>>> @define
470470
... class E:
471471
... an_int: int
472-
472+
473473
>>> c.structure({"an_int": 1, "else": 2}, E)
474474
Traceback (most recent call last):
475475
...
476-
ForbiddenExtraKeyError: Extra fields in constructor for E: else
476+
cattrs.errors.ForbiddenExtraKeysError: Extra fields in constructor for E: else
477477
478478
479479
A complex use case for hook factories is described over at :ref:`Using factory hooks`.

docs/validation.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,4 @@ Non-detailed validation
7171
-----------------------
7272
7373
Non-detailed validation can be enabled by initializing any of the converters with ``detailed_validation=False``.
74-
In this mode, any errors during un/structuring will bubble up directly as soon as they happen
74+
In this mode, any errors during un/structuring will bubble up directly as soon as they happen.

src/cattr/preconf/bson.py

+34-4
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,47 @@
11
"""Preconfigured converters for bson."""
22
from datetime import datetime
3-
from typing import Any
3+
from typing import Any, Type, TypeVar
4+
5+
from bson import DEFAULT_CODEC_OPTIONS, CodecOptions, ObjectId, decode, encode
6+
7+
from cattrs._compat import Set, is_mapping
48

5-
from .._compat import Set, is_mapping
69
from ..converters import GenConverter
710
from . import validate_datetime
811

12+
T = TypeVar("T")
13+
14+
15+
class BsonConverter(GenConverter):
16+
def dumps(
17+
self,
18+
obj: Any,
19+
unstructure_as=None,
20+
check_keys: bool = False,
21+
codec_options: CodecOptions = DEFAULT_CODEC_OPTIONS,
22+
) -> bytes:
23+
return encode(
24+
self.unstructure(obj, unstructure_as=unstructure_as),
25+
check_keys=check_keys,
26+
codec_options=codec_options,
27+
)
28+
29+
def loads(
30+
self,
31+
data: bytes,
32+
cl: Type[T],
33+
codec_options: CodecOptions = DEFAULT_CODEC_OPTIONS,
34+
) -> T:
35+
return self.structure(decode(data, codec_options=codec_options), cl)
36+
937

1038
def configure_converter(converter: GenConverter):
1139
"""
1240
Configure the converter for use with the bson library.
1341
1442
* sets are serialized as lists
1543
* non-string mapping keys are coerced into strings when unstructuring
44+
* a deserialization hook is registered for bson.ObjectId by default
1645
"""
1746

1847
def gen_unstructure_mapping(cl: Any, unstructure_to=None):
@@ -29,14 +58,15 @@ def gen_unstructure_mapping(cl: Any, unstructure_to=None):
2958
)
3059

3160
converter.register_structure_hook(datetime, validate_datetime)
61+
converter.register_structure_hook(ObjectId, lambda v, _: ObjectId(v))
3262

3363

34-
def make_converter(*args, **kwargs) -> GenConverter:
64+
def make_converter(*args, **kwargs) -> BsonConverter:
3565
kwargs["unstruct_collection_overrides"] = {
3666
**kwargs.get("unstruct_collection_overrides", {}),
3767
Set: list,
3868
}
39-
res = GenConverter(*args, **kwargs)
69+
res = BsonConverter(*args, **kwargs)
4070
configure_converter(res)
4171

4272
return res

src/cattr/preconf/json.py

+16-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
11
"""Preconfigured converters for the stdlib json."""
22
from base64 import b85decode, b85encode
33
from datetime import datetime
4+
from json import dumps, loads
5+
from typing import Any, Type, TypeVar, Union
6+
7+
from cattrs._compat import Counter, Set
48

5-
from .._compat import Counter, Set
69
from ..converters import Converter, GenConverter
710

11+
T = TypeVar("T")
12+
13+
14+
class JsonConverter(GenConverter):
15+
def dumps(self, obj: Any, unstructure_as=None, **kwargs) -> str:
16+
return dumps(self.unstructure(obj, unstructure_as=unstructure_as), **kwargs)
17+
18+
def loads(self, data: Union[bytes, str], cl: Type[T], **kwargs) -> T:
19+
return self.structure(loads(data, **kwargs), cl)
20+
821

922
def configure_converter(converter: Converter):
1023
"""
@@ -23,13 +36,13 @@ def configure_converter(converter: Converter):
2336
converter.register_structure_hook(datetime, lambda v, _: datetime.fromisoformat(v))
2437

2538

26-
def make_converter(*args, **kwargs) -> GenConverter:
39+
def make_converter(*args, **kwargs) -> JsonConverter:
2740
kwargs["unstruct_collection_overrides"] = {
2841
**kwargs.get("unstruct_collection_overrides", {}),
2942
Set: list,
3043
Counter: dict,
3144
}
32-
res = GenConverter(*args, **kwargs)
45+
res = JsonConverter(*args, **kwargs)
3346
configure_converter(res)
3447

3548
return res

src/cattr/preconf/msgpack.py

+17-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
11
"""Preconfigured converters for msgpack."""
22
from datetime import datetime, timezone
3+
from typing import Any, Type, TypeVar
4+
5+
from msgpack import dumps, loads
6+
7+
from cattrs._compat import Set
38

4-
from .._compat import Set
59
from ..converters import GenConverter
610

11+
T = TypeVar("T")
12+
13+
14+
class MsgpackConverter(GenConverter):
15+
def dumps(self, obj: Any, unstructure_as=None, **kwargs) -> bytes:
16+
return dumps(self.unstructure(obj, unstructure_as=unstructure_as), **kwargs)
17+
18+
def loads(self, data: bytes, cl: Type[T], **kwargs) -> T:
19+
return self.structure(loads(data, **kwargs), cl)
20+
721

822
def configure_converter(converter: GenConverter):
923
"""
@@ -18,12 +32,12 @@ def configure_converter(converter: GenConverter):
1832
)
1933

2034

21-
def make_converter(*args, **kwargs) -> GenConverter:
35+
def make_converter(*args, **kwargs) -> MsgpackConverter:
2236
kwargs["unstruct_collection_overrides"] = {
2337
**kwargs.get("unstruct_collection_overrides", {}),
2438
Set: list,
2539
}
26-
res = GenConverter(*args, **kwargs)
40+
res = MsgpackConverter(*args, **kwargs)
2741
configure_converter(res)
2842

2943
return res

src/cattr/preconf/orjson.py

+17-4
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,24 @@
22
from base64 import b85decode, b85encode
33
from datetime import datetime
44
from enum import Enum
5-
from typing import Any
5+
from typing import Any, Type, TypeVar
6+
7+
from orjson import dumps, loads
8+
9+
from cattrs._compat import Set, is_mapping
610

7-
from .._compat import Set, is_mapping
811
from ..converters import GenConverter
912

13+
T = TypeVar("T")
14+
15+
16+
class OrjsonConverter(GenConverter):
17+
def dumps(self, obj: Any, unstructure_as=None, **kwargs) -> bytes:
18+
return dumps(self.unstructure(obj, unstructure_as=unstructure_as), **kwargs)
19+
20+
def loads(self, data: bytes, cl: Type[T]) -> T:
21+
return self.structure(loads(data), cl)
22+
1023

1124
def configure_converter(converter: GenConverter):
1225
"""
@@ -43,12 +56,12 @@ def key_handler(v):
4356
)
4457

4558

46-
def make_converter(*args, **kwargs) -> GenConverter:
59+
def make_converter(*args, **kwargs) -> OrjsonConverter:
4760
kwargs["unstruct_collection_overrides"] = {
4861
**kwargs.get("unstruct_collection_overrides", {}),
4962
Set: list,
5063
}
51-
res = GenConverter(*args, **kwargs)
64+
res = OrjsonConverter(*args, **kwargs)
5265
configure_converter(res)
5366

5467
return res

src/cattr/preconf/pyyaml.py

+17-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,24 @@
11
"""Preconfigured converters for pyyaml."""
22
from datetime import datetime
3+
from typing import Any, Type, TypeVar
4+
5+
from yaml import safe_dump, safe_load
6+
7+
from cattrs._compat import FrozenSetSubscriptable
38

4-
from .._compat import FrozenSetSubscriptable
59
from ..converters import GenConverter
610
from . import validate_datetime
711

12+
T = TypeVar("T")
13+
14+
15+
class PyyamlConverter(GenConverter):
16+
def dumps(self, obj: Any, unstructure_as=None, **kwargs) -> str:
17+
return safe_dump(self.unstructure(obj, unstructure_as=unstructure_as), **kwargs)
18+
19+
def loads(self, data: str, cl: Type[T]) -> T:
20+
return self.structure(safe_load(data), cl)
21+
822

923
def configure_converter(converter: GenConverter):
1024
"""
@@ -19,12 +33,12 @@ def configure_converter(converter: GenConverter):
1933
converter.register_structure_hook(datetime, validate_datetime)
2034

2135

22-
def make_converter(*args, **kwargs) -> GenConverter:
36+
def make_converter(*args, **kwargs) -> PyyamlConverter:
2337
kwargs["unstruct_collection_overrides"] = {
2438
**kwargs.get("unstruct_collection_overrides", {}),
2539
FrozenSetSubscriptable: list,
2640
}
27-
res = GenConverter(*args, **kwargs)
41+
res = PyyamlConverter(*args, **kwargs)
2842
configure_converter(res)
2943

3044
return res

src/cattr/preconf/tomlkit.py

+17-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
11
"""Preconfigured converters for tomlkit."""
22
from base64 import b85decode, b85encode
33
from datetime import datetime
4-
from typing import Any
4+
from typing import Any, Type, TypeVar
5+
6+
from tomlkit import dumps, loads
7+
8+
from cattrs._compat import Set, is_mapping
59

6-
from .._compat import Set, is_mapping
710
from ..converters import GenConverter
811
from . import validate_datetime
912

13+
T = TypeVar("T")
14+
15+
16+
class TomlkitConverter(GenConverter):
17+
def dumps(self, obj: Any, unstructure_as=None, **kwargs) -> str:
18+
return dumps(self.unstructure(obj, unstructure_as=unstructure_as), **kwargs)
19+
20+
def loads(self, data: str, cl: Type[T]) -> T:
21+
return self.structure(loads(data), cl)
22+
1023

1124
def configure_converter(converter: GenConverter):
1225
"""
@@ -37,13 +50,13 @@ def gen_unstructure_mapping(cl: Any, unstructure_to=None):
3750
converter.register_structure_hook(datetime, validate_datetime)
3851

3952

40-
def make_converter(*args, **kwargs) -> GenConverter:
53+
def make_converter(*args, **kwargs) -> TomlkitConverter:
4154
kwargs["unstruct_collection_overrides"] = {
4255
**kwargs.get("unstruct_collection_overrides", {}),
4356
Set: list,
4457
tuple: list,
4558
}
46-
res = GenConverter(*args, **kwargs)
59+
res = TomlkitConverter(*args, **kwargs)
4760
configure_converter(res)
4861

4962
return res

0 commit comments

Comments
 (0)