Skip to content

Commit f0c0a53

Browse files
authored
Structure abstract sets to frozensets (#686)
* Structure abstract sets to frozensets * More docs
1 parent 64f6305 commit f0c0a53

File tree

7 files changed

+66
-3
lines changed

7 files changed

+66
-3
lines changed

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/defaulthooks.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,13 +301,15 @@ Deques are unstructured into lists, or into deques when using the {class}`BaseCo
301301
Sets and frozensets can be structured from any iterable object.
302302
Types converting to sets are:
303303

304-
- `collections.abc.Set[T]`
305304
- `collections.abc.MutableSet[T]`
306305
- `set[T]`
306+
- `typing.Set[T]` (deprecated since Python 3.9, use `set[T]` instead)
307307

308308
Types converting to frozensets are:
309309

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

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

321-
Sets and frozensets are unstructured into the same class.
323+
Sets and frozensets are unstructured into the matching class.
322324

325+
```{versionchanged} NEXT
326+
Abstract sets are now structured into frozensets instead of sets.
327+
```
323328

324329
### Typed Dicts
325330

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)