Skip to content

Commit

Permalink
Simplify and optimize iterable unstructuring (#516)
Browse files Browse the repository at this point in the history
* Simplify and optimize iterable unstructuring

* Handle TypeVars after all

* Add test
  • Loading branch information
Tinche authored Mar 6, 2024
1 parent 8196a2e commit 39e698f
Show file tree
Hide file tree
Showing 4 changed files with 24 additions and 20 deletions.
25 changes: 9 additions & 16 deletions src/cattrs/gen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -675,29 +675,22 @@ def make_iterable_unstructure_fn(
"""Generate a specialized unstructure function for an iterable."""
handler = converter.unstructure

fn_name = "unstructure_iterable"

# Let's try fishing out the type args
# Unspecified tuples have `__args__` as empty tuples, so guard
# against IndexError.
if getattr(cl, "__args__", None) not in (None, ()):
type_arg = cl.__args__[0]
# We don't know how to handle the TypeVar on this level,
# so we skip doing the dispatch here.
if not isinstance(type_arg, TypeVar):
handler = converter.get_unstructure_hook(type_arg, cache_result=False)

globs = {"__cattr_seq_cl": unstructure_to or cl, "__cattr_u": handler}
lines = []

lines.append(f"def {fn_name}(iterable):")
lines.append(" res = __cattr_seq_cl(__cattr_u(i) for i in iterable)")
if isinstance(type_arg, TypeVar):
type_arg = getattr(type_arg, "__default__", Any)
handler = converter.get_unstructure_hook(type_arg, cache_result=False)
if handler == identity:
# Save ourselves the trouble of iterating over it all.
return unstructure_to or cl

total_lines = [*lines, " return res"]

eval(compile("\n".join(total_lines), "", "exec"), globs)
def unstructure_iterable(iterable, _seq_cl=unstructure_to or cl, _hook=handler):
return _seq_cl(_hook(i) for i in iterable)

return globs[fn_name]
return unstructure_iterable


#: A type alias for heterogeneous tuple unstructure hooks.
Expand Down
5 changes: 2 additions & 3 deletions src/cattrs/preconf/msgspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,15 +106,14 @@ def configure_passthroughs(converter: Converter) -> None:
)


def seq_unstructure_factory(type, converter: BaseConverter) -> UnstructureHook:
def seq_unstructure_factory(type, converter: Converter) -> UnstructureHook:
"""The msgspec unstructure hook factory for sequences."""
if is_bare(type):
type_arg = Any
handler = converter.get_unstructure_hook(type_arg, cache_result=False)
else:
args = get_args(type)
type_arg = args[0]
handler = converter.get_unstructure_hook(type_arg, cache_result=False)
handler = converter.get_unstructure_hook(type_arg, cache_result=False)

if handler in (identity, to_builtins):
return handler
Expand Down
File renamed without changes.
14 changes: 13 additions & 1 deletion tests/test_generics_696.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Tests for generics under PEP 696 (type defaults)."""
from typing import Generic
from typing import Generic, List

import pytest
from attrs import define, fields
Expand Down Expand Up @@ -48,3 +48,15 @@ class D(Generic[TD]):
# But allows other types
assert genconverter.structure({"a": "1"}, D[str]) == D("1")
assert genconverter.structure({"a": 1}, D[int]) == D(1)


def test_unstructure_iterable(genconverter):
"""Unstructuring iterables with defaults works."""
genconverter.register_unstructure_hook(str, lambda v: v + "_str")

@define
class C(Generic[TD]):
a: List[TD]

assert genconverter.unstructure(C(["a"])) == {"a": ["a_str"]}
assert genconverter.unstructure(["a"], List[TD]) == ["a_str"]

0 comments on commit 39e698f

Please sign in to comment.