Skip to content
Draft
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
27 changes: 27 additions & 0 deletions docs/source/users/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -300,3 +300,30 @@ and the
.. tab:: python

.. autocodeblock:: target.example_reused_validators


.. _example_dataclasses:

--------------------
Pydantic Dataclasses
--------------------

Pydantic dataclasses are also supported

.. tabs::

.. tab:: *rendered output*

.. automodule:: target.example_dataclasses
:members:

.. tab:: reST

.. code-block::

.. automodule:: target.example_dataclasses
:members:

.. tab:: python

.. autocodeblock:: target.example_dataclasses
6 changes: 5 additions & 1 deletion sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py
Original file line number Diff line number Diff line change
Expand Up @@ -705,7 +705,7 @@ def can_document_member(
field_name=membername,
)

return is_valid and is_field and isattr
return is_valid and is_field

@property
def pydantic_field_name(self) -> str:
Expand All @@ -716,6 +716,10 @@ def pydantic_field_name(self) -> str:

return self.objpath[-1]

def should_suppress_value_header(self) -> bool:
"""We handle value ourself so suppress the superclass adding it."""
return True

def add_directive_header(self, sig: str) -> None:
"""Delegate header options."""
super().add_directive_header(sig)
Expand Down
40 changes: 31 additions & 9 deletions sphinxcontrib/autodoc_pydantic/inspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,16 @@
except ImportError:
from typing_extensions import TypeGuard

from pydantic import BaseModel, ConfigDict, PydanticInvalidForJsonSchema, create_model
import dataclasses

from pydantic import (
BaseModel,
ConfigDict,
PydanticInvalidForJsonSchema,
TypeAdapter,
create_model,
)
from pydantic.dataclasses import is_pydantic_dataclass
from pydantic_settings import BaseSettings

ASTERISK_FIELD_NAME = 'all fields'
Expand Down Expand Up @@ -105,7 +114,12 @@ class FieldInspector(BaseInspectionComposite):
def __init__(self, parent: ModelInspector) -> None:
super().__init__(parent)
# json schema can reliably be created only at model level
self.attribute = self.model.model_fields
if is_pydantic_dataclass(self.model):
self.attribute = {
field.name: field.default for field in dataclasses.fields(self.model)
}
else:
self.attribute = self.model.model_fields

@property
def names(self) -> list[str]:
Expand Down Expand Up @@ -154,7 +168,7 @@ def _get_meta_items(meta_class: Any) -> dict[str, str]: # noqa: ANN401
def get_constraints(self, field_name: str) -> dict[str, Any]:
"""Get constraints for given `field_name`."""

metadata = self.model.model_fields[field_name].metadata
metadata = self.attribute[field_name].metadata
available = [meta for meta in metadata if meta is not None]

return {
Expand Down Expand Up @@ -289,8 +303,10 @@ def _get_values_per_type(self) -> dict[str, Any]:
values. Hence, the default values are removed.

"""

cfg = self.model.model_config
if is_pydantic_dataclass(self.model):
cfg = self.model.__pydantic_config__
else:
cfg = self.model.model_config

if issubclass(self.model, BaseSettings):
default = tuple(BaseSettings.model_config.items())
Expand Down Expand Up @@ -389,7 +405,10 @@ def sanitized(self) -> dict:
try:
with warnings.catch_warnings():
warnings.simplefilter('ignore')
schema = self.model.model_json_schema()
if is_pydantic_dataclass(self.model):
schema = TypeAdapter(self.model).json_schema()
else:
schema = self.model.model_json_schema()

except (TypeError, ValueError, PydanticInvalidForJsonSchema):
new_model = self.create_sanitized_model()
Expand Down Expand Up @@ -419,7 +438,7 @@ def is_pydantic_model(obj: Any) -> TypeGuard[type[BaseModel]]: # noqa: ANN401
"""Determine if object is a valid pydantic model."""

try:
return issubclass(obj, BaseModel)
return issubclass(obj, BaseModel) or is_pydantic_dataclass(obj)
except TypeError:
return False

Expand All @@ -429,8 +448,11 @@ def is_pydantic_field(cls, parent: Any, field_name: str) -> bool: # noqa: ANN40

if not cls.is_pydantic_model(parent):
return False

return field_name in parent.model_fields
if is_pydantic_dataclass(parent):
fields = {field.name for field in dataclasses.fields(parent)}
else:
fields = parent.model_fields
return field_name in fields

@classmethod
def is_validator_by_name(cls, name: str, obj: Any) -> bool: # noqa: ANN401
Expand Down
11 changes: 11 additions & 0 deletions tests/roots/test-base/target/example_dataclasses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from pydantic import Field
from pydantic.dataclasses import dataclass


@dataclass
class ExampleDataclass:
"""An example Pydantic dataclass."""

name: str = Field(description="The name of the object")
age: int = Field(5, description="The age of the object")

7 changes: 7 additions & 0 deletions tests/roots/test-base/target/examples.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from pydantic import BaseModel, field_validator, Field, ConfigDict
from pydantic.dataclasses import dataclass


class PlainModel(BaseModel):
Expand All @@ -12,6 +13,12 @@ class ModelWithField(BaseModel):
"""Doc field"""


@dataclass
class DataclassWithField:
"""Dataclass With Field."""
field: int = Field(5, description="The Field")


class ModelWithFieldValidator(BaseModel):
"""Model With Field Validator."""

Expand Down
31 changes: 31 additions & 0 deletions tests/test_autodoc_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,37 @@ def test_model_with_field(autodocument):
]


def test_dataclass_with_field(autodocument):
options_app = dict(autodoc_pydantic_model_show_json=False)
options_doc = dict(members=None)
actual = autodocument(
documenter='pydantic_model',
object_path='target.examples.DataclassWithField',
options_doc=options_doc,
options_app=options_app,
)

assert actual == [
'',
'.. py:pydantic_model:: DataclassWithField',
' :module: target.examples',
'',
' Dataclass With Field.',
'',
' :Fields:',
' - :py:obj:`field (int) <target.examples.DataclassWithField.field>`',
'',
'',
' .. py:pydantic_field:: DataclassWithField.field',
' :module: target.examples',
' :type: int',
' :value: 5',
'',
' The Field',
'',
]


def test_model_with_field_validator(autodocument):
options_app = dict(
autodoc_pydantic_model_show_json=False,
Expand Down
Loading