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

1""" 

2Private logic for creating models. 

3""" 

4from __future__ import annotations as _annotations 

5 

6import typing 

7from functools import partial 

8from types import FunctionType 

9from typing import Any, Callable 

10 

11from pydantic_core import SchemaSerializer, SchemaValidator 

12 

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 

22 

23if typing.TYPE_CHECKING: 

24 from inspect import Signature 

25 

26 from ..config import ConfigDict 

27 from ..main import BaseModel 

28 

29 

30IGNORED_TYPES: tuple[Any, ...] = ( 

31 FunctionType, 

32 property, 

33 type, 

34 classmethod, 

35 staticmethod, 

36 PydanticDescriptorProxy, 

37 ComputedFieldInfo, 

38) 

39object_setattr = object.__setattr__ 

40 

41 

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. 

45 

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) 

52 

53 

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 

66 

67 private_attributes: dict[str, ModelPrivateAttr] = {} 

68 raw_annotations = namespace.get('__annotations__', {}) 

69 

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 ) 

79 

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 ) 

126 

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() 

137 

138 return private_attributes 

139 

140 

141def single_underscore(name: str) -> bool: 

142 return name.startswith('_') and not name.startswith('__') 

143 

144 

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) 

151 

152 apply_alias_generator(cls.model_config, fields) 

153 cls.model_fields = fields 

154 cls.__class_vars__.update(class_vars) 

155 

156 

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. 

167 

168 Returns `True` if the model is successfully completed, else `False`. 

169 

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 

202 

203 core_config = config_wrapper.core_config(cls) 

204 

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 

210 

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 

216 

217 

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 

226 

227 present_params = signature(init).parameters.values() 

228 merged_params: dict[str, Parameter] = {} 

229 var_kw = None 

230 use_var_kw = False 

231 

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 

241 

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 

258 

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 ) 

264 

265 if config_wrapper.extra == 'allow': 

266 use_var_kw = True 

267 

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 

281 

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) 

286 

287 return Signature(parameters=list(merged_params.values()), return_annotation=None) 

288 

289 

290class MockValidator: 

291 """ 

292 Mocker for `pydantic_core.SchemaValidator` which just raises an error when one of its methods is accessed. 

293 """ 

294 

295 __slots__ = '_error_message', '_code' 

296 

297 def __init__(self, error_message: str, *, code: PydanticErrorCodes) -> None: 

298 self._error_message = error_message 

299 self._code: PydanticErrorCodes = code 

300 

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) 

306 

307 

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 

312 

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