Skip to content

Commit 2d6e841

Browse files
committed
preconf: faster enum handling
1 parent 735446d commit 2d6e841

File tree

11 files changed

+281
-26
lines changed

11 files changed

+281
-26
lines changed

HISTORY.md

+4
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ Our backwards-compatibility policy can be found [here](https://github.com/python
2222
- Some `defaultdicts` are now [supported by default](https://catt.rs/en/latest/defaulthooks.html#defaultdicts), and
2323
{func}`cattrs.cols.is_defaultdict`{func} and `cattrs.cols.defaultdict_structure_factory` are exposed through {mod}`cattrs.cols`.
2424
([#519](https://github.com/python-attrs/cattrs/issues/519) [#588](https://github.com/python-attrs/cattrs/pull/588))
25+
- Many preconf converters (_bson_, stdlib JSON, _cbor2_, _msgpack_, _msgspec_, _orjson_, _ujson_) skip unstructuring `int` and `str` enums,
26+
leaving them to the underlying libraries to handle with greater efficiency.
27+
([#598](https://github.com/python-attrs/cattrs/pull/598))
2528
- Literals containing enums are now unstructured properly.
29+
([#598](https://github.com/python-attrs/cattrs/pull/598))
2630
- Replace `cattrs.gen.MappingStructureFn` with `cattrs.SimpleStructureHook[In, T]`.
2731
- Python 3.13 is now supported.
2832
([#543](https://github.com/python-attrs/cattrs/pull/543) [#547](https://github.com/python-attrs/cattrs/issues/547))

docs/preconf.md

+11-2
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,26 @@
22

33
The {mod}`cattrs.preconf` package contains factories for preconfigured converters, specifically adjusted for particular serialization libraries.
44

5-
For example, to get a converter configured for BSON:
5+
For example, to get a converter configured for _orjson_:
66

77
```{doctest}
88
9-
>>> from cattrs.preconf.bson import make_converter
9+
>>> from cattrs.preconf.orjson import make_converter
1010
1111
>>> converter = make_converter() # Takes the same parameters as the `cattrs.Converter`
1212
```
1313

1414
Converters obtained this way can be customized further, just like any other converter.
1515

16+
For compatibility and performance reasons, these converters are usually configured to unstructure differently than ordinary `Converters`.
17+
A couple of examples:
18+
* the {class}`_orjson_ converter <cattrs.preconf.orjson.OrjsonConverter>` is configured to pass `datetime` instances unstructured since _orjson_ can handle them faster.
19+
* the {class}`_msgspec_ JSON converter <cattrs.preconf.msgspec.MsgspecJsonConverter>` is configured to pass through some dataclasses and _attrs_classes,
20+
if the output is identical to what normal unstructuring would have produced, since _msgspec_ can handle them faster.
21+
22+
The intended usage is to pass the unstructured output directly to the underlying library,
23+
or use `converter.dumps` which will do it for you.
24+
1625
These converters support all [default hooks](defaulthooks.md)
1726
and the following additional classes and type annotations,
1827
both for structuring and unstructuring:

src/cattrs/preconf/__init__.py

+11
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import sys
22
from datetime import datetime
3+
from enum import Enum
34
from typing import Any, Callable, TypeVar
45

6+
from .._compat import is_subclass
7+
58
if sys.version_info[:2] < (3, 10):
69
from typing_extensions import ParamSpec
710
else:
@@ -25,3 +28,11 @@ def impl(x: Callable[..., T]) -> Callable[P, T]:
2528
return x
2629

2730
return impl
31+
32+
33+
def is_primitive_enum(type: Any, include_bare_enums: bool = False) -> bool:
34+
"""Is this a string or int enum that can be passed through?"""
35+
return is_subclass(type, Enum) and (
36+
is_subclass(type, (str, int))
37+
or (include_bare_enums and type.mro()[1:] == Enum.mro())
38+
)

src/cattrs/preconf/bson.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111

1212
from ..converters import BaseConverter, Converter
1313
from ..dispatch import StructureHook
14+
from ..fns import identity
1415
from ..strategies import configure_union_passthrough
15-
from . import validate_datetime, wrap
16+
from . import is_primitive_enum, validate_datetime, wrap
1617

1718
T = TypeVar("T")
1819

@@ -52,6 +53,10 @@ def configure_converter(converter: BaseConverter):
5253
* byte mapping keys are base85-encoded into strings when unstructuring, and reverse
5354
* non-string, non-byte mapping keys are coerced into strings when unstructuring
5455
* a deserialization hook is registered for bson.ObjectId by default
56+
* string and int enums are passed through when unstructuring
57+
58+
.. versionchanged: 24.2.0
59+
Enums are left to the library to unstructure, speeding them up.
5560
"""
5661

5762
def gen_unstructure_mapping(cl: Any, unstructure_to=None):
@@ -92,6 +97,7 @@ def gen_structure_mapping(cl: Any) -> StructureHook:
9297
converter.register_structure_hook(datetime, validate_datetime)
9398
converter.register_unstructure_hook(date, lambda v: v.isoformat())
9499
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
100+
converter.register_unstructure_hook_func(is_primitive_enum, identity)
95101

96102

97103
@wrap(BsonConverter)

src/cattrs/preconf/cbor2.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
from cattrs._compat import AbstractSet
99

1010
from ..converters import BaseConverter, Converter
11+
from ..fns import identity
1112
from ..strategies import configure_union_passthrough
12-
from . import wrap
13+
from . import is_primitive_enum, wrap
1314

1415
T = TypeVar("T")
1516

@@ -28,13 +29,15 @@ def configure_converter(converter: BaseConverter):
2829
2930
* datetimes are serialized as timestamp floats
3031
* sets are serialized as lists
32+
* string and int enums are passed through when unstructuring
3133
"""
3234
converter.register_unstructure_hook(datetime, lambda v: v.timestamp())
3335
converter.register_structure_hook(
3436
datetime, lambda v, _: datetime.fromtimestamp(v, timezone.utc)
3537
)
3638
converter.register_unstructure_hook(date, lambda v: v.isoformat())
3739
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
40+
converter.register_unstructure_hook_func(is_primitive_enum, identity)
3841
configure_union_passthrough(Union[str, bool, int, float, None, bytes], converter)
3942

4043

src/cattrs/preconf/json.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77

88
from .._compat import AbstractSet, Counter
99
from ..converters import BaseConverter, Converter
10+
from ..fns import identity
1011
from ..strategies import configure_union_passthrough
11-
from . import wrap
12+
from . import is_primitive_enum, wrap
1213

1314
T = TypeVar("T")
1415

@@ -29,8 +30,12 @@ def configure_converter(converter: BaseConverter):
2930
* datetimes are serialized as ISO 8601
3031
* counters are serialized as dicts
3132
* sets are serialized as lists
33+
* string and int enums are passed through when unstructuring
3234
* union passthrough is configured for unions of strings, bools, ints,
3335
floats and None
36+
37+
.. versionchanged: 24.2.0
38+
Enums are left to the library to unstructure, speeding them up.
3439
"""
3540
converter.register_unstructure_hook(
3641
bytes, lambda v: (b85encode(v) if v else b"").decode("utf8")
@@ -40,6 +45,7 @@ def configure_converter(converter: BaseConverter):
4045
converter.register_structure_hook(datetime, lambda v, _: datetime.fromisoformat(v))
4146
converter.register_unstructure_hook(date, lambda v: v.isoformat())
4247
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
48+
converter.register_unstructure_hook_func(is_primitive_enum, identity)
4349
configure_union_passthrough(Union[str, bool, int, float, None], converter)
4450

4551

src/cattrs/preconf/msgpack.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
from cattrs._compat import AbstractSet
99

1010
from ..converters import BaseConverter, Converter
11+
from ..fns import identity
1112
from ..strategies import configure_union_passthrough
12-
from . import wrap
13+
from . import is_primitive_enum, wrap
1314

1415
T = TypeVar("T")
1516

@@ -28,6 +29,10 @@ def configure_converter(converter: BaseConverter):
2829
2930
* datetimes are serialized as timestamp floats
3031
* sets are serialized as lists
32+
* string and int enums are passed through when unstructuring
33+
34+
.. versionchanged: 24.2.0
35+
Enums are left to the library to unstructure, speeding them up.
3136
"""
3237
converter.register_unstructure_hook(datetime, lambda v: v.timestamp())
3338
converter.register_structure_hook(
@@ -39,6 +44,7 @@ def configure_converter(converter: BaseConverter):
3944
converter.register_structure_hook(
4045
date, lambda v, _: datetime.fromtimestamp(v, timezone.utc).date()
4146
)
47+
converter.register_unstructure_hook_func(is_primitive_enum, identity)
4248
configure_union_passthrough(Union[str, bool, int, float, None, bytes], converter)
4349

4450

src/cattrs/preconf/msgspec.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,15 @@ def configure_converter(converter: Converter) -> None:
7272
* datetimes and dates are passed through to be serialized as RFC 3339 directly
7373
* enums are passed through to msgspec directly
7474
* union passthrough configured for str, bool, int, float and None
75+
* bare, string and int enums are passed through when unstructuring
76+
77+
.. versionchanged: 24.2.0
78+
Enums are left to the library to unstructure, speeding them up.
7579
"""
7680
configure_passthroughs(converter)
7781

7882
converter.register_unstructure_hook(Struct, to_builtins)
79-
converter.register_unstructure_hook(Enum, to_builtins)
83+
converter.register_unstructure_hook(Enum, identity)
8084

8185
converter.register_structure_hook(Struct, convert)
8286
converter.register_structure_hook(bytes, lambda v, _: b64decode(v))
@@ -100,7 +104,7 @@ def configure_passthroughs(converter: Converter) -> None:
100104
converter.register_unstructure_hook(bytes, to_builtins)
101105
converter.register_unstructure_hook_factory(is_mapping, mapping_unstructure_factory)
102106
converter.register_unstructure_hook_factory(is_sequence, seq_unstructure_factory)
103-
converter.register_unstructure_hook_factory(has, attrs_unstructure_factory)
107+
converter.register_unstructure_hook_factory(has, msgspec_attrs_unstructure_factory)
104108
converter.register_unstructure_hook_factory(
105109
is_namedtuple, namedtuple_unstructure_factory
106110
)
@@ -145,7 +149,9 @@ def mapping_unstructure_factory(type, converter: BaseConverter) -> UnstructureHo
145149
return converter.gen_unstructure_mapping(type)
146150

147151

148-
def attrs_unstructure_factory(type: Any, converter: Converter) -> UnstructureHook:
152+
def msgspec_attrs_unstructure_factory(
153+
type: Any, converter: Converter
154+
) -> UnstructureHook:
149155
"""Choose whether to use msgspec handling or our own."""
150156
origin = get_origin(type)
151157
attribs = fields(origin or type)

src/cattrs/preconf/orjson.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from ..converters import BaseConverter, Converter
1414
from ..fns import identity
1515
from ..strategies import configure_union_passthrough
16-
from . import wrap
16+
from . import is_primitive_enum, wrap
1717

1818
T = TypeVar("T")
1919

@@ -36,9 +36,12 @@ def configure_converter(converter: BaseConverter):
3636
* sets are serialized as lists
3737
* string enum mapping keys have special handling
3838
* mapping keys are coerced into strings when unstructuring
39+
* bare, string and int enums are passed through when unstructuring
3940
4041
.. versionchanged: 24.1.0
4142
Add support for typed namedtuples.
43+
.. versionchanged: 24.2.0
44+
Enums are left to the library to unstructure, speeding them up.
4245
"""
4346
converter.register_unstructure_hook(
4447
bytes, lambda v: (b85encode(v) if v else b"").decode("utf8")
@@ -80,6 +83,9 @@ def key_handler(v):
8083
),
8184
]
8285
)
86+
converter.register_unstructure_hook_func(
87+
partial(is_primitive_enum, include_bare_enums=True), identity
88+
)
8389
configure_union_passthrough(Union[str, bool, int, float, None], converter)
8490

8591

src/cattrs/preconf/ujson.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66

77
from ujson import dumps, loads
88

9-
from cattrs._compat import AbstractSet
10-
9+
from .._compat import AbstractSet
1110
from ..converters import BaseConverter, Converter
11+
from ..fns import identity
1212
from ..strategies import configure_union_passthrough
13-
from . import wrap
13+
from . import is_primitive_enum, wrap
1414

1515
T = TypeVar("T")
1616

@@ -30,6 +30,10 @@ def configure_converter(converter: BaseConverter):
3030
* bytes are serialized as base64 strings
3131
* datetimes are serialized as ISO 8601
3232
* sets are serialized as lists
33+
* string and int enums are passed through when unstructuring
34+
35+
.. versionchanged: 24.2.0
36+
Enums are left to the library to unstructure, speeding them up.
3337
"""
3438
converter.register_unstructure_hook(
3539
bytes, lambda v: (b85encode(v) if v else b"").decode("utf8")
@@ -40,6 +44,7 @@ def configure_converter(converter: BaseConverter):
4044
converter.register_structure_hook(datetime, lambda v, _: datetime.fromisoformat(v))
4145
converter.register_unstructure_hook(date, lambda v: v.isoformat())
4246
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
47+
converter.register_unstructure_hook_func(is_primitive_enum, identity)
4348
configure_union_passthrough(Union[str, bool, int, float, None], converter)
4449

4550

0 commit comments

Comments
 (0)