diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8a0e89dd..ce18b777 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -5,7 +5,6 @@ on: push: branches: ["main"] pull_request: - branches: ["main"] workflow_dispatch: jobs: @@ -47,7 +46,7 @@ jobs: steps: - uses: "actions/checkout@v3" - + - uses: "actions/setup-python@v4" with: cache: "pip" @@ -76,7 +75,7 @@ jobs: with: name: "html-report" path: "htmlcov" - + - name: "Make badge" if: github.ref == 'refs/heads/main' uses: "schneegans/dynamic-badges-action@v1.4.0" diff --git a/HISTORY.md b/HISTORY.md index b772db02..affa1316 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,10 @@ # History +## 23.2.2 (UNRELEASED) + +- Fix a regression when unstructuring `Any | None`. + ([#453](https://github.com/python-attrs/cattrs/issues/453)) + ## 23.2.1 (2023-11-18) - Fix unnecessary `typing_extensions` import on Python 3.11. diff --git a/src/cattrs/converters.py b/src/cattrs/converters.py index 3ba1ecad..5e9e2b9c 100644 --- a/src/cattrs/converters.py +++ b/src/cattrs/converters.py @@ -954,7 +954,12 @@ def gen_unstructure_optional(self, cl: Type[T]) -> Callable[[T], Any]: """Generate an unstructuring hook for optional types.""" union_params = cl.__args__ other = union_params[0] if union_params[1] is NoneType else union_params[1] - handler = self._unstructure_func.dispatch(other) + + # TODO: Remove this special case when we make unstructuring Any consistent. + if other is Any: + handler = self.unstructure + else: + handler = self._unstructure_func.dispatch(other) def unstructure_optional(val, _handler=handler): return None if val is None else _handler(val) diff --git a/tests/test_optionals.py b/tests/test_optionals.py index 510fecf0..5eec5c0b 100644 --- a/tests/test_optionals.py +++ b/tests/test_optionals.py @@ -1,8 +1,10 @@ -from typing import NewType, Optional +from typing import Any, NewType, Optional import pytest from attrs import define +from cattrs import Converter + from ._compat import is_py310_plus @@ -39,3 +41,13 @@ class ModelWithFoo: "total_foo": "bar", "maybe_foo": "is it a bar?", } + + +def test_optional_any(converter: Converter): + """Unstructuring Any|None is equivalent to unstructuring as v.__class__.""" + + @define + class A: + pass + + assert converter.unstructure(A(), Optional[Any]) == {}