Coverage for tests/test_loader.py: 96.91%
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""Tests for [the `loader` module][pytkdocs.loader]."""
3import os
4import sys
5from pathlib import Path
6from typing import Set
8import pytest
9from django.db.models.fields import CharField
10from marshmallow import fields
12from pytkdocs.loader import Loader, get_object_tree
13from tests import FIXTURES_DIR
16def test_import_no_path():
17 """Raise error when getting tree for empty object name."""
18 with pytest.raises(ValueError):
19 get_object_tree("")
22def test_import_error():
23 """Raise error when getting tree for missing object."""
24 with pytest.raises(ImportError):
25 get_object_tree("eeeeeeeeeeeeeeeeeee")
28def test_can_find_class_real_path():
29 """Find real path of a class."""
30 leaf = get_object_tree("tests.fixtures.real_path.module_a.DefinedInModuleB")
31 assert leaf.dotted_path == "tests.fixtures.real_path.module_b.DefinedInModuleB"
34def test_can_find_class_method_real_path():
35 """Find real path of a class method."""
36 leaf = get_object_tree("tests.fixtures.real_path.module_a.DefinedInModuleB.method")
37 assert leaf.dotted_path == "tests.fixtures.real_path.module_b.DefinedInModuleB.method"
40def test_can_find_class_attribute_real_path():
41 """Find real path of a class attribute."""
42 leaf = get_object_tree("tests.fixtures.real_path.module_a.DefinedInModuleB.ATTRIBUTE")
43 assert leaf.dotted_path == "tests.fixtures.real_path.module_b.DefinedInModuleB.ATTRIBUTE"
46def test_cannot_find_module_attribute_real_path():
47 """Find real path of a module attribute."""
48 leaf = get_object_tree("tests.fixtures.real_path.module_a.ATTRIBUTE")
49 assert leaf.dotted_path != "tests.fixtures.real_path.module_b.ATTRIBUTE"
52def test_import_module_with_colon_path_syntax():
53 """Import a module using the "colon" path syntax."""
54 leaf = get_object_tree("tests.fixtures.the_package.the_module", new_path_syntax=True)
57def test_import_attribute_with_colon_path_syntax():
58 """Import an attribute using the "colon" path syntax."""
59 leaf = get_object_tree("tests.fixtures.the_package.the_module:THE_ATTRIBUTE")
62def test_import_nested_attribute_with_colon_path_syntax():
63 """Import a nested attribute using the "colon" path syntax."""
64 leaf = get_object_tree("tests.fixtures.the_package.the_module:TheClass.THE_ATTRIBUTE")
67def test_fail_to_import_module_with_colon_path_syntax():
68 """Import a module using the "colon" path syntax."""
69 with pytest.raises(ImportError):
70 get_object_tree("tests.fixtures.does_not_exist", new_path_syntax=True)
73def test_fail_to_import_attribute_with_colon_path_syntax():
74 """Import an attribute using the "colon" path syntax."""
75 with pytest.raises(AttributeError) as error:
76 leaf = get_object_tree("tests.fixtures.the_package.the_module:does_not_exist")
79def test_fail_to_import_nested_attribute_with_colon_path_syntax():
80 """Import a nested attribute using the "colon" path syntax."""
81 with pytest.raises(AttributeError) as error:
82 leaf = get_object_tree("tests.fixtures.the_package.the_module:TheClass.does_not_exist")
85def test_fail_to_import_module_with_dot_path_syntax():
86 """Import a module using the "dot" path syntax."""
87 with pytest.raises(ImportError, match=r"possible causes"):
88 get_object_tree("does_not_exist")
91def test_fail_to_import_attribute_with_dot_path_syntax():
92 """Import an attribute using the "dot" path syntax."""
93 with pytest.raises(AttributeError) as error:
94 leaf = get_object_tree("tests.fixtures.the_package.the_module.does_not_exist")
97def test_fail_to_import_nested_attribute_with_dot_path_syntax():
98 """Import a nested attribute using the "dot" path syntax."""
99 with pytest.raises(AttributeError) as error:
100 leaf = get_object_tree("tests.fixtures.the_package.the_module.TheClass.does_not_exist")
103def test_inheriting_enum_Enum():
104 """Handle `enum.Enum` classes."""
105 """See details at [tests.fixtures.inheriting_enum_Enum][]."""
106 loader = Loader()
107 loader.get_object_documentation("tests.fixtures.inheriting_enum_Enum")
108 assert not loader.errors
111def test_inheriting_typing_NamedTuple():
112 """
113 Handle `typing.NamedTuple classes`.
115 See details at [tests.fixtures.inheriting_typing_NamedTuple][].
116 """
117 loader = Loader()
118 loader.get_object_documentation("tests.fixtures.inheriting_typing_NamedTuple")
119 assert len(loader.errors) == 0
122def test_nested_class():
123 """Handle nested classes."""
124 loader = Loader()
125 obj = loader.get_object_documentation("tests.fixtures.nested_class")
126 assert obj.classes
127 assert obj.classes[0].docstring == "Main docstring."
128 assert obj.classes[0].classes
129 assert obj.classes[0].classes[0].docstring == "Nested docstring."
132def test_loading_deep_package():
133 """Handle deep nesting of packages."""
134 loader = Loader()
135 obj = loader.get_object_documentation("tests.fixtures.pkg1.pkg2.pkg3.pkg4.pkg5")
136 assert obj.docstring == "Hello from the abyss."
137 assert obj.path == "tests.fixtures.pkg1.pkg2.pkg3.pkg4.pkg5"
140def test_loading_package():
141 """Handle basic packages."""
142 loader = Loader()
143 obj = loader.get_object_documentation("tests.fixtures.the_package")
144 assert obj.docstring == "The package docstring."
147def test_loading_namespace_package():
148 """Handle native namespace packages."""
149 loader = Loader()
150 old_paths = list(sys.path)
151 sys.path.append(str(Path(FIXTURES_DIR).resolve()))
152 obj = loader.get_object_documentation("test_namespace.subspace")
153 assert obj.docstring == "The subspace package docstring."
154 assert obj.relative_file_path == f"subspace{os.sep}__init__.py"
155 sys.path = old_paths
158def test_loading_module():
159 """Handle single modules."""
160 loader = Loader()
161 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module")
162 assert obj.docstring == "The module docstring."
165def test_loading_class():
166 """Handle classes."""
167 loader = Loader()
168 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass")
169 assert obj.docstring == "The class docstring."
170 assert obj.bases == ["object"]
173def test_loading_class_with_multiline_docstring_starting_on_first_line():
174 """Handle classes with multiline docstrings where the first line is next to the triple-quotes."""
175 loader = Loader()
176 obj = loader.get_object_documentation("tests.fixtures.first_line_class_docstring.TheClass")
177 assert obj.docstring == """The first line of the docstring.\n\nA bit more of the docstring."""
180def test_loading_dataclass():
181 """Handle dataclasses."""
182 loader = Loader()
183 obj = loader.get_object_documentation("tests.fixtures.dataclass.Person")
184 assert obj.docstring == "Simple dataclass for a person's information"
185 assert len(obj.attributes) == 2
186 name_attr = next(attr for attr in obj.attributes if attr.name == "name") 186 ↛ exitline 186 didn't finish the generator expression on line 186
187 assert name_attr.type == str
188 age_attr = next(attr for attr in obj.attributes if attr.name == "age") 188 ↛ exitline 188 didn't finish the generator expression on line 188
189 assert age_attr.type == int
190 assert age_attr.docstring == "Field description."
191 assert "dataclass" in obj.properties
193 not_dataclass = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.TheNestedClass")
194 assert "dataclass" not in not_dataclass.properties
197def test_loading_empty_dataclass():
198 """Handle empty dataclasses."""
199 loader = Loader()
200 obj = loader.get_object_documentation("tests.fixtures.dataclass.Empty")
201 assert obj.docstring == "A dataclass without any fields"
202 assert len(obj.attributes) == 0
203 assert "dataclass" in obj.properties
206def test_loading_pydantic_model():
207 """Handle Pydantic models."""
208 loader = Loader()
209 obj = loader.get_object_documentation("tests.fixtures.pydantic.Person")
210 assert obj.docstring == "Simple Pydantic Model for a person's information"
211 assert "pydantic-model" in obj.properties
212 name_attr = next(attr for attr in obj.attributes if attr.name == "name") 212 ↛ exitline 212 didn't finish the generator expression on line 212
213 assert name_attr.type == str
214 assert name_attr.docstring == "The person's name"
215 assert "pydantic-field" in name_attr.properties
216 age_attr = next(attr for attr in obj.attributes if attr.name == "age") 216 ↛ exitline 216 didn't finish the generator expression on line 216
217 assert age_attr.type == int
218 assert age_attr.docstring == "The person's age which must be at minimum 18"
219 assert "pydantic-field" in age_attr.properties
220 labels_attr = next(attr for attr in obj.attributes if attr.name == "labels") 220 ↛ exitline 220 didn't finish the generator expression on line 220
221 assert labels_attr.type == Set[str]
222 assert labels_attr.docstring == "Set of labels the person can be referred by"
223 assert "pydantic-field" in labels_attr.properties
226def test_loading_django_model():
227 """Handle Django models"""
228 loader = Loader()
229 obj = loader.get_object_documentation("tests.fixtures.django.Person")
230 assert obj.docstring == "Simple Django Model for a person's information"
231 name_attr = next(attr for attr in obj.attributes if attr.name == "name") 231 ↛ exitline 231 didn't finish the generator expression on line 231
232 assert name_attr.type == CharField
233 assert name_attr.docstring == "Name"
236def test_loading_marshmallow_model():
237 """Handle Marshmallow models."""
238 loader = Loader()
239 obj = loader.get_object_documentation("tests.fixtures.marshmallow.Person")
240 assert obj.docstring == "Simple Marshmallow Model for a person's information"
241 assert "marshmallow-model" in obj.properties
242 name_attr = next(attr for attr in obj.attributes if attr.name == "name") 242 ↛ exitline 242 didn't finish the generator expression on line 242
243 assert name_attr.type == fields.Str
244 assert name_attr.docstring == "The person's name"
245 assert "marshmallow-field" in name_attr.properties
246 assert "required" in name_attr.properties
247 age_attr = next(attr for attr in obj.attributes if attr.name == "age") 247 ↛ exitline 247 didn't finish the generator expression on line 247
248 assert age_attr.type == fields.Int
249 assert age_attr.docstring == "The person's age which must be at minimum 18"
250 assert "marshmallow-field" in age_attr.properties
253def test_loading_nested_class():
254 """Select nested class."""
255 loader = Loader()
256 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.TheNestedClass")
257 assert obj.docstring == "The nested class docstring."
260def test_loading_double_nested_class():
261 """Select double-nested class."""
262 loader = Loader()
263 obj = loader.get_object_documentation(
264 "tests.fixtures.the_package.the_module.TheClass.TheNestedClass.TheDoubleNestedClass"
265 )
266 assert obj.docstring == "The double nested class docstring."
269def test_loading_class_attribute():
270 """Select class attribute."""
271 loader = Loader()
272 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.THE_ATTRIBUTE")
273 assert obj.docstring == "The attribute 0.1 docstring."
276def test_loading_nested_class_attribute():
277 """Select nested-class attribute."""
278 loader = Loader()
279 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.TheNestedClass.THE_ATTRIBUTE")
280 assert obj.docstring == "The attribute 0.2 docstring."
283def test_loading_double_nested_class_attribute():
284 """Select double-nested-class attribute."""
285 loader = Loader()
286 obj = loader.get_object_documentation(
287 "tests.fixtures.the_package.the_module.TheClass.TheNestedClass.TheDoubleNestedClass.THE_ATTRIBUTE"
288 )
289 assert obj.docstring == "The attribute 0.3 docstring."
292def test_loading_class_method():
293 """Select class method."""
294 loader = Loader()
295 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.the_method")
296 assert obj.docstring == "The method1 docstring."
299def test_loading_nested_class_method():
300 """Select nested class method."""
301 loader = Loader()
302 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.TheNestedClass.the_method")
303 assert obj.docstring == "The method2 docstring."
306def test_loading_double_nested_class_method():
307 """Select double-nested class method."""
308 loader = Loader()
309 obj = loader.get_object_documentation(
310 "tests.fixtures.the_package.the_module.TheClass.TheNestedClass.TheDoubleNestedClass.the_method"
311 )
312 assert obj.docstring == "The method3 docstring."
315def test_loading_staticmethod():
316 """Select static method."""
317 loader = Loader()
318 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.the_static_method")
319 assert obj.docstring == "The static method docstring."
322def test_loading_classmethod():
323 """Select class method."""
324 loader = Loader()
325 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.the_class_method")
326 assert obj.docstring == "The class method docstring."
329def test_loading_property():
330 """Select property."""
331 loader = Loader()
332 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.the_property")
333 assert obj.docstring == "The property docstring."
336def test_loading_writable_property():
337 """Select writable property."""
338 loader = Loader()
339 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.the_writable_property")
340 assert obj.docstring == "The writable property getter docstring."
343def test_loading_function():
344 """Select function."""
345 loader = Loader()
346 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.the_function")
347 assert obj.docstring == "The function docstring."
350def test_loading_attribute():
351 """Select attribute."""
352 loader = Loader()
353 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.THE_ATTRIBUTE")
354 assert obj.docstring == "The attribute docstring."
357def test_loading_explicit_members():
358 """Select members explicitly."""
359 loader = Loader()
360 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module", members={"TheClass"})
361 assert len(obj.children) == 1
362 assert obj.children[0].name == "TheClass"
365def test_loading_no_members():
366 """Select no members."""
367 loader = Loader()
368 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module", members=False)
369 assert not obj.children
372def test_loading_with_filters():
373 """Select with filters."""
374 loader = Loader(filters=["!^[A-Z_]+$"])
375 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module")
376 for child in obj.children:
377 assert child.name != "THE_ATTRIBUTE"
380def test_loading_with_filters_reselection():
381 """A filter can cancel a previous filter."""
382 loader = Loader(filters=["![A-Z_]", "[a-z]"])
383 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module")
384 assert obj.classes
385 assert obj.classes[0].name == "TheClass"
388def test_loading_with_members_and_filters():
389 """Select members with filters."""
390 loader = Loader(filters=["!THE"])
391 obj = loader.get_object_documentation(
392 "tests.fixtures.the_package.the_module", members={"THE_ATTRIBUTE", "TheClass"}
393 )
394 assert obj.attributes
395 assert obj.attributes[0].name == "THE_ATTRIBUTE"
396 assert obj.classes
397 assert obj.classes[0].name == "TheClass"
398 assert not any(a.name == "THE_ATTRIBUTE" for a in obj.classes[0].attributes)
401def test_loading_members_set_at_import_time():
402 """Select dynamic members."""
403 loader = Loader()
404 obj = loader.get_object_documentation("tests.fixtures.dynamic_members")
405 assert obj.functions
406 assert len(obj.classes) == 1
407 class_ = obj.classes[0]
408 assert class_.methods
411def test_loading_inherited_members():
412 """Select inherited members."""
413 loader = Loader(inherited_members=True)
414 obj = loader.get_object_documentation("tests.fixtures.inherited_members.Child")
415 for child_name in ("method1", "method2", "V1", "V2"):
416 assert child_name in (child.name for child in obj.children) 416 ↛ exitline 416 didn't finish the generator expression on line 416
419def test_not_loading_inherited_members():
420 """Do not select inherited members."""
421 loader = Loader(inherited_members=False)
422 obj = loader.get_object_documentation("tests.fixtures.inherited_members.Child")
423 for child_name in ("method1", "V1"):
424 assert child_name not in (child.name for child in obj.children)
425 for child_name in ("method2", "V2"):
426 assert child_name in (child.name for child in obj.children) 426 ↛ exitline 426 didn't finish the generator expression on line 426
429def test_loading_selected_inherited_members():
430 """Select specific members, some of them being inherited."""
431 loader = Loader(inherited_members=True)
432 obj = loader.get_object_documentation("tests.fixtures.inherited_members.Child", members={"V1", "V2"})
433 for child_name in ("V1", "V2"):
434 assert child_name in (child.name for child in obj.children) 434 ↛ exitline 434 didn't finish the generator expression on line 434
437def test_loading_pydantic_inherited_members():
438 """Select inherited members in Pydantic models."""
439 loader = Loader(inherited_members=True)
440 obj = loader.get_object_documentation("tests.fixtures.inherited_members.ChildModel")
441 for child_name in ("a", "b"):
442 assert child_name in (child.name for child in obj.children) 442 ↛ exitline 442 didn't finish the generator expression on line 442
445def test_not_loading_pydantic_inherited_members():
446 """Do not select inherited members in Pydantic models."""
447 loader = Loader(inherited_members=False)
448 obj = loader.get_object_documentation("tests.fixtures.inherited_members.ChildModel")
449 assert "a" not in (child.name for child in obj.children)
452def test_loading_wrapped_function():
453 """Load documentation for wrapped function, not wrapper."""
454 loader = Loader()
455 obj = loader.get_object_documentation("tests.fixtures.wrapped_objects.my_function")
456 assert obj.docstring == "My docstring."
459def test_loading_module_wrapped_members():
460 """Load documentation for wrapped function, not wrapper."""
461 loader = Loader()
462 obj = loader.get_object_documentation("tests.fixtures.wrapped_objects")
463 assert obj.functions and obj.functions[0].docstring == "My docstring."
464 assert obj.classes and obj.classes[0].methods and obj.classes[0].methods[0].docstring == "Hello!"
467def test_unwrap_object_with_getattr_method_raising_exception():
468 """Try loading an object that defines a `__getattr__` method which raises an exception."""
469 loader = Loader()
470 loader.get_object_documentation("tests.fixtures.unwrap_getattr_raises")
473def test_loading_coroutine():
474 """Load documentation for a coroutine."""
475 loader = Loader()
476 obj = loader.get_object_documentation("tests.fixtures.asyncio.coroutine_function")
477 assert "async" in obj.properties
480def test_loading_coroutine_method():
481 """Load documentation for a coroutine method."""
482 loader = Loader()
483 obj = loader.get_object_documentation("tests.fixtures.asyncio.ClassContainingCoroutineMethod.coroutine_method")
484 assert "async" in obj.properties
487def test_loading_function_without_async_property():
488 """Load documentation for a function that is not a coroutine."""
489 loader = Loader()
490 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.the_function")
491 assert "async" not in obj.properties
494def test_loading_method_without_async_property():
495 """Load documentation for a method that is not a coroutine."""
496 loader = Loader()
497 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.the_method")
498 assert "async" not in obj.properties
501def test_inherited_properties_docstrings():
502 """Load docstrings from parent class for inherited properties."""
503 loader = Loader(new_path_syntax=True)
504 obj = loader.get_object_documentation("tests.fixtures.inherited_properties:SubClass.read_only")
505 assert obj.docstring == "SuperClass.read_only docs"
506 obj = loader.get_object_documentation("tests.fixtures.inherited_properties:SubClass.mutable")
507 assert obj.docstring == "SuperClass.mutable getter docs"
510def test_loading_cached_properties():
511 """Load cached properties."""
512 loader = Loader(new_path_syntax=True)
513 obj = loader.get_object_documentation("tests.fixtures.cached_properties:C")
514 assert len(obj.children) == 1
515 assert obj.children[0].name == obj.children[0].docstring == "aaa"
516 assert "cached" in obj.children[0].properties
519def test_method_descriptor():
520 """Load a method descriptor."""
521 loader = Loader(new_path_syntax=True)
522 obj = loader.get_object_documentation("tests.fixtures.method_descriptor:descriptor")
523 assert obj.name == "descriptor"
524 assert obj.signature
525 assert len(obj.signature.parameters) == 2
526 assert obj.docstring
527 assert obj.category == "method"
530def test_load_decorated_function():
531 """Load a decorated function."""
532 loader = Loader(new_path_syntax=True)
533 obj = loader.get_object_documentation("tests.fixtures.decorated_function")
534 assert [child.name for child in obj.children] == ["add", "sub"]
535 for child in obj.children:
536 assert child.category == "function"
537 assert child.parent is child.root
538 assert child.parent.name == "decorated_function"