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