Coverage for src/pytkdocs/loader.py: 91.49%
408 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"""This module is responsible for loading the documentation from Python objects.
3It uses [`inspect`](https://docs.python.org/3/library/inspect.html) for introspecting objects,
4iterating over their members, etc.
5"""
7import importlib
8import inspect
9import pkgutil
10import re
11from collections.abc import Mapping, Sequence
12from contextlib import suppress
13from functools import cache
14from itertools import chain
15from operator import attrgetter
16from pathlib import Path
17from typing import Any, Callable, Optional, Union
19from pytkdocs.objects import Attribute, Class, Function, Method, Module, Object, Source
20from pytkdocs.parsers.attributes import get_class_attributes, get_instance_attributes, get_module_attributes, merge
21from pytkdocs.parsers.docstrings import PARSERS
22from pytkdocs.properties import RE_SPECIAL
24try:
25 from functools import cached_property
26except ImportError:
27 from cached_property import cached_property # type: ignore[no-redef]
30class ObjectNode:
31 """Helper class to represent an object tree.
33 It's not really a tree but more a backward-linked list:
34 each node has a reference to its parent, but not to its child (for simplicity purposes and to avoid bugs).
36 Each node stores an object, its name, and a reference to its parent node.
37 """
39 def __init__(self, obj: Any, name: str, parent: Optional["ObjectNode"] = None) -> None:
40 """Initialize the object.
42 Arguments:
43 obj: A Python object.
44 name: The object's name.
45 parent: The object's parent node.
46 """
47 try: # noqa: SIM105
48 obj = inspect.unwrap(obj)
49 except Exception: # noqa: S110 , BLE001 (we purposely catch every possible exception)
50 # inspect.unwrap at some point runs hasattr(obj, "__wrapped__"),
51 # which triggers the __getattr__ method of the object, which in
52 # turn can raise various exceptions. Probably not just __getattr__.
53 # See https://github.com/pawamoy/pytkdocs/issues/45
54 pass
56 self.obj: Any = obj
57 """The actual Python object."""
59 self.name: str = name
60 """The Python object's name."""
62 self.parent: Optional[ObjectNode] = parent
63 """The parent node."""
65 @property
66 def dotted_path(self) -> str:
67 """Return the Python dotted path to the object.
69 Returns:
70 The Python dotted path to the object.
71 """
72 parts = [self.name]
73 current = self.parent
74 while current:
75 parts.append(current.name)
76 current = current.parent
77 return ".".join(reversed(parts))
79 @property
80 def file_path(self) -> str:
81 """Return the object's module file path.
83 Returns:
84 The object's module file path.
85 """
86 return inspect.getabsfile(self.root.obj)
88 @property
89 def root(self) -> "ObjectNode":
90 """Return the root of the tree.
92 Returns:
93 The root of the tree.
94 """
95 if self.parent is not None:
96 return self.parent.root
97 return self
99 def is_module(self) -> bool:
100 """Tell if this node's object is a module.
102 Returns:
103 The root of the tree.
104 """
105 return inspect.ismodule(self.obj)
107 def is_class(self) -> bool:
108 """Tell if this node's object is a class.
110 Returns:
111 If this node's object is a class.
112 """
113 return inspect.isclass(self.obj)
115 def is_function(self) -> bool:
116 """Tell if this node's object is a function.
118 Returns:
119 If this node's object is a function.
120 """
121 return inspect.isfunction(self.obj)
123 def is_coroutine_function(self) -> bool:
124 """Tell if this node's object is a coroutine.
126 Returns:
127 If this node's object is a coroutine.
128 """
129 return inspect.iscoroutinefunction(self.obj)
131 def is_property(self) -> bool:
132 """Tell if this node's object is a property.
134 Returns:
135 If this node's object is a property.
136 """
137 return isinstance(self.obj, property) or self.is_cached_property()
139 def is_cached_property(self) -> bool:
140 """Tell if this node's object is a cached property.
142 Returns:
143 If this node's object is a cached property.
144 """
145 return isinstance(self.obj, cached_property)
147 def parent_is_class(self) -> bool:
148 """Tell if the object of this node's parent is a class.
150 Returns:
151 If the object of this node's parent is a class.
152 """
153 return bool(self.parent and self.parent.is_class())
155 def is_method(self) -> bool:
156 """Tell if this node's object is a method.
158 Returns:
159 If this node's object is a method.
160 """
161 function_type = type(lambda: None)
162 return self.parent_is_class() and isinstance(self.obj, function_type)
164 def is_method_descriptor(self) -> bool:
165 """Tell if this node's object is a method descriptor.
167 Built-in methods (e.g. those implemented in C/Rust) are often
168 method descriptors, rather than normal methods.
170 Returns:
171 If this node's object is a method descriptor.
172 """
173 return inspect.ismethoddescriptor(self.obj)
175 def is_staticmethod(self) -> bool:
176 """Tell if this node's object is a staticmethod.
178 Returns:
179 If this node's object is a staticmethod.
180 """
181 if not self.parent: 181 ↛ 182line 181 didn't jump to line 182 because the condition on line 181 was never true
182 return False
183 self_from_parent = self.parent.obj.__dict__.get(self.name, None)
184 return self.parent_is_class() and isinstance(self_from_parent, staticmethod)
186 def is_classmethod(self) -> bool:
187 """Tell if this node's object is a classmethod.
189 Returns:
190 If this node's object is a classmethod.
191 """
192 if not self.parent: 192 ↛ 193line 192 didn't jump to line 193 because the condition on line 192 was never true
193 return False
194 self_from_parent = self.parent.obj.__dict__.get(self.name, None)
195 return self.parent_is_class() and isinstance(self_from_parent, classmethod)
198# New path syntax: the new path syntax uses a colon to separate the
199# modules (to import) from the objects (to get with getattr).
200# It's easier to deal with, and it naturally improves error handling.
201# At first, we default to the old syntax, then at some point we will
202# default to the new syntax, and later again we will drop the old syntax.
203def get_object_tree(path: str, new_path_syntax: bool = False) -> ObjectNode: # noqa: FBT001, FBT002
204 """Transform a path into an actual Python object.
206 The path can be arbitrary long. You can pass the path to a package,
207 a module, a class, a function or a global variable, as deep as you
208 want, as long as the deepest module is importable through
209 `importlib.import_module` and each object is obtainable through
210 the `getattr` method. It is not possible to load local objects.
212 Args:
213 path: The dot/colon-separated path of the object.
214 new_path_syntax: Whether to use the "colon" syntax for the path.
216 Raises:
217 ValueError: When the path is not valid (evaluates to `False`).
218 ImportError: When the object or its parent module could not be imported.
220 Returns:
221 The leaf node representing the object and its parents.
222 """
223 if not path:
224 raise ValueError(f"path must be a valid Python path, not {path}")
226 objects: list[str] = []
228 if ":" in path or new_path_syntax:
229 try:
230 module_path, object_path = path.split(":")
231 except ValueError: # no colon
232 module_path, objects = path, []
233 else:
234 objects = object_path.split(".")
236 # let the ImportError bubble up
237 parent_module = importlib.import_module(module_path)
239 else:
240 # We will try to import the longest dotted-path first.
241 # If it fails, we remove the right-most part and put it in a list of "objects", used later.
242 # We loop until we find the deepest importable submodule.
243 obj_parent_modules = path.split(".")
245 while True:
246 parent_module_path = ".".join(obj_parent_modules)
247 try:
248 parent_module = importlib.import_module(parent_module_path)
249 except ImportError as error:
250 if len(obj_parent_modules) == 1:
251 raise ImportError(
252 f"Importing '{path}' failed, possible causes are:\n"
253 f"- an exception happened while importing\n"
254 f"- an element in the path does not exist",
255 ) from error
256 objects.insert(0, obj_parent_modules.pop(-1))
257 else:
258 break
260 # We now have the module containing the desired object.
261 # We will build the object tree by iterating over the previously stored objects names
262 # and trying to get them as attributes.
263 current_node = ObjectNode(parent_module, parent_module.__name__)
264 for obj_name in objects:
265 obj = getattr(current_node.obj, obj_name)
266 child = ObjectNode(obj, obj_name, parent=current_node)
267 current_node = child
269 leaf = current_node
271 # We now try to get the "real" parent module, not the one the object was imported into.
272 # This is important if we want to be able to retrieve the docstring of an attribute for example.
273 # Once we find an object for which we could get the module, we stop trying to get the module.
274 # Once we reach the node before the root, we apply the module if found, and break.
275 real_module = None
276 while current_node.parent is not None:
277 if real_module is None:
278 real_module = inspect.getmodule(current_node.obj)
279 if inspect.ismodule(current_node.parent.obj):
280 if real_module is not None and real_module is not current_node.parent.obj:
281 current_node.parent = ObjectNode(real_module, real_module.__name__)
282 break
283 current_node = current_node.parent
285 return leaf
288class Loader:
289 """This class contains the object documentation loading mechanisms.
291 Any error that occurred during collection of the objects and their documentation is stored in the `errors` list.
292 """
294 def __init__(
295 self,
296 filters: Optional[list[str]] = None,
297 docstring_style: str = "google",
298 docstring_options: Optional[dict] = None,
299 inherited_members: bool = False, # noqa: FBT001, FBT002
300 new_path_syntax: bool = False, # noqa: FBT001, FBT002
301 ) -> None:
302 """Initialize the object.
304 Arguments:
305 filters: A list of regular expressions to fine-grain select members. It is applied recursively.
306 docstring_style: The style to use when parsing docstrings.
307 docstring_options: The options to pass to the docstrings parser.
308 inherited_members: Whether to select inherited members for classes.
309 new_path_syntax: Whether to use the "colon" syntax for the path.
310 """
311 if not filters:
312 filters = []
314 self.filters = [(filtr, re.compile(filtr.lstrip("!"))) for filtr in filters]
315 self.docstring_parser = PARSERS[docstring_style](**(docstring_options or {}))
316 self.errors: list[str] = []
317 self.select_inherited_members = inherited_members
318 self.new_path_syntax = new_path_syntax
320 def get_object_documentation(self, dotted_path: str, members: Optional[Union[set[str], bool]] = None) -> Object:
321 """Get the documentation for an object and its children.
323 Arguments:
324 dotted_path: The Python dotted path to the desired object.
325 members: `True` to select members and filter them, `False` to select no members,
326 or a list of names to explicitly select the members with these names.
327 It is applied only on the root object.
329 Returns:
330 The documented object.
331 """
332 if members is True: 332 ↛ 333line 332 didn't jump to line 333 because the condition on line 332 was never true
333 members = set()
335 root_object: Object
336 leaf = get_object_tree(dotted_path, self.new_path_syntax)
338 if leaf.is_module():
339 root_object = self.get_module_documentation(leaf, members)
340 elif leaf.is_class():
341 root_object = self.get_class_documentation(leaf, members)
342 elif leaf.is_staticmethod():
343 root_object = self.get_staticmethod_documentation(leaf)
344 elif leaf.is_classmethod():
345 root_object = self.get_classmethod_documentation(leaf)
346 elif leaf.is_method_descriptor() or leaf.is_method():
347 root_object = self.get_regular_method_documentation(leaf)
348 elif leaf.is_function():
349 root_object = self.get_function_documentation(leaf)
350 elif leaf.is_property():
351 root_object = self.get_property_documentation(leaf)
352 else:
353 root_object = self.get_attribute_documentation(leaf)
355 root_object.parse_all_docstrings(self.docstring_parser)
357 return root_object
359 def get_module_documentation(
360 self,
361 node: ObjectNode,
362 select_members: Optional[Union[set[str], bool]] = None,
363 ) -> Module:
364 """Get the documentation for a module and its children.
366 Arguments:
367 node: The node representing the module and its parents.
368 select_members: Explicit members to select.
370 Returns:
371 The documented module object.
372 """
373 module = node.obj
374 path = node.dotted_path
375 name = path.split(".")[-1]
376 source: Optional[Source]
378 try:
379 source = Source(inspect.getsource(module), 1)
380 except OSError:
381 try:
382 code = Path(node.file_path).read_text()
383 except (OSError, UnicodeDecodeError):
384 source = None
385 else:
386 source = Source(code, 1) if code else None
388 root_object = Module(
389 name=name,
390 path=path,
391 file_path=node.file_path,
392 docstring=inspect.getdoc(module),
393 source=source,
394 )
396 if select_members is False:
397 return root_object
399 select_members = select_members or set()
401 attributes_data = get_module_attributes(module)
402 root_object.parse_docstring(self.docstring_parser, attributes=attributes_data)
404 for member_name, member in inspect.getmembers(module):
405 if self.select(member_name, select_members): # type: ignore[arg-type]
406 child_node = ObjectNode(member, member_name, parent=node)
407 if child_node.is_class() and node.root.obj is inspect.getmodule(child_node.obj):
408 root_object.add_child(self.get_class_documentation(child_node))
409 elif child_node.is_function() and node.root.obj is inspect.getmodule(child_node.obj):
410 root_object.add_child(self.get_function_documentation(child_node))
411 elif member_name in attributes_data:
412 root_object.add_child(self.get_attribute_documentation(child_node, attributes_data[member_name]))
414 if hasattr(module, "__path__"):
415 for _, modname, _ in pkgutil.iter_modules(module.__path__):
416 if self.select(modname, select_members): # type: ignore[arg-type] 416 ↛ 415line 416 didn't jump to line 415 because the condition on line 416 was always true
417 leaf = get_object_tree(f"{path}.{modname}")
418 root_object.add_child(self.get_module_documentation(leaf))
420 return root_object
422 @staticmethod
423 def _class_path(cls: type) -> str: # noqa: PLW0211
424 mod = cls.__module__
425 qname = cls.__qualname__
426 if mod == "builtins":
427 return qname
428 return f"{mod}.{qname}"
430 def get_class_documentation(
431 self,
432 node: ObjectNode,
433 select_members: Optional[Union[set[str], bool]] = None,
434 ) -> Class:
435 """Get the documentation for a class and its children.
437 Arguments:
438 node: The node representing the class and its parents.
439 select_members: Explicit members to select.
441 Returns:
442 The documented class object.
443 """
444 class_ = node.obj
445 docstring = inspect.cleandoc(class_.__doc__ or "")
446 bases = [self._class_path(b) for b in class_.__bases__]
448 source: Optional[Source]
450 try:
451 source = Source(*inspect.getsourcelines(node.obj))
452 except (OSError, TypeError):
453 source = None
455 root_object = Class(
456 name=node.name,
457 path=node.dotted_path,
458 file_path=node.file_path,
459 docstring=docstring,
460 bases=bases,
461 source=source,
462 )
464 # Even if we don't select members, we want to correctly parse the docstring
465 attributes_data: dict[str, dict[str, Any]] = {}
466 for parent_class in reversed(class_.__mro__[:-1]):
467 merge(attributes_data, get_class_attributes(parent_class))
468 context: dict[str, Any] = {"attributes": attributes_data}
469 if "__init__" in class_.__dict__:
470 try:
471 attributes_data.update(get_instance_attributes(class_.__init__))
472 context["signature"] = inspect.signature(class_.__init__)
473 except (TypeError, ValueError):
474 pass
475 root_object.parse_docstring(self.docstring_parser, **context)
477 if select_members is False: 477 ↛ 478line 477 didn't jump to line 478 because the condition on line 477 was never true
478 return root_object
480 select_members = select_members or set()
482 # Build the list of members
483 members = {}
484 inherited = set()
485 direct_members = class_.__dict__
486 all_members = dict(inspect.getmembers(class_))
487 for member_name, member in all_members.items():
488 if member is class_: 488 ↛ 489line 488 didn't jump to line 489 because the condition on line 488 was never true
489 continue
490 if not (member is type or member is object) and self.select(member_name, select_members): # type: ignore[arg-type]
491 if member_name not in direct_members:
492 if self.select_inherited_members:
493 members[member_name] = member
494 inherited.add(member_name)
495 else:
496 members[member_name] = member
498 # Iterate on the selected members
499 child: Object
500 for member_name, member in members.items():
501 child_node = ObjectNode(member, member_name, parent=node)
502 if child_node.is_class():
503 child = self.get_class_documentation(child_node)
504 elif child_node.is_classmethod():
505 child = self.get_classmethod_documentation(child_node)
506 elif child_node.is_staticmethod():
507 child = self.get_staticmethod_documentation(child_node)
508 elif child_node.is_method():
509 child = self.get_regular_method_documentation(child_node)
510 elif child_node.is_property():
511 child = self.get_property_documentation(child_node)
512 elif member_name in attributes_data:
513 child = self.get_attribute_documentation(child_node, attributes_data[member_name])
514 else:
515 continue
516 if member_name in inherited:
517 child.properties.append("inherited")
518 root_object.add_child(child)
520 for attr_name, properties, add_method in (
521 ("__fields__", ["pydantic-model"], self.get_pydantic_field_documentation),
522 ("_declared_fields", ["marshmallow-model"], self.get_marshmallow_field_documentation),
523 ("_meta.get_fields", ["django-model"], self.get_django_field_documentation),
524 ("__dataclass_fields__", ["dataclass"], self.get_annotated_dataclass_field),
525 ):
526 if self.detect_field_model(attr_name, direct_members, all_members):
527 root_object.properties.extend(properties)
528 self.add_fields(
529 node,
530 root_object,
531 attr_name,
532 all_members,
533 select_members,
534 class_,
535 add_method,
536 )
537 break
539 return root_object
541 def detect_field_model(self, attr_name: str, direct_members: Sequence[str], all_members: dict) -> bool:
542 """Detect if an attribute is present in members.
544 Arguments:
545 attr_name: The name of the attribute to detect, can contain dots.
546 direct_members: The direct members of the class.
547 all_members: All members of the class.
549 Returns:
550 Whether the attribute is present.
551 """
552 first_order_attr_name, remainder = split_attr_name(attr_name)
553 if not (
554 first_order_attr_name in direct_members
555 or (self.select_inherited_members and first_order_attr_name in all_members)
556 ):
557 return False
559 if remainder:
560 with suppress(AttributeError):
561 return bool(attrgetter(remainder)(all_members[first_order_attr_name]))
562 return True
564 def add_fields(
565 self,
566 node: ObjectNode,
567 root_object: Object,
568 attr_name: str,
569 members: Mapping[str, Any],
570 select_members: Optional[Union[set[str], bool]],
571 base_class: type,
572 add_method: Callable,
573 ) -> None:
574 """Add detected fields to the current object.
576 Arguments:
577 node: The current object node.
578 root_object: The current object.
579 attr_name: The fields attribute name.
580 members: The members to pick the fields attribute in.
581 select_members: The members to select.
582 base_class: The class declaring the fields.
583 add_method: The method to add the children object.
584 """
585 fields = get_fields(attr_name, members=members)
587 for field_name, field in fields.items():
588 select_field = self.select(field_name, select_members) # type: ignore[arg-type]
589 is_inherited = field_is_inherited(field_name, attr_name, base_class)
591 if select_field and (self.select_inherited_members or not is_inherited): 591 ↛ 587line 591 didn't jump to line 587 because the condition on line 591 was always true
592 child_node = ObjectNode(obj=field, name=field_name, parent=node)
593 root_object.add_child(add_method(child_node))
595 def get_function_documentation(self, node: ObjectNode) -> Function:
596 """Get the documentation for a function.
598 Arguments:
599 node: The node representing the function and its parents.
601 Returns:
602 The documented function object.
603 """
604 function = node.obj
605 source: Optional[Source]
606 signature: Optional[inspect.Signature]
608 try:
609 signature = inspect.signature(function)
610 except TypeError:
611 signature = None
613 try:
614 source = Source(*inspect.getsourcelines(function))
615 except OSError:
616 source = None
618 properties: list[str] = []
619 if node.is_coroutine_function():
620 properties.append("async")
622 return Function(
623 name=node.name,
624 path=node.dotted_path,
625 file_path=node.file_path,
626 docstring=inspect.getdoc(function),
627 signature=signature,
628 source=source,
629 properties=properties,
630 )
632 def get_property_documentation(self, node: ObjectNode) -> Attribute:
633 """Get the documentation for a property.
635 Arguments:
636 node: The node representing the property and its parents.
638 Returns:
639 The documented attribute object (properties are considered attributes for now).
640 """
641 prop = node.obj
642 path = node.dotted_path
643 properties = ["property"]
644 if node.is_cached_property():
645 # cached_property is always writable, see the docs
646 properties.extend(["writable", "cached"])
647 sig_source_func = prop.func
648 else:
649 properties.append("readonly" if prop.fset is None else "writable")
650 sig_source_func = prop.fget
652 source: Optional[Source]
654 try:
655 signature = inspect.signature(sig_source_func)
656 except (TypeError, ValueError):
657 attr_type = None
658 else:
659 attr_type = signature.return_annotation
661 try:
662 source = Source(*inspect.getsourcelines(sig_source_func))
663 except (OSError, TypeError):
664 source = None
666 return Attribute(
667 name=node.name,
668 path=path,
669 file_path=node.file_path,
670 docstring=inspect.getdoc(prop),
671 attr_type=attr_type,
672 properties=properties,
673 source=source,
674 )
676 @staticmethod
677 def get_pydantic_field_documentation(node: ObjectNode) -> Attribute:
678 """Get the documentation for a Pydantic Field.
680 Arguments:
681 node: The node representing the Field and its parents.
683 Returns:
684 The documented attribute object.
685 """
686 prop = node.obj
687 path = node.dotted_path
688 properties = ["pydantic-field"]
689 if prop.required:
690 properties.append("required")
692 return Attribute(
693 name=node.name,
694 path=path,
695 file_path=node.file_path,
696 docstring=prop.field_info.description,
697 attr_type=prop.outer_type_,
698 properties=properties,
699 )
701 @staticmethod
702 def get_django_field_documentation(node: ObjectNode) -> Attribute:
703 """Get the documentation for a Django Field.
705 Arguments:
706 node: The node representing the Field and its parents.
708 Returns:
709 The documented attribute object.
710 """
711 prop = node.obj
712 path = node.dotted_path
713 properties = ["django-field"]
715 if prop.null: 715 ↛ 716line 715 didn't jump to line 716 because the condition on line 715 was never true
716 properties.append("nullable")
717 if prop.blank: 717 ↛ 718line 717 didn't jump to line 718 because the condition on line 717 was never true
718 properties.append("blank")
720 # set correct docstring based on verbose_name and help_text
721 # both should be converted to str type in case lazy translation
722 # is being used, which is common scenario in django
723 docstring = f"{prop.verbose_name}: {prop.help_text}" if prop.help_text else str(prop.verbose_name)
725 return Attribute(
726 name=node.name,
727 path=path,
728 file_path=node.file_path,
729 docstring=docstring,
730 attr_type=prop.__class__,
731 properties=properties,
732 )
734 @staticmethod
735 def get_marshmallow_field_documentation(node: ObjectNode) -> Attribute:
736 """Get the documentation for a Marshmallow Field.
738 Arguments:
739 node: The node representing the Field and its parents.
741 Returns:
742 The documented attribute object.
743 """
744 prop = node.obj
745 path = node.dotted_path
746 properties = ["marshmallow-field"]
747 if prop.required:
748 properties.append("required")
750 return Attribute(
751 name=node.name,
752 path=path,
753 file_path=node.file_path,
754 docstring=prop.metadata.get("description"),
755 attr_type=type(prop),
756 properties=properties,
757 )
759 @staticmethod
760 def get_annotated_dataclass_field(node: ObjectNode, attribute_data: Optional[dict] = None) -> Attribute:
761 """Get the documentation for a dataclass field.
763 Arguments:
764 node: The node representing the annotation and its parents.
765 attribute_data: Docstring and annotation for this attribute.
767 Returns:
768 The documented attribute object.
769 """
770 if attribute_data is None: 770 ↛ 776line 770 didn't jump to line 776 because the condition on line 770 was always true
771 if node.parent_is_class(): 771 ↛ 774line 771 didn't jump to line 774 because the condition on line 771 was always true
772 attribute_data = get_class_attributes(node.parent.obj).get(node.name, {}) # type: ignore[union-attr]
773 else:
774 attribute_data = get_module_attributes(node.root.obj).get(node.name, {})
776 return Attribute(
777 name=node.name,
778 path=node.dotted_path,
779 file_path=node.file_path,
780 docstring=attribute_data["docstring"],
781 attr_type=attribute_data["annotation"],
782 properties=["dataclass-field"],
783 )
785 def get_classmethod_documentation(self, node: ObjectNode) -> Method:
786 """Get the documentation for a class-method.
788 Arguments:
789 node: The node representing the class-method and its parents.
791 Returns:
792 The documented method object.
793 """
794 return self.get_method_documentation(node, ["classmethod"])
796 def get_staticmethod_documentation(self, node: ObjectNode) -> Method:
797 """Get the documentation for a static-method.
799 Arguments:
800 node: The node representing the static-method and its parents.
802 Returns:
803 The documented method object.
804 """
805 return self.get_method_documentation(node, ["staticmethod"])
807 def get_regular_method_documentation(self, node: ObjectNode) -> Method:
808 """Get the documentation for a regular method (not class- nor static-method).
810 We do extra processing in this method to discard docstrings of `__init__` methods
811 that were inherited from parent classes.
813 Arguments:
814 node: The node representing the method and its parents.
816 Returns:
817 The documented method object.
818 """
819 method = self.get_method_documentation(node)
820 if node.parent: 820 ↛ 834line 820 didn't jump to line 834 because the condition on line 820 was always true
821 class_ = node.parent.obj
822 if RE_SPECIAL.match(node.name):
823 docstring = method.docstring
824 parent_classes = class_.__mro__[1:]
825 for parent_class in parent_classes:
826 try:
827 parent_method = getattr(parent_class, node.name)
828 except AttributeError:
829 continue
830 else:
831 if docstring == inspect.getdoc(parent_method):
832 method.docstring = ""
833 break
834 return method
836 def get_method_documentation(self, node: ObjectNode, properties: Optional[list[str]] = None) -> Method:
837 """Get the documentation for a method or method descriptor.
839 Arguments:
840 node: The node representing the method and its parents.
841 properties: A list of properties to apply to the method.
843 Returns:
844 The documented method object.
845 """
846 method = node.obj
847 path = node.dotted_path
848 signature: Optional[inspect.Signature]
849 source: Optional[Source]
851 try:
852 source = Source(*inspect.getsourcelines(method))
853 except OSError:
854 source = None
855 except TypeError:
856 source = None
858 if node.is_coroutine_function():
859 if properties is None: 859 ↛ 862line 859 didn't jump to line 862 because the condition on line 859 was always true
860 properties = ["async"]
861 else:
862 properties.append("async")
864 try:
865 # for "built-in" functions, e.g. those implemented in C,
866 # inspect.signature() uses the __text_signature__ attribute, which
867 # provides a limited but still useful amount of signature information.
868 # "built-in" functions with no __text_signature__ will
869 # raise a ValueError().
870 signature = inspect.signature(method)
871 except ValueError:
872 signature = None
874 return Method(
875 name=node.name,
876 path=path,
877 file_path=node.file_path,
878 docstring=inspect.getdoc(method),
879 signature=signature,
880 properties=properties or [],
881 source=source,
882 )
884 @staticmethod
885 def get_attribute_documentation(node: ObjectNode, attribute_data: Optional[dict] = None) -> Attribute:
886 """Get the documentation for an attribute.
888 Arguments:
889 node: The node representing the method and its parents.
890 attribute_data: Docstring and annotation for this attribute.
892 Returns:
893 The documented attribute object.
894 """
895 if attribute_data is None:
896 if node.parent_is_class():
897 attribute_data = get_class_attributes(node.parent.obj).get(node.name, {}) # type: ignore[union-attr]
898 else:
899 attribute_data = get_module_attributes(node.root.obj).get(node.name, {}) # type: ignore[union-attr]
900 return Attribute(
901 name=node.name,
902 path=node.dotted_path,
903 file_path=node.file_path,
904 docstring=attribute_data.get("docstring", ""),
905 attr_type=attribute_data.get("annotation", None),
906 )
908 def select(self, name: str, names: set[str]) -> bool:
909 """Tells whether we should select an object or not, given its name.
911 If the set of names is not empty, we check against it, otherwise we check against filters.
913 Arguments:
914 name: The name of the object to select or not.
915 names: An explicit list of names to select.
917 Returns:
918 Yes or no.
919 """
920 if names:
921 return name in names
922 return not self.filter_name_out(name)
924 @cache # noqa: B019
925 def filter_name_out(self, name: str) -> bool:
926 """Filter a name based on the loader's filters.
928 Arguments:
929 name: The name to filter.
931 Returns:
932 True if the name was filtered out, False otherwise.
933 """
934 if not self.filters:
935 return False
936 keep = True
937 for fltr, regex in self.filters:
938 is_matching = bool(regex.search(name))
939 if is_matching:
940 if str(fltr).startswith("!"):
941 is_matching = not is_matching
942 keep = is_matching
943 return not keep
946def field_is_inherited(field_name: str, fields_name: str, base_class: type) -> bool:
947 """Check if a field with a certain name was inherited from parent classes.
949 Arguments:
950 field_name: The name of the field to check.
951 fields_name: The name of the attribute in which the fields are stored.
952 base_class: The base class in which the field appears.
954 Returns:
955 Whether the field was inherited.
956 """
957 # To tell if a field was inherited, we check if it exists in parent classes __fields__ attributes.
958 # We don't check the current class, nor the top one (object), hence __mro__[1:-1]
959 return field_name in set(
960 chain(
961 *(getattr(parent_class, fields_name, {}).keys() for parent_class in base_class.__mro__[1:-1]),
962 ),
963 )
966def split_attr_name(attr_name: str) -> tuple[str, Optional[str]]:
967 """Split an attribute name into a first-order attribute name and remainder.
969 Args:
970 attr_name: Attribute name (a)
972 Returns:
973 Tuple containing:
974 first_order_attr_name: Name of the first order attribute (a)
975 remainder: The remainder (b.c)
977 """
978 first_order_attr_name, *remaining = attr_name.split(".", maxsplit=1)
979 remainder = remaining[0] if remaining else None
980 return first_order_attr_name, remainder
983def get_fields( # noqa: D103
984 attr_name: str,
985 *,
986 members: Optional[Mapping[str, Any]] = None,
987 class_obj: Optional[type] = None,
988) -> dict[str, Any]:
989 if not (bool(members) ^ bool(class_obj)): 989 ↛ 990line 989 didn't jump to line 990 because the condition on line 989 was never true
990 raise ValueError("Either members or class_obj is required.")
991 first_order_attr_name, remainder = split_attr_name(attr_name)
992 fields = members[first_order_attr_name] if members else dict(vars(class_obj)).get(first_order_attr_name, {})
993 if remainder:
994 fields = attrgetter(remainder)(fields)
996 if callable(fields):
997 fields = fields()
999 if not isinstance(fields, dict):
1000 # Support Django models
1001 try:
1002 fields = {getattr(f, "name", str(f)): f for f in fields if not getattr(f, "auto_created", False)}
1003 except TypeError:
1004 fields = {}
1006 return fields