Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tweak set customization #266

Merged
merged 3 commits into from
May 5, 2022
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
2 changes: 2 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ History
* PyPy support (and tests, using a minimal Hypothesis profile) restored.
(`#253 <https://github.com/python-attrs/cattrs/issues/253>`_)
* Fix propagating the `detailed_validation` flag to mapping and counter structuring generators.
* Fix ``typing.Set`` applying too broadly when used with the ``GenConverter.unstruct_collection_overrides`` parameter on Python versions below 3.9. Switch to ``typing.AbstractSet`` on those versions to restore the old behavior.
(`#264 <https://github.com/python-attrs/cattrs/issues/264>`_)

22.1.0 (2022-04-03)
-------------------
Expand Down
4 changes: 2 additions & 2 deletions docs/customizing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Customizing class un/structuring
This section deals with customizing the unstructuring and structuring processes in `cattrs`.

Using ``cattr.Converter``
********************************
*************************

The default ``Converter``, upon first encountering an ``attrs`` class, will use
the generation functions mentioned here to generate the specialized hooks for it,
Expand All @@ -20,7 +20,7 @@ them for types using :py:attr:`Converter.register_structure_hook <cattrs.Convert
flexible but also requires the most amount of boilerplate.

Using ``cattrs.gen`` generators
******************************
*******************************

`cattrs` includes a module, :mod:`cattrs.gen`, which allows for generating and
compiling specialized functions for unstructuring ``attrs`` classes.
Expand Down
9 changes: 9 additions & 0 deletions src/cattrs/_compat.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import builtins
import sys
from collections.abc import MutableSet as AbcMutableSet
from collections.abc import Set as AbcSet
from dataclasses import MISSING
from dataclasses import fields as dataclass_fields
from dataclasses import is_dataclass
from typing import AbstractSet as TypingAbstractSet
from typing import Any, Dict, FrozenSet, List
from typing import Mapping as TypingMapping
from typing import MutableMapping as TypingMutableMapping
Expand Down Expand Up @@ -109,9 +112,14 @@ def is_protocol(type: Any) -> bool:
return issubclass(type, Protocol) and getattr(type, "_is_protocol", False)


OriginAbstractSet = AbcSet
OriginMutableSet = AbcMutableSet

if is_py37 or is_py38:
Set = TypingSet
AbstractSet = TypingAbstractSet
MutableSet = TypingMutableSet

Sequence = TypingSequence
MutableSequence = TypingMutableSequence
MutableMapping = TypingMutableMapping
Expand Down Expand Up @@ -241,6 +249,7 @@ def is_literal(_) -> bool:
return False

Set = AbcSet
AbstractSet = AbcSet
MutableSet = AbcMutableSet
Sequence = AbcSequence
MutableSequence = AbcMutableSequence
Expand Down
22 changes: 13 additions & 9 deletions src/cattrs/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
Mapping,
MutableMapping,
MutableSequence,
MutableSet,
OriginAbstractSet,
OriginMutableSet,
Sequence,
Set,
fields,
get_newtype_base,
get_origin,
Expand Down Expand Up @@ -647,23 +647,27 @@ def __init__(
self.forbid_extra_keys = forbid_extra_keys
self.type_overrides = dict(type_overrides)

unstruct_collection_overrides = {
get_origin(k) or k: v for k, v in unstruct_collection_overrides.items()
}

self._unstruct_collection_overrides = unstruct_collection_overrides

# Do a little post-processing magic to make things easier for users.
co = unstruct_collection_overrides

# abc.Set overrides, if defined, apply to abc.MutableSets and sets
if Set in co:
if MutableSet not in co:
co[MutableSet] = co[Set]
co[AbcMutableSet] = co[Set] # For 3.7/3.8 compatibility.
if OriginAbstractSet in co:
if OriginMutableSet not in co:
co[OriginMutableSet] = co[OriginAbstractSet]
co[AbcMutableSet] = co[OriginAbstractSet] # For 3.7/3.8 compatibility.
if FrozenSetSubscriptable not in co:
co[FrozenSetSubscriptable] = co[Set]
co[FrozenSetSubscriptable] = co[OriginAbstractSet]

# abc.MutableSet overrrides, if defined, apply to sets
if MutableSet in co:
if OriginMutableSet in co:
if set not in co:
co[set] = co[MutableSet]
co[set] = co[OriginMutableSet]

if FrozenSetSubscriptable in co:
co[frozenset] = co[FrozenSetSubscriptable] # For 3.7/3.8 compatibility.
Expand Down
4 changes: 2 additions & 2 deletions src/cattrs/preconf/bson.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from bson import DEFAULT_CODEC_OPTIONS, CodecOptions, ObjectId, decode, encode

from cattrs._compat import Set, is_mapping
from cattrs._compat import AbstractSet, is_mapping
from cattrs.gen import make_mapping_structure_fn

from ..converters import BaseConverter, Converter
Expand Down Expand Up @@ -89,7 +89,7 @@ def gen_structure_mapping(cl: Any):
def make_converter(*args, **kwargs) -> BsonConverter:
kwargs["unstruct_collection_overrides"] = {
**kwargs.get("unstruct_collection_overrides", {}),
Set: list,
AbstractSet: list,
}
res = BsonConverter(*args, **kwargs)
configure_converter(res)
Expand Down
4 changes: 2 additions & 2 deletions src/cattrs/preconf/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from json import dumps, loads
from typing import Any, Type, TypeVar, Union

from cattrs._compat import Counter, Set
from cattrs._compat import AbstractSet, Counter

from ..converters import BaseConverter, Converter

Expand Down Expand Up @@ -39,7 +39,7 @@ def configure_converter(converter: BaseConverter):
def make_converter(*args, **kwargs) -> JsonConverter:
kwargs["unstruct_collection_overrides"] = {
**kwargs.get("unstruct_collection_overrides", {}),
Set: list,
AbstractSet: list,
Counter: dict,
}
res = JsonConverter(*args, **kwargs)
Expand Down
4 changes: 2 additions & 2 deletions src/cattrs/preconf/msgpack.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from msgpack import dumps, loads

from cattrs._compat import Set
from cattrs._compat import AbstractSet

from ..converters import BaseConverter, Converter

Expand Down Expand Up @@ -35,7 +35,7 @@ def configure_converter(converter: BaseConverter):
def make_converter(*args, **kwargs) -> MsgpackConverter:
kwargs["unstruct_collection_overrides"] = {
**kwargs.get("unstruct_collection_overrides", {}),
Set: list,
AbstractSet: list,
}
res = MsgpackConverter(*args, **kwargs)
configure_converter(res)
Expand Down
4 changes: 2 additions & 2 deletions src/cattrs/preconf/orjson.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from orjson import dumps, loads

from cattrs._compat import Set, is_mapping
from cattrs._compat import AbstractSet, is_mapping

from ..converters import BaseConverter, Converter

Expand Down Expand Up @@ -69,7 +69,7 @@ def key_handler(v):
def make_converter(*args, **kwargs) -> OrjsonConverter:
kwargs["unstruct_collection_overrides"] = {
**kwargs.get("unstruct_collection_overrides", {}),
Set: list,
AbstractSet: list,
}
res = OrjsonConverter(*args, **kwargs)
configure_converter(res)
Expand Down
4 changes: 2 additions & 2 deletions src/cattrs/preconf/tomlkit.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from tomlkit import dumps, loads

from cattrs._compat import Set, is_mapping
from cattrs._compat import AbstractSet, is_mapping

from ..converters import BaseConverter, Converter
from . import validate_datetime
Expand Down Expand Up @@ -59,7 +59,7 @@ def key_handler(k: bytes):
def make_converter(*args, **kwargs) -> TomlkitConverter:
kwargs["unstruct_collection_overrides"] = {
**kwargs.get("unstruct_collection_overrides", {}),
Set: list,
AbstractSet: list,
tuple: list,
}
res = TomlkitConverter(*args, **kwargs)
Expand Down
4 changes: 2 additions & 2 deletions src/cattrs/preconf/ujson.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from ujson import dumps, loads

from cattrs._compat import Set
from cattrs._compat import AbstractSet

from ..converters import BaseConverter, Converter

Expand Down Expand Up @@ -40,7 +40,7 @@ def configure_converter(converter: BaseConverter):
def make_converter(*args, **kwargs) -> UjsonConverter:
kwargs["unstruct_collection_overrides"] = {
**kwargs.get("unstruct_collection_overrides", {}),
Set: list,
AbstractSet: list,
}
res = UjsonConverter(*args, **kwargs)
configure_converter(res)
Expand Down
42 changes: 42 additions & 0 deletions tests/test_unstructure_collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,48 @@ def test_collection_unstructure_override_set():
assert c.unstructure({1, 2, 3}) == [1, 2, 3]


@pytest.mark.skipif(is_py39_plus, reason="Requires Python 3.8 or lower")
def test_collection_unstructure_override_set_38():
"""Test overriding unstructuring sets."""
from typing import AbstractSet, MutableSet, Set

# First approach, predicate hook with is_mutable_set
c = Converter()

c._unstructure_func.register_func_list(
[
(
is_mutable_set,
partial(c.gen_unstructure_iterable, unstructure_to=list),
True,
)
]
)

assert c.unstructure({1, 2, 3}, unstructure_as=Set[int]) == [1, 2, 3]

# Second approach, using __builtins__.set
c = Converter(unstruct_collection_overrides={set: list})

assert c.unstructure({1, 2, 3}, unstructure_as=Set[int]) == [1, 2, 3]
assert c.unstructure({1, 2, 3}, unstructure_as=MutableSet[int]) == {1, 2, 3}
assert c.unstructure({1, 2, 3}) == [1, 2, 3]

# Second approach, using typing.MutableSet
c = Converter(unstruct_collection_overrides={MutableSet: list})

assert c.unstructure({1, 2, 3}, unstructure_as=Set[int]) == [1, 2, 3]
assert c.unstructure({1, 2, 3}, unstructure_as=MutableSet[int]) == [1, 2, 3]
assert c.unstructure({1, 2, 3}) == [1, 2, 3]

# Second approach, using typing.AbstractSet
c = Converter(unstruct_collection_overrides={AbstractSet: list})

assert c.unstructure({1, 2, 3}, unstructure_as=Set[int]) == [1, 2, 3]
assert c.unstructure({1, 2, 3}, unstructure_as=MutableSet[int]) == [1, 2, 3]
assert c.unstructure({1, 2, 3}) == [1, 2, 3]


@pytest.mark.skipif(not is_py39_plus, reason="Requires Python 3.9+")
def test_collection_unstructure_override_seq():
"""Test overriding unstructuring seq."""
Expand Down