From 9a4c33c65976fdbf68b3a3d57d2833f7f602a064 Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Wed, 9 Aug 2023 14:09:07 +0200 Subject: [PATCH 1/2] Change gen defaults --- HISTORY.md | 2 ++ docs/customizing.md | 5 +++ src/cattrs/gen/__init__.py | 19 +++++++++-- src/cattrs/gen/typeddicts.py | 18 +++++++++-- tests/test_gen_dict.py | 61 ++++++++++++++++++++++++++++++++++++ tests/test_typeddicts.py | 60 ++++++++++++++++++++++++++++++++++- 6 files changed, 159 insertions(+), 6 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 1551c06b..b91ec999 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -28,6 +28,8 @@ ([#400](https://github.com/python-attrs/cattrs/pull/400)) - `AttributeValidationNote` and `IterableValidationNote` are now picklable. ([#408](https://github.com/python-attrs/cattrs/pull/408)) +- `cattrs.gen.make_dict_structure_fn` and `cattrs.gen.typeddict.make_dict_structure_fn` will use the values for `detailed_validation` and `forbid_extra_keys` parameters from the given converter by default now. + ([#410](https://github.com/python-attrs/cattrs/issues/410)) ## 23.1.2 (2023-06-02) diff --git a/docs/customizing.md b/docs/customizing.md index 524c8e0b..894d4131 100644 --- a/docs/customizing.md +++ b/docs/customizing.md @@ -102,6 +102,11 @@ TestClass(number=1) This behavior can only be applied to classes or to the default for the {class}`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 diff --git a/src/cattrs/gen/__init__.py b/src/cattrs/gen/__init__.py index aebf3b8f..3cf0ced2 100644 --- a/src/cattrs/gen/__init__.py +++ b/src/cattrs/gen/__init__.py @@ -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 @@ -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, @@ -245,6 +247,10 @@ 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` @@ -252,6 +258,9 @@ def make_dict_structure_fn( .. 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 = {} @@ -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 diff --git a/src/cattrs/gen/typeddicts.py b/src/cattrs/gen/typeddicts.py index cbbc4bdc..dd2f5c85 100644 --- a/src/cattrs/gen/typeddicts.py +++ b/src/cattrs/gen/typeddicts.py @@ -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 @@ -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"] @@ -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. @@ -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 = {} @@ -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 diff --git a/tests/test_gen_dict.py b/tests/test_gen_dict.py index e5a12f93..0a4e8512 100644 --- a/tests/test_gen_dict.py +++ b/tests/test_gen_dict.py @@ -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) diff --git a/tests/test_typeddicts.py b/tests/test_typeddicts.py index 2d7c428f..f805945b 100644 --- a/tests/test_typeddicts.py +++ b/tests/test_typeddicts.py @@ -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 @@ -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) From 9bcfde23f33a94e764cdfad0ccc269b17271bcb2 Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Thu, 10 Aug 2023 01:47:35 +0200 Subject: [PATCH 2/2] Update changelog --- HISTORY.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index b91ec999..590ad5b3 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -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() ` parameter working for recursive calls to {py:func}`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. @@ -16,20 +20,19 @@ ([#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 ` and {py:func}`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' ` 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 ` and {py:class}`IterableValidationNote ` are now picklable. ([#408](https://github.com/python-attrs/cattrs/pull/408)) -- `cattrs.gen.make_dict_structure_fn` and `cattrs.gen.typeddict.make_dict_structure_fn` will use the values for `detailed_validation` and `forbid_extra_keys` parameters from the given converter by default now. - ([#410](https://github.com/python-attrs/cattrs/issues/410)) + ## 23.1.2 (2023-06-02)