Skip to content

Commit 33c5b5e

Browse files
committed
fixup! show alias
1 parent 15a5d9e commit 33c5b5e

11 files changed

Lines changed: 65 additions & 217 deletions

File tree

docs/index.md

Lines changed: 0 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -49,64 +49,3 @@ hide:
4949
```
5050

5151
///
52-
53-
### Show as alias
54-
55-
When the extension is configured with `show_alias=True`, fields with a `alias` will appear under their alias names in the documentation. This is useful for APIs where the serialized output uses different field names than the Python attribute names. See [Pydantic's alias documentation](https://docs.pydantic.dev/latest/concepts/alias/) for more information.
56-
57-
To enable this feature in your documentation configuration, configure the extension as follows:
58-
59-
```yaml
60-
plugins:
61-
- mkdocstrings:
62-
handlers:
63-
python:
64-
extensions:
65-
- griffe_pydantic:
66-
show_alias: true
67-
```
68-
69-
Or use the local configuration:
70-
71-
```markdown
72-
::: model_ext.ExampleModel
73-
options:
74-
extensions:
75-
- griffe_pydantic:
76-
show_alias: true
77-
```
78-
79-
80-
/// tab | Pydantic model
81-
82-
```python
83-
--8<-- "examples/model_serialize.py"
84-
```
85-
86-
///
87-
88-
/// tab | Without alias
89-
90-
```md exec="true" updatetoc="false"
91-
::: model_noserialize.UserModel
92-
options:
93-
heading_level: 4
94-
extensions:
95-
- griffe_pydantic: {show_alias: false}
96-
skip_local_inventory: true
97-
```
98-
99-
///
100-
101-
/// tab | With alias
102-
103-
```md exec="true" updatetoc="false"
104-
::: model_serialize.UserModel
105-
options:
106-
heading_level: 4
107-
extensions:
108-
- griffe_pydantic: {show_alias: true}
109-
skip_local_inventory: true
110-
```
111-
112-
///

examples/model_ext.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ class ExampleModel(BaseModel):
2020
default=5, ge=0, le=100, description="Shows constraints within doc string."
2121
)
2222

23+
field_with_alias: int = Field(alias="alias_field")
24+
"""Shows the field with its (validation and serialization) alias."""
25+
26+
field_with_validation_alias: int = Field(validation_alias="validation_alias_field")
27+
"""Shows the field with its validation alias."""
28+
29+
field_with_serialization_alias: int = Field(field_with_serialization_aliasalias="serialization_alias_field")
30+
"""Shows the field with its serialization alias."""
31+
2332
@field_validator("field_with_validator_and_alias", "field_without_default", mode="before")
2433
@classmethod
2534
def check_max_length_ten(cls, v) -> str:

examples/model_noserialize.py

Lines changed: 0 additions & 24 deletions
This file was deleted.

examples/model_serialize.py

Lines changed: 0 additions & 23 deletions
This file was deleted.

src/griffe_pydantic/_internal/dynamic.py

Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,7 @@
2020
_logger = get_logger("griffe_pydantic")
2121

2222

23-
def _process_attribute(
24-
obj: Any,
25-
attr: Attribute,
26-
cls: Class,
27-
*,
28-
processed: set[str],
29-
show_alias: bool = False,
30-
) -> None:
23+
def _process_attribute(obj: Any, attr: Attribute, cls: Class, *, processed: set[str]) -> None:
3124
"""Handle Pydantic fields."""
3225
from pydantic.fields import FieldInfo # noqa: PLC0415
3326

@@ -50,9 +43,10 @@ def _process_attribute(
5043
attr.extra[common._self_namespace]["constraints"] = constraints
5144

5245
# Store alias if present
53-
if show_alias and obj.alias:
54-
attr.extra[common._self_namespace]["alias"] = obj.alias
55-
attr.extra[common._mkdocstrings_namespace]["template"] = "pydantic_attribute_alias.html.jinja"
46+
if obj.alias:
47+
attr.extra[common._self_namespace]["validation_alias"] = obj.alias
48+
attr.extra[common._self_namespace]["serialization_alias"] = obj.alias
49+
attr.extra[common._mkdocstrings_namespace]["template"] = "pydantic_field.html.jinja"
5650

5751
# Populate docstring from the field's `description` argument.
5852
if not attr.docstring and (docstring := obj.description):
@@ -68,14 +62,7 @@ def _process_function(obj: Callable, func: Function, cls: Class, *, processed: s
6862
common._process_function(func, cls, dec_info.fields)
6963

7064

71-
def _process_class(
72-
obj: type,
73-
cls: Class,
74-
*,
75-
processed: set[str],
76-
schema: bool = False,
77-
show_alias: bool = False,
78-
) -> None:
65+
def _process_class(obj: type, cls: Class, *, processed: set[str], schema: bool = False) -> None:
7966
"""Detect and prepare Pydantic models."""
8067
common._process_class(cls)
8168
if schema:
@@ -92,7 +79,6 @@ def _process_class(
9279
member, # ty: ignore[invalid-argument-type]
9380
cls,
9481
processed=processed,
95-
show_alias=show_alias,
9682
)
9783
elif kind is Kind.FUNCTION:
9884
_process_function(getattr(obj, member.name), member, cls, processed=processed) # ty: ignore[invalid-argument-type]
@@ -108,10 +94,4 @@ def _process_class(
10894
endlineno=0,
10995
)
11096
cls.members[field_name] = attr # ty: ignore[invalid-assignment]
111-
_process_attribute(
112-
field_info,
113-
attr,
114-
cls,
115-
processed=processed,
116-
show_alias=show_alias,
117-
)
97+
_process_attribute(field_info, attr, cls, processed=processed)

src/griffe_pydantic/_internal/extension.py

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,37 +22,23 @@
2222
class PydanticExtension(Extension):
2323
"""Griffe extension for Pydantic."""
2424

25-
def __init__(self, *, schema: bool = False, show_alias: bool = False) -> None:
25+
def __init__(self, *, schema: bool = False) -> None:
2626
"""Initialize the extension.
2727
2828
Parameters:
2929
schema: Whether to compute and store the JSON schema of models.
30-
show_alias: Whether to use `alias` as the field name in documentation.
31-
When enabled, fields with a `alias` will be keyed by that alias instead of their Python attribute name.
3230
"""
3331
super().__init__()
3432
self._schema = schema
35-
self._show_alias = show_alias
3633
self._processed: set[str] = set()
3734
self._recorded: list[tuple[ObjectNode, Class]] = []
3835

3936
def on_package(self, *, pkg: Module, **kwargs: Any) -> None: # noqa: ARG002
4037
"""Detect models once the whole package is loaded."""
4138
for node, cls in self._recorded:
4239
self._processed.add(cls.canonical_path)
43-
dynamic._process_class(
44-
node.obj,
45-
cls,
46-
processed=self._processed,
47-
schema=self._schema,
48-
show_alias=self._show_alias,
49-
)
50-
static._process_module(
51-
pkg,
52-
processed=self._processed,
53-
schema=self._schema,
54-
show_alias=self._show_alias,
55-
)
40+
dynamic._process_class(node.obj, cls, processed=self._processed, schema=self._schema)
41+
static._process_module(pkg, processed=self._processed, schema=self._schema)
5642

5743
def on_class_instance(self, *, node: ast.AST | ObjectNode, cls: Class, **kwargs: Any) -> None: # noqa: ARG002
5844
"""Detect and prepare Pydantic models."""

src/griffe_pydantic/_internal/static.py

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,21 @@ def _pydantic_validator(func: Function) -> ExprCall | None:
9696
return None
9797

9898

99-
def _process_attribute(attr: Attribute, cls: Class, *, processed: set[str], show_alias: bool = False) -> None:
99+
def _literal_or_debug(value: Expr | str | None, /, *, path: str) -> str | None:
100+
if value is None:
101+
return None
102+
if isinstance(value, str):
103+
try:
104+
return ast.literal_eval(value)
105+
except ValueError:
106+
return value
107+
elif isinstance(value, (ExprName, Expr)):
108+
_logger.debug(f"Could not resolve expression '{value}' as a literal for field {path}")
109+
return None
110+
return None
111+
112+
113+
def _process_attribute(attr: Attribute, cls: Class, *, processed: set[str]) -> None:
100114
"""Handle Pydantic fields."""
101115
if attr.canonical_path in processed:
102116
return
@@ -191,19 +205,16 @@ def _process_attribute(attr: Attribute, cls: Class, *, processed: set[str], show
191205
attr.value = kwargs.get("default")
192206
constraints = {kwarg: value for kwarg, value in kwargs.items() if kwarg not in {"default", "description", "alias"}}
193207
attr.extra[common._self_namespace]["constraints"] = constraints
208+
attr.extra[common._mkdocstrings_namespace]["template"] = "pydantic_field.html.jinja"
194209

195-
# Store alias if present
196-
if show_alias and (alias := kwargs.get("alias")):
197-
if isinstance(alias, str):
198-
try:
199-
attr.extra[common._self_namespace]["alias"] = ast.literal_eval(alias)
200-
except ValueError:
201-
attr.extra[common._self_namespace]["alias"] = alias
202-
# Set the attribute template to the custom template, which will use the alias instead of the attribute name.
203-
attr.extra[common._mkdocstrings_namespace]["template"] = "pydantic_attribute_alias.html.jinja"
204-
elif isinstance(alias, (ExprName, Expr)):
205-
# For now, we can't resolve expressions at static analysis time
206-
_logger.debug(f"Could not resolve alias expression for field '{attr.path}'")
210+
# Store validation/serialization aliases if defined.
211+
if alias := _literal_or_debug(kwargs.get("alias"), path=attr.path):
212+
attr.extra[common._self_namespace]["validation_alias"] = alias
213+
attr.extra[common._self_namespace]["serialization_alias"] = alias
214+
elif validation_alias := _literal_or_debug(kwargs.get("validation_alias"), path=attr.path):
215+
attr.extra[common._self_namespace]["validation_alias"] = validation_alias
216+
elif serialization_alias := _literal_or_debug(kwargs.get("serialization_alias"), path=attr.path):
217+
attr.extra[common._self_namespace]["serialization_alias"] = serialization_alias
207218

208219
# Populate docstring from the field's `description` argument.
209220
if not attr.docstring and (description_expr := kwargs.get("description")):
@@ -228,7 +239,7 @@ def _process_function(func: Function, cls: Class, *, processed: set[str]) -> Non
228239
common._process_function(func, cls, fields)
229240

230241

231-
def _process_class(cls: Class, *, processed: set[str], schema: bool = False, show_alias: bool = False) -> None:
242+
def _process_class(cls: Class, *, processed: set[str], schema: bool = False) -> None:
232243
"""Finalize the Pydantic model data."""
233244
if cls.canonical_path in processed:
234245
return
@@ -263,20 +274,14 @@ def _process_class(cls: Class, *, processed: set[str], schema: bool = False, sho
263274
for member in cls.all_members.values():
264275
kind = member.kind
265276
if kind is Kind.ATTRIBUTE:
266-
_process_attribute(member, cls, processed=processed, show_alias=show_alias) # ty: ignore[invalid-argument-type]
277+
_process_attribute(member, cls, processed=processed) # ty: ignore[invalid-argument-type]
267278
elif kind is Kind.FUNCTION:
268279
_process_function(member, cls, processed=processed) # ty: ignore[invalid-argument-type]
269280
elif kind is Kind.CLASS:
270-
_process_class(member, processed=processed, schema=schema, show_alias=show_alias) # ty: ignore[invalid-argument-type]
281+
_process_class(member, processed=processed, schema=schema) # ty: ignore[invalid-argument-type]
271282

272283

273-
def _process_module(
274-
mod: Module,
275-
*,
276-
processed: set[str],
277-
schema: bool = False,
278-
show_alias: bool = False,
279-
) -> None:
284+
def _process_module(mod: Module, *, processed: set[str], schema: bool = False) -> None:
280285
"""Handle Pydantic models in a module."""
281286
if mod.canonical_path in processed:
282287
return
@@ -285,9 +290,9 @@ def _process_module(
285290
for cls in mod.classes.values():
286291
# Don't process aliases, real classes will be processed at some point anyway.
287292
if not cls.is_alias:
288-
_process_class(cls, processed=processed, schema=schema, show_alias=show_alias)
293+
_process_class(cls, processed=processed, schema=schema)
289294

290295
for submodule in mod.modules.values():
291296
# Same for modules, don't process aliased ones.
292297
if not submodule.is_alias:
293-
_process_module(submodule, processed=processed, schema=schema, show_alias=show_alias)
298+
_process_module(submodule, processed=processed, schema=schema)

src/griffe_pydantic/templates/material/_base/pydantic_attribute_alias.html.jinja

Lines changed: 0 additions & 19 deletions
This file was deleted.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{% extends "_base/attribute.html.jinja" %}
2+
3+
{% block contents scoped %}
4+
{% if attribute.extra.griffe_pydantic.validation_alias or attribute.extra.griffe_pydantic.serialization_alias %}
5+
<ul>
6+
{% if attribute.extra.griffe_pydantic.validation_alias %}
7+
<li>Input / Validation alias: <code>{{ attribute.extra.griffe_pydantic.validation_alias }}</code></li>
8+
{% endif %}
9+
{% if attribute.extra.griffe_pydantic.serialization_alias %}
10+
<li>Output / Serialization alias: <code>{{ attribute.extra.griffe_pydantic.serialization_alias }}</code></li>
11+
{% endif %}
12+
</ul>
13+
{% endif %}
14+
{{ super() }}
15+
{% endblock contents %}

src/griffe_pydantic/templates/material/pydantic_attribute_alias.html.jinja renamed to src/griffe_pydantic/templates/material/pydantic_field.html.jinja

File renamed without changes.

0 commit comments

Comments
 (0)