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_simple_roundtrip(cls_and_vals, strat): """ Simple classes with metadata can be unstructured and restructured. """ converter = Converter(unstruct_strat=strat) cl, vals = cls_and_vals inst = cl(*vals) assert inst == converter.structure(converter.unstructure(inst), cl)
def test_simple_recursive(): c = GenConverter() orig = A([A([])]) unstructured = c.unstructure(orig) assert unstructured == {"inner": [{"inner": []}]} assert c.structure(unstructured, A) == orig
def test_nested_roundtrip(cls_and_vals, strat): """ Nested classes with metadata can be unstructured and restructured. """ converter = Converter(unstruct_strat=strat) cl, vals = cls_and_vals # Vals are a tuple, convert into a dictionary. inst = cl(*vals) assert inst == converter.structure(converter.unstructure(inst), cl)
def test_simple_roundtrip_with_extra_keys_forbidden(cls_and_vals, strat): """ Simple classes can be unstructured and restructured with forbid_extra_keys=True. """ converter = Converter(unstruct_strat=strat, forbid_extra_keys=True) cl, vals = cls_and_vals inst = cl(*vals) unstructured = converter.unstructure(inst) assert "Hyp" not in repr(unstructured) assert inst == converter.structure(unstructured, cl)
def test_union_field_roundtrip(cl_and_vals_a, cl_and_vals_b, strat): """ Classes with union fields can be unstructured and structured. """ converter = Converter(unstruct_strat=strat) cl_a, vals_a = cl_and_vals_a cl_b, vals_b = cl_and_vals_b a_field_names = {a.name for a in fields(cl_a)} b_field_names = {a.name for a in fields(cl_b)} assume(a_field_names) assume(b_field_names) common_names = a_field_names & b_field_names assume(len(a_field_names) > len(common_names)) @attr.s class C(object): a = attr.ib(type=Union[cl_a, cl_b]) inst = C(a=cl_a(*vals_a)) if strat is UnstructureStrategy.AS_DICT: assert inst == converter.structure(converter.unstructure(inst), C) else: # Our disambiguation functions only support dictionaries for now. with pytest.raises(ValueError): converter.structure(converter.unstructure(inst), C) def handler(obj, _): return converter.structure(obj, cl_a) converter._union_registry[Union[cl_a, cl_b]] = handler assert inst == converter.structure(converter.unstructure(inst), C) del converter._union_registry[Union[cl_a, cl_b]]
def test_structure_linecache(): """Linecaching for structuring should work.""" @define class A: a: int c = GenConverter() try: c.structure({"a": "test"}, A) except ValueError: res = format_exc() assert "'a'" in res
def test_forbid_extra_keys_defaults(attr_and_vals): """ Restructuring fails when a dict key is renamed (if forbid_extra_keys set) """ a, _ = attr_and_vals cl = make_class("HypClass", {"a": a}) converter = Converter(forbid_extra_keys=True) inst = cl() unstructured = converter.unstructure(inst) unstructured["aa"] = unstructured.pop("a") with pytest.raises(Exception): converter.structure(unstructured, cl)
def _structure(data: dict[str, Any], cls: Type[Layer], converter: GenConverter) -> Layer: return cls( name=data.get("name", DEFAULT_LAYER_NAME), glyphs={ k: converter.structure(v, Glyph) for k, v in data.get("glyphs", {}).items() }, color=data.get("color"), lib=converter.structure(data.get("lib", {}), Lib), default=data.get("default", False), )
def test_forbid_extra_keys(cls_and_vals): """ Restructuring fails when extra keys are present (when configured) """ converter = Converter(forbid_extra_keys=True) cl, vals = cls_and_vals inst = cl(*vals) unstructured = converter.unstructure(inst) bad_key = list(unstructured)[0] + "A" if unstructured else "Hyp" while bad_key in unstructured: bad_key += "A" unstructured[bad_key] = 1 with pytest.raises(Exception): converter.structure(unstructured, cl)
def test_unstructure_linecache(): """Linecaching for unstructuring should work.""" @define class Inner: a: int @define class Outer: inner: Inner c = GenConverter() try: c.unstructure(Outer({})) except AttributeError: res = format_exc() assert "'a'" in res
def _unstructure_data(value: Any, converter: GenConverter) -> Any: if isinstance(value, bytes): return {"type": DATA_LIB_KEY, "data": converter.unstructure(value)} elif isinstance(value, (list, tuple)): return [_unstructure_data(v, converter) for v in value] elif isinstance(value, Mapping): return {k: _unstructure_data(v, converter) for k, v in value.items()} return value
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_optional_field_roundtrip(cl_and_vals): """ Classes with optional fields can be unstructured and structured. """ converter = Converter() cl, vals = cl_and_vals @attr.s class C(object): a = attr.ib(type=Optional[cl]) inst = C(a=cl(*vals)) assert inst == converter.structure(converter.unstructure(inst), C) inst = C(a=None) unstructured = converter.unstructure(inst) assert inst == converter.structure(unstructured, C)
def test_type_overrides(cl_and_vals): """ Type overrides on the GenConverter work. """ converter = Converter(type_overrides={int: override(omit_if_default=True)}) cl, vals = cl_and_vals inst = cl(*vals) unstructured = converter.unstructure(inst) for field, val in zip(fields(cl), vals): if field.type is int: if field.default is not None: if isinstance(field.default, Factory): if not field.default.takes_self and field.default() == val: assert field.name not in unstructured elif field.default == val: assert field.name not in unstructured
def _unstructure(self, converter: GenConverter) -> dict[str, Any]: # avoid encoding if converter supports bytes natively test = converter.unstructure(b"\0") if isinstance(test, bytes): return dict(self) elif not isinstance(test, str): raise NotImplementedError(type(test)) data: dict[str, Any] = _unstructure_data(self, converter) return data
def _unstructure(self, converter: GenConverter) -> dict[str, str]: # avoid encoding if converter supports bytes natively test = converter.unstructure(b"\0") if isinstance(test, bytes): # mypy complains that 'Argument 1 to "dict" has incompatible type # "DataStore"; expected "SupportsKeysAndGetItem[str, Dict[str, str]]"'. # We _are_ a subclass of Mapping so we do support keys and getitem... return dict(self) # type: ignore elif not isinstance(test, str): raise NotImplementedError(type(test)) data: dict[str, str] = { k: converter.unstructure(v) for k, v in self.items() } # since we unpacked all data by now, we're no longer lazy if self._lazy: self._lazy = False return data
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 _structure_data_inplace(key: Union[int, str], value: Any, container: Any, converter: GenConverter) -> None: if isinstance(value, list): for i, v in enumerate(value): _structure_data_inplace(i, v, value, converter) elif is_data_dict(value): container[key] = converter.structure(value["data"], bytes) elif isinstance(value, Mapping): for k, v in value.items(): _structure_data_inplace(k, v, value, converter)
def test_39_structure_generics_with_cols(t, result): @define class GenericCols(Generic[T]): a: T b: list[T] c: dict[str, T] expected = GenericCols(*result) res = GenConverter().structure(asdict(expected), GenericCols[t]) assert res == expected
def test_omit_default_roundtrip(cl_and_vals): """ Omit default on the converter works. """ converter = Converter(omit_if_default=True) cl, vals = cl_and_vals @attr.s class C(object): a: int = attr.ib(default=1) b: cl = attr.ib(factory=lambda: cl(*vals)) inst = C() unstructured = converter.unstructure(inst) assert unstructured == {} assert inst == converter.structure(unstructured, C) inst = C(0) unstructured = converter.unstructure(inst) assert unstructured == {"a": 0} assert inst == converter.structure(unstructured, C)
def test_unstructure_generic_attrs(): c = GenConverter() @attrs(auto_attribs=True) class Inner(Generic[T]): a: T @attrs(auto_attribs=True) class Outer: inner: Inner[int] initial = Outer(Inner(1)) raw = c.unstructure(initial) assert raw == {"inner": {"a": 1}} new = c.structure(raw, Outer) assert initial == new @attrs(auto_attribs=True) class OuterStr: inner: Inner[str] assert c.structure(raw, OuterStr) == OuterStr(Inner("1"))
def _structure( data: Mapping[str, Any], cls: Type[DataStore], converter: GenConverter, ) -> DataStore: self = cls() for k, v in data.items(): if isinstance(v, str): self[k] = converter.structure(v, bytes) elif isinstance(v, bytes): self[k] = v else: raise TypeError( f"Expected (base64) str or bytes, found: {type(v).__name__!r}" ) return self
def test_simple_roundtrip_defaults(cls_and_vals, strat): """ Simple classes with metadata can be unstructured and restructured. """ a, _ = cls_and_vals cl = make_class("HypClass", {"a": a}) converter = Converter(unstruct_strat=strat) inst = cl() assert converter.unstructure(converter.structure( {}, cl)) == converter.unstructure(inst) assert inst == converter.structure(converter.unstructure(inst), cl)
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_overriding_generated_structure(): """Test overriding a generated structure hook works.""" converter = Converter() @attr.define class Inner: a: int @attr.define class Outer: i: Inner inst = Outer(Inner(1)) raw = converter.unstructure(inst) converter.structure(raw, Outer) converter.register_structure_hook(Inner, lambda p, _: Inner(p["a"] + 1)) r = converter.structure(raw, Outer) assert r.i.a == 2
def _unstructure(self, converter: GenConverter) -> dict[str, Any]: # omit glyph name attribute, already used as key glyphs: dict[str, dict[str, Any]] = {} for glyph_name in self._glyphs: g = converter.unstructure(self[glyph_name]) assert glyph_name == g.pop("name") glyphs[glyph_name] = g d: dict[str, Any] = { # never omit name even if == 'public.default' as that acts as # the layer's "key" in the layerSet. "name": self._name, } default: Any for key, value, default in [ ("default", self._default, self._name == DEFAULT_LAYER_NAME), ("glyphs", glyphs, {}), ("lib", self._lib, {}), ]: if not converter.omit_if_default or value != default: d[key] = value if self.color is not None: d["color"] = self.color return d
def test_collection_unstructure_override_seq(): """Test overriding unstructuring seq.""" # First approach, predicate hook c = GenConverter() c._unstructure_func.register_func_list([( is_sequence, partial(c.gen_unstructure_iterable, unstructure_to=tuple), True, )]) assert c.unstructure([1, 2, 3], unstructure_as=Sequence[int]) == (1, 2, 3) @attr.define class MyList: args = attr.ib(converter=list) # Second approach, using abc.MutableSequence c = GenConverter(unstruct_collection_overrides={MutableSequence: MyList}) assert c.unstructure([1, 2, 3], unstructure_as=Sequence[int]) == [1, 2, 3] assert c.unstructure([1, 2, 3], unstructure_as=MutableSequence[int]) == MyList([ 1, 2, 3, ]) assert c.unstructure([1, 2, 3]) == MyList([ 1, 2, 3, ]) assert c.unstructure((1, 2, 3)) == [ 1, 2, 3, ] # Second approach, using abc.Sequence c = GenConverter(unstruct_collection_overrides={Sequence: MyList}) assert c.unstructure([1, 2, 3], unstructure_as=Sequence[int]) == MyList([1, 2, 3]) assert c.unstructure( [1, 2, 3], unstructure_as=MutableSequence[int]) == MyList([1, 2, 3]) assert c.unstructure([1, 2, 3]) == MyList([1, 2, 3]) assert c.unstructure((1, 2, 3), unstructure_as=tuple[int, ...]) == MyList([ 1, 2, 3, ]) # Second approach, using __builtins__.list c = GenConverter(unstruct_collection_overrides={list: MyList}) assert c.unstructure([1, 2, 3], unstructure_as=Sequence[int]) == [1, 2, 3] assert c.unstructure([1, 2, 3], unstructure_as=MutableSequence[int]) == [ 1, 2, 3, ] assert c.unstructure([1, 2, 3]) == MyList([ 1, 2, 3, ]) assert c.unstructure((1, 2, 3)) == [ 1, 2, 3, ] # Second approach, using __builtins__.tuple c = GenConverter(unstruct_collection_overrides={tuple: MyList}) assert c.unstructure([1, 2, 3], unstructure_as=Sequence[int]) == [1, 2, 3] assert c.unstructure([1, 2, 3], unstructure_as=MutableSequence[int]) == [ 1, 2, 3, ] assert c.unstructure([1, 2, 3]) == [ 1, 2, 3, ] assert c.unstructure((1, 2, 3)) == MyList([ 1, 2, 3, ])
def _structure(data: list[dict[str, Any]], cls: Type[LayerSet], converter: GenConverter) -> LayerSet: return cls.from_iterable( converter.structure(layer, Layer) for layer in data)
def _unstructure(self, converter: GenConverter) -> list[dict[str, Any]]: return [converter.unstructure(layer) for layer in self]