diff --git a/HISTORY.md b/HISTORY.md index 2bc9ab0a..4d880500 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -28,6 +28,10 @@ Our backwards-compatibility policy can be found [here](https://github.com/python - For {class}`cattrs.errors.StructureHandlerNotFoundError` and {class}`cattrs.errors.ForbiddenExtraKeysError` correctly set {attr}`BaseException.args` in `super()` and hence make them pickable. ([#666](https://github.com/python-attrs/cattrs/pull/666)) +- Do not Unstructure the faulty state {data}`attrs.NOTHING` instead of to `1` (int). The unstructure to `1` was caused + by a change in attrs 22.2 that {data}`attrs.NOTHING` is internally represented as an integer {class}`enum.Enum`, + but as it is logically just a Singleton we apply now the identity-function on unstructuring it. + ([#667](https://github.com/python-attrs/cattrs/pull/667)) ## 25.1.1 (2025-06-04) diff --git a/src/cattrs/converters.py b/src/cattrs/converters.py index 81c75996..571c0e04 100644 --- a/src/cattrs/converters.py +++ b/src/cattrs/converters.py @@ -11,6 +11,7 @@ from pathlib import Path from typing import Any, Optional, Tuple, TypeVar, overload +from attr._make import _Nothing from attrs import Attribute, resolve_types from attrs import has as attrs_has from typing_extensions import Self @@ -223,7 +224,7 @@ def __init__( unstructure_fallback_factory, self ) self._unstructure_func.register_cls_list( - [(bytes, identity), (str, identity), (Path, str)] + [(bytes, identity), (str, identity), (Path, str), (_Nothing, identity)] ) self._unstructure_func.register_func_list( [ diff --git a/tests/test_enums.py b/tests/test_enums.py index 59ebb6b6..fdf3f4a0 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -1,5 +1,6 @@ """Tests for enums.""" +from attrs import NOTHING from hypothesis import given from hypothesis.strategies import data, sampled_from from pytest import raises @@ -29,3 +30,17 @@ def test_enum_failure(enum): converter.structure("", type) assert exc_info.value.args[0] == f" not in literal {type!r}" + + +def test_nothing_from_attrs(): + """Test that `NOTHING` from attrs does not unstructure to `1` (int), but remains `NOTHING`.""" + converter = BaseConverter() + + assert ( + converter.unstructure(NOTHING) != 1 + ), "NOTHING should not unstructure to 1 (int)." + assert not isinstance(converter.unstructure(NOTHING), int) + assert not converter.unstructure( + NOTHING + ) # bool(NOTHING) should be False although `bool(1)` is True + assert converter.unstructure(NOTHING) is NOTHING diff --git a/tests/test_errors.py b/tests/test_errors.py index fcea0a54..37cf2150 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -18,9 +18,9 @@ "err_cls, err_args", [ (StructureHandlerNotFoundError, ("Structure Message", int)), - (ForbiddenExtraKeysError, ("Forbidden Message", int, {"foo", "bar"})), - (ForbiddenExtraKeysError, ("", str, {"foo", "bar"})), - (ForbiddenExtraKeysError, (None, list, {"foo", "bar"})), + (ForbiddenExtraKeysError, ("Forbidden Message", int, {"foo"})), + (ForbiddenExtraKeysError, ("", str, {"foo"})), + (ForbiddenExtraKeysError, (None, list, {"foo"})), ( BaseValidationError, ("BaseValidation Message", [ValueError("Test BaseValidation")], int),