8000 Add support for `doc` attribute on dataclass fields by Viicos · Pull Request #12077 · pydantic/pydantic · GitHub
[go: up one dir, main page]

Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions pydantic/_internal/_dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,15 @@ def is_stdlib_dataclass(cls: type[Any], /) -> TypeIs[type[StandardDataclass]]:
def as_dataclass_field(pydantic_field: FieldInfo) -> dataclasses.Field[Any]:
field_args: dict[str, Any] = {'default': pydantic_field}

# Needed because if `doc` is set, the dataclass slots will be a dict (field name -> doc) instead of a tuple:
if sys.version_info >= (3, 14) and pydantic_field.description is not None:
field_args['doc'] = pydantic_field.description

# Needed as the stdlib dataclass module processes kw_only in a specific way during class construction:
if sys.version_info >= (3, 10) and pydantic_field.kw_only:
field_args['kw_only'] = True

# Needed as the stdlib dataclass modules generates `__repr__()` during class construction:
if pydantic_field.repr is not True:
field_args['repr'] = pydantic_field.repr

Expand Down Expand Up @@ -289,7 +295,7 @@ class A:
for field_name, field in dc_fields.items()
if isinstance(field.default, FieldInfo)
# Only do the patching if one of the affected attributes is set:
and (field.default.kw_only or field.default.repr is not True)
and (field.default.description is not None or field.default.kw_only or field.default.repr is not True)
}
if dc_fields_with_pydantic_field_defaults:
original_fields_list.append((dc_fields, dc_fields_with_pydantic_field_defaults))
Expand All @@ -298,7 +304,9 @@ class A:
# `dataclasses.Field` isn't documented as working with `copy.copy()`.
# It is a class with `__slots__`, so should work (and we hope for the best):
new_dc_field = copy.copy(field)
if default.kw_only:
# For base fields, no need to set `doc` from `FieldInfo.description`, this is only relevant
# for the class under construction and handled in `as_dataclass_field()`.
if sys.version_info >= (3, 10) and default.kw_only:
new_dc_field.kw_only = True
if default.repr is not True:
new_dc_field.repr = default.repr
Expand Down
2 changes: 2 additions & 0 deletions pydantic/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,8 @@ def _from_dataclass_field(dc_field: DataclassField[Any]) -> FieldInfo:

# use the `Field` function so in correct kwargs raise the correct `TypeError`
dc_field_metadata = {k: v for k, v in dc_field.metadata.items() if k in _FIELD_ARG_NAMES}
if sys.version_info >= (3, 14) and dc_field.doc is not None:
dc_field_metadata['description'] = dc_field.doc
return Field(default=default, default_factory=default_factory, repr=dc_field.repr, **dc_field_metadata) # pyright: ignore[reportCallIssue]

@staticmethod
Expand Down
23 changes: 23 additions & 0 deletions tests/test_dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -1841,6 +1841,20 @@ class Child(Parent):
assert child.y == 1


def test_kw_only_inheritance_on_field() -> None:
Copy link
Member Author

Choose a reason for hiding this comment

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

This would previously fail with an unhandled exception with 3.9. Although it doesn't make sense to use it in 3.9 (and we could add a warning -- not worth the effort as we'll drop support for it soon), still better to not hard error.

@dataclasses.dataclass
class A:
x: int = Field(kw_only=True)

@pydantic.dataclasses.dataclass
class B(A):
pass

if sys.version_info >= (3, 10): # On 3.9, we ignore kw_only.
with pytest.raises(ValidationError):
B(1)


def test_repr_inheritance() -> None:
@dataclasses.dataclass
class A:
Expand All @@ -1853,6 +1867,15 @@ class B(A):
assert repr(B(a=1)).endswith('B()')


@pytest.mark.skipif(sys.version_info < (3, 14), reason='`doc` added in 3.14')
def test_description_as_doc_in_slots() -> None:
@pydantic.dataclasses.dataclass(slots=True)
class A:
a: int = Field(description='a doc')

assert A.__slots__ == {'a': 'a doc'}


def test_extra_forbid_list_no_error():
@pydantic.dataclasses.dataclass(config=dict(extra='forbid'))
class Bar: ...
Expand Down
27 changes: 17 additions & 10 deletions tests/test_docs_extraction.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import sys
import textwrap
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import Annotated, Generic, TypeVar

import pytest
Expand Down Expand Up @@ -230,30 +230,37 @@ class MyModel:
def dummy_method(self) -> None:
"""Docs for dummy_method that won't be used for d"""

e: int = Field(1, description='Real description')
e: int = Field(1, description='Real description e')
"""Won't be used"""

f: int = 1
"""F docs"""

"""Useless docs"""
if sys.version_info >= (3, 14):
f: int = field(default=1, doc='Real description f')
"""Won't be used"""

g: int = 1
"""G docs"""

h = 1
"""Useless docs"""

h: int = 1
"""H docs"""

i: Annotated[int, Field(description='Real description')] = 1
i = 1
"""I docs, not a field"""

j: Annotated[int, Field(description='Real description j')] = 1
"""Won't be used"""

assert MyModel.__pydantic_fields__['a'].description == 'A docs'
assert MyModel.__pydantic_fields__['b'].description == 'B docs'
assert MyModel.__pydantic_fields__['c'].description is None
assert MyModel.__pydantic_fields__['d'].description is None
assert MyModel.__pydantic_fields__['e'].description == 'Real description'
assert MyModel.__pydantic_fields__['e'].description == 'Real description e'
if sys.version_info >= (3, 14):
assert MyModel.__pydantic_fields__['f'].description == 'Real description f'
assert MyModel.__pydantic_fields__['g'].description == 'G docs'
assert MyModel.__pydantic_fields__['i'].description == 'Real description'
assert MyModel.__pydantic_fields__['h'].description == 'H docs'
assert MyModel.__pydantic_fields__['j'].description == 'Real description j'

# https://github.com/pydantic/pydantic/issues/11243:
# Even though the `FieldInfo` instances had the correct description set,
Expand Down
13 changes: 13 additions & 0 deletions tests/test_json_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -2922,6 +2922,19 @@ class Model:
}


@pytest.mark.skipif(sys.version_info < (3, 14), reason='`doc` added in 3.14')
@pytest.mark.parametrize(
'dataclass_decorator',
[dataclass, pydantic.dataclasses.dataclass],
)
def test_dataclass_doc_json_schema(dataclass_decorator) -> None:
@dataclass_decorator
class A:
a: bool = dataclasses.field(doc='a doc')

assert TypeAdapter(A).json_schema()['properties']['a'] == {'title': 'A', 'type': 'boolean', 'description': 'a doc'}


def test_schema_attributes():
class ExampleEnum(Enum):
"""This is a test description."""
Expand Down
Loading
0