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 @@ -22,6 +22,8 @@ Our backwards-compatibility policy can be found [here](https://github.com/python
- [`typing.Self`](https://docs.python.org/3/library/typing.html#typing.Self) is now supported in _attrs_ classes, dataclasses, TypedDicts and the dict NamedTuple factories.
See [`typing.Self`](https://catt.rs/en/latest/defaulthooks.html#typing-self) for details.
([#299](https://github.com/python-attrs/cattrs/issues/299) [#627](https://github.com/python-attrs/cattrs/pull/627))
- PEP 695 type aliases can now be used with {meth}`Converter.register_structure_hook` and {meth}`Converter.register_unstructure_hook`.
Previously, they required the use of {meth}`Converter.register_structure_hook_func` (which is still supported).
- Expose {func}`cattrs.cols.mapping_unstructure_factory` through {mod}`cattrs.cols`.
- Some `defaultdicts` are now [supported by default](https://catt.rs/en/latest/defaulthooks.html#defaultdicts), and
{func}`cattrs.cols.is_defaultdict` and {func}`cattrs.cols.defaultdict_structure_factory` are exposed through {mod}`cattrs.cols`.
Expand Down
3 changes: 2 additions & 1 deletion docs/basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
```

All _cattrs_ functionality is exposed through a {class}`cattrs.Converter` object.
A global converter is provided for convenience as {data}`cattrs.global_converter` but more complex customizations should be performed on private instances, any number of which can be made.
A global converter is provided for convenience as {data}`cattrs.global_converter`
but more complex customizations should be performed on private instances, any number of which can be made.


## Converters and Hooks
Expand Down
10 changes: 7 additions & 3 deletions docs/customizing.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ the hook for `int`, which should be already present.
## Custom (Un-)structuring Hooks

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>`.
This approach is the most flexible but also requires the most amount of boilerplate.

{meth}`register_structure_hook() <cattrs.BaseConverter.register_structure_hook>` and {meth}`register_unstructure_hook() <cattrs.BaseConverter.register_unstructure_hook>` use a Python [_singledispatch_](https://docs.python.org/3/library/functools.html#functools.singledispatch) under the hood.
_singledispatch_ is powerful and fast but comes with some limitations; namely that it performs checks using `issubclass()` which doesn't work with many Python types.
Expand All @@ -30,10 +29,15 @@ Some examples of this are:
- generics (`MyClass[int]` is not a _subclass_ of `MyClass`)
- protocols, unless they are `runtime_checkable`
- various modifiers, such as `Final` and `NotRequired`
- newtypes and 3.12 type aliases
- `typing.Annotated`

... and many others. In these cases, predicate functions should be used instead.
... and many others. In these cases, [predicate hooks](#predicate-hooks) should be used instead.

Even though unions, [newtypes](https://docs.python.org/3/library/typing.html#newtype)
and [modern type aliases](https://docs.python.org/3/library/typing.html#type-aliases)
do not work with _singledispatch_,
these methods have special support for these type forms and can be used with them.
Instead of using _singledispatch_, predicate hooks will automatically be used instead.

### Use as Decorators

Expand Down
214 changes: 67 additions & 147 deletions pdm.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ docs = [
"furo>=2024.1.29",
"sphinx-copybutton>=0.5.2",
"myst-parser>=1.0.0",
"pendulum>=2.1.2",
"pendulum>=3.1.0",
"sphinx-autobuild",
]
bench = [
Expand Down
9 changes: 9 additions & 0 deletions src/cattrs/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,8 @@ def register_unstructure_hook(

.. versionchanged:: 24.1.0
This method may now be used as a decorator.
.. versionchanged:: 25.1.0
Modern type aliases are now supported.
"""
if func is None:
# Autodetecting decorator.
Expand All @@ -353,6 +355,8 @@ def register_unstructure_hook(
resolve_types(cls)
if is_union_type(cls):
self._unstructure_func.register_func_list([(lambda t: t == cls, func)])
elif is_type_alias(cls):
self._unstructure_func.register_func_list([(lambda t: t is cls, func)])
elif get_newtype_base(cls) is not None:
# This is a newtype, so we handle it specially.
self._unstructure_func.register_func_list([(lambda t: t is cls, func)])
Expand Down Expand Up @@ -475,6 +479,8 @@ def register_structure_hook(

.. versionchanged:: 24.1.0
This method may now be used as a decorator.
.. versionchanged:: 25.1.0
Modern type aliases are now supported.
"""
if func is None:
# The autodetecting decorator.
Expand All @@ -488,6 +494,9 @@ def register_structure_hook(
if is_union_type(cl):
self._union_struct_registry[cl] = func
self._structure_func.clear_cache()
elif is_type_alias(cl):
# Type aliases are special-cased.
self._structure_func.register_func_list([(lambda t: t is cl, func)])
elif get_newtype_base(cl) is not None:
# This is a newtype, so we handle it specially.
self._structure_func.register_func_list([(lambda t: t is cl, func)])
Expand Down
11 changes: 11 additions & 0 deletions tests/test_generics_695.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,17 @@ def test_type_aliases(converter: BaseConverter):
assert converter.unstructure(100, my_other_int) == 80


def test_type_aliases_simple_hooks(converter: BaseConverter):
"""PEP 695 type aliases work with `register_un/structure_hook`."""
type my_other_int = int

converter.register_structure_hook(my_other_int, lambda v, _: v + 10)
converter.register_unstructure_hook(my_other_int, lambda v: v - 20)

assert converter.structure(1, my_other_int) == 11
assert converter.unstructure(100, my_other_int) == 80


def test_type_aliases_overwrite_base_hooks(converter: BaseConverter):
"""Overwriting base hooks should affect type aliases."""
converter.register_structure_hook(int, lambda v, _: v + 10)
Expand Down