Coverage for tests/test_loader.py: 96.23%
331 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-09 18:24 +0100
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-09 18:24 +0100
1"""Tests for [the `loader` module][pytkdocs.loader]."""
3import os
4import sys
5from pathlib import Path
7import pytest
8from django.db.models.fields import CharField
9from marshmallow import fields
11from pytkdocs.loader import Loader, get_object_tree
12from tests import FIXTURES_DIR
15def test_import_no_path() -> None:
16 """Raise error when getting tree for empty object name."""
17 with pytest.raises(ValueError): # noqa: PT011
18 get_object_tree("")
21def test_import_error() -> None:
22 """Raise error when getting tree for missing object."""
23 with pytest.raises(ImportError):
24 get_object_tree("eeeeeeeeeeeeeeeeeee")
27def test_can_find_class_real_path() -> None:
28 """Find real path of a class."""
29 leaf = get_object_tree("tests.fixtures.real_path.module_a.DefinedInModuleB")
30 assert leaf.dotted_path == "tests.fixtures.real_path.module_b.DefinedInModuleB"
33def test_can_find_class_method_real_path() -> None:
34 """Find real path of a class method."""
35 leaf = get_object_tree("tests.fixtures.real_path.module_a.DefinedInModuleB.method")
36 assert leaf.dotted_path == "tests.fixtures.real_path.module_b.DefinedInModuleB.method"
39def test_can_find_class_attribute_real_path() -> None:
40 """Find real path of a class attribute."""
41 leaf = get_object_tree("tests.fixtures.real_path.module_a.DefinedInModuleB.ATTRIBUTE")
42 assert leaf.dotted_path == "tests.fixtures.real_path.module_b.DefinedInModuleB.ATTRIBUTE"
45def test_cannot_find_module_attribute_real_path() -> None:
46 """Find real path of a module attribute."""
47 leaf = get_object_tree("tests.fixtures.real_path.module_a.ATTRIBUTE")
48 assert leaf.dotted_path != "tests.fixtures.real_path.module_b.ATTRIBUTE"
51def test_import_module_with_colon_path_syntax() -> None:
52 """Import a module using the "colon" path syntax."""
53 get_object_tree("tests.fixtures.the_package.the_module", new_path_syntax=True)
56def test_import_attribute_with_colon_path_syntax() -> None:
57 """Import an attribute using the "colon" path syntax."""
58 get_object_tree("tests.fixtures.the_package.the_module:THE_ATTRIBUTE")
61def test_import_nested_attribute_with_colon_path_syntax() -> None:
62 """Import a nested attribute using the "colon" path syntax."""
63 get_object_tree("tests.fixtures.the_package.the_module:TheClass.THE_ATTRIBUTE")
66def test_fail_to_import_module_with_colon_path_syntax() -> None:
67 """Import a module using the "colon" path syntax."""
68 with pytest.raises(ImportError):
69 get_object_tree("tests.fixtures.does_not_exist", new_path_syntax=True)
72def test_fail_to_import_attribute_with_colon_path_syntax() -> None:
73 """Import an attribute using the "colon" path syntax."""
74 with pytest.raises(AttributeError):
75 get_object_tree("tests.fixtures.the_package.the_module:does_not_exist")
78def test_fail_to_import_nested_attribute_with_colon_path_syntax() -> None:
79 """Import a nested attribute using the "colon" path syntax."""
80 with pytest.raises(AttributeError):
81 get_object_tree("tests.fixtures.the_package.the_module:TheClass.does_not_exist")
84def test_fail_to_import_module_with_dot_path_syntax() -> None:
85 """Import a module using the "dot" path syntax."""
86 with pytest.raises(ImportError, match=r"possible causes"):
87 get_object_tree("does_not_exist")
90def test_fail_to_import_attribute_with_dot_path_syntax() -> None:
91 """Import an attribute using the "dot" path syntax."""
92 with pytest.raises(AttributeError):
93 get_object_tree("tests.fixtures.the_package.the_module.does_not_exist")
96def test_fail_to_import_nested_attribute_with_dot_path_syntax() -> None:
97 """Import a nested attribute using the "dot" path syntax."""
98 with pytest.raises(AttributeError):
99 get_object_tree("tests.fixtures.the_package.the_module.TheClass.does_not_exist")
102def test_inheriting_enum_Enum() -> None: # noqa: N802
103 """Handle `enum.Enum` classes."""
104 """See details at [tests.fixtures.inheriting_enum_Enum][]."""
105 loader = Loader()
106 loader.get_object_documentation("tests.fixtures.inheriting_enum_Enum")
107 assert not loader.errors
110def test_inheriting_typing_NamedTuple() -> None: # noqa: N802
111 """Handle `typing.NamedTuple classes`.
113 See details at [tests.fixtures.inheriting_typing_NamedTuple][].
114 """
115 loader = Loader()
116 loader.get_object_documentation("tests.fixtures.inheriting_typing_NamedTuple")
117 assert len(loader.errors) == 0
120def test_nested_class() -> None:
121 """Handle nested classes."""
122 loader = Loader()
123 obj = loader.get_object_documentation("tests.fixtures.nested_class")
124 assert obj.classes
125 assert obj.classes[0].docstring == "Main docstring."
126 assert obj.classes[0].classes
127 assert obj.classes[0].classes[0].docstring == "Nested docstring."
130def test_loading_deep_package() -> None:
131 """Handle deep nesting of packages."""
132 loader = Loader()
133 obj = loader.get_object_documentation("tests.fixtures.pkg1.pkg2.pkg3.pkg4.pkg5")
134 assert obj.docstring == "Hello from the abyss."
135 assert obj.path == "tests.fixtures.pkg1.pkg2.pkg3.pkg4.pkg5"
138def test_loading_package() -> None:
139 """Handle basic packages."""
140 loader = Loader()
141 obj = loader.get_object_documentation("tests.fixtures.the_package")
142 assert obj.docstring == "The package docstring."
145def test_loading_namespace_package() -> None:
146 """Handle native namespace packages."""
147 loader = Loader()
148 old_paths = list(sys.path)
149 sys.path.append(str(Path(FIXTURES_DIR).resolve()))
150 obj = loader.get_object_documentation("test_namespace.subspace")
151 assert obj.docstring == "The subspace package docstring."
152 assert obj.relative_file_path == f"subspace{os.sep}__init__.py"
153 sys.path = old_paths
156def test_loading_module() -> None:
157 """Handle single modules."""
158 loader = Loader()
159 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module")
160 assert obj.docstring == "The module docstring."
163def test_loading_class() -> None:
164 """Handle classes."""
165 loader = Loader()
166 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass")
167 assert obj.docstring == "The class docstring."
168 assert obj.bases == ["object"] # type: ignore[attr-defined]
171def test_loading_class_with_multiline_docstring_starting_on_first_line() -> None:
172 """Handle classes with multiline docstrings where the first line is next to the triple-quotes."""
173 loader = Loader()
174 obj = loader.get_object_documentation("tests.fixtures.first_line_class_docstring.TheClass")
175 assert obj.docstring == """The first line of the docstring.\n\nA bit more of the docstring."""
178def test_loading_dataclass() -> None:
179 """Handle dataclasses."""
180 loader = Loader()
181 obj = loader.get_object_documentation("tests.fixtures.dataclass.Person")
182 assert obj.docstring == "Simple dataclass for a person's information"
183 assert len(obj.attributes) == 2
184 name_attr = next(attr for attr in obj.attributes if attr.name == "name")
185 assert name_attr.type is str
186 age_attr = next(attr for attr in obj.attributes if attr.name == "age")
187 assert age_attr.type is int
188 assert age_attr.docstring == "Field description."
189 assert "dataclass" in obj.properties
191 not_dataclass = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.TheNestedClass")
192 assert "dataclass" not in not_dataclass.properties
195def test_loading_empty_dataclass() -> None:
196 """Handle empty dataclasses."""
197 loader = Loader()
198 obj = loader.get_object_documentation("tests.fixtures.dataclass.Empty")
199 assert obj.docstring == "A dataclass without any fields"
200 assert len(obj.attributes) == 0
201 assert "dataclass" in obj.properties
204@pytest.mark.xfail(reason="Probable change in Pydantic since v1")
205def test_loading_pydantic_model() -> None:
206 """Handle Pydantic models."""
207 loader = Loader()
208 obj = loader.get_object_documentation("tests.fixtures.pydantic.Person")
209 assert obj.docstring == "Simple Pydantic Model for a person's information"
210 assert "pydantic-model" in obj.properties
211 name_attr = next(attr for attr in obj.attributes if attr.name == "name")
212 assert name_attr.type is str
213 assert name_attr.docstring == "The person's name"
214 assert "pydantic-field" in name_attr.properties
215 age_attr = next(attr for attr in obj.attributes if attr.name == "age")
216 assert age_attr.type is int
217 assert age_attr.docstring == "The person's age which must be at minimum 18"
218 assert "pydantic-field" in age_attr.properties
219 labels_attr = next(attr for attr in obj.attributes if attr.name == "labels")
220 assert labels_attr.type == set[str]
221 assert labels_attr.docstring == "Set of labels the person can be referred by"
222 assert "pydantic-field" in labels_attr.properties
225def test_loading_django_model() -> None:
226 """Handle Django models."""
227 loader = Loader()
228 obj = loader.get_object_documentation("tests.fixtures.django.Person")
229 assert obj.docstring == "Simple Django Model for a person's information"
230 name_attr = next(attr for attr in obj.attributes if attr.name == "name")
231 assert name_attr.type == CharField
232 assert name_attr.docstring == "Name"
235@pytest.mark.xfail(
236 reason="Change in marshmallow: Passing field metadata as keyword arguments is deprecated. "
237 "Use the explicit `metadata=...` argument instead.",
238)
239def test_loading_marshmallow_model() -> None:
240 """Handle Marshmallow models."""
241 loader = Loader()
242 obj = loader.get_object_documentation("tests.fixtures.marshmallow.Person")
243 assert obj.docstring == "Simple Marshmallow Model for a person's information"
244 assert "marshmallow-model" in obj.properties
245 name_attr = next(attr for attr in obj.attributes if attr.name == "name")
246 assert name_attr.type == fields.Str
247 assert name_attr.docstring == "The person's name"
248 assert "marshmallow-field" in name_attr.properties
249 assert "required" in name_attr.properties
250 age_attr = next(attr for attr in obj.attributes if attr.name == "age")
251 assert age_attr.type == fields.Int
252 assert age_attr.docstring == "The person's age which must be at minimum 18"
253 assert "marshmallow-field" in age_attr.properties
256def test_loading_nested_class() -> None:
257 """Select nested class."""
258 loader = Loader()
259 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.TheNestedClass")
260 assert obj.docstring == "The nested class docstring."
263def test_loading_double_nested_class() -> None:
264 """Select double-nested class."""
265 loader = Loader()
266 obj = loader.get_object_documentation(
267 "tests.fixtures.the_package.the_module.TheClass.TheNestedClass.TheDoubleNestedClass",
268 )
269 assert obj.docstring == "The double nested class docstring."
272def test_loading_class_attribute() -> None:
273 """Select class attribute."""
274 loader = Loader()
275 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.THE_ATTRIBUTE")
276 assert obj.docstring == "The attribute 0.1 docstring."
279def test_loading_nested_class_attribute() -> None:
280 """Select nested-class attribute."""
281 loader = Loader()
282 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.TheNestedClass.THE_ATTRIBUTE")
283 assert obj.docstring == "The attribute 0.2 docstring."
286def test_loading_double_nested_class_attribute() -> None:
287 """Select double-nested-class attribute."""
288 loader = Loader()
289 obj = loader.get_object_documentation(
290 "tests.fixtures.the_package.the_module.TheClass.TheNestedClass.TheDoubleNestedClass.THE_ATTRIBUTE",
291 )
292 assert obj.docstring == "The attribute 0.3 docstring."
295def test_loading_class_method() -> None:
296 """Select class method."""
297 loader = Loader()
298 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.the_method")
299 assert obj.docstring == "The method1 docstring."
302def test_loading_nested_class_method() -> None:
303 """Select nested class method."""
304 loader = Loader()
305 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.TheNestedClass.the_method")
306 assert obj.docstring == "The method2 docstring."
309def test_loading_double_nested_class_method() -> None:
310 """Select double-nested class method."""
311 loader = Loader()
312 obj = loader.get_object_documentation(
313 "tests.fixtures.the_package.the_module.TheClass.TheNestedClass.TheDoubleNestedClass.the_method",
314 )
315 assert obj.docstring == "The method3 docstring."
318def test_loading_staticmethod() -> None:
319 """Select static method."""
320 loader = Loader()
321 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.the_static_method")
322 assert obj.docstring == "The static method docstring."
325def test_loading_classmethod() -> None:
326 """Select class method."""
327 loader = Loader()
328 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.the_class_method")
329 assert obj.docstring == "The class method docstring."
332def test_loading_property() -> None:
333 """Select property."""
334 loader = Loader()
335 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.the_property")
336 assert obj.docstring == "The property docstring."
339def test_loading_writable_property() -> None:
340 """Select writable property."""
341 loader = Loader()
342 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.the_writable_property")
343 assert obj.docstring == "The writable property getter docstring."
346def test_loading_function() -> None:
347 """Select function."""
348 loader = Loader()
349 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.the_function")
350 assert obj.docstring == "The function docstring."
353def test_loading_attribute() -> None:
354 """Select attribute."""
355 loader = Loader()
356 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.THE_ATTRIBUTE")
357 assert obj.docstring == "The attribute docstring."
360def test_loading_explicit_members() -> None:
361 """Select members explicitly."""
362 loader = Loader()
363 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module", members={"TheClass"})
364 assert len(obj.children) == 1
365 assert obj.children[0].name == "TheClass"
368def test_loading_no_members() -> None:
369 """Select no members."""
370 loader = Loader()
371 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module", members=False)
372 assert not obj.children
375def test_loading_with_filters() -> None:
376 """Select with filters."""
377 loader = Loader(filters=["!^[A-Z_]+$"])
378 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module")
379 for child in obj.children:
380 assert child.name != "THE_ATTRIBUTE"
383def test_loading_with_filters_reselection() -> None:
384 """A filter can cancel a previous filter."""
385 loader = Loader(filters=["![A-Z_]", "[a-z]"])
386 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module")
387 assert obj.classes
388 assert obj.classes[0].name == "TheClass"
391def test_loading_with_members_and_filters() -> None:
392 """Select members with filters."""
393 loader = Loader(filters=["!THE"])
394 obj = loader.get_object_documentation(
395 "tests.fixtures.the_package.the_module",
396 members={"THE_ATTRIBUTE", "TheClass"},
397 )
398 assert obj.attributes
399 assert obj.attributes[0].name == "THE_ATTRIBUTE"
400 assert obj.classes
401 assert obj.classes[0].name == "TheClass"
402 assert not any(a.name == "THE_ATTRIBUTE" for a in obj.classes[0].attributes)
405def test_loading_members_set_at_import_time() -> None:
406 """Select dynamic members."""
407 loader = Loader()
408 obj = loader.get_object_documentation("tests.fixtures.dynamic_members")
409 assert obj.functions
410 assert len(obj.classes) == 1
411 class_ = obj.classes[0]
412 assert class_.methods
415def test_loading_inherited_members() -> None:
416 """Select inherited members."""
417 loader = Loader(inherited_members=True)
418 obj = loader.get_object_documentation("tests.fixtures.inherited_members.Child")
419 for child_name in ("method1", "method2", "V1", "V2"):
420 assert child_name in (child.name for child in obj.children)
423def test_not_loading_inherited_members() -> None:
424 """Do not select inherited members."""
425 loader = Loader(inherited_members=False)
426 obj = loader.get_object_documentation("tests.fixtures.inherited_members.Child")
427 for child_name in ("method1", "V1"):
428 assert child_name not in (child.name for child in obj.children)
429 for child_name in ("method2", "V2"):
430 assert child_name in (child.name for child in obj.children)
433def test_loading_selected_inherited_members() -> None:
434 """Select specific members, some of them being inherited."""
435 loader = Loader(inherited_members=True)
436 obj = loader.get_object_documentation("tests.fixtures.inherited_members.Child", members={"V1", "V2"})
437 for child_name in ("V1", "V2"):
438 assert child_name in (child.name for child in obj.children)
441@pytest.mark.xfail(reason="Probable change in Pydantic since v1")
442def test_loading_pydantic_inherited_members() -> None:
443 """Select inherited members in Pydantic models."""
444 loader = Loader(inherited_members=True)
445 obj = loader.get_object_documentation("tests.fixtures.inherited_members.ChildModel")
446 for child_name in ("a", "b"): 446 ↛ exitline 446 didn't return from function 'test_loading_pydantic_inherited_members' because the loop on line 446 didn't complete
447 assert child_name in (child.name for child in obj.children)
450def test_not_loading_pydantic_inherited_members() -> None:
451 """Do not select inherited members in Pydantic models."""
452 loader = Loader(inherited_members=False)
453 obj = loader.get_object_documentation("tests.fixtures.inherited_members.ChildModel")
454 assert "a" not in (child.name for child in obj.children)
457def test_loading_wrapped_function() -> None:
458 """Load documentation for wrapped function, not wrapper."""
459 loader = Loader()
460 obj = loader.get_object_documentation("tests.fixtures.wrapped_objects.my_function")
461 assert obj.docstring == "My docstring."
464def test_loading_module_wrapped_members() -> None:
465 """Load documentation for wrapped function, not wrapper."""
466 loader = Loader()
467 obj = loader.get_object_documentation("tests.fixtures.wrapped_objects")
468 assert obj.functions
469 assert obj.functions[0].docstring == "My docstring."
470 assert obj.classes
471 assert obj.classes[0].methods
472 assert obj.classes[0].methods[0].docstring == "Hello!"
475def test_unwrap_object_with_getattr_method_raising_exception() -> None:
476 """Try loading an object that defines a `__getattr__` method which raises an exception."""
477 loader = Loader()
478 loader.get_object_documentation("tests.fixtures.unwrap_getattr_raises")
481def test_loading_coroutine() -> None:
482 """Load documentation for a coroutine."""
483 loader = Loader()
484 obj = loader.get_object_documentation("tests.fixtures.asyncio.coroutine_function")
485 assert "async" in obj.properties
488def test_loading_coroutine_method() -> None:
489 """Load documentation for a coroutine method."""
490 loader = Loader()
491 obj = loader.get_object_documentation("tests.fixtures.asyncio.ClassContainingCoroutineMethod.coroutine_method")
492 assert "async" in obj.properties
495def test_loading_function_without_async_property() -> None:
496 """Load documentation for a function that is not a coroutine."""
497 loader = Loader()
498 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.the_function")
499 assert "async" not in obj.properties
502def test_loading_method_without_async_property() -> None:
503 """Load documentation for a method that is not a coroutine."""
504 loader = Loader()
505 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.the_method")
506 assert "async" not in obj.properties
509def test_inherited_properties_docstrings() -> None:
510 """Load docstrings from parent class for inherited properties."""
511 loader = Loader(new_path_syntax=True)
512 obj = loader.get_object_documentation("tests.fixtures.inherited_properties:SubClass.read_only")
513 assert obj.docstring == "SuperClass.read_only docs"
514 obj = loader.get_object_documentation("tests.fixtures.inherited_properties:SubClass.mutable")
515 assert obj.docstring == "SuperClass.mutable getter docs"
518def test_loading_cached_properties() -> None:
519 """Load cached properties."""
520 loader = Loader(new_path_syntax=True)
521 obj = loader.get_object_documentation("tests.fixtures.cached_properties:C")
522 assert len(obj.children) == 1
523 assert obj.children[0].name == obj.children[0].docstring == "aaa"
524 assert "cached" in obj.children[0].properties
527def test_method_descriptor() -> None:
528 """Load a method descriptor."""
529 loader = Loader(new_path_syntax=True)
530 obj = loader.get_object_documentation("tests.fixtures.method_descriptor:descriptor")
531 assert obj.name == "descriptor"
532 assert obj.signature # type: ignore[attr-defined]
533 assert len(obj.signature.parameters) == 2 # type: ignore[attr-defined]
534 assert obj.docstring
535 assert obj.category == "method"
538def test_load_decorated_function() -> None:
539 """Load a decorated function."""
540 loader = Loader(new_path_syntax=True)
541 obj = loader.get_object_documentation("tests.fixtures.decorated_function")
542 assert [child.name for child in obj.children] == ["add", "sub"]
543 for child in obj.children:
544 assert child.category == "function"
545 assert child.parent is child.root
546 assert child.parent.name == "decorated_function"