Skip to content

Commit 4df7624

Browse files
authored
Add unified tuple validator that can handle "variadic" tuples via PEP-646 (#865)
1 parent a1b934f commit 4df7624

19 files changed

+603
-518
lines changed

generate_self_schema.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ def type_dict_schema( # noqa: C901
142142
'type': 'union',
143143
'choices': [
144144
schema_ref_validator,
145-
{'type': 'tuple-positional', 'items_schema': [schema_ref_validator, {'type': 'str'}]},
145+
{'type': 'tuple', 'items_schema': [schema_ref_validator, {'type': 'str'}]},
146146
],
147147
},
148148
}

python/pydantic_core/core_schema.py

Lines changed: 74 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1384,16 +1384,7 @@ def list_schema(
13841384
)
13851385

13861386

1387-
class TuplePositionalSchema(TypedDict, total=False):
1388-
type: Required[Literal['tuple-positional']]
1389-
items_schema: Required[List[CoreSchema]]
1390-
extras_schema: CoreSchema
1391-
strict: bool
1392-
ref: str
1393-
metadata: Any
1394-
serialization: IncExSeqOrElseSerSchema
1395-
1396-
1387+
# @deprecated('tuple_positional_schema is deprecated. Use pydantic_core.core_schema.tuple_schema instead.')
13971388
def tuple_positional_schema(
13981389
items_schema: list[CoreSchema],
13991390
*,
@@ -1402,7 +1393,7 @@ def tuple_positional_schema(
14021393
ref: str | None = None,
14031394
metadata: Any = None,
14041395
serialization: IncExSeqOrElseSerSchema | None = None,
1405-
) -> TuplePositionalSchema:
1396+
) -> TupleSchema:
14061397
"""
14071398
Returns a schema that matches a tuple of schemas, e.g.:
14081399
@@ -1427,20 +1418,70 @@ def tuple_positional_schema(
14271418
metadata: Any other information you want to include with the schema, not used by pydantic-core
14281419
serialization: Custom serialization schema
14291420
"""
1430-
return _dict_not_none(
1431-
type='tuple-positional',
1421+
if extras_schema is not None:
1422+
variadic_item_index = len(items_schema)
1423+
items_schema = items_schema + [extras_schema]
1424+
else:
1425+
variadic_item_index = None
1426+
return tuple_schema(
14321427
items_schema=items_schema,
1433-
extras_schema=extras_schema,
1428+
variadic_item_index=variadic_item_index,
14341429
strict=strict,
14351430
ref=ref,
14361431
metadata=metadata,
14371432
serialization=serialization,
14381433
)
14391434

14401435

1441-
class TupleVariableSchema(TypedDict, total=False):
1442-
type: Required[Literal['tuple-variable']]
1443-
items_schema: CoreSchema
1436+
# @deprecated('tuple_variable_schema is deprecated. Use pydantic_core.core_schema.tuple_schema instead.')
1437+
def tuple_variable_schema(
1438+
items_schema: CoreSchema | None = None,
1439+
*,
1440+
min_length: int | None = None,
1441+
max_length: int | None = None,
1442+
strict: bool | None = None,
1443+
ref: str | None = None,
1444+
metadata: Any = None,
1445+
serialization: IncExSeqOrElseSerSchema | None = None,
1446+
) -> TupleSchema:
1447+
"""
1448+
Returns a schema that matches a tuple of a given schema, e.g.:
1449+
1450+
```py
1451+
from pydantic_core import SchemaValidator, core_schema
1452+
1453+
schema = core_schema.tuple_variable_schema(
1454+
items_schema=core_schema.int_schema(), min_length=0, max_length=10
1455+
)
1456+
v = SchemaValidator(schema)
1457+
assert v.validate_python(('1', 2, 3)) == (1, 2, 3)
1458+
```
1459+
1460+
Args:
1461+
items_schema: The value must be a tuple with items that match this schema
1462+
min_length: The value must be a tuple with at least this many items
1463+
max_length: The value must be a tuple with at most this many items
1464+
strict: The value must be a tuple with exactly this many items
1465+
ref: Optional unique identifier of the schema, used to reference the schema in other places
1466+
metadata: Any other information you want to include with the schema, not used by pydantic-core
1467+
serialization: Custom serialization schema
1468+
"""
1469+
return tuple_schema(
1470+
items_schema=[items_schema or any_schema()],
1471+
variadic_item_index=0,
1472+
min_length=min_length,
1473+
max_length=max_length,
1474+
strict=strict,
1475+
ref=ref,
1476+
metadata=metadata,
1477+
serialization=serialization,
1478+
)
1479+
1480+
1481+
class TupleSchema(TypedDict, total=False):
1482+
type: Required[Literal['tuple']]
1483+
items_schema: Required[List[CoreSchema]]
1484+
variadic_item_index: int
14441485
min_length: int
14451486
max_length: int
14461487
strict: bool
@@ -1449,41 +1490,45 @@ class TupleVariableSchema(TypedDict, total=False):
14491490
serialization: IncExSeqOrElseSerSchema
14501491

14511492

1452-
def tuple_variable_schema(
1453-
items_schema: CoreSchema | None = None,
1493+
def tuple_schema(
1494+
items_schema: list[CoreSchema],
14541495
*,
1496+
variadic_item_index: int | None = None,
14551497
min_length: int | None = None,
14561498
max_length: int | None = None,
14571499
strict: bool | None = None,
14581500
ref: str | None = None,
14591501
metadata: Any = None,
14601502
serialization: IncExSeqOrElseSerSchema | None = None,
1461-
) -> TupleVariableSchema:
1503+
) -> TupleSchema:
14621504
"""
1463-
Returns a schema that matches a tuple of a given schema, e.g.:
1505+
Returns a schema that matches a tuple of schemas, with an optional variadic item, e.g.:
14641506
14651507
```py
14661508
from pydantic_core import SchemaValidator, core_schema
14671509
1468-
schema = core_schema.tuple_variable_schema(
1469-
items_schema=core_schema.int_schema(), min_length=0, max_length=10
1510+
schema = core_schema.tuple_schema(
1511+
[core_schema.int_schema(), core_schema.str_schema(), core_schema.float_schema()],
1512+
variadic_item_index=1,
14701513
)
14711514
v = SchemaValidator(schema)
1472-
assert v.validate_python(('1', 2, 3)) == (1, 2, 3)
1515+
assert v.validate_python((1, 'hello', 'world', 1.5)) == (1, 'hello', 'world', 1.5)
14731516
```
14741517
14751518
Args:
1476-
items_schema: The value must be a tuple with items that match this schema
1519+
items_schema: The value must be a tuple with items that match these schemas
1520+
variadic_item_index: The index of the schema in `items_schema` to be treated as variadic (following PEP 646)
14771521
min_length: The value must be a tuple with at least this many items
14781522
max_length: The value must be a tuple with at most this many items
14791523
strict: The value must be a tuple with exactly this many items
1480-
ref: optional unique identifier of the schema, used to reference the schema in other places
1524+
ref: Optional unique identifier of the schema, used to reference the schema in other places
14811525
metadata: Any other information you want to include with the schema, not used by pydantic-core
14821526
serialization: Custom serialization schema
14831527
"""
14841528
return _dict_not_none(
1485-
type='tuple-variable',
1529+
type='tuple',
14861530
items_schema=items_schema,
1531+
variadic_item_index=variadic_item_index,
14871532
min_length=min_length,
14881533
max_length=max_length,
14891534
strict=strict,
@@ -3634,8 +3679,7 @@ def definition_reference_schema(
36343679
IsSubclassSchema,
36353680
CallableSchema,
36363681
ListSchema,
3637-
TuplePositionalSchema,
3638-
TupleVariableSchema,
3682+
TupleSchema,
36393683
SetSchema,
36403684
FrozenSetSchema,
36413685
GeneratorSchema,
@@ -3689,8 +3733,7 @@ def definition_reference_schema(
36893733
'is-subclass',
36903734
'callable',
36913735
'list',
3692-
'tuple-positional',
3693-
'tuple-variable',
3736+
'tuple',
36943737
'set',
36953738
'frozenset',
36963739
'generator',

src/serializers/shared.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,7 @@ combined_serializer! {
140140
Union: super::type_serializers::union::UnionSerializer;
141141
Literal: super::type_serializers::literal::LiteralSerializer;
142142
Recursive: super::type_serializers::definitions::DefinitionRefSerializer;
143-
TuplePositional: super::type_serializers::tuple::TuplePositionalSerializer;
144-
TupleVariable: super::type_serializers::tuple::TupleVariableSerializer;
143+
Tuple: super::type_serializers::tuple::TupleSerializer;
145144
}
146145
}
147146

@@ -248,8 +247,7 @@ impl PyGcTraverse for CombinedSerializer {
248247
CombinedSerializer::Union(inner) => inner.py_gc_traverse(visit),
249248
CombinedSerializer::Literal(inner) => inner.py_gc_traverse(visit),
250249
CombinedSerializer::Recursive(inner) => inner.py_gc_traverse(visit),
251-
CombinedSerializer::TuplePositional(inner) => inner.py_gc_traverse(visit),
252-
CombinedSerializer::TupleVariable(inner) => inner.py_gc_traverse(visit),
250+
CombinedSerializer::Tuple(inner) => inner.py_gc_traverse(visit),
253251
CombinedSerializer::Uuid(inner) => inner.py_gc_traverse(visit),
254252
}
255253
}

0 commit comments

Comments
 (0)