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

Fix config.defer_build for serialization first cases #7024

Merged
merged 8 commits into from Aug 23, 2023
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
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