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: support pydantic.Field(kw_only=True) with inherited dataclasses #7827

Merged
merged 3 commits into from Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
24 changes: 23 additions & 1 deletion pydantic/dataclasses.py
Expand Up @@ -12,7 +12,7 @@
from ._internal import _dataclasses as _pydantic_dataclasses
from ._migration import getattr_migration
from .config import ConfigDict
from .fields import Field
from .fields import Field, FieldInfo

if TYPE_CHECKING:
from ._internal._dataclasses import PydanticDataclass
Expand Down Expand Up @@ -143,9 +143,29 @@ def dataclass(

if sys.version_info >= (3, 10):
kwargs = dict(kw_only=kw_only, slots=slots)

def make_pydantic_fields_compatible(cls: type[Any]) -> None:
"""Make sure that stdlib `dataclasses` understands `Field` kwargs like `kw_only`
To do that, we simply change
`x: int = pydantic.Field(..., kw_only=True)`
into
`x: int = dataclasses.field(default=pydantic.Field(..., kw_only=True), kw_only=True)`
"""
for field_name in cls.__annotations__:
try:
field_value = getattr(cls, field_name)
except AttributeError:
# no default value has been set for this field
continue
if isinstance(field_value, FieldInfo) and field_value.kw_only:
Copy link
Member Author

Choose a reason for hiding this comment

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

For now I went with the smallest change possible instead of wrapping them all the time and forward other stuff like init. I suggest we wait to see if this method needs to more than that

setattr(cls, field_name, dataclasses.field(default=field_value, kw_only=True))

else:
kwargs = {}

def make_pydantic_fields_compatible(_):
sydney-runkle marked this conversation as resolved.
Show resolved Hide resolved
return None

def create_dataclass(cls: type[Any]) -> type[PydanticDataclass]:
"""Create a Pydantic dataclass from a regular dataclass.

Expand Down Expand Up @@ -185,6 +205,8 @@ def create_dataclass(cls: type[Any]) -> type[PydanticDataclass]:
bases = bases + (generic_base,)
cls = types.new_class(cls.__name__, bases)

make_pydantic_fields_compatible(cls)

cls = dataclasses.dataclass( # type: ignore[call-overload]
cls,
# the value of init here doesn't affect anything except that it makes it easier to generate a signature
Expand Down
15 changes: 15 additions & 0 deletions tests/test_dataclasses.py
Expand Up @@ -1644,6 +1644,21 @@ class A:
assert A(b='hi').b == 'hi'


@pytest.mark.skipif(sys.version_info < (3, 10), reason='kw_only is not available in python < 3.10')
def test_kw_only_subclass():
@pydantic.dataclasses.dataclass
class A:
x: int
y: int = pydantic.Field(default=0, kw_only=True)

@pydantic.dataclasses.dataclass
class B(A):
z: int

assert B(1, 2) == B(x=1, y=0, z=2)
assert B(1, y=2, z=3) == B(x=1, y=2, z=3)


def dataclass_decorators(include_identity: bool = False, exclude_combined: bool = False):
decorators = [pydantic.dataclasses.dataclass, dataclasses.dataclass]
ids = ['pydantic', 'stdlib']
Expand Down