Coverage for pydantic/_internal/_signature.py: 93.65%
80 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-02-13 19:35 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-02-13 19:35 +0000
1from __future__ import annotations 1yzabcdefghFABCijklmnopGHIJKLMDEqrstuvwx
3import dataclasses 1yzabcdefghFABCijklmnopGHIJKLMDEqrstuvwx
4from inspect import Parameter, Signature, signature 1yzabcdefghFABCijklmnopGHIJKLMDEqrstuvwx
5from typing import TYPE_CHECKING, Any, Callable 1yzabcdefghFABCijklmnopGHIJKLMDEqrstuvwx
7from pydantic_core import PydanticUndefined 1yzabcdefghFABCijklmnopGHIJKLMDEqrstuvwx
9from ._utils import is_valid_identifier 1yzabcdefghFABCijklmnopGHIJKLMDEqrstuvwx
11if TYPE_CHECKING: 1yzabcdefghFABCijklmnopGHIJKLMDEqrstuvwx
12 from ..config import ExtraValues
13 from ..fields import FieldInfo
16# Copied over from stdlib dataclasses
17class _HAS_DEFAULT_FACTORY_CLASS: 1yzabcdefghFABCijklmnopGHIJKLMDEqrstuvwx
18 def __repr__(self): 1yzabcdefghFABCijklmnopGHIJKLMDEqrstuvwx
19 return '<factory>' 1yzabcdefghFABCijklmnopDEqrstuvwx
22_HAS_DEFAULT_FACTORY = _HAS_DEFAULT_FACTORY_CLASS() 1yzabcdefghFABCijklmnopGHIJKLMDEqrstuvwx
25def _field_name_for_signature(field_name: str, field_info: FieldInfo) -> str: 1yzabcdefghFABCijklmnopGHIJKLMDEqrstuvwx
26 """Extract the correct name to use for the field when generating a signature.
28 Assuming the field has a valid alias, this will return the alias. Otherwise, it will return the field name.
29 First priority is given to the alias, then the validation_alias, then the field name.
31 Args:
32 field_name: The name of the field
33 field_info: The corresponding FieldInfo object.
35 Returns:
36 The correct name to use when generating a signature.
37 """
38 if isinstance(field_info.alias, str) and is_valid_identifier(field_info.alias): 1yzabcdefghFABCijklmnopDEqrstuvwx
39 return field_info.alias 1yzabcdefghFABCijklmnopDEqrstuvwx
40 if isinstance(field_info.validation_alias, str) and is_valid_identifier(field_info.validation_alias): 40 ↛ 41line 40 didn't jump to line 41 because the condition on line 40 was never true1yzabcdefghFABCijklmnopDEqrstuvwx
41 return field_info.validation_alias
43 return field_name 1yzabcdefghFABCijklmnopDEqrstuvwx
46def _process_param_defaults(param: Parameter) -> Parameter: 1yzabcdefghFABCijklmnopGHIJKLMDEqrstuvwx
47 """Modify the signature for a parameter in a dataclass where the default value is a FieldInfo instance.
49 Args:
50 param (Parameter): The parameter
52 Returns:
53 Parameter: The custom processed parameter
54 """
55 from ..fields import FieldInfo 1yzabcdefghFABCijklmnopDEqrstuvwx
57 param_default = param.default 1yzabcdefghFABCijklmnopDEqrstuvwx
58 if isinstance(param_default, FieldInfo): 1yzabcdefghFABCijklmnopDEqrstuvwx
59 annotation = param.annotation 1yzabcdefghFABCijklmnopDEqrstuvwx
60 # Replace the annotation if appropriate
61 # inspect does "clever" things to show annotations as strings because we have
62 # `from __future__ import annotations` in main, we don't want that
63 if annotation == 'Any': 63 ↛ 64line 63 didn't jump to line 64 because the condition on line 63 was never true1yzabcdefghFABCijklmnopDEqrstuvwx
64 annotation = Any
66 # Replace the field default
67 default = param_default.default 1yzabcdefghFABCijklmnopDEqrstuvwx
68 if default is PydanticUndefined: 1yzabcdefghFABCijklmnopDEqrstuvwx
69 if param_default.default_factory is PydanticUndefined: 69 ↛ 70line 69 didn't jump to line 70 because the condition on line 69 was never true1yzabcdefghFABCijklmnopDEqrstuvwx
70 default = Signature.empty
71 else:
72 # this is used by dataclasses to indicate a factory exists:
73 default = dataclasses._HAS_DEFAULT_FACTORY # type: ignore 1yzabcdefghFABCijklmnopDEqrstuvwx
74 return param.replace( 1yzabcdefghFABCijklmnopDEqrstuvwx
75 annotation=annotation, name=_field_name_for_signature(param.name, param_default), default=default
76 )
77 return param 1yzabcdefghFABCijklmnopDEqrstuvwx
80def _generate_signature_parameters( # noqa: C901 (ignore complexity, could use a refactor) 1yzabcdefghFABCijklmnopGHIJKLMDEqrstuvwx
81 init: Callable[..., None],
82 fields: dict[str, FieldInfo],
83 populate_by_name: bool,
84 extra: ExtraValues | None,
85) -> dict[str, Parameter]:
86 """Generate a mapping of parameter names to Parameter objects for a pydantic BaseModel or dataclass."""
87 from itertools import islice 1yzabcdefghFABCijklmnopDEqrstuvwx
89 present_params = signature(init).parameters.values() 1yzabcdefghFABCijklmnopDEqrstuvwx
90 merged_params: dict[str, Parameter] = {} 1yzabcdefghFABCijklmnopDEqrstuvwx
91 var_kw = None 1yzabcdefghFABCijklmnopDEqrstuvwx
92 use_var_kw = False 1yzabcdefghFABCijklmnopDEqrstuvwx
94 for param in islice(present_params, 1, None): # skip self arg 1yzabcdefghFABCijklmnopDEqrstuvwx
95 # inspect does "clever" things to show annotations as strings because we have
96 # `from __future__ import annotations` in main, we don't want that
97 if fields.get(param.name): 1yzabcdefghFABCijklmnopDEqrstuvwx
98 # exclude params with init=False
99 if getattr(fields[param.name], 'init', True) is False: 1yzabcdefghFABCijklmnopDEqrstuvwx
100 continue 1yzabcdefghFABCijklmnopDEqrstuvwx
101 param = param.replace(name=_field_name_for_signature(param.name, fields[param.name])) 1yzabcdefghFABCijklmnopDEqrstuvwx
102 if param.annotation == 'Any': 1yzabcdefghFABCijklmnopDEqrstuvwx
103 param = param.replace(annotation=Any) 1yzabcdefghFABCijklmnopDEqrstuvwx
104 if param.kind is param.VAR_KEYWORD: 1yzabcdefghFABCijklmnopDEqrstuvwx
105 var_kw = param 1yzabcdefghFABCijklmnopDEqrstuvwx
106 continue 1yzabcdefghFABCijklmnopDEqrstuvwx
107 merged_params[param.name] = param 1yzabcdefghFABCijklmnopDEqrstuvwx
109 if var_kw: # if custom init has no var_kw, fields which are not declared in it cannot be passed through 1yzabcdefghFABCijklmnopDEqrstuvwx
110 allow_names = populate_by_name 1yzabcdefghFABCijklmnopDEqrstuvwx
111 for field_name, field in fields.items(): 1yzabcdefghFABCijklmnopDEqrstuvwx
112 # when alias is a str it should be used for signature generation
113 param_name = _field_name_for_signature(field_name, field) 1yzabcdefghFABCijklmnopDEqrstuvwx
115 if field_name in merged_params or param_name in merged_params: 1yzabcdefghFABCijklmnopDEqrstuvwx
116 continue 1abcdefghAijklmnopqrstuvwx
118 if not is_valid_identifier(param_name): 1yzabcdefghFABCijklmnopDEqrstuvwx
119 if allow_names: 119 ↛ 120line 119 didn't jump to line 120 because the condition on line 119 was never true1yzabcdefghFABCijklmnopDEqrstuvwx
120 param_name = field_name
121 else:
122 use_var_kw = True 1yzabcdefghFABCijklmnopDEqrstuvwx
123 continue 1yzabcdefghFABCijklmnopDEqrstuvwx
125 if field.is_required(): 1yzabcdefghFABCijklmnopDEqrstuvwx
126 default = Parameter.empty 1yzabcdefghFABCijklmnopDEqrstuvwx
127 elif field.default_factory is not None: 1yzabcdefghFABCijklmnopDEqrstuvwx
128 # Mimics stdlib dataclasses:
129 default = _HAS_DEFAULT_FACTORY 1yzabcdefghFABCijklmnopDEqrstuvwx
130 else:
131 default = field.default 1yzabcdefghFABCijklmnopDEqrstuvwx
132 merged_params[param_name] = Parameter( 1yzabcdefghFABCijklmnopDEqrstuvwx
133 param_name,
134 Parameter.KEYWORD_ONLY,
135 annotation=field.rebuild_annotation(),
136 default=default,
137 )
139 if extra == 'allow': 1yzabcdefghFABCijklmnopDEqrstuvwx
140 use_var_kw = True 1yzabcdefghFABCijklmnopDEqrstuvwx
142 if var_kw and use_var_kw: 1yzabcdefghFABCijklmnopDEqrstuvwx
143 # Make sure the parameter for extra kwargs
144 # does not have the same name as a field
145 default_model_signature = [ 1yzabcdefghFABCijklmnopDEqrstuvwx
146 ('self', Parameter.POSITIONAL_ONLY),
147 ('data', Parameter.VAR_KEYWORD),
148 ]
149 if [(p.name, p.kind) for p in present_params] == default_model_signature: 1yzabcdefghFABCijklmnopDEqrstuvwx
150 # if this is the standard model signature, use extra_data as the extra args name
151 var_kw_name = 'extra_data' 1yzabcdefghFABCijklmnopDEqrstuvwx
152 else:
153 # else start from var_kw
154 var_kw_name = var_kw.name 1yzabcdefghFABCijklmnopDEqrstuvwx
156 # generate a name that's definitely unique
157 while var_kw_name in fields: 1yzabcdefghFABCijklmnopDEqrstuvwx
158 var_kw_name += '_' 1yzabcdefghFABCijklmnopDEqrstuvwx
159 merged_params[var_kw_name] = var_kw.replace(name=var_kw_name) 1yzabcdefghFABCijklmnopDEqrstuvwx
161 return merged_params 1yzabcdefghFABCijklmnopDEqrstuvwx
164def generate_pydantic_signature( 1yzabcdefghBCijklmnopGHIJKLMDEqrstuvwx
165 init: Callable[..., None],
166 fields: dict[str, FieldInfo],
167 populate_by_name: bool,
168 extra: ExtraValues | None,
169 is_dataclass: bool = False,
170) -> Signature:
171 """Generate signature for a pydantic BaseModel or dataclass.
173 Args:
174 init: The class init.
175 fields: The model fields.
176 populate_by_name: The `populate_by_name` value of the config.
177 extra: The `extra` value of the config.
178 is_dataclass: Whether the model is a dataclass.
180 Returns:
181 The dataclass/BaseModel subclass signature.
182 """
183 merged_params = _generate_signature_parameters(init, fields, populate_by_name, extra) 1yzabcdefghFABCijklmnopDEqrstuvwx
185 if is_dataclass: 1yzabcdefghFABCijklmnopDEqrstuvwx
186 merged_params = {k: _process_param_defaults(v) for k, v in merged_params.items()} 1yzabcdefghFABCijklmnopDEqrstuvwx
188 return Signature(parameters=list(merged_params.values()), return_annotation=None) 1yzabcdefghFABCijklmnopDEqrstuvwx