Skip to content

Commit d7f6d6f

Browse files
authored
msgspec support (#481)
* msgspec first pass * Fix typing import * Test carefully for PyPy * Docs * Fix typing wrapper * Fix PyPy CI some more * Remove unused paramspec * Use msgspec's datetime structurer * More msgspec * Ignore _cpython tests on PyPy * More msgspec * More doc work * Fix * Docs * Fix test * More msgspec work * Pass through mapping to msgspec * Fix counters
1 parent 6e90368 commit d7f6d6f

30 files changed

+591
-79
lines changed

HISTORY.md

+5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ Our backwards-compatibility policy can be found [here](https://github.com/python
1717
([#473](https://github.com/python-attrs/cattrs/pull/473))
1818
- Introduce {meth}`BaseConverter.get_structure_hook` and {meth}`BaseConverter.get_unstructure_hook` methods.
1919
([#432](https://github.com/python-attrs/cattrs/issues/432) [#472](https://github.com/python-attrs/cattrs/pull/472))
20+
- Introduce the [_msgspec_](https://jcristharif.com/msgspec/) {mod}`preconf converter <cattrs.preconf.msgspec>`.
21+
Only JSON is supported for now, with other formats supported by _msgspec_ to come later.
22+
([#481](https://github.com/python-attrs/cattrs/pull/481))
2023
- The default union handler now properly takes renamed fields into account.
2124
([#472](https://github.com/python-attrs/cattrs/pull/472))
2225
- The default union handler now also handles dataclasses.
@@ -25,6 +28,8 @@ Our backwards-compatibility policy can be found [here](https://github.com/python
2528
([#452](https://github.com/python-attrs/cattrs/pull/452))
2629
- The `include_subclasses` strategy now fetches the member hooks from the converter (making use of converter defaults) if overrides are not provided, instead of generating new hooks with no overrides.
2730
([#429](https://github.com/python-attrs/cattrs/issues/429) [#472](https://github.com/python-attrs/cattrs/pull/472))
31+
- The preconf `make_converter` factories are now correctly typed.
32+
([#481](https://github.com/python-attrs/cattrs/pull/481))
2833
- The {class}`orjson preconf converter <cattrs.preconf.orjson.OrjsonConverter>` now passes through dates and datetimes to orjson while unstructuring, greatly improving speed.
2934
([#463](https://github.com/python-attrs/cattrs/pull/463))
3035
- `cattrs.gen` generators now attach metadata to the generated functions, making them introspectable.

docs/_static/custom.css

+2-2
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ span:target ~ h6:first-of-type {
7272

7373
div.article-container > article {
7474
font-size: 17px;
75-
line-height: 31px;
75+
line-height: 29px;
7676
}
7777

7878
div.admonition {
@@ -89,7 +89,7 @@ p.admonition-title {
8989

9090
article > li > a {
9191
font-size: 19px;
92-
line-height: 31px;
92+
line-height: 29px;
9393
}
9494

9595
div.tab-set {

docs/cattrs.preconf.rst

+8
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ cattrs.preconf.msgpack module
4141
:undoc-members:
4242
:show-inheritance:
4343

44+
cattrs.preconf.msgspec module
45+
-----------------------------
46+
47+
.. automodule:: cattrs.preconf.msgspec
48+
:members:
49+
:undoc-members:
50+
:show-inheritance:
51+
4452
cattrs.preconf.orjson module
4553
----------------------------
4654

docs/customizing.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
This section describes customizing the unstructuring and structuring processes in _cattrs_.
44

5-
## Manual Un/structuring Hooks
5+
## Custom Un/structuring Hooks
66

77
You can write your own structuring and unstructuring functions and register them for types using {meth}`Converter.register_structure_hook() <cattrs.BaseConverter.register_structure_hook>` and {meth}`Converter.register_unstructure_hook() <cattrs.BaseConverter.register_unstructure_hook>`.
88
This approach is the most flexible but also requires the most amount of boilerplate.

docs/index.md

+19-1
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,40 @@
22
---
33
maxdepth: 2
44
hidden: true
5+
caption: Introduction
56
---
67
78
self
89
basics
910
defaulthooks
11+
```
12+
13+
```{toctree}
14+
---
15+
maxdepth: 2
16+
hidden: true
17+
caption: User Guide
18+
---
19+
1020
customizing
1121
strategies
1222
validation
1323
preconf
1424
unions
1525
usage
1626
indepth
27+
```
28+
29+
```{toctree}
30+
---
31+
maxdepth: 2
32+
hidden: true
33+
caption: Dev Guide
34+
---
35+
1736
history
1837
benchmarking
1938
contributing
20-
API <modules>
2139
```
2240

2341
```{include} ../README.md

docs/preconf.md

+61-13
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ Optional install targets should match the name of the {mod}`cattrs.preconf` modu
4444
# Using pip
4545
$ pip install cattrs[ujson]
4646

47+
# Using pdm
48+
$ pdm add cattrs[orjson]
49+
4750
# Using poetry
4851
$ poetry add --extras tomlkit cattrs
4952
```
@@ -56,15 +59,6 @@ Found at {mod}`cattrs.preconf.json`.
5659
Bytes are serialized as base 85 strings. Counters are serialized as dictionaries. Sets are serialized as lists, and deserialized back into sets. `datetime` s and `date` s are serialized as ISO 8601 strings.
5760

5861

59-
## _ujson_
60-
61-
Found at {mod}`cattrs.preconf.ujson`.
62-
63-
Bytes are serialized as base 85 strings. Sets are serialized as lists, and deserialized back into sets. `datetime` s and `date` s are serialized as ISO 8601 strings.
64-
65-
`ujson` doesn't support integers less than -9223372036854775808, and greater than 9223372036854775807, nor does it support `float('inf')`.
66-
67-
6862
## _orjson_
6963

7064
Found at {mod}`cattrs.preconf.orjson`.
@@ -77,6 +71,61 @@ _orjson_ doesn't support integers less than -9223372036854775808, and greater th
7771
_orjson_ only supports mappings with string keys so mappings will have their keys stringified before serialization, and destringified during deserialization.
7872

7973

74+
## _msgspec_
75+
76+
Found at {mod}`cattrs.preconf.msgspec`.
77+
Only JSON functionality is currently available, other formats supported by msgspec to follow in the future.
78+
79+
[_msgspec_ structs](https://jcristharif.com/msgspec/structs.html) are supported, but not composable - a struct will be handed over to _msgspec_ directly, and _msgspec_ will handle and all of its fields, recursively.
80+
_cattrs_ may get more sophisticated handling of structs in the future.
81+
82+
[_msgspec_ strict mode](https://jcristharif.com/msgspec/usage.html#strict-vs-lax-mode) is used by default.
83+
This can be customized by changing the {meth}`encoder <cattrs.preconf.msgspec.MsgspecJsonConverter.encoder>` attribute on the converter.
84+
85+
What _cattrs_ calls _unstructuring_ and _structuring_, _msgspec_ calls [`to_builtins` and `convert`](https://jcristharif.com/msgspec/converters.html).
86+
What _cattrs_ refers to as _dumping_ and _loading_, _msgspec_ refers to as [`encoding` and `decoding`](https://jcristharif.com/msgspec/usage.html).
87+
88+
Compatibility notes:
89+
- Bytes are un/structured as base 64 strings directly by _msgspec_ itself.
90+
- _msgspec_ [encodes special float values](https://jcristharif.com/msgspec/supported-types.html#float) (`NaN, Inf, -Inf`) as `null`.
91+
- `datetime` s and `date` s are passed through to be unstructured into RFC 3339 by _msgspec_ itself.
92+
- _attrs_ classes, dataclasses and sequences are handled directly by _msgspec_ if possible, otherwise by the normal _cattrs_ machinery.
93+
This means it's possible the validation errors produced may be _msgspec_ validation errors instead of _cattrs_ validation errors.
94+
95+
This converter supports {meth}`get_loads_hook() <cattrs.preconf.msgspec.MsgspecJsonConverter.get_loads_hook>` and {meth}`get_dumps_hook() <cattrs.preconf.msgspec.MsgspecJsonConverter.get_loads_hook>`.
96+
These are factories for dumping and loading functions (as opposed to unstructuring and structuring); the hooks returned by this may be further optimized to offload as much work as possible to _msgspec_.
97+
98+
```python
99+
>>> from cattrs.preconf.msgspec import make_converter
100+
101+
>>> @define
102+
... class Test:
103+
... a: int
104+
105+
>>> converter = make_converter()
106+
>>> dumps = converter.get_dumps_hook(A)
107+
108+
>>> dumps(Test(1)) # Will use msgspec directly.
109+
b'{"a":1}'
110+
```
111+
112+
Due to its complexity, this converter is currently _provisional_ and may slightly change as the best integration patterns are discovered.
113+
114+
_msgspec_ doesn't support PyPy.
115+
116+
```{versionadded} 24.1.0
117+
118+
```
119+
120+
## _ujson_
121+
122+
Found at {mod}`cattrs.preconf.ujson`.
123+
124+
Bytes are serialized as base 85 strings. Sets are serialized as lists, and deserialized back into sets. `datetime` s and `date` s are serialized as ISO 8601 strings.
125+
126+
_ujson_ doesn't support integers less than -9223372036854775808, and greater than 9223372036854775807, nor does it support `float('inf')`.
127+
128+
80129
## _msgpack_
81130

82131
Found at {mod}`cattrs.preconf.msgpack`.
@@ -90,10 +139,6 @@ When parsing msgpack data from bytes, the library needs to be passed `strict_map
90139

91140
## _cbor2_
92141

93-
```{versionadded} 23.1.0
94-
95-
```
96-
97142
Found at {mod}`cattrs.preconf.cbor2`.
98143

99144
_cbor2_ implements a fully featured CBOR encoder with several extensions for handling shared references, big integers, rational numbers and so on.
@@ -112,6 +157,9 @@ Use keyword argument `canonical=True` for efficient encoding to the smallest bin
112157
Floats can be forced to smaller output by casting to lower-precision formats by casting to `numpy` floats (and back to Python floats).
113158
Example: `float(np.float32(value))` or `float(np.float16(value))`
114159

160+
```{versionadded} 23.1.0
161+
162+
```
115163

116164
## _bson_
117165

pdm.lock

+46-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+3
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ cbor2 = [
9494
bson = [
9595
"pymongo>=4.4.0",
9696
]
97+
msgspec = [
98+
"msgspec>=0.18.5",
99+
]
97100

98101
[tool.pytest.ini_options]
99102
addopts = "-l --benchmark-sort=fullname --benchmark-warmup=true --benchmark-warmup-iterations=5 --benchmark-group-by=fullname"

src/cattr/gen.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from cattrs.gen import (
2-
AttributeOverride,
32
make_dict_structure_fn,
43
make_dict_unstructure_fn,
54
make_hetero_tuple_unstructure_fn,
@@ -8,6 +7,7 @@
87
make_mapping_unstructure_fn,
98
override,
109
)
10+
from cattrs.gen._consts import AttributeOverride
1111

1212
__all__ = [
1313
"AttributeOverride",

src/cattrs/converters.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -750,12 +750,10 @@ def _get_dis_func(
750750
) -> Callable[[Any], type]:
751751
"""Fetch or try creating a disambiguation function for a union."""
752752
union_types = union.__args__
753-
if NoneType in union_types: # type: ignore
753+
if NoneType in union_types:
754754
# We support unions of attrs classes and NoneType higher in the
755755
# logic.
756-
union_types = tuple(
757-
e for e in union_types if e is not NoneType # type: ignore
758-
)
756+
union_types = tuple(e for e in union_types if e is not NoneType)
759757

760758
# TODO: technically both disambiguators could support TypedDicts and
761759
# dataclasses...

src/cattrs/gen/__init__.py

+10-10
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,9 @@ def make_dict_unstructure_fn(
166166
# type of the default to dispatch on.
167167
t = a.default.__class__
168168
try:
169-
handler = converter._unstructure_func.dispatch(t)
169+
handler = converter.get_unstructure_hook(
170+
t, cache_result=False
171+
)
170172
except RecursionError:
171173
# There's a circular reference somewhere down the line
172174
handler = converter.unstructure
@@ -293,9 +295,6 @@ def make_dict_structure_fn(
293295
mapping = generate_mapping(base, mapping)
294296
break
295297

296-
if isinstance(cl, TypeVar):
297-
cl = mapping.get(cl.__name__, cl)
298-
299298
cl_name = cl.__name__
300299
fn_name = "structure_" + cl_name
301300

@@ -677,7 +676,7 @@ def make_iterable_unstructure_fn(
677676
# We don't know how to handle the TypeVar on this level,
678677
# so we skip doing the dispatch here.
679678
if not isinstance(type_arg, TypeVar):
680-
handler = converter._unstructure_func.dispatch(type_arg)
679+
handler = converter.get_unstructure_hook(type_arg, cache_result=False)
681680

682681
globs = {"__cattr_seq_cl": unstructure_to or cl, "__cattr_u": handler}
683682
lines = []
@@ -706,7 +705,8 @@ def make_hetero_tuple_unstructure_fn(
706705

707706
# We can do the dispatch here and now.
708707
handlers = [
709-
converter._unstructure_func.dispatch(type_arg) for type_arg in type_args
708+
converter.get_unstructure_hook(type_arg, cache_result=False)
709+
for type_arg in type_args
710710
]
711711

712712
globs = {f"__cattr_u_{i}": h for i, h in enumerate(handlers)}
@@ -761,11 +761,11 @@ def make_mapping_unstructure_fn(
761761
# Probably a Counter
762762
key_arg, val_arg = args, Any
763763
# We can do the dispatch here and now.
764-
kh = key_handler or converter._unstructure_func.dispatch(key_arg)
764+
kh = key_handler or converter.get_unstructure_hook(key_arg, cache_result=False)
765765
if kh == identity:
766766
kh = None
767767

768-
val_handler = converter._unstructure_func.dispatch(val_arg)
768+
val_handler = converter.get_unstructure_hook(val_arg, cache_result=False)
769769
if val_handler == identity:
770770
val_handler = None
771771

@@ -833,11 +833,11 @@ def make_mapping_structure_fn(
833833
is_bare_dict = val_type is Any and key_type is Any
834834
if not is_bare_dict:
835835
# We can do the dispatch here and now.
836-
key_handler = converter.get_structure_hook(key_type)
836+
key_handler = converter.get_structure_hook(key_type, cache_result=False)
837837
if key_handler == converter._structure_call:
838838
key_handler = key_type
839839

840-
val_handler = converter.get_structure_hook(val_type)
840+
val_handler = converter.get_structure_hook(val_type, cache_result=False)
841841
if val_handler == converter._structure_call:
842842
val_handler = val_type
843843

0 commit comments

Comments
 (0)