Skip to content

Commit

Permalink
Fix config.defer_build for serialization first cases (#7024)
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelcolvin committed Aug 23, 2023
1 parent 820da6f commit bd2b524
Show file tree
Hide file tree
Showing 9 changed files with 197 additions and 106 deletions.
1 change: 1 addition & 0 deletions Makefile
Expand Up @@ -11,6 +11,7 @@ sources = pydantic tests docs/plugins

.PHONY: install ## Install the package, dependencies, and pre-commit for local development
install: .pdm .pre-commit
pdm info
pdm install --group :all
pre-commit install --install-hooks

Expand Down
2 changes: 1 addition & 1 deletion pydantic/_internal/_dataclasses.py
Expand Up @@ -20,7 +20,7 @@
from ._fields import collect_dataclass_fields
from ._generate_schema import GenerateSchema
from ._generics import get_standard_typevars_map
from ._mock_validator import set_dataclass_mock_validator
from ._mock_val_ser import set_dataclass_mock_validator
from ._schema_generation_shared import CallbackGetCoreSchemaHandler

if typing.TYPE_CHECKING:
Expand Down
118 changes: 118 additions & 0 deletions pydantic/_internal/_mock_val_ser.py
@@ -0,0 +1,118 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Callable, Generic, TypeVar

from pydantic_core import SchemaSerializer, SchemaValidator
from typing_extensions import Literal

from ..errors import PydanticErrorCodes, PydanticUserError

if TYPE_CHECKING:
from ..dataclasses import PydanticDataclass
from ..main import BaseModel


ValSer = TypeVar('ValSer', SchemaValidator, SchemaSerializer)


class MockValSer(Generic[ValSer]):
"""Mocker for `pydantic_core.SchemaValidator` or `pydantic_core.SchemaSerializer` 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', '_val_or_ser', '_attempt_rebuild'

def __init__(
self,
error_message: str,
*,
code: PydanticErrorCodes,
val_or_ser: Literal['validator', 'serializer'],
attempt_rebuild: Callable[[], ValSer | None] | None = None,
) -> None:
self._error_message = error_message
self._val_or_ser = SchemaValidator if val_or_ser == 'validator' else SchemaSerializer
self._code: PydanticErrorCodes = code
self._attempt_rebuild = attempt_rebuild

def __getattr__(self, item: str) -> None:
__tracebackhide__ = True
if self._attempt_rebuild:
val_ser = self._attempt_rebuild()
if val_ser is not None:
return getattr(val_ser, item)

# raise an AttributeError if `item` doesn't exist
getattr(self._val_or_ser, item)
raise PydanticUserError(self._error_message, code=self._code)

def rebuild(self) -> ValSer | 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


def set_model_mocks(cls: type[BaseModel], cls_name: str, undefined_name: str = 'all referenced types') -> None:
"""Set `__pydantic_validator__` and `__pydantic_serializer__` to `MockValSer`s on a model.
Args:
cls: The model class to set the mocks on
cls_name: Name of the model class, used in error messages
undefined_name: Name of the undefined thing, used in error messages
"""
undefined_type_error_message = (
f'`{cls_name}` is not fully defined; you should define {undefined_name},'
f' then call `{cls_name}.model_rebuild()`.'
)

def attempt_rebuild_validator() -> SchemaValidator | None:
if cls.model_rebuild(raise_errors=False, _parent_namespace_depth=5):
return cls.__pydantic_validator__
else:
return None

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,
)

def attempt_rebuild_serializer() -> SchemaSerializer | None:
if cls.model_rebuild(raise_errors=False, _parent_namespace_depth=5):
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,
)


def set_dataclass_mock_validator(cls: type[PydanticDataclass], cls_name: str, undefined_name: str) -> None:
undefined_type_error_message = (
f'`{cls_name}` is not fully defined; you should define {undefined_name},'
f' then call `pydantic.dataclasses.rebuild_dataclass({cls_name})`.'
)

def attempt_rebuild() -> SchemaValidator | None:
from ..dataclasses import rebuild_dataclass

if rebuild_dataclass(cls, raise_errors=False, _parent_namespace_depth=5):
return cls.__pydantic_validator__
else:
return None

cls.__pydantic_validator__ = MockValSer( # type: ignore[assignment]
undefined_type_error_message,
code='class-not-fully-defined',
val_or_ser='validator',
attempt_rebuild=attempt_rebuild,
)
86 changes: 0 additions & 86 deletions pydantic/_internal/_mock_validator.py

This file was deleted.

10 changes: 5 additions & 5 deletions pydantic/_internal/_model_construction.py
Expand Up @@ -22,7 +22,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_validator import MockValidator, set_basemodel_mock_validator
from ._mock_val_ser import MockValSer, set_model_mocks
from ._schema_generation_shared import CallbackGetCoreSchemaHandler
from ._typing_extra import get_cls_types_namespace, is_classvar, parent_frame_namespace
from ._utils import ClassAttribute, is_valid_identifier
Expand Down Expand Up @@ -202,7 +202,7 @@ def __getattr__(self, item: str) -> Any:
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, MockValidator):
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
Expand Down Expand Up @@ -461,23 +461,23 @@ def complete_model_class(
)

if config_wrapper.defer_build:
set_basemodel_mock_validator(cls, cls_name)
set_model_mocks(cls, cls_name)
return False

try:
schema = cls.__get_pydantic_core_schema__(cls, handler)
except PydanticUndefinedAnnotation as e:
if raise_errors:
raise
set_basemodel_mock_validator(cls, cls_name, f'`{e.name}`')
set_model_mocks(cls, cls_name, f'`{e.name}`')
return False

core_config = config_wrapper.core_config(cls)

schema = gen_schema.collect_definitions(schema)
schema = apply_discriminators(flatten_schema_defs(schema))
if collect_invalid_schemas(schema):
set_basemodel_mock_validator(cls, cls_name)
set_model_mocks(cls, cls_name)
return False

# debug(schema)
Expand Down
4 changes: 2 additions & 2 deletions pydantic/json_schema.py
Expand Up @@ -41,7 +41,7 @@

from pydantic._internal import _annotated_handlers, _internal_dataclass

from ._internal import _core_metadata, _core_utils, _mock_validator, _schema_generation_shared, _typing_extra
from ._internal import _core_metadata, _core_utils, _mock_val_ser, _schema_generation_shared, _typing_extra
from .config import JsonSchemaExtraCallable
from .errors import PydanticInvalidForJsonSchema, PydanticUserError

Expand Down Expand Up @@ -2119,7 +2119,7 @@ def model_json_schema(
The generated JSON Schema.
"""
schema_generator_instance = schema_generator(by_alias=by_alias, ref_template=ref_template)
if isinstance(cls.__pydantic_validator__, _mock_validator.MockValidator):
if isinstance(cls.__pydantic_validator__, _mock_val_ser.MockValSer):
cls.__pydantic_validator__.rebuild()
assert '__pydantic_core_schema__' in cls.__dict__, 'this is a bug! please report it'
return schema_generator_instance.generate(cls.__pydantic_core_schema__, mode=mode)
Expand Down
10 changes: 8 additions & 2 deletions pydantic/main.py
Expand Up @@ -18,7 +18,7 @@
_fields,
_forward_ref,
_generics,
_mock_validator,
_mock_val_ser,
_model_construction,
_repr,
_typing_extra,
Expand Down Expand Up @@ -134,8 +134,14 @@ class BaseModel(metaclass=_model_construction.ModelMetaclass):
model_fields = {}
__pydantic_decorators__ = _decorators.DecoratorInfos()
# Prevent `BaseModel` from being instantiated directly:
__pydantic_validator__ = _mock_validator.MockValidator(
__pydantic_validator__ = _mock_val_ser.MockValSer(
'Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly',
val_or_ser='validator',
code='base-model-instantiated',
)
__pydantic_serializer__ = _mock_val_ser.MockValSer(
'Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly',
val_or_ser='serializer',
code='base-model-instantiated',
)

Expand Down

0 comments on commit bd2b524

Please sign in to comment.