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 src/cattr/gen.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from cattrs.gen import (
make_dict_structure_fn,
make_dict_unstructure_fn,
make_hetero_tuple_structure_fn,
make_hetero_tuple_unstructure_fn,
make_mapping_structure_fn,
make_mapping_unstructure_fn,
Expand All @@ -13,6 +14,7 @@
"AttributeOverride",
"make_dict_structure_fn",
"make_dict_unstructure_fn",
"make_hetero_tuple_structure_fn",
"make_hetero_tuple_unstructure_fn",
"make_iterable_unstructure_fn",
"make_mapping_structure_fn",
Expand Down
11 changes: 11 additions & 0 deletions src/cattrs/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,13 @@
from .fns import Predicate, identity, raise_error
from .gen import (
AttributeOverride,
HeteroTupleStructureFn,
HeteroTupleUnstructureFn,
IterableUnstructureFn,
MappingUnstructureFn,
make_dict_structure_fn,
make_dict_unstructure_fn,
make_hetero_tuple_structure_fn,
make_hetero_tuple_unstructure_fn,
)
from .gen.typeddicts import make_dict_structure_fn as make_typeddict_dict_struct_fn
Expand Down Expand Up @@ -902,6 +904,12 @@ def _structure_optional(self, obj, union):
# We can't actually have a Union of a Union, so this is safe.
return self._structure_func.dispatch(other)(obj, other)

def gen_structure_hetero_tuple(self, cl: Any) -> HeteroTupleStructureFn:
"""Generate a heterogeneous tuple structure function."""
return make_hetero_tuple_structure_fn(
cl, self, detailed_validation=self.detailed_validation
)

def _structure_tuple(self, obj: Iterable, tup: type[T]) -> T:
"""Deal with structuring into a tuple."""
tup_params = None if tup in (Tuple, tuple) else tup.__args__
Expand Down Expand Up @@ -1183,6 +1191,9 @@ def __init__(
)

self.register_structure_hook_factory(is_annotated, self.gen_structure_annotated)
self.register_structure_hook_factory(
is_hetero_tuple, self.gen_structure_hetero_tuple
)
self.register_structure_hook_factory(is_mapping, self.gen_structure_mapping)
self.register_structure_hook_factory(is_counter, self.gen_structure_counter)
self.register_structure_hook_factory(
Expand Down
102 changes: 102 additions & 0 deletions src/cattrs/gen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"make_dict_structure_fn_from_attrs",
"make_dict_unstructure_fn",
"make_dict_unstructure_fn_from_attrs",
"make_hetero_tuple_structure_fn",
"make_hetero_tuple_unstructure_fn",
"make_iterable_unstructure_fn",
"make_mapping_structure_fn",
Expand Down Expand Up @@ -852,6 +853,107 @@ def make_dict_structure_fn(

#: A type alias for heterogeneous tuple unstructure hooks.
HeteroTupleUnstructureFn: TypeAlias = Callable[[tuple[Any, ...]], Any]
HeteroTupleStructureFn: TypeAlias = Callable[[Iterable[Any], Any], tuple[Any, ...]]


def make_hetero_tuple_structure_fn(
cl: Any,
converter: BaseConverter,
detailed_validation: bool | Literal["from_converter"] = "from_converter",
use_linecache: bool = True,
) -> HeteroTupleStructureFn:
"""Generate a specialized structuring function for a heterogenous tuple.

.. versionadded:: NEXT
"""
fn_name = "structure_tuple"

if detailed_validation == "from_converter":
detailed_validation = converter.detailed_validation

type_args = get_args(cl)
globs = {}
lines = []
internal_arg_parts = {"__cl": cl}

if detailed_validation:
internal_arg_parts["__c_ive"] = IterableValidationError
internal_arg_parts["__c_ivn"] = IterableValidationNote
lines.extend([" errors = []", " res = []"])

for ix, t in enumerate(type_args):
handler = converter.get_structure_hook(t)
struct_handler_name = f"__c_structure_{ix}"
type_name = f"__c_type_{ix}"
internal_arg_parts[struct_handler_name] = handler
internal_arg_parts[type_name] = t
if handler == converter._structure_call:
internal_arg_parts[struct_handler_name] = t
invocation = f"{struct_handler_name}(o[{ix}])"
else:
invocation = f"{struct_handler_name}(o[{ix}], {type_name})"
lines.extend(
[
f" if len(o) > {ix}:",
" try:",
f" res.append({invocation})",
" except Exception as e:",
(
f" e.__notes__ = [*getattr(e, '__notes__', []), "
f"__c_ivn('Structuring {cl} @ index {ix}', {ix}, {type_name})]"
),
" errors.append(e)",
]
)

lines.extend(
[
f" if len(o) != {len(type_args)}:",
(
" problem = 'Not enough' if len(o) < "
f"{len(type_args)} else 'Too many'"
),
' exc = ValueError(f"{problem} values in {o!r} to structure as {__cl!r}")',
" exc.__notes__ = [f'Structuring {__cl}']",
" errors.append(exc)",
" if errors:",
" raise __c_ive(f'While structuring {__cl!r}', errors, __cl)",
" return tuple(res)",
]
)
else:
for ix, t in enumerate(type_args):
handler = converter.get_structure_hook(t)
struct_handler_name = f"__c_structure_{ix}"
type_name = f"__c_type_{ix}"
internal_arg_parts[struct_handler_name] = handler
internal_arg_parts[type_name] = t
if handler == converter._structure_call:
internal_arg_parts[struct_handler_name] = t
invocation = f"{struct_handler_name}(o[{ix}])"
else:
invocation = f"{struct_handler_name}(o[{ix}], {type_name})"
lines.append(f" {invocation},")

total_len = len(type_args)
lines = [
f" if len(o) != {total_len}:",
(f" problem = 'Not enough' if len(o) < {total_len} else 'Too many'"),
' raise ValueError(f"{problem} values in {o!r} to structure as {__cl!r}")',
" return (",
*lines,
" )",
]

internal_arg_line = ", ".join([f"{i}={i}" for i in internal_arg_parts])
globs.update(internal_arg_parts)
script = "\n".join([f"def {fn_name}(o, _=__cl, {internal_arg_line}):", *lines])

fname = generate_unique_filename(
cl, "structure", lines=script.splitlines() if use_linecache else []
)
eval(compile(script, fname, "exec"), globs)
return globs[fn_name]


def make_hetero_tuple_unstructure_fn(
Expand Down
29 changes: 28 additions & 1 deletion tests/test_gen_collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,15 @@

from typing import Generic, Mapping, NewType, Tuple, TypeVar

from pytest import raises

from cattrs import Converter
from cattrs.gen import make_hetero_tuple_unstructure_fn, make_mapping_structure_fn
from cattrs.errors import IterableValidationError
from cattrs.gen import (
make_hetero_tuple_structure_fn,
make_hetero_tuple_unstructure_fn,
make_mapping_structure_fn,
)


def test_structuring_mappings(genconverter: Converter):
Expand All @@ -30,3 +37,23 @@ def test_unstructure_hetero_tuple_to_tuple(genconverter: Converter):
fn = make_hetero_tuple_unstructure_fn(Tuple[int, str, int], genconverter, tuple)

assert fn((1, "1", 2)) == (1, "1", 2)


def test_structure_hetero_tuple(genconverter: Converter):
"""`make_hetero_tuple_structure_fn` structures heterogeneous tuples."""
fn = make_hetero_tuple_structure_fn(tuple[int, str], genconverter)

assert fn(["1", 2], tuple[int, str]) == (1, "2")


def test_structure_hetero_tuple_validation():
"""`make_hetero_tuple_structure_fn` preserves detailed validation."""
conv = Converter()
fn = make_hetero_tuple_structure_fn(Tuple[int, int], conv)

with raises(IterableValidationError) as exc:
fn(["1", "a"], Tuple[int, int])

assert exc.value.exceptions[0].__notes__ == [
"Structuring typing.Tuple[int, int] @ index 1"
]
22 changes: 20 additions & 2 deletions tests/test_tuples.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,31 @@
from attrs import Factory, define
from pytest import raises

from cattrs import Converter
from cattrs.cols import (
is_namedtuple,
namedtuple_dict_structure_factory,
namedtuple_dict_unstructure_factory,
)
from cattrs.converters import Converter
from cattrs.errors import ClassValidationError, ForbiddenExtraKeysError
from cattrs.converters import BaseConverter
from cattrs.errors import (
ClassValidationError,
ForbiddenExtraKeysError,
IterableValidationError,
)


def test_structuring_invalid_tuples(converter: BaseConverter):
"""Structuring (hetero) tuples raises properly."""

if converter.detailed_validation:
with raises(IterableValidationError) as exc_info:
converter.structure(["1", 2, "c"], tuple[int, int, int])
assert isinstance(exc_info.value.exceptions[0], ValueError)
else:
# `int("c")` raises a ValueError
with raises(ValueError):
converter.structure(["1", 2, "c"], tuple[int, int, int])


def test_simple_hetero_tuples(genconverter: Converter):
Expand Down
Loading