-
Notifications
You must be signed in to change notification settings - Fork 13
/
converters.py
155 lines (123 loc) · 5.24 KB
/
converters.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
from __future__ import annotations
import sys
from functools import partial
from typing import Any, Callable, Tuple, Type, cast
from attrs import fields, has, resolve_types
from cattrs import Converter
from cattrs.gen import (
AttributeOverride,
make_dict_structure_fn,
make_dict_unstructure_fn,
override,
)
from fontTools.misc.transform import Transform
is_py37 = sys.version_info[:2] == (3, 7)
if is_py37:
def get_origin(cl: Type[Any]) -> Any:
return getattr(cl, "__origin__", None)
else:
from typing import get_origin # type: ignore
__all__ = [
"register_hooks",
"structure",
"unstructure",
]
def is_ufoLib2_class(cls: Type[Any]) -> bool:
mod: str = getattr(cls, "__module__", "")
return mod.split(".")[0] == "ufoLib2"
def is_ufoLib2_attrs_class(cls: Type[Any]) -> bool:
return is_ufoLib2_class(cls) and (has(cls) or has(get_origin(cls)))
def is_ufoLib2_class_with_custom_unstructure(cls: Type[Any]) -> bool:
return is_ufoLib2_class(cls) and hasattr(cls, "_unstructure")
def is_ufoLib2_class_with_custom_structure(cls: Type[Any]) -> bool:
return is_ufoLib2_class(cls) and hasattr(cls, "_structure")
def register_hooks(conv: Converter, allow_bytes: bool = True) -> None:
def attrs_hook_factory(
cls: Type[Any], gen_fn: Callable[..., Callable[..., Any]], structuring: bool
) -> Callable[..., 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] = {
"_cattrs_detailed_validation": conv.detailed_validation
}
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 = Converter(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], 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)
default_converter = Converter(
omit_if_default=True,
forbid_extra_keys=True,
prefer_attrib_converters=False,
)
register_hooks(default_converter, allow_bytes=False)
structure = default_converter.structure
unstructure = default_converter.unstructure
# same as default_converter but allows bytes
binary_converter = Converter(
omit_if_default=True,
forbid_extra_keys=True,
prefer_attrib_converters=False,
)
register_hooks(binary_converter, allow_bytes=True)