Skip to content

Commit 0b3b5f6

Browse files
authored
msgspec: test on 3.13 (#624)
* msgspec: test on 3.13 * msgspec: optimize dataclasses with private fields
1 parent 966132d commit 0b3b5f6

File tree

7 files changed

+82
-60
lines changed

7 files changed

+82
-60
lines changed

‎HISTORY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ Our backwards-compatibility policy can be found [here](https://github.com/python
2727
- Many preconf converters (_bson_, stdlib JSON, _cbor2_, _msgpack_, _msgspec_, _orjson_, _ujson_) skip unstructuring `int` and `str` enums,
2828
leaving them to the underlying libraries to handle with greater efficiency.
2929
([#598](https://github.com/python-attrs/cattrs/pull/598))
30+
- The {class}`msgspec JSON preconf converter <cattrs.preconf.msgspec.MsgspecJsonConverter>` now handles dataclasses with private attributes more efficiently.
31+
([#624](https://github.com/python-attrs/cattrs/pull/624))
3032
- Literals containing enums are now unstructured properly, and their unstructuring is greatly optimized in the _bson_, stdlib JSON, _cbor2_, _msgpack_, _msgspec_, _orjson_ and _ujson_ preconf converters.
3133
([#598](https://github.com/python-attrs/cattrs/pull/598))
3234
- Preconf converters now handle dictionaries with literal keys properly.

‎pdm.lock

Lines changed: 40 additions & 40 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[tool.black]
22
skip-magic-trailing-comma = true
33

4-
[tool.pdm.dev-dependencies]
4+
[dependency-groups]
55
lint = [
66
"black>=24.2.0",
77
"ruff>=0.0.277",
@@ -93,7 +93,7 @@ bson = [
9393
"pymongo>=4.4.0",
9494
]
9595
msgspec = [
96-
"msgspec>=0.18.5; implementation_name == \"cpython\"",
96+
"msgspec>=0.19.0; implementation_name == \"cpython\"",
9797
]
9898

9999
[tool.pytest.ini_options]

‎src/cattrs/preconf/msgspec.py

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
from base64 import b64decode
6+
from dataclasses import is_dataclass
67
from datetime import date, datetime
78
from enum import Enum
89
from functools import partial
@@ -13,15 +14,7 @@
1314
from msgspec import Struct, convert, to_builtins
1415
from msgspec.json import Encoder, decode
1516

16-
from .._compat import (
17-
fields,
18-
get_args,
19-
get_origin,
20-
has,
21-
is_bare,
22-
is_mapping,
23-
is_sequence,
24-
)
17+
from .._compat import fields, get_args, get_origin, is_bare, is_mapping, is_sequence
2518
from ..cols import is_namedtuple
2619
from ..converters import BaseConverter, Converter
2720
from ..dispatch import UnstructureHook
@@ -104,11 +97,20 @@ def configure_passthroughs(converter: Converter) -> None:
10497
"""Configure optimizing passthroughs.
10598
10699
A passthrough is when we let msgspec handle something automatically.
100+
101+
.. versionchanged:: 25.1.0
102+
Dataclasses with private attributes are now passed through.
107103
"""
108104
converter.register_unstructure_hook(bytes, to_builtins)
109105
converter.register_unstructure_hook_factory(is_mapping, mapping_unstructure_factory)
110106
converter.register_unstructure_hook_factory(is_sequence, seq_unstructure_factory)
111-
converter.register_unstructure_hook_factory(has, msgspec_attrs_unstructure_factory)
107+
converter.register_unstructure_hook_factory(
108+
attrs_has, msgspec_attrs_unstructure_factory
109+
)
110+
converter.register_unstructure_hook_factory(
111+
is_dataclass,
112+
partial(msgspec_attrs_unstructure_factory, msgspec_skips_private=False),
113+
)
112114
converter.register_unstructure_hook_factory(
113115
is_namedtuple, namedtuple_unstructure_factory
114116
)
@@ -154,16 +156,21 @@ def mapping_unstructure_factory(type, converter: BaseConverter) -> UnstructureHo
154156

155157

156158
def msgspec_attrs_unstructure_factory(
157-
type: Any, converter: Converter
159+
type: Any, converter: Converter, msgspec_skips_private: bool = True
158160
) -> UnstructureHook:
159-
"""Choose whether to use msgspec handling or our own."""
161+
"""Choose whether to use msgspec handling or our own.
162+
163+
Args:
164+
msgspec_skips_private: Whether the msgspec library skips unstructuring
165+
private attributes, making us do the work.
166+
"""
160167
origin = get_origin(type)
161168
attribs = fields(origin or type)
162169
if attrs_has(type) and any(isinstance(a.type, str) for a in attribs):
163170
resolve_types(type)
164171
attribs = fields(origin or type)
165172

166-
if any(
173+
if msgspec_skips_private and any(
167174
attr.name.startswith("_")
168175
or (
169176
converter.get_unstructure_hook(attr.type, cache_result=False)

‎tests/conftest.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,3 @@ def converter_cls(request):
3737
collect_ignore_glob.append("*_695.py")
3838
if platform.python_implementation() == "PyPy":
3939
collect_ignore_glob.append("*_cpython.py")
40-
if sys.version_info >= (3, 13): # Remove when msgspec supports 3.13.
41-
collect_ignore_glob.append("*test_msgspec_cpython.py")

‎tests/preconf/test_msgspec_cpython.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
from dataclasses import dataclass
56
from enum import Enum
67
from typing import (
78
Any,
@@ -48,6 +49,18 @@ class C:
4849
_a: int
4950

5051

52+
@dataclass
53+
class DataclassA:
54+
a: int
55+
56+
57+
@dataclass
58+
class DataclassC:
59+
"""Msgspec doesn't skip private attributes on dataclasses, so this should work OOB."""
60+
61+
_a: int
62+
63+
5164
class N(NamedTuple):
5265
a: int
5366

@@ -107,6 +120,11 @@ def test_unstructure_pt_product_types(converter: Conv):
107120
assert not is_passthrough(converter.get_unstructure_hook(B))
108121
assert not is_passthrough(converter.get_unstructure_hook(C))
109122

123+
assert is_passthrough(converter.get_unstructure_hook(DataclassA))
124+
assert is_passthrough(converter.get_unstructure_hook(DataclassC))
125+
126+
assert converter.unstructure(DataclassC(1)) == {"_a": 1}
127+
110128
assert is_passthrough(converter.get_unstructure_hook(N))
111129
assert is_passthrough(converter.get_unstructure_hook(NA))
112130
assert not is_passthrough(converter.get_unstructure_hook(NC))

‎tox.ini

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,6 @@ setenv =
4848
PDM_IGNORE_SAVED_PYTHON="1"
4949
COVERAGE_PROCESS_START={toxinidir}/pyproject.toml
5050
COVERAGE_CORE=sysmon
51-
commands_pre =
52-
pdm sync -G ujson,msgpack,pyyaml,tomlkit,cbor2,bson,orjson,test
53-
python -c 'import pathlib; pathlib.Path("{env_site_packages_dir}/cov.pth").write_text("import coverage; coverage.process_startup()")'
5451

5552
[testenv:pypy3]
5653
setenv =

0 commit comments

Comments
 (0)