diff --git a/HISTORY.md b/HISTORY.md
index 7bcef0a4..7407829d 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -4,6 +4,9 @@
## 24.1.0 (UNRELEASED)
+- **Potentially breaking**: Unstructuring hooks for `typing.Any` are consistent now: values are unstructured using their runtime type.
+ Previously this behavior was underspecified and inconsistent, but followed this rule in the majority of cases.
+ ([#473](https://github.com/python-attrs/cattrs/pull/473))
- Introduce {meth}`BaseConverter.get_structure_hook` and {meth}`BaseConverter.get_unstructure_hook` methods.
([#432](https://github.com/python-attrs/cattrs/issues/432) [#472](https://github.com/python-attrs/cattrs/pull/472))
- The default union handler now properly takes renamed fields into account.
@@ -26,6 +29,8 @@
- Tests are run with the pytest-xdist plugin by default.
- Rework the introductory parts of the documentation, introducing the Basics section.
([#472](https://github.com/python-attrs/cattrs/pull/472))
+- The documentation has been significantly reworked.
+ ([#473](https://github.com/python-attrs/cattrs/pull/473))
- The docs now use the Inter font.
## 23.2.3 (2023-11-30)
diff --git a/README.md b/README.md
index 704e6aad..0419682f 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,9 @@
# cattrs
+
+ Great software needs great data structures.
+
+
@@ -103,7 +107,7 @@ When you're done, `unstructure` the data to its unstructured form and pass it al
Use [attrs type metadata](http://attrs.readthedocs.io/en/stable/examples.html#types) to add type metadata to attributes, so _cattrs_ will know how to structure and destructure them.
- Free software: MIT license
-- Documentation: https://catt.rs
+- Documentation: [https://catt.rs](https://catt.rs)
- Python versions supported: 3.8 and up. (Older Python versions are supported by older versions; see the changelog.)
## Features
diff --git a/docs/Makefile b/docs/Makefile
index c13822c9..00a11a8f 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -171,8 +171,9 @@ pseudoxml:
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
+.PHONY: apidoc
apidoc:
- pdm run sphinx-apidoc -o . ../src/cattrs/ -f
+ pdm run sphinx-apidoc -o . ../src/cattrs/ '../**/converters.py' -f -M
## htmlview to open the index page built by the html target in your browser
.PHONY: htmlview
diff --git a/docs/basics.md b/docs/basics.md
index 23441547..cf978de6 100644
--- a/docs/basics.md
+++ b/docs/basics.md
@@ -3,16 +3,16 @@
```
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.
+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
+## Converters and Hooks
-The core functionality of a converter is [structuring](structuring.md) and [unstructuring](unstructuring.md) data by composing provided and [custom handling functions](customizing.md), called _hooks_.
+The core functionality of a converter is structuring and unstructuring data by composing [provided](defaulthooks.md) and [custom handling functions](customizing.md), called _hooks_.
To create a private converter, instantiate a {class}`cattrs.Converter`. Converters are relatively cheap; users are encouraged to have as many as they need.
-The two main methods are {meth}`structure ` and {meth}`unstructure `, these are used to convert between _structured_ and _unstructured_ data.
+The two main methods, {meth}`structure ` and {meth}`unstructure `, are used to convert between _structured_ and _unstructured_ data.
```python
>>> from cattrs import structure, unstructure
@@ -28,53 +28,54 @@ The two main methods are {meth}`structure ` and
Model(a=1)
```
-_cattrs_ comes with a rich library of un/structuring rules by default, but it excels at composing custom rules with built-in ones.
+_cattrs_ comes with a rich library of un/structuring hooks by default but it excels at composing custom hooks with built-in ones.
-The simplest approach to customization is wrapping an existing hook with your own function.
-A base hook can be obtained from a converter and be subjected to the very rich machinery of function composition in Python.
+The simplest approach to customization is writing a new hook from scratch.
+For example, we can write our own hook for the `int` class.
```python
->>> from cattrs import get_structure_hook
+>>> def int_hook(value, type):
+... if not isinstance(value, int):
+... raise ValueError('not an int!')
+... return value
+```
->>> base_hook = get_structure_hook(Model)
+We can then register this hook to a converter and any other hook converting an `int` will use it.
->>> def my_hook(value, type):
+```python
+>>> from cattrs import Converter
+
+>>> converter = Converter()
+>>> converter.register_structure_hook(int, int_hook)
+```
+
+Another approach to customization is wrapping an existing hook with your own function.
+A base hook can be obtained from a converter and then be subjected to the very rich machinery of function composition that Python offers.
+
+
+```python
+>>> base_hook = converter.get_structure_hook(Model)
+
+>>> def my_model_hook(value, type):
... # Apply any preprocessing to the value.
... result = base_hook(value, type)
-... # Apply any postprocessing to the value.
+... # Apply any postprocessing to the model.
... return result
```
-This new hook can be used directly or registered to a converter (the original instance, or a different one).
-
(`cattrs.structure({}, Model)` is shorthand for `cattrs.get_structure_hook(Model)({}, Model)`.)
-Another approach is to write a hook from scratch instead of wrapping an existing one.
-For example, we can write our own hook for the `int` class.
+This new hook can be used directly or registered to a converter (the original instance, or a different one):
```python
->>> def my_int_hook(value, type):
-... if not isinstance(value, int):
-... raise ValueError('not an int!')
-... return value
+>>> converter.register_structure_hook(Model, my_model_hook)
```
-We can then register this hook to a converter, and any other hook converting an `int` will use it.
-Since this is an impactful change, we will switch to using a private converter.
-
-```python
->>> from cattrs import Converter
-
->>> c = Converter()
-
->>> c.register_structure_hook(int, my_int_hook)
-```
-Now, if we ask our new converter for a `Model` hook, through the ✨magic of function composition✨ that hook will use our new `my_int_hook`.
+Now if we use this hook to structure a `Model`, through the ✨magic of function composition✨ that hook will use our old `int_hook`.
```python
->>> base_hook = c.get_structure_hook(Model)
->>> base_hook({"a": "1"}, Model)
+>>> converter.structure({"a": "1"}, Model)
+ Exception Group Traceback (most recent call last):
| File "...", line 22, in
| base_hook({"a": "1"}, Model)
@@ -95,7 +96,7 @@ More advanced structuring customizations are commonly called [](strategies.md).
## Global Converter
-Global _cattrs_ functions, such as {meth}`cattrs.unstructure`, use a single {data}`global converter `.
+Global _cattrs_ functions, such as {meth}`cattrs.structure`, use a single {data}`global converter `.
Changes done to this global converter, such as registering new structure and unstructure hooks, affect all code using the global functions.
The following functions implicitly use this global converter:
diff --git a/docs/cattrs.gen.rst b/docs/cattrs.gen.rst
index 1968fcae..390cfca7 100644
--- a/docs/cattrs.gen.rst
+++ b/docs/cattrs.gen.rst
@@ -1,6 +1,11 @@
cattrs.gen package
==================
+.. automodule:: cattrs.gen
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
Submodules
----------
@@ -11,11 +16,3 @@ cattrs.gen.typeddicts module
:members:
:undoc-members:
:show-inheritance:
-
-Module contents
----------------
-
-.. automodule:: cattrs.gen
- :members:
- :undoc-members:
- :show-inheritance:
diff --git a/docs/cattrs.preconf.rst b/docs/cattrs.preconf.rst
index f51586a2..61a94d2c 100644
--- a/docs/cattrs.preconf.rst
+++ b/docs/cattrs.preconf.rst
@@ -1,6 +1,11 @@
cattrs.preconf package
======================
+.. automodule:: cattrs.preconf
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
Submodules
----------
@@ -67,11 +72,3 @@ cattrs.preconf.ujson module
:members:
:undoc-members:
:show-inheritance:
-
-Module contents
----------------
-
-.. automodule:: cattrs.preconf
- :members:
- :undoc-members:
- :show-inheritance:
diff --git a/docs/cattrs.rst b/docs/cattrs.rst
index df008424..5170d264 100644
--- a/docs/cattrs.rst
+++ b/docs/cattrs.rst
@@ -1,6 +1,11 @@
cattrs package
==============
+.. automodule:: cattrs
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
Subpackages
-----------
@@ -14,14 +19,6 @@ Subpackages
Submodules
----------
-cattrs.converters module
-------------------------
-
-.. automodule:: cattrs.converters
- :members:
- :undoc-members:
- :show-inheritance:
-
cattrs.disambiguators module
----------------------------
@@ -61,11 +58,3 @@ cattrs.v module
:members:
:undoc-members:
:show-inheritance:
-
-Module contents
----------------
-
-.. automodule:: cattrs
- :members:
- :undoc-members:
- :show-inheritance:
diff --git a/docs/cattrs.strategies.rst b/docs/cattrs.strategies.rst
index bce804b2..d85e24a0 100644
--- a/docs/cattrs.strategies.rst
+++ b/docs/cattrs.strategies.rst
@@ -1,9 +1,6 @@
cattrs.strategies package
=========================
-Module contents
----------------
-
.. automodule:: cattrs.strategies
:members:
:undoc-members:
diff --git a/docs/conf.py b/docs/conf.py
index e1130b3b..5badbac3 100755
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
# cattrs documentation build configuration file, created by
# sphinx-quickstart on Tue Jul 9 22:26:36 2013.
#
@@ -289,6 +287,7 @@
"from typing import *;"
"from enum import Enum, unique"
)
+autodoc_member_order = "bysource"
autodoc_typehints = "description"
autosectionlabel_prefix_document = True
copybutton_prompt_text = r">>> |\.\.\. "
diff --git a/docs/customizing.md b/docs/customizing.md
index 43c43220..7efc229b 100644
--- a/docs/customizing.md
+++ b/docs/customizing.md
@@ -62,8 +62,7 @@ Hook factories are registered using {meth}`Converter.register_unstructure_hook_f
Here's an example showing how to use hook factories to apply the `forbid_extra_keys` to all attrs classes:
-```{doctest}
-
+```python
>>> from attrs import define, has
>>> from cattrs.gen import make_dict_structure_fn
@@ -135,7 +134,7 @@ So we apply the `omit_if_default` rule to the class, but not to the `dateTime` f
>>> @define
... class TestClass:
... a: Optional[int] = None
-... b: dateTime = Factory(datetime.utcnow)
+... b: datetime = Factory(datetime.utcnow)
>>> c = cattrs.Converter()
>>> hook = make_dict_unstructure_fn(TestClass, c, _cattrs_omit_if_default=True, b=override(omit_if_default=False))
diff --git a/docs/defaulthooks.md b/docs/defaulthooks.md
new file mode 100644
index 00000000..ad7cb34e
--- /dev/null
+++ b/docs/defaulthooks.md
@@ -0,0 +1,572 @@
+# Built-in Hooks
+
+```{currentmodule} cattrs
+```
+
+_cattrs_ converters come with with a large repertoire of un/structuring hooks built-in.
+As always, complex hooks compose with simpler ones.
+
+## Primitive Values
+
+### `int`, `float`, `str`, `bytes`
+
+When structuring, use any of these types to coerce the object to that type.
+
+```{doctest}
+
+>>> cattrs.structure(1, str)
+'1'
+>>> cattrs.structure("1", float)
+1.0
+```
+
+In case the conversion isn't possible the expected exceptions will be propagated out.
+The particular exceptions are the same as if you'd tried to do the coercion directly.
+
+```python
+>>> cattrs.structure("not-an-int", int)
+Traceback (most recent call last):
+...
+ValueError: invalid literal for int() with base 10: 'not-an-int'
+```
+
+Coercion is performed for performance and compatibility reasons.
+Any of these hooks can be overriden if pure validation is required instead.
+
+```{doctest}
+>>> c = Converter()
+
+>>> def validate(value, type):
+... if not isinstance(value, type):
+... raise ValueError(f'{value!r} not an instance of {type}')
+...
+
+>>> c.register_structure_hook(int, validate)
+
+>>> c.structure("1", int)
+Traceback (most recent call last):
+...
+ValueError: '1' not an instance of
+```
+
+When unstructuring, these types are passed through unchanged.
+
+### Enums
+
+Enums are structured by their values, and unstructured to their values.
+This works even for complex values, like tuples.
+
+```{doctest}
+
+>>> @unique
+... class CatBreed(Enum):
+... SIAMESE = "siamese"
+... MAINE_COON = "maine_coon"
+... SACRED_BIRMAN = "birman"
+
+>>> cattrs.structure("siamese", CatBreed)
+
+
+>>> cattrs.unstructure(CatBreed.SIAMESE)
+'siamese'
+```
+
+Again, in case of errors, the expected exceptions are raised.
+
+### `pathlib.Path`
+
+[`pathlib.Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) objects are structured using their string value,
+and unstructured into their string value.
+
+```{doctest}
+>>> from pathlib import Path
+
+>>> cattrs.structure("/root", Path)
+PosixPath('/root')
+
+>>> cattrs.unstructure(Path("/root"))
+'/root'
+```
+
+In case the conversion isn't possible, the resulting exception is propagated out.
+
+```{versionadded} 23.1.0
+
+```
+
+
+## Collections and Related Generics
+
+
+### Optionals
+
+`Optional` primitives and collections are supported out of the box.
+[PEP 604](https://peps.python.org/pep-0604/) optionals (`T | None`) are also supported on Python 3.10 and later.
+
+```{doctest}
+
+>>> cattrs.structure(None, int)
+Traceback (most recent call last):
+...
+TypeError: int() argument must be a string, a bytes-like object or a number, not 'NoneType'
+
+>>> cattrs.structure(None, int | None)
+>>> # None was returned.
+```
+
+Bare `Optional` s (non-parameterized, just `Optional`, as opposed to `Optional[str]`) aren't supported; `Optional[Any]` should be used instead.
+
+
+### Lists
+
+Lists can be structured from any iterable object.
+Types converting to lists are:
+
+- `typing.Sequence[T]`
+- `typing.MutableSequence[T]`
+- `typing.List[T]`
+- `list[T]`
+
+In all cases, a new list will be returned, so this operation can be used to copy an iterable into a list.
+A bare type, for example `Sequence` instead of `Sequence[int]`, is equivalent to `Sequence[Any]`.
+
+```{doctest}
+
+>>> cattrs.structure((1, 2, 3), MutableSequence[int])
+[1, 2, 3]
+```
+
+When unstructuring, lists are copied and their contents are handled according to their inner type.
+A useful use case for unstructuring collections is to create a deep copy of a complex or recursive collection.
+
+### Dictionaries
+
+Dictionaries can be produced from other mapping objects.
+More precisely, the unstructured object must expose an [`items()`](https://docs.python.org/3/library/stdtypes.html#dict.items) method producing an iterable of key-value tuples, and be able to be passed to the `dict` constructor as an argument.
+Types converting to dictionaries are:
+
+- `typing.Dict[K, V]`
+- `typing.MutableMapping[K, V]`
+- `typing.Mapping[K, V]`
+- `dict[K, V]`
+
+In all cases, a new dict will be returned, so this operation can be used to copy a mapping into a dict.
+Any type parameters set to `typing.Any` will be passed through unconverted.
+If both type parameters are absent, they will be treated as `Any` too.
+
+```{doctest}
+
+>>> from collections import OrderedDict
+>>> cattrs.structure(OrderedDict([(1, 2), (3, 4)]), dict)
+{1: 2, 3: 4}
+```
+
+Both keys and values are converted.
+
+```{doctest}
+
+>>> cattrs.structure({1: None, 2: 2.0}, dict[str, Optional[int]])
+{'1': None, '2': 2}
+```
+
+
+### Homogeneous and Heterogeneous Tuples
+
+Homogeneous and heterogeneous tuples can be structured from iterable objects.
+Heterogeneous tuples require an iterable with the number of elements matching the number of type parameters exactly.
+
+Use:
+
+- `Tuple[A, B, C, D]`
+- `tuple[A, B, C, D]`
+
+Homogeneous tuples use:
+
+- `Tuple[T, ...]`
+- `tuple[T, ...]`
+
+In all cases a tuple will be produced.
+Any type parameters set to `typing.Any` will be passed through unconverted.
+
+```{doctest}
+
+>>> cattrs.structure([1, 2, 3], tuple[int, str, float])
+(1, '2', 3.0)
+```
+
+
+### Deques
+
+Deques can be structured from any iterable object.
+Types converting to deques are:
+
+- `typing.Deque[T]`
+- `collections.deque[T]`
+
+In all cases, a new **unbounded** deque (`maxlen=None`) will be produced, so this operation can be used to copy an iterable into a deque.
+If you want to convert into bounded `deque`, registering a custom structuring hook is a good approach.
+
+```{doctest}
+
+>>> from collections import deque
+>>> cattrs.structure((1, 2, 3), deque[int])
+deque([1, 2, 3])
+```
+
+Deques are unstructured into lists, or into deques when using the {class}`BaseConverter`.
+
+```{versionadded} 23.1.0
+
+```
+
+
+### Sets and Frozensets
+
+Sets and frozensets can be structured from any iterable object.
+Types converting to sets are:
+
+- `typing.Set[T]`
+- `typing.MutableSet[T]`
+- `set[T]`
+
+Types converting to frozensets are:
+
+- `typing.FrozenSet[T]`
+- `frozenset[T]`
+
+In all cases, a new set or frozenset will be returned.
+A bare type, for example `MutableSet` instead of `MutableSet[int]`, is equivalent to `MutableSet[Any]`.
+
+```{doctest}
+
+>>> cattrs.structure([1, 2, 3, 4], set)
+{1, 2, 3, 4}
+```
+
+Sets and frozensets are unstructured into the same class.
+
+
+### Typed Dicts
+
+[TypedDicts](https://peps.python.org/pep-0589/) can be structured from mapping objects, usually dictionaries.
+
+```{doctest}
+>>> from typing import TypedDict
+
+>>> class MyTypedDict(TypedDict):
+... a: int
+
+>>> cattrs.structure({"a": "1"}, MyTypedDict)
+{'a': 1}
+```
+
+Both [_total_ and _non-total_](https://peps.python.org/pep-0589/#totality) TypedDicts are supported, and inheritance between any combination works (except on 3.8 when `typing.TypedDict` is used, see below).
+Generic TypedDicts work on Python 3.11 and later, since that is the first Python version that supports them in general.
+
+[`typing.Required` and `typing.NotRequired`](https://peps.python.org/pep-0655/) are supported.
+
+On Python 3.8, using `typing_extensions.TypedDict` is recommended since `typing.TypedDict` doesn't support all necessary features so certain combinations of subclassing, totality and `typing.Required` won't work.
+
+[Similar to _attrs_ classes](customizing.md#using-cattrsgen-generators), un/structuring can be customized using {meth}`cattrs.gen.typeddicts.make_dict_structure_fn` and {meth}`cattrs.gen.typeddicts.make_dict_unstructure_fn`.
+
+```{doctest}
+>>> from typing import TypedDict
+>>> from cattrs import Converter
+>>> from cattrs.gen import override
+>>> from cattrs.gen.typeddicts import make_dict_structure_fn
+
+>>> class MyTypedDict(TypedDict):
+... a: int
+... b: int
+
+>>> c = Converter()
+>>> c.register_structure_hook(
+... MyTypedDict,
+... make_dict_structure_fn(
+... MyTypedDict,
+... c,
+... a=override(rename="a-with-dash")
+... )
+... )
+
+>>> c.structure({"a-with-dash": 1, "b": 2}, MyTypedDict)
+{'b': 2, 'a': 1}
+```
+
+TypedDicts unstructure into dictionaries, potentially unchanged (depending on the exact field types and registered hooks).
+
+```{doctest}
+>>> from typing import TypedDict
+>>> from datetime import datetime, timezone
+>>> from cattrs import Converter
+
+>>> class MyTypedDict(TypedDict):
+... a: datetime
+
+>>> c = Converter()
+>>> c.register_unstructure_hook(datetime, lambda d: d.timestamp())
+
+>>> c.unstructure({"a": datetime(1970, 1, 1, tzinfo=timezone.utc)}, unstructure_as=MyTypedDict)
+{'a': 0.0}
+```
+
+```{versionadded} 23.1.0
+
+```
+
+
+## _attrs_ Classes and Dataclasses
+
+_attrs_ classes and dataclasses work out of the box.
+The fields require type annotations (even if static type-checking is not being used), or they will be treated as [](#typingany).
+
+When structuring, given a mapping `d` and class `A`, _cattrs_ will instantiate `A` with `d` unpacked.
+
+```{doctest}
+
+>>> @define
+... class A:
+... a: int
+... b: int
+
+>>> cattrs.structure({'a': 1, 'b': '2'}, A)
+A(a=1, b=2)
+```
+
+Tuples can be structured into classes using {meth}`structure_attrs_fromtuple() ` (`fromtuple` as in the opposite of [`attrs.astuple`](https://www.attrs.org/en/stable/api.html#attrs.astuple) and {meth}`BaseConverter.unstructure_attrs_astuple`).
+
+```{doctest}
+
+>>> @define
+... class A:
+... a: str
+... b: int
+
+>>> cattrs.structure_attrs_fromtuple(['string', '2'], A)
+A(a='string', b=2)
+```
+
+Loading from tuples can be made the default by creating a new {class}`Converter ` with `unstruct_strat=cattr.UnstructureStrategy.AS_TUPLE`.
+
+```{doctest}
+
+>>> converter = cattrs.Converter(unstruct_strat=cattr.UnstructureStrategy.AS_TUPLE)
+>>> @define
+... class A:
+... a: str
+... b: int
+
+>>> converter.structure(['string', '2'], A)
+A(a='string', b=2)
+```
+
+Structuring from tuples can also be made the default for specific classes only by registering a hook the usual way.
+
+```{doctest}
+
+>>> converter = cattrs.Converter()
+
+>>> @define
+... class A:
+... a: str
+... b: int
+
+>>> converter.register_structure_hook(A, converter.structure_attrs_fromtuple)
+```
+
+
+### Generics
+
+Generic _attrs_ classes and dataclasses are fully supported, both using `typing.Generic` and [PEP 695](https://peps.python.org/pep-0695/).
+
+```python
+>>> @define
+... class A[T]:
+... a: T
+
+>>> cattrs.structure({"a": "1"}, A[int])
+A(a=1)
+```
+
+
+### Using Attribute Types and Converters
+
+By default, {meth}`structure() ` will use hooks registered using {meth}`register_structure_hook() `
+to convert values to the attribute type, and proceed to invoking any converters registered on attributes with `field`.
+
+```{doctest}
+
+>>> from ipaddress import IPv4Address, ip_address
+>>> converter = cattrs.Converter()
+
+# Note: register_structure_hook has not been called, so this will fallback to 'ip_address'
+>>> @define
+... class A:
+... a: IPv4Address = field(converter=ip_address)
+
+>>> converter.structure({'a': '127.0.0.1'}, A)
+A(a=IPv4Address('127.0.0.1'))
+```
+
+Priority is still given to hooks registered with {meth}`register_structure_hook() `,
+but this priority can be inverted by setting `prefer_attrib_converters` to `True`.
+
+```{doctest}
+
+>>> converter = cattrs.Converter(prefer_attrib_converters=True)
+
+>>> @define
+... class A:
+... a: int = field(converter=lambda v: int(v) + 5)
+
+>>> converter.structure({'a': '10'}, A)
+A(a=15)
+```
+
+```{seealso}
+If an _attrs_ or dataclass class uses inheritance and as such has one or several subclasses, it can be structured automatically to its exact subtype by using the [include subclasses](strategies.md#include-subclasses-strategy) strategy.
+```
+
+
+## Unions
+
+Unions of `NoneType` and a single other type (also known as optionals) are supported by a [special case](#optionals).
+
+
+### Automatic Disambiguation
+
+_cattrs_ includes an opinionated strategy for automatically handling unions of _attrs_ classes; see [](unions.md#default-union-strategy) for details.
+
+When unstructuring these kinds of unions, each union member will be unstructured according to the hook for that type.
+
+
+### Unions of Simple Types
+
+_cattrs_ comes with the [](strategies.md#union-passthrough), which enables converters to structure unions of many primitive types and literals.
+This strategy can be applied to any converter, and is pre-applied to all [preconf](preconf.md) converters according to their underlying protocols.
+
+
+## Special Typing Forms
+
+
+### `typing.Any`
+
+When structuring, use `typing.Any` to avoid applying any conversions to the object you're structuring; it will simply be passed through.
+
+```{doctest}
+
+>>> cattrs.structure(1, Any)
+1
+>>> d = {1: 1}
+>>> cattrs.structure(d, Any) is d
+True
+```
+
+When unstructuring, `typing.Any` will make the value be unstructured according to its runtime class.
+
+```{versionchanged} 24.1.0
+Previously, the unstructuring rules for `Any` were underspecified, leading to inconsistent behavior.
+```
+
+### `typing.Literal`
+
+When structuring, [PEP 586](https://peps.python.org/pep-0586/) literals are validated to be in the allowed set of values.
+
+```{doctest}
+>>> from typing import Literal
+
+>>> cattrs.structure(1, Literal[1, 2])
+1
+```
+
+When unstructuring, literals are passed through.
+
+```{versionadded} 1.7.0
+
+```
+
+
+### `typing.Final`
+
+[PEP 591](https://peps.python.org/pep-0591/) Final attribute types (`Final[int]`) are supported and handled according to the inner type (in this case, `int`).
+
+```{versionadded} 23.1.0
+
+```
+
+
+### `typing.Annotated`
+
+[PEP 593](https://www.python.org/dev/peps/pep-0593/) annotations (`typing.Annotated[type, ...]`) are supported and are handled using the first type present in the annotated type.
+
+```{versionadded} 1.4.0
+
+```
+
+
+### Type Aliases
+
+[Type aliases](https://docs.python.org/3/library/typing.html#type-aliases) are supported on Python 3.12+ and are handled according to the rules for their underlying type.
+Their hooks can also be overriden using [](customizing.md#predicate-hooks).
+
+```{warning}
+Type aliases using [`typing.TypeAlias`](https://docs.python.org/3/library/typing.html#typing.TypeAlias) aren't supported since there is no way at runtime to distinguish them from their underlying types.
+```
+
+```python
+>>> from datetime import datetime, UTC
+
+>>> type IsoDate = datetime
+
+>>> converter = cattrs.Converter()
+>>> converter.register_structure_hook_func(
+... lambda t: t is IsoDate, lambda v, _: datetime.fromisoformat(v)
+... )
+>>> converter.register_unstructure_hook_func(
+... lambda t: t is IsoDate, lambda v: v.isoformat()
+... )
+
+>>> converter.structure("2022-01-01", IsoDate)
+datetime.datetime(2022, 1, 1, 0, 0)
+>>> converter.unstructure(datetime.now(UTC), unstructure_as=IsoDate)
+'2023-11-20T23:10:46.728394+00:00'
+```
+
+```{versionadded} 24.1.0
+
+```
+
+
+### `typing.NewType`
+
+[NewTypes](https://docs.python.org/3/library/typing.html#newtype) are supported and are handled according to the rules for their underlying type.
+Their hooks can also be overriden using {meth}`Converter.register_structure_hook() `.
+
+```{doctest}
+
+>>> from typing import NewType
+>>> from datetime import datetime
+
+>>> IsoDate = NewType("IsoDate", datetime)
+
+>>> converter = cattrs.Converter()
+>>> converter.register_structure_hook(IsoDate, lambda v, _: datetime.fromisoformat(v))
+
+>>> converter.structure("2022-01-01", IsoDate)
+datetime.datetime(2022, 1, 1, 0, 0)
+```
+
+```{versionadded} 22.2.0
+
+```
+
+
+### `typing.Protocol`
+
+[Protocols](https://peps.python.org/pep-0544/) cannot be structured by default and so require custom hooks.
+
+Protocols are unstructured according to the actual runtime type of the value.
+
+```{versionadded} 1.9.0
+
+```
\ No newline at end of file
diff --git a/docs/indepth.md b/docs/indepth.md
index 0d7802e2..94048349 100644
--- a/docs/indepth.md
+++ b/docs/indepth.md
@@ -2,8 +2,6 @@
```{currentmodule} cattrs
```
-## Converters
-
Converters are registries of rules _cattrs_ uses to perform function composition and generate its un/structuring functions.
Currently, a converter contains the following state:
@@ -18,7 +16,57 @@ Currently, a converter contains the following state:
Converters may be cloned using the {meth}`Converter.copy() ` method.
The new copy may be changed through the `copy` arguments, but will retain all manually registered hooks from the original.
-### Fallback Hook Factories
+
+## Customizing Collection Unstructuring
+
+```{important}
+This feature is supported for Python 3.9 and later.
+```
+
+Overriding collection unstructuring in a generic way can be a very useful feature.
+A common example is using a JSON library that doesn't support sets, but expects lists and tuples instead.
+
+Using ordinary unstructuring hooks for this is unwieldy due to the semantics of
+[singledispatch](https://docs.python.org/3/library/functools.html#functools.singledispatch);
+in other words, you'd need to register hooks for all specific types of set you're using (`set[int]`, `set[float]`,
+`set[str]`...), which is not useful.
+
+Function-based hooks can be used instead, but come with their own set of challenges - they're complicated to write efficiently.
+
+The {class}`Converter` supports easy customizations of collection unstructuring using its `unstruct_collection_overrides` parameter.
+For example, to unstructure all sets into lists, use the following:
+
+```{doctest}
+
+>>> from collections.abc import Set
+>>> converter = cattrs.Converter(unstruct_collection_overrides={Set: list})
+
+>>> converter.unstructure({1, 2, 3})
+[1, 2, 3]
+```
+
+Going even further, the `Converter` contains heuristics to support the following Python types, in order of decreasing generality:
+
+- `typing.Sequence`, `typing.MutableSequence`, `list`, `deque`, `tuple`
+- `typing.Set`, `frozenset`, `typing.MutableSet`, `set`
+- `typing.Mapping`, `typing.MutableMapping`, `dict`, `defaultdict`, `collections.OrderedDict`, `collections.Counter`
+
+For example, if you override the unstructure type for `Sequence`, but not for `MutableSequence`, `list` or `tuple`, the override will also affect those types.
+An easy way to remember the rule:
+
+- all `MutableSequence` s are `Sequence` s, so the override will apply
+- all `list` s are `MutableSequence` s, so the override will apply
+- all `tuple` s are `Sequence` s, so the override will apply
+
+If, however, you override only `MutableSequence`, fields annotated as `Sequence` will not be affected (since not all sequences are mutable sequences), and fields annotated as tuples will not be affected (since tuples
+are not mutable sequences in the first place).
+
+Similar logic applies to the set and mapping hierarchies.
+
+Make sure you're using the types from `collections.abc` on Python 3.9+, and from `typing` on older Python versions.
+
+
+## Fallback Hook Factories
By default, when a {class}`converter ` cannot handle a type it will:
@@ -54,7 +102,7 @@ This also enables converters to be chained.
```
-### `cattrs.Converter`
+## `cattrs.Converter`
The {class}`Converter` is a converter variant that automatically generates, compiles and caches specialized structuring and unstructuring hooks for _attrs_ classes, dataclasses and TypedDicts.
@@ -68,7 +116,7 @@ The {class}`Converter` is a converter variant that automatically generates, comp
The {class}`Converter` used to be called `GenConverter`, and that alias is still present for backwards compatibility.
-### `cattrs.BaseConverter`
+## `cattrs.BaseConverter`
The {class}`BaseConverter` is a simpler and slower converter variant.
It does no code generation, so it may be faster on first-use which can be useful in specific cases, like CLI applications where startup time is more important than throughput.
diff --git a/docs/index.md b/docs/index.md
index 691836e9..7e7eb8a3 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -6,8 +6,7 @@ hidden: true
self
basics
-structuring
-unstructuring
+defaulthooks
customizing
strategies
validation
diff --git a/docs/preconf.md b/docs/preconf.md
index d68b3dfa..48d75ce3 100644
--- a/docs/preconf.md
+++ b/docs/preconf.md
@@ -13,13 +13,8 @@ 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 classes and type annotations, both for structuring and unstructuring:
+These converters support the following additional classes and type annotations, both for structuring and unstructuring:
-- `str`, `bytes`, `int`, `float`, `pathlib.Path` int enums, string enums
-- _attrs_ classes and dataclasses
-- lists, homogenous tuples, heterogenous tuples, dictionaries, counters, sets, frozensets
-- optionals
-- sequences, mutable sequences, mappings, mutable mappings, sets, mutable sets
- `datetime.datetime`, `datetime.date`
```{versionadded} 22.1.0
@@ -42,23 +37,25 @@ All preconf converters now have `loads` and `dumps` methods, which combine un/st
Particular libraries may have additional constraints documented below.
-Third-party libraries can be specified as optional (extra) dependencies on `cattrs` during installation.
+Third-party libraries can be specified as optional (extra) dependencies on _cattrs_ during installation.
Optional install targets should match the name of the {mod}`cattrs.preconf` modules.
```console
# Using pip
-pip install cattrs[ujson]
+$ pip install cattrs[ujson]
# Using poetry
-poetry add --extras tomlkit cattrs
+$ poetry add --extras tomlkit cattrs
```
+
## Standard Library _json_
Found at {mod}`cattrs.preconf.json`.
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.
+
## _ujson_
Found at {mod}`cattrs.preconf.ujson`.
@@ -67,15 +64,19 @@ Bytes are serialized as base 85 strings. Sets are serialized as lists, and deser
`ujson` doesn't support integers less than -9223372036854775808, and greater than 9223372036854775807, nor does it support `float('inf')`.
+
## _orjson_
Found at {mod}`cattrs.preconf.orjson`.
-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.
+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.
_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.
+
## _msgpack_
Found at {mod}`cattrs.preconf.msgpack`.
@@ -86,6 +87,7 @@ _msgpack_ doesn't support integers less than -9223372036854775808, and greater t
When parsing msgpack data from bytes, the library needs to be passed `strict_map_key=False` to get the full range of compatibility.
+
## _cbor2_
```{versionadded} 23.1.0
@@ -110,6 +112,7 @@ Use keyword argument `canonical=True` for efficient encoding to the smallest bin
Floats can be forced to smaller output by casting to lower-precision formats by casting to `numpy` floats (and back to Python floats).
Example: `float(np.float32(value))` or `float(np.float16(value))`
+
## _bson_
Found at {mod}`cattrs.preconf.bson`. Tested against the _bson_ module bundled with the _pymongo_ library, not the standalone PyPI _bson_ package.
@@ -124,12 +127,14 @@ The _bson_ datetime representation doesn't support microsecond accuracy.
When encoding and decoding, the library needs to be passed `codec_options=bson.CodecOptions(tz_aware=True)` to get the full range of compatibility.
+
## _pyyaml_
Found at {mod}`cattrs.preconf.pyyaml`.
Frozensets are serialized as lists, and deserialized back into frozensets. `date` s are serialized as ISO 8601 strings.
+
## _tomlkit_
Found at {mod}`cattrs.preconf.tomlkit`.
diff --git a/docs/structuring.md b/docs/structuring.md
deleted file mode 100644
index 8ca19a81..00000000
--- a/docs/structuring.md
+++ /dev/null
@@ -1,603 +0,0 @@
-# What You Can Structure and How
-
-The philosophy of _cattrs_ structuring is simple: give it an instance of Python built-in types and collections, and a type describing the data you want out.
-_cattrs_ will convert the input data into the type you want, or throw an exception.
-
-All structuring conversions are composable, where applicable.
-This is demonstrated further in the examples.
-
-## Primitive Values
-
-### `typing.Any`
-
-Use `typing.Any` to avoid applying any conversions to the object you're
-structuring; it will simply be passed through.
-
-```{doctest}
-
->>> cattrs.structure(1, Any)
-1
->>> d = {1: 1}
->>> cattrs.structure(d, Any) is d
-True
-```
-
-### `int`, `float`, `str`, `bytes`
-
-Use any of these primitive types to convert the object to the type.
-
-```{doctest}
-
->>> cattrs.structure(1, str)
-'1'
->>> cattrs.structure("1", float)
-1.0
-```
-
-In case the conversion isn't possible, the expected exceptions will be
-propagated out. The particular exceptions are the same as if you'd tried to
-do the conversion yourself, directly.
-
-```python
->>> cattrs.structure("not-an-int", int)
-Traceback (most recent call last):
-...
-ValueError: invalid literal for int() with base 10: 'not-an-int'
-```
-
-### Enums
-
-Enums will be structured by their values. This works even for complex values,
-like tuples.
-
-```{doctest}
-
->>> @unique
-... class CatBreed(Enum):
-... SIAMESE = "siamese"
-... MAINE_COON = "maine_coon"
-... SACRED_BIRMAN = "birman"
-
->>> cattrs.structure("siamese", CatBreed)
-
-```
-
-Again, in case of errors, the expected exceptions will fly out.
-
-```python
->>> cattrs.structure("alsatian", CatBreed)
-Traceback (most recent call last):
-...
-ValueError: 'alsatian' is not a valid CatBreed
-```
-
-### `pathlib.Path`
-
-[`pathlib.Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) objects are structured using their string value.
-
-```{doctest}
->>> from pathlib import Path
-
->>> cattrs.structure("/root", Path)
-PosixPath('/root')
-```
-
-In case the conversion isn't possible, the resulting exception is propagated out.
-
-```{versionadded} 23.1.0
-
-```
-
-## Collections and Other Generics
-
-### Optionals
-
-`Optional` primitives and collections are supported out of the box.
-
-```{doctest}
-
->>> cattrs.structure(None, int)
-Traceback (most recent call last):
-...
-TypeError: int() argument must be a string, a bytes-like object or a number, not 'NoneType'
->>> cattrs.structure(None, Optional[int])
->>> # None was returned.
-```
-
-Bare `Optional` s (non-parameterized, just `Optional`, as opposed to
-`Optional[str]`) aren't supported, use `Optional[Any]` instead.
-
-The Python 3.10 more readable syntax, `str | None` instead of `Optional[str]`, is also supported.
-
-This generic type is composable with all other converters.
-
-```{doctest}
-
->>> cattrs.structure(1, Optional[float])
-1.0
-```
-
-### Lists
-
-Lists can be produced from any iterable object. Types converting to lists are:
-
-- `Sequence[T]`
-- `MutableSequence[T]`
-- `List[T]`
-- `list[T]`
-
-In all cases, a new list will be returned, so this operation can be used to
-copy an iterable into a list. A bare type, for example `Sequence` instead of
-`Sequence[int]`, is equivalent to `Sequence[Any]`.
-
-```{doctest}
-
->>> cattrs.structure((1, 2, 3), MutableSequence[int])
-[1, 2, 3]
-```
-
-These generic types are composable with all other converters.
-
-```{doctest}
-
->>> cattrs.structure((1, None, 3), list[Optional[str]])
-['1', None, '3']
-```
-
-### Deques
-
-Deques can be produced from any iterable object. Types converting
-to deques are:
-
-- `Deque[T]`
-- `deque[T]`
-
-In all cases, a new **unbounded** deque (`maxlen=None`) will be returned,
-so this operation can be used to copy an iterable into a deque.
-If you want to convert into bounded `deque`, registering a custom structuring hook is a good approach.
-
-```{doctest}
-
->>> from collections import deque
->>> cattrs.structure((1, 2, 3), deque[int])
-deque([1, 2, 3])
-```
-
-These generic types are composable with all other converters.
-
-```{doctest}
->>> cattrs.structure((1, None, 3), deque[Optional[str]])
-deque(['1', None, '3'])
-```
-
-```{versionadded} 23.1.0
-
-```
-
-### Sets and Frozensets
-
-Sets and frozensets can be produced from any iterable object. Types converting
-to sets are:
-
-- `Set[T]`
-- `MutableSet[T]`
-- `set[T]`
-
-Types converting to frozensets are:
-
-- `FrozenSet[T]`
-- `frozenset[T]`
-
-In all cases, a new set or frozenset will be returned, so this operation can be
-used to copy an iterable into a set. A bare type, for example `MutableSet`
-instead of `MutableSet[int]`, is equivalent to `MutableSet[Any]`.
-
-```{doctest}
-
->>> cattrs.structure([1, 2, 3, 4], Set)
-{1, 2, 3, 4}
-```
-
-These generic types are composable with all other converters.
-
-```{doctest}
-
->>> cattrs.structure([[1, 2], [3, 4]], set[frozenset[str]])
-{frozenset({'2', '1'}), frozenset({'4', '3'})}
-```
-
-### Dictionaries
-
-Dicts can be produced from other mapping objects. To be more precise, the
-object being converted must expose an `items()` method producing an iterable
-key-value tuples, and be able to be passed to the `dict` constructor as an
-argument. Types converting to dictionaries are:
-
-- `Dict[K, V]`
-- `MutableMapping[K, V]`
-- `Mapping[K, V]`
-- `dict[K, V]`
-
-In all cases, a new dict will be returned, so this operation can be
-used to copy a mapping into a dict. Any type parameters set to `typing.Any`
-will be passed through unconverted. If both type parameters are absent,
-they will be treated as `Any` too.
-
-```{doctest}
-
->>> from collections import OrderedDict
->>> cattrs.structure(OrderedDict([(1, 2), (3, 4)]), Dict)
-{1: 2, 3: 4}
-```
-
-These generic types are composable with all other converters. Note both keys
-and values can be converted.
-
-```{doctest}
-
->>> cattrs.structure({1: None, 2: 2.0}, dict[str, Optional[int]])
-{'1': None, '2': 2}
-```
-
-### Typed Dicts
-
-[TypedDicts](https://peps.python.org/pep-0589/) can be produced from mapping objects, usually dictionaries.
-
-```{doctest}
->>> from typing import TypedDict
-
->>> class MyTypedDict(TypedDict):
-... a: int
-
->>> cattrs.structure({"a": "1"}, MyTypedDict)
-{'a': 1}
-```
-
-Both [_total_ and _non-total_](https://peps.python.org/pep-0589/#totality) TypedDicts are supported, and inheritance between any combination works (except on 3.8 when `typing.TypedDict` is used, see below).
-Generic TypedDicts work on Python 3.11 and later, since that is the first Python version that supports them in general.
-
-[`typing.Required` and `typing.NotRequired`](https://peps.python.org/pep-0655/) are supported.
-
-On Python 3.8, using `typing_extensions.TypedDict` is recommended since `typing.TypedDict` doesn't support all necessary features, so certain combinations of subclassing, totality and `typing.Required` won't work.
-
-[Similar to _attrs_ classes](customizing.md#using-cattrsgen-generators), structuring can be customized using {meth}`cattrs.gen.typeddicts.make_dict_structure_fn`.
-
-```{doctest}
->>> from typing import TypedDict
->>> from cattrs import Converter
->>> from cattrs.gen import override
->>> from cattrs.gen.typeddicts import make_dict_structure_fn
-
->>> class MyTypedDict(TypedDict):
-... a: int
-... b: int
-
->>> c = Converter()
->>> c.register_structure_hook(
-... MyTypedDict,
-... make_dict_structure_fn(
-... MyTypedDict,
-... c,
-... a=override(rename="a-with-dash")
-... )
-... )
-
->>> c.structure({"a-with-dash": 1, "b": 2}, MyTypedDict)
-{'b': 2, 'a': 1}
-```
-
-```{seealso} [Unstructuring TypedDicts.](unstructuring.md#typed-dicts)
-
-```
-
-```{versionadded} 23.1.0
-
-```
-
-### Homogeneous and Heterogeneous Tuples
-
-Homogeneous and heterogeneous tuples can be produced from iterable objects.
-Heterogeneous tuples require an iterable with the number of elements matching
-the number of type parameters exactly. Use:
-
-- `Tuple[A, B, C, D]`
-- `tuple[A, B, C, D]`
-
-Homogeneous tuples use:
-
-- `Tuple[T, ...]`
-- `tuple[T, ...]`
-
-In all cases a tuple will be returned. Any type parameters set to
-`typing.Any` will be passed through unconverted.
-
-```{doctest}
-
->>> cattrs.structure([1, 2, 3], tuple[int, str, float])
-(1, '2', 3.0)
-```
-
-The tuple conversion is composable with all other converters.
-
-```{doctest}
-
->>> cattrs.structure([{1: 1}, {2: 2}], tuple[dict[str, float], ...])
-({'1': 1.0}, {'2': 2.0})
-```
-
-### Unions
-
-Unions of `NoneType` and a single other type are supported (also known as
-`Optional` s). All other unions require a disambiguation function.
-
-#### Automatic Disambiguation
-
-In the case of a union consisting exclusively of `attrs` classes, `cattrs`
-will attempt to generate a disambiguation function automatically; this will
-succeed only if each class has a unique field. Given the following classes:
-
-```python
->>> @define
-... class A:
-... a = field()
-... x = field()
-
->>> @define
-... class B:
-... a = field()
-... y = field()
-
->>> @define
-... class C:
-... a = field()
-... z = field()
-```
-
-`cattrs` can deduce only instances of `A` will contain `x`, only instances
-of `B` will contain `y`, etc. A disambiguation function using this
-information will then be generated and cached. This will happen automatically,
-the first time an appropriate union is structured.
-
-#### Manual Disambiguation
-
-To support arbitrary unions, register a custom structuring hook for the union
-(see [Registering custom structuring hooks](structuring.md#registering-custom-structuring-hooks)).
-
-Another option is to use a custom tagged union strategy (see [Strategies - Tagged Unions](strategies.md#tagged-unions-strategy)).
-
-### `typing.Final`
-
-[PEP 591](https://peps.python.org/pep-0591/) Final attribute types (`Final[int]`) are supported and structured appropriately.
-
-```{versionadded} 23.1.0
-
-```
-
-```{seealso} [Unstructuring Final.](unstructuring.md#typingfinal)
-
-```
-
-## `typing.Annotated`
-
-[PEP 593](https://www.python.org/dev/peps/pep-0593/) annotations (`typing.Annotated[type, ...]`) are supported and are matched using the first type present in the annotated type.
-
-## Type Aliases
-
-[Type aliases](https://docs.python.org/3/library/typing.html#type-aliases) are supported on Python 3.12+ and are structured according to the rules for their underlying type.
-Their hooks can also be overriden using {meth}`Converter.register_structure_hook_func() `.
-(Since type aliases aren't proper classes they cannot be used with {meth}`Converter.register_structure_hook() `.)
-
-```{warning}
-Type aliases using [`typing.TypeAlias`](https://docs.python.org/3/library/typing.html#typing.TypeAlias) aren't supported since there is no way at runtime to distinguish them from their underlying types.
-```
-
-```python
->>> from datetime import datetime
-
->>> type IsoDate = datetime
-
->>> converter = cattrs.Converter()
->>> converter.register_structure_hook_func(
-... lambda t: t is IsoDate, lambda v, _: datetime.fromisoformat(v)
-... )
-
->>> converter.structure("2022-01-01", IsoDate)
-datetime.datetime(2022, 1, 1, 0, 0)
-```
-
-```{versionadded} 24.1.0
-
-```
-
-```{seealso} [Unstructuring Type Aliases.](unstructuring.md#type-aliases)
-
-```
-
-## `typing.NewType`
-
-[NewTypes](https://docs.python.org/3/library/typing.html#newtype) are supported and are structured according to the rules for their underlying type.
-Their hooks can also be overriden using {meth}`Converter.register_structure_hook() `.
-
-```{doctest}
-
->>> from typing import NewType
->>> from datetime import datetime
-
->>> IsoDate = NewType("IsoDate", datetime)
-
->>> converter = cattrs.Converter()
->>> converter.register_structure_hook(IsoDate, lambda v, _: datetime.fromisoformat(v))
-
->>> converter.structure("2022-01-01", IsoDate)
-datetime.datetime(2022, 1, 1, 0, 0)
-```
-
-```{versionadded} 22.2.0
-
-```
-
-```{seealso} [Unstructuring NewTypes.](unstructuring.md#typingnewtype)
-
-```
-
-## _attrs_ Classes and Dataclasses
-
-### Simple _attrs_ Classes and Dataclasses
-
-_attrs_ classes and dataclasses using primitives, collections of primitives
-and their own converters work out of the box. Given a mapping `d` and class
-`A`, _cattrs_ will simply instantiate `A` with `d` unpacked.
-
-```{doctest}
-
->>> @define
-... class A:
-... a: int
-... b: int
-
->>> cattrs.structure({'a': 1, 'b': '2'}, A)
-A(a=1, b=2)
-```
-
-Classes like these deconstructed into tuples can be structured using
-{meth}`structure_attrs_fromtuple() ` (`fromtuple` as in the opposite of
-`attr.astuple` and `converter.unstructure_attrs_astuple`).
-
-```{doctest}
-
->>> @define
-... class A:
-... a: str
-... b: int
-
->>> cattrs.structure_attrs_fromtuple(['string', '2'], A)
-A(a='string', b=2)
-```
-
-Loading from tuples can be made the default by creating a new {class}`Converter ` with
-`unstruct_strat=cattr.UnstructureStrategy.AS_TUPLE`.
-
-```{doctest}
-
->>> converter = cattrs.Converter(unstruct_strat=cattr.UnstructureStrategy.AS_TUPLE)
->>> @define
-... class A:
-... a: str
-... b: int
-
->>> converter.structure(['string', '2'], A)
-A(a='string', b=2)
-```
-
-Structuring from tuples can also be made the default for specific classes only;
-see registering custom structure hooks below.
-
-### Using Attribute Types and Converters
-
-By default, {meth}`structure() ` will use hooks registered using {meth}`register_structure_hook() `,
-to convert values to the attribute type, and fallback to invoking any converters registered on
-attributes with `field`.
-
-```{doctest}
-
->>> from ipaddress import IPv4Address, ip_address
->>> converter = cattrs.Converter()
-
-# Note: register_structure_hook has not been called, so this will fallback to 'ip_address'
->>> @define
-... class A:
-... a: IPv4Address = field(converter=ip_address)
-
->>> converter.structure({'a': '127.0.0.1'}, A)
-A(a=IPv4Address('127.0.0.1'))
-```
-
-Priority is still given to hooks registered with {meth}`register_structure_hook() `,
-but this priority can be inverted by setting `prefer_attrib_converters` to `True`.
-
-```{doctest}
-
->>> converter = cattrs.Converter(prefer_attrib_converters=True)
-
->>> @define
-... class A:
-... a: int = field(converter=lambda v: int(v) + 5)
-
->>> converter.structure({'a': '10'}, A)
-A(a=15)
-```
-
-### Complex _attrs_ Classes and Dataclasses
-
-Complex _attrs_ classes and dataclasses are classes with type information available for some or all attributes.
-These classes support almost arbitrary nesting.
-
-```{doctest}
-
->>> @define
-... class A:
-... a: int
-
->>> attrs.fields(A).a
-Attribute(name='a', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='a')
-```
-
-Type information can be used for all attribute types, not only attributes holding _attrs_ classes and dataclasses.
-
-```{doctest}
-
->>> @define
-... class A:
-... a: int = 0
-
->>> @define
-... class B:
-... b: A
-
->>> cattrs.structure({'b': {'a': '1'}}, B)
-B(b=A(a=1))
-```
-
-Generic _attrs_ classes and dataclasses are fully supported, both using `typing.Generic` and [PEP 695](https://peps.python.org/pep-0695/).
-
-```python
->>> @define
-... class A[T]:
-... a: T
-
->>> cattrs.structure({"a": "1"}, A[int])
-A(a=1)
-```
-
-Finally, if an _attrs_ or dataclass class uses inheritance and as such has one or several subclasses, it can be structured automatically to its exact subtype by using the [include subclasses](strategies.md#include-subclasses-strategy) strategy.
-
-## Registering Custom Structuring Hooks
-
-_cattrs_ doesn't know how to structure non-_attrs_ classes by default, so it has to be taught.
-This can be done by registering structuring hooks on a converter instance (including the global converter).
-
-Here's an example involving a simple, classic (i.e. non-_attrs_) Python class.
-
-```{doctest}
-
->>> class C:
-... def __init__(self, a):
-... self.a = a
-... def __repr__(self):
-... return f'C(a={self.a})'
-
->>> cattrs.structure({'a': 1}, C)
-Traceback (most recent call last):
-...
-StructureHandlerNotFoundError: Unsupported type: . Register a structure hook for it.
-
->>> cattrs.register_structure_hook(C, lambda d, t: C(**d))
->>> cattrs.structure({'a': 1}, C)
-C(a=1)
-```
-
-The structuring hooks are callables that take two arguments: the object to convert to the desired class and the type to convert to.
-(The type may seem redundant but is useful when dealing with generic types.)
-
-When using {meth}`cattrs.register_structure_hook`, the hook will be registered on the global converter.
-If you want to avoid changing the global converter, create an instance of {class}`cattrs.Converter` and register the hook on that.
\ No newline at end of file
diff --git a/docs/unions.md b/docs/unions.md
index 29e9352b..c564b9ec 100644
--- a/docs/unions.md
+++ b/docs/unions.md
@@ -1,8 +1,6 @@
-# Tips for Handling Unions
+# Handling Unions
-This sections contains information for advanced union handling.
-
-As mentioned in the structuring section, _cattrs_ is able to handle simple unions of _attrs_ classes [automatically](#default-union-strategy).
+_cattrs_ is able to handle simple unions of _attrs_ classes [automatically](#default-union-strategy).
More complex cases require converter customization (since there are many ways of handling unions).
_cattrs_ also comes with a number of strategies to help handle unions:
@@ -34,7 +32,7 @@ class ClassB:
In this case, a payload containing `{"field_one": "one"}` will produce an instance of `ClassA`.
````{note}
-The following snippet can be used to disable the use of literal fields, restoring the previous behavior.
+The following snippet can be used to disable the use of literal fields, restoring legacy behavior.
```python
from functools import partial
diff --git a/docs/unstructuring.md b/docs/unstructuring.md
deleted file mode 100644
index 2918ec47..00000000
--- a/docs/unstructuring.md
+++ /dev/null
@@ -1,333 +0,0 @@
-# What You Can Unstructure and How
-
-Unstructuring is intended to convert high-level, structured Python data (like
-instances of complex classes) into simple, unstructured data (like
-dictionaries).
-
-Unstructuring is simpler than structuring in that no target types are required.
-Simply provide an argument to {meth}`Converter.unstructure() ` and _cattrs_ will produce a
-result based on the registered unstructuring hooks.
-A number of default unstructuring hooks are documented here.
-
-## Primitive Types and Collections
-
-Primitive types (integers, floats, strings...) are simply passed through.
-Collections are copied. There's relatively little value in unstructuring
-these types directly as they are already unstructured and third-party
-libraries tend to support them directly.
-
-A useful use case for unstructuring collections is to create a deep copy of
-a complex or recursive collection.
-
-```{doctest}
-
->>> # A dictionary of strings to lists of tuples of floats.
->>> data = {'a': [[1.0, 2.0], [3.0, 4.0]]}
-
->>> copy = cattrs.unstructure(data)
->>> data == copy
-True
->>> data is copy
-False
-```
-
-### Typed Dicts
-
-[TypedDicts](https://peps.python.org/pep-0589/) unstructure into dictionaries, potentially unchanged (depending on the exact field types and registered hooks).
-
-```{doctest}
->>> from typing import TypedDict
->>> from datetime import datetime, timezone
->>> from cattrs import Converter
-
->>> class MyTypedDict(TypedDict):
-... a: datetime
-
->>> c = Converter()
->>> c.register_unstructure_hook(datetime, lambda d: d.timestamp())
-
->>> c.unstructure({"a": datetime(1970, 1, 1, tzinfo=timezone.utc)}, unstructure_as=MyTypedDict)
-{'a': 0.0}
-```
-
-Generic TypedDicts work on Python 3.11 and later, since that is the first Python version that supports them in general.
-
-On Python 3.8, using `typing_extensions.TypedDict` is recommended since `typing.TypedDict` doesn't support all necessary features, so certain combinations of subclassing, totality and `typing.Required` won't work.
-
-[Similar to _attrs_ classes](customizing.md#using-cattrsgen-generators), unstructuring can be customized using {meth}`cattrs.gen.typeddicts.make_dict_unstructure_fn`.
-
-```{doctest}
->>> from typing import TypedDict
->>> from cattrs import Converter
->>> from cattrs.gen import override
->>> from cattrs.gen.typeddicts import make_dict_unstructure_fn
-
->>> class MyTypedDict(TypedDict):
-... a: int
-... b: int
-
->>> c = Converter()
->>> c.register_unstructure_hook(
-... MyTypedDict,
-... make_dict_unstructure_fn(
-... MyTypedDict,
-... c,
-... a=override(omit=True)
-... )
-... )
-
->>> c.unstructure({"a": 1, "b": 2}, unstructure_as=MyTypedDict)
-{'b': 2}
-```
-
-```{seealso} [Structuring TypedDicts.](structuring.md#typed-dicts)
-
-```
-
-```{versionadded} 23.1.0
-
-```
-
-## `pathlib.Path`
-
-[`pathlib.Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) objects are unstructured into their string value.
-
-```{doctest}
->>> from pathlib import Path
-
->>> cattrs.unstructure(Path("/root"))
-'/root'
-```
-
-```{versionadded} 23.1.0
-
-```
-
-## Customizing Collection Unstructuring
-
-```{important}
-This feature is supported for Python 3.9 and later.
-```
-
-Sometimes it's useful to be able to override collection unstructuring in a
-generic way. A common example is using a JSON library that doesn't support
-sets, but expects lists and tuples instead.
-
-Using ordinary unstructuring hooks for this is unwieldy due to the semantics of
-[singledispatch](https://docs.python.org/3/library/functools.html#functools.singledispatch);
-in other words, you'd need to register hooks for all specific types of set you're using (`set[int]`, `set[float]`,
-`set[str]`...), which is not useful.
-
-Function-based hooks can be used instead, but come with their own set of
-challenges - they're complicated to write efficiently.
-
-The {class}`Converter ` supports easy customizations of collection unstructuring
-using its `unstruct_collection_overrides` parameter. For example, to
-unstructure all sets into lists, try the following:
-
-```{doctest}
-
->>> from collections.abc import Set
->>> converter = cattrs.Converter(unstruct_collection_overrides={Set: list})
-
->>> converter.unstructure({1, 2, 3})
-[1, 2, 3]
-```
-
-Going even further, the Converter contains heuristics to support the
-following Python types, in order of decreasing generality:
-
-- `Sequence`, `MutableSequence`, `list`, `deque`, `tuple`
-- `Set`, `frozenset`, `MutableSet`, `set`
-- `Mapping`, `MutableMapping`, `dict`, `defaultdict`, `OrderedDict`, `Counter`
-
-For example, if you override the unstructure type for `Sequence`, but not for
-`MutableSequence`, `list` or `tuple`, the override will also affect those
-types. An easy way to remember the rule:
-
-- all `MutableSequence` s are `Sequence` s, so the override will apply
-- all `list` s are `MutableSequence` s, so the override will apply
-- all `tuple` s are `Sequence` s, so the override will apply
-
-If, however, you override only `MutableSequence`, fields annotated as
-`Sequence` will not be affected (since not all sequences are mutable
-sequences), and fields annotated as tuples will not be affected (since tuples
-are not mutable sequences in the first place).
-
-Similar logic applies to the set and mapping hierarchies.
-
-Make sure you're using the types from `collections.abc` on Python 3.9+, and
-from `typing` on older Python versions.
-
-### `typing.Final`
-
-[PEP 591](https://peps.python.org/pep-0591/) Final attribute types (`Final[int]`) are supported and unstructured appropriately.
-
-```{versionadded} 23.1.0
-
-```
-
-```{seealso} [Structuring Final.](structuring.md#typingfinal)
-
-```
-
-## `typing.Annotated`
-
-[PEP 593](https://www.python.org/dev/peps/pep-0593/) `typing.Annotated[type, ...]` are supported and are matched using the first type present in the annotated type.
-
-## Type Aliases
-
-[Type aliases](https://docs.python.org/3/library/typing.html#type-aliases) are supported on Python 3.12+ and are unstructured according to the rules for their underlying type.
-Their hooks can also be overriden using {meth}`Converter.register_unstructure_hook() `.
-(Since type aliases aren't proper classes they cannot be used with {meth}`Converter.register_unstructure_hook() `.)
-
-```{warning}
-Type aliases using [`typing.TypeAlias`](https://docs.python.org/3/library/typing.html#typing.TypeAlias) aren't supported since there is no way at runtime to distinguish them from their underlying types.
-```
-
-```python
->>> from datetime import datetime, UTC
-
->>> type IsoDate = datetime
-
->>> converter = cattrs.Converter()
->>> converter.register_unstructure_hook_func(
-... lambda t: t is IsoDate,
-... lambda v: v.isoformat()
-... )
-
->>> converter.unstructure(datetime.now(UTC), unstructure_as=IsoDate)
-'2023-11-20T23:10:46.728394+00:00'
-```
-
-```{versionadded} 24.1.0
-
-```
-
-```{seealso} [Structuring Type Aliases.](structuring.md#type-aliases)
-
-```
-
-
-## `typing.NewType`
-
-[NewTypes](https://docs.python.org/3/library/typing.html#newtype) are supported and are unstructured according to the rules for their underlying type.
-Their hooks can also be overriden using {meth}`Converter.register_unstructure_hook() `.
-
-```{versionadded} 22.2.0
-
-```
-
-```{seealso} [Structuring NewTypes.](structuring.md#typingnewtype)
-
-```
-
-```{note}
-NewTypes are not supported by the legacy {class}`BaseConverter `.
-```
-
-## _attrs_ Classes and Dataclasses
-
-_attrs_ classes and dataclasses are supported out of the box.
-{class}`cattrs.Converters ` support two unstructuring strategies:
-
-- `UnstructureStrategy.AS_DICT` - similar to [`attrs.asdict()`](https://www.attrs.org/en/stable/api.html#attrs.asdict), unstructures _attrs_ and dataclass instances into dictionaries. This is the default.
-- `UnstructureStrategy.AS_TUPLE` - similar to [`attrs.astuple()`](https://www.attrs.org/en/stable/api.html#attrs.astuple), unstructures _attrs_ and dataclass instances into tuples.
-
-```{doctest}
-
->>> @define
-... class C:
-... a = field()
-... b = field()
-
->>> inst = C(1, 'a')
-
->>> converter = cattrs.Converter(unstruct_strat=cattrs.UnstructureStrategy.AS_TUPLE)
-
->>> converter.unstructure(inst)
-(1, 'a')
-```
-
-## Mixing and Matching Strategies
-
-Converters publicly expose two helper methods, {meth}`Converter.unstructure_attrs_asdict() `
-and {meth}`Converter.unstructure_attrs_astuple() `.
-These methods can be used with custom unstructuring hooks to selectively apply one strategy to instances of particular classes.
-
-Assume two nested _attrs_ classes, `Inner` and `Outer`; instances of `Outer` contain instances of `Inner`.
-Instances of `Outer` should be unstructured as dictionaries, and instances of `Inner` as tuples.
-Here's how to do this.
-
-```{doctest}
-
->>> @define
-... class Inner:
-... a: int
-
->>> @define
-... class Outer:
-... i: Inner
-
->>> inst = Outer(i=Inner(a=1))
-
->>> converter = cattrs.Converter()
->>> converter.register_unstructure_hook(Inner, converter.unstructure_attrs_astuple)
-
->>> converter.unstructure(inst)
-{'i': (1,)}
-```
-
-Of course, these methods can be used directly as well, without changing the converter strategy.
-
-```{doctest}
-
->>> @define
-... class C:
-... a: int
-... b: str
-
->>> inst = C(1, 'a')
-
->>> converter = cattrs.Converter()
-
->>> converter.unstructure_attrs_astuple(inst) # Default is AS_DICT.
-(1, 'a')
-```
-
-## Unstructuring Hook Factories
-
-Hook factories operate one level higher than unstructuring hooks; unstructuring
-hooks are functions registered to a class or predicate, and hook factories
-are functions (registered via a predicate) that produce unstructuring hooks.
-
-Unstructuring hooks factories are registered using {meth}`Converter.register_unstructure_hook_factory() `.
-
-Here's a small example showing how to use factory hooks to skip unstructuring `init=False` attributes on all _attrs_ classes.
-
-```{doctest}
-
->>> from attrs import define, has, field, fields
->>> from cattrs import override
->>> from cattrs.gen import make_dict_unstructure_fn
-
->>> c = cattrs.Converter()
->>> c.register_unstructure_hook_factory(
-... has,
-... lambda cl: make_dict_unstructure_fn(
-... cl, c, **{a.name: override(omit=True) for a in fields(cl) if not a.init}
-... )
-... )
-
->>> @define
-... class E:
-... an_int: int
-... another_int: int = field(init=False)
-
->>> inst = E(1)
->>> inst.another_int = 5
->>> c.unstructure(inst)
-{'an_int': 1}
-```
-
-A complex use case for hook factories is described over at {ref}`usage:Using factory hooks`.
diff --git a/docs/validation.md b/docs/validation.md
index 385d12ab..72302a91 100644
--- a/docs/validation.md
+++ b/docs/validation.md
@@ -1,18 +1,19 @@
# Validation
_cattrs_ has a detailed validation mode since version 22.1.0, and this mode is enabled by default.
-When running under detailed validation, the un/structuring hooks are slightly slower but produce more precise and exhaustive error messages.
+When running under detailed validation, the structuring hooks are slightly slower but produce richer and more precise error messages.
+Unstructuring hooks are not affected.
## Detailed Validation
```{versionadded} 22.1.0
```
-In detailed validation mode, any un/structuring errors will be grouped and raised together as a {class}`cattrs.BaseValidationError`, which is a [PEP 654 ExceptionGroup](https://www.python.org/dev/peps/pep-0654/).
+In detailed validation mode, any structuring errors will be grouped and raised together as a {class}`cattrs.BaseValidationError`, which is a [PEP 654 ExceptionGroup](https://www.python.org/dev/peps/pep-0654/).
ExceptionGroups are special exceptions which contain lists of other exceptions, which may themselves be other ExceptionGroups.
In essence, ExceptionGroups are trees of exceptions.
-When un/structuring a class, _cattrs_ will gather any exceptions on a field-by-field basis and raise them as a {class}`cattrs.ClassValidationError`, which is a subclass of {class}`BaseValidationError `.
+When structuring a class, _cattrs_ will gather any exceptions on a field-by-field basis and raise them as a {class}`cattrs.ClassValidationError`, which is a subclass of {class}`BaseValidationError `.
When structuring sequences and mappings, _cattrs_ will gather any exceptions on a key- or index-basis and raise them as a {class}`cattrs.IterableValidationError`, which is a subclass of {class}`BaseValidationError `.
@@ -72,23 +73,27 @@ class Class:
```
-ExceptionGroup stack traces are great while you're developing, but sometimes a more compact representation of validation errors is better.
+ExceptionGroup stack traces are useful while developing, but sometimes a more compact representation of validation errors is required.
_cattrs_ provides a helper function, {func}`cattrs.transform_error`, which transforms validation errors into lists of error messages.
The example from the previous paragraph produces the following error messages:
-```python
->>> from cattrs import transform_error
+```{testsetup} class
+@define
+class Class:
+ a_list: list[int]
+ a_dict: dict[str, int]
+```
+
+```{doctest} class
+
+>>> from cattrs import structure, transform_error
>>> try:
... structure({"a_list": ["a"], "a_dict": {"str": "a"}}, Class)
... except Exception as exc:
... print(transform_error(exc))
-
-[
- 'invalid value for type, expected int @ $.a_list[0]',
- "invalid value for type, expected int @ $.a_dict['str']"
-]
+['invalid value for type, expected int @ $.a_list[0]', "invalid value for type, expected int @ $.a_dict['str']"]
```
A small number of built-in exceptions are converted into error messages automatically.
diff --git a/src/cattrs/__init__.py b/src/cattrs/__init__.py
index 6ed83139..db496363 100644
--- a/src/cattrs/__init__.py
+++ b/src/cattrs/__init__.py
@@ -13,38 +13,32 @@
from .gen import override
from .v import transform_error
-__all__ = (
- "AttributeValidationNote",
+__all__ = [
+ "structure",
+ "unstructure",
+ "get_structure_hook",
+ "get_unstructure_hook",
+ "register_structure_hook_func",
+ "register_structure_hook",
+ "register_unstructure_hook_func",
+ "register_unstructure_hook",
+ "structure_attrs_fromdict",
+ "structure_attrs_fromtuple",
+ "global_converter",
"BaseConverter",
+ "Converter",
+ "AttributeValidationNote",
"BaseValidationError",
"ClassValidationError",
- "Converter",
- "converters",
- "disambiguators",
- "dispatch",
- "errors",
"ForbiddenExtraKeysError",
- "gen",
"GenConverter",
- "global_converter",
"IterableValidationError",
"IterableValidationNote",
"override",
- "preconf",
- "register_structure_hook_func",
- "register_structure_hook",
- "register_unstructure_hook_func",
- "register_unstructure_hook",
- "structure_attrs_fromdict",
- "structure_attrs_fromtuple",
- "structure",
"StructureHandlerNotFoundError",
"transform_error",
- "unstructure",
"UnstructureStrategy",
- "get_structure_hook",
- "get_unstructure_hook",
-)
+]
#: The global converter. Prefer creating your own if customizations are required.
global_converter: Final = Converter()
diff --git a/src/cattrs/converters.py b/src/cattrs/converters.py
index 6a17902f..172a7584 100644
--- a/src/cattrs/converters.py
+++ b/src/cattrs/converters.py
@@ -171,6 +171,7 @@ def __init__(
(lambda t: issubclass(t, Enum), self._unstructure_enum),
(has, self._unstructure_attrs),
(is_union_type, self._unstructure_union),
+ (lambda t: t is Any, self.unstructure),
]
)
@@ -1001,8 +1002,7 @@ def gen_unstructure_optional(self, cl: type[T]) -> Callable[[T], Any]:
union_params = cl.__args__
other = union_params[0] if union_params[1] is NoneType else union_params[1]
- # TODO: Remove this special case when we make unstructuring Any consistent.
- if other is Any or isinstance(other, TypeVar):
+ if isinstance(other, TypeVar):
handler = self.unstructure
else:
handler = self._unstructure_func.dispatch(other)
diff --git a/src/cattrs/gen/__init__.py b/src/cattrs/gen/__init__.py
index 4d201f8f..02a676d7 100644
--- a/src/cattrs/gen/__init__.py
+++ b/src/cattrs/gen/__init__.py
@@ -6,6 +6,7 @@
from attrs import NOTHING, Factory, resolve_types
from .._compat import (
+ TypeAlias,
adapted_fields,
get_args,
get_origin,
@@ -34,6 +35,15 @@
from cattr.converters import BaseConverter
+__all__ = [
+ "make_dict_unstructure_fn",
+ "make_dict_structure_fn",
+ "make_iterable_unstructure_fn",
+ "make_hetero_tuple_unstructure_fn",
+ "make_mapping_unstructure_fn",
+ "make_mapping_structure_fn",
+]
+
def override(
omit_if_default: bool | None = None,
@@ -682,7 +692,8 @@ def make_iterable_unstructure_fn(
return globs[fn_name]
-HeteroTupleUnstructureFn = Callable[[Tuple[Any, ...]], Any]
+#: A type alias for heterogeneous tuple unstructure hooks.
+HeteroTupleUnstructureFn: TypeAlias = Callable[[Tuple[Any, ...]], Any]
def make_hetero_tuple_unstructure_fn(
@@ -754,11 +765,9 @@ def make_mapping_unstructure_fn(
if kh == identity:
kh = None
- if val_arg is not Any:
- # TODO: Remove this once we have more consistent Any handling in place.
- val_handler = converter._unstructure_func.dispatch(val_arg)
- if val_handler == identity:
- val_handler = None
+ val_handler = converter._unstructure_func.dispatch(val_arg)
+ if val_handler == identity:
+ val_handler = None
globs = {
"__cattr_mapping_cl": unstructure_to or cl,
diff --git a/tests/test_any.py b/tests/test_any.py
index 94fa0bd9..c580bfc5 100644
--- a/tests/test_any.py
+++ b/tests/test_any.py
@@ -1,5 +1,5 @@
"""Tests for handling `typing.Any`."""
-from typing import Any, Dict
+from typing import Any, Dict, Optional
from attrs import define
@@ -12,3 +12,15 @@ class A:
def test_unstructuring_dict_of_any(converter):
"""Dicts with Any values should use runtime dispatch for their values."""
assert converter.unstructure({"a": A()}, Dict[str, Any]) == {"a": {}}
+
+
+def test_unstructuring_any(converter):
+ """`Any` should use runtime dispatch."""
+
+ assert converter.unstructure(A(), Any) == {}
+
+
+def test_unstructure_optional_any(converter):
+ """Unstructuring `Optional[Any]` should use the runtime value."""
+
+ assert converter.unstructure(A(), Optional[Any]) == {}
diff --git a/tests/test_final.py b/tests/test_final.py
index 5f6680e8..1d780f61 100644
--- a/tests/test_final.py
+++ b/tests/test_final.py
@@ -37,8 +37,8 @@ def test_unstructure_bare_final(genconverter: Converter) -> None:
assert genconverter.unstructure(D(1)) == {"a": 1, "b": 5, "c": 3}
genconverter.register_unstructure_hook(int, lambda i: str(i))
- # Bare finals don't work with factories.
- assert genconverter.unstructure(D(1)) == {"a": "1", "b": "5", "c": 3}
+ # Bare finals resolve to `Final[Any]`, so the custom hook works.
+ assert genconverter.unstructure(D(1)) == {"a": "1", "b": "5", "c": "3"}
def test_structure_bare_final(genconverter: Converter) -> None:
diff --git a/tests/typed.py b/tests/typed.py
index 6bed20d8..98a2ba82 100644
--- a/tests/typed.py
+++ b/tests/typed.py
@@ -125,7 +125,7 @@ def simple_typed_attrs(
) -> SearchStrategy[Tuple[_CountingAttr, SearchStrategy[PosArgs]]]:
if not is_39_or_later:
res = (
- bare_typed_attrs(defaults, kw_only)
+ any_typed_attrs(defaults, kw_only)
| int_typed_attrs(defaults, kw_only)
| str_typed_attrs(defaults, kw_only)
| float_typed_attrs(defaults, kw_only)
@@ -170,7 +170,7 @@ def simple_typed_attrs(
)
else:
res = (
- bare_typed_attrs(defaults, kw_only)
+ any_typed_attrs(defaults, kw_only)
| int_typed_attrs(defaults, kw_only)
| str_typed_attrs(defaults, kw_only)
| float_typed_attrs(defaults, kw_only)
@@ -316,11 +316,10 @@ def key(t):
@composite
-def bare_typed_attrs(draw, defaults=None, kw_only=None):
- """
- Generate a tuple of an attribute and a strategy that yields values
- appropriate for that attribute.
- """
+def any_typed_attrs(
+ draw: DrawFn, defaults=None, kw_only=None
+) -> Tuple[_CountingAttr, SearchStrategy[None]]:
+ """Attributes typed as `Any`, having values of `None`."""
default = NOTHING
if defaults is True or (defaults is None and draw(booleans())):
default = None