Coverage for /pythoncovmergedfiles/medio/medio/src/pydantic/pydantic/_internal/_fields.py: 47%

130 statements  

« prev     ^ index     » next       coverage.py v7.2.3, created at 2023-04-27 07:38 +0000

1""" 

2Private logic related to fields (the `Field()` function and `FieldInfo` class), and arguments to `Annotated`. 

3""" 

4from __future__ import annotations as _annotations 

5 

6import dataclasses 

7import sys 

8from abc import ABC, abstractmethod 

9from copy import copy 

10from typing import TYPE_CHECKING, Any 

11 

12from pydantic_core import core_schema 

13 

14from . import _typing_extra 

15from ._forward_ref import PydanticForwardRef 

16from ._repr import Representation 

17from ._typing_extra import get_cls_type_hints_lenient, get_type_hints, is_classvar, is_finalvar 

18 

19if TYPE_CHECKING: 

20 from ..fields import FieldInfo 

21 from ._dataclasses import StandardDataclass 

22 

23 

24def get_type_hints_infer_globalns( 

25 obj: Any, 

26 localns: dict[str, Any] | None = None, 

27 include_extras: bool = False, 

28) -> dict[str, Any]: 

29 module_name = getattr(obj, '__module__', None) 

30 globalns: dict[str, Any] | None = None 

31 if module_name: 

32 try: 

33 globalns = sys.modules[module_name].__dict__ 

34 except KeyError: 

35 # happens occasionally, see https://github.com/pydantic/pydantic/issues/2363 

36 pass 

37 return get_type_hints(obj, globalns=globalns, localns=localns, include_extras=include_extras) 

38 

39 

40class _UndefinedType: 

41 """ 

42 Singleton class to represent an undefined value. 

43 """ 

44 

45 def __repr__(self) -> str: 

46 return 'PydanticUndefined' 

47 

48 def __copy__(self) -> _UndefinedType: 

49 return self 

50 

51 def __reduce__(self) -> str: 

52 return 'Undefined' 

53 

54 def __deepcopy__(self, _: Any) -> _UndefinedType: 

55 return self 

56 

57 

58Undefined = _UndefinedType() 

59 

60 

61class PydanticMetadata(Representation): 

62 """ 

63 Base class for annotation markers like `Strict`. 

64 """ 

65 

66 __slots__ = () 

67 

68 

69class PydanticGeneralMetadata(PydanticMetadata): 

70 def __init__(self, **metadata: Any): 

71 self.__dict__ = metadata 

72 

73 

74class SchemaRef(Representation): 

75 """ 

76 Holds a reference to another schema. 

77 """ 

78 

79 __slots__ = ('__pydantic_core_schema__',) 

80 

81 def __init__(self, schema: core_schema.CoreSchema): 

82 self.__pydantic_core_schema__ = schema 

83 

84 

85class CustomValidator(ABC): 

86 """ 

87 Used to define functional validators which can be updated with constraints. 

88 """ 

89 

90 @abstractmethod 

91 def __pydantic_update_schema__(self, schema: core_schema.CoreSchema, **constraints: Any) -> None: 

92 raise NotImplementedError() 

93 

94 @abstractmethod 

95 def __call__(self, __input_value: Any, __info: core_schema.ValidationInfo) -> Any: 

96 raise NotImplementedError() 

97 

98 def _update_attrs(self, constraints: dict[str, Any], attrs: set[str] | None = None) -> None: 

99 """ 

100 Utility for updating attributes/slots and raising an error if they don't exist, to be used by 

101 implementations of `CustomValidator`. 

102 """ 

103 attrs = attrs or set(self.__slots__) # type: ignore[attr-defined] 

104 for k, v in constraints.items(): 

105 if k not in attrs: 

106 raise TypeError(f'{k!r} is not a valid constraint for {self.__class__.__name__}') 

107 setattr(self, k, v) 

108 

109 

110# KW_ONLY is only available in Python 3.10+ 

111DC_KW_ONLY = getattr(dataclasses, 'KW_ONLY', None) 

112 

113 

114def collect_model_fields( # noqa: C901 

115 cls: type[Any], 

116 bases: tuple[type[Any], ...], 

117 types_namespace: dict[str, Any] | None, 

118 *, 

119 typevars_map: dict[Any, Any] | None = None, 

120) -> tuple[dict[str, FieldInfo], set[str]]: 

121 """ 

122 Collect the fields of a nascent pydantic model 

123 

124 Also collect the names of any ClassVars present in the type hints. 

125 

126 The returned value is a tuple of two items: the fields dict, and the set of ClassVar names. 

127 

128 :param cls: BaseModel or dataclass 

129 :param bases: parents of the class, generally `cls.__bases__` 

130 :param types_namespace: optional extra namespace to look for types in 

131 """ 

132 from ..fields import FieldInfo 

133 

134 type_hints = get_cls_type_hints_lenient(cls, types_namespace) 

135 

136 # https://docs.python.org/3/howto/annotations.html#accessing-the-annotations-dict-of-an-object-in-python-3-9-and-older 

137 # annotations is only used for finding fields in parent classes 

