Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pydantic/_internal/_known_annotated_metadata.py: 54%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

162 statements  

1from __future__ import annotations 

2 

3from collections import defaultdict 

4from collections.abc import Iterable 

5from copy import copy 

6from functools import lru_cache, partial 

7from typing import TYPE_CHECKING, Any 

8 

9from pydantic_core import CoreSchema, PydanticCustomError, ValidationError, to_jsonable_python 

10from pydantic_core import core_schema as cs 

11 

12from ._fields import PydanticMetadata 

13from ._import_utils import import_cached_field_info 

14 

15if TYPE_CHECKING: 

16 pass 

17 

18STRICT = {'strict'} 

19FAIL_FAST = {'fail_fast'} 

20LENGTH_CONSTRAINTS = {'min_length', 'max_length'} 

21INEQUALITY = {'le', 'ge', 'lt', 'gt'} 

22NUMERIC_CONSTRAINTS = {'multiple_of', *INEQUALITY} 

23ALLOW_INF_NAN = {'allow_inf_nan'} 

24 

25STR_CONSTRAINTS = { 

26 *LENGTH_CONSTRAINTS, 

27 *STRICT, 

28 'strip_whitespace', 

29 'to_lower', 

30 'to_upper', 

31 'pattern', 

32 'coerce_numbers_to_str', 

33 'ascii_only', 

34} 

35BYTES_CONSTRAINTS = {*LENGTH_CONSTRAINTS, *STRICT} 

36 

37LIST_CONSTRAINTS = {*LENGTH_CONSTRAINTS, *STRICT, *FAIL_FAST} 

38TUPLE_CONSTRAINTS = {*LENGTH_CONSTRAINTS, *STRICT, *FAIL_FAST} 

39SET_CONSTRAINTS = {*LENGTH_CONSTRAINTS, *STRICT, *FAIL_FAST} 

40DICT_CONSTRAINTS = {*LENGTH_CONSTRAINTS, *STRICT, *FAIL_FAST} 

41GENERATOR_CONSTRAINTS = LENGTH_CONSTRAINTS 

42SEQUENCE_CONSTRAINTS = {*LENGTH_CONSTRAINTS, *FAIL_FAST} 

43 

44FLOAT_CONSTRAINTS = {*NUMERIC_CONSTRAINTS, *ALLOW_INF_NAN, *STRICT} 

45DECIMAL_CONSTRAINTS = {'max_digits', 'decimal_places', *FLOAT_CONSTRAINTS} 

46FRACTION_CONSTRAINTS = {*NUMERIC_CONSTRAINTS, *STRICT} 

47INT_CONSTRAINTS = {*NUMERIC_CONSTRAINTS, *STRICT} 

48BOOL_CONSTRAINTS = STRICT 

49UUID_CONSTRAINTS = STRICT 

50 

51DATE_TIME_CONSTRAINTS = {*INEQUALITY, *STRICT} 

52TIMEDELTA_CONSTRAINTS = {*NUMERIC_CONSTRAINTS, *STRICT} 

53LAX_OR_STRICT_CONSTRAINTS = STRICT 

54ENUM_CONSTRAINTS = STRICT 

55COMPLEX_CONSTRAINTS = STRICT 

56 

57UNION_CONSTRAINTS = {'union_mode'} 

58URL_CONSTRAINTS = { 

59 'max_length', 

60 'allowed_schemes', 

61 'host_required', 

62 'default_host', 

63 'default_port', 

64 'default_path', 

65 *STRICT, 

66} 

67 

68CONSTRAINTS_TO_ALLOWED_SCHEMAS: dict[str, set[str]] = defaultdict(set) 

69 

70constraint_schema_pairings: list[tuple[set[str], tuple[str, ...]]] = [ 

71 (STR_CONSTRAINTS, ('str',)), 

72 (BYTES_CONSTRAINTS, ('bytes',)), 

73 (LIST_CONSTRAINTS, ('list', 'deque')), 

74 (TUPLE_CONSTRAINTS, ('tuple',)), 

75 (SET_CONSTRAINTS, ('set', 'frozenset')), 

76 (DICT_CONSTRAINTS, ('dict', 'frozendict', 'ordered-dict', 'counter')), 

77 (GENERATOR_CONSTRAINTS, ('generator',)), 

78 (FLOAT_CONSTRAINTS, ('float',)), 

79 (INT_CONSTRAINTS, ('int',)), 

80 (DATE_TIME_CONSTRAINTS, ('date', 'time', 'datetime')), 

81 (TIMEDELTA_CONSTRAINTS, ('timedelta',)), 

82 (STRICT, ('typed-dict', 'model')), 

83 (UNION_CONSTRAINTS, ('union',)), 

84 (URL_CONSTRAINTS, ('url', 'multi-host-url')), 

85 (BOOL_CONSTRAINTS, ('bool',)), 

86 (UUID_CONSTRAINTS, ('uuid',)), 

87 (LAX_OR_STRICT_CONSTRAINTS, ('lax-or-strict',)), 

88 (ENUM_CONSTRAINTS, ('enum',)), 

89 (DECIMAL_CONSTRAINTS, ('decimal',)), 

90 (COMPLEX_CONSTRAINTS, ('complex',)), 

91 (FRACTION_CONSTRAINTS, ('fraction',)), 

92] 

