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

Disallow config specification when initializing a TypeAdapter when the annotated type has config already #8365

Merged
merged 3 commits into from Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
31 changes: 23 additions & 8 deletions pydantic/type_adapter.py
Expand Up @@ -6,7 +6,7 @@
from typing import TYPE_CHECKING, Any, Dict, Generic, Iterable, Set, TypeVar, Union, cast, overload

from pydantic_core import CoreSchema, SchemaSerializer, SchemaValidator, Some
from typing_extensions import Literal, is_typeddict
from typing_extensions import Literal, get_args, is_typeddict

from pydantic.errors import PydanticUserError
from pydantic.main import BaseModel
Expand Down Expand Up @@ -98,6 +98,15 @@ def _getattr_no_parents(obj: Any, attribute: str) -> Any:
raise AttributeError(attribute)


def _type_has_config(type_: Any) -> bool:
"""Returns whether the type has config."""
try:
return issubclass(type_, BaseModel) or is_dataclass(type_) or is_typeddict(type_)
except TypeError:
# type is not a class
return False


class TypeAdapter(Generic[T]):
"""Type adapters provide a flexible way to perform validation and serialization based on a Python type.

Expand Down Expand Up @@ -168,13 +177,9 @@ def __init__(
Returns:
A type adapter configured for the specified `type`.
"""
config_wrapper = _config.ConfigWrapper(config)

try:
type_has_config = issubclass(type, BaseModel) or is_dataclass(type) or is_typeddict(type)
except TypeError:
# type is not a class
type_has_config = False
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:
raise PydanticUserError(
Expand All @@ -185,6 +190,16 @@ def __init__(
code='type-adapter-config-unused',
)

# If `type` is annotated, attempt to use the config from the annotated type.
# The checks below for '__pydantic_core_schema__', '__pydantic_validator__', and '__pydantic_serializer__'
# don't work for annotated types, so we need to use the config from the annotated type
# in the calls to _get_schema, create_schema_validator, and SchemaSerializer
# to make ensure that the schema, validatior, and serializer respect the annotated type's config.
if config is None and type_is_annotated:
config = getattr(annotated_type, 'model_config', getattr(annotated_type, '__pydantic_config__', None))

config_wrapper = _config.ConfigWrapper(config)

core_schema: CoreSchema
try:
core_schema = _getattr_no_parents(type, '__pydantic_core_schema__')
Expand Down
31 changes: 30 additions & 1 deletion tests/test_type_adapter.py
Expand Up @@ -6,10 +6,11 @@

import pytest
from pydantic_core import ValidationError
from typing_extensions import TypeAlias, TypedDict
from typing_extensions import Annotated, TypeAlias, TypedDict

from pydantic import BaseModel, TypeAdapter, ValidationInfo, field_validator
from pydantic.config import ConfigDict
from pydantic.errors import PydanticUserError

ItemType = TypeVar('ItemType')

Expand Down Expand Up @@ -309,3 +310,31 @@ def test_validate_strings_dict(strict):
1: date(2017, 1, 1),
2: date(2017, 1, 2),
}


def test_annotated_type_disallows_config() -> None:
class Model(BaseModel):
x: int

with pytest.raises(PydanticUserError, match='Cannot use `config`'):
TypeAdapter(Annotated[Model, ...], config=ConfigDict(strict=False))


def test_ta_config_with_annotated_type() -> None:
class TestValidator(BaseModel):
x: str

model_config = ConfigDict(str_to_lower=True)

assert TestValidator(x='ABC').x == 'abc'
assert TypeAdapter(TestValidator).validate_python({'x': 'ABC'}).x == 'abc'
assert TypeAdapter(Annotated[TestValidator, ...]).validate_python({'x': 'ABC'}).x == 'abc'

class TestSerializer(BaseModel):
some_bytes: bytes
model_config = ConfigDict(ser_json_bytes='base64')

result = TestSerializer(some_bytes=b'\xaa')
assert result.model_dump(mode='json') == {'some_bytes': 'qg=='}
assert TypeAdapter(TestSerializer).dump_python(result, mode='json') == {'some_bytes': 'qg=='}
assert TypeAdapter(Annotated[TestSerializer, ...]).dump_python(result, mode='json') == {'some_bytes': 'qg=='}