Skip to content
Open
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
1 change: 0 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,3 @@ hide:
```

///

9 changes: 9 additions & 0 deletions examples/model_ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ class ExampleModel(BaseModel):
default=5, ge=0, le=100, description="Shows constraints within doc string."
)

field_with_alias: int = Field(alias="alias_field")
"""Shows the field with its (validation and serialization) alias."""

field_with_validation_alias: int = Field(validation_alias="validation_alias_field")
"""Shows the field with its validation alias."""

field_with_serialization_alias: int = Field(serialization_alias="serialization_alias_field")
"""Shows the field with its serialization alias."""

@field_validator("field_with_validator_and_alias", "field_without_default", mode="before")
@classmethod
def check_max_length_ten(cls, v) -> str:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ maintain = [
ci = [
"duty>=1.6",
"griffe>=2.0",
"pydantic>=2.10",
"pydantic>=2.12",
Comment thread
watermarkhu marked this conversation as resolved.
"pytest>=8.2",
"pytest-cov>=5.0",
"pytest-randomly>=3.15",
Expand Down
23 changes: 23 additions & 0 deletions src/griffe_pydantic/_internal/dynamic.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ def _process_attribute(obj: Any, attr: Attribute, cls: Class, *, processed: set[
if (value := getattr(obj, constraint, None)) is not None:
constraints[constraint] = value
attr.extra[common._self_namespace]["constraints"] = constraints
attr.extra[common._mkdocstrings_namespace]["template"] = "pydantic_field.html.jinja"

# Store validation/serialization aliases if defined.
if obj.alias:
attr.extra[common._self_namespace]["validation_alias"] = obj.alias
attr.extra[common._self_namespace]["serialization_alias"] = obj.alias
elif obj.validation_alias:
attr.extra[common._self_namespace]["validation_alias"] = obj.validation_alias
elif obj.serialization_alias:
attr.extra[common._self_namespace]["serialization_alias"] = obj.serialization_alias

# Populate docstring from the field's `description` argument.
if not attr.docstring and (docstring := obj.description):
Expand Down Expand Up @@ -71,3 +81,16 @@ def _process_class(obj: type, cls: Class, *, processed: set[str], schema: bool =
_process_attribute(getattr(obj, member.name), member, cls, processed=processed) # ty: ignore[invalid-argument-type]
elif kind is Kind.FUNCTION:
_process_function(getattr(obj, member.name), member, cls, processed=processed) # ty: ignore[invalid-argument-type]

# Process model fields that may not have been discovered by Griffe.
if hasattr(obj, "model_fields") and isinstance(obj.model_fields, dict):
for field_name, field_info in obj.model_fields.items():
if field_name not in cls.all_members:
# Create an Attribute object for this field
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Create an Attribute object for this field
# Create an Attribute object for this field.

attr = Attribute(
name=field_name,
lineno=0,
endlineno=0,
)
Comment on lines +90 to +94
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we set a docstring as well? To me this whole snippet shows that there's an issue with how we collect fields dynamically, and we should probably first fix that in another PR.

cls.members[field_name] = attr # ty: ignore[invalid-assignment]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
cls.members[field_name] = attr # ty: ignore[invalid-assignment]
cls.set_member(field_name, attr)

_process_attribute(field_info, attr, cls, processed=processed)
30 changes: 29 additions & 1 deletion src/griffe_pydantic/_internal/static.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,20 @@ def _pydantic_validator(func: Function) -> ExprCall | None:
return None


def _literal_or_debug(value: Expr | str | None, /, *, path: str) -> str | None:
if value is None:
return None
if isinstance(value, str):
try:
return ast.literal_eval(value)
except ValueError:
return value
elif isinstance(value, (ExprName, Expr)):
_logger.debug(f"Could not resolve expression '{value}' as a literal for field {path}")
return None
return None


def _process_attribute(attr: Attribute, cls: Class, *, processed: set[str]) -> None:
"""Handle Pydantic fields."""
if attr.canonical_path in processed:
Expand Down Expand Up @@ -189,8 +203,22 @@ def _process_attribute(attr: Attribute, cls: Class, *, processed: set[str]) -> N
attr.labels.discard("instance-attribute")

attr.value = kwargs.get("default")
constraints = {kwarg: value for kwarg, value in kwargs.items() if kwarg not in {"default", "description"}}
constraints = {
kwarg: value
for kwarg, value in kwargs.items()
if kwarg not in {"default", "description", "alias", "validation_alias", "serialization_alias"}
}
attr.extra[common._self_namespace]["constraints"] = constraints
attr.extra[common._mkdocstrings_namespace]["template"] = "pydantic_field.html.jinja"

# Store validation/serialization aliases if defined.
if alias := _literal_or_debug(kwargs.get("alias"), path=attr.path):
attr.extra[common._self_namespace]["validation_alias"] = alias
attr.extra[common._self_namespace]["serialization_alias"] = alias
elif validation_alias := _literal_or_debug(kwargs.get("validation_alias"), path=attr.path):
attr.extra[common._self_namespace]["validation_alias"] = validation_alias
elif serialization_alias := _literal_or_debug(kwargs.get("serialization_alias"), path=attr.path):
attr.extra[common._self_namespace]["serialization_alias"] = serialization_alias

# Populate docstring from the field's `description` argument.
if not attr.docstring and (description_expr := kwargs.get("description")):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{% extends "_base/attribute.html.jinja" %}

{% block contents scoped %}
{% if attribute.extra.griffe_pydantic.validation_alias or attribute.extra.griffe_pydantic.serialization_alias %}
<ul>
{% if attribute.extra.griffe_pydantic.validation_alias == attribute.extra.griffe_pydantic.serialization_alias %}
<li>Alias: <code>{{ attribute.extra.griffe_pydantic.validation_alias }}</code></li>
{% else %}
{% if attribute.extra.griffe_pydantic.validation_alias %}
<li>Input / Validation alias: <code>{{ attribute.extra.griffe_pydantic.validation_alias }}</code></li>
{% endif %}
{% if attribute.extra.griffe_pydantic.serialization_alias %}
<li>Output / Serialization alias: <code>{{ attribute.extra.griffe_pydantic.serialization_alias }}</code></li>
{% endif %}
{% endif %}
</ul>
{% endif %}
{{ super() }}
{% endblock contents %}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
</details>
{% endif %}
{% endblock schema %}

{% block config scoped %}
{% if class.extra.griffe_pydantic.config %}
<p>Config:</p>
Expand All @@ -31,6 +31,21 @@
{% for name, field in fields.items() %}
<li>
<code><autoref optional hover identifier="{{ field.path }}">{{ name }}</autoref></code>
{% if field.extra.griffe_pydantic.validation_alias or field.extra.griffe_pydantic.serialization_alias %}
(
{% if field.extra.griffe_pydantic.validation_alias == field.extra.griffe_pydantic.serialization_alias %}
alias: <code>{{ field.extra.griffe_pydantic.validation_alias }}</code>
{% else %}
{%- if field.extra.griffe_pydantic.validation_alias -%}
validation alias: <code>{{ field.extra.griffe_pydantic.validation_alias }}</code>
{% if field.extra.griffe_pydantic.serialization_alias %}, {% endif %}
{%- endif -%}
{%- if field.extra.griffe_pydantic.serialization_alias -%}
serialization alias: <code>{{ field.extra.griffe_pydantic.serialization_alias }}</code>
{%- endif -%}
{% endif %}
)
{% endif %}
{% with expression = field.annotation %}
(<code>{% include "expression.html.jinja" with context %}</code>)
{% endwith %}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{% extends "_base/pydantic_field.html.jinja" %}
19 changes: 19 additions & 0 deletions tests/test_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ def regular_method(self):

class RegularClass(object):
regular_attr = 1

class AliasClass(BaseModel):
internal_name: str = Field(default="test", alias="external_name")
regular_field: int = Field(default=42)
"""


Expand Down Expand Up @@ -82,6 +86,21 @@ def test_extension(analysis: str) -> None:
schema = package.classes["ExampleModel"].extra["griffe_pydantic"]["schema"]
assert schema.startswith('{\n "description"')

fields = package.classes["ExampleModel"].extra["griffe_pydantic"]["fields"]()
assert "field_without_default" in fields
assert "field_plain_with_validator" in fields
assert "field_with_validator_and_alias" in fields
assert "field_with_constraints_and_description" in fields
assert "regular_method" not in fields

assert "AliasClass" in package.classes
assert package.classes["AliasClass"].labels == {"pydantic-model"}

fields = package.classes["AliasClass"].extra["griffe_pydantic"]["fields"]()
assert "internal_name" in fields
assert "regular_field" in fields
assert "external_name" not in fields


def test_imported_models() -> None:
"""Test the extension with imported models."""
Expand Down
Loading