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
19 changes: 12 additions & 7 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@
- **Potentially breaking**: skip _attrs_ fields marked as `init=False` by default. This change is potentially breaking for unstructuring.
See [here](https://catt.rs/en/latest/customizing.html#include_init_false) for instructions on how to restore the old behavior.
([#40](https://github.com/python-attrs/cattrs/issues/40) [#395](https://github.com/python-attrs/cattrs/pull/395))
- The `omit` parameter of `cattrs.override()` is now of type `bool | None` (from `bool`). `None` is the new default and means to apply default `cattrs` handling to the attribute.
- Fix `format_exception` parameter working for recursive calls to `transform_error`
([#389](https://github.com/python-attrs/cattrs/issues/389)
- **Potentially breaking**: {py:func}`cattrs.gen.make_dict_structure_fn` and {py:func}`cattrs.gen.typeddicts.make_dict_structure_fn` will use the values for the `detailed_validation` and `forbid_extra_keys` parameters from the given converter by default now.
If you're using these functions directly, the old behavior can be restored by passing in the desired values directly.
([#410](https://github.com/python-attrs/cattrs/issues/410) [#411](https://github.com/python-attrs/cattrs/pull/411))
- The `omit` parameter of {py:func}`cattrs.override` is now of type `bool | None` (from `bool`).
`None` is the new default and means to apply default _cattrs_ handling to the attribute, which is to omit the attribute if it's marked as `init=False`, and keep it otherwise.
- Fix {py:func}`format_exception() <cattrs.v.format_exception>` parameter working for recursive calls to {py:func}`transform_error <cattrs.transform_error>`.
([#389](https://github.com/python-attrs/cattrs/issues/389))
- [_attrs_ aliases](https://www.attrs.org/en/stable/init.html#private-attributes-and-aliases) are now supported, although aliased fields still map to their attribute name instead of their alias by default when un/structuring.
([#322](https://github.com/python-attrs/cattrs/issues/322) [#391](https://github.com/python-attrs/cattrs/pull/391))
- Use [PDM](https://pdm.fming.dev/latest/) instead of Poetry.
Expand All @@ -16,19 +20,20 @@
([#376](https://github.com/python-attrs/cattrs/issues/376) [#377](https://github.com/python-attrs/cattrs/pull/377))
- Optimize and improve unstructuring of `Optional` (unions of one type and `None`).
([#380](https://github.com/python-attrs/cattrs/issues/380) [#381](https://github.com/python-attrs/cattrs/pull/381))
- Fix `format_exception` and `transform_error` type annotations.
- Fix {py:func}`format_exception <cattrs.v.format_exception>` and {py:func}`transform_error <cattrs.transform_error>` type annotations.
- Improve the implementation of `cattrs._compat.is_typeddict`. The implementation is now simpler, and relies on fewer private implementation details from `typing` and typing_extensions.
([#384](https://github.com/python-attrs/cattrs/pull/384))
- Improve handling of TypedDicts with forward references.
- Speed up generated _attrs_ and TypedDict structuring functions by changing their signature slightly.
([#388](https://github.com/python-attrs/cattrs/pull/388))
- Fix copying of converters using function hooks.
- Fix copying of converters with function hooks.
([#398](https://github.com/python-attrs/cattrs/issues/398) [#399](https://github.com/python-attrs/cattrs/pull/399))
- Broaden loads' type definition for the preconf orjson converter.
- Broaden {py:func}`loads' <cattrs.preconf.orjson.OrjsonConverter.loads>` type definition for the preconf orjson converter.
([#400](https://github.com/python-attrs/cattrs/pull/400))
- `AttributeValidationNote` and `IterableValidationNote` are now picklable.
- {py:class}`AttributeValidationNote <cattrs.AttributeValidationNote>` and {py:class}`IterableValidationNote <cattrs.IterableValidationNote>` are now picklable.
([#408](https://github.com/python-attrs/cattrs/pull/408))


## 23.1.2 (2023-06-02)

- Improve `typing_extensions` version bound. ([#372](https://github.com/python-attrs/cattrs/issues/372))
Expand Down
5 changes: 5 additions & 0 deletions docs/customizing.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ TestClass(number=1)

This behavior can only be applied to classes or to the default for the {class}`Converter <cattrs.Converter>`, and has no effect when generating unstructuring functions.

```{versionchanged} 23.2.0
The value for the `make_dict_structure_fn._cattrs_forbid_extra_keys` parameter is now taken from the given converter by default.
```


### `rename`

Using the rename override makes `cattrs` simply use the provided name instead
Expand Down
19 changes: 17 additions & 2 deletions src/cattrs/gen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
from ._shared import find_structure_handler

if TYPE_CHECKING: # pragma: no cover
from typing_extensions import Literal

from cattr.converters import BaseConverter


Expand Down Expand Up @@ -233,10 +235,10 @@ def make_dict_unstructure_fn(
def make_dict_structure_fn(
cl: type[T],
converter: BaseConverter,
_cattrs_forbid_extra_keys: bool = False,
_cattrs_forbid_extra_keys: bool | Literal["from_converter"] = "from_converter",
_cattrs_use_linecache: bool = True,
_cattrs_prefer_attrib_converters: bool = False,
_cattrs_detailed_validation: bool = True,
_cattrs_detailed_validation: bool | Literal["from_converter"] = "from_converter",
_cattrs_use_alias: bool = False,
_cattrs_include_init_false: bool = False,
**kwargs: AttributeOverride,
Expand All @@ -245,13 +247,20 @@ def make_dict_structure_fn(
Generate a specialized dict structuring function for an attrs class or
dataclass.

:param _cattrs_forbid_extra_keys: Whether the structuring function should raise a
`ForbiddenExtraKeysError` if unknown keys are encountered.
:param _cattrs_detailed_validation: Whether to use a slower mode that produces
more detailed errors.
:param _cattrs_use_alias: If true, the attribute alias will be used as the
dictionary key by default.
:param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False`
will be included.

.. versionadded:: 23.2.0 *_cattrs_use_alias*
.. versionadded:: 23.2.0 *_cattrs_include_init_false*
.. versionchanged:: 23.2.0
The `_cattrs_forbid_extra_keys` and `_cattrs_detailed_validation` parameters
take their values from the given converter by default.
"""

mapping = {}
Expand Down Expand Up @@ -305,6 +314,12 @@ def make_dict_structure_fn(
resolve_types(cl)

allowed_fields = set()
if _cattrs_forbid_extra_keys == "from_converter":
# BaseConverter doesn't have it so we're careful.
_cattrs_forbid_extra_keys = getattr(converter, "forbid_extra_keys", False)
if _cattrs_detailed_validation == "from_converter":
_cattrs_detailed_validation = converter.detailed_validation

if _cattrs_forbid_extra_keys:
globs["__c_a"] = allowed_fields
globs["__c_feke"] = ForbiddenExtraKeysError
Expand Down
18 changes: 15 additions & 3 deletions src/cattrs/gen/typeddicts.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import sys
from typing import TYPE_CHECKING, Any, Callable, TypeVar

from attr import NOTHING, Attribute
from attrs import NOTHING, Attribute

try:
from inspect import get_annotations
Expand Down Expand Up @@ -51,6 +51,8 @@ def get_annots(cl) -> dict[str, Any]:
from ._shared import find_structure_handler

if TYPE_CHECKING: # pragma: no cover
from typing_extensions import Literal

from cattr.converters import BaseConverter

__all__ = ["make_dict_unstructure_fn", "make_dict_structure_fn"]
Expand Down Expand Up @@ -242,9 +244,9 @@ def make_dict_unstructure_fn(
def make_dict_structure_fn(
cl: Any,
converter: BaseConverter,
_cattrs_forbid_extra_keys: bool = False,
_cattrs_forbid_extra_keys: bool | Literal["from_converter"] = "from_converter",
_cattrs_use_linecache: bool = True,
_cattrs_detailed_validation: bool = True,
_cattrs_detailed_validation: bool | Literal["from_converter"] = "from_converter",
**kwargs: AttributeOverride,
) -> Callable[[dict, Any], Any]:
"""Generate a specialized dict structuring function for typed dicts.
Expand All @@ -259,6 +261,10 @@ def make_dict_structure_fn(
`ForbiddenExtraKeysError` if unknown keys are encountered.
:param _cattrs_detailed_validation: Whether to store the generated code in the
_linecache_, for easier debugging and better stack traces.

.. versionchanged:: 23.2.0
The `_cattrs_forbid_extra_keys` and `_cattrs_detailed_validation` parameters
take their values from the given converter by default.
"""

mapping = {}
Expand Down Expand Up @@ -307,6 +313,12 @@ def make_dict_structure_fn(
req_keys = _required_keys(cl)

allowed_fields = set()
if _cattrs_forbid_extra_keys == "from_converter":
# BaseConverter doesn't have it so we're careful.
_cattrs_forbid_extra_keys = getattr(converter, "forbid_extra_keys", False)
if _cattrs_detailed_validation == "from_converter":
_cattrs_detailed_validation = converter.detailed_validation

if _cattrs_forbid_extra_keys:
globs["__c_a"] = allowed_fields
globs["__c_feke"] = ForbiddenExtraKeysError
Expand Down
61 changes: 61 additions & 0 deletions tests/test_gen_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -534,3 +534,64 @@ class A:
assert structured.b == 2
assert structured._c == 3
assert structured.d == -4


@given(forbid_extra_keys=..., detailed_validation=...)
def test_forbid_extra_keys_from_converter(
forbid_extra_keys: bool, detailed_validation: bool
):
"""
`forbid_extra_keys` is taken from the converter by default.
"""
c = Converter(
forbid_extra_keys=forbid_extra_keys, detailed_validation=detailed_validation
)

@define
class A:
a: int

c.register_structure_hook(A, make_dict_structure_fn(A, c))

if forbid_extra_keys:
with pytest.raises((ForbiddenExtraKeysError, ClassValidationError)):
c.structure({"a": 1, "b": 2}, A)
else:
c.structure({"a": 1, "b": 2}, A)


@given(detailed_validation=...)
def test_forbid_extra_keys_from_baseconverter(detailed_validation: bool):
"""
`forbid_extra_keys` is taken from the converter by default.

BaseConverter should default to False.
"""
c = BaseConverter(detailed_validation=detailed_validation)

@define
class A:
a: int

c.register_structure_hook(A, make_dict_structure_fn(A, c))

c.structure({"a": 1, "b": 2}, A)


def test_detailed_validation_from_converter(converter: BaseConverter):
"""
`detailed_validation` is taken from the converter by default.
"""

@define
class A:
a: int

converter.register_structure_hook(A, make_dict_structure_fn(A, converter))

if converter.detailed_validation:
with pytest.raises(ClassValidationError):
converter.structure({"a": "a"}, A)
else:
with pytest.raises(ValueError):
converter.structure({"a": "a"}, A)
60 changes: 59 additions & 1 deletion tests/test_typeddicts.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from hypothesis.strategies import booleans
from pytest import raises

from cattrs import Converter
from cattrs import BaseConverter, Converter
from cattrs._compat import ExtensionsTypedDict, is_generic
from cattrs.errors import ClassValidationError, ForbiddenExtraKeysError
from cattrs.gen import already_generating, override
Expand Down Expand Up @@ -357,3 +357,61 @@ class A(ExtensionsTypedDict):
genconverter.register_unstructure_hook(int, lambda v: v + 1)

assert genconverter.unstructure({"a": 1}, A) == {"a": 2}


@given(forbid_extra_keys=..., detailed_validation=...)
def test_forbid_extra_keys_from_converter(
forbid_extra_keys: bool, detailed_validation: bool
):
"""
`forbid_extra_keys` is taken from the converter by default.
"""
c = Converter(
forbid_extra_keys=forbid_extra_keys, detailed_validation=detailed_validation
)

class A(ExtensionsTypedDict):
a: int

c.register_structure_hook(A, make_dict_structure_fn(A, c))

if forbid_extra_keys:
with pytest.raises((ForbiddenExtraKeysError, ClassValidationError)):
c.structure({"a": 1, "b": 2}, A)
else:
c.structure({"a": 1, "b": 2}, A)


@given(detailed_validation=...)
def test_forbid_extra_keys_from_baseconverter(detailed_validation: bool):
"""
`forbid_extra_keys` is taken from the converter by default.

BaseConverter should default to False.
"""
c = BaseConverter(detailed_validation=detailed_validation)

class A(ExtensionsTypedDict):
a: int

c.register_structure_hook(A, make_dict_structure_fn(A, c))

c.structure({"a": 1, "b": 2}, A)


def test_detailed_validation_from_converter(converter: BaseConverter):
"""
`detailed_validation` is taken from the converter by default.
"""

class A(ExtensionsTypedDict):
a: int

converter.register_structure_hook(A, make_dict_structure_fn(A, converter))

if converter.detailed_validation:
with pytest.raises(ClassValidationError):
converter.structure({"a": "a"}, A)
else:
with pytest.raises(ValueError):
converter.structure({"a": "a"}, A)