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