Skip to content

Commit

Permalink
Fix docs, update furo (#517)
Browse files Browse the repository at this point in the history
* Update Black

* Fix docs, update furo
  • Loading branch information
Tinche committed Mar 6, 2024
1 parent 39e698f commit b3c6ba7
Show file tree
Hide file tree
Showing 61 changed files with 291 additions and 213 deletions.
165 changes: 82 additions & 83 deletions docs/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,28 @@ In certain situations, you might want to deviate from this behavior and use alte
For example, consider the following `Point` class describing points in 2D space, which offers two `classmethod`s for alternative creation:

```{doctest}
from __future__ import annotations
import math
from attrs import define
@define
class Point:
"""A point in 2D space."""
x: float
y: float
@classmethod
def from_tuple(cls, coordinates: tuple[float, float]) -> Point:
"""Create a point from a tuple of Cartesian coordinates."""
return Point(*coordinates)
@classmethod
def from_polar(cls, radius: float, angle: float) -> Point:
"""Create a point from its polar coordinates."""
return Point(radius * math.cos(angle), radius * math.sin(angle))
>>> from __future__ import annotations
>>> import math
>>> from attrs import define
>>> @define
... class Point:
... """A point in 2D space."""
... x: float
... y: float
...
... @classmethod
... def from_tuple(cls, coordinates: tuple[float, float]) -> Point:
... """Create a point from a tuple of Cartesian coordinates."""
... return Point(*coordinates)
...
... @classmethod
... def from_polar(cls, radius: float, angle: float) -> Point:
... """Create a point from its polar coordinates."""
... return Point(radius * math.cos(angle), radius * math.sin(angle))
```


Expand All @@ -42,33 +41,33 @@ class Point:
A simple way to _statically_ set one of the `classmethod`s as initializer is to register a structuring hook that holds a reference to the respective callable:

```{doctest}
from inspect import signature
from typing import Callable, TypedDict
from cattrs import Converter
from cattrs.dispatch import StructureHook
def signature_to_typed_dict(fn: Callable) -> type[TypedDict]:
"""Create a TypedDict reflecting a callable's signature."""
params = {p: t.annotation for p, t in signature(fn).parameters.items()}
return TypedDict(f"{fn.__name__}_args", params)
def make_initializer_from(fn: Callable, conv: Converter) -> StructureHook:
"""Return a structuring hook from a given callable."""
td = signature_to_typed_dict(fn)
td_hook = conv.get_structure_hook(td)
return lambda v, _: fn(**td_hook(v, td))
>>> from inspect import signature
>>> from typing import Callable, TypedDict
>>> from cattrs import Converter
>>> from cattrs.dispatch import StructureHook
>>> def signature_to_typed_dict(fn: Callable) -> type[TypedDict]:
... """Create a TypedDict reflecting a callable's signature."""
... params = {p: t.annotation for p, t in signature(fn).parameters.items()}
... return TypedDict(f"{fn.__name__}_args", params)
>>> def make_initializer_from(fn: Callable, conv: Converter) -> StructureHook:
... """Return a structuring hook from a given callable."""
... td = signature_to_typed_dict(fn)
... td_hook = conv.get_structure_hook(td)
... return lambda v, _: fn(**td_hook(v, td))
```

Now, you can easily structure `Point`s from the specified alternative representation:

```{doctest}
c = Converter()
c.register_structure_hook(Point, make_initializer_from(Point.from_polar, c))
>>> c = Converter()
>>> c.register_structure_hook(Point, make_initializer_from(Point.from_polar, c))
p0 = Point(1.0, 0.0)
p1 = c.structure({"radius": 1.0, "angle": 0.0}, Point)
assert p0 == p1
>>> p0 = Point(1.0, 0.0)
>>> p1 = c.structure({"radius": 1.0, "angle": 0.0}, Point)
>>> assert p0 == p1
```


Expand All @@ -80,49 +79,49 @@ A typical scenario would be when object structuring happens behind an API and yo
In such situations, the following hook factory can help you achieve your goal:

```{doctest}
from inspect import signature
from typing import Callable, TypedDict
from cattrs import Converter
from cattrs.dispatch import StructureHook
def signature_to_typed_dict(fn: Callable) -> type[TypedDict]:
"""Create a TypedDict reflecting a callable's signature."""
params = {p: t.annotation for p, t in signature(fn).parameters.items()}
return TypedDict(f"{fn.__name__}_args", params)
def make_initializer_selection_hook(
initializer_key: str,
converter: Converter,
) -> StructureHook:
"""Return a structuring hook that dynamically switches between initializers."""
def select_initializer_hook(specs: dict, cls: type[T]) -> T:
"""Deserialization with dynamic initializer selection."""
# If no initializer keyword is specified, use regular __init__
if initializer_key not in specs:
return converter.structure_attrs_fromdict(specs, cls)
# Otherwise, call the specified initializer with deserialized arguments
specs = specs.copy()
initializer_name = specs.pop(initializer_key)
initializer = getattr(cls, initializer_name)
td = signature_to_typed_dict(initializer)
td_hook = converter.get_structure_hook(td)
return initializer(**td_hook(specs, td))
return select_initializer_hook
>>> from inspect import signature
>>> from typing import Callable, TypedDict
>>> from cattrs import Converter
>>> from cattrs.dispatch import StructureHook
>>> def signature_to_typed_dict(fn: Callable) -> type[TypedDict]:
... """Create a TypedDict reflecting a callable's signature."""
... params = {p: t.annotation for p, t in signature(fn).parameters.items()}
... return TypedDict(f"{fn.__name__}_args", params)
>>> def make_initializer_selection_hook(
... initializer_key: str,
... converter: Converter,
... ) -> StructureHook:
... """Return a structuring hook that dynamically switches between initializers."""
...
... def select_initializer_hook(specs: dict, cls: type[T]) -> T:
... """Deserialization with dynamic initializer selection."""
...
... # If no initializer keyword is specified, use regular __init__
... if initializer_key not in specs:
... return converter.structure_attrs_fromdict(specs, cls)
...
... # Otherwise, call the specified initializer with deserialized arguments
... specs = specs.copy()
... initializer_name = specs.pop(initializer_key)
... initializer = getattr(cls, initializer_name)
... td = signature_to_typed_dict(initializer)
... td_hook = converter.get_structure_hook(td)
... return initializer(**td_hook(specs, td))
...
... return select_initializer_hook
```

Specifying the key that determines the initializer to be used now lets you dynamically select the `classmethod` as part of the object specification itself:

```{doctest}
c = Converter()
c.register_structure_hook(Point, make_initializer_selection_hook("initializer", c))
>>> c = Converter()
>>> c.register_structure_hook(Point, make_initializer_selection_hook("initializer", c))
p0 = Point(1.0, 0.0)
p1 = c.structure({"initializer": "from_polar", "radius": 1.0, "angle": 0.0}, Point)
p2 = c.structure({"initializer": "from_tuple", "coordinates": (1.0, 0.0)}, Point)
assert p0 == p1 == p2
>>> p0 = Point(1.0, 0.0)
>>> p1 = c.structure({"initializer": "from_polar", "radius": 1.0, "angle": 0.0}, Point)
>>> p2 = c.structure({"initializer": "from_tuple", "coordinates": (1.0, 0.0)}, Point)
>>> assert p0 == p1 == p2
```
52 changes: 28 additions & 24 deletions pdm.lock

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

4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ skip-magic-trailing-comma = true

[tool.pdm.dev-dependencies]
lint = [
"black>=23.3.0",
"black>=24.2.0",
"ruff>=0.0.277",
]
test = [
Expand All @@ -17,7 +17,7 @@ test = [
]
docs = [
"sphinx>=5.3.0",
"furo>=2023.3.27",
"furo>=2024.1.29",
"sphinx-copybutton>=0.5.2",
"myst-parser>=1.0.0",
"pendulum>=2.1.2",
Expand Down
1 change: 1 addition & 0 deletions src/cattr/preconf/bson.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Preconfigured converters for bson."""

from cattrs.preconf.bson import BsonConverter, configure_converter, make_converter

__all__ = ["BsonConverter", "configure_converter", "make_converter"]
1 change: 1 addition & 0 deletions src/cattr/preconf/json.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Preconfigured converters for the stdlib json."""

from cattrs.preconf.json import JsonConverter, configure_converter, make_converter

__all__ = ["configure_converter", "JsonConverter", "make_converter"]
1 change: 1 addition & 0 deletions src/cattr/preconf/msgpack.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Preconfigured converters for msgpack."""

from cattrs.preconf.msgpack import MsgpackConverter, configure_converter, make_converter

__all__ = ["configure_converter", "make_converter", "MsgpackConverter"]
1 change: 1 addition & 0 deletions src/cattr/preconf/orjson.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Preconfigured converters for orjson."""

from cattrs.preconf.orjson import OrjsonConverter, configure_converter, make_converter

__all__ = ["configure_converter", "make_converter", "OrjsonConverter"]
1 change: 1 addition & 0 deletions src/cattr/preconf/pyyaml.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Preconfigured converters for pyyaml."""

from cattrs.preconf.pyyaml import PyyamlConverter, configure_converter, make_converter

__all__ = ["configure_converter", "make_converter", "PyyamlConverter"]
1 change: 1 addition & 0 deletions src/cattr/preconf/tomlkit.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Preconfigured converters for tomlkit."""

from cattrs.preconf.tomlkit import TomlkitConverter, configure_converter, make_converter

__all__ = ["configure_converter", "make_converter", "TomlkitConverter"]
1 change: 1 addition & 0 deletions src/cattr/preconf/ujson.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Preconfigured converters for ujson."""

from cattrs.preconf.ujson import UjsonConverter, configure_converter, make_converter

__all__ = ["configure_converter", "make_converter", "UjsonConverter"]
14 changes: 8 additions & 6 deletions src/cattrs/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,12 +151,14 @@ def adapted_fields(cl) -> List[Attribute]:
return [
Attribute(
attr.name,
attr.default
if attr.default is not MISSING
else (
Factory(attr.default_factory)
if attr.default_factory is not MISSING
else NOTHING
(
attr.default
if attr.default is not MISSING
else (
Factory(attr.default_factory)
if attr.default_factory is not MISSING
else NOTHING
)
),
None,
True,
Expand Down
8 changes: 5 additions & 3 deletions src/cattrs/_generics.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ def deep_copy_with(t, mapping: Mapping[str, Any]):
args = (args[0],)
new_args = (
tuple(
mapping[a.__name__]
if hasattr(a, "__name__") and a.__name__ in mapping
else (deep_copy_with(a, mapping) if is_generic(a) else a)
(
mapping[a.__name__]
if hasattr(a, "__name__") and a.__name__ in mapping
else (deep_copy_with(a, mapping) if is_generic(a) else a)
)
for a in args
)
+ rest
Expand Down

0 comments on commit b3c6ba7

Please sign in to comment.