From 5c56cd7a89b19ca244e73bdb0ede430f567bc607 Mon Sep 17 00:00:00 2001 From: Debasish Padhy Date: Sun, 18 Oct 2026 23:03:22 +0530 Subject: [PATCH 1/4] fix pyyaml converter crashing on non-enum str subclasses --- src/cattrs/preconf/pyyaml.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/cattrs/preconf/pyyaml.py b/src/cattrs/preconf/pyyaml.py index 9d8f76ec..c83370a5 100644 --- a/src/cattrs/preconf/pyyaml.py +++ b/src/cattrs/preconf/pyyaml.py @@ -1,6 +1,7 @@ """Preconfigured converters for pyyaml.""" from datetime import date, datetime +from enum import Enum from functools import partial from typing import Any, TypeVar, Union @@ -24,6 +25,15 @@ def validate_date(v: Any, _): return v +def _unstructure_str(v: str) -> Any: + # `safe_dump` rejects str subclasses, so they have to become plain strings. + if v.__class__ is str: + return v + if isinstance(v, Enum): + return v.value + return str(v) + + class PyyamlConverter(Converter): def dumps(self, obj: Any, unstructure_as: Any = None, **kwargs: Any) -> str: return safe_dump(self.unstructure(obj, unstructure_as=unstructure_as), **kwargs) @@ -43,16 +53,16 @@ def configure_converter(converter: BaseConverter) -> None: Configure the converter for use with the pyyaml library. * frozensets are serialized as lists - * string enums are converted into strings explicitly + * string enums and other str subclasses are converted into strings explicitly * datetimes and dates are validated * typed namedtuples are serialized as lists .. versionchanged:: 24.1.0 Add support for typed namedtuples. + .. versionchanged:: NEXT + Non-enum str subclasses are unstructured into plain strings. """ - converter.register_unstructure_hook( - str, lambda v: v if v.__class__ is str else v.value - ) + converter.register_unstructure_hook(str, _unstructure_str) # datetime inherits from date, so identity unstructure hook used # here to prevent the date unstructure hook running. From 8a000780b51a65143445c5e395e106cceb2f84fb Mon Sep 17 00:00:00 2001 From: Debasish Padhy Date: Sun, 18 Oct 2026 23:03:22 +0530 Subject: [PATCH 2/4] add pyyaml test for unstructuring str subclasses --- tests/preconf/test_pyyaml.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/preconf/test_pyyaml.py b/tests/preconf/test_pyyaml.py index 11341896..ecd1f4fb 100644 --- a/tests/preconf/test_pyyaml.py +++ b/tests/preconf/test_pyyaml.py @@ -82,3 +82,21 @@ class A: assert ( repr(exc_info.value.exceptions[1]) == "ValueError('Expected date, got 1')" ) + + +def test_pyyaml_str_subclass(): + """Non-enum str subclasses are dumped as plain strings.""" + + class MyStr(str): + pass + + @define + class A: + a: str + + converter = make_converter() + + unstructured = converter.unstructure(A(MyStr("x"))) + assert unstructured == {"a": "x"} + assert unstructured["a"].__class__ is str + assert converter.loads(converter.dumps(A(MyStr("x"))), A) == A("x") From d46ebb67f57f62b99e4a5fd9b89424053411e802 Mon Sep 17 00:00:00 2001 From: Debasish Padhy Date: Sun, 18 Oct 2026 23:03:22 +0530 Subject: [PATCH 3/4] add changelog entry for pyyaml str subclass fix --- HISTORY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/HISTORY.md b/HISTORY.md index a4464ea6..169b43b1 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -13,6 +13,7 @@ Our backwards-compatibility policy can be found [here](https://github.com/python ## NEXT +- The PyYAML preconf converter no longer fails with an `AttributeError` when unstructuring `str` subclasses that aren't enums (such as `markupsafe.Markup`); they are now unstructured into plain strings. - Dict unstructuring hook factories now inherit the converter's `omit_if_default` setting when `_cattrs_omit_if_default` is omitted. Explicit `True` or `False` values and per-attribute overrides retain their precedence. ([#238](https://github.com/python-attrs/cattrs/issues/238)) - Support the [`frozendict`](https://docs.python.org/3.15/builtins/stdtypes.html#frozendict) built-in on Python 3.15+. From 92c713768f60ae868625c15d2632302397b0ac83 Mon Sep 17 00:00:00 2001 From: Debasish Padhy Date: Sun, 18 Oct 2026 23:04:52 +0530 Subject: [PATCH 4/4] add PR link to changelog entry --- HISTORY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/HISTORY.md b/HISTORY.md index 169b43b1..9ddae2f3 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -14,6 +14,7 @@ Our backwards-compatibility policy can be found [here](https://github.com/python ## NEXT - The PyYAML preconf converter no longer fails with an `AttributeError` when unstructuring `str` subclasses that aren't enums (such as `markupsafe.Markup`); they are now unstructured into plain strings. + ([#804](https://github.com/python-attrs/cattrs/pull/804)) - Dict unstructuring hook factories now inherit the converter's `omit_if_default` setting when `_cattrs_omit_if_default` is omitted. Explicit `True` or `False` values and per-attribute overrides retain their precedence. ([#238](https://github.com/python-attrs/cattrs/issues/238)) - Support the [`frozendict`](https://docs.python.org/3.15/builtins/stdtypes.html#frozendict) built-in on Python 3.15+.