93 

94for constraints, schemas in constraint_schema_pairings: 

95 for c in constraints: 

96 CONSTRAINTS_TO_ALLOWED_SCHEMAS[c].update(schemas) 

97 

98 

99def as_jsonable_value(v: Any) -> Any: 

100 if type(v) not in (int, str, float, bytes, bool, type(None)): 

101 return to_jsonable_python(v) 

102 return v 

103 

104 

105def expand_grouped_metadata(annotations: Iterable[Any]) -> Iterable[Any]: 

106 """Expand the annotations. 

107 

108 Args: 

109 annotations: An iterable of annotations. 

110 

111 Returns: 

112 An iterable of expanded annotations. 

113 

114 Example: 

115 ```python 

116 from annotated_types import Ge, Len 

117 

118 from pydantic._internal._known_annotated_metadata import expand_grouped_metadata 

119 

120 print(list(expand_grouped_metadata([Ge(4), Len(5)]))) 

121 #> [Ge(ge=4), MinLen(min_length=5)] 

122 ``` 

123 """ 

124 import annotated_types as at 

125 

126 FieldInfo = import_cached_field_info() 

127 

128 for annotation in annotations: 

129 if isinstance(annotation, at.GroupedMetadata): 

130 yield from annotation 

131 elif isinstance(annotation, FieldInfo): 

132 yield from annotation.metadata 

133 # this is a bit problematic in that it results in duplicate metadata 

134 # all of our "consumers" can handle it, but it is not ideal 

135 # we probably should split up FieldInfo into: 

136 # - annotated types metadata 

137 # - individual metadata known only to Pydantic 

138 annotation = copy(annotation) 

139 annotation.metadata = [] 

140 yield annotation 

141 else: 

142 yield annotation 

143 

144 

145@lru_cache 

146def _get_at_to_constraint_map() -> dict[type, str]: 

147 """Return a mapping of annotated types to constraints. 

148 

149 Normally, we would define a mapping like this in the module scope, but we can't do that 

150 because we don't permit module level imports of `annotated_types`, in an attempt to speed up 

151 the import time of `pydantic`. We still only want to have this dictionary defined in one place, 

152 so we use this function to cache the result. 

153 """ 

154 import annotated_types as at 

155 

156 return { 

157 at.Gt: 'gt', 

158 at.Ge: 'ge', 

159 at.Lt: 'lt', 

160 at.Le: 'le', 

161 at.MultipleOf: 'multiple_of', 

162 at.MinLen: 'min_length', 

163 at.MaxLen: 'max_length', 

164 } 

165 

166 

167def apply_known_metadata(annotation: Any, schema: CoreSchema) -> CoreSchema | None: # noqa: C901 

168 """Apply `annotation` to `schema` if it is an annotation we know about (Gt, Le, etc.). 

169 Otherwise return `None`. 

170 

171 This does not handle all known annotations. If / when it does, it can always 

172 return a CoreSchema and return the unmodified schema if the annotation should be ignored. 

173 

174 Assumes that GroupedMetadata has already been expanded via `expand_grouped_metadata`. 

175 

176 Args: 

177 annotation: The annotation. 

178 schema: The schema. 

179 

180 Returns: 

181 An updated schema with annotation if it is an annotation we know about, `None` otherwise. 

182 

183 Raises: 

184 RuntimeError: If a constraint can't be applied to a specific schema type. 

185 ValueError: If an unknown constraint is encountered. 

186 """ 

187 import annotated_types as at 

188 

189 from ._validators import NUMERIC_VALIDATOR_LOOKUP, forbid_inf_nan_check 

190 

191 schema = schema.copy() 

192 schema_update, other_metadata = collect_known_metadata([annotation]) 

193 schema_type = schema['type'] 

194 

195 chain_schema_constraints: set[str] = { 

196 'pattern', 

197 'strip_whitespace', 

198 'to_lower', 

199 'to_upper', 

200 'coerce_numbers_to_str', 

201 'ascii_only', 

202 } 

203 chain_schema_steps: list[CoreSchema] = [] 

204 

205 for constraint, value in schema_update.items(): 

206 if constraint not in CONSTRAINTS_TO_ALLOWED_SCHEMAS: 

207 raise ValueError(f'Unknown constraint {constraint}') 

