Skip to content

Commit ca3e698

Browse files
committed
Structure abstract sets to frozensets
1 parent 75bfffa commit ca3e698

6 files changed

Lines changed: 59 additions & 1 deletion

File tree

HISTORY.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ Our backwards-compatibility policy can be found [here](https://github.com/python
1313

1414
## NEXT (UNRELEASED)
1515

16+
- **Potentially breaking**: [Abstract sets](https://docs.python.org/3/library/collections.abc.html#collections.abc.Set) are now structured into frozensets.
17+
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.
18+
See [Migrations](https://catt.rs/en/latest/migrations.html#abstract-sets-structuring-into-frozensets) for steps to restore legacy behavior.
19+
([#](https://github.com/python-attrs/cattrs/pull/))
1620
- Fix unstructuring NewTypes with the {class}`BaseConverter`.
1721
([#684](https://github.com/python-attrs/cattrs/pull/684))
1822
- Make some Hypothesis tests more robust.

Justfile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
python := ""
22
covcleanup := "true"
33

4+
sync:
5+
uv sync {{ if python != '' { '-p ' + python } else { '' } }} --all-groups --all-extras
6+
47
lint:
58
uv run -p python3.13 --group lint ruff check src/ tests bench
69
uv run -p python3.13 --group lint black --check src tests docs/conf.py
@@ -10,11 +13,11 @@ test *args="-x --ff -n auto tests":
1013

1114
testall:
1215
just python=python3.9 test
16+
just python=pypy3.9 test
1317
just python=python3.10 test
1418
just python=python3.11 test
1519
just python=python3.12 test
1620
just python=python3.13 test
17-
just python=pypy3.9 test
1821

1922
cov *args="-x --ff -n auto tests":
2023
uv run {{ if python != '' { '-p ' + python } else { '' } }} --all-extras --group test coverage run -m pytest {{args}}

docs/migrations.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,25 @@
11
# Migrations
22

3+
```{currentmodule} cattrs
4+
```
5+
36
_cattrs_ sometimes changes in backwards-incompatible ways.
47
This page contains guidance for changes and workarounds for restoring legacy behavior.
58

9+
## 25.3.0
10+
11+
### Abstract sets structuring into frozensets
12+
13+
From this version on, abstract sets (`collection.abc.Set`) structure into frozensets.
14+
15+
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.
16+
17+
```python
18+
>>> from cattrs.cols import is_abstract_set
19+
20+
>>> converter.register_structure_hook_func(is_abstract_set, converter._structure_set)
21+
```
22+
623
## 25.2.0
724

825
### Sequences structuring into tuples

src/cattrs/cols.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
from ._compat import (
2121
ANIES,
22+
AbcSet,
2223
get_args,
2324
get_origin,
2425
is_bare,
@@ -49,6 +50,7 @@
4950
__all__ = [
5051
"defaultdict_structure_factory",
5152
"homogenous_tuple_structure_factory",
53+
"is_abstract_set",
5254
"is_any_set",
5355
"is_defaultdict",
5456
"is_frozenset",
@@ -73,6 +75,11 @@ def is_any_set(type) -> bool:
7375
return is_set(type) or is_frozenset(type)
7476

7577

78+
def is_abstract_set(type) -> bool:
79+
"""A predicate function for abstract (collection.abc) sets."""
80+
return type is AbcSet or (getattr(type, "__origin__", None) is AbcSet)
81+
82+
7683
def is_namedtuple(type: Any) -> bool:
7784
"""A predicate function for named tuples."""
7885

src/cattrs/converters.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
from .cols import (
5656
defaultdict_structure_factory,
5757
homogenous_tuple_structure_factory,
58+
is_abstract_set,
5859
is_defaultdict,
5960
is_namedtuple,
6061
is_sequence,
@@ -281,6 +282,7 @@ def __init__(
281282
(is_mutable_sequence, list_structure_factory, "extended"),
282283
(is_deque, self._structure_deque),
283284
(is_mutable_set, self._structure_set),
285+
(is_abstract_set, self._structure_frozenset),
284286
(is_frozenset, self._structure_frozenset),
285287
(is_tuple, self._structure_tuple),
286288
(is_namedtuple, namedtuple_structure_factory, "extended"),

tests/test_cols.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from cattrs import BaseConverter, Converter
99
from cattrs._compat import FrozenSet
1010
from cattrs.cols import (
11+
is_abstract_set,
1112
is_any_set,
1213
is_sequence,
1314
iterable_unstructure_factory,
@@ -75,3 +76,27 @@ def test_structure_mut_sequences(converter: BaseConverter):
7576
"""Mutable sequences are structured to lists."""
7677

7778
assert converter.structure(["1", 2, 3.0], MutableSequence[int]) == [1, 2, 3]
79+
80+
81+
def test_abstract_set_predicate():
82+
"""`is_abstract_set` works."""
83+
84+
assert is_abstract_set(Set)
85+
assert is_abstract_set(Set[str])
86+
87+
assert not is_abstract_set(set)
88+
assert not is_abstract_set(set[str])
89+
90+
91+
def test_structure_abstract_sets(converter: BaseConverter):
92+
"""Abstract sets structure to frozensets."""
93+
94+
assert converter.structure(["1", "2", "3"], Set[int]) == frozenset([1, 2, 3])
95+
assert isinstance(converter.structure([1, 2, 3], Set[int]), frozenset)
96+
97+
98+
def test_structure_abstract_sets_override(converter: BaseConverter):
99+
"""Abstract sets can be overridden to structure to mutable sets, as before."""
100+
converter.register_structure_hook_func(is_abstract_set, converter._structure_set)
101+
102+
assert converter.structure(["1", 2, 3.0], Set[int]) == {1, 2, 3}

0 commit comments

Comments
 (0)