1from __future__ import annotations
2
3import re
4from collections.abc import Callable, Iterable, Mapping
5from typing import TYPE_CHECKING, Any, Final, Literal, TypeVar
6
7from attrs import NOTHING, Attribute, Factory
8from typing_extensions import NoDefault
9
10from .._compat import (
11 ANIES,
12 TypeAlias,
13 adapted_fields,
14 get_args,
15 get_origin,
16 is_annotated,
17 is_bare,
18 is_bare_final,
19 is_generic,
20)
21from .._generics import deep_copy_with
22from ..dispatch import UnstructureHook
23from ..errors import (
24 AttributeValidationNote,
25 ClassValidationError,
26 ForbiddenExtraKeysError,
27 IterableValidationError,
28 IterableValidationNote,
29 StructureHandlerNotFoundError,
30)
31from ..fns import identity
32from ..types import SimpleStructureHook
33from ._consts import AttributeOverride, already_generating, neutral
34from ._generics import generate_mapping
35from ._lc import generate_unique_filename
36from ._shared import find_structure_handler
37
38if TYPE_CHECKING:
39 from ..converters import BaseConverter
40
41__all__ = [
42 "make_dict_structure_fn",
43 "make_dict_structure_fn_from_attrs",
44 "make_dict_unstructure_fn",
45 "make_dict_unstructure_fn_from_attrs",
46 "make_hetero_tuple_unstructure_fn",
47 "make_iterable_unstructure_fn",
48 "make_mapping_structure_fn",
49 "make_mapping_unstructure_fn",
50]
51
52
53def override(
54 omit_if_default: bool | None = None,
55 rename: str | None = None,
56 omit: bool | None = None,
57 struct_hook: Callable[[Any, Any], Any] | None = None,
58 unstruct_hook: Callable[[Any], Any] | None = None,
59) -> AttributeOverride:
60 """Override how a particular field is handled.
61
62 :param omit: Whether to skip the field or not. `None` means apply default handling.
63 """
64 return AttributeOverride(omit_if_default, rename, omit, struct_hook, unstruct_hook)
65
66
67T = TypeVar("T")
68
69
70def make_dict_unstructure_fn_from_attrs(
71 attrs: list[Attribute],
72 cl: type[T],
73 converter: BaseConverter,
74 typevar_map: dict[str, Any] = {},
75 _cattrs_omit_if_default: bool = False,
76 _cattrs_use_linecache: bool = True,
77 _cattrs_use_alias: bool = False,
78 _cattrs_include_init_false: bool = False,
79 **kwargs: AttributeOverride,
80) -> Callable[[T], dict[str, Any]]:
81 """
82 Generate a specialized dict unstructuring function for a list of attributes.
83
84 Usually used as a building block by more specialized hook factories.
85
86 Any provided overrides are attached to the generated function under the
87 `overrides` attribute.
88
89 :param cl: The class for which the function is generated; used mostly for its name,
90 module name and qualname.
91 :param _cattrs_omit_if_default: if true, attributes equal to their default values
92 will be omitted in the result dictionary.
93 :param _cattrs_use_alias: If true, the attribute alias will be used as the
94 dictionary key by default.
95 :param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False`
96 will be included.
97
98 .. versionadded:: 24.1.0
99 """
100
101 fn_name = "unstructure_" + cl.__name__
102 globs = {}
103 lines = []
104 invocation_lines = []
105 internal_arg_parts = {}
106
107 for a in attrs:
108 attr_name = a.name
109 override = kwargs.get(attr_name, neutral)
110 if override.omit:
111 continue
112 if override.omit is None and not a.init and not _cattrs_include_init_false:
113 continue
114 if override.rename is None:
115 kn = attr_name if not _cattrs_use_alias else a.alias
116 else:
117 kn = override.rename
118 d = a.default
119
120 # For each attribute, we try resolving the type here and now.
121 # If a type is manually overwritten, this function should be
122 # regenerated.
123 handler = None
124 if override.unstruct_hook is not None:
125 handler = override.unstruct_hook
126 else:
127 if a.type is not None:
128 t = a.type
129 if isinstance(t, TypeVar):
130 if t.__name__ in typevar_map:
131 t = typevar_map[t.__name__]
132 else:
133 handler = converter.unstructure
134 elif is_generic(t) and not is_bare(t) and not is_annotated(t):
135 t = deep_copy_with(t, typevar_map, cl)
136
137 if handler is None:
138 if (
139 is_bare_final(t)
140 and a.default is not NOTHING
141 and not isinstance(a.default, Factory)
142 ):
143 # This is a special case where we can use the
144 # type of the default to dispatch on.
145 t = a.default.__class__
146 try:
147 handler = converter.get_unstructure_hook(t, cache_result=False)
148 except RecursionError:
149 # There's a circular reference somewhere down the line
150 handler = converter.unstructure
151 else:
152 handler = converter.unstructure
153
154 is_identity = handler == identity
155
156 if not is_identity:
157 unstruct_handler_name = f"__c_unstr_{attr_name}"
158 globs[unstruct_handler_name] = handler
159 internal_arg_parts[unstruct_handler_name] = handler
160 invoke = f"{unstruct_handler_name}(instance.{attr_name})"
161 else:
162 invoke = f"instance.{attr_name}"
163
164 if d is not NOTHING and (
165 (_cattrs_omit_if_default and override.omit_if_default is not False)
166 or override.omit_if_default
167 ):
168 def_name = f"__c_def_{attr_name}"
169
170 if isinstance(d, Factory):
171 globs[def_name] = d.factory
172 internal_arg_parts[def_name] = d.factory
173 if d.takes_self:
174 lines.append(f" if instance.{attr_name} != {def_name}(instance):")
175 else:
176 lines.append(f" if instance.{attr_name} != {def_name}():")
177 lines.append(f" res['{kn}'] = {invoke}")
178 else:
179 globs[def_name] = d
180 internal_arg_parts[def_name] = d
181 lines.append(f" if instance.{attr_name} != {def_name}:")
182 lines.append(f" res['{kn}'] = {invoke}")
183
184 else:
185 # No default or no override.
186 invocation_lines.append(f"'{kn}': {invoke},")
187
188 internal_arg_line = ", ".join([f"{i}={i}" for i in internal_arg_parts])
189 if internal_arg_line:
190 internal_arg_line = f", {internal_arg_line}"
191 for k, v in internal_arg_parts.items():
192 globs[k] = v
193
194 total_lines = (
195 [f"def {fn_name}(instance{internal_arg_line}):"]
196 + [" res = {"]
197 + [f" {line}" for line in invocation_lines]
198 + [" }"]
199 + lines
200 + [" return res"]
201 )
202 script = "\n".join(total_lines)
203 fname = generate_unique_filename(
204 cl, "unstructure", lines=total_lines if _cattrs_use_linecache else []
205 )
206
207 eval(compile(script, fname, "exec"), globs)
208
209 res = globs[fn_name]
210 res.overrides = kwargs
211
212 return res
213
214
215def make_dict_unstructure_fn(
216 cl: type[T],
217 converter: BaseConverter,
218 _cattrs_omit_if_default: bool = False,
219 _cattrs_use_linecache: bool = True,
220 _cattrs_use_alias: bool = False,
221 _cattrs_include_init_false: bool = False,
222 **kwargs: AttributeOverride,
223) -> Callable[[T], dict[str, Any]]:
224 """
225 Generate a specialized dict unstructuring function for an attrs class or a
226 dataclass.
227
228 Any provided overrides are attached to the generated function under the
229 `overrides` attribute.
230
231 :param _cattrs_omit_if_default: if true, attributes equal to their default values
232 will be omitted in the result dictionary.
233 :param _cattrs_use_alias: If true, the attribute alias will be used as the
234 dictionary key by default.
235 :param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False`
236 will be included.
237
238 .. versionadded:: 23.2.0 *_cattrs_use_alias*
239 .. versionadded:: 23.2.0 *_cattrs_include_init_false*
240 """
241 origin = get_origin(cl)
242 attrs = adapted_fields(origin or cl) # type: ignore
243
244 mapping = {}
245 if is_generic(cl):
246 mapping = generate_mapping(cl, mapping)
247
248 if origin is not None:
249 cl = origin
250
251 # We keep track of what we're generating to help with recursive
252 # class graphs.
253 try:
254 working_set = already_generating.working_set
255 except AttributeError:
256 working_set = set()
257 already_generating.working_set = working_set
258 if cl in working_set:
259 raise RecursionError()
260
261 working_set.add(cl)
262
263 try:
264 return make_dict_unstructure_fn_from_attrs(
265 attrs,
266 cl,
267 converter,
268 mapping,
269 _cattrs_omit_if_default=_cattrs_omit_if_default,
270 _cattrs_use_linecache=_cattrs_use_linecache,
271 _cattrs_use_alias=_cattrs_use_alias,
272 _cattrs_include_init_false=_cattrs_include_init_false,
273 **kwargs,
274 )
275 finally:
276 working_set.remove(cl)
277 if not working_set:
278 del already_generating.working_set
279
280
281def make_dict_structure_fn_from_attrs(
282 attrs: list[Attribute],
283 cl: type[T],
284 converter: BaseConverter,
285 typevar_map: dict[str, Any] = {},
286 _cattrs_forbid_extra_keys: bool | Literal["from_converter"] = "from_converter",
287 _cattrs_use_linecache: bool = True,
288 _cattrs_prefer_attrib_converters: (
289 bool | Literal["from_converter"]
290 ) = "from_converter",
291 _cattrs_detailed_validation: bool | Literal["from_converter"] = "from_converter",
292 _cattrs_use_alias: bool = False,
293 _cattrs_include_init_false: bool = False,
294 **kwargs: AttributeOverride,
295) -> SimpleStructureHook[Mapping[str, Any], T]:
296 """
297 Generate a specialized dict structuring function for a list of attributes.
298
299 Usually used as a building block by more specialized hook factories.
300
301 Any provided overrides are attached to the generated function under the
302 `overrides` attribute.
303
304 :param _cattrs_forbid_extra_keys: Whether the structuring function should raise a
305 `ForbiddenExtraKeysError` if unknown keys are encountered.
306 :param _cattrs_use_linecache: Whether to store the source code in the Python
307 linecache.
308 :param _cattrs_prefer_attrib_converters: If an _attrs_ converter is present on a
309 field, use it instead of processing the field normally.
310 :param _cattrs_detailed_validation: Whether to use a slower mode that produces
311 more detailed errors.
312 :param _cattrs_use_alias: If true, the attribute alias will be used as the
313 dictionary key by default.
314 :param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False`
315 will be included.
316
317 .. versionadded:: 24.1.0
318 """
319
320 cl_name = cl.__name__
321 fn_name = "structure_" + cl_name
322
323 # We have generic parameters and need to generate a unique name for the function
324 for p in getattr(cl, "__parameters__", ()):
325 # This is nasty, I am not sure how best to handle `typing.List[str]` or
326 # `TClass[int, int]` as a parameter type here
327 try:
328 name_base = typevar_map[p.__name__]
329 except KeyError:
330 pn = p.__name__
331 raise StructureHandlerNotFoundError(
332 f"Missing type for generic argument {pn}, specify it when structuring.",
333 p,
334 ) from None
335 name = getattr(name_base, "__name__", None) or str(name_base)
336 # `<>` can be present in lambdas
337 # `|` can be present in unions
338 name = re.sub(r"[\[\.\] ,<>]", "_", name)
339 name = re.sub(r"\|", "u", name)
340 fn_name += f"_{name}"
341
342 internal_arg_parts = {"__cl": cl}
343 globs = {}
344 lines = []
345 post_lines = []
346 pi_lines = [] # post instantiation lines
347 invocation_lines = []
348
349 allowed_fields = set()
350 if _cattrs_forbid_extra_keys == "from_converter":
351 # BaseConverter doesn't have it so we're careful.
352 _cattrs_forbid_extra_keys = getattr(converter, "forbid_extra_keys", False)
353 if _cattrs_detailed_validation == "from_converter":
354 _cattrs_detailed_validation = converter.detailed_validation
355 if _cattrs_prefer_attrib_converters == "from_converter":
356 _cattrs_prefer_attrib_converters = converter._prefer_attrib_converters
357
358 if _cattrs_forbid_extra_keys:
359 globs["__c_a"] = allowed_fields
360 globs["__c_feke"] = ForbiddenExtraKeysError
361
362 if _cattrs_detailed_validation:
363 lines.append(" res = {}")
364 lines.append(" errors = []")
365 invocation_lines.append("**res,")
366 internal_arg_parts["__c_cve"] = ClassValidationError
367 internal_arg_parts["__c_avn"] = AttributeValidationNote
368 for a in attrs:
369 an = a.name
370 override = kwargs.get(an, neutral)
371 if override.omit:
372 continue
373 if override.omit is None and not a.init and not _cattrs_include_init_false:
374 continue
375 t = a.type
376 if isinstance(t, TypeVar):
377 t = typevar_map.get(t.__name__, t)
378 elif is_generic(t) and not is_bare(t) and not is_annotated(t):
379 t = deep_copy_with(t, typevar_map, cl)
380
381 # For each attribute, we try resolving the type here and now.
382 # If a type is manually overwritten, this function should be
383 # regenerated.
384 if override.struct_hook is not None:
385 # If the user has requested an override, just use that.
386 handler = override.struct_hook
387 else:
388 handler = find_structure_handler(
389 a, t, converter, _cattrs_prefer_attrib_converters
390 )
391
392 struct_handler_name = f"__c_structure_{an}"
393 if handler is not None:
394 internal_arg_parts[struct_handler_name] = handler
395
396 ian = a.alias
397 if override.rename is None:
398 kn = an if not _cattrs_use_alias else a.alias
399 else:
400 kn = override.rename
401
402 allowed_fields.add(kn)
403 i = " "
404
405 if not a.init:
406 if a.default is not NOTHING:
407 pi_lines.append(f"{i}if '{kn}' in o:")
408 i = f"{i} "
409 pi_lines.append(f"{i}try:")
410 i = f"{i} "
411 type_name = f"__c_type_{an}"
412 internal_arg_parts[type_name] = t
413 if handler is not None:
414 if handler == converter._structure_call:
415 internal_arg_parts[struct_handler_name] = t
416 pi_lines.append(
417 f"{i}instance.{an} = {struct_handler_name}(o['{kn}'])"
418 )
419 else:
420 tn = f"__c_type_{an}"
421 internal_arg_parts[tn] = t
422 pi_lines.append(
423 f"{i}instance.{an} = {struct_handler_name}(o['{kn}'], {tn})"
424 )
425 else:
426 pi_lines.append(f"{i}instance.{an} = o['{kn}']")
427 i = i[:-2]
428 pi_lines.append(f"{i}except Exception as e:")
429 i = f"{i} "
430 pi_lines.append(
431 f'{i}e.__notes__ = getattr(e, \'__notes__\', []) + [__c_avn("Structuring class {cl.__qualname__} @ attribute {an}", "{an}", __c_type_{an})]'
432 )
433 pi_lines.append(f"{i}errors.append(e)")
434
435 else:
436 if a.default is not NOTHING:
437 lines.append(f"{i}if '{kn}' in o:")
438 i = f"{i} "
439 lines.append(f"{i}try:")
440 i = f"{i} "
441 type_name = f"__c_type_{an}"
442 internal_arg_parts[type_name] = t
443 if handler:
444 if handler == converter._structure_call:
445 internal_arg_parts[struct_handler_name] = t
446 lines.append(
447 f"{i}res['{ian}'] = {struct_handler_name}(o['{kn}'])"
448 )
449 else:
450 lines.append(
451 f"{i}res['{ian}'] = {struct_handler_name}(o['{kn}'], {type_name})"
452 )
453 else:
454 lines.append(f"{i}res['{ian}'] = o['{kn}']")
455 i = i[:-2]
456 lines.append(f"{i}except Exception as e:")
457 i = f"{i} "
458 lines.append(
459 f'{i}e.__notes__ = getattr(e, \'__notes__\', []) + [__c_avn("Structuring class {cl.__qualname__} @ attribute {an}", "{an}", __c_type_{an})]'
460 )
461 lines.append(f"{i}errors.append(e)")
462
463 if _cattrs_forbid_extra_keys:
464 post_lines += [
465 " unknown_fields = set(o.keys()) - __c_a",
466 " if unknown_fields:",
467 " errors.append(__c_feke('', __cl, unknown_fields))",
468 ]
469
470 post_lines.append(
471 f" if errors: raise __c_cve('While structuring ' + {cl_name!r}, errors, __cl)"
472 )
473 if not pi_lines:
474 instantiation_lines = (
475 [" try:"]
476 + [" return __cl("]
477 + [f" {line}" for line in invocation_lines]
478 + [" )"]
479 + [
480 f" except Exception as exc: raise __c_cve('While structuring ' + {cl_name!r}, [exc], __cl)"
481 ]
482 )
483 else:
484 instantiation_lines = (
485 [" try:"]
486 + [" instance = __cl("]
487 + [f" {line}" for line in invocation_lines]
488 + [" )"]
489 + [
490 f" except Exception as exc: raise __c_cve('While structuring ' + {cl_name!r}, [exc], __cl)"
491 ]
492 )
493 pi_lines.append(" return instance")
494 else:
495 non_required = []
496 # The first loop deals with required args.
497 for a in attrs:
498 an = a.name
499 override = kwargs.get(an, neutral)
500 if override.omit:
501 continue
502 if override.omit is None and not a.init and not _cattrs_include_init_false:
503 continue
504 if a.default is not NOTHING:
505 non_required.append(a)
506 continue
507 t = a.type
508 if isinstance(t, TypeVar):
509 t = typevar_map.get(t.__name__, t)
510 elif is_generic(t) and not is_bare(t) and not is_annotated(t):
511 t = deep_copy_with(t, typevar_map, cl)
512
513 # For each attribute, we try resolving the type here and now.
514 # If a type is manually overwritten, this function should be
515 # regenerated.
516 if override.struct_hook is not None:
517 # If the user has requested an override, just use that.
518 handler = override.struct_hook
519 else:
520 handler = find_structure_handler(
521 a, t, converter, _cattrs_prefer_attrib_converters
522 )
523
524 if override.rename is None:
525 kn = an if not _cattrs_use_alias else a.alias
526 else:
527 kn = override.rename
528 allowed_fields.add(kn)
529
530 if not a.init:
531 if handler is not None:
532 struct_handler_name = f"__c_structure_{an}"
533 internal_arg_parts[struct_handler_name] = handler
534 if handler == converter._structure_call:
535 internal_arg_parts[struct_handler_name] = t
536 pi_line = f" instance.{an} = {struct_handler_name}(o['{kn}'])"
537 else:
538 tn = f"__c_type_{an}"
539 internal_arg_parts[tn] = t
540 pi_line = (
541 f" instance.{an} = {struct_handler_name}(o['{kn}'], {tn})"
542 )
543 else:
544 pi_line = f" instance.{an} = o['{kn}']"
545
546 pi_lines.append(pi_line)
547 else:
548 if handler:
549 struct_handler_name = f"__c_structure_{an}"
550 internal_arg_parts[struct_handler_name] = handler
551 if handler == converter._structure_call:
552 internal_arg_parts[struct_handler_name] = t
553 invocation_line = f"{struct_handler_name}(o['{kn}']),"
554 else:
555 tn = f"__c_type_{an}"
556 internal_arg_parts[tn] = t
557 invocation_line = f"{struct_handler_name}(o['{kn}'], {tn}),"
558 else:
559 invocation_line = f"o['{kn}'],"
560
561 if a.kw_only:
562 invocation_line = f"{a.alias}={invocation_line}"
563 invocation_lines.append(invocation_line)
564
565 # The second loop is for optional args.
566 if non_required:
567 invocation_lines.append("**res,")
568 lines.append(" res = {}")
569
570 for a in non_required:
571 an = a.name
572 override = kwargs.get(an, neutral)
573 t = a.type
574 if isinstance(t, TypeVar):
575 t = typevar_map.get(t.__name__, t)
576 elif is_generic(t) and not is_bare(t) and not is_annotated(t):
577 t = deep_copy_with(t, typevar_map, cl)
578
579 # For each attribute, we try resolving the type here and now.
580 # If a type is manually overwritten, this function should be
581 # regenerated.
582 if override.struct_hook is not None:
583 # If the user has requested an override, just use that.
584 handler = override.struct_hook
585 else:
586 handler = find_structure_handler(
587 a, t, converter, _cattrs_prefer_attrib_converters
588 )
589
590 struct_handler_name = f"__c_structure_{an}"
591 internal_arg_parts[struct_handler_name] = handler
592
593 if override.rename is None:
594 kn = an if not _cattrs_use_alias else a.alias
595 else:
596 kn = override.rename
597 allowed_fields.add(kn)
598 if not a.init:
599 pi_lines.append(f" if '{kn}' in o:")
600 if handler:
601 if handler == converter._structure_call:
602 internal_arg_parts[struct_handler_name] = t
603 pi_lines.append(
604 f" instance.{an} = {struct_handler_name}(o['{kn}'])"
605 )
606 else:
607 tn = f"__c_type_{an}"
608 internal_arg_parts[tn] = t
609 pi_lines.append(
610 f" instance.{an} = {struct_handler_name}(o['{kn}'], {tn})"
611 )
612 else:
613 pi_lines.append(f" instance.{an} = o['{kn}']")
614 else:
615 post_lines.append(f" if '{kn}' in o:")
616 if handler:
617 if handler == converter._structure_call:
618 internal_arg_parts[struct_handler_name] = t
619 post_lines.append(
620 f" res['{a.alias}'] = {struct_handler_name}(o['{kn}'])"
621 )
622 else:
623 tn = f"__c_type_{an}"
624 internal_arg_parts[tn] = t
625 post_lines.append(
626 f" res['{a.alias}'] = {struct_handler_name}(o['{kn}'], {tn})"
627 )
628 else:
629 post_lines.append(f" res['{a.alias}'] = o['{kn}']")
630 if not pi_lines:
631 instantiation_lines = (
632 [" return __cl("]
633 + [f" {line}" for line in invocation_lines]
634 + [" )"]
635 )
636 else:
637 instantiation_lines = (
638 [" instance = __cl("]
639 + [f" {line}" for line in invocation_lines]
640 + [" )"]
641 )
642 pi_lines.append(" return instance")
643
644 if _cattrs_forbid_extra_keys:
645 post_lines += [
646 " unknown_fields = set(o.keys()) - __c_a",
647 " if unknown_fields:",
648 " raise __c_feke('', __cl, unknown_fields)",
649 ]
650
651 # At the end, we create the function header.
652 internal_arg_line = ", ".join([f"{i}={i}" for i in internal_arg_parts])
653 globs.update(internal_arg_parts)
654
655 total_lines = [
656 f"def {fn_name}(o, _=__cl, {internal_arg_line}):",
657 *lines,
658 *post_lines,
659 *instantiation_lines,
660 *pi_lines,
661 ]
662
663 script = "\n".join(total_lines)
664 fname = generate_unique_filename(
665 cl, "structure", lines=total_lines if _cattrs_use_linecache else []
666 )
667
668 eval(compile(script, fname, "exec"), globs)
669
670 res = globs[fn_name]
671 res.overrides = kwargs
672
673 return res
674
675
676def make_dict_structure_fn(
677 cl: type[T],
678 converter: BaseConverter,
679 _cattrs_forbid_extra_keys: bool | Literal["from_converter"] = "from_converter",
680 _cattrs_use_linecache: bool = True,
681 _cattrs_prefer_attrib_converters: (
682 bool | Literal["from_converter"]
683 ) = "from_converter",
684 _cattrs_detailed_validation: bool | Literal["from_converter"] = "from_converter",
685 _cattrs_use_alias: bool = False,
686 _cattrs_include_init_false: bool = False,
687 **kwargs: AttributeOverride,
688) -> SimpleStructureHook[Mapping[str, Any], T]:
689 """
690 Generate a specialized dict structuring function for an attrs class or
691 dataclass.
692
693 Any provided overrides are attached to the generated function under the
694 `overrides` attribute.
695
696 :param _cattrs_forbid_extra_keys: Whether the structuring function should raise a
697 `ForbiddenExtraKeysError` if unknown keys are encountered.
698 :param _cattrs_use_linecache: Whether to store the source code in the Python
699 linecache.
700 :param _cattrs_prefer_attrib_converters: If an _attrs_ converter is present on a
701 field, use it instead of processing the field normally.
702 :param _cattrs_detailed_validation: Whether to use a slower mode that produces
703 more detailed errors.
704 :param _cattrs_use_alias: If true, the attribute alias will be used as the
705 dictionary key by default.
706 :param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False`
707 will be included.
708
709 .. versionadded:: 23.2.0 *_cattrs_use_alias*
710 .. versionadded:: 23.2.0 *_cattrs_include_init_false*
711 .. versionchanged:: 23.2.0
712 The `_cattrs_forbid_extra_keys` and `_cattrs_detailed_validation` parameters
713 take their values from the given converter by default.
714 .. versionchanged:: 24.1.0
715 The `_cattrs_prefer_attrib_converters` parameter takes its value from the given
716 converter by default.
717 """
718
719 mapping = {}
720 if is_generic(cl):
721 base = get_origin(cl)
722 mapping = generate_mapping(cl, mapping)
723 if base is not None:
724 cl = base
725
726 for base in getattr(cl, "__orig_bases__", ()):
727 if is_generic(base) and not str(base).startswith("typing.Generic"):
728 mapping = generate_mapping(base, mapping)
729 break
730
731 attrs = adapted_fields(cl)
732
733 # We keep track of what we're generating to help with recursive
734 # class graphs.
735 try:
736 working_set = already_generating.working_set
737 except AttributeError:
738 working_set = set()
739 already_generating.working_set = working_set
740 else:
741 if cl in working_set:
742 raise RecursionError()
743
744 working_set.add(cl)
745
746 try:
747 return make_dict_structure_fn_from_attrs(
748 attrs,
749 cl,
750 converter,
751 mapping,
752 _cattrs_forbid_extra_keys=_cattrs_forbid_extra_keys,
753 _cattrs_use_linecache=_cattrs_use_linecache,
754 _cattrs_prefer_attrib_converters=_cattrs_prefer_attrib_converters,
755 _cattrs_detailed_validation=_cattrs_detailed_validation,
756 _cattrs_use_alias=_cattrs_use_alias,
757 _cattrs_include_init_false=_cattrs_include_init_false,
758 **kwargs,
759 )
760 finally:
761 working_set.remove(cl)
762 if not working_set:
763 del already_generating.working_set
764
765
766IterableUnstructureFn = Callable[[Iterable[Any]], Any]
767
768
769#: A type alias for heterogeneous tuple unstructure hooks.
770HeteroTupleUnstructureFn: TypeAlias = Callable[[tuple[Any, ...]], Any]
771
772
773def make_hetero_tuple_unstructure_fn(
774 cl: Any,
775 converter: BaseConverter,
776 unstructure_to: Any = None,
777 type_args: tuple | None = None,
778) -> HeteroTupleUnstructureFn:
779 """Generate a specialized unstructure function for a heterogenous tuple.
780
781 :param type_args: If provided, override the type arguments.
782 """
783 fn_name = "unstructure_tuple"
784
785 type_args = get_args(cl) if type_args is None else type_args
786
787 # We can do the dispatch here and now.
788 handlers = [converter.get_unstructure_hook(type_arg) for type_arg in type_args]
789
790 globs = {f"__cattr_u_{i}": h for i, h in enumerate(handlers)}
791 if unstructure_to is not tuple:
792 globs["__cattr_seq_cl"] = unstructure_to or cl
793 lines = []
794
795 lines.append(f"def {fn_name}(tup):")
796 if unstructure_to is not tuple:
797 lines.append(" res = __cattr_seq_cl((")
798 else:
799 lines.append(" res = (")
800 for i in range(len(handlers)):
801 if handlers[i] == identity:
802 lines.append(f" tup[{i}],")
803 else:
804 lines.append(f" __cattr_u_{i}(tup[{i}]),")
805
806 if unstructure_to is not tuple:
807 lines.append(" ))")
808 else:
809 lines.append(" )")
810
811 total_lines = [*lines, " return res"]
812
813 eval(compile("\n".join(total_lines), "", "exec"), globs)
814
815 return globs[fn_name]
816
817
818MappingUnstructureFn = Callable[[Mapping[Any, Any]], Any]
819
820
821# This factory is here for backwards compatibility and circular imports.
822def mapping_unstructure_factory(
823 cl: Any,
824 converter: BaseConverter,
825 unstructure_to: Any = None,
826 key_handler: Callable[[Any, Any | None], Any] | None = None,
827) -> MappingUnstructureFn:
828 """Generate a specialized unstructure function for a mapping.
829
830 :param unstructure_to: The class to unstructure to; defaults to the
831 same class as the mapping being unstructured.
832 """
833 kh = key_handler or converter.unstructure
834 val_handler = converter.unstructure
835
836 fn_name = "unstructure_mapping"
837 origin = cl
838
839 # Let's try fishing out the type args.
840 if getattr(cl, "__args__", None) is not None:
841 args = get_args(cl)
842 if len(args) == 2:
843 key_arg, val_arg = args
844 else:
845 # Probably a Counter
846 key_arg, val_arg = args, Any
847 # We can do the dispatch here and now.
848 kh = key_handler or converter.get_unstructure_hook(key_arg, cache_result=False)
849 if kh == identity:
850 kh = None
851
852 val_handler = converter.get_unstructure_hook(val_arg, cache_result=False)
853 if val_handler == identity:
854 val_handler = None
855
856 origin = get_origin(cl)
857
858 globs = {"__cattr_k_u": kh, "__cattr_v_u": val_handler}
859
860 k_u = "__cattr_k_u(k)" if kh is not None else "k"
861 v_u = "__cattr_v_u(v)" if val_handler is not None else "v"
862
863 lines = [f"def {fn_name}(mapping):"]
864
865 if unstructure_to is dict or (unstructure_to is None and origin is dict):
866 if kh is None and val_handler is None:
867 # Simplest path.
868 return dict
869
870 lines.append(f" return {{{k_u}: {v_u} for k, v in mapping.items()}}")
871 else:
872 globs["__cattr_mapping_cl"] = unstructure_to or cl
873 lines.append(
874 f" res = __cattr_mapping_cl(({k_u}, {v_u}) for k, v in mapping.items())"
875 )
876
877 lines = [*lines, " return res"]
878
879 eval(compile("\n".join(lines), "", "exec"), globs)
880
881 return globs[fn_name]
882
883
884make_mapping_unstructure_fn: Final = mapping_unstructure_factory
885
886
887# This factory is here for backwards compatibility and circular imports.
888def mapping_structure_factory(
889 cl: type[T],
890 converter: BaseConverter,
891 structure_to: type = dict,
892 key_type=NOTHING,
893 val_type=NOTHING,
894 detailed_validation: bool | Literal["from_converter"] = "from_converter",
895) -> SimpleStructureHook[Mapping[Any, Any], T]:
896 """Generate a specialized structure function for a mapping."""
897 fn_name = "structure_mapping"
898
899 if detailed_validation == "from_converter":
900 detailed_validation = converter.detailed_validation
901
902 globs: dict[str, type] = {"__cattr_mapping_cl": structure_to}
903
904 lines = []
905 internal_arg_parts = {}
906
907 # Let's try fishing out the type args.
908 if not is_bare(cl):
909 args = get_args(cl)
910 if len(args) == 2:
911 key_arg_cand, val_arg_cand = args
912 if key_type is NOTHING:
913 key_type = key_arg_cand
914 if val_type is NOTHING:
915 val_type = val_arg_cand
916 else:
917 if key_type is not NOTHING and val_type is NOTHING:
918 (val_type,) = args
919 elif key_type is NOTHING and val_type is not NOTHING:
920 (key_type,) = args
921 else:
922 # Probably a Counter
923 (key_type,) = args
924 val_type = Any
925
926 is_bare_dict = val_type in ANIES and key_type in ANIES
927 if not is_bare_dict:
928 # We can do the dispatch here and now.
929 key_handler = converter.get_structure_hook(key_type, cache_result=False)
930 if key_handler == converter._structure_call:
931 key_handler = key_type
932
933 val_handler = converter.get_structure_hook(val_type, cache_result=False)
934 if val_handler == converter._structure_call:
935 val_handler = val_type
936
937 globs["__cattr_k_t"] = key_type
938 globs["__cattr_v_t"] = val_type
939 globs["__cattr_k_s"] = key_handler
940 globs["__cattr_v_s"] = val_handler
941 k_s = (
942 "__cattr_k_s(k, __cattr_k_t)"
943 if key_handler != key_type
944 else "__cattr_k_s(k)"
945 )
946 v_s = (
947 "__cattr_v_s(v, __cattr_v_t)"
948 if val_handler != val_type
949 else "__cattr_v_s(v)"
950 )
951 else:
952 is_bare_dict = True
953
954 if is_bare_dict:
955 # No args, it's a bare dict.
956 lines.append(" res = dict(mapping)")
957 else:
958 if detailed_validation:
959 internal_arg_parts["IterableValidationError"] = IterableValidationError
960 internal_arg_parts["IterableValidationNote"] = IterableValidationNote
961 internal_arg_parts["val_type"] = (
962 val_type if val_type is not NOTHING else Any
963 )
964 internal_arg_parts["key_type"] = (
965 key_type if key_type is not NOTHING else Any
966 )
967 globs["enumerate"] = enumerate
968
969 lines.append(" res = {}; errors = []")
970 lines.append(" for k, v in mapping.items():")
971 lines.append(" try:")
972 lines.append(f" value = {v_s}")
973 lines.append(" except Exception as e:")
974 lines.append(
975 " e.__notes__ = getattr(e, '__notes__', []) + [IterableValidationNote(f'Structuring mapping value @ key {k!r}', k, val_type)]"
976 )
977 lines.append(" errors.append(e)")
978 lines.append(" continue")
979 lines.append(" try:")
980 lines.append(f" key = {k_s}")
981 lines.append(" res[key] = value")
982 lines.append(" except Exception as e:")
983 lines.append(
984 " e.__notes__ = getattr(e, '__notes__', []) + [IterableValidationNote(f'Structuring mapping key @ key {k!r}', k, key_type)]"
985 )
986 lines.append(" errors.append(e)")
987 lines.append(" if errors:")
988 lines.append(
989 f" raise IterableValidationError('While structuring ' + {repr(cl)!r}, errors, __cattr_mapping_cl)"
990 )
991 else:
992 lines.append(f" res = {{{k_s}: {v_s} for k, v in mapping.items()}}")
993 if structure_to is not dict:
994 lines.append(" res = __cattr_mapping_cl(res)")
995
996 internal_arg_line = ", ".join([f"{i}={i}" for i in internal_arg_parts])
997 if internal_arg_line:
998 internal_arg_line = f", {internal_arg_line}"
999 for k, v in internal_arg_parts.items():
1000 globs[k] = v
1001
1002 globs["cl"] = cl
1003 def_line = f"def {fn_name}(mapping, cl=cl{internal_arg_line}):"
1004 total_lines = [def_line, *lines, " return res"]
1005 script = "\n".join(total_lines)
1006
1007 eval(compile(script, "", "exec"), globs)
1008
1009 return globs[fn_name]
1010
1011
1012make_mapping_structure_fn: Final = mapping_structure_factory
1013
1014
1015# This factory is here for backwards compatibility and circular imports.
1016def iterable_unstructure_factory(
1017 cl: Any, converter: BaseConverter, unstructure_to: Any = None
1018) -> UnstructureHook:
1019 """A hook factory for unstructuring iterables.
1020
1021 :param unstructure_to: Force unstructuring to this type, if provided.
1022
1023 .. versionchanged:: 24.2.0
1024 `typing.NoDefault` is now correctly handled as `Any`.
1025 """
1026 handler = converter.unstructure
1027
1028 # Let's try fishing out the type args
1029 # Unspecified tuples have `__args__` as empty tuples, so guard
1030 # against IndexError.
1031 if getattr(cl, "__args__", None) not in (None, ()):
1032 type_arg = cl.__args__[0]
1033 if isinstance(type_arg, TypeVar):
1034 type_arg = getattr(type_arg, "__default__", Any)
1035 if type_arg is NoDefault:
1036 type_arg = Any
1037 handler = converter.get_unstructure_hook(type_arg, cache_result=False)
1038 if handler == identity:
1039 # Save ourselves the trouble of iterating over it all.
1040 return unstructure_to or cl
1041
1042 def unstructure_iterable(iterable, _seq_cl=unstructure_to or cl, _hook=handler):
1043 return _seq_cl(_hook(i) for i in iterable)
1044
1045 return unstructure_iterable
1046
1047
1048make_iterable_unstructure_fn: Final = iterable_unstructure_factory