208 allowed_schemas = CONSTRAINTS_TO_ALLOWED_SCHEMAS[constraint] 

209 

210 # if it becomes necessary to handle more than one constraint 

211 # in this recursive case with function-after or function-wrap, we should refactor 

212 # this is a bit challenging because we sometimes want to apply constraints to the inner schema, 

213 # whereas other times we want to wrap the existing schema with a new one that enforces a new constraint. 

214 if schema_type in {'function-before', 'function-wrap', 'function-after'} and constraint == 'strict': 

215 schema['schema'] = apply_known_metadata(annotation, schema['schema']) # type: ignore # schema is function schema 

216 return schema 

217 

218 # if we're allowed to apply constraint directly to the schema, like le to int, do that 

219 if schema_type in allowed_schemas: 

220 if constraint == 'union_mode' and schema_type == 'union': 

221 schema['mode'] = value # type: ignore # schema is UnionSchema 

222 else: 

223 schema[constraint] = value 

224 continue 

225 

226 # else, apply a function after validator to the schema to enforce the corresponding constraint 

227 if constraint in chain_schema_constraints: 

228 

229 def _apply_constraint_with_incompatibility_info( 

230 value: Any, handler: cs.ValidatorFunctionWrapHandler 

231 ) -> Any: 

232 try: 

233 x = handler(value) 

234 except ValidationError as ve: 

235 # if the error is about the type, it's likely that the constraint is incompatible the type of the field 

236 # for example, the following invalid schema wouldn't be caught during schema build, but rather at this point 

237 # with a cryptic 'string_type' error coming from the string validator, 

238 # that we'd rather express as a constraint incompatibility error (TypeError) 

239 # Annotated[list[int], Field(pattern='abc')] 

240 if 'type' in ve.errors()[0]['type']: 

241 raise TypeError( 

242 f"Unable to apply constraint '{constraint}' to supplied value {value} for schema of type '{schema_type}'" # noqa: B023 

243 ) 

244 raise ve 

245 return x 

246 

247 chain_schema_steps.append( 

248 cs.no_info_wrap_validator_function( 

249 _apply_constraint_with_incompatibility_info, cs.str_schema(**{constraint: value}) 

250 ) 

251 ) 

252 elif constraint in NUMERIC_VALIDATOR_LOOKUP: 

253 if constraint in LENGTH_CONSTRAINTS: 

254 inner_schema = schema 

255 while inner_schema['type'] in {'function-before', 'function-wrap', 'function-after'}: 

256 inner_schema = inner_schema['schema'] # type: ignore 

257 inner_schema_type = inner_schema['type'] 

258 if inner_schema_type == 'list' or ( 

259 inner_schema_type == 'json-or-python' and inner_schema['json_schema']['type'] == 'list' # type: ignore 

260 ): 

261 js_constraint_key = 'minItems' if constraint == 'min_length' else 'maxItems' 

262 else: 

263 js_constraint_key = 'minLength' if constraint == 'min_length' else 'maxLength' 

264 else: 

265 js_constraint_key = constraint 

266 

267 validator_kwargs = {constraint: value} 

268 if constraint == 'decimal_places' and schema_update.get('max_digits') is not None: 

269 # The whole digits limit depends on both constraints, and is checked alongside `decimal_places`: 

270 validator_kwargs['max_digits'] = schema_update['max_digits'] 

271 schema = cs.no_info_after_validator_function( 

272 partial(NUMERIC_VALIDATOR_LOOKUP[constraint], **validator_kwargs), schema 

273 ) 

274 metadata = schema.get('metadata', {}) 

275 if (existing_json_schema_updates := metadata.get('pydantic_js_updates')) is not None: 

276 metadata['pydantic_js_updates'] = { 

277 **existing_json_schema_updates, 

278 **{js_constraint_key: as_jsonable_value(value)}, 

279 } 

280 else: 

281 metadata['pydantic_js_updates'] = {js_constraint_key: as_jsonable_value(value)} 

282 schema['metadata'] = metadata 

283 elif constraint == 'allow_inf_nan' and value is False: 

284 schema = cs.no_info_after_validator_function( 

285 forbid_inf_nan_check, 

286 schema, 

287 ) 

288 else: 

289 # It's rare that we'd get here, but it's possible if we add a new constraint and forget to handle it 

290 # Most constraint errors are caught at runtime during attempted application 

291 raise RuntimeError(f"Unable to apply constraint '{constraint}' to schema of type '{schema_type}'") 

292 

293 for annotation in other_metadata: 

294 if (annotation_type := type(annotation)) in (at_to_constraint_map := _get_at_to_constraint_map()): 

295 constraint = at_to_constraint_map[annotation_type] 

296 validator = NUMERIC_VALIDATOR_LOOKUP.get(constraint) 

297 if validator is None: 

