Coverage for /pythoncovmergedfiles/medio/medio/src/pydantic/pydantic/_internal/_model_construction.py: 60%
155 statements
« prev ^ index » next coverage.py v7.2.3, created at 2023-04-27 07:38 +0000
« prev ^ index » next coverage.py v7.2.3, created at 2023-04-27 07:38 +0000
1"""
2Private logic for creating models.
3"""
4from __future__ import annotations as _annotations
6import typing
7from functools import partial
8from types import FunctionType
9from typing import Any, Callable
11from pydantic_core import SchemaSerializer, SchemaValidator
13from ..errors import PydanticErrorCodes, PydanticUndefinedAnnotation, PydanticUserError
14from ..fields import FieldInfo, ModelPrivateAttr, PrivateAttr
15from ._config import ConfigWrapper
16from ._decorators import ComputedFieldInfo, PydanticDescriptorProxy
17from ._fields import Undefined, collect_model_fields
18from ._generate_schema import GenerateSchema
19from ._generics import get_model_typevars_map
20from ._typing_extra import is_classvar
21from ._utils import ClassAttribute, is_valid_identifier
23if typing.TYPE_CHECKING:
24 from inspect import Signature
26 from ..config import ConfigDict
27 from ..main import BaseModel
30IGNORED_TYPES: tuple[Any, ...] = (
31 FunctionType,
32 property,
33 type,
34 classmethod,
35 staticmethod,
36 PydanticDescriptorProxy,
37 ComputedFieldInfo,
38)
39object_setattr = object.__setattr__
42def init_private_attributes(self: BaseModel, __context: Any) -> None:
43 """
44 This function is meant to behave like a BaseModel method to initialise private attributes.
46 It takes context as an argument since that's what pydantic-core passes when calling it.
47 """
48 for name, private_attr in self.__private_attributes__.items():
49 default = private_attr.get_default()
50 if default is not Undefined:
51 object_setattr(self, name, default)
54def inspect_namespace( # noqa C901
55 namespace: dict[str, Any],
56 ignored_types: tuple[type[Any], ...],
57 base_class_vars: set[str],
58 base_class_fields: set[str],
59) -> dict[str, ModelPrivateAttr]:
60 """
61 iterate over the namespace and:
62 * gather private attributes
63 * check for items which look like fields but are not (e.g. have no annotation) and warn
64 """
65 all_ignored_types = ignored_types + IGNORED_TYPES
67 private_attributes: dict[str, ModelPrivateAttr] = {}
68 raw_annotations = namespace.get('__annotations__', {})
70 if '__root__' in raw_annotations or '__root__' in namespace:
71 # TODO: Update error message with migration description and/or link to documentation
72 # Needs to mention:
73 # * Use root_validator to wrap input data in a dict
74 # * Use model_serializer to extract wrapped data during dumping
75 # * Use model_modify_json_schema (or whatever it becomes) to unwrap the JSON schema
76 raise TypeError(
77 '__root__ models are no longer supported in v2; a migration guide will be added in the near future'
78 )
80 ignored_names: set[str] = set()
81 for var_name, value in list(namespace.items()):
82 if var_name == 'model_config':
83 continue
84 elif isinstance(value, all_ignored_types) or value.__class__.__module__ == 'functools':
85 ignored_names.add(var_name)
86 continue
87 elif isinstance(value, ModelPrivateAttr):
88 if var_name.startswith('__'):
89 raise NameError(
90 f'Private attributes "{var_name}" must not have dunder names; '
91 'use a single underscore prefix instead.'
92 )
93 elif not single_underscore(var_name):
94 raise NameError(
95 f'Private attributes "{var_name}" must not be a valid field name; '
96 f'use sunder names, e.g. "_{var_name}"'
97 )
98 private_attributes[var_name] = value
99 del namespace[var_name]
100 elif var_name.startswith('__'):
101 continue
102 elif var_name.startswith('_'):
103 if var_name in raw_annotations and not is_classvar(raw_annotations[var_name]):
104 private_attributes[var_name] = PrivateAttr(default=value)
105 del namespace[var_name]
106 elif var_name in base_class_vars:
107 continue
108 elif var_name not in raw_annotations:
109 if var_name in base_class_fields:
110 raise PydanticUserError(
111 f'Field {var_name!r} defined on a base class was overridden by a non-annotated attribute. '
112 f'All field definitions, including overrides, require a type annotation.',
113 code='model-field-overridden',
114 )
115 elif isinstance(value, FieldInfo):
116 raise PydanticUserError(
117 f'Field {var_name!r} requires a type annotation', code='model-field-missing-annotation'
118 )
119 else:
120 raise PydanticUserError(
121 f"A non-annotated attribute was detected: `{var_name} = {value!r}`. All model fields require a "
122 f"type annotation; if `{var_name}` is not meant to be a field, you may be able to resolve this "
123 f"error by annotating it as a `ClassVar` or updating `model_config['ignored_types']`.",
124 code='model-field-missing-annotation',
125 )
127 for ann_name, ann_type in raw_annotations.items():
128 if (
129 single_underscore(ann_name)
130 and ann_name not in private_attributes
131 and ann_name not in ignored_names
132 and not is_classvar(ann_type)
133 and ann_type not in all_ignored_types
134 and ann_type.__module__ != 'functools'
135 ):
136 private_attributes[ann_name] = PrivateAttr()
138 return private_attributes
141def single_underscore(name: str) -> bool:
142 return name.startswith('_') and not name.startswith('__')
145def set_model_fields(cls: type[BaseModel], bases: tuple[type[Any], ...], types_namespace: dict[str, Any]) -> None:
146 """
147 Collect and set `cls.model_fields` and `cls.__class_vars__`.
148 """
149 typevars_map = get_model_typevars_map(cls)
150 fields, class_vars = collect_model_fields(cls, bases, types_namespace, typevars_map=typevars_map)
152 apply_alias_generator(cls.model_config, fields)
153 cls.model_fields = fields
154 cls.__class_vars__.update(class_vars)
157def complete_model_class(
158 cls: type[BaseModel],
159 cls_name: str,
160 config_wrapper: ConfigWrapper,
161 *,
162 raise_errors: bool = True,
163 types_namespace: dict[str, Any] | None,
164) -> bool:
165 """
166 Finish building a model class.
168 Returns `True` if the model is successfully completed, else `False`.
170 This logic must be called after class has been created since validation functions must be bound
171 and `get_type_hints` requires a class object.
172 """
173 typevars_map = get_model_typevars_map(cls)
174 gen_schema = GenerateSchema(
175 config_wrapper,
176 types_namespace,
177 typevars_map,
178 )
179 try:
180 schema = cls.__get_pydantic_core_schema__(
181 cls, partial(gen_schema.generate_schema, from_dunder_get_core_schema=False)
182 )
183 except PydanticUndefinedAnnotation as e:
184 if raise_errors:
185 raise
186 if config_wrapper.undefined_types_warning:
187 config_warning_string = (
188 f'`{cls_name}` has an undefined annotation: `{e.name}`. '
189 f'It may be possible to resolve this by setting '
190 f'undefined_types_warning=False in the config for `{cls_name}`.'
191 )
192 # FIXME UserWarning should not be raised here, but rather warned!
193 raise UserWarning(config_warning_string)
194 usage_warning_string = (
195 f'`{cls_name}` is not fully defined; you should define `{e.name}`, then call `{cls_name}.model_rebuild()` '
196 f'before the first `{cls_name}` instance is created.'
197 )
198 cls.__pydantic_validator__ = MockValidator( # type: ignore[assignment]
199 usage_warning_string, code='model-not-fully-defined'
200 )
201 return False
203 core_config = config_wrapper.core_config(cls)
205 # debug(schema)
206 cls.__pydantic_core_schema__ = schema
207 cls.__pydantic_validator__ = SchemaValidator(schema, core_config)
208 cls.__pydantic_serializer__ = SchemaSerializer(schema, core_config)
209 cls.__pydantic_model_complete__ = True
211 # set __signature__ attr only for model class, but not for its instances
212 cls.__signature__ = ClassAttribute(
213 '__signature__', generate_model_signature(cls.__init__, cls.model_fields, config_wrapper)
214 )
215 return True
218def generate_model_signature(
219 init: Callable[..., None], fields: dict[str, FieldInfo], config_wrapper: ConfigWrapper
220) -> Signature:
221 """
222 Generate signature for model based on its fields
223 """
224 from inspect import Parameter, Signature, signature
225 from itertools import islice
227 present_params = signature(init).parameters.values()
228 merged_params: dict[str, Parameter] = {}
229 var_kw = None
230 use_var_kw = False
232 for param in islice(present_params, 1, None): # skip self arg
233 # inspect does "clever" things to show annotations as strings because we have
234 # `from __future__ import annotations` in main, we don't want that
235 if param.annotation == 'Any':
236 param = param.replace(annotation=Any)
237 if param.kind is param.VAR_KEYWORD:
238 var_kw = param
239 continue
240 merged_params[param.name] = param
242 if var_kw: # if custom init has no var_kw, fields which are not declared in it cannot be passed through
243 allow_names = config_wrapper.populate_by_name
244 for field_name, field in fields.items():
245 # when alias is a str it should be used for signature generation
246 if isinstance(field.alias, str):
247 param_name = field.alias
248 else:
249 param_name = field_name
250 if field_name in merged_params or param_name in merged_params:
251 continue
252 elif not is_valid_identifier(param_name):
253 if allow_names and is_valid_identifier(field_name):
254 param_name = field_name
255 else:
256 use_var_kw = True
257 continue
259 # TODO: replace annotation with actual expected types once #1055 solved
260 kwargs = {} if field.is_required() else {'default': field.get_default(call_default_factory=False)}
261 merged_params[param_name] = Parameter(
262 param_name, Parameter.KEYWORD_ONLY, annotation=field.rebuild_annotation(), **kwargs
263 )
265 if config_wrapper.extra == 'allow':
266 use_var_kw = True
268 if var_kw and use_var_kw:
269 # Make sure the parameter for extra kwargs
270 # does not have the same name as a field
271 default_model_signature = [
272 ('__pydantic_self__', Parameter.POSITIONAL_OR_KEYWORD),
273 ('data', Parameter.VAR_KEYWORD),
274 ]
275 if [(p.name, p.kind) for p in present_params] == default_model_signature:
276 # if this is the standard model signature, use extra_data as the extra args name
277 var_kw_name = 'extra_data'
278 else:
279 # else start from var_kw
280 var_kw_name = var_kw.name
282 # generate a name that's definitely unique
283 while var_kw_name in fields:
284 var_kw_name += '_'
285 merged_params[var_kw_name] = var_kw.replace(name=var_kw_name)
287 return Signature(parameters=list(merged_params.values()), return_annotation=None)
290class MockValidator:
291 """
292 Mocker for `pydantic_core.SchemaValidator` which just raises an error when one of its methods is accessed.
293 """
295 __slots__ = '_error_message', '_code'
297 def __init__(self, error_message: str, *, code: PydanticErrorCodes) -> None:
298 self._error_message = error_message
299 self._code: PydanticErrorCodes = code
301 def __getattr__(self, item: str) -> None:
302 __tracebackhide__ = True
303 # raise an AttributeError if `item` doesn't exist
304 getattr(SchemaValidator, item)
305 raise PydanticUserError(self._error_message, code=self._code)
308def apply_alias_generator(config: ConfigDict, fields: dict[str, FieldInfo]) -> None:
309 alias_generator = config.get('alias_generator')
310 if alias_generator is None:
311 return
313 for name, field_info in fields.items():
314 if field_info.alias_priority is None or field_info.alias_priority <= 1:
315 alias = alias_generator(name)
316 if not isinstance(alias, str):
317 raise TypeError(f'alias_generator {alias_generator} must return str, not {alias.__class__}')
318 field_info.alias = alias
319 field_info.validation_alias = alias
320 field_info.serialization_alias = alias
321 field_info.alias_priority = 1