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 Generic Dataclass Fields Mutation Bug (when using TypeAdapter) #7435

Merged
merged 5 commits into from Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
4 changes: 2 additions & 2 deletions 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, _GenericAlias, cast # type: ignore

from pydantic_core import CoreSchema, core_schema
from typing_extensions import TypeAliasType, TypeGuard, get_args
Expand Down Expand Up @@ -65,7 +65,7 @@ def get_type_ref(type_: type[Any], args_override: tuple[type[Any], ...] | None =
when creating generic models without needing to create a concrete class.
"""
origin = type_
args = args_override or ()
args = type_.__args__ if isinstance(type_, _GenericAlias) else (args_override or ()) # type: ignore
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dmontagu, could you please explain why we changed this line?

I understand the value of the deepcopy on the model's fields in _generate_schema.py, but I don't quite understand the purpose of this line here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just noticed this misbehavior — right now, it's not producing a type ref that reflects the generic parameters like it is supposed to. In particular, this means that in principle you could get two different schemas with the same ref but that should have different parameters. I don't know why that doesn't seem to be affecting the validation behavior, but I do think this is an improvement either way.

generic_metadata = getattr(type_, '__pydantic_generic_metadata__', None)
if generic_metadata:
origin = generic_metadata['origin'] or origin
Expand Down
4 changes: 2 additions & 2 deletions pydantic/_internal/_generate_schema.py
Expand Up @@ -9,7 +9,7 @@
import typing
import warnings
from contextlib import contextmanager
from copy import copy
from copy import copy, deepcopy
from enum import Enum
from functools import partial
from inspect import Parameter, _ParameterKind, signature
Expand Down Expand Up @@ -1275,7 +1275,7 @@ def _dataclass_schema(
from ..dataclasses import is_pydantic_dataclass

if is_pydantic_dataclass(dataclass):
fields = dataclass.__pydantic_fields__
fields = deepcopy(dataclass.__pydantic_fields__)
if typevars_map:
for field in fields.values():
field.apply_typevars_map(typevars_map, self._types_namespace)
Expand Down
18 changes: 18 additions & 0 deletions tests/test_dataclasses.py
Expand Up @@ -2058,6 +2058,24 @@ class GenericDataclass(Generic[T]):
assert exc_info.value.errors(include_url=False) == output_value


def test_multiple_parametrized_generic_dataclasses():
T = TypeVar('T')

@pydantic.dataclasses.dataclass
class GenericDataclass(Generic[T]):
x: T

validator1 = pydantic.TypeAdapter(GenericDataclass[int])
validator2 = pydantic.TypeAdapter(GenericDataclass[str])

# verify that generic parameters are showing up in the type ref for generic dataclasses
assert '[int:' in validator1.core_schema['schema']['schema_ref']
assert '[str:' in validator2.core_schema['schema']['schema_ref']

assert validator1.validate_python({'x': 1}).x == 1
assert validator2.validate_python({'x': 'hello world'}).x == 'hello world'


@pytest.mark.parametrize('dataclass_decorator', **dataclass_decorators(include_identity=True))
def test_pydantic_dataclass_preserves_metadata(dataclass_decorator: Callable[[Any], Any]) -> None:
@dataclass_decorator
Expand Down