Skip to content

msgspec: test on 3.13 #624

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ Our backwards-compatibility policy can be found [here](https://github.com/python
- Many preconf converters (_bson_, stdlib JSON, _cbor2_, _msgpack_, _msgspec_, _orjson_, _ujson_) skip unstructuring `int` and `str` enums,
leaving them to the underlying libraries to handle with greater efficiency.
([#598](https://github.com/python-attrs/cattrs/pull/598))
- The {class}`msgspec JSON preconf converter <cattrs.preconf.msgspec.MsgspecJsonConverter>` now handles dataclasses with private attributes more efficiently.
([#624](https://github.com/python-attrs/cattrs/pull/624))
- 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.
([#598](https://github.com/python-attrs/cattrs/pull/598))
- Preconf converters now handle dictionaries with literal keys properly.
Expand Down
80 changes: 40 additions & 40 deletions pdm.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[tool.black]
skip-magic-trailing-comma = true

[tool.pdm.dev-dependencies]
[dependency-groups]
lint = [
"black>=24.2.0",
"ruff>=0.0.277",
Expand Down Expand Up @@ -93,7 +93,7 @@ bson = [
"pymongo>=4.4.0",
]
msgspec = [
"msgspec>=0.18.5; implementation_name == \"cpython\"",
"msgspec>=0.19.0; implementation_name == \"cpython\"",
]

[tool.pytest.ini_options]
Expand Down
33 changes: 20 additions & 13 deletions src/cattrs/preconf/msgspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

from base64 import b64decode
from dataclasses import is_dataclass
from datetime import date, datetime
from enum import Enum
from functools import partial
Expand All @@ -13,15 +14,7 @@
from msgspec import Struct, convert, to_builtins
from msgspec.json import Encoder, decode

from .._compat import (
fields,
get_args,
get_origin,
has,
is_bare,
is_mapping,
is_sequence,
)
from .._compat import fields, get_args, get_origin, is_bare, is_mapping, is_sequence
from ..cols import is_namedtuple
from ..converters import BaseConverter, Converter
from ..dispatch import UnstructureHook
Expand Down Expand Up @@ -104,11 +97,20 @@ def configure_passthroughs(converter: Converter) -> None:
"""Configure optimizing passthroughs.

A passthrough is when we let msgspec handle something automatically.

.. versionchanged:: 25.1.0
Dataclasses with private attributes are now passed through.
"""
converter.register_unstructure_hook(bytes, to_builtins)
converter.register_unstructure_hook_factory(is_mapping, mapping_unstructure_factory)
converter.register_unstructure_hook_factory(is_sequence, seq_unstructure_factory)
converter.register_unstructure_hook_factory(has, msgspec_attrs_unstructure_factory)
converter.register_unstructure_hook_factory(
attrs_has, msgspec_attrs_unstructure_factory
)
converter.register_unstructure_hook_factory(
is_dataclass,
partial(msgspec_attrs_unstructure_factory, msgspec_skips_private=False),
)
converter.register_unstructure_hook_factory(
is_namedtuple, namedtuple_unstructure_factory
)
Expand Down Expand Up @@ -154,16 +156,21 @@ def mapping_unstructure_factory(type, converter: BaseConverter) -> UnstructureHo


def msgspec_attrs_unstructure_factory(
type: Any, converter: Converter
type: Any, converter: Converter, msgspec_skips_private: bool = True
) -> UnstructureHook:
"""Choose whether to use msgspec handling or our own."""
"""Choose whether to use msgspec handling or our own.

Args:
msgspec_skips_private: Whether the msgspec library skips unstructuring
private attributes, making us do the work.
"""
origin = get_origin(type)
attribs = fields(origin or type)
if attrs_has(type) and any(isinstance(a.type, str) for a in attribs):
resolve_types(type)
attribs = fields(origin or type)

if any(
if msgspec_skips_private and any(
attr.name.startswith("_")
or (
converter.get_unstructure_hook(attr.type, cache_result=False)
Expand Down
2 changes: 0 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,3 @@ def converter_cls(request):
collect_ignore_glob.append("*_695.py")
if platform.python_implementation() == "PyPy":
collect_ignore_glob.append("*_cpython.py")
if sys.version_info >= (3, 13): # Remove when msgspec supports 3.13.
collect_ignore_glob.append("*test_msgspec_cpython.py")
18 changes: 18 additions & 0 deletions tests/preconf/test_msgspec_cpython.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

from dataclasses import dataclass
from enum import Enum
from typing import (
Any,
Expand Down Expand Up @@ -48,6 +49,18 @@ class C:
_a: int


@dataclass
class DataclassA:
a: int


@dataclass
class DataclassC:
"""Msgspec doesn't skip private attributes on dataclasses, so this should work OOB."""

_a: int


class N(NamedTuple):
a: int

Expand Down Expand Up @@ -107,6 +120,11 @@ def test_unstructure_pt_product_types(converter: Conv):
assert not is_passthrough(converter.get_unstructure_hook(B))
assert not is_passthrough(converter.get_unstructure_hook(C))

assert is_passthrough(converter.get_unstructure_hook(DataclassA))
assert is_passthrough(converter.get_unstructure_hook(DataclassC))

assert converter.unstructure(DataclassC(1)) == {"_a": 1}

assert is_passthrough(converter.get_unstructure_hook(N))
assert is_passthrough(converter.get_unstructure_hook(NA))
assert not is_passthrough(converter.get_unstructure_hook(NC))
Expand Down
3 changes: 0 additions & 3 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,6 @@ setenv =
PDM_IGNORE_SAVED_PYTHON="1"
COVERAGE_PROCESS_START={toxinidir}/pyproject.toml
COVERAGE_CORE=sysmon
commands_pre =
pdm sync -G ujson,msgpack,pyyaml,tomlkit,cbor2,bson,orjson,test
python -c 'import pathlib; pathlib.Path("{env_site_packages_dir}/cov.pth").write_text("import coverage; coverage.process_startup()")'

[testenv:pypy3]
setenv =
Expand Down