Skip to content
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 @@ -33,6 +33,8 @@ can now be used as decorators and have gained new features when used this way.
([#426](https://github.com/python-attrs/cattrs/issues/426) [#477](https://github.com/python-attrs/cattrs/pull/477))
- Add support for [PEP 695](https://peps.python.org/pep-0695/) type aliases.
([#452](https://github.com/python-attrs/cattrs/pull/452))
- Add support for named tuples with type metadata ([`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)).
([#425](https://github.com/python-attrs/cattrs/issues/425) [#491](https://github.com/python-attrs/cattrs/pull/491))
- 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.
([#429](https://github.com/python-attrs/cattrs/issues/429) [#472](https://github.com/python-attrs/cattrs/pull/472))
- The preconf `make_converter` factories are now correctly typed.
Expand Down
11 changes: 11 additions & 0 deletions docs/defaulthooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,10 @@ Any type parameters set to `typing.Any` will be passed through unconverted.

When unstructuring, heterogeneous tuples unstructure into tuples since it's faster and virtually all serialization libraries support tuples natively.

```{note}
Structuring heterogenous tuples are not supported by the BaseConverter.
```

### Deques

Deques can be structured from any iterable object.
Expand Down Expand Up @@ -490,6 +494,13 @@ When unstructuring, literals are passed through.

```

### `typing.NamedTuple`

Named tuples with type hints (created from [`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)) are supported.

```{versionadded} 24.1.0

```

### `typing.Final`

Expand Down
10 changes: 7 additions & 3 deletions docs/preconf.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ For example, to get a converter configured for BSON:

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

These converters support the following additional classes and type annotations, both for structuring and unstructuring:
These converters support all [default hooks](defaulthooks.md)
and the following additional classes and type annotations,
both for structuring and unstructuring:

- `datetime.datetime`, `datetime.date`

Expand Down Expand Up @@ -66,6 +68,7 @@ Found at {mod}`cattrs.preconf.orjson`.
Bytes are un/structured as base 85 strings.
Sets are unstructured into lists, and structured back into sets.
`datetime` s and `date` s are passed through to be unstructured into RFC 3339 by _orjson_ itself.
Typed named tuples are unstructured into ordinary tuples, and then into JSON arrays by _orjson_.

_orjson_ doesn't support integers less than -9223372036854775808, and greater than 9223372036854775807.
_orjson_ only supports mappings with string keys so mappings will have their keys stringified before serialization, and destringified during deserialization.
Expand Down Expand Up @@ -180,8 +183,9 @@ When encoding and decoding, the library needs to be passed `codec_options=bson.C

Found at {mod}`cattrs.preconf.pyyaml`.

Frozensets are serialized as lists, and deserialized back into frozensets. `date` s are serialized as ISO 8601 strings.

Frozensets are serialized as lists, and deserialized back into frozensets.
`date` s are serialized as ISO 8601 strings.
Typed named tuples are unstructured into ordinary tuples, and then into YAML arrays by _pyyaml_.

## _tomlkit_

Expand Down
14 changes: 13 additions & 1 deletion src/cattrs/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from dataclasses import Field
from enum import Enum
from inspect import Signature
from inspect import signature as inspect_signature
from pathlib import Path
from typing import Any, Callable, Iterable, Optional, Tuple, TypeVar, overload

Expand Down Expand Up @@ -81,6 +82,11 @@
)
from .gen.typeddicts import make_dict_structure_fn as make_typeddict_dict_struct_fn
from .gen.typeddicts import make_dict_unstructure_fn as make_typeddict_dict_unstruct_fn
from .tuples import (
is_namedtuple,
namedtuple_structure_factory,
namedtuple_unstructure_factory,
)

__all__ = ["UnstructureStrategy", "BaseConverter", "Converter", "GenConverter"]

Expand Down Expand Up @@ -224,6 +230,7 @@ def __init__(
(is_mutable_set, self._structure_set),
(is_frozenset, self._structure_frozenset),
(is_tuple, self._structure_tuple),
(is_namedtuple, namedtuple_structure_factory, "extended"),
(is_mapping, self._structure_dict),
(is_supported_union, self._gen_attrs_union_structure, True),
(
Expand Down Expand Up @@ -365,7 +372,9 @@ def register_unstructure_hook_factory(

def decorator(factory):
# Is this an extended factory (takes a converter too)?
sig = signature(factory)
# We use the original `inspect.signature` to not evaluate string
# annotations.
sig = inspect_signature(factory)
if (
len(sig.parameters) >= 2
and (list(sig.parameters.values())[1]).default is Signature.empty
Expand Down Expand Up @@ -1095,6 +1104,9 @@ def __init__(
self.register_unstructure_hook_factory(
is_hetero_tuple, self.gen_unstructure_hetero_tuple
)
self.register_unstructure_hook_factory(is_namedtuple)(
namedtuple_unstructure_factory
)
self.register_unstructure_hook_factory(
is_sequence, self.gen_unstructure_iterable
)
Expand Down
18 changes: 16 additions & 2 deletions src/cattrs/dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from attrs import Factory, define

from cattrs._compat import TypeAlias
from ._compat import TypeAlias

if TYPE_CHECKING:
from .converters import BaseConverter
Expand Down Expand Up @@ -36,6 +36,12 @@ class FunctionDispatch:
first argument in the method, and return True or False.

objects that help determine dispatch should be instantiated objects.

:param converter: A converter to be used for factories that require converters.

.. versionchanged:: 24.1.0
Support for factories that require converters, hence this requires a
converter when creating.
"""

_converter: BaseConverter
Expand Down Expand Up @@ -86,11 +92,15 @@ class MultiStrategyDispatch(Generic[Hook]):
MultiStrategyDispatch uses a combination of exact-match dispatch,
singledispatch, and FunctionDispatch.

:param converter: A converter to be used for factories that require converters.
:param fallback_factory: A hook factory to be called when a hook cannot be
produced.

.. versionchanged:: 23.2.0
.. versionchanged:: 23.2.0
Fallbacks are now factories.
.. versionchanged:: 24.1.0
Support for factories that require converters, hence this requires a
converter when creating.
"""

_fallback_factory: HookFactory[Hook]
Expand Down Expand Up @@ -150,6 +160,10 @@ def register_func_list(
"""
Register a predicate function to determine if the handler
should be used for the type.

:param pred_and_handler: The list of predicates and their associated
handlers. If a handler is registered in `extended` mode, it's a
factory that requires a converter.
"""
for tup in pred_and_handler:
if len(tup) == 2:
Expand Down
19 changes: 11 additions & 8 deletions src/cattrs/gen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
if TYPE_CHECKING: # pragma: no cover
from typing_extensions import Literal

from cattr.converters import BaseConverter
from ..converters import BaseConverter

__all__ = [
"make_dict_unstructure_fn",
Expand Down Expand Up @@ -698,18 +698,21 @@ def make_iterable_unstructure_fn(


def make_hetero_tuple_unstructure_fn(
cl: Any, converter: BaseConverter, unstructure_to: Any = None
cl: Any,
converter: BaseConverter,
unstructure_to: Any = None,
type_args: tuple | None = None,
) -> HeteroTupleUnstructureFn:
"""Generate a specialized unstructure function for a heterogenous tuple."""
"""Generate a specialized unstructure function for a heterogenous tuple.

:param type_args: If provided, override the type arguments.
"""
fn_name = "unstructure_tuple"

type_args = get_args(cl)
type_args = get_args(cl) if type_args is None else type_args

# We can do the dispatch here and now.
handlers = [
converter.get_unstructure_hook(type_arg, cache_result=False)
for type_arg in type_args
]
handlers = [converter.get_unstructure_hook(type_arg) for type_arg in type_args]

globs = {f"__cattr_u_{i}": h for i, h in enumerate(handlers)}
if unstructure_to is not tuple:
Expand Down
153 changes: 79 additions & 74 deletions src/cattrs/preconf/msgspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@
from msgspec.json import Encoder, decode

from cattrs._compat import fields, get_origin, has, is_bare, is_mapping, is_sequence
from cattrs.dispatch import HookFactory, UnstructureHook
from cattrs.dispatch import UnstructureHook
from cattrs.fns import identity

from ..converters import Converter
from ..converters import BaseConverter, Converter
from ..gen import make_hetero_tuple_unstructure_fn
from ..strategies import configure_union_passthrough
from ..tuples import is_namedtuple
from . import wrap

T = TypeVar("T")
Expand Down Expand Up @@ -85,86 +87,89 @@ def configure_passthroughs(converter: Converter) -> None:
A passthrough is when we let msgspec handle something automatically.
"""
converter.register_unstructure_hook(bytes, to_builtins)
converter.register_unstructure_hook_factory(
is_mapping, make_unstructure_mapping_factory(converter)
)
converter.register_unstructure_hook_factory(
is_sequence, make_unstructure_seq_factory(converter)
)
converter.register_unstructure_hook_factory(
has, make_attrs_unstruct_factory(converter)
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)(attrs_unstructure_factory)
converter.register_unstructure_hook_factory(is_namedtuple)(
namedtuple_unstructure_factory
)


def make_unstructure_seq_factory(converter: Converter) -> HookFactory[UnstructureHook]:
def unstructure_seq_factory(type) -> UnstructureHook:
if is_bare(type):
type_arg = Any
handler = converter.get_unstructure_hook(type_arg, cache_result=False)
elif getattr(type, "__args__", None) not in (None, ()):
type_arg = type.__args__[0]
handler = converter.get_unstructure_hook(type_arg, cache_result=False)
else:
handler = None

if handler in (identity, to_builtins):
return handler
return converter.gen_unstructure_iterable(type)

return unstructure_seq_factory


def make_unstructure_mapping_factory(
converter: Converter,
) -> HookFactory[UnstructureHook]:
def unstructure_mapping_factory(type) -> UnstructureHook:
if is_bare(type):
key_arg = Any
val_arg = Any
key_handler = converter.get_unstructure_hook(key_arg, cache_result=False)
value_handler = converter.get_unstructure_hook(val_arg, cache_result=False)
elif (args := getattr(type, "__args__", None)) not in (None, ()):
if len(args) == 2:
key_arg, val_arg = args
else:
# Probably a Counter
key_arg, val_arg = args, Any
key_handler = converter.get_unstructure_hook(key_arg, cache_result=False)
value_handler = converter.get_unstructure_hook(val_arg, cache_result=False)
def seq_unstructure_factory(type, converter: BaseConverter) -> UnstructureHook:
if is_bare(type):
type_arg = Any
handler = converter.get_unstructure_hook(type_arg, cache_result=False)
elif getattr(type, "__args__", None) not in (None, ()):
type_arg = type.__args__[0]
handler = converter.get_unstructure_hook(type_arg, cache_result=False)
else:
handler = None

if handler in (identity, to_builtins):
return handler
return converter.gen_unstructure_iterable(type)


def mapping_unstructure_factory(type, converter: BaseConverter) -> UnstructureHook:
if is_bare(type):
key_arg = Any
val_arg = Any
key_handler = converter.get_unstructure_hook(key_arg, cache_result=False)
value_handler = converter.get_unstructure_hook(val_arg, cache_result=False)
elif (args := getattr(type, "__args__", None)) not in (None, ()):
if len(args) == 2:
key_arg, val_arg = args
else:
key_handler = value_handler = None
# Probably a Counter
key_arg, val_arg = args, Any
key_handler = converter.get_unstructure_hook(key_arg, cache_result=False)
value_handler = converter.get_unstructure_hook(val_arg, cache_result=False)
else:
key_handler = value_handler = None

if key_handler in (identity, to_builtins) and value_handler in (
identity,
to_builtins,
):
return to_builtins
return converter.gen_unstructure_mapping(type)


if key_handler in (identity, to_builtins) and value_handler in (
identity,
to_builtins,
):
return to_builtins
return converter.gen_unstructure_mapping(type)
def attrs_unstructure_factory(type: Any, converter: BaseConverter) -> UnstructureHook:
"""Choose whether to use msgspec handling or our own."""
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)

return unstructure_mapping_factory
if any(
attr.name.startswith("_")
or (
converter.get_unstructure_hook(attr.type, cache_result=False)
not in (identity, to_builtins)
)
for attr in attribs
):
return converter.gen_unstructure_attrs_fromdict(type)

return to_builtins

def make_attrs_unstruct_factory(converter: Converter) -> HookFactory[UnstructureHook]:
"""Short-circuit attrs and dataclass handling if it matches msgspec."""

def attrs_factory(type: Any) -> UnstructureHook:
"""Choose whether to use msgspec handling or our own."""
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(
attr.name.startswith("_")
or (
converter.get_unstructure_hook(attr.type, cache_result=False)
not in (identity, to_builtins)
)
for attr in attribs
):
return converter.gen_unstructure_attrs_fromdict(type)
def namedtuple_unstructure_factory(
type: type[tuple], converter: BaseConverter
) -> UnstructureHook:
"""A hook factory for unstructuring namedtuples, modified for msgspec."""

return to_builtins
if all(
converter.get_unstructure_hook(t) in (identity, to_builtins)
for t in type.__annotations__.values()
):
return identity

return attrs_factory
return make_hetero_tuple_unstructure_fn(
type,
converter,
unstructure_to=tuple,
type_args=tuple(type.__annotations__.values()),
)
Loading