-
-
Notifications
You must be signed in to change notification settings - Fork 205
Support Pydantic model defaults + field descriptions #802
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
base: main
Are you sure you want to change the base?
Changes from 43 commits
9dcfd25
991910c
b077891
19efc29
7a32d2b
a413dd9
25090dd
bad5751
a2ab163
83f467c
5381116
adbe853
10c35b8
3206e0f
9f9e5d6
236e97d
b576d73
db7938d
5d44d35
cdee9e5
010d26b
5182cd0
ee2a783
3f26ff7
fb31c98
9327e13
1795c40
9a19f6e
82d54fe
976c236
3506b48
cd271f2
5fd7462
7567fe5
8f0441c
33d9cd9
84289e6
938c762
7ef7160
c2d876c
f75ff51
715ab6b
121a462
631921a
c80ffaa
5fe87cf
2e3b63a
47fac11
09a881f
84ff322
332a95e
7392041
408f9a2
efaafcd
d5b4056
d16fda6
670dd71
b879d38
d2b5a20
4e7b623
7273f92
8324327
267b995
cbcc6d0
51288cd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
"""Work with Pydantic models.""" | ||
|
||
from importlib import import_module | ||
from types import ModuleType | ||
from typing import TYPE_CHECKING | ||
from typing import Any | ||
from typing import Final | ||
from typing import Optional | ||
from typing import TypeVar | ||
from typing import cast | ||
|
||
from pdoc.docstrings import AnyException | ||
|
||
if TYPE_CHECKING: | ||
import pydantic | ||
else: # pragma: no cover | ||
pydantic: Optional[ModuleType] | ||
try: | ||
pydantic = import_module("pydantic") | ||
except AnyException: | ||
pydantic = None | ||
|
||
|
||
_IGNORED_FIELDS: Final[list[str]] = ( | ||
jinnovation marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
[ | ||
"__fields__", | ||
] | ||
+ list(pydantic.BaseModel.__dict__.keys()) | ||
if pydantic is not None | ||
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) -> bool: | ||
"""Returns whether an object is a Pydantic model. | ||
Raises: | ||
ModuleNotFoundError: when function is called but Pydantic is not on the PYTHONPATH. | ||
jinnovation marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
""" | ||
if pydantic is None: # pragma: no cover | ||
raise ModuleNotFoundError( | ||
"_pydantic.is_pydantic_model() needs Pydantic installed" | ||
) | ||
|
||
return pydantic.BaseModel in obj.__bases__ | ||
jinnovation marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
|
||
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 ( | ||
pydantic is not None | ||
and isinstance(parent, type) | ||
and issubclass(parent, pydantic.BaseModel) | ||
): | ||
jinnovation marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
_parent = cast(pydantic.BaseModel, 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 pydantic is not None and issubclass(parent, pydantic.BaseModel): | ||
jinnovation marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
pydantic_fields = parent.__pydantic_fields__ | ||
return ( | ||
pydantic_fields[field_name].description | ||
if field_name in pydantic_fields | ||
else None | ||
) | ||
|
||
return None |
jinnovation marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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 | ||||||||||||||||||||||||||||||||||||
|
@@ -257,6 +259,7 @@ def members(self) -> dict[str, Doc]: | |||||||||||||||||||||||||||||||||||
for name, obj in self._member_objects.items(): | ||||||||||||||||||||||||||||||||||||
qualname = f"{self.qualname}.{name}".lstrip(".") | ||||||||||||||||||||||||||||||||||||
taken_from = self._taken_from(name, obj) | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
jinnovation marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||
doc: Doc[Any] | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
is_classmethod = isinstance(obj, classmethod) | ||||||||||||||||||||||||||||||||||||
|
@@ -319,13 +322,22 @@ 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] | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
_docstring: str | None = None | ||||||||||||||||||||||||||||||||||||
if self.kind == "class": | ||||||||||||||||||||||||||||||||||||
_docstring = _pydantic.get_field_docstring(cast(type, self.obj), name) | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
if _docstring is None: | ||||||||||||||||||||||||||||||||||||
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] | ||||||||||||||||||||||||||||||||||||
else: | ||||||||||||||||||||||||||||||||||||
doc.docstring = _docstring | ||||||||||||||||||||||||||||||||||||
|
_docstring: str | None = None | |
if self.kind == "class": | |
_docstring = _pydantic.get_field_docstring(cast(type, self.obj), name) | |
if _docstring is None: | |
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] | |
else: | |
doc.docstring = _docstring | |
if doc := _pydantic.get_field_docstring(self.obj, name): | |
doc.docstring = doc | |
elif doc := self._var_docstrings.get(name): | |
doc.docstring = doc | |
elif doc := self._func_docstrings.get(name) and not doc.docstring: | |
doc.docstring = doc |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got something that I think works. Let me know what you think.
jinnovation marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Large diffs are not rendered by default.
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.""" |
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.> | ||
> | ||
> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any reason why plain
import pydantic
does not work?Also, let's get rid of the TYPE_CHECKING extra logic and just slap
# type: ignore[no-redef]
on thepydantic = None
line. That should work?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Went a simpler (possibly) direction for the optional-import stuff. Let me know your thoughts.