138 annotations = cls.__dict__.get('__annotations__', {}) 

139 fields: dict[str, FieldInfo] = {} 

140 

141 class_vars: set[str] = set() 

142 for ann_name, ann_type in type_hints.items(): 

143 if is_classvar(ann_type): 

144 class_vars.add(ann_name) 

145 continue 

146 if _is_finalvar_with_default_val(ann_type, getattr(cls, ann_name, Undefined)): 

147 class_vars.add(ann_name) 

148 continue 

149 if ann_name.startswith('_'): 

150 continue 

151 

152 # when building a generic model with `MyModel[int]`, the generic_origin check makes sure we don't get 

153 # "... shadows an attribute" errors 

154 generic_origin = getattr(cls, '__pydantic_generic_metadata__', {}).get('origin') 

155 for base in bases: 

156 if hasattr(base, ann_name): 

157 if base is generic_origin: 

158 # Don't error when "shadowing" of attributes in parametrized generics 

159 continue 

160 raise NameError( 

161 f'Field name "{ann_name}" shadows an attribute in parent "{base.__qualname__}"; ' 

162 f'you might want to use a different field name with "alias=\'{ann_name}\'".' 

163 ) 

164 

165 try: 

166 default = getattr(cls, ann_name, Undefined) 

167 if default is Undefined: 

168 raise AttributeError 

169 except AttributeError: 

170 if ann_name in annotations or isinstance(ann_type, PydanticForwardRef): 

171 field_info = FieldInfo.from_annotation(ann_type) 

172 else: 

173 # if field has no default value and is not in __annotations__ this means that it is 

174 # defined in a base class and we can take it from there 

175 model_fields_lookup: dict[str, FieldInfo] = {} 

176 for x in cls.__bases__[::-1]: 

177 model_fields_lookup.update(getattr(x, 'model_fields', {})) 

178 if ann_name in model_fields_lookup: 

179 # The field was present on one of the (possibly multiple) base classes 

180 # copy the field to make sure typevar substitutions don't cause issues with the base classes 

181 field_info = copy(model_fields_lookup[ann_name]) 

182 else: 

183 # The field was not found on any base classes; this seems to be caused by fields not getting 

184 # generated thanks to models not being fully defined while initializing recursive models. 

185 # Nothing stops us from just creating a new FieldInfo for this type hint, so we do this. 

186 field_info = FieldInfo.from_annotation(ann_type) 

187 else: 

188 field_info = FieldInfo.from_annotated_attribute(ann_type, default) 

189 # attributes which are fields are removed from the class namespace: 

190 # 1. To match the behaviour of annotation-only fields 

191 # 2. To avoid false positives in the NameError check above 

192 try: 

193 delattr(cls, ann_name) 

194 except AttributeError: 

195 pass # indicates the attribute was on a parent class 

196 

197 fields[ann_name] = field_info 

198 

199 if typevars_map: 

200 for field in fields.values(): 

201 field.apply_typevars_map(typevars_map, types_namespace) 

202 

203 return fields, class_vars 

204 

205 

206def _is_finalvar_with_default_val(type_: type[Any], val: Any) -> bool: 

207 from ..fields import FieldInfo 

208 

209 if not is_finalvar(type_): 

210 return False 

211 elif val is Undefined: 

212 return False 

213 elif isinstance(val, FieldInfo) and (val.default is Undefined and val.default_factory is None): 

214 return False 

215 else: 

216 return True 

217 

218 

219def collect_dataclass_fields( 

220 cls: type[StandardDataclass], types_namespace: dict[str, Any] | None, *, typevars_map: dict[Any, Any] | None = None 

221) -> dict[str, FieldInfo]: 

222 from ..fields import FieldInfo 

223 

224 fields: dict[str, FieldInfo] = {} 

225 dataclass_fields: dict[str, dataclasses.Field] = cls.__dataclass_fields__ 

226 cls_localns = dict(vars(cls)) # this matches get_cls_type_hints_lenient, but all tests pass with `= None` instead 

227 

228 for ann_name, dataclass_field in dataclass_fields.items(): 

229 ann_type = _typing_extra.eval_type_lenient(dataclass_field.type, types_namespace, cls_localns) 

230 if is_classvar(ann_type): 

231 continue 

232 

233 if not dataclass_field.init: 

234 # TODO: We should probably do something with this so that validate_assignment behaves properly 

235 # See https://github.com/pydantic/pydantic/issues/5470 

236 continue 

237 

238 if isinstance(dataclass_field.default, FieldInfo): 

239 field_info = FieldInfo.from_annotated_attribute(ann_type, dataclass_field.default) 

240 else: 

241 field_info = FieldInfo.from_annotated_attribute(ann_type, dataclass_field) 

242 fields[ann_name] = field_info 

243 

244 if field_info.default is not Undefined: 

245 # We need this to fix the default when the "default" from __dataclass_fields__ is a pydantic.FieldInfo 

246 setattr(cls, ann_name, field_info.default) 

247 

248 if typevars_map: 

249 for field in fields.values(): 

250 field.apply_typevars_map(typevars_map, types_namespace) 

251 

252 return fields