def test_calling_back(): """Calling unstructure_attrs_asdict from a hook should not override a manual hook.""" converter = Converter() @attr.define class C: a: int = attr.ib(default=1) def handler(obj): return { "type_tag": obj.__class__.__name__, **converter.unstructure_attrs_asdict(obj), } converter.register_unstructure_hook(C, handler) inst = C() expected = {"type_tag": "C", "a": 1} unstructured1 = converter.unstructure(inst) unstructured2 = converter.unstructure(inst) assert unstructured1 == expected, repr(unstructured1) assert unstructured2 == unstructured1, repr(unstructured2)
def test_annotated_attrs(): """Annotation support works for attrs classes.""" from typing import Annotated converter = Converter() @attr.define class Inner: a: int @attr.define class Outer: i: Annotated[Inner, "test"] j: list[Annotated[Inner, "test"]] orig = Outer(Inner(1), [Inner(1)]) raw = converter.unstructure(orig) assert raw == {"i": {"a": 1}, "j": [{"a": 1}]} structured = converter.structure(raw, Outer) assert structured == orig # Now register a hook and rerun the test. converter.register_unstructure_hook(Inner, lambda v: {"a": 2}) raw = converter.unstructure(Outer(Inner(1), [Inner(1)])) assert raw == {"i": {"a": 2}, "j": [{"a": 2}]} structured = converter.structure(raw, Outer) assert structured == Outer(Inner(2), [Inner(2)])
def test_no_linecache(): """Linecaching should be disableable.""" @define class A: a: int c = GenConverter() before = len(linecache.cache) c.structure(c.unstructure(A(1)), A) after = len(linecache.cache) assert after == before + 2 @define class B: a: int before = len(linecache.cache) c.register_structure_hook( B, make_dict_structure_fn(B, c, _cattrs_use_linecache=False)) c.register_unstructure_hook( B, make_dict_unstructure_fn(B, c, _cattrs_use_linecache=False)) c.structure(c.unstructure(B(1)), B) assert len(linecache.cache) == before
def test_overriding_generated_unstructure(): """Test overriding a generated unstructure hook works.""" converter = Converter() @attr.define class Inner: a: int @attr.define class Outer: i: Inner inst = Outer(Inner(1)) converter.unstructure(inst) converter.register_unstructure_hook(Inner, lambda _: {"a": 2}) r = converter.structure(converter.unstructure(inst), Outer) assert r.i.a == 2
def test_seq_of_simple_classes_unstructure(cls_and_vals, seq_type_and_annotation): """Dumping a sequence of primitives is a simple copy operation.""" converter = Converter() test_val = ("test", 1) for cl, _ in cls_and_vals: converter.register_unstructure_hook(cl, lambda _: test_val) break # Just register the first class. seq_type, annotation = seq_type_and_annotation inputs = seq_type(cl(*vals) for cl, vals in cls_and_vals) outputs = converter.unstructure( inputs, unstructure_as=annotation[cl] if annotation not in (Tuple, tuple) else annotation[cl, ...], ) assert all(e == test_val for e in outputs)
def register_hooks(conv: GenConverter, allow_bytes: bool = True) -> None: def attrs_hook_factory(cls: Type[Any], gen_fn: Callable[..., Callable[[Any], Any]], structuring: bool) -> Callable[[Any], Any]: base = get_origin(cls) if base is None: base = cls attribs = fields(base) # PEP563 postponed annotations need resolving as we check Attribute.type below resolve_types(base) kwargs: dict[str, bool | AttributeOverride] = {} if structuring: kwargs["_cattrs_forbid_extra_keys"] = conv.forbid_extra_keys kwargs[ "_cattrs_prefer_attrib_converters"] = conv._prefer_attrib_converters else: kwargs["_cattrs_omit_if_default"] = conv.omit_if_default for a in attribs: if a.type in conv.type_overrides: # cattrs' gen_(un)structure_attrs_fromdict (used by default for attrs # classes that don't have a custom hook registered) check for any # type_overrides (Dict[Type, AttributeOverride]); they allow a custom # converter to omit specific attributes of given type e.g.: # >>> conv = GenConverter(type_overrides={Image: override(omit=True)}) attrib_override = conv.type_overrides[a.type] else: # by default, we omit all Optional attributes (i.e. with None default), # overriding a Converter's global 'omit_if_default' option. Specific # attibutes can still define their own 'omit_if_default' behavior in # the Attribute.metadata dict. attrib_override = override( omit_if_default=a.metadata.get("omit_if_default", a.default is None or None), rename=a.metadata.get( "rename_attr", a.name[1:] if a.name[0] == "_" else None), omit=not a.init, ) kwargs[a.name] = attrib_override return gen_fn(cls, conv, **kwargs) def custom_unstructure_hook_factory( cls: Type[Any]) -> Callable[[Any], Any]: return partial(cls._unstructure, converter=conv) def custom_structure_hook_factory(cls: Type[Any]) -> Callable[[Any], Any]: return partial(cls._structure, converter=conv) def unstructure_transform(t: Transform) -> Tuple[float]: return cast(Tuple[float], tuple(t)) conv.register_unstructure_hook_factory( is_ufoLib2_attrs_class, partial(attrs_hook_factory, gen_fn=make_dict_unstructure_fn, structuring=False), ) conv.register_unstructure_hook_factory( is_ufoLib2_class_with_custom_unstructure, custom_unstructure_hook_factory, ) conv.register_unstructure_hook(cast(Type[Transform], Transform), unstructure_transform) conv.register_structure_hook_factory( is_ufoLib2_attrs_class, partial(attrs_hook_factory, gen_fn=make_dict_structure_fn, structuring=True), ) conv.register_structure_hook_factory( is_ufoLib2_class_with_custom_structure, custom_structure_hook_factory, ) if not allow_bytes: from base64 import b64decode, b64encode def unstructure_bytes(v: bytes) -> str: return (b64encode(v) if v else b"").decode("utf8") def structure_bytes(v: str, _: Any) -> bytes: return b64decode(v) conv.register_unstructure_hook(bytes, unstructure_bytes) conv.register_structure_hook(bytes, structure_bytes)