22
33from collections import defaultdict
44from copy import copy
5- from functools import partial
5+ from functools import lru_cache , partial
66from typing import TYPE_CHECKING , Any , Callable , Iterable
77
88from pydantic_core import CoreSchema , PydanticCustomError , to_jsonable_python
1313if TYPE_CHECKING :
1414 from ..annotated_handlers import GetJsonSchemaHandler
1515
16-
1716STRICT = {'strict' }
1817FAIL_FAST = {'fail_fast' }
19- SEQUENCE_CONSTRAINTS = {'min_length' , 'max_length' , * FAIL_FAST }
18+ LENGTH_CONSTRAINTS = {'min_length' , 'max_length' }
2019INEQUALITY = {'le' , 'ge' , 'lt' , 'gt' }
21- NUMERIC_CONSTRAINTS = {'multiple_of' , 'allow_inf_nan' , * INEQUALITY }
20+ NUMERIC_CONSTRAINTS = {'multiple_of' , * INEQUALITY }
21+ ALLOW_INF_NAN = {'allow_inf_nan' }
2222
2323STR_CONSTRAINTS = {
24- * SEQUENCE_CONSTRAINTS ,
24+ * LENGTH_CONSTRAINTS ,
2525 * STRICT ,
2626 'strip_whitespace' ,
2727 'to_lower' ,
2828 'to_upper' ,
2929 'pattern' ,
3030 'coerce_numbers_to_str' ,
3131}
32- BYTES_CONSTRAINTS = {* SEQUENCE_CONSTRAINTS , * STRICT }
32+ BYTES_CONSTRAINTS = {* LENGTH_CONSTRAINTS , * STRICT }
3333
34- LIST_CONSTRAINTS = {* SEQUENCE_CONSTRAINTS , * STRICT , * FAIL_FAST }
35- TUPLE_CONSTRAINTS = {* SEQUENCE_CONSTRAINTS , * STRICT , * FAIL_FAST }
36- SET_CONSTRAINTS = {* SEQUENCE_CONSTRAINTS , * STRICT , * FAIL_FAST }
37- DICT_CONSTRAINTS = {* SEQUENCE_CONSTRAINTS , * STRICT }
38- GENERATOR_CONSTRAINTS = {* SEQUENCE_CONSTRAINTS , * STRICT }
34+ LIST_CONSTRAINTS = {* LENGTH_CONSTRAINTS , * STRICT , * FAIL_FAST }
35+ TUPLE_CONSTRAINTS = {* LENGTH_CONSTRAINTS , * STRICT , * FAIL_FAST }
36+ SET_CONSTRAINTS = {* LENGTH_CONSTRAINTS , * STRICT , * FAIL_FAST }
37+ DICT_CONSTRAINTS = {* LENGTH_CONSTRAINTS , * STRICT }
38+ GENERATOR_CONSTRAINTS = {* LENGTH_CONSTRAINTS , * STRICT }
39+ SEQUENCE_CONSTRAINTS = {* LENGTH_CONSTRAINTS , * FAIL_FAST }
3940
40- FLOAT_CONSTRAINTS = {* NUMERIC_CONSTRAINTS , * STRICT }
41+ FLOAT_CONSTRAINTS = {* NUMERIC_CONSTRAINTS , * ALLOW_INF_NAN , * STRICT }
4142DECIMAL_CONSTRAINTS = {'max_digits' , 'decimal_places' , * FLOAT_CONSTRAINTS }
42- INT_CONSTRAINTS = {* NUMERIC_CONSTRAINTS , * STRICT }
43+ INT_CONSTRAINTS = {* NUMERIC_CONSTRAINTS , * ALLOW_INF_NAN , * STRICT }
4344BOOL_CONSTRAINTS = STRICT
4445UUID_CONSTRAINTS = STRICT
4546
6465NUMERIC_SCHEMA_TYPES = ('float' , 'int' , 'date' , 'time' , 'timedelta' , 'datetime' )
6566
6667CONSTRAINTS_TO_ALLOWED_SCHEMAS : dict [str , set [str ]] = defaultdict (set )
67- for constraint in STR_CONSTRAINTS :
68- CONSTRAINTS_TO_ALLOWED_SCHEMAS [constraint ].update (TEXT_SCHEMA_TYPES )
69- for constraint in BYTES_CONSTRAINTS :
70- CONSTRAINTS_TO_ALLOWED_SCHEMAS [constraint ].update (('bytes' ,))
71- for constraint in LIST_CONSTRAINTS :
72- CONSTRAINTS_TO_ALLOWED_SCHEMAS [constraint ].update (('list' ,))
73- for constraint in TUPLE_CONSTRAINTS :
74- CONSTRAINTS_TO_ALLOWED_SCHEMAS [constraint ].update (('tuple' ,))
75- for constraint in SET_CONSTRAINTS :
76- CONSTRAINTS_TO_ALLOWED_SCHEMAS [constraint ].update (('set' , 'frozenset' ))
77- for constraint in DICT_CONSTRAINTS :
78- CONSTRAINTS_TO_ALLOWED_SCHEMAS [constraint ].update (('dict' ,))
79- for constraint in GENERATOR_CONSTRAINTS :
80- CONSTRAINTS_TO_ALLOWED_SCHEMAS [constraint ].update (('generator' ,))
81- for constraint in FLOAT_CONSTRAINTS :
82- CONSTRAINTS_TO_ALLOWED_SCHEMAS [constraint ].update (('float' ,))
83- for constraint in INT_CONSTRAINTS :
84- CONSTRAINTS_TO_ALLOWED_SCHEMAS [constraint ].update (('int' ,))
85- for constraint in DATE_TIME_CONSTRAINTS :
86- CONSTRAINTS_TO_ALLOWED_SCHEMAS [constraint ].update (('date' , 'time' , 'datetime' ))
87- for constraint in TIMEDELTA_CONSTRAINTS :
88- CONSTRAINTS_TO_ALLOWED_SCHEMAS [constraint ].update (('timedelta' ,))
89- for constraint in TIME_CONSTRAINTS :
90- CONSTRAINTS_TO_ALLOWED_SCHEMAS [constraint ].update (('time' ,))
91- for schema_type in (* TEXT_SCHEMA_TYPES , * SEQUENCE_SCHEMA_TYPES , * NUMERIC_SCHEMA_TYPES , 'typed-dict' , 'model' ):
92- CONSTRAINTS_TO_ALLOWED_SCHEMAS ['strict' ].add (schema_type )
93- for constraint in UNION_CONSTRAINTS :
94- CONSTRAINTS_TO_ALLOWED_SCHEMAS [constraint ].update (('union' ,))
95- for constraint in URL_CONSTRAINTS :
96- CONSTRAINTS_TO_ALLOWED_SCHEMAS [constraint ].update (('url' , 'multi-host-url' ))
97- for constraint in BOOL_CONSTRAINTS :
98- CONSTRAINTS_TO_ALLOWED_SCHEMAS [constraint ].update (('bool' ,))
99- for constraint in UUID_CONSTRAINTS :
100- CONSTRAINTS_TO_ALLOWED_SCHEMAS [constraint ].update (('uuid' ,))
101- for constraint in LAX_OR_STRICT_CONSTRAINTS :
102- CONSTRAINTS_TO_ALLOWED_SCHEMAS [constraint ].update (('lax-or-strict' ,))
103- for constraint in ENUM_CONSTRAINTS :
104- CONSTRAINTS_TO_ALLOWED_SCHEMAS [constraint ].update (('enum' ,))
105- for constraint in DECIMAL_CONSTRAINTS :
106- CONSTRAINTS_TO_ALLOWED_SCHEMAS [constraint ].update (('decimal' ,))
68+
69+ constraint_schema_pairings : list [tuple [set [str ], tuple [str , ...]]] = [
70+ (STR_CONSTRAINTS , TEXT_SCHEMA_TYPES ),
71+ (BYTES_CONSTRAINTS , ('bytes' ,)),
72+ (LIST_CONSTRAINTS , ('list' ,)),
73+ (TUPLE_CONSTRAINTS , ('tuple' ,)),
74+ (SET_CONSTRAINTS , ('set' , 'frozenset' )),
75+ (DICT_CONSTRAINTS , ('dict' ,)),
76+ (GENERATOR_CONSTRAINTS , ('generator' ,)),
77+ (FLOAT_CONSTRAINTS , ('float' ,)),
78+ (INT_CONSTRAINTS , ('int' ,)),
79+ (DATE_TIME_CONSTRAINTS , ('date' , 'time' , 'datetime' )),
80+ (TIMEDELTA_CONSTRAINTS , ('timedelta' ,)),
81+ (TIME_CONSTRAINTS , ('time' ,)),
82+ # TODO: this is a bit redundant, we could probably avoid some of these
83+ (STRICT , (* TEXT_SCHEMA_TYPES , * SEQUENCE_SCHEMA_TYPES , * NUMERIC_SCHEMA_TYPES , 'typed-dict' , 'model' )),
84+ (UNION_CONSTRAINTS , ('union' ,)),
85+ (URL_CONSTRAINTS , ('url' , 'multi-host-url' )),
86+ (BOOL_CONSTRAINTS , ('bool' ,)),
87+ (UUID_CONSTRAINTS , ('uuid' ,)),
88+ (LAX_OR_STRICT_CONSTRAINTS , ('lax-or-strict' ,)),
89+ (ENUM_CONSTRAINTS , ('enum' ,)),
90+ (DECIMAL_CONSTRAINTS , ('decimal' ,)),
91+ ]
92+
93+ for constraints , schemas in constraint_schema_pairings :
94+ for c in constraints :
95+ CONSTRAINTS_TO_ALLOWED_SCHEMAS [c ].update (schemas )
10796
10897
10998def add_js_update_schema (s : cs .CoreSchema , f : Callable [[], dict [str , Any ]]) -> None :
@@ -168,6 +157,28 @@ def expand_grouped_metadata(annotations: Iterable[Any]) -> Iterable[Any]:
168157 yield annotation
169158
170159
160+ @lru_cache
161+ def _get_at_to_constraint_map () -> dict [type , str ]:
162+ """Return a mapping of annotated types to constraints.
163+
164+ Normally, we would define a mapping like this in the module scope, but we can't do that
165+ because we don't permit module level imports of `annotated_types`, in an attempt to speed up
166+ the import time of `pydantic`. We still only want to have this dictionary defined in one place,
167+ so we use this function to cache the result.
168+ """
169+ import annotated_types as at
170+
171+ return {
172+ at .Gt : 'gt' ,
173+ at .Ge : 'ge' ,
174+ at .Lt : 'lt' ,
175+ at .Le : 'le' ,
176+ at .MultipleOf : 'multiple_of' ,
177+ at .MinLen : 'min_length' ,
178+ at .MaxLen : 'max_length' ,
179+ }
180+
181+
171182def apply_known_metadata (annotation : Any , schema : CoreSchema ) -> CoreSchema | None : # noqa: C901
172183 """Apply `annotation` to `schema` if it is an annotation we know about (Gt, Le, etc.).
173184 Otherwise return `None`.
@@ -189,32 +200,19 @@ def apply_known_metadata(annotation: Any, schema: CoreSchema) -> CoreSchema | No
189200 """
190201 import annotated_types as at
191202
192- from . import _validators
193-
194- COMPARISON_VALIDATORS = {
195- 'gt' : _validators .greater_than_validator ,
196- 'ge' : _validators .greater_than_or_equal_validator ,
197- 'lt' : _validators .less_than_validator ,
198- 'le' : _validators .less_than_or_equal_validator ,
199- 'multiple_of' : _validators .multiple_of_validator ,
200- 'min_length' : _validators .min_length_validator ,
201- 'max_length' : _validators .max_length_validator ,
202- }
203-
204- CONSTRAINT_STR_FROM_ANNOTATED_TYPE = {
205- at .Gt : 'gt' ,
206- at .Ge : 'ge' ,
207- at .Lt : 'lt' ,
208- at .Le : 'le' ,
209- at .MultipleOf : 'multiple_of' ,
210- at .MinLen : 'min_length' ,
211- at .MaxLen : 'max_length' ,
212- }
203+ from ._validators import forbid_inf_nan_check , get_constraint_validator
213204
214205 schema = schema .copy ()
215206 schema_update , other_metadata = collect_known_metadata ([annotation ])
216207 schema_type = schema ['type' ]
217208
209+ chain_schema_constraints : set [str ] = {
210+ 'pattern' ,
211+ 'strip_whitespace' ,
212+ 'to_lower' ,
213+ 'to_upper' ,
214+ 'coerce_numbers_to_str' ,
215+ }
218216 chain_schema_steps : list [CoreSchema ] = []
219217
220218 for constraint , value in schema_update .items ():
@@ -224,6 +222,8 @@ def apply_known_metadata(annotation: Any, schema: CoreSchema) -> CoreSchema | No
224222
225223 # if it becomes necessary to handle more than one constraint
226224 # in this recursive case with function-after or function-wrap, we should refactor
225+ # this is a bit challenging because we sometimes want to apply constraints to the inner schema,
226+ # whereas other times we want to wrap the existing schema with a new one that enforces a new constraint.
227227 if schema_type in {'function-before' , 'function-wrap' , 'function-after' } and constraint == 'strict' :
228228 schema ['schema' ] = apply_known_metadata (annotation , schema ['schema' ]) # type: ignore # schema is function-after schema
229229 return schema
@@ -235,40 +235,42 @@ def apply_known_metadata(annotation: Any, schema: CoreSchema) -> CoreSchema | No
235235 schema [constraint ] = value
236236 continue
237237
238- if constraint in { 'pattern' , 'strip_whitespace' , 'to_lower' , 'to_upper' , 'coerce_numbers_to_str' } :
238+ if constraint in chain_schema_constraints :
239239 chain_schema_steps .append (cs .str_schema (** {constraint : value }))
240- elif constraint in {'gt' , 'ge' , 'lt' , 'le' , 'multiple_of' , 'min_length' , 'max_length' }:
241- if constraint == 'multiple_of' :
242- json_schema_constraint = 'multiple_of'
243- elif constraint in {'min_length' , 'max_length' }:
244- if schema ['type' ] == 'list' or (
245- schema ['type' ] == 'json-or-python' and schema ['json_schema' ]['type' ] == 'list'
240+ elif constraint in {* NUMERIC_CONSTRAINTS , * LENGTH_CONSTRAINTS }:
241+ if constraint in NUMERIC_CONSTRAINTS :
242+ json_schema_constraint = constraint
243+ elif constraint in LENGTH_CONSTRAINTS :
244+ inner_schema = schema
245+ while inner_schema ['type' ] in {'function-before' , 'function-wrap' , 'function-after' }:
246+ inner_schema = inner_schema ['schema' ] # type: ignore
247+ inner_schema_type = inner_schema ['type' ]
248+ if inner_schema_type == 'list' or (
249+ inner_schema_type == 'json-or-python' and inner_schema ['json_schema' ]['type' ] == 'list' # type: ignore
246250 ):
247251 json_schema_constraint = 'minItems' if constraint == 'min_length' else 'maxItems'
248252 else :
249253 json_schema_constraint = 'minLength' if constraint == 'min_length' else 'maxLength'
250- else :
251- json_schema_constraint = constraint
252254
253255 schema = cs .no_info_after_validator_function (
254- partial (COMPARISON_VALIDATORS [ constraint ] , ** {constraint : value }), schema
256+ partial (get_constraint_validator ( constraint ) , ** {constraint : value }), schema
255257 )
256-
257258 add_js_update_schema (schema , lambda : {json_schema_constraint : as_jsonable_value (value )})
258259 elif constraint == 'allow_inf_nan' and value is False :
259260 schema = cs .no_info_after_validator_function (
260- _validators . forbid_inf_nan_check ,
261+ forbid_inf_nan_check ,
261262 schema ,
262263 )
263264 else :
264265 raise RuntimeError (f'Unable to apply constraint { constraint } to schema { schema_type } ' )
265266
266267 for annotation in other_metadata :
267- if isinstance ( annotation , ( at . Gt , at . Ge , at . Lt , at . Le , at . MultipleOf , at . MinLen , at . MaxLen )):
268- constraint = CONSTRAINT_STR_FROM_ANNOTATED_TYPE [ type ( annotation ) ]
268+ if ( annotation_type := type ( annotation )) in ( at_to_constraint_map := _get_at_to_constraint_map ( )):
269+ constraint = at_to_constraint_map [ annotation_type ]
269270 schema = cs .no_info_after_validator_function (
270- partial (COMPARISON_VALIDATORS [ constraint ] , {constraint : getattr (annotation , constraint )}), schema
271+ partial (get_constraint_validator ( constraint ) , {constraint : getattr (annotation , constraint )}), schema
271272 )
273+ continue
272274 elif isinstance (annotation , at .Predicate ):
273275 predicate_name = f'{ annotation .func .__qualname__ } ' if hasattr (annotation .func , '__qualname__' ) else ''
274276
@@ -281,9 +283,10 @@ def val_func(v: Any) -> Any:
281283 )
282284 return v
283285
284- return cs .no_info_after_validator_function (val_func , schema )
285- # ignore any other unknown metadata
286- return None
286+ schema = cs .no_info_after_validator_function (val_func , schema )
287+ else :
288+ # ignore any other unknown metadata
289+ return None
287290
288291 if chain_schema_steps :
289292 chain_schema_steps = [schema ] + chain_schema_steps
@@ -311,31 +314,19 @@ def collect_known_metadata(annotations: Iterable[Any]) -> tuple[dict[str, Any],
311314 #> ({'gt': 1, 'min_length': 42}, [Ellipsis])
312315 ```
313316 """
314- import annotated_types as at
315-
316317 annotations = expand_grouped_metadata (annotations )
317318
318319 res : dict [str , Any ] = {}
319320 remaining : list [Any ] = []
321+
320322 for annotation in annotations :
321323 # isinstance(annotation, PydanticMetadata) also covers ._fields:_PydanticGeneralMetadata
322324 if isinstance (annotation , PydanticMetadata ):
323325 res .update (annotation .__dict__ )
324326 # we don't use dataclasses.asdict because that recursively calls asdict on the field values
325- elif isinstance (annotation , at .MinLen ):
326- res .update ({'min_length' : annotation .min_length })
327- elif isinstance (annotation , at .MaxLen ):
328- res .update ({'max_length' : annotation .max_length })
329- elif isinstance (annotation , at .Gt ):
330- res .update ({'gt' : annotation .gt })
331- elif isinstance (annotation , at .Ge ):
332- res .update ({'ge' : annotation .ge })
333- elif isinstance (annotation , at .Lt ):
334- res .update ({'lt' : annotation .lt })
335- elif isinstance (annotation , at .Le ):
336- res .update ({'le' : annotation .le })
337- elif isinstance (annotation , at .MultipleOf ):
338- res .update ({'multiple_of' : annotation .multiple_of })
327+ elif (annotation_type := type (annotation )) in (at_to_constraint_map := _get_at_to_constraint_map ()):
328+ constraint = at_to_constraint_map [annotation_type ]
329+ res [constraint ] = getattr (annotation , constraint )
339330 elif isinstance (annotation , type ) and issubclass (annotation , PydanticMetadata ):
340331 # also support PydanticMetadata classes being used without initialisation,
341332 # e.g. `Annotated[int, Strict]` as well as `Annotated[int, Strict()]`
0 commit comments