Skip to content

Commit 2f37b7e

Browse files
committed
refactor: Move code to internal API, update docs accordingly
1 parent 28ef155 commit 2f37b7e

File tree

15 files changed

+646
-389
lines changed

15 files changed

+646
-389
lines changed

docs/reference/griffe_pydantic.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# ::: griffe_pydantic
2+
options:
3+
show_submodules: true

mkdocs.yml

+1-7
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ nav:
1919
- Changelog: changelog.md
2020
- Credits: credits.md
2121
- License: license.md
22-
# defer to gen-files + literate-nav
23-
- API reference: reference/
22+
- API reference: reference/griffe_pydantic.md
2423
- Development:
2524
- Contributing: contributing.md
2625
- Code of Conduct: code_of_conduct.md
@@ -109,11 +108,6 @@ plugins:
109108
- search
110109
- markdown-exec
111110
- section-index
112-
- gen-files:
113-
scripts:
114-
- scripts/gen_api_ref.py
115-
- literate-nav:
116-
nav_file: SUMMARY.txt
117111
- coverage
118112
- mkdocstrings:
119113
handlers:

pyproject.toml

-2
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,7 @@ ci = [
9797
"markdown-exec>=1.8",
9898
"mkdocs>=1.6",
9999
"mkdocs-coverage>=1.0",
100-
"mkdocs-gen-files>=0.5",
101100
"mkdocs-git-revision-date-localized-plugin>=1.2",
102-
"mkdocs-literate-nav>=0.6",
103101
"mkdocs-llmstxt>=0.1",
104102
"mkdocs-material>=9.5",
105103
"mkdocs-minify-plugin>=0.8",

scripts/make.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from collections.abc import Iterator
1515

1616

17-
PYTHON_VERSIONS = os.getenv("PYTHON_VERSIONS", "3.9 3.10 3.11 3.12 3.13 3.14").split()
17+
PYTHON_VERSIONS = os.getenv("PYTHON_VERSIONS", "3.9 3.10 3.11 3.12 3.13").split()
1818

1919

2020
def shell(cmd: str, *, capture_output: bool = False, **kwargs: Any) -> str | None:

src/griffe_pydantic/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from pathlib import Path
99

10-
from griffe_pydantic.extension import PydanticExtension
10+
from griffe_pydantic._internal.extension import PydanticExtension
1111

1212

1313
def get_templates_path() -> Path:
+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
from __future__ import annotations
2+
3+
import json
4+
from functools import partial
5+
from typing import TYPE_CHECKING
6+
7+
if TYPE_CHECKING:
8+
from collections.abc import Sequence
9+
10+
from griffe import Attribute, Class, Function
11+
from pydantic import BaseModel
12+
13+
_self_namespace = "griffe_pydantic"
14+
_mkdocstrings_namespace = "mkdocstrings"
15+
16+
_field_constraints = {
17+
"gt",
18+
"ge",
19+
"lt",
20+
"le",
21+
"multiple_of",
22+
"min_length",
23+
"max_length",
24+
"pattern",
25+
"allow_inf_nan",
26+
"max_digits",
27+
"decimal_place",
28+
}
29+
30+
31+
def _model_fields(cls: Class) -> dict[str, Attribute]:
32+
return {name: attr for name, attr in cls.all_members.items() if "pydantic-field" in attr.labels} # type: ignore[misc]
33+
34+
35+
def _model_validators(cls: Class) -> dict[str, Function]:
36+
return {name: func for name, func in cls.all_members.items() if "pydantic-validator" in func.labels} # type: ignore[misc]
37+
38+
39+
def _json_schema(model: type[BaseModel]) -> str:
40+
"""Produce a model schema as JSON.
41+
42+
Parameters:
43+
model: A Pydantic model.
44+
45+
Returns:
46+
A schema as JSON.
47+
"""
48+
return json.dumps(model.model_json_schema(), indent=2)
49+
50+
51+
def _process_class(cls: Class) -> None:
52+
"""Set metadata on a Pydantic model.
53+
54+
Parameters:
55+
cls: The Griffe class representing the Pydantic model.
56+
"""
57+
cls.labels.add("pydantic-model")
58+
cls.extra[_self_namespace]["fields"] = partial(_model_fields, cls)
59+
cls.extra[_self_namespace]["validators"] = partial(_model_validators, cls)
60+
cls.extra[_mkdocstrings_namespace]["template"] = "pydantic_model.html.jinja"
61+
62+
63+
def _process_function(func: Function, cls: Class, fields: Sequence[str]) -> None:
64+
"""Set metadata on a Pydantic validator.
65+
66+
Parameters:
67+
cls: A Griffe function representing the Pydantic validator.
68+
"""
69+
func.labels = {"pydantic-validator"}
70+
targets = [cls.all_members[field] for field in fields]
71+
72+
func.extra[_self_namespace].setdefault("targets", [])
73+
func.extra[_self_namespace]["targets"].extend(targets)
74+
for target in targets:
75+
target.extra[_self_namespace].setdefault("validators", [])
76+
target.extra[_self_namespace]["validators"].append(func)
+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from __future__ import annotations
2+
3+
from typing import Any, Callable
4+
5+
from griffe import (
6+
Attribute,
7+
Class,
8+
Docstring,
9+
Function,
10+
Kind,
11+
get_logger,
12+
)
13+
from pydantic.fields import FieldInfo
14+
15+
from griffe_pydantic._internal import common
16+
17+
_logger = get_logger(__name__)
18+
19+
20+
def _process_attribute(obj: Any, attr: Attribute, cls: Class, *, processed: set[str]) -> None:
21+
"""Handle Pydantic fields."""
22+
if attr.canonical_path in processed:
23+
return
24+
processed.add(attr.canonical_path)
25+
if attr.name == "model_config":
26+
cls.extra[common._self_namespace]["config"] = obj
27+
return
28+
29+
if not isinstance(obj, FieldInfo):
30+
return
31+
32+
attr.labels = {"pydantic-field"}
33+
attr.value = obj.default
34+
constraints = {}
35+
for constraint in common._field_constraints:
36+
if (value := getattr(obj, constraint, None)) is not None:
37+
constraints[constraint] = value
38+
attr.extra[common._self_namespace]["constraints"] = constraints
39+
40+
# Populate docstring from the field's `description` argument.
41+
if not attr.docstring and (docstring := obj.description):
42+
attr.docstring = Docstring(docstring, parent=attr)
43+
44+
45+
def _process_function(obj: Callable, func: Function, cls: Class, *, processed: set[str]) -> None:
46+
"""Handle Pydantic field validators."""
47+
if func.canonical_path in processed:
48+
return
49+
processed.add(func.canonical_path)
50+
if dec_info := getattr(obj, "decorator_info", None):
51+
common._process_function(func, cls, dec_info.fields)
52+
53+
54+
def _process_class(obj: type, cls: Class, *, processed: set[str], schema: bool = False) -> None:
55+
"""Detect and prepare Pydantic models."""
56+
common._process_class(cls)
57+
if schema:
58+
cls.extra[common._self_namespace]["schema"] = common._json_schema(obj)
59+
for member in cls.all_members.values():
60+
kind = member.kind
61+
if kind is Kind.ATTRIBUTE:
62+
_process_attribute(getattr(obj, member.name), member, cls, processed=processed) # type: ignore[arg-type]
63+
elif kind is Kind.FUNCTION:
64+
_process_function(getattr(obj, member.name), member, cls, processed=processed) # type: ignore[arg-type]
+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from __future__ import annotations
2+
3+
import ast
4+
from typing import TYPE_CHECKING, Any
5+
6+
from griffe import (
7+
Class,
8+
Extension,
9+
Module,
10+
get_logger,
11+
)
12+
13+
from griffe_pydantic._internal import dynamic, static
14+
15+
if TYPE_CHECKING:
16+
from griffe import ObjectNode
17+
18+
19+
_logger = get_logger(__name__)
20+
21+
22+
class PydanticExtension(Extension):
23+
"""Griffe extension for Pydantic."""
24+
25+
def __init__(self, *, schema: bool = False) -> None:
26+
"""Initialize the extension.
27+
28+
Parameters:
29+
schema: Whether to compute and store the JSON schema of models.
30+
"""
31+
super().__init__()
32+
self._schema = schema
33+
self._processed: set[str] = set()
34+
self._recorded: list[tuple[ObjectNode, Class]] = []
35+
36+
def on_package_loaded(self, *, pkg: Module, **kwargs: Any) -> None: # noqa: ARG002
37+
"""Detect models once the whole package is loaded."""
38+
for node, cls in self._recorded:
39+
self._processed.add(cls.canonical_path)
40+
dynamic._process_class(node.obj, cls, processed=self._processed, schema=self._schema)
41+
static._process_module(pkg, processed=self._processed, schema=self._schema)
42+
43+
def on_class_instance(self, *, node: ast.AST | ObjectNode, cls: Class, **kwargs: Any) -> None: # noqa: ARG002
44+
"""Detect and prepare Pydantic models."""
45+
# Prevent running during static analysis.
46+
if isinstance(node, ast.AST):
47+
return
48+
49+
try:
50+
import pydantic
51+
except ImportError:
52+
_logger.warning("could not import pydantic - models will not be detected")
53+
return
54+
55+
if issubclass(node.obj, pydantic.BaseModel):
56+
self._recorded.append((node, cls))

0 commit comments

Comments
 (0)