Skip to content
Merged
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
4 changes: 4 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ Our backwards-compatibility policy can be found [here](https://github.com/python

## NEXT (UNRELEASED)

- **Potentially breaking**: [Abstract sets](https://docs.python.org/3/library/collections.abc.html#collections.abc.Set) are now structured into frozensets.
This allows hashability, better immutability and is more consistent with the [`collections.abc.Set`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Set) type.
See [Migrations](https://catt.rs/en/latest/migrations.html#abstract-sets-structuring-into-frozensets) for steps to restore legacy behavior.
([#](https://github.com/python-attrs/cattrs/pull/))
- Fix unstructuring NewTypes with the {class}`BaseConverter`.
([#684](https://github.com/python-attrs/cattrs/pull/684))
- Make some Hypothesis tests more robust.
Expand Down
5 changes: 4 additions & 1 deletion Justfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
python := ""
covcleanup := "true"

sync:
uv sync {{ if python != '' { '-p ' + python } else { '' } }} --all-groups --all-extras

lint:
uv run -p python3.13 --group lint ruff check src/ tests bench
uv run -p python3.13 --group lint black --check src tests docs/conf.py
Expand All @@ -10,11 +13,11 @@ test *args="-x --ff -n auto tests":

testall:
just python=python3.9 test
just python=pypy3.9 test
just python=python3.10 test
just python=python3.11 test
just python=python3.12 test
just python=python3.13 test
just python=pypy3.9 test

cov *args="-x --ff -n auto tests":
uv run {{ if python != '' { '-p ' + python } else { '' } }} --all-extras --group test coverage run -m pytest {{args}}
Expand Down
9 changes: 7 additions & 2 deletions docs/defaulthooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,13 +301,15 @@ Deques are unstructured into lists, or into deques when using the {class}`BaseCo
Sets and frozensets can be structured from any iterable object.
Types converting to sets are:

- `collections.abc.Set[T]`
- `collections.abc.MutableSet[T]`
- `set[T]`
- `typing.Set[T]` (deprecated since Python 3.9, use `set[T]` instead)

Types converting to frozensets are:

- `collections.abc.Set[T]`
- `frozenset[T]`
- `typing.FrozenSet[T]` (deprecated since Python 3.9, use `frozenset[T]` instead)

In all cases, a new set or frozenset will be returned.
A bare type, for example `MutableSet` instead of `MutableSet[int]`, is equivalent to `MutableSet[Any]`.
Expand All @@ -318,8 +320,11 @@ A bare type, for example `MutableSet` instead of `MutableSet[int]`, is equivalen
{1, 2, 3, 4}
```

Sets and frozensets are unstructured into the same class.
Sets and frozensets are unstructured into the matching class.

```{versionchanged} NEXT
Abstract sets are now structured into frozensets instead of sets.
```

### Typed Dicts

Expand Down
17 changes: 17 additions & 0 deletions docs/migrations.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,25 @@
# Migrations

```{currentmodule} cattrs
```

_cattrs_ sometimes changes in backwards-incompatible ways.
This page contains guidance for changes and workarounds for restoring legacy behavior.

## 25.3.0

### Abstract sets structuring into frozensets

From this version on, abstract sets (`collection.abc.Set`) structure into frozensets.

The old behavior can be restored by registering the {meth}`BaseConverter._structure_set <cattrs.BaseConverter._structure_set>` method using the {meth}`is_abstract_set <cattrs.cols.is_abstract_set>` predicate on a converter.

```python
>>> from cattrs.cols import is_abstract_set

>>> converter.register_structure_hook_func(is_abstract_set, converter._structure_set)
```

## 25.2.0

### Sequences structuring into tuples
Expand Down
7 changes: 7 additions & 0 deletions src/cattrs/cols.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

from ._compat import (
ANIES,
AbcSet,
get_args,
get_origin,
is_bare,
Expand Down Expand Up @@ -49,6 +50,7 @@
__all__ = [
"defaultdict_structure_factory",
"homogenous_tuple_structure_factory",
"is_abstract_set",
"is_any_set",
"is_defaultdict",
"is_frozenset",
Expand All @@ -73,6 +75,11 @@ def is_any_set(type) -> bool:
return is_set(type) or is_frozenset(type)


def is_abstract_set(type) -> bool:
"""A predicate function for abstract (collection.abc) sets."""
return type is AbcSet or (getattr(type, "__origin__", None) is AbcSet)


def is_namedtuple(type: Any) -> bool:
"""A predicate function for named tuples."""

Expand Down
2 changes: 2 additions & 0 deletions src/cattrs/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
from .cols import (
defaultdict_structure_factory,
homogenous_tuple_structure_factory,
is_abstract_set,
is_defaultdict,
is_namedtuple,
is_sequence,
Expand Down Expand Up @@ -281,6 +282,7 @@ def __init__(
(is_mutable_sequence, list_structure_factory, "extended"),
(is_deque, self._structure_deque),
(is_mutable_set, self._structure_set),
(is_abstract_set, self._structure_frozenset),
(is_frozenset, self._structure_frozenset),
(is_tuple, self._structure_tuple),
(is_namedtuple, namedtuple_structure_factory, "extended"),
Expand Down
25 changes: 25 additions & 0 deletions tests/test_cols.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from cattrs import BaseConverter, Converter
from cattrs._compat import FrozenSet
from cattrs.cols import (
is_abstract_set,
is_any_set,
is_sequence,
iterable_unstructure_factory,
Expand Down Expand Up @@ -75,3 +76,27 @@ def test_structure_mut_sequences(converter: BaseConverter):
"""Mutable sequences are structured to lists."""

assert converter.structure(["1", 2, 3.0], MutableSequence[int]) == [1, 2, 3]


def test_abstract_set_predicate():
"""`is_abstract_set` works."""

assert is_abstract_set(Set)
assert is_abstract_set(Set[str])

assert not is_abstract_set(set)
assert not is_abstract_set(set[str])


def test_structure_abstract_sets(converter: BaseConverter):
"""Abstract sets structure to frozensets."""

assert converter.structure(["1", "2", "3"], Set[int]) == frozenset([1, 2, 3])
assert isinstance(converter.structure([1, 2, 3], Set[int]), frozenset)


def test_structure_abstract_sets_override(converter: BaseConverter):
"""Abstract sets can be overridden to structure to mutable sets, as before."""
converter.register_structure_hook_func(is_abstract_set, converter._structure_set)

assert converter.structure(["1", 2, 3.0], Set[int]) == {1, 2, 3}