Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ 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+.
Expand Down
18 changes: 14 additions & 4 deletions src/cattrs/preconf/pyyaml.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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)
Expand All @@ -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.
Expand Down
18 changes: 18 additions & 0 deletions tests/preconf/test_pyyaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")