Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add unified tuple validator that can handle "variadic" tuples via PEP-646 #865

Merged
merged 1 commit into from Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion generate_self_schema.py
Expand Up @@ -142,7 +142,7 @@ def type_dict_schema( # noqa: C901
'type': 'union',
'choices': [
schema_ref_validator,
{'type': 'tuple-positional', 'items_schema': [schema_ref_validator, {'type': 'str'}]},
{'type': 'tuple', 'items_schema': [schema_ref_validator, {'type': 'str'}]},
],
},
}
Expand Down
105 changes: 74 additions & 31 deletions python/pydantic_core/core_schema.py
Expand Up @@ -1384,16 +1384,7 @@ def list_schema(
)


class TuplePositionalSchema(TypedDict, total=False):
type: Required[Literal['tuple-positional']]
items_schema: Required[List[CoreSchema]]
extras_schema: CoreSchema
strict: bool
ref: str
metadata: Any
serialization: IncExSeqOrElseSerSchema


# @deprecated('tuple_positional_schema is deprecated. Use pydantic_core.core_schema.tuple_schema instead.')
def tuple_positional_schema(
items_schema: list[CoreSchema],
*,
Expand All @@ -1402,7 +1393,7 @@ def tuple_positional_schema(
ref: str | None = None,
metadata: Any = None,
serialization: IncExSeqOrElseSerSchema | None = None,
) -> TuplePositionalSchema:
) -> TupleSchema:
"""
Returns a schema that matches a tuple of schemas, e.g.:

Expand All @@ -1427,20 +1418,70 @@ def tuple_positional_schema(
metadata: Any other information you want to include with the schema, not used by pydantic-core
serialization: Custom serialization schema
"""
return _dict_not_none(
type='tuple-positional',
if extras_schema is not None:
variadic_item_index = len(items_schema)
items_schema = items_schema + [extras_schema]
else:
variadic_item_index = None
return tuple_schema(
items_schema=items_schema,
extras_schema=extras_schema,
variadic_item_index=variadic_item_index,
strict=strict,
ref=ref,
metadata=metadata,
serialization=serialization,
)


class TupleVariableSchema(TypedDict, total=False):
type: Required[Literal['tuple-variable']]
items_schema: CoreSchema
# @deprecated('tuple_variable_schema is deprecated. Use pydantic_core.core_schema.tuple_schema instead.')
def tuple_variable_schema(
items_schema: CoreSchema | None = None,
*,
min_length: int | None = None,
max_length: int | None = None,
strict: bool | None = None,
ref: str | None = None,
metadata: Any = None,
serialization: IncExSeqOrElseSerSchema | None = None,
) -> TupleSchema:
"""
Returns a schema that matches a tuple of a given schema, e.g.:

```py
from pydantic_core import SchemaValidator, core_schema

schema = core_schema.tuple_variable_schema(
items_schema=core_schema.int_schema(), min_length=0, max_length=10
)
v = SchemaValidator(schema)
assert v.validate_python(('1', 2, 3)) == (1, 2, 3)
```

Args:
items_schema: The value must be a tuple with items that match this schema
min_length: The value must be a tuple with at least this many items
max_length: The value must be a tuple with at most this many items
strict: The value must be a tuple with exactly this many items
ref: Optional unique identifier of the schema, used to reference the schema in other places
metadata: Any other information you want to include with the schema, not used by pydantic-core
serialization: Custom serialization schema
"""
return tuple_schema(
items_schema=[items_schema or any_schema()],
variadic_item_index=0,
min_length=min_length,
max_length=max_length,
strict=strict,
ref=ref,
metadata=metadata,
serialization=serialization,
)


class TupleSchema(TypedDict, total=False):
type: Required[Literal['tuple']]
items_schema: Required[List[CoreSchema]]
variadic_item_index: int
min_length: int
max_length: int
strict: bool
Expand All @@ -1449,41 +1490,45 @@ class TupleVariableSchema(TypedDict, total=False):
serialization: IncExSeqOrElseSerSchema


def tuple_variable_schema(
items_schema: CoreSchema | None = None,
def tuple_schema(
items_schema: list[CoreSchema],
*,
variadic_item_index: int | None = None,
min_length: int | None = None,
max_length: int | None = None,
strict: bool | None = None,
ref: str | None = None,
metadata: Any = None,
serialization: IncExSeqOrElseSerSchema | None = None,
) -> TupleVariableSchema:
) -> TupleSchema:
"""
Returns a schema that matches a tuple of a given schema, e.g.:
Returns a schema that matches a tuple of schemas, with an optional variadic item, e.g.:

```py
from pydantic_core import SchemaValidator, core_schema

schema = core_schema.tuple_variable_schema(
items_schema=core_schema.int_schema(), min_length=0, max_length=10
schema = core_schema.tuple_schema(
[core_schema.int_schema(), core_schema.str_schema(), core_schema.float_schema()],
variadic_item_index=1,
)
v = SchemaValidator(schema)
assert v.validate_python(('1', 2, 3)) == (1, 2, 3)
assert v.validate_python((1, 'hello', 'world', 1.5)) == (1, 'hello', 'world', 1.5)
```

Args:
items_schema: The value must be a tuple with items that match this schema
items_schema: The value must be a tuple with items that match these schemas
variadic_item_index: The index of the schema in `items_schema` to be treated as variadic (following PEP 646)
min_length: The value must be a tuple with at least this many items
max_length: The value must be a tuple with at most this many items
strict: The value must be a tuple with exactly this many items
ref: optional unique identifier of the schema, used to reference the schema in other places
ref: Optional unique identifier of the schema, used to reference the schema in other places
metadata: Any other information you want to include with the schema, not used by pydantic-core
serialization: Custom serialization schema
"""
return _dict_not_none(
type='tuple-variable',
type='tuple',
items_schema=items_schema,
variadic_item_index=variadic_item_index,
min_length=min_length,
max_length=max_length,
strict=strict,
Expand Down Expand Up @@ -3634,8 +3679,7 @@ def definition_reference_schema(
IsSubclassSchema,
CallableSchema,
ListSchema,
TuplePositionalSchema,
TupleVariableSchema,
TupleSchema,
SetSchema,
FrozenSetSchema,
GeneratorSchema,
Expand Down Expand Up @@ -3689,8 +3733,7 @@ def definition_reference_schema(
'is-subclass',
'callable',
'list',
'tuple-positional',
'tuple-variable',
'tuple',
'set',
'frozenset',
'generator',
Expand Down
6 changes: 2 additions & 4 deletions src/serializers/shared.rs
Expand Up @@ -140,8 +140,7 @@ combined_serializer! {
Union: super::type_serializers::union::UnionSerializer;
Literal: super::type_serializers::literal::LiteralSerializer;
Recursive: super::type_serializers::definitions::DefinitionRefSerializer;
TuplePositional: super::type_serializers::tuple::TuplePositionalSerializer;
TupleVariable: super::type_serializers::tuple::TupleVariableSerializer;
Tuple: super::type_serializers::tuple::TupleSerializer;
}
}

Expand Down Expand Up @@ -248,8 +247,7 @@ impl PyGcTraverse for CombinedSerializer {
CombinedSerializer::Union(inner) => inner.py_gc_traverse(visit),
CombinedSerializer::Literal(inner) => inner.py_gc_traverse(visit),
CombinedSerializer::Recursive(inner) => inner.py_gc_traverse(visit),
CombinedSerializer::TuplePositional(inner) => inner.py_gc_traverse(visit),
CombinedSerializer::TupleVariable(inner) => inner.py_gc_traverse(visit),
CombinedSerializer::Tuple(inner) => inner.py_gc_traverse(visit),
CombinedSerializer::Uuid(inner) => inner.py_gc_traverse(visit),
}
}
Expand Down