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