diff --git a/mkdocs.yml b/mkdocs.yml index 917cc55452..a090debb1e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -171,7 +171,7 @@ plugins: options: members_order: source separate_signature: true - filters: ["!^_"] + filters: ["!^_(?!defer_build_mode)"] docstring_options: ignore_init_summary: true merge_init_into_class: true diff --git a/pydantic/_internal/_config.py b/pydantic/_internal/_config.py index 027aee8f20..01ba57143c 100644 --- a/pydantic/_internal/_config.py +++ b/pydantic/_internal/_config.py @@ -75,6 +75,7 @@ class ConfigWrapper: protected_namespaces: tuple[str, ...] hide_input_in_errors: bool defer_build: bool + _defer_build_mode: tuple[Literal['model', 'type_adapter'], ...] plugin_settings: dict[str, object] | None schema_generator: type[GenerateSchema] | None json_schema_serialization_defaults_required: bool @@ -254,6 +255,7 @@ def push(self, config_wrapper: ConfigWrapper | ConfigDict | None): hide_input_in_errors=False, json_encoders=None, defer_build=False, + _defer_build_mode=('model',), plugin_settings=None, schema_generator=None, json_schema_serialization_defaults_required=False, diff --git a/pydantic/_internal/_generate_schema.py b/pydantic/_internal/_generate_schema.py index 8cd46c887e..8c0d221196 100644 --- a/pydantic/_internal/_generate_schema.py +++ b/pydantic/_internal/_generate_schema.py @@ -76,6 +76,7 @@ from ._fields import collect_dataclass_fields, get_type_hints_infer_globalns from ._forward_ref import PydanticRecursiveRef from ._generics import get_standard_typevars_map, has_instance_in_type, recursively_defined_type_refs, replace_types +from ._mock_val_ser import MockCoreSchema from ._schema_generation_shared import CallbackGetCoreSchemaHandler from ._typing_extra import is_finalvar, is_self_type from ._utils import lenient_issubclass @@ -646,9 +647,11 @@ def _generate_schema_from_property(self, obj: Any, source: Any) -> core_schema.C source, CallbackGetCoreSchemaHandler(self._generate_schema_inner, self, ref_mode=ref_mode) ) # fmt: off - elif (existing_schema := getattr(obj, '__pydantic_core_schema__', None)) is not None and existing_schema.get( - 'cls', None - ) == obj: + elif ( + (existing_schema := getattr(obj, '__pydantic_core_schema__', None)) is not None + and not isinstance(existing_schema, MockCoreSchema) + and existing_schema.get('cls', None) == obj + ): schema = existing_schema # fmt: on elif (validators := getattr(obj, '__get_validators__', None)) is not None: diff --git a/pydantic/_internal/_mock_val_ser.py b/pydantic/_internal/_mock_val_ser.py index b303fed21d..063918a60c 100644 --- a/pydantic/_internal/_mock_val_ser.py +++ b/pydantic/_internal/_mock_val_ser.py @@ -1,8 +1,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Callable, Generic, TypeVar +from typing import TYPE_CHECKING, Any, Callable, Generic, Iterator, Mapping, TypeVar -from pydantic_core import SchemaSerializer, SchemaValidator +from pydantic_core import CoreSchema, SchemaSerializer, SchemaValidator from typing_extensions import Literal from ..errors import PydanticErrorCodes, PydanticUserError @@ -13,6 +13,57 @@ ValSer = TypeVar('ValSer', SchemaValidator, SchemaSerializer) +T = TypeVar('T') + + +class MockCoreSchema(Mapping[str, Any]): + """Mocker for `pydantic_core.CoreSchema` which optionally attempts to + rebuild the thing it's mocking when one of its methods is accessed and raises an error if that fails. + """ + + __slots__ = '_error_message', '_code', '_attempt_rebuild', '_built_memo' + + def __init__( + self, + error_message: str, + *, + code: PydanticErrorCodes, + attempt_rebuild: Callable[[], CoreSchema | None] | None = None, + ) -> None: + self._error_message = error_message + self._code: PydanticErrorCodes = code + self._attempt_rebuild = attempt_rebuild + self._built_memo: CoreSchema | None = None + + def __getitem__(self, key: str) -> Any: + return self._get_built().__getitem__(key) + + def __len__(self) -> int: + return self._get_built().__len__() + + def __iter__(self) -> Iterator[str]: + return self._get_built().__iter__() + + def _get_built(self) -> CoreSchema: + if self._built_memo is not None: + return self._built_memo + + if self._attempt_rebuild: + schema = self._attempt_rebuild() + if schema is not None: + self._built_memo = schema + return schema + raise PydanticUserError(self._error_message, code=self._code) + + def rebuild(self) -> CoreSchema | None: + self._built_memo = None + if self._attempt_rebuild: + val_ser = self._attempt_rebuild() + if val_ser is not None: + return val_ser + else: + raise PydanticUserError(self._error_message, code=self._code) + return None class MockValSer(Generic[ValSer]): @@ -69,30 +120,31 @@ def set_model_mocks(cls: type[BaseModel], cls_name: str, undefined_name: str = ' f' then call `{cls_name}.model_rebuild()`.' ) - def attempt_rebuild_validator() -> SchemaValidator | None: - if cls.model_rebuild(raise_errors=False, _parent_namespace_depth=5) is not False: - return cls.__pydantic_validator__ - else: - return None + def attempt_rebuild_fn(attr_fn: Callable[[type[BaseModel]], T]) -> Callable[[], T | None]: + def handler() -> T | None: + if cls.model_rebuild(raise_errors=False, _parent_namespace_depth=5) is not False: + return attr_fn(cls) + else: + return None + return handler + + cls.__pydantic_core_schema__ = MockCoreSchema( # type: ignore[assignment] + undefined_type_error_message, + code='class-not-fully-defined', + attempt_rebuild=attempt_rebuild_fn(lambda c: c.__pydantic_core_schema__), + ) cls.__pydantic_validator__ = MockValSer( # type: ignore[assignment] undefined_type_error_message, code='class-not-fully-defined', val_or_ser='validator', - attempt_rebuild=attempt_rebuild_validator, + attempt_rebuild=attempt_rebuild_fn(lambda c: c.__pydantic_validator__), ) - - def attempt_rebuild_serializer() -> SchemaSerializer | None: - if cls.model_rebuild(raise_errors=False, _parent_namespace_depth=5) is not False: - return cls.__pydantic_serializer__ - else: - return None - cls.__pydantic_serializer__ = MockValSer( # type: ignore[assignment] undefined_type_error_message, code='class-not-fully-defined', val_or_ser='serializer', - attempt_rebuild=attempt_rebuild_serializer, + attempt_rebuild=attempt_rebuild_fn(lambda c: c.__pydantic_serializer__), ) @@ -113,28 +165,29 @@ def set_dataclass_mocks( f' then call `pydantic.dataclasses.rebuild_dataclass({cls_name})`.' ) - def attempt_rebuild_validator() -> SchemaValidator | None: - if rebuild_dataclass(cls, raise_errors=False, _parent_namespace_depth=5) is not False: - return cls.__pydantic_validator__ - else: - return None + def attempt_rebuild_fn(attr_fn: Callable[[type[PydanticDataclass]], T]) -> Callable[[], T | None]: + def handler() -> T | None: + if rebuild_dataclass(cls, raise_errors=False, _parent_namespace_depth=5) is not False: + return attr_fn(cls) + else: + return None + + return handler + cls.__pydantic_core_schema__ = MockCoreSchema( # type: ignore[assignment] + undefined_type_error_message, + code='class-not-fully-defined', + attempt_rebuild=attempt_rebuild_fn(lambda c: c.__pydantic_core_schema__), + ) cls.__pydantic_validator__ = MockValSer( # type: ignore[assignment] undefined_type_error_message, code='class-not-fully-defined', val_or_ser='validator', - attempt_rebuild=attempt_rebuild_validator, + attempt_rebuild=attempt_rebuild_fn(lambda c: c.__pydantic_validator__), ) - - def attempt_rebuild_serializer() -> SchemaSerializer | None: - if rebuild_dataclass(cls, raise_errors=False, _parent_namespace_depth=5) is not False: - return cls.__pydantic_serializer__ - else: - return None - cls.__pydantic_serializer__ = MockValSer( # type: ignore[assignment] undefined_type_error_message, code='class-not-fully-defined', val_or_ser='validator', - attempt_rebuild=attempt_rebuild_serializer, + attempt_rebuild=attempt_rebuild_fn(lambda c: c.__pydantic_serializer__), ) diff --git a/pydantic/_internal/_model_construction.py b/pydantic/_internal/_model_construction.py index 86bdba2b8e..2d64bfd509 100644 --- a/pydantic/_internal/_model_construction.py +++ b/pydantic/_internal/_model_construction.py @@ -23,7 +23,7 @@ from ._fields import collect_model_fields, is_valid_field_name, is_valid_privateattr_name from ._generate_schema import GenerateSchema from ._generics import PydanticGenericMetadata, get_model_typevars_map -from ._mock_val_ser import MockValSer, set_model_mocks +from ._mock_val_ser import set_model_mocks from ._schema_generation_shared import CallbackGetCoreSchemaHandler from ._signature import generate_pydantic_signature from ._typing_extra import get_cls_types_namespace, is_annotated, is_classvar, parent_frame_namespace @@ -231,14 +231,6 @@ def __getattr__(self, item: str) -> Any: private_attributes = self.__dict__.get('__private_attributes__') if private_attributes and item in private_attributes: return private_attributes[item] - if item == '__pydantic_core_schema__': - # This means the class didn't get a schema generated for it, likely because there was an undefined reference - maybe_mock_validator = getattr(self, '__pydantic_validator__', None) - if isinstance(maybe_mock_validator, MockValSer): - rebuilt_validator = maybe_mock_validator.rebuild() - if rebuilt_validator is not None: - # In this case, a validator was built, and so `__pydantic_core_schema__` should now be set - return getattr(self, '__pydantic_core_schema__') raise AttributeError(item) @classmethod @@ -531,7 +523,7 @@ def complete_model_class( ref_mode='unpack', ) - if config_wrapper.defer_build: + if config_wrapper.defer_build and 'model' in config_wrapper._defer_build_mode: set_model_mocks(cls, cls_name) return False diff --git a/pydantic/_internal/_typing_extra.py b/pydantic/_internal/_typing_extra.py index 087353713f..3b10cec2d6 100644 --- a/pydantic/_internal/_typing_extra.py +++ b/pydantic/_internal/_typing_extra.py @@ -114,6 +114,10 @@ def is_annotated(ann_type: Any) -> bool: return origin is not None and lenient_issubclass(origin, Annotated) +def annotated_type(type_: Any) -> Any | None: + return get_args(type_)[0] if is_annotated(type_) else None + + def is_namedtuple(type_: type[Any]) -> bool: """Check if a given class is a named tuple. It can be either a `typing.NamedTuple` or `collections.namedtuple`. diff --git a/pydantic/config.py b/pydantic/config.py index 7edf7c60a0..cc31bf2a8c 100644 --- a/pydantic/config.py +++ b/pydantic/config.py @@ -709,11 +709,35 @@ class Model(BaseModel): defer_build: bool """ - Whether to defer model validator and serializer construction until the first model validation. + Whether to defer model validator and serializer construction until the first model validation. Defaults to False. This can be useful to avoid the overhead of building models which are only used nested within other models, or when you want to manually define type namespace via - [`Model.model_rebuild(_types_namespace=...)`][pydantic.BaseModel.model_rebuild]. Defaults to False. + [`Model.model_rebuild(_types_namespace=...)`][pydantic.BaseModel.model_rebuild]. + + See also [`_defer_build_mode`][pydantic.config.ConfigDict._defer_build_mode]. + + !!! note + `defer_build` does not work by default with FastAPI Pydantic models. By default, the validator and serializer + for said models is constructed immediately for FastAPI routes. You also need to define + [`_defer_build_mode=('model', 'type_adapter')`][pydantic.config.ConfigDict._defer_build_mode] with FastAPI + models in order for `defer_build=True` to take effect. This additional (experimental) parameter is required for + the deferred building due to FastAPI relying on `TypeAdapter`s. + """ + + _defer_build_mode: tuple[Literal['model', 'type_adapter'], ...] + """ + Controls when [`defer_build`][pydantic.config.ConfigDict.defer_build] is applicable. Defaults to `('model',)`. + + Due to backwards compatibility reasons [`TypeAdapter`][pydantic.type_adapter.TypeAdapter] does not by default + respect `defer_build`. Meaning when `defer_build` is `True` and `_defer_build_mode` is the default `('model',)` + then `TypeAdapter` immediately constructs its validator and serializer instead of postponing said construction until + the first model validation. Set this to `('model', 'type_adapter')` to make `TypeAdapter` respect the `defer_build` + so it postpones validator and serializer construction until the first validation or serialization. + + !!! note + The `_defer_build_mode` parameter is named with an underscore to suggest this is an experimental feature. It may + be removed or changed in the future in a minor release. """ plugin_settings: dict[str, object] | None diff --git a/pydantic/json_schema.py b/pydantic/json_schema.py index 8dfde7f985..f877afc7a6 100644 --- a/pydantic/json_schema.py +++ b/pydantic/json_schema.py @@ -2226,12 +2226,14 @@ def model_json_schema( from .main import BaseModel schema_generator_instance = schema_generator(by_alias=by_alias, ref_template=ref_template) - if isinstance(cls.__pydantic_validator__, _mock_val_ser.MockValSer): - cls.__pydantic_validator__.rebuild() + + if isinstance(cls.__pydantic_core_schema__, _mock_val_ser.MockCoreSchema): + cls.__pydantic_core_schema__.rebuild() if cls is BaseModel: raise AttributeError('model_json_schema() must be called on a subclass of BaseModel, not BaseModel itself.') - assert '__pydantic_core_schema__' in cls.__dict__, 'this is a bug! please report it' + + assert not isinstance(cls.__pydantic_core_schema__, _mock_val_ser.MockCoreSchema), 'this is a bug! please report it' return schema_generator_instance.generate(cls.__pydantic_core_schema__, mode=mode) @@ -2263,8 +2265,8 @@ def models_json_schema( element, along with the optional title and description keys. """ for cls, _ in models: - if isinstance(cls.__pydantic_validator__, _mock_val_ser.MockValSer): - cls.__pydantic_validator__.rebuild() + if isinstance(cls.__pydantic_core_schema__, _mock_val_ser.MockCoreSchema): + cls.__pydantic_core_schema__.rebuild() instance = schema_generator(by_alias=by_alias, ref_template=ref_template) inputs = [(m, mode, m.__pydantic_core_schema__) for m, mode in models] diff --git a/pydantic/main.py b/pydantic/main.py index 684f58bb1f..93da879314 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -145,6 +145,10 @@ class BaseModel(metaclass=_model_construction.ModelMetaclass): __pydantic_decorators__ = _decorators.DecoratorInfos() __pydantic_parent_namespace__ = None # Prevent `BaseModel` from being instantiated directly: + __pydantic_core_schema__ = _mock_val_ser.MockCoreSchema( + 'Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly', + code='base-model-instantiated', + ) __pydantic_validator__ = _mock_val_ser.MockValSer( 'Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly', val_or_ser='validator', @@ -594,7 +598,8 @@ def __get_pydantic_core_schema__(cls, source: type[BaseModel], handler: GetCoreS """ # Only use the cached value from this _exact_ class; we don't want one from a parent class # This is why we check `cls.__dict__` and don't use `cls.__pydantic_core_schema__` or similar. - if '__pydantic_core_schema__' in cls.__dict__: + schema = cls.__dict__.get('__pydantic_core_schema__') + if schema is not None and not isinstance(schema, _mock_val_ser.MockCoreSchema): # Due to the way generic classes are built, it's possible that an invalid schema may be temporarily # set on generic classes. I think we could resolve this to ensure that we get proper schema caching # for generics, but for simplicity for now, we just always rebuild if the class has a generic origin. diff --git a/pydantic/type_adapter.py b/pydantic/type_adapter.py index 81a1fb54f8..468f3d2f4c 100644 --- a/pydantic/type_adapter.py +++ b/pydantic/type_adapter.py @@ -2,16 +2,32 @@ from __future__ import annotations as _annotations import sys +from contextlib import contextmanager from dataclasses import is_dataclass -from typing import TYPE_CHECKING, Any, Dict, Generic, Iterable, Set, TypeVar, Union, cast, final, overload +from functools import cached_property, wraps +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Generic, + Iterable, + Iterator, + Set, + TypeVar, + Union, + cast, + final, + overload, +) from pydantic_core import CoreSchema, SchemaSerializer, SchemaValidator, Some -from typing_extensions import Literal, get_args, is_typeddict +from typing_extensions import Literal, is_typeddict from pydantic.errors import PydanticUserError from pydantic.main import BaseModel -from ._internal import _config, _generate_schema, _typing_extra +from ._internal import _config, _generate_schema, _mock_val_ser, _typing_extra, _utils from .config import ConfigDict from .json_schema import ( DEFAULT_REF_TEMPLATE, @@ -23,6 +39,7 @@ from .plugin._schema_validator import create_schema_validator T = TypeVar('T') +R = TypeVar('R') if TYPE_CHECKING: @@ -100,6 +117,7 @@ def _getattr_no_parents(obj: Any, attribute: str) -> Any: def _type_has_config(type_: Any) -> bool: """Returns whether the type has config.""" + type_ = _typing_extra.annotated_type(type_) or type_ try: return issubclass(type_, BaseModel) or is_dataclass(type_) or is_typeddict(type_) except TypeError: @@ -107,6 +125,21 @@ def _type_has_config(type_: Any) -> bool: return False +# This is keeping track of the frame depth for the TypeAdapter functions. This is required for _parent_depth used for +# ForwardRef resolution. We may enter the TypeAdapter schema building via different TypeAdapter functions. Hence, we +# need to keep track of the frame depth relative to the originally provided _parent_depth. +def _frame_depth(depth: int) -> Callable[[Callable[..., R]], Callable[..., R]]: + def wrapper(func: Callable[..., R]) -> Callable[..., R]: + @wraps(func) + def wrapped(self: TypeAdapter, *args: Any, **kwargs: Any) -> R: + with self._with_frame_depth(depth + 1): # depth + 1 for the wrapper function + return func(self, *args, **kwargs) + + return wrapped + + return wrapper + + @final class TypeAdapter(Generic[T]): """Usage docs: https://docs.pydantic.dev/2.7/concepts/type_adapter/ @@ -118,6 +151,13 @@ class TypeAdapter(Generic[T]): **Note:** `TypeAdapter` instances are not types, and cannot be used as type annotations for fields. + **Note:** By default, `TypeAdapter` does not respect the + [`defer_build=True`][pydantic.config.ConfigDict.defer_build] setting in the + [`model_config`][pydantic.BaseModel.model_config] or in the `TypeAdapter` constructor `config`. You need to also + explicitly set [`_defer_build_mode=('model', 'type_adapter')`][pydantic.config.ConfigDict._defer_build_mode] of the + config to defer the model validator and serializer construction. Thus, this feature is opt-in to ensure backwards + compatibility. + Attributes: core_schema: The core schema for the type. validator (SchemaValidator): The schema validator for the type. @@ -190,11 +230,7 @@ def __init__( Returns: A type adapter configured for the specified `type`. """ - type_is_annotated: bool = _typing_extra.is_annotated(type) - annotated_type: Any = get_args(type)[0] if type_is_annotated else None - type_has_config: bool = _type_has_config(annotated_type if type_is_annotated else type) - - if type_has_config and config is not None: + if _type_has_config(type) and config is not None: raise PydanticUserError( 'Cannot use `config` when the type is a BaseModel, dataclass or TypedDict.' ' These types can have their own config and setting the config via the `config`' @@ -203,36 +239,107 @@ def __init__( code='type-adapter-config-unused', ) - config_wrapper = _config.ConfigWrapper(config) - - core_schema: CoreSchema + self._type = type + self._config = config + self._parent_depth = _parent_depth + if module is None: + f = sys._getframe(1) + self._module_name = cast(str, f.f_globals.get('__name__', '')) + else: + self._module_name = module + + self._core_schema: CoreSchema | None = None + self._validator: SchemaValidator | None = None + self._serializer: SchemaSerializer | None = None + + if not self._defer_build(): + # Immediately initialize the core schema, validator and serializer + with self._with_frame_depth(1): # +1 frame depth for this __init__ + # Model itself may be using deferred building. For backward compatibility we don't rebuild model mocks + # here as part of __init__ even though TypeAdapter itself is not using deferred building. + self._init_core_attrs(rebuild_mocks=False) + + @contextmanager + def _with_frame_depth(self, depth: int) -> Iterator[None]: + self._parent_depth += depth try: - core_schema = _getattr_no_parents(type, '__pydantic_core_schema__') - except AttributeError: - core_schema = _get_schema(type, config_wrapper, parent_depth=_parent_depth + 1) + yield + finally: + self._parent_depth -= depth - core_config = config_wrapper.core_config(None) - validator: SchemaValidator - try: - validator = _getattr_no_parents(type, '__pydantic_validator__') - except AttributeError: - if module is None: - f = sys._getframe(1) - module = cast(str, f.f_globals.get('__name__', '')) - validator = create_schema_validator( - core_schema, type, module, str(type), 'TypeAdapter', core_config, config_wrapper.plugin_settings - ) # type: ignore - - serializer: SchemaSerializer + @_frame_depth(1) + def _init_core_attrs(self, rebuild_mocks: bool) -> None: try: - serializer = _getattr_no_parents(type, '__pydantic_serializer__') + self._core_schema = _getattr_no_parents(self._type, '__pydantic_core_schema__') + self._validator = _getattr_no_parents(self._type, '__pydantic_validator__') + self._serializer = _getattr_no_parents(self._type, '__pydantic_serializer__') except AttributeError: - serializer = SchemaSerializer(core_schema, core_config) + config_wrapper = _config.ConfigWrapper(self._config) + core_config = config_wrapper.core_config(None) + + self._core_schema = _get_schema(self._type, config_wrapper, parent_depth=self._parent_depth) + self._validator = create_schema_validator( + schema=self._core_schema, + schema_type=self._type, + schema_type_module=self._module_name, + schema_type_name=str(self._type), + schema_kind='TypeAdapter', + config=core_config, + plugin_settings=config_wrapper.plugin_settings, + ) + self._serializer = SchemaSerializer(self._core_schema, core_config) + + if rebuild_mocks and isinstance(self._core_schema, _mock_val_ser.MockCoreSchema): + self._core_schema.rebuild() + self._init_core_attrs(rebuild_mocks=False) + assert not isinstance(self._core_schema, _mock_val_ser.MockCoreSchema) + assert not isinstance(self._validator, _mock_val_ser.MockValSer) + assert not isinstance(self._serializer, _mock_val_ser.MockValSer) + + @cached_property + @_frame_depth(2) # +2 for @cached_property and core_schema(self) + def core_schema(self) -> CoreSchema: + """The pydantic-core schema used to build the SchemaValidator and SchemaSerializer.""" + if self._core_schema is None or isinstance(self._core_schema, _mock_val_ser.MockCoreSchema): + self._init_core_attrs(rebuild_mocks=True) # Do not expose MockCoreSchema from public function + assert self._core_schema is not None and not isinstance(self._core_schema, _mock_val_ser.MockCoreSchema) + return self._core_schema + + @cached_property + @_frame_depth(2) # +2 for @cached_property + validator(self) + def validator(self) -> SchemaValidator: + """The pydantic-core SchemaValidator used to validate instances of the model.""" + if not isinstance(self._validator, SchemaValidator): + self._init_core_attrs(rebuild_mocks=True) # Do not expose MockValSer from public function + assert isinstance(self._validator, SchemaValidator) + return self._validator + + @cached_property + @_frame_depth(2) # +2 for @cached_property + serializer(self) + def serializer(self) -> SchemaSerializer: + """The pydantic-core SchemaSerializer used to dump instances of the model.""" + if not isinstance(self._serializer, SchemaSerializer): + self._init_core_attrs(rebuild_mocks=True) # Do not expose MockValSer from public function + assert isinstance(self._serializer, SchemaSerializer) + return self._serializer + + def _defer_build(self) -> bool: + config = self._config if self._config is not None else self._model_config() + return self._is_defer_build_config(config) if config is not None else False + + def _model_config(self) -> ConfigDict | None: + type_: Any = _typing_extra.annotated_type(self._type) or self._type # Eg FastAPI heavily uses Annotated + if _utils.lenient_issubclass(type_, BaseModel): + return type_.model_config + return getattr(type_, '__pydantic_config__', None) - self.core_schema = core_schema - self.validator = validator - self.serializer = serializer + @staticmethod + def _is_defer_build_config(config: ConfigDict) -> bool: + # TODO reevaluate this logic when we have a better understanding of how defer_build should work with TypeAdapter + # Should we drop the special _defer_build_mode check? + return config.get('defer_build', False) is True and 'type_adapter' in config.get('_defer_build_mode', tuple()) + @_frame_depth(1) def validate_python( self, object: Any, @@ -259,6 +366,7 @@ def validate_python( """ return self.validator.validate_python(object, strict=strict, from_attributes=from_attributes, context=context) + @_frame_depth(1) def validate_json( self, data: str | bytes, /, *, strict: bool | None = None, context: dict[str, Any] | None = None ) -> T: @@ -276,6 +384,7 @@ def validate_json( """ return self.validator.validate_json(data, strict=strict, context=context) + @_frame_depth(1) def validate_strings(self, obj: Any, /, *, strict: bool | None = None, context: dict[str, Any] | None = None) -> T: """Validate object contains string data against the model. @@ -289,6 +398,7 @@ def validate_strings(self, obj: Any, /, *, strict: bool | None = None, context: """ return self.validator.validate_strings(obj, strict=strict, context=context) + @_frame_depth(1) def get_default_value(self, *, strict: bool | None = None, context: dict[str, Any] | None = None) -> Some[T] | None: """Get the default value for the wrapped type. @@ -301,6 +411,7 @@ def get_default_value(self, *, strict: bool | None = None, context: dict[str, An """ return self.validator.get_default_value(strict=strict, context=context) + @_frame_depth(1) def dump_python( self, instance: T, @@ -349,6 +460,7 @@ def dump_python( serialize_as_any=serialize_as_any, ) + @_frame_depth(1) def dump_json( self, instance: T, @@ -399,6 +511,7 @@ def dump_json( serialize_as_any=serialize_as_any, ) + @_frame_depth(1) def json_schema( self, *, @@ -456,7 +569,10 @@ def json_schemas( """ schema_generator_instance = schema_generator(by_alias=by_alias, ref_template=ref_template) - inputs_ = [(key, mode, adapter.core_schema) for key, mode, adapter in inputs] + inputs_ = [] + for key, mode, adapter in inputs: + with adapter._with_frame_depth(1): # +1 for json_schemas staticmethod + inputs_.append((key, mode, adapter.core_schema)) json_schemas_map, definitions = schema_generator_instance.generate_definitions(inputs_) diff --git a/tests/conftest.py b/tests/conftest.py index c5dcce2523..e0c0dee018 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,8 @@ import pytest from _pytest.assertion.rewrite import AssertionRewritingHook +from pydantic import GenerateSchema + def pytest_addoption(parser: pytest.Parser): parser.addoption('--test-mypy', action='store_true', help='run mypy tests') @@ -114,3 +116,30 @@ def __repr__(self): def message_escaped(self): return re.escape(self.message) + + +@dataclass +class CallCounter: + count: int = 0 + + def reset(self) -> None: + self.count = 0 + + +@pytest.fixture +def generate_schema_calls(monkeypatch) -> CallCounter: + orig_generate_schema = GenerateSchema.generate_schema + counter = CallCounter() + depth = 0 # generate_schema can be called recursively + + def generate_schema_call_counter(*args: Any, **kwargs: Any) -> Any: + nonlocal depth + counter.count += 1 if depth == 0 else 0 + depth += 1 + try: + return orig_generate_schema(*args, **kwargs) + finally: + depth -= 1 + + monkeypatch.setattr(GenerateSchema, 'generate_schema', generate_schema_call_counter) + return counter diff --git a/tests/test_config.py b/tests/test_config.py index b03562e15d..7de133896c 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -4,7 +4,7 @@ from contextlib import nullcontext as does_not_raise from decimal import Decimal from inspect import signature -from typing import Any, ContextManager, Iterable, NamedTuple, Optional, Type, Union +from typing import Any, ContextManager, Dict, Iterable, NamedTuple, Optional, Tuple, Type, Union from dirty_equals import HasRepr, IsPartialDict from pydantic_core import SchemaError, SchemaSerializer, SchemaValidator @@ -32,10 +32,12 @@ from pydantic.type_adapter import TypeAdapter from pydantic.warnings import PydanticDeprecationWarning +from .conftest import CallCounter + if sys.version_info < (3, 9): - from typing_extensions import Annotated + from typing_extensions import Annotated, Literal else: - from typing import Annotated + from typing import Annotated, Literal import pytest @@ -701,54 +703,132 @@ def test_json_encoders_type_adapter() -> None: assert json.loads(ta.dump_json(1)) == '2' -def test_config_model_defer_build(): - class MyModel(BaseModel, defer_build=True): +@pytest.mark.parametrize('defer_build_mode', [None, tuple(), ('model',), ('type_adapter',), ('model', 'type_adapter')]) +def test_config_model_defer_build( + defer_build_mode: Optional[Tuple[Literal['model', 'type_adapter'], ...]], generate_schema_calls: CallCounter +): + config = ConfigDict(defer_build=True) + if defer_build_mode is not None: + config['_defer_build_mode'] = defer_build_mode + + class MyModel(BaseModel): + model_config = config x: int - assert isinstance(MyModel.__pydantic_validator__, MockValSer) - assert isinstance(MyModel.__pydantic_serializer__, MockValSer) + if defer_build_mode is None or 'model' in defer_build_mode: + assert isinstance(MyModel.__pydantic_validator__, MockValSer) + assert isinstance(MyModel.__pydantic_serializer__, MockValSer) + assert generate_schema_calls.count == 0, 'Should respect _defer_build_mode' + else: + assert isinstance(MyModel.__pydantic_validator__, SchemaValidator) + assert isinstance(MyModel.__pydantic_serializer__, SchemaSerializer) + assert generate_schema_calls.count == 1, 'Should respect _defer_build_mode' m = MyModel(x=1) assert m.x == 1 + assert m.model_dump()['x'] == 1 + assert m.model_validate({'x': 2}).x == 2 + assert m.model_json_schema()['type'] == 'object' assert isinstance(MyModel.__pydantic_validator__, SchemaValidator) assert isinstance(MyModel.__pydantic_serializer__, SchemaSerializer) + assert generate_schema_calls.count == 1, 'Should not build duplicated core schemas' -def test_config_type_adapter_defer_build(): - class MyModel(BaseModel, defer_build=True): +@pytest.mark.parametrize('defer_build_mode', [None, tuple(), ('model',), ('type_adapter',), ('model', 'type_adapter')]) +def test_config_model_type_adapter_defer_build( + defer_build_mode: Optional[Tuple[Literal['model', 'type_adapter'], ...]], generate_schema_calls: CallCounter +): + config = ConfigDict(defer_build=True) + if defer_build_mode is not None: + config['_defer_build_mode'] = defer_build_mode + + class MyModel(BaseModel): + model_config = config x: int + is_deferred = defer_build_mode is None or 'model' in defer_build_mode + assert generate_schema_calls.count == (0 if is_deferred else 1) + generate_schema_calls.reset() + ta = TypeAdapter(MyModel) - assert isinstance(ta.validator, MockValSer) - assert isinstance(ta.serializer, MockValSer) + assert generate_schema_calls.count == 0, 'Should use model generated schema' - m = ta.validate_python({'x': 1}) - assert m.x == 1 - m2 = ta.validate_python({'x': 2}) - assert m2.x == 2 + assert ta.validate_python({'x': 1}).x == 1 + assert ta.validate_python({'x': 2}).x == 2 + assert ta.dump_python(MyModel.model_construct(x=1))['x'] == 1 + assert ta.json_schema()['type'] == 'object' - # in the future, can reassign said validators to the TypeAdapter - assert isinstance(MyModel.__pydantic_validator__, SchemaValidator) - assert isinstance(MyModel.__pydantic_serializer__, SchemaSerializer) + assert generate_schema_calls.count == (1 if is_deferred else 0), 'Should not build duplicate core schemas' + + +@pytest.mark.parametrize('defer_build_mode', [None, tuple(), ('model',), ('type_adapter',), ('model', 'type_adapter')]) +def test_config_plain_type_adapter_defer_build( + defer_build_mode: Optional[Tuple[Literal['model', 'type_adapter'], ...]], generate_schema_calls: CallCounter +): + config = ConfigDict(defer_build=True) + if defer_build_mode is not None: + config['_defer_build_mode'] = defer_build_mode + is_deferred = defer_build_mode is not None and 'type_adapter' in defer_build_mode + + ta = TypeAdapter(Dict[str, int], config=config) + assert generate_schema_calls.count == (0 if is_deferred else 1) + generate_schema_calls.reset() -def test_config_model_defer_build_nested(): - class MyNestedModel(BaseModel, defer_build=True): + assert ta.validate_python({}) == {} + assert ta.validate_python({'x': 1}) == {'x': 1} + assert ta.dump_python({'x': 2}) == {'x': 2} + assert ta.json_schema()['type'] == 'object' + + assert generate_schema_calls.count == (1 if is_deferred else 0), 'Should not build duplicate core schemas' + + +@pytest.mark.parametrize('defer_build_mode', [None, ('model',), ('type_adapter',), ('model', 'type_adapter')]) +def test_config_model_defer_build_nested( + defer_build_mode: Optional[Tuple[Literal['model', 'type_adapter'], ...]], generate_schema_calls: CallCounter +): + config = ConfigDict(defer_build=True) + if defer_build_mode: + config['_defer_build_mode'] = defer_build_mode + + assert generate_schema_calls.count == 0 + + class MyNestedModel(BaseModel): + model_config = config x: int class MyModel(BaseModel): y: MyNestedModel - assert isinstance(MyNestedModel.__pydantic_validator__, MockValSer) - assert isinstance(MyNestedModel.__pydantic_serializer__, MockValSer) + assert isinstance(MyModel.__pydantic_validator__, SchemaValidator) + assert isinstance(MyModel.__pydantic_serializer__, SchemaSerializer) + + expected_schema_count = 1 if defer_build_mode is None or 'model' in defer_build_mode else 2 + assert generate_schema_calls.count == expected_schema_count, 'Should respect _defer_build_mode' + + if defer_build_mode is None or 'model' in defer_build_mode: + assert isinstance(MyNestedModel.__pydantic_validator__, MockValSer) + assert isinstance(MyNestedModel.__pydantic_serializer__, MockValSer) + else: + assert isinstance(MyNestedModel.__pydantic_validator__, SchemaValidator) + assert isinstance(MyNestedModel.__pydantic_serializer__, SchemaSerializer) m = MyModel(y={'x': 1}) + assert m.y.x == 1 assert m.model_dump() == {'y': {'x': 1}} + assert m.model_validate({'y': {'x': 1}}).y.x == 1 + assert m.model_json_schema()['type'] == 'object' + + if defer_build_mode is None or 'model' in defer_build_mode: + assert isinstance(MyNestedModel.__pydantic_validator__, MockValSer) + assert isinstance(MyNestedModel.__pydantic_serializer__, MockValSer) + else: + assert isinstance(MyNestedModel.__pydantic_validator__, SchemaValidator) + assert isinstance(MyNestedModel.__pydantic_serializer__, SchemaSerializer) - assert isinstance(MyNestedModel.__pydantic_validator__, MockValSer) - assert isinstance(MyNestedModel.__pydantic_serializer__, MockValSer) + assert generate_schema_calls.count == expected_schema_count, 'Should not build duplicated core schemas' def test_config_model_defer_build_ser_first(): diff --git a/tests/test_main.py b/tests/test_main.py index a25da1b2c8..1a8718055c 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -48,6 +48,8 @@ constr, field_validator, ) +from pydantic._internal._mock_val_ser import MockCoreSchema +from pydantic.dataclasses import dataclass as pydantic_dataclass def test_success(): @@ -2986,17 +2988,27 @@ class Bar: assert ta.validate_python(bar) is bar -def test_deferred_core_schema() -> None: - class Foo(BaseModel): - x: 'Bar' +@pytest.mark.parametrize('is_dataclass', [False, True]) +def test_deferred_core_schema(is_dataclass: bool) -> None: + if is_dataclass: + + @pydantic_dataclass + class Foo: + x: 'Bar' + else: + + class Foo(BaseModel): + x: 'Bar' + assert isinstance(Foo.__pydantic_core_schema__, MockCoreSchema) with pytest.raises(PydanticUserError, match='`Foo` is not fully defined'): - Foo.__pydantic_core_schema__ + Foo.__pydantic_core_schema__['type'] class Bar(BaseModel): pass - assert Foo.__pydantic_core_schema__ + assert Foo.__pydantic_core_schema__['type'] == ('dataclass' if is_dataclass else 'model') + assert isinstance(Foo.__pydantic_core_schema__, dict) def test_help(create_module): diff --git a/tests/test_type_adapter.py b/tests/test_type_adapter.py index c9ca6bec78..ff28eb0153 100644 --- a/tests/test_type_adapter.py +++ b/tests/test_type_adapter.py @@ -2,20 +2,25 @@ import sys from dataclasses import dataclass from datetime import date, datetime -from typing import Any, Dict, ForwardRef, Generic, List, NamedTuple, Tuple, TypeVar, Union +from typing import Any, Dict, ForwardRef, Generic, List, NamedTuple, Optional, Tuple, TypeVar, Union import pytest from pydantic_core import ValidationError from typing_extensions import Annotated, TypeAlias, TypedDict -from pydantic import BaseModel, TypeAdapter, ValidationInfo, field_validator +from pydantic import BaseModel, Field, TypeAdapter, ValidationInfo, create_model, field_validator +from pydantic._internal._typing_extra import annotated_type from pydantic.config import ConfigDict +from pydantic.dataclasses import dataclass as pydantic_dataclass from pydantic.errors import PydanticUserError +from pydantic.type_adapter import _type_has_config ItemType = TypeVar('ItemType') NestedList = List[List[ItemType]] +DEFER_ENABLE_MODE = ('model', 'type_adapter') + class PydanticModel(BaseModel): x: int @@ -65,29 +70,118 @@ def test_types(tp: Any, val: Any, expected: Any): OuterDict = Dict[str, 'IntList'] -def test_global_namespace_variables(): - v = TypeAdapter(OuterDict).validate_python - res = v({'foo': [1, '2']}) - assert res == {'foo': [1, 2]} +@pytest.mark.parametrize('defer_build', [False, True]) +@pytest.mark.parametrize('method', ['validate', 'serialize', 'json_schema', 'json_schemas']) +def test_global_namespace_variables(defer_build: bool, method: str, generate_schema_calls): + config = ConfigDict(defer_build=True, _defer_build_mode=DEFER_ENABLE_MODE) if defer_build else None + ta = TypeAdapter(OuterDict, config=config) + + assert generate_schema_calls.count == (0 if defer_build else 1), 'Should be built deferred' + + if method == 'validate': + assert ta.validate_python({'foo': [1, '2']}) == {'foo': [1, 2]} + elif method == 'serialize': + assert ta.dump_python({'foo': [1, 2]}) == {'foo': [1, 2]} + elif method == 'json_schema': + assert ta.json_schema()['type'] == 'object' + else: + assert method == 'json_schemas' + schemas, _ = TypeAdapter.json_schemas([(OuterDict, 'validation', ta)]) + assert schemas[(OuterDict, 'validation')]['type'] == 'object' + + +@pytest.mark.parametrize('defer_build', [False, True]) +@pytest.mark.parametrize('method', ['validate', 'serialize', 'json_schema', 'json_schemas']) +def test_model_global_namespace_variables(defer_build: bool, method: str, generate_schema_calls): + class MyModel(BaseModel): + model_config = ConfigDict(defer_build=True, _defer_build_mode=DEFER_ENABLE_MODE) if defer_build else None + x: OuterDict + ta = TypeAdapter(MyModel) -def test_local_namespace_variables(): + assert generate_schema_calls.count == (0 if defer_build else 1), 'Should be built deferred' + + if method == 'validate': + assert ta.validate_python({'x': {'foo': [1, '2']}}) == MyModel(x={'foo': [1, 2]}) + elif method == 'serialize': + assert ta.dump_python(MyModel(x={'foo': [1, 2]})) == {'x': {'foo': [1, 2]}} + elif method == 'json_schema': + assert ta.json_schema()['title'] == 'MyModel' + else: + assert method == 'json_schemas' + _, json_schema = TypeAdapter.json_schemas([(MyModel, 'validation', TypeAdapter(MyModel))]) + assert 'MyModel' in json_schema['$defs'] + + +@pytest.mark.parametrize('defer_build', [False, True]) +@pytest.mark.parametrize('method', ['validate', 'serialize', 'json_schema', 'json_schemas']) +def test_local_namespace_variables(defer_build: bool, method: str, generate_schema_calls): IntList = List[int] # noqa: F841 OuterDict = Dict[str, 'IntList'] - v = TypeAdapter(OuterDict).validate_python + config = ConfigDict(defer_build=True, _defer_build_mode=DEFER_ENABLE_MODE) if defer_build else None + ta = TypeAdapter(OuterDict, config=config) + + assert generate_schema_calls.count == (0 if defer_build else 1), 'Should be built deferred' + + if method == 'validate': + assert ta.validate_python({'foo': [1, '2']}) == {'foo': [1, 2]} + elif method == 'serialize': + assert ta.dump_python({'foo': [1, 2]}) == {'foo': [1, 2]} + elif method == 'json_schema': + assert ta.json_schema()['type'] == 'object' + else: + assert method == 'json_schemas' + schemas, _ = TypeAdapter.json_schemas([(OuterDict, 'validation', ta)]) + assert schemas[(OuterDict, 'validation')]['type'] == 'object' + + +@pytest.mark.parametrize('defer_build', [False, True]) +@pytest.mark.parametrize('method', ['validate', 'serialize', 'json_schema', 'json_schemas']) +def test_model_local_namespace_variables(defer_build: bool, method: str, generate_schema_calls): + IntList = List[int] # noqa: F841 + + class MyModel(BaseModel): + model_config = ConfigDict(defer_build=True, _defer_build_mode=DEFER_ENABLE_MODE) if defer_build else None + x: Dict[str, 'IntList'] + + ta = TypeAdapter(MyModel) - res = v({'foo': [1, '2']}) - assert res == {'foo': [1, 2]} + assert generate_schema_calls.count == (0 if defer_build else 1), 'Should be built deferred' + + if method == 'validate': + assert ta.validate_python({'x': {'foo': [1, '2']}}) == MyModel(x={'foo': [1, 2]}) + elif method == 'serialize': + assert ta.dump_python(MyModel(x={'foo': [1, 2]})) == {'x': {'foo': [1, 2]}} + elif method == 'json_schema': + assert ta.json_schema()['title'] == 'MyModel' + else: + assert method == 'json_schemas' + _, json_schema = TypeAdapter.json_schemas([(MyModel, 'validation', ta)]) + assert 'MyModel' in json_schema['$defs'] +@pytest.mark.parametrize('defer_build', [False, True]) +@pytest.mark.parametrize('method', ['validate', 'serialize', 'json_schema', 'json_schemas']) @pytest.mark.skipif(sys.version_info < (3, 9), reason="ForwardRef doesn't accept module as a parameter in Python < 3.9") -def test_top_level_fwd_ref(): +def test_top_level_fwd_ref(defer_build: bool, method: str, generate_schema_calls): + config = ConfigDict(defer_build=True, _defer_build_mode=DEFER_ENABLE_MODE) if defer_build else None + FwdRef = ForwardRef('OuterDict', module=__name__) - v = TypeAdapter(FwdRef).validate_python + ta = TypeAdapter(FwdRef, config=config) - res = v({'foo': [1, '2']}) - assert res == {'foo': [1, 2]} + assert generate_schema_calls.count == (0 if defer_build else 1), 'Should be built deferred' + + if method == 'validate': + assert ta.validate_python({'foo': [1, '2']}) == {'foo': [1, 2]} + elif method == 'serialize': + assert ta.dump_python({'foo': [1, 2]}) == {'foo': [1, 2]} + elif method == 'json_schema': + assert ta.json_schema()['type'] == 'object' + else: + assert method == 'json_schemas' + schemas, _ = TypeAdapter.json_schemas([(FwdRef, 'validation', ta)]) + assert schemas[(FwdRef, 'validation')]['type'] == 'object' MyUnion: TypeAlias = 'Union[str, int]' @@ -295,14 +389,23 @@ class UnrelatedClass: ], ids=repr, ) -def test_validate_strings(field_type, input_value, expected, raises_match, strict): - ta = TypeAdapter(field_type) +@pytest.mark.parametrize('defer_build', [False, True]) +def test_validate_strings( + field_type, input_value, expected, raises_match, strict, defer_build: bool, generate_schema_calls +): + config = ConfigDict(defer_build=True, _defer_build_mode=DEFER_ENABLE_MODE) if defer_build else None + ta = TypeAdapter(field_type, config=config) + + assert generate_schema_calls.count == (0 if defer_build else 1), 'Should be built deferred' + if raises_match is not None: with pytest.raises(expected, match=raises_match): ta.validate_strings(input_value, strict=strict) else: assert ta.validate_strings(input_value, strict=strict) == expected + assert generate_schema_calls.count == 1, 'Should not build duplicates' + @pytest.mark.parametrize('strict', [True, False]) def test_validate_strings_dict(strict): @@ -371,3 +474,92 @@ def test_eval_type_backport(): assert exc_info.value.errors(include_url=False) == [ {'type': 'list_type', 'loc': (), 'msg': 'Input should be a valid list', 'input': 'not a list'} ] + + +def defer_build_test_models(config: ConfigDict) -> List[Any]: + class Model(BaseModel): + model_config = config + x: int + + class SubModel(Model): + y: Optional[int] = None + + @pydantic_dataclass(config=config) + class DataClassModel: + x: int + + @pydantic_dataclass + class SubDataClassModel(DataClassModel): + y: Optional[int] = None + + class TypedDictModel(TypedDict): + __pydantic_config__ = config # type: ignore + x: int + + models = [ + Model, + SubModel, + create_model('DynamicModel', __base__=Model), + create_model('DynamicSubModel', __base__=SubModel), + DataClassModel, + SubDataClassModel, + TypedDictModel, + Dict[str, int], + ] + return [ + *models, + # FastAPI heavily uses Annotated so test that as well + *[Annotated[model, Field(title='abc')] for model in models], + ] + + +CONFIGS = [ + ConfigDict(defer_build=False, _defer_build_mode=('model',)), + ConfigDict(defer_build=False, _defer_build_mode=DEFER_ENABLE_MODE), + ConfigDict(defer_build=True, _defer_build_mode=('model',)), + ConfigDict(defer_build=True, _defer_build_mode=DEFER_ENABLE_MODE), +] +MODELS_CONFIGS: List[Tuple[Any, ConfigDict]] = [ + (model, config) for config in CONFIGS for model in defer_build_test_models(config) +] + + +@pytest.mark.parametrize('model, config', MODELS_CONFIGS) +@pytest.mark.parametrize('method', ['schema', 'validate', 'dump']) +def test_core_schema_respects_defer_build(model: Any, config: ConfigDict, method: str, generate_schema_calls) -> None: + type_ = annotated_type(model) or model + dumped = dict(x=1) if 'Dict[' in str(type_) else type_(x=1) + generate_schema_calls.reset() + + type_adapter = TypeAdapter(model) if _type_has_config(model) else TypeAdapter(model, config=config) + + if config['defer_build'] and 'type_adapter' in config['_defer_build_mode']: + assert generate_schema_calls.count == 0, 'Should be built deferred' + assert type_adapter._core_schema is None, 'Should be initialized deferred' + assert type_adapter._validator is None, 'Should be initialized deferred' + assert type_adapter._serializer is None, 'Should be initialized deferred' + else: + built_inside_type_adapter = 'Dict' in str(model) or 'Annotated' in str(model) + assert generate_schema_calls.count == (1 if built_inside_type_adapter else 0), f'Should be built ({model})' + assert type_adapter._core_schema is not None, 'Should be initialized before usage' + assert type_adapter._validator is not None, 'Should be initialized before usage' + assert type_adapter._serializer is not None, 'Should be initialized before usage' + + if method == 'schema': + json_schema = type_adapter.json_schema() # Use it + assert "'type': 'integer'" in str(json_schema) # Sanity check + # Do not check generate_schema_calls count here as the json_schema generation uses generate schema internally + # assert generate_schema_calls.count < 2, 'Should not build duplicates' + elif method == 'validate': + validated = type_adapter.validate_python({'x': 1}) # Use it + assert (validated['x'] if isinstance(validated, dict) else getattr(validated, 'x')) == 1 # Sanity check + assert generate_schema_calls.count < 2, 'Should not build duplicates' + else: + assert method == 'dump' + raw = type_adapter.dump_json(dumped) # Use it + assert json.loads(raw.decode())['x'] == 1 # Sanity check + assert generate_schema_calls.count < 2, 'Should not build duplicates' + + assert type_adapter._core_schema is not None, 'Should be initialized after the usage' + assert type_adapter._validator is not None, 'Should be initialized after the usage' + assert type_adapter._serializer is not None, 'Should be initialized after the usage'