Skip to content

Commit c8aeafc

Browse files
authored
Change gen defaults (#411)
* Change gen defaults * Update changelog
1 parent 992b137 commit c8aeafc

6 files changed

Lines changed: 169 additions & 13 deletions

File tree

HISTORY.md

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@
55
- **Potentially breaking**: skip _attrs_ fields marked as `init=False` by default. This change is potentially breaking for unstructuring.
66
See [here](https://catt.rs/en/latest/customizing.html#include_init_false) for instructions on how to restore the old behavior.
77
([#40](https://github.com/python-attrs/cattrs/issues/40) [#395](https://github.com/python-attrs/cattrs/pull/395))
8-
- 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.
9-
- Fix `format_exception` parameter working for recursive calls to `transform_error`
10-
([#389](https://github.com/python-attrs/cattrs/issues/389)
8+
- **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.
9+
If you're using these functions directly, the old behavior can be restored by passing in the desired values directly.
10+
([#410](https://github.com/python-attrs/cattrs/issues/410) [#411](https://github.com/python-attrs/cattrs/pull/411))
11+
- The `omit` parameter of {py:func}`cattrs.override` is now of type `bool | None` (from `bool`).
12+
`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.
13+
- Fix {py:func}`format_exception() <cattrs.v.format_exception>` parameter working for recursive calls to {py:func}`transform_error <cattrs.transform_error>`.
14+
([#389](https://github.com/python-attrs/cattrs/issues/389))
1115
- [_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.
1216
([#322](https://github.com/python-attrs/cattrs/issues/322) [#391](https://github.com/python-attrs/cattrs/pull/391))
1317
- Use [PDM](https://pdm.fming.dev/latest/) instead of Poetry.
@@ -16,19 +20,20 @@
1620
([#376](https://github.com/python-attrs/cattrs/issues/376) [#377](https://github.com/python-attrs/cattrs/pull/377))
1721
- Optimize and improve unstructuring of `Optional` (unions of one type and `None`).
1822
([#380](https://github.com/python-attrs/cattrs/issues/380) [#381](https://github.com/python-attrs/cattrs/pull/381))
19-
- Fix `format_exception` and `transform_error` type annotations.
23+
- Fix {py:func}`format_exception <cattrs.v.format_exception>` and {py:func}`transform_error <cattrs.transform_error>` type annotations.
2024
- 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.
2125
([#384](https://github.com/python-attrs/cattrs/pull/384))
2226
- Improve handling of TypedDicts with forward references.
2327
- Speed up generated _attrs_ and TypedDict structuring functions by changing their signature slightly.
2428
([#388](https://github.com/python-attrs/cattrs/pull/388))
25-
- Fix copying of converters using function hooks.
29+
- Fix copying of converters with function hooks.
2630
([#398](https://github.com/python-attrs/cattrs/issues/398) [#399](https://github.com/python-attrs/cattrs/pull/399))
27-
- Broaden loads' type definition for the preconf orjson converter.
31+
- Broaden {py:func}`loads' <cattrs.preconf.orjson.OrjsonConverter.loads>` type definition for the preconf orjson converter.
2832
([#400](https://github.com/python-attrs/cattrs/pull/400))
29-
- `AttributeValidationNote` and `IterableValidationNote` are now picklable.
33+
- {py:class}`AttributeValidationNote <cattrs.AttributeValidationNote>` and {py:class}`IterableValidationNote <cattrs.IterableValidationNote>` are now picklable.
3034
([#408](https://github.com/python-attrs/cattrs/pull/408))
3135

36+
3237
## 23.1.2 (2023-06-02)
3338

3439
- Improve `typing_extensions` version bound. ([#372](https://github.com/python-attrs/cattrs/issues/372))

docs/customizing.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,11 @@ TestClass(number=1)
102102

103103
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.
104104

105+
```{versionchanged} 23.2.0
106+
The value for the `make_dict_structure_fn._cattrs_forbid_extra_keys` parameter is now taken from the given converter by default.
107+
```
108+
109+
105110
### `rename`
106111

107112
Using the rename override makes `cattrs` simply use the provided name instead

src/cattrs/gen/__init__.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
from ._shared import find_structure_handler
3131

3232
if TYPE_CHECKING: # pragma: no cover
33+
from typing_extensions import Literal
34+
3335
from cattr.converters import BaseConverter
3436

3537

@@ -233,10 +235,10 @@ def make_dict_unstructure_fn(
233235
def make_dict_structure_fn(
234236
cl: type[T],
235237
converter: BaseConverter,
236-
_cattrs_forbid_extra_keys: bool = False,
238+
_cattrs_forbid_extra_keys: bool | Literal["from_converter"] = "from_converter",
237239
_cattrs_use_linecache: bool = True,
238240
_cattrs_prefer_attrib_converters: bool = False,
239-
_cattrs_detailed_validation: bool = True,
241+
_cattrs_detailed_validation: bool | Literal["from_converter"] = "from_converter",
240242
_cattrs_use_alias: bool = False,
241243
_cattrs_include_init_false: bool = False,
242244
**kwargs: AttributeOverride,
@@ -245,13 +247,20 @@ def make_dict_structure_fn(
245247
Generate a specialized dict structuring function for an attrs class or
246248
dataclass.
247249
250+
:param _cattrs_forbid_extra_keys: Whether the structuring function should raise a
251+
`ForbiddenExtraKeysError` if unknown keys are encountered.
252+
:param _cattrs_detailed_validation: Whether to use a slower mode that produces
253+
more detailed errors.
248254
:param _cattrs_use_alias: If true, the attribute alias will be used as the
249255
dictionary key by default.
250256
:param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False`
251257
will be included.
252258
253259
.. versionadded:: 23.2.0 *_cattrs_use_alias*
254260
.. versionadded:: 23.2.0 *_cattrs_include_init_false*
261+
.. versionchanged:: 23.2.0
262+
The `_cattrs_forbid_extra_keys` and `_cattrs_detailed_validation` parameters
263+
take their values from the given converter by default.
255264
"""
256265

257266
mapping = {}
@@ -305,6 +314,12 @@ def make_dict_structure_fn(
305314
resolve_types(cl)
306315

307316
allowed_fields = set()
317+
if _cattrs_forbid_extra_keys == "from_converter":
318+
# BaseConverter doesn't have it so we're careful.
319+
_cattrs_forbid_extra_keys = getattr(converter, "forbid_extra_keys", False)
320+
if _cattrs_detailed_validation == "from_converter":
321+
_cattrs_detailed_validation = converter.detailed_validation
322+
308323
if _cattrs_forbid_extra_keys:
309324
globs["__c_a"] = allowed_fields
310325
globs["__c_feke"] = ForbiddenExtraKeysError

src/cattrs/gen/typeddicts.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import sys
66
from typing import TYPE_CHECKING, Any, Callable, TypeVar
77

8-
from attr import NOTHING, Attribute
8+
from attrs import NOTHING, Attribute
99

1010
try:
1111
from inspect import get_annotations
@@ -51,6 +51,8 @@ def get_annots(cl) -> dict[str, Any]:
5151
from ._shared import find_structure_handler
5252

5353
if TYPE_CHECKING: # pragma: no cover
54+
from typing_extensions import Literal
55+
5456
from cattr.converters import BaseConverter
5557

5658
__all__ = ["make_dict_unstructure_fn", "make_dict_structure_fn"]
@@ -242,9 +244,9 @@ def make_dict_unstructure_fn(
242244
def make_dict_structure_fn(
243245
cl: Any,
244246
converter: BaseConverter,
245-
_cattrs_forbid_extra_keys: bool = False,
247+
_cattrs_forbid_extra_keys: bool | Literal["from_converter"] = "from_converter",
246248
_cattrs_use_linecache: bool = True,
247-
_cattrs_detailed_validation: bool = True,
249+
_cattrs_detailed_validation: bool | Literal["from_converter"] = "from_converter",
248250
**kwargs: AttributeOverride,
249251
) -> Callable[[dict, Any], Any]:
250252
"""Generate a specialized dict structuring function for typed dicts.
@@ -259,6 +261,10 @@ def make_dict_structure_fn(
259261
`ForbiddenExtraKeysError` if unknown keys are encountered.
260262
:param _cattrs_detailed_validation: Whether to store the generated code in the
261263
_linecache_, for easier debugging and better stack traces.
264+
265+
.. versionchanged:: 23.2.0
266+
The `_cattrs_forbid_extra_keys` and `_cattrs_detailed_validation` parameters
267+
take their values from the given converter by default.
262268
"""
263269

264270
mapping = {}
@@ -307,6 +313,12 @@ def make_dict_structure_fn(
307313
req_keys = _required_keys(cl)
308314

309315
allowed_fields = set()
316+
if _cattrs_forbid_extra_keys == "from_converter":
317+
# BaseConverter doesn't have it so we're careful.
318+
_cattrs_forbid_extra_keys = getattr(converter, "forbid_extra_keys", False)
319+
if _cattrs_detailed_validation == "from_converter":
320+
_cattrs_detailed_validation = converter.detailed_validation
321+
310322
if _cattrs_forbid_extra_keys:
311323
globs["__c_a"] = allowed_fields
312324
globs["__c_feke"] = ForbiddenExtraKeysError

tests/test_gen_dict.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,3 +534,64 @@ class A:
534534
assert structured.b == 2
535535
assert structured._c == 3
536536
assert structured.d == -4
537+
538+
539+
@given(forbid_extra_keys=..., detailed_validation=...)
540+
def test_forbid_extra_keys_from_converter(
541+
forbid_extra_keys: bool, detailed_validation: bool
542+
):
543+
"""
544+
`forbid_extra_keys` is taken from the converter by default.
545+
"""
546+
c = Converter(
547+
forbid_extra_keys=forbid_extra_keys, detailed_validation=detailed_validation
548+
)
549+
550+
@define
551+
class A:
552+
a: int
553+
554+
c.register_structure_hook(A, make_dict_structure_fn(A, c))
555+
556+
if forbid_extra_keys:
557+
with pytest.raises((ForbiddenExtraKeysError, ClassValidationError)):
558+
c.structure({"a": 1, "b": 2}, A)
559+
else:
560+
c.structure({"a": 1, "b": 2}, A)
561+
562+
563+
@given(detailed_validation=...)
564+
def test_forbid_extra_keys_from_baseconverter(detailed_validation: bool):
565+
"""
566+
`forbid_extra_keys` is taken from the converter by default.
567+
568+
BaseConverter should default to False.
569+
"""
570+
c = BaseConverter(detailed_validation=detailed_validation)
571+
572+
@define
573+
class A:
574+
a: int
575+
576+
c.register_structure_hook(A, make_dict_structure_fn(A, c))
577+
578+
c.structure({"a": 1, "b": 2}, A)
579+
580+
581+
def test_detailed_validation_from_converter(converter: BaseConverter):
582+
"""
583+
`detailed_validation` is taken from the converter by default.
584+
"""
585+
586+
@define
587+
class A:
588+
a: int
589+
590+
converter.register_structure_hook(A, make_dict_structure_fn(A, converter))
591+
592+
if converter.detailed_validation:
593+
with pytest.raises(ClassValidationError):
594+
converter.structure({"a": "a"}, A)
595+
else:
596+
with pytest.raises(ValueError):
597+
converter.structure({"a": "a"}, A)

tests/test_typeddicts.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from hypothesis.strategies import booleans
88
from pytest import raises
99

10-
from cattrs import Converter
10+
from cattrs import BaseConverter, Converter
1111
from cattrs._compat import ExtensionsTypedDict, is_generic
1212
from cattrs.errors import ClassValidationError, ForbiddenExtraKeysError
1313
from cattrs.gen import already_generating, override
@@ -357,3 +357,61 @@ class A(ExtensionsTypedDict):
357357
genconverter.register_unstructure_hook(int, lambda v: v + 1)
358358

359359
assert genconverter.unstructure({"a": 1}, A) == {"a": 2}
360+
361+
362+
@given(forbid_extra_keys=..., detailed_validation=...)
363+
def test_forbid_extra_keys_from_converter(
364+
forbid_extra_keys: bool, detailed_validation: bool
365+
):
366+
"""
367+
`forbid_extra_keys` is taken from the converter by default.
368+
"""
369+
c = Converter(
370+
forbid_extra_keys=forbid_extra_keys, detailed_validation=detailed_validation
371+
)
372+
373+
class A(ExtensionsTypedDict):
374+
a: int
375+
376+
c.register_structure_hook(A, make_dict_structure_fn(A, c))
377+
378+
if forbid_extra_keys:
379+
with pytest.raises((ForbiddenExtraKeysError, ClassValidationError)):
380+
c.structure({"a": 1, "b": 2}, A)
381+
else:
382+
c.structure({"a": 1, "b": 2}, A)
383+
384+
385+
@given(detailed_validation=...)
386+
def test_forbid_extra_keys_from_baseconverter(detailed_validation: bool):
387+
"""
388+
`forbid_extra_keys` is taken from the converter by default.
389+
390+
BaseConverter should default to False.
391+
"""
392+
c = BaseConverter(detailed_validation=detailed_validation)
393+
394+
class A(ExtensionsTypedDict):
395+
a: int
396+
397+
c.register_structure_hook(A, make_dict_structure_fn(A, c))
398+
399+
c.structure({"a": 1, "b": 2}, A)
400+
401+
402+
def test_detailed_validation_from_converter(converter: BaseConverter):
403+
"""
404+
`detailed_validation` is taken from the converter by default.
405+
"""
406+
407+
class A(ExtensionsTypedDict):
408+
a: int
409+
410+
converter.register_structure_hook(A, make_dict_structure_fn(A, converter))
411+
412+
if converter.detailed_validation:
413+
with pytest.raises(ClassValidationError):
414+
converter.structure({"a": "a"}, A)
415+
else:
416+
with pytest.raises(ValueError):
417+
converter.structure({"a": "a"}, A)

0 commit comments

Comments
 (0)