From 53385bc34e5785034571374aa585a3b50af5ffaf Mon Sep 17 00:00:00 2001 From: David Montague <35119617+dmontagu@users.noreply.github.com> Date: Mon, 21 Aug 2023 11:41:41 -0600 Subject: [PATCH] Use tuple-schema from pydantic-core branch --- docs/plugins/conversion_table.py | 16 +++++----- pydantic/_internal/_core_utils.py | 27 +++------------- pydantic/_internal/_generate_schema.py | 4 +++ pydantic/json_schema.py | 43 ++++++++++++++------------ tests/test_utils.py | 28 ----------------- 5 files changed, 40 insertions(+), 78 deletions(-) diff --git a/docs/plugins/conversion_table.py b/docs/plugins/conversion_table.py index 2d27f8bdd0..6a28975332 100644 --- a/docs/plugins/conversion_table.py +++ b/docs/plugins/conversion_table.py @@ -682,50 +682,50 @@ def filtered(self, predicate: typing.Callable[[Row], bool]) -> ConversionTable: tuple, strict=True, python_input=True, - core_schemas=[core_schema.TuplePositionalSchema, core_schema.TupleVariableSchema], + core_schemas=[core_schema.TupleSchema], ), Row( tuple, 'Array', strict=True, json_input=True, - core_schemas=[core_schema.TuplePositionalSchema, core_schema.TupleVariableSchema], + core_schemas=[core_schema.TupleSchema], ), Row( tuple, list, python_input=True, - core_schemas=[core_schema.TuplePositionalSchema, core_schema.TupleVariableSchema], + core_schemas=[core_schema.TupleSchema], ), Row( tuple, set, python_input=True, - core_schemas=[core_schema.TuplePositionalSchema, core_schema.TupleVariableSchema], + core_schemas=[core_schema.TupleSchema], ), Row( tuple, frozenset, python_input=True, - core_schemas=[core_schema.TuplePositionalSchema, core_schema.TupleVariableSchema], + core_schemas=[core_schema.TupleSchema], ), Row( tuple, deque, python_input=True, - core_schemas=[core_schema.TuplePositionalSchema, core_schema.TupleVariableSchema], + core_schemas=[core_schema.TupleSchema], ), Row( tuple, 'dict_keys', python_input=True, - core_schemas=[core_schema.TuplePositionalSchema, core_schema.TupleVariableSchema], + core_schemas=[core_schema.TupleSchema], ), Row( tuple, 'dict_values', python_input=True, - core_schemas=[core_schema.TuplePositionalSchema, core_schema.TupleVariableSchema], + core_schemas=[core_schema.TupleSchema], ), Row( set, diff --git a/pydantic/_internal/_core_utils.py b/pydantic/_internal/_core_utils.py index f53e8ef6c5..82c53ad400 100644 --- a/pydantic/_internal/_core_utils.py +++ b/pydantic/_internal/_core_utils.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections import defaultdict -from typing import Any, Callable, Hashable, Iterable, TypeVar, Union, cast +from typing import Any, Callable, Hashable, Iterable, TypeVar, Union from pydantic_core import CoreSchema, core_schema from typing_extensions import TypeAliasType, TypeGuard, get_args @@ -29,7 +29,7 @@ _CORE_SCHEMA_FIELD_TYPES = {'typed-dict-field', 'dataclass-field', 'model-field', 'computed-field'} _FUNCTION_WITH_INNER_SCHEMA_TYPES = {'function-before', 'function-after', 'function-wrap'} -_LIST_LIKE_SCHEMA_WITH_ITEMS_TYPES = {'list', 'tuple-variable', 'set', 'frozenset'} +_LIST_LIKE_SCHEMA_WITH_ITEMS_TYPES = {'list', 'set', 'frozenset'} def is_core_schema( @@ -52,9 +52,7 @@ def is_function_with_inner_schema( def is_list_like_schema_with_items_schema( schema: CoreSchema, -) -> TypeGuard[ - core_schema.ListSchema | core_schema.TupleVariableSchema | core_schema.SetSchema | core_schema.FrozenSetSchema -]: +) -> TypeGuard[core_schema.ListSchema | core_schema.SetSchema | core_schema.FrozenSetSchema]: return schema['type'] in _LIST_LIKE_SCHEMA_WITH_ITEMS_TYPES @@ -248,23 +246,8 @@ def handle_generator_schema(self, schema: core_schema.GeneratorSchema, f: Walk) schema['items_schema'] = self.walk(items_schema, f) return schema - def handle_tuple_variable_schema( - self, schema: core_schema.TupleVariableSchema | core_schema.TuplePositionalSchema, f: Walk - ) -> core_schema.CoreSchema: - schema = cast(core_schema.TupleVariableSchema, schema) - items_schema = schema.get('items_schema') - if items_schema is not None: - schema['items_schema'] = self.walk(items_schema, f) - return schema - - def handle_tuple_positional_schema( - self, schema: core_schema.TupleVariableSchema | core_schema.TuplePositionalSchema, f: Walk - ) -> core_schema.CoreSchema: - schema = cast(core_schema.TuplePositionalSchema, schema) - schema['items_schema'] = [self.walk(v, f) for v in schema['items_schema']] - extra_schema = schema.get('extra_schema') - if extra_schema is not None: - schema['extra_schema'] = self.walk(extra_schema, f) + def handle_tuple_schema(self, schema: core_schema.TupleSchema, f: Walk) -> core_schema.CoreSchema: + schema['items_schema'] = [self.walk(x, f) for x in schema['items_schema']] return schema def handle_dict_schema(self, schema: core_schema.DictSchema, f: Walk) -> core_schema.CoreSchema: diff --git a/pydantic/_internal/_generate_schema.py b/pydantic/_internal/_generate_schema.py index d9d86e60c8..bb022a0698 100644 --- a/pydantic/_internal/_generate_schema.py +++ b/pydantic/_internal/_generate_schema.py @@ -181,6 +181,10 @@ def apply_each_item_validators( if inner_schema is None: inner_schema = core_schema.any_schema() schema['items_schema'] = apply_validators(inner_schema, each_item_validators, field_name) + elif schema['type'] == 'tuple': + inner_schemas = schema['items_schema'] + if len(inner_schemas) == 1 and schema.get('variadic_item_index') == 0: + schema['items_schema'] = [apply_validators(inner_schemas[0], each_item_validators, field_name)] elif schema['type'] == 'dict': # push down any `each_item=True` validators onto dict _values_ # this is super arbitrary but it's the V1 behavior diff --git a/pydantic/json_schema.py b/pydantic/json_schema.py index 8e753e827f..5db2fcd8de 100644 --- a/pydantic/json_schema.py +++ b/pydantic/json_schema.py @@ -781,7 +781,7 @@ def list_schema(self, schema: core_schema.ListSchema) -> JsonSchemaValue: self.update_with_validations(json_schema, schema, self.ValidationsMapping.array) return json_schema - def tuple_positional_schema(self, schema: core_schema.TuplePositionalSchema) -> JsonSchemaValue: + def tuple_schema(self, schema: core_schema.TupleSchema) -> JsonSchemaValue: """Generates a JSON schema that matches a positional tuple schema e.g. `Tuple[int, str, bool]`. Args: @@ -790,29 +790,32 @@ def tuple_positional_schema(self, schema: core_schema.TuplePositionalSchema) -> Returns: The generated JSON schema. """ - json_schema: JsonSchemaValue = {'type': 'array'} - json_schema['minItems'] = len(schema['items_schema']) - prefixItems = [self.generate_inner(item) for item in schema['items_schema']] - if prefixItems: - json_schema['prefixItems'] = prefixItems - if 'extra_schema' in schema: - json_schema['items'] = self.generate_inner(schema['extra_schema']) + items_schema = schema['items_schema'] + variadic_item_index = schema.get('variadic_item_index') + + if variadic_item_index is None: + prefix_items = [self.generate_inner(item) for item in items_schema] + suffix_items = [] + min_items = len(items_schema) + max_items: int | None = len(items_schema) else: - json_schema['maxItems'] = len(schema['items_schema']) - self.update_with_validations(json_schema, schema, self.ValidationsMapping.array) - return json_schema + prefix_items = [self.generate_inner(item) for item in items_schema[:variadic_item_index]] + # Note the first suffix item might be repeated, but JSON schema can't express this + suffix_items = [self.generate_inner(item) for item in items_schema[variadic_item_index:]] + min_items = len(items_schema) - 1 or None # don't specify min items for variable-length tuple with no min + max_items = None - def tuple_variable_schema(self, schema: core_schema.TupleVariableSchema) -> JsonSchemaValue: - """Generates a JSON schema that matches a variable tuple schema e.g. `Tuple[int, ...]`. + json_schema: JsonSchemaValue = {'type': 'array'} + if min_items is not None: + json_schema['minItems'] = min_items + if max_items is not None: + json_schema['maxItems'] = max_items - Args: - schema: The core schema. + if prefix_items: + json_schema['prefixItems'] = prefix_items + if suffix_items: + json_schema['items'] = self.get_flattened_anyof(suffix_items) - Returns: - The generated JSON schema. - """ - items_schema = {} if 'items_schema' not in schema else self.generate_inner(schema['items_schema']) - json_schema = {'type': 'array', 'items': items_schema} self.update_with_validations(json_schema, schema, self.ValidationsMapping.array) return json_schema diff --git a/tests/test_utils.py b/tests/test_utils.py index c76b53c298..520a906a95 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -542,34 +542,6 @@ def test_camel2snake(value: str, result: str) -> None: assert to_snake(value) == result -@pytest.mark.parametrize( - 'params,expected_extra_schema', - ( - pytest.param({}, {}, id='Positional tuple without extra_schema'), - pytest.param( - {'extra_schema': core_schema.float_schema()}, - {'extra_schema': {'type': 'str'}}, - id='Positional tuple with extra_schema', - ), - ), -) -def test_handle_tuple_positional_schema(params, expected_extra_schema): - schema = core_schema.tuple_positional_schema([core_schema.str_schema()], **params) - - def walk(s, recurse): - # change extra_schema['type'] to 'str' - if s['type'] == 'float': - s['type'] = 'str' - return s - - schema = _WalkCoreSchema().handle_tuple_positional_schema(schema, walk) - assert schema == { - **expected_extra_schema, - 'items_schema': [{'type': 'str'}], - 'type': 'tuple-positional', - } - - @pytest.mark.parametrize( 'params,expected_extra_schema', (