298 raise ValueError(f'Unknown constraint {constraint}') 

299 schema = cs.no_info_after_validator_function( 

300 partial(validator, {constraint: getattr(annotation, constraint)}), schema 

301 ) 

302 continue 

303 elif isinstance(annotation, (at.Predicate, at.Not)): 

304 predicate_name = f'{annotation.func.__qualname__!r} ' if hasattr(annotation.func, '__qualname__') else '' 

305 

306 # Note: B023 is ignored because even though we iterate over `other_metadata`, it is guaranteed 

307 # to be of length 1. `apply_known_metadata()` is called from `GenerateSchema`, where annotations 

308 # were already expanded via `expand_grouped_metadata()`. Confusing, but this falls into the annotations 

309 # refactor. 

310 if isinstance(annotation, at.Predicate): 

311 

312 def val_func(v: Any) -> Any: 

313 predicate_satisfied = annotation.func(v) # noqa: B023 

314 if not predicate_satisfied: 

315 raise PydanticCustomError( 

316 'predicate_failed', 

317 f'Predicate {predicate_name}failed', # pyright: ignore[reportArgumentType] # noqa: B023 

318 ) 

319 return v 

320 

321 else: 

322 

323 def val_func(v: Any) -> Any: 

324 predicate_satisfied = annotation.func(v) # noqa: B023 

325 if predicate_satisfied: 

326 raise PydanticCustomError( 

327 'not_operation_failed', 

328 f'Not of {predicate_name}failed', # pyright: ignore[reportArgumentType] # noqa: B023 

329 ) 

330 return v 

331 

332 schema = cs.no_info_after_validator_function(val_func, schema) 

333 else: 

334 # ignore any other unknown metadata 

335 return None 

336 

337 if chain_schema_steps: 

338 chain_schema_steps = [schema] + chain_schema_steps 

339 return cs.chain_schema(chain_schema_steps) 

340 

341 return schema 

342 

343 

344def collect_known_metadata(annotations: Iterable[Any]) -> tuple[dict[str, Any], list[Any]]: 

345 """Split `annotations` into known metadata and unknown annotations. 

346 

347 Args: 

348 annotations: An iterable of annotations. 

349 

350 Returns: 

351 A tuple contains a dict of known metadata and a list of unknown annotations. 

352 

353 Example: 

354 ```python 

355 from annotated_types import Gt, Len 

356 

357 from pydantic._internal._known_annotated_metadata import collect_known_metadata 

358 

359 print(collect_known_metadata([Gt(1), Len(42), ...])) 

360 #> ({'gt': 1, 'min_length': 42}, [Ellipsis]) 

361 ``` 

362 """ 

363 annotations = expand_grouped_metadata(annotations) 

364 

365 res: dict[str, Any] = {} 

366 remaining: list[Any] = [] 

367 

368 for annotation in annotations: 

369 # isinstance(annotation, PydanticMetadata) also covers ._fields:_PydanticGeneralMetadata 

370 if isinstance(annotation, PydanticMetadata): 

371 res.update(annotation.__dict__) 

372 # we don't use dataclasses.asdict because that recursively calls asdict on the field values 

373 elif (annotation_type := type(annotation)) in (at_to_constraint_map := _get_at_to_constraint_map()): 

374 constraint = at_to_constraint_map[annotation_type] 

375 res[constraint] = getattr(annotation, constraint) 

376 elif isinstance(annotation, type) and issubclass(annotation, PydanticMetadata): 

377 # also support PydanticMetadata classes being used without initialisation, 

378 # e.g. `Annotated[int, Strict]` as well as `Annotated[int, Strict()]` 

379 res.update({k: v for k, v in vars(annotation).items() if not k.startswith('_')}) 

380 else: 

381 remaining.append(annotation) 

382 # Nones can sneak in but pydantic-core will reject them 

383 # it'd be nice to clean things up so we don't put in None (we probably don't _need_ to, it was just easier) 

384 # but this is simple enough to kick that can down the road 

385 res = {k: v for k, v in res.items() if v is not None} 

386 return res, remaining 

387 

388 

389def check_metadata(metadata: dict[str, Any], allowed: Iterable[str], source_type: Any) -> None: 

390 """A small utility function to validate that the given metadata can be applied to the target. 

391 More than saving lines of code, this gives us a consistent error message for all of our internal implementations. 

392 

393 Args: 

394 metadata: A dict of metadata. 

395 allowed: An iterable of allowed metadata. 

396 source_type: The source type. 

397 

398 Raises: 

399 TypeError: If there is metadatas that can't be applied on source type. 

400 """ 

401 unknown = metadata.keys() - set(allowed) 

402 if unknown: 

403 raise TypeError( 

404 f'The following constraints cannot be applied to {source_type!r}: {", ".join([f"{k!r}" for k in unknown])}' 

405 )