|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
5 | 5 | from sys import version_info |
6 | | -from typing import TYPE_CHECKING, Any, Iterable, NamedTuple, Tuple, TypeVar |
| 6 | +from typing import ( |
| 7 | + TYPE_CHECKING, |
| 8 | + Any, |
| 9 | + Iterable, |
| 10 | + Literal, |
| 11 | + NamedTuple, |
| 12 | + Tuple, |
| 13 | + TypeVar, |
| 14 | + get_type_hints, |
| 15 | +) |
| 16 | + |
| 17 | +from attrs import NOTHING, Attribute |
7 | 18 |
|
8 | 19 | from ._compat import ANIES, is_bare, is_frozenset, is_sequence, is_subclass |
9 | 20 | from ._compat import is_mutable_set as is_set |
10 | 21 | from .dispatch import StructureHook, UnstructureHook |
11 | 22 | from .errors import IterableValidationError, IterableValidationNote |
12 | 23 | from .fns import identity |
13 | | -from .gen import make_hetero_tuple_unstructure_fn |
| 24 | +from .gen import ( |
| 25 | + AttributeOverride, |
| 26 | + already_generating, |
| 27 | + make_dict_structure_fn_from_attrs, |
| 28 | + make_dict_unstructure_fn_from_attrs, |
| 29 | + make_hetero_tuple_unstructure_fn, |
| 30 | +) |
| 31 | +from .gen import make_iterable_unstructure_fn as iterable_unstructure_factory |
14 | 32 |
|
15 | 33 | if TYPE_CHECKING: |
16 | 34 | from .converters import BaseConverter |
|
25 | 43 | "list_structure_factory", |
26 | 44 | "namedtuple_structure_factory", |
27 | 45 | "namedtuple_unstructure_factory", |
| 46 | + "namedtuple_dict_structure_factory", |
| 47 | + "namedtuple_dict_unstructure_factory", |
28 | 48 | ] |
29 | 49 |
|
30 | 50 |
|
@@ -133,57 +153,134 @@ def structure_list( |
133 | 153 | return structure_list |
134 | 154 |
|
135 | 155 |
|
136 | | -def iterable_unstructure_factory( |
137 | | - cl: Any, converter: BaseConverter, unstructure_to: Any = None |
138 | | -) -> UnstructureHook: |
139 | | - """A hook factory for unstructuring iterables. |
140 | | -
|
141 | | - :param unstructure_to: Force unstructuring to this type, if provided. |
142 | | - """ |
143 | | - handler = converter.unstructure |
144 | | - |
145 | | - # Let's try fishing out the type args |
146 | | - # Unspecified tuples have `__args__` as empty tuples, so guard |
147 | | - # against IndexError. |
148 | | - if getattr(cl, "__args__", None) not in (None, ()): |
149 | | - type_arg = cl.__args__[0] |
150 | | - if isinstance(type_arg, TypeVar): |
151 | | - type_arg = getattr(type_arg, "__default__", Any) |
152 | | - handler = converter.get_unstructure_hook(type_arg, cache_result=False) |
153 | | - if handler == identity: |
154 | | - # Save ourselves the trouble of iterating over it all. |
155 | | - return unstructure_to or cl |
156 | | - |
157 | | - def unstructure_iterable(iterable, _seq_cl=unstructure_to or cl, _hook=handler): |
158 | | - return _seq_cl(_hook(i) for i in iterable) |
159 | | - |
160 | | - return unstructure_iterable |
161 | | - |
162 | | - |
163 | 156 | def namedtuple_unstructure_factory( |
164 | | - type: type[tuple], converter: BaseConverter, unstructure_to: Any = None |
| 157 | + cl: type[tuple], converter: BaseConverter, unstructure_to: Any = None |
165 | 158 | ) -> UnstructureHook: |
166 | 159 | """A hook factory for unstructuring namedtuples. |
167 | 160 |
|
168 | 161 | :param unstructure_to: Force unstructuring to this type, if provided. |
169 | 162 | """ |
170 | 163 |
|
171 | | - if unstructure_to is None and _is_passthrough(type, converter): |
| 164 | + if unstructure_to is None and _is_passthrough(cl, converter): |
172 | 165 | return identity |
173 | 166 |
|
174 | 167 | return make_hetero_tuple_unstructure_fn( |
175 | | - type, |
| 168 | + cl, |
176 | 169 | converter, |
177 | 170 | unstructure_to=tuple if unstructure_to is None else unstructure_to, |
178 | | - type_args=tuple(type.__annotations__.values()), |
| 171 | + type_args=tuple(cl.__annotations__.values()), |
179 | 172 | ) |
180 | 173 |
|
181 | 174 |
|
182 | 175 | def namedtuple_structure_factory( |
183 | | - type: type[tuple], converter: BaseConverter |
| 176 | + cl: type[tuple], converter: BaseConverter |
184 | 177 | ) -> StructureHook: |
185 | | - """A hook factory for structuring namedtuples.""" |
| 178 | + """A hook factory for structuring namedtuples from iterables.""" |
186 | 179 | # We delegate to the existing infrastructure for heterogenous tuples. |
187 | | - hetero_tuple_type = Tuple[tuple(type.__annotations__.values())] |
| 180 | + hetero_tuple_type = Tuple[tuple(cl.__annotations__.values())] |
188 | 181 | base_hook = converter.get_structure_hook(hetero_tuple_type) |
189 | | - return lambda v, _: type(*base_hook(v, hetero_tuple_type)) |
| 182 | + return lambda v, _: cl(*base_hook(v, hetero_tuple_type)) |
| 183 | + |
| 184 | + |
| 185 | +def _namedtuple_to_attrs(cl: type[tuple]) -> list[Attribute]: |
| 186 | + """Generate pseudo attributes for a namedtuple.""" |
| 187 | + return [ |
| 188 | + Attribute( |
| 189 | + name, |
| 190 | + cl._field_defaults.get(name, NOTHING), |
| 191 | + None, |
| 192 | + False, |
| 193 | + False, |
| 194 | + False, |
| 195 | + True, |
| 196 | + False, |
| 197 | + type=a, |
| 198 | + alias=name, |
| 199 | + ) |
| 200 | + for name, a in get_type_hints(cl).items() |
| 201 | + ] |
| 202 | + |
| 203 | + |
| 204 | +def namedtuple_dict_structure_factory( |
| 205 | + cl: type[tuple], |
| 206 | + converter: BaseConverter, |
| 207 | + detailed_validation: bool | Literal["from_converter"] = "from_converter", |
| 208 | + forbid_extra_keys: bool = False, |
| 209 | + use_linecache: bool = True, |
| 210 | + /, |
| 211 | + **kwargs: AttributeOverride, |
| 212 | +) -> StructureHook: |
| 213 | + """A hook factory for hooks structuring namedtuples from dictionaries. |
| 214 | +
|
| 215 | + :param forbid_extra_keys: Whether the hook should raise a `ForbiddenExtraKeysError` |
| 216 | + if unknown keys are encountered. |
| 217 | + :param use_linecache: Whether to store the source code in the Python linecache. |
| 218 | +
|
| 219 | + .. versionadded:: 24.1.0 |
| 220 | + """ |
| 221 | + try: |
| 222 | + working_set = already_generating.working_set |
| 223 | + except AttributeError: |
| 224 | + working_set = set() |
| 225 | + already_generating.working_set = working_set |
| 226 | + else: |
| 227 | + if cl in working_set: |
| 228 | + raise RecursionError() |
| 229 | + |
| 230 | + working_set.add(cl) |
| 231 | + |
| 232 | + try: |
| 233 | + return make_dict_structure_fn_from_attrs( |
| 234 | + _namedtuple_to_attrs(cl), |
| 235 | + cl, |
| 236 | + converter, |
| 237 | + _cattrs_forbid_extra_keys=forbid_extra_keys, |
| 238 | + _cattrs_use_detailed_validation=detailed_validation, |
| 239 | + _cattrs_use_linecache=use_linecache, |
| 240 | + **kwargs, |
| 241 | + ) |
| 242 | + finally: |
| 243 | + working_set.remove(cl) |
| 244 | + if not working_set: |
| 245 | + del already_generating.working_set |
| 246 | + |
| 247 | + |
| 248 | +def namedtuple_dict_unstructure_factory( |
| 249 | + cl: type[tuple], |
| 250 | + converter: BaseConverter, |
| 251 | + omit_if_default: bool = False, |
| 252 | + use_linecache: bool = True, |
| 253 | + /, |
| 254 | + **kwargs: AttributeOverride, |
| 255 | +) -> UnstructureHook: |
| 256 | + """A hook factory for hooks unstructuring namedtuples to dictionaries. |
| 257 | +
|
| 258 | + :param omit_if_default: When true, attributes equal to their default values |
| 259 | + will be omitted in the result dictionary. |
| 260 | + :param use_linecache: Whether to store the source code in the Python linecache. |
| 261 | +
|
| 262 | + .. versionadded:: 24.1.0 |
| 263 | + """ |
| 264 | + try: |
| 265 | + working_set = already_generating.working_set |
| 266 | + except AttributeError: |
| 267 | + working_set = set() |
| 268 | + already_generating.working_set = working_set |
| 269 | + if cl in working_set: |
| 270 | + raise RecursionError() |
| 271 | + |
| 272 | + working_set.add(cl) |
| 273 | + |
| 274 | + try: |
| 275 | + return make_dict_unstructure_fn_from_attrs( |
| 276 | + _namedtuple_to_attrs(cl), |
| 277 | + cl, |
| 278 | + converter, |
| 279 | + _cattrs_omit_if_default=omit_if_default, |
| 280 | + _cattrs_use_linecache=use_linecache, |
| 281 | + **kwargs, |
| 282 | + ) |
| 283 | + finally: |
| 284 | + working_set.remove(cl) |
| 285 | + if not working_set: |
| 286 | + del already_generating.working_set |
0 commit comments