Coverage for tests/test_extension.py: 100.00%
52 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-05 17:55 +0200
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-05 17:55 +0200
1"""Tests for the `extension` module."""
3from __future__ import annotations
5import logging
6from typing import TYPE_CHECKING
8import pytest
9from griffe import Extensions, temporary_inspected_package, temporary_visited_package
11from griffe_pydantic._internal.extension import PydanticExtension
13if TYPE_CHECKING:
14 from mkdocstrings_handlers.python.handler import PythonHandler
17code = """
18 from pydantic import field_validator, ConfigDict, BaseModel, Field
21 class ExampleParentModel(BaseModel):
22 '''An example parent model.'''
23 parent_field: str = Field(..., description="Parent field.")
26 class ExampleModel(ExampleParentModel):
27 '''An example child model.'''
29 model_config = ConfigDict(frozen=False)
31 field_without_default: str
32 '''Shows the *[Required]* marker in the signature.'''
34 field_plain_with_validator: int = 100
35 '''Show standard field with type annotation.'''
37 field_with_validator_and_alias: str = Field("FooBar", alias="BarFoo", validation_alias="BarFoo")
38 '''Shows corresponding validator with link/anchor.'''
40 field_with_constraints_and_description: int = Field(
41 default=5, ge=0, le=100, description="Shows constraints within doc string."
42 )
44 @field_validator("field_with_validator_and_alias", "field_plain_with_validator", mode="before")
45 @classmethod
46 def check_max_length_ten(cls, v):
47 '''Show corresponding field with link/anchor.'''
48 if len(v) >= 10:
49 raise ValueError("No more than 10 characters allowed")
50 return v
52 def regular_method(self):
53 pass
56 class RegularClass(object):
57 regular_attr = 1
58"""
61@pytest.mark.parametrize("analysis", ["static", "dynamic"])
62def test_extension(analysis: str) -> None:
63 """Test the extension."""
64 loader = {"static": temporary_visited_package, "dynamic": temporary_inspected_package}[analysis]
65 with loader(
66 "package",
67 modules={"__init__.py": code},
68 extensions=Extensions(PydanticExtension(schema=True)),
69 search_sys_path=analysis == "dynamic",
70 ) as package:
71 assert package
73 assert "ExampleParentModel" in package.classes
74 assert package.classes["ExampleParentModel"].labels == {"pydantic-model"}
76 assert "ExampleModel" in package.classes
77 assert package.classes["ExampleModel"].labels == {"pydantic-model"}
79 config = package.classes["ExampleModel"].extra["griffe_pydantic"]["config"]
80 assert config == {"frozen": False}
82 schema = package.classes["ExampleModel"].extra["griffe_pydantic"]["schema"]
83 assert schema.startswith('{\n "description"')
86def test_imported_models() -> None:
87 """Test the extension with imported models."""
88 with temporary_visited_package(
89 "package",
90 modules={
91 "__init__.py": "from ._private import MyModel\n\n__all__ = ['MyModel']",
92 "_private.py": "from pydantic import BaseModel\n\nclass MyModel(BaseModel):\n field1: str\n '''Some field.'''\n",
93 },
94 extensions=Extensions(PydanticExtension(schema=False)),
95 ) as package:
96 assert package["MyModel"].labels == {"pydantic-model"}
97 assert package["MyModel.field1"].labels == {"pydantic-field"}
100def test_rendering_model_config_using_configdict(python_handler: PythonHandler) -> None:
101 """Test the extension with model config using ConfigDict."""
102 code = """
103 from pydantic import BaseModel, ConfigDict, Field
105 class Model(BaseModel):
106 usage: str | None = Field(
107 None,
108 description="Some description.",
109 example="Some example.",
110 )
111 model_config = ConfigDict(
112 json_schema_extra={
113 "example": {
114 "usage": "Some usage.",
115 "limitations": "Some limitations.",
116 "billing": "Some value.",
117 "notice_period": "Some value.",
118 }
119 }
120 )
121 """
122 with temporary_visited_package(
123 "package",
124 modules={"__init__.py": code},
125 extensions=Extensions(PydanticExtension(schema=False)),
126 ) as package:
127 python_handler.render(package["Model"], python_handler.get_options({})) # Assert no errors.
130def test_not_crashing_on_dynamic_field_description(caplog: pytest.LogCaptureFixture) -> None:
131 """Test the extension with dynamic field description."""
132 code = """
133 import pydantic
135 desc = "xyz"
137 class TestModel(pydantic.BaseModel):
138 abc: str = pydantic.Field(description=desc)
139 """
140 with (
141 caplog.at_level(logging.DEBUG),
142 temporary_visited_package(
143 "package",
144 modules={"__init__.py": code},
145 extensions=Extensions(PydanticExtension(schema=False)),
146 ),
147 ):
148 assert any(
149 record.levelname == "DEBUG" and "field 'package.TestModel.abc' as literal" in record.message
150 for record in caplog.records
151 )
154def test_ignore_classvars() -> None:
155 """Test the extension ignores class variables."""
156 code = """
157 from pydantic import BaseModel
158 from typing import ClassVar
160 class Model(BaseModel):
161 field: str
162 class_var: ClassVar[int] = 1
163 """
164 with temporary_visited_package(
165 "package",
166 modules={"__init__.py": code},
167 extensions=Extensions(PydanticExtension(schema=False)),
168 ) as package:
169 assert "pydantic-field" not in package["Model.class_var"].labels
170 assert "class-attribute" in package["Model.class_var"].labels
173def test_wildcard_field_validator() -> None:
174 """Test field validator that works on all fields."""
175 code = """
176 from pydantic import BaseModel, field_validator
178 class Schema(BaseModel):
179 a: int
180 b: int
182 @field_validator('*', mode='before')
183 @classmethod
184 def set_if_none(cls, v: Any, info):
185 ...
186 """
187 with temporary_visited_package(
188 "package",
189 modules={"__init__.py": code},
190 extensions=Extensions(PydanticExtension(schema=False)),
191 ) as package:
192 validator = package["Schema.set_if_none"]
193 assert validator.labels == {"pydantic-validator"}
194 assert validator in package["Schema.a"].extra["griffe_pydantic"]["validators"]
195 assert validator in package["Schema.b"].extra["griffe_pydantic"]["validators"]
198def test_ignoring_properties() -> None:
199 """Properties are not fields and must be ignored."""
200 code = """
201 from pydantic import BaseModel, field
203 class Base(BaseModel):
204 @property
205 def a(self) -> int:
206 return 0
208 class Model(Base):
209 b: int = field(default=1)
210 """
211 with temporary_visited_package(
212 "package",
213 modules={"__init__.py": code},
214 extensions=Extensions(PydanticExtension(schema=False)),
215 ) as package:
216 assert "pydantic-field" not in package["Model.a"].labels
219def test_process_non_model_base_class_fields() -> None:
220 """Fields in a non-model base class must be processed."""
221 code = """
222 from pydantic import BaseModel, field
224 class A:
225 a: int = 0
227 class B(BaseModel, A):
228 b: int = 1
229 """
230 with temporary_visited_package(
231 "package",
232 modules={"__init__.py": code},
233 extensions=Extensions(PydanticExtension(schema=False)),
234 ) as package:
235 assert "pydantic-field" in package["B.a"].labels