|
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