Skip to content
Open
Show file tree
Hide file tree
Changes from 59 commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
9dcfd25
Support defaults from Pydantic fields
jinnovation May 1, 2025
991910c
gate specifically on pydantic
jinnovation May 2, 2025
b077891
do not include importerror block in test coverage
jinnovation May 2, 2025
19efc29
extend pragma no cover
jinnovation May 2, 2025
7a32d2b
WIP: Convert to snapshot test
jinnovation May 2, 2025
a413dd9
remove dataclass import
jinnovation May 2, 2025
25090dd
create _pydantic module
jinnovation Oct 11, 2025
bad5751
Merge remote-tracking branch 'upstream/main' into pydantic
jinnovation Oct 11, 2025
a2ab163
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 11, 2025
83f467c
fix lint
jinnovation Oct 11, 2025
5381116
fix importlib import
jinnovation Oct 11, 2025
adbe853
mark generated files
jinnovation Oct 11, 2025
10c35b8
suppress BaseModel fields
jinnovation Oct 11, 2025
3206e0f
harden
jinnovation Oct 11, 2025
9f9e5d6
update snapshot
jinnovation Oct 11, 2025
236e97d
add note
jinnovation Oct 11, 2025
b576d73
changelog
jinnovation Oct 11, 2025
db7938d
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 11, 2025
5d44d35
Merge remote-tracking branch 'upstream/main' into pydantic
jinnovation Oct 11, 2025
cdee9e5
add docs
jinnovation Oct 11, 2025
010d26b
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 11, 2025
5182cd0
Merge branch 'main' into pydantic
jinnovation Oct 11, 2025
ee2a783
undo gitattributes
jinnovation Oct 12, 2025
3f26ff7
Update pdoc/__init__.py
jinnovation Oct 12, 2025
fb31c98
render docstring
jinnovation Oct 12, 2025
9327e13
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 12, 2025
1795c40
_pydantic.skip_field
jinnovation Oct 12, 2025
9a19f6e
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 12, 2025
82d54fe
fix lint
jinnovation Oct 12, 2025
976c236
refining pydantic-installed detection logic
jinnovation Oct 12, 2025
3506b48
fix lint
jinnovation Oct 12, 2025
cd271f2
support 3.9 type checking
jinnovation Oct 12, 2025
5fd7462
expand type annotations
jinnovation Oct 12, 2025
7567fe5
cleanup
jinnovation Oct 17, 2025
8f0441c
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 17, 2025
33d9cd9
typecast
jinnovation Oct 17, 2025
84289e6
Merge remote-tracking branch 'jinnovation/pydantic' into pydantic
jinnovation Oct 17, 2025
938c762
move field exclusion to `Class._member_objects`
jinnovation Oct 17, 2025
7ef7160
rm defunct fn
jinnovation Oct 17, 2025
c2d876c
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 17, 2025
f75ff51
simplify
jinnovation Oct 17, 2025
715ab6b
Merge remote-tracking branch 'jinnovation/pydantic' into pydantic
jinnovation Oct 17, 2025
121a462
refine type annotations
jinnovation Oct 17, 2025
631921a
reuse is_pydantic_model logic
jinnovation Oct 19, 2025
c80ffaa
simplify
jinnovation Oct 19, 2025
5fe87cf
use frozenset
jinnovation Oct 19, 2025
2e3b63a
rm unused
jinnovation Oct 19, 2025
47fac11
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 19, 2025
09a881f
is_pydantic_model return False if pydantic not installed
jinnovation Oct 19, 2025
84ff322
Merge remote-tracking branch 'jinnovation/pydantic' into pydantic
jinnovation Oct 19, 2025
332a95e
simplify (ish) conditional import logic
jinnovation Oct 19, 2025
7392041
simplify field-pruning
jinnovation Oct 19, 2025
408f9a2
localize more logic to _pydantic
jinnovation Oct 19, 2025
efaafcd
import TypeGuard from typing_extensions
jinnovation Oct 19, 2025
d5b4056
simplify docstring logic
jinnovation Oct 19, 2025
d16fda6
Update pdoc/doc.py
jinnovation Oct 19, 2025
670dd71
simplify
jinnovation Oct 20, 2025
b879d38
Merge remote-tracking branch 'jinnovation/pydantic' into pydantic
jinnovation Oct 20, 2025
d2b5a20
Merge remote-tracking branch 'origin/main' into pydantic
jinnovation Oct 20, 2025
4e7b623
Update pdoc/_pydantic.py
jinnovation Oct 20, 2025
7273f92
Revert "import TypeGuard from typing_extensions"
jinnovation Oct 21, 2025
8324327
Merge remote-tracking branch 'jinnovation/pydantic' into pydantic
jinnovation Oct 21, 2025
267b995
Merge remote-tracking branch 'origin/main' into pydantic
jinnovation Oct 21, 2025
cbcc6d0
add typing.cast back in
jinnovation Oct 21, 2025
51288cd
simplify
jinnovation Oct 21, 2025
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
([#831](https://github.com/mitmproxy/pdoc/issues/831), @iFreilicht)
- Replace vendored version of `markdown2` with the [official
upstream](https://github.com/trentm/python-markdown2)
- Add support for Pydantic-style field docstrings,
e.g. `pydantic.Field(description="...")`
([#802](https://github.com/mitmproxy/pdoc/pull/802), @jinnovation)

## 2025-06-04: pdoc 15.0.4

Expand Down
19 changes: 19 additions & 0 deletions pdoc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,25 @@ class GoldenRetriever(Dog):
Adding additional syntax elements is usually easy. If you feel that pdoc doesn't parse a docstring element properly,
please amend `pdoc.docstrings` and send us a pull request!

## ...document Pydantic models?

For [Pydantic models](https://docs.pydantic.dev/latest/concepts/models/), pdoc
will extract [field](https://docs.pydantic.dev/latest/concepts/fields/)
descriptions and treat them just like [documented
variables](#document-variables). For example, the following two Pydantic models
would have identical pdoc-rendered documentation:

```python
from pydantic import BaseModel, Field

class Foo(BaseModel):
a: int = Field(description="Docs for field a.")

class OtherFoo(BaseModel):
a: int
"""Docs for field a."""

```

## ...render math formulas?

Expand Down
68 changes: 68 additions & 0 deletions pdoc/_pydantic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Work with Pydantic models."""

from __future__ import annotations

from typing import Any
from typing import Optional
from typing import TypeVar

from typing_extensions import TypeGuard

_HAVE_PYDANTIC: bool = False
try:
import pydantic

_HAVE_PYDANTIC = True
except ImportError: # pragma: no cover
_HAVE_PYDANTIC = False

_IGNORED_FIELDS: frozenset[str] = frozenset(
[
"__fields__",
]
+ list(pydantic.BaseModel.__dict__.keys())
if _HAVE_PYDANTIC
else []
)
"""Fields to ignore when generating docs, e.g. those that emit deprecation
warnings or that are not relevant to users of BaseModel-derived classes."""

T = TypeVar("T")


def is_pydantic_model(obj: type) -> TypeGuard[pydantic.BaseModel]:
"""Returns whether an object is a Pydantic model.
If Pydantic is not isntalled, returns False unconditionally.
"""
if not _HAVE_PYDANTIC: # pragma: no cover
return False

return isinstance(obj, type) and issubclass(obj, pydantic.BaseModel)


def default_value(parent: Any, name: str, obj: T) -> T:
"""Determine the default value of obj.
If pydantic is not installed or the parent type is not a Pydantic model,
simply returns obj.
"""
if is_pydantic_model(parent):
pydantic_fields = parent.__pydantic_fields__
return pydantic_fields[name].default if name in pydantic_fields else obj

return obj


def get_field_docstring(parent: type, field_name: str) -> Optional[str]:
if is_pydantic_model(parent):
pydantic_fields = parent.__pydantic_fields__
return (
pydantic_fields[field_name].description
if field_name in pydantic_fields
else None
)

return None
29 changes: 24 additions & 5 deletions pdoc/doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,11 @@
from typing import TypedDict
from typing import TypeVar
from typing import Union
from typing import cast
from typing import get_origin
import warnings

from pdoc import _pydantic
from pdoc import doc_ast
from pdoc import doc_pyi
from pdoc import extract
Expand Down Expand Up @@ -319,13 +321,24 @@ def members(self) -> dict[str, Doc]:
qualname,
docstring="",
annotation=self._var_annotations.get(name, empty),
default_value=obj,
default_value=_pydantic.default_value(self.obj, name, obj),
taken_from=taken_from,
)
if self._var_docstrings.get(name):
doc.docstring = self._var_docstrings[name]
if self._func_docstrings.get(name) and not doc.docstring:
doc.docstring = self._func_docstrings[name]

doc.docstring = next(
(
d
for d in [
_pydantic.get_field_docstring(cast(type, self.obj), name),
self._var_docstrings.get(name),
doc.docstring,
self._func_docstrings.get(name),
]
if d
),
doc.docstring,
)

members[doc.name] = doc

if isinstance(self, Module):
Expand Down Expand Up @@ -775,6 +788,12 @@ def _member_objects(self) -> dict[str, Any]:
for cls in self._bases:
sorted, unsorted = doc_ast.sort_by_source(cls, sorted, unsorted)
sorted.update(unsorted)

if _pydantic.is_pydantic_model(self.obj):
sorted = {
k: v for k, v in sorted.items() if k not in _pydantic._IGNORED_FIELDS
}

return sorted

@cached_property
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies = [
"pygments >= 2.12.0",
"MarkupSafe >= 1.1.1",
"markdown2>=2.5.4",
"typing-extensions>=4.15.0",
]

classifiers = [
Expand Down Expand Up @@ -52,6 +53,7 @@ dev-dependencies = [
"pytest-timeout>=2.3.1",
"hypothesis>=6.113.0",
"pdoc-pyo3-sample-library>=1.0.11",
"pydantic>=2.12.0",
]

[build-system]
Expand Down
1 change: 1 addition & 0 deletions test/test_snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ def outfile(self, format: str) -> Path:
"include_undocumented": False,
},
),
Snapshot("with_pydantic"),
]


Expand Down
143 changes: 143 additions & 0 deletions test/testdata/with_pydantic.html

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions test/testdata/with_pydantic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""
A small example with Pydantic entities.
"""

import pydantic


class Foo(pydantic.BaseModel):
"""Foo class documentation."""

model_config = pydantic.ConfigDict(
use_attribute_docstrings=True,
)

a: int = pydantic.Field(default=1, description="Docstring for a")

b: int = 2
"""Docstring for b."""
6 changes: 6 additions & 0 deletions test/testdata/with_pydantic.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<module with_pydantic # A small example with…
<class with_pydantic.Foo # Foo class documentat…
<var a: int = 1 # Docstring for a>
<var b: int = 2 # Docstring for b.>
>
>
Loading