Coverage for src/pytkdocs/loader.py: 93.48%
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"""
2This module is responsible for loading the documentation from Python objects.
4It uses [`inspect`](https://docs.python.org/3/library/inspect.html) for introspecting objects,
5iterating over their members, etc.
6"""
8import importlib
9import inspect
10import pkgutil
11import re
12from functools import lru_cache
13from itertools import chain
14from operator import attrgetter
15from pathlib import Path
16from typing import Any, Dict, List, Mapping, Optional, Set, Tuple, Union
18from pytkdocs.objects import Attribute, Class, Function, Method, Module, Object, Source
19from pytkdocs.parsers.attributes import get_class_attributes, get_instance_attributes, get_module_attributes, merge
20from pytkdocs.parsers.docstrings import PARSERS
21from pytkdocs.properties import RE_SPECIAL
23try:
24 from functools import cached_property # type: ignore
25except ImportError:
26 from cached_property import cached_property # type: ignore
29class ObjectNode:
30 """
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 """
41 Initialize the object.
43 Arguments:
44 obj: A Python object.
45 name: The object's name.
46 parent: The object's parent node.
47 """
48 try:
49 obj = inspect.unwrap(obj)
50 except Exception: # noqa: S110,W0703 (we purposely catch every possible exception)
51 # inspect.unwrap at some point runs hasattr(obj, "__wrapped__"),
52 # which triggers the __getattr__ method of the object, which in
53 # turn can raise various exceptions. Probably not just __getattr__.
54 # See https://github.com/pawamoy/pytkdocs/issues/45
55 pass # noqa: WPS420 (no other way than passing)
57 self.obj: Any = obj
58 """The actual Python object."""
60 self.name: str = name
61 """The Python object's name."""
63 self.parent: Optional[ObjectNode] = parent
64 """The parent node."""
66 @property
67 def dotted_path(self) -> str:
68 """
69 Return the Python dotted path to the object.
71 Returns:
72 The Python dotted path to the object.
73 """
74 parts = [self.name]
75 current = self.parent
76 while current:
77 parts.append(current.name)
78 current = current.parent
79 return ".".join(reversed(parts))
81 @property
82 def file_path(self) -> str:
83 """
84 Return the object's module file path.
86 Returns:
87 The object's module file path.
88 """
89 return inspect.getabsfile(self.root.obj)
91 @property
92 def root(self) -> "ObjectNode":
93 """
94 Return the root of the tree.
96 Returns:
97 The root of the tree.
98 """
99 if self.parent is not None:
100 return self.parent.root
101 return self
103 def is_module(self) -> bool:
104 """
105 Tell if this node's object is a module.
107 Returns:
108 The root of the tree.
109 """
110 return inspect.ismodule(self.obj)
112 def is_class(self) -> bool:
113 """
114 Tell if this node's object is a class.
116 Returns:
117 If this node's object is a class.
118 """
119 return inspect.isclass(self.obj)
121 def is_function(self) -> bool:
122 """
123 Tell if this node's object is a function.
125 Returns:
126 If this node's object is a function.
127 """
128 return inspect.isfunction(self.obj)
130 def is_coroutine_function(self) -> bool:
131 """
132 Tell if this node's object is a coroutine.
134 Returns:
135 If this node's object is a coroutine.
136 """
137 return inspect.iscoroutinefunction(self.obj)
139 def is_property(self) -> bool:
140 """
141 Tell if this node's object is a property.
143 Returns:
144 If this node's object is a property.
145 """
146 return isinstance(self.obj, property) or self.is_cached_property()
148 def is_cached_property(self) -> bool:
149 """
150 Tell if this node's object is a cached property.
152 Returns:
153 If this node's object is a cached property.
154 """
155 return isinstance(self.obj, cached_property)
157 def parent_is_class(self) -> bool:
158 """
159 Tell if the object of this node's parent is a class.
161 Returns:
162 If the object of this node's parent is a class.
163 """
164 return bool(self.parent and self.parent.is_class())
166 def is_method(self) -> bool:
167 """
168 Tell if this node's object is a method.
170 Returns:
171 If this node's object is a method.
172 """
173 function_type = type(lambda: None) 173 ↛ exitline 173 didn't run the lambda on line 173
174 return self.parent_is_class() and isinstance(self.obj, function_type)
176 def is_method_descriptor(self) -> bool:
177 """
178 Tell if this node's object is a method descriptor.
180 Built-in methods (e.g. those implemented in C/Rust) are often
181 method descriptors, rather than normal methods.
183 Returns:
184 If this node's object is a method descriptor.
185 """
186 return inspect.ismethoddescriptor(self.obj)
188 def is_staticmethod(self) -> bool:
189 """
190 Tell if this node's object is a staticmethod.
192 Returns:
193 If this node's object is a staticmethod.
194 """
195 if not self.parent: 195 ↛ 196line 195 didn't jump to line 196, because the condition on line 195 was never true
196 return False
197 self_from_parent = self.parent.obj.__dict__.get(self.name, None)
198 return self.parent_is_class() and isinstance(self_from_parent, staticmethod)
200 def is_classmethod(self) -> bool:
201 """
202 Tell if this node's object is a classmethod.
204 Returns:
205 If this node's object is a classmethod.
206 """
207 if not self.parent: 207 ↛ 208line 207 didn't jump to line 208, because the condition on line 207 was never true
208 return False
209 self_from_parent = self.parent.obj.__dict__.get(self.name, None)
210 return self.parent_is_class() and isinstance(self_from_parent, classmethod)
213# New path syntax: the new path syntax uses a colon to separate the
214# modules (to import) from the objects (to get with getattr).
215# It's easier to deal with, and it naturally improves error handling.
216# At first, we default to the old syntax, then at some point we will
217# default to the new syntax, and later again we will drop the old syntax.
218def get_object_tree(path: str, new_path_syntax: bool = False) -> ObjectNode:
219 """
220 Transform a path into an actual Python object.
222 The path can be arbitrary long. You can pass the path to a package,
223 a module, a class, a function or a global variable, as deep as you
224 want, as long as the deepest module is importable through
225 `importlib.import_module` and each object is obtainable through
226 the `getattr` method. It is not possible to load local objects.
228 Args:
229 path: The dot/colon-separated path of the object.
230 new_path_syntax: Whether to use the "colon" syntax for the path.
232 Raises:
233 ValueError: When the path is not valid (evaluates to `False`).
234 ImportError: When the object or its parent module could not be imported.
236 Returns:
237 The leaf node representing the object and its parents.
238 """
239 if not path:
240 raise ValueError(f"path must be a valid Python path, not {path}")
242 objects: List[str] = []
244 if ":" in path or new_path_syntax:
245 try:
246 module_path, object_path = path.split(":")
247 except ValueError: # no colon
248 module_path, objects = path, []
249 else:
250 objects = object_path.split(".")
252 # let the ImportError bubble up
253 parent_module = importlib.import_module(module_path)
255 else:
256 # We will try to import the longest dotted-path first.
257 # If it fails, we remove the right-most part and put it in a list of "objects", used later.
258 # We loop until we find the deepest importable submodule.
259 obj_parent_modules = path.split(".")
261 while True:
262 parent_module_path = ".".join(obj_parent_modules)
263 try:
264 parent_module = importlib.import_module(parent_module_path)
265 except ImportError as error:
266 if len(obj_parent_modules) == 1:
267 raise ImportError(
268 f"Importing '{path}' failed, possible causes are:\n"
269 f"- an exception happened while importing\n"
270 f"- an element in the path does not exist",
271 ) from error
272 objects.insert(0, obj_parent_modules.pop(-1))
273 else:
274 break
276 # We now have the module containing the desired object.
277 # We will build the object tree by iterating over the previously stored objects names
278 # and trying to get them as attributes.
279 current_node = ObjectNode(parent_module, parent_module.__name__)
280 for obj_name in objects:
281 obj = getattr(current_node.obj, obj_name)
282 child = ObjectNode(obj, obj_name, parent=current_node)
283 current_node = child
285 leaf = current_node
287 # We now try to get the "real" parent module, not the one the object was imported into.
288 # This is important if we want to be able to retrieve the docstring of an attribute for example.
289 # Once we find an object for which we could get the module, we stop trying to get the module.
290 # Once we reach the node before the root, we apply the module if found, and break.
291 real_module = None
292 while current_node.parent is not None:
293 if real_module is None:
294 real_module = inspect.getmodule(current_node.obj)
295 if inspect.ismodule(current_node.parent.obj):
296 if real_module is not None and real_module is not current_node.parent.obj:
297 current_node.parent = ObjectNode(real_module, real_module.__name__)
298 break
299 current_node = current_node.parent
301 return leaf
304class Loader:
305 """
306 This class contains the object documentation loading mechanisms.
308 Any error that occurred during collection of the objects and their documentation is stored in the `errors` list.
309 """
311 def __init__(
312 self,
313 filters: Optional[List[str]] = None,
314 docstring_style: str = "google",
315 docstring_options: Optional[dict] = None,
316 inherited_members: bool = False,
317 new_path_syntax: bool = False,
318 ) -> None:
319 """
320 Initialize the object.
322 Arguments:
323 filters: A list of regular expressions to fine-grain select members. It is applied recursively.
324 docstring_style: The style to use when parsing docstrings.
325 docstring_options: The options to pass to the docstrings parser.
326 inherited_members: Whether to select inherited members for classes.
327 new_path_syntax: Whether to use the "colon" syntax for the path.
328 """
329 if not filters:
330 filters = []
332 self.filters = [(filtr, re.compile(filtr.lstrip("!"))) for filtr in filters]
333 self.docstring_parser = PARSERS[docstring_style](**(docstring_options or {})) # type: ignore
334 self.errors: List[str] = []
335 self.select_inherited_members = inherited_members
336 self.new_path_syntax = new_path_syntax
338 def get_object_documentation(self, dotted_path: str, members: Optional[Union[Set[str], bool]] = None) -> Object:
339 """
340 Get the documentation for an object and its children.
342 Arguments:
343 dotted_path: The Python dotted path to the desired object.
344 members: `True` to select members and filter them, `False` to select no members,
345 or a list of names to explicitly select the members with these names.
346 It is applied only on the root object.
348 Returns:
349 The documented object.
350 """
351 if members is True: 351 ↛ 352line 351 didn't jump to line 352, because the condition on line 351 was never true
352 members = set()
354 root_object: Object
355 leaf = get_object_tree(dotted_path, self.new_path_syntax)
357 if leaf.is_module():
358 root_object = self.get_module_documentation(leaf, members)
359 elif leaf.is_class():
360 root_object = self.get_class_documentation(leaf, members)
361 elif leaf.is_staticmethod():
362 root_object = self.get_staticmethod_documentation(leaf)
363 elif leaf.is_classmethod():
364 root_object = self.get_classmethod_documentation(leaf)
365 elif leaf.is_method_descriptor():
366 root_object = self.get_regular_method_documentation(leaf)
367 elif leaf.is_method():
368 root_object = self.get_regular_method_documentation(leaf)
369 elif leaf.is_function():
370 root_object = self.get_function_documentation(leaf)
371 elif leaf.is_property():
372 root_object = self.get_property_documentation(leaf)
373 else:
374 root_object = self.get_attribute_documentation(leaf)
376 root_object.parse_all_docstrings(self.docstring_parser)
378 return root_object
380 def get_module_documentation(self, node: ObjectNode, select_members=None) -> Module:
381 """
382 Get the documentation for a module and its children.
384 Arguments:
385 node: The node representing the module and its parents.
386 select_members: Explicit members to select.
388 Returns:
389 The documented module object.
390 """
391 module = node.obj
392 path = node.dotted_path
393 name = path.split(".")[-1]
394 source: Optional[Source]
396 try:
397 source = Source(inspect.getsource(module), 1)
398 except OSError as error:
399 try:
400 code = Path(node.file_path).read_text()
401 except (OSError, UnicodeDecodeError):
402 source = None
403 else:
404 source = Source(code, 1) if code else None
406 root_object = Module(
407 name=name,
408 path=path,
409 file_path=node.file_path,
410 docstring=inspect.getdoc(module),
411 source=source,
412 )
414 if select_members is False:
415 return root_object
417 select_members = select_members or set()
419 attributes_data = get_module_attributes(module)
420 root_object.parse_docstring(self.docstring_parser, attributes=attributes_data)
422 for member_name, member in inspect.getmembers(module):
423 if self.select(member_name, select_members):
424 child_node = ObjectNode(member, member_name, parent=node)
425 if child_node.is_class() and node.root.obj is inspect.getmodule(child_node.obj):
426 root_object.add_child(self.get_class_documentation(child_node))
427 elif child_node.is_function() and node.root.obj is inspect.getmodule(child_node.obj):
428 root_object.add_child(self.get_function_documentation(child_node))
429 elif member_name in attributes_data:
430 root_object.add_child(self.get_attribute_documentation(child_node, attributes_data[member_name]))
432 if hasattr(module, "__path__"): # noqa: WPS421 (hasattr)
433 for _, modname, _ in pkgutil.iter_modules(module.__path__):
434 if self.select(modname, select_members): 434 ↛ 433line 434 didn't jump to line 433, because the condition on line 434 was never false
435 leaf = get_object_tree(f"{path}.{modname}")
436 root_object.add_child(self.get_module_documentation(leaf))
438 return root_object
440 @staticmethod
441 def _class_path(cls):
442 mod = cls.__module__
443 qname = cls.__qualname__
444 if mod == "builtins":
445 return qname
446 else:
447 return f"{mod}.{qname}"
449 def get_class_documentation(self, node: ObjectNode, select_members=None) -> Class:
450 """
451 Get the documentation for a class and its children.
453 Arguments:
454 node: The node representing the class and its parents.
455 select_members: Explicit members to select.
457 Returns:
458 The documented class object.
459 """
460 class_ = node.obj
461 docstring = inspect.cleandoc(class_.__doc__ or "")
462 bases = [self._class_path(b) for b in class_.__bases__]
464 source: Optional[Source]
466 try:
467 source = Source(*inspect.getsourcelines(node.obj))
468 except (OSError, TypeError) as error:
469 source = None
471 root_object = Class(
472 name=node.name,
473 path=node.dotted_path,
474 file_path=node.file_path,
475 docstring=docstring,
476 bases=bases,
477 source=source,
478 )
480 # Even if we don't select members, we want to correctly parse the docstring
481 attributes_data: Dict[str, Dict[str, Any]] = {}
482 for parent_class in reversed(class_.__mro__[:-1]):
483 merge(attributes_data, get_class_attributes(parent_class))
484 context: Dict[str, Any] = {"attributes": attributes_data}
485 if "__init__" in class_.__dict__:
486 try:
487 attributes_data.update(get_instance_attributes(class_.__init__))
488 context["signature"] = inspect.signature(class_.__init__)
489 except (TypeError, ValueError):
490 pass
491 root_object.parse_docstring(self.docstring_parser, **context)
493 if select_members is False: 493 ↛ 494line 493 didn't jump to line 494, because the condition on line 493 was never true
494 return root_object
496 select_members = select_members or set()
498 # Build the list of members
499 members = {}
500 inherited = set()
501 direct_members = class_.__dict__
502 all_members = dict(inspect.getmembers(class_))
503 for member_name, member in all_members.items():
504 if member is class_: 504 ↛ 505line 504 didn't jump to line 505, because the condition on line 504 was never true
505 continue
506 if not (member is type or member is object) and self.select(member_name, select_members):
507 if member_name not in direct_members:
508 if self.select_inherited_members:
509 members[member_name] = member
510 inherited.add(member_name)
511 else:
512 members[member_name] = member
514 # Iterate on the selected members
515 child: Object
516 for member_name, member in members.items():
517 child_node = ObjectNode(member, member_name, parent=node)
518 if child_node.is_class():
519 child = self.get_class_documentation(child_node)
520 elif child_node.is_classmethod():
521 child = self.get_classmethod_documentation(child_node)
522 elif child_node.is_staticmethod():
523 child = self.get_staticmethod_documentation(child_node)
524 elif child_node.is_method():
525 child = self.get_regular_method_documentation(child_node)
526 elif child_node.is_property():
527 child = self.get_property_documentation(child_node)
528 elif member_name in attributes_data:
529 child = self.get_attribute_documentation(child_node, attributes_data[member_name])
530 else:
531 continue
532 if member_name in inherited:
533 child.properties.append("inherited")
534 root_object.add_child(child)
536 for attr_name, properties, add_method in (
537 ("__fields__", ["pydantic-model"], self.get_pydantic_field_documentation),
538 ("_declared_fields", ["marshmallow-model"], self.get_marshmallow_field_documentation),
539 ("_meta.get_fields", ["django-model"], self.get_django_field_documentation),
540 ("__dataclass_fields__", ["dataclass"], self.get_annotated_dataclass_field),
541 ):
542 if self.detect_field_model(attr_name, direct_members, all_members):
543 root_object.properties.extend(properties)
544 self.add_fields(
545 node,
546 root_object,
547 attr_name,
548 all_members,
549 select_members,
550 class_,
551 add_method,
552 )
553 break
555 return root_object
557 def detect_field_model(self, attr_name: str, direct_members, all_members) -> bool:
558 """
559 Detect if an attribute is present in members.
561 Arguments:
562 attr_name: The name of the attribute to detect, can contain dots.
563 direct_members: The direct members of the class.
564 all_members: All members of the class.
566 Returns:
567 Whether the attribute is present.
568 """
570 first_order_attr_name, remainder = split_attr_name(attr_name)
571 if not (
572 first_order_attr_name in direct_members
573 or (self.select_inherited_members and first_order_attr_name in all_members)
574 ):
575 return False
577 if remainder and not attrgetter(remainder)(all_members[first_order_attr_name]): 577 ↛ 578line 577 didn't jump to line 578, because the condition on line 577 was never true
578 return False
579 return True
581 def add_fields(
582 self,
583 node: ObjectNode,
584 root_object: Object,
585 attr_name: str,
586 members,
587 select_members,
588 base_class,
589 add_method,
590 ) -> None:
591 """
592 Add detected fields to the current object.
594 Arguments:
595 node: The current object node.
596 root_object: The current object.
597 attr_name: The fields attribute name.
598 members: The members to pick the fields attribute in.
599 select_members: The members to select.
600 base_class: The class declaring the fields.
601 add_method: The method to add the children object.
602 """
604 fields = get_fields(attr_name, members=members)
606 for field_name, field in fields.items():
607 select_field = self.select(field_name, select_members)
608 is_inherited = field_is_inherited(field_name, attr_name, base_class)
610 if select_field and (self.select_inherited_members or not is_inherited):
611 child_node = ObjectNode(obj=field, name=field_name, parent=node)
612 root_object.add_child(add_method(child_node))
614 def get_function_documentation(self, node: ObjectNode) -> Function:
615 """
616 Get the documentation for a function.
618 Arguments:
619 node: The node representing the function and its parents.
621 Returns:
622 The documented function object.
623 """
624 function = node.obj
625 path = node.dotted_path
626 source: Optional[Source]
627 signature: Optional[inspect.Signature]
629 try:
630 signature = inspect.signature(function)
631 except TypeError as error:
632 signature = None
634 try:
635 source = Source(*inspect.getsourcelines(function))
636 except OSError as error:
637 source = None
639 properties: List[str] = []
640 if node.is_coroutine_function():
641 properties.append("async")
643 return Function(
644 name=node.name,
645 path=node.dotted_path,
646 file_path=node.file_path,
647 docstring=inspect.getdoc(function),
648 signature=signature,
649 source=source,
650 properties=properties,
651 )
653 def get_property_documentation(self, node: ObjectNode) -> Attribute:
654 """
655 Get the documentation for a property.
657 Arguments:
658 node: The node representing the property and its parents.
660 Returns:
661 The documented attribute object (properties are considered attributes for now).
662 """
663 prop = node.obj
664 path = node.dotted_path
665 properties = ["property"]
666 if node.is_cached_property():
667 # cached_property is always writable, see the docs
668 properties.extend(["writable", "cached"])
669 sig_source_func = prop.func
670 else:
671 properties.append("readonly" if prop.fset is None else "writable")
672 sig_source_func = prop.fget
674 source: Optional[Source]
676 try:
677 signature = inspect.signature(sig_source_func)
678 except (TypeError, ValueError) as error:
679 attr_type = None
680 else:
681 attr_type = signature.return_annotation
683 try:
684 source = Source(*inspect.getsourcelines(sig_source_func))
685 except (OSError, TypeError) as error:
686 source = None
688 return Attribute(
689 name=node.name,
690 path=path,
691 file_path=node.file_path,
692 docstring=inspect.getdoc(prop),
693 attr_type=attr_type,
694 properties=properties,
695 source=source,
696 )
698 @staticmethod
699 def get_pydantic_field_documentation(node: ObjectNode) -> Attribute:
700 """
701 Get the documentation for a Pydantic Field.
703 Arguments:
704 node: The node representing the Field and its parents.
706 Returns:
707 The documented attribute object.
708 """
709 prop = node.obj
710 path = node.dotted_path
711 properties = ["pydantic-field"]
712 if prop.required:
713 properties.append("required")
715 return Attribute(
716 name=node.name,
717 path=path,
718 file_path=node.file_path,
719 docstring=prop.field_info.description,
720 attr_type=prop.outer_type_,
721 properties=properties,
722 )
724 @staticmethod
725 def get_django_field_documentation(node: ObjectNode) -> Attribute:
726 """
727 Get the documentation for a Django Field.
729 Arguments:
730 node: The node representing the Field and its parents.
732 Returns:
733 The documented attribute object.
734 """
735 prop = node.obj
736 path = node.dotted_path
737 properties = ["django-field"]
739 if prop.null: 739 ↛ 740line 739 didn't jump to line 740, because the condition on line 739 was never true
740 properties.append("nullable")
741 if prop.blank: 741 ↛ 742line 741 didn't jump to line 742, because the condition on line 741 was never true
742 properties.append("blank")
744 # set correct docstring based on verbose_name and help_text
745 # both should be converted to str type in case lazy translation
746 # is being used, which is common scenario in django
747 if prop.help_text: 747 ↛ 748line 747 didn't jump to line 748, because the condition on line 747 was never true
748 docstring = f"{prop.verbose_name}: {prop.help_text}"
749 else:
750 docstring = str(prop.verbose_name)
752 return Attribute(
753 name=node.name,
754 path=path,
755 file_path=node.file_path,
756 docstring=docstring,
757 attr_type=prop.__class__,
758 properties=properties,
759 )
761 @staticmethod
762 def get_marshmallow_field_documentation(node: ObjectNode) -> Attribute:
763 """
764 Get the documentation for a Marshmallow Field.
766 Arguments:
767 node: The node representing the Field and its parents.
769 Returns:
770 The documented attribute object.
771 """
772 prop = node.obj
773 path = node.dotted_path
774 properties = ["marshmallow-field"]
775 if prop.required:
776 properties.append("required")
778 return Attribute(
779 name=node.name,
780 path=path,
781 file_path=node.file_path,
782 docstring=prop.metadata.get("description"),
783 attr_type=type(prop),
784 properties=properties,
785 )
787 @staticmethod
788 def get_annotated_dataclass_field(node: ObjectNode, attribute_data: Optional[dict] = None) -> Attribute:
789 """
790 Get the documentation for a dataclass field.
792 Arguments:
793 node: The node representing the annotation and its parents.
794 attribute_data: Docstring and annotation for this attribute.
796 Returns:
797 The documented attribute object.
798 """
799 if attribute_data is None: 799 ↛ 805line 799 didn't jump to line 805, because the condition on line 799 was never false
800 if node.parent_is_class(): 800 ↛ 803line 800 didn't jump to line 803, because the condition on line 800 was never false
801 attribute_data = get_class_attributes(node.parent.obj).get(node.name, {}) # type: ignore
802 else:
803 attribute_data = get_module_attributes(node.root.obj).get(node.name, {})
805 return Attribute(
806 name=node.name,
807 path=node.dotted_path,
808 file_path=node.file_path,
809 docstring=attribute_data["docstring"],
810 attr_type=attribute_data["annotation"],
811 properties=["dataclass-field"],
812 )
814 def get_classmethod_documentation(self, node: ObjectNode) -> Method:
815 """
816 Get the documentation for a class-method.
818 Arguments:
819 node: The node representing the class-method and its parents.
821 Returns:
822 The documented method object.
823 """
824 return self.get_method_documentation(node, ["classmethod"])
826 def get_staticmethod_documentation(self, node: ObjectNode) -> Method:
827 """
828 Get the documentation for a static-method.
830 Arguments:
831 node: The node representing the static-method and its parents.
833 Returns:
834 The documented method object.
835 """
836 return self.get_method_documentation(node, ["staticmethod"])
838 def get_regular_method_documentation(self, node: ObjectNode) -> Method:
839 """
840 Get the documentation for a regular method (not class- nor static-method).
842 We do extra processing in this method to discard docstrings of `__init__` methods
843 that were inherited from parent classes.
845 Arguments:
846 node: The node representing the method and its parents.
848 Returns:
849 The documented method object.
850 """
851 method = self.get_method_documentation(node)
852 if node.parent: 852 ↛ 866line 852 didn't jump to line 866, because the condition on line 852 was never false
853 class_ = node.parent.obj
854 if RE_SPECIAL.match(node.name):
855 docstring = method.docstring
856 parent_classes = class_.__mro__[1:]
857 for parent_class in parent_classes:
858 try:
859 parent_method = getattr(parent_class, node.name)
860 except AttributeError:
861 continue
862 else:
863 if docstring == inspect.getdoc(parent_method):
864 method.docstring = ""
865 break
866 return method
868 def get_method_documentation(self, node: ObjectNode, properties: Optional[List[str]] = None) -> Method:
869 """
870 Get the documentation for a method or method descriptor.
872 Arguments:
873 node: The node representing the method and its parents.
874 properties: A list of properties to apply to the method.
876 Returns:
877 The documented method object.
878 """
879 method = node.obj
880 path = node.dotted_path
881 signature: Optional[inspect.Signature]
882 source: Optional[Source]
884 try:
885 source = Source(*inspect.getsourcelines(method))
886 except OSError as error:
887 source = None
888 except TypeError:
889 source = None
891 if node.is_coroutine_function():
892 if properties is None: 892 ↛ 895line 892 didn't jump to line 895, because the condition on line 892 was never false
893 properties = ["async"]
894 else:
895 properties.append("async")
897 try:
898 # for "built-in" functions, e.g. those implemented in C,
899 # inspect.signature() uses the __text_signature__ attribute, which
900 # provides a limited but still useful amount of signature information.
901 # "built-in" functions with no __text_signature__ will
902 # raise a ValueError().
903 signature = inspect.signature(method)
904 except ValueError as error:
905 signature = None
907 return Method(
908 name=node.name,
909 path=path,
910 file_path=node.file_path,
911 docstring=inspect.getdoc(method),
912 signature=signature,
913 properties=properties or [],
914 source=source,
915 )
917 @staticmethod
918 def get_attribute_documentation(node: ObjectNode, attribute_data: Optional[dict] = None) -> Attribute:
919 """
920 Get the documentation for an attribute.
922 Arguments:
923 node: The node representing the method and its parents.
924 attribute_data: Docstring and annotation for this attribute.
926 Returns:
927 The documented attribute object.
928 """
929 if attribute_data is None:
930 if node.parent_is_class():
931 attribute_data = get_class_attributes(node.parent.obj).get(node.name, {}) # type: ignore
932 else:
933 attribute_data = get_module_attributes(node.root.obj).get(node.name, {})
934 return Attribute(
935 name=node.name,
936 path=node.dotted_path,
937 file_path=node.file_path,
938 docstring=attribute_data.get("docstring", ""),
939 attr_type=attribute_data.get("annotation", None),
940 )
942 def select(self, name: str, names: Set[str]) -> bool:
943 """
944 Tells whether we should select an object or not, given its name.
946 If the set of names is not empty, we check against it, otherwise we check against filters.
948 Arguments:
949 name: The name of the object to select or not.
950 names: An explicit list of names to select.
952 Returns:
953 Yes or no.
954 """
955 if names:
956 return name in names
957 return not self.filter_name_out(name)
959 @lru_cache(maxsize=None)
960 def filter_name_out(self, name: str) -> bool:
961 """
962 Filter a name based on the loader's filters.
964 Arguments:
965 name: The name to filter.
967 Returns:
968 True if the name was filtered out, False otherwise.
969 """
970 if not self.filters:
971 return False
972 keep = True
973 for fltr, regex in self.filters:
974 is_matching = bool(regex.search(name))
975 if is_matching:
976 if str(fltr).startswith("!"):
977 is_matching = not is_matching
978 keep = is_matching
979 return not keep
982def field_is_inherited(field_name: str, fields_name: str, base_class: type) -> bool:
983 """
984 Check if a field with a certain name was inherited from parent classes.
986 Arguments:
987 field_name: The name of the field to check.
988 fields_name: The name of the attribute in which the fields are stored.
989 base_class: The base class in which the field appears.
991 Returns:
992 Whether the field was inherited.
993 """
994 # To tell if a field was inherited, we check if it exists in parent classes __fields__ attributes.
995 # We don't check the current class, nor the top one (object), hence __mro__[1:-1]
996 return field_name in set(
997 chain(
998 *(getattr(parent_class, fields_name, {}).keys() for parent_class in base_class.__mro__[1:-1]),
999 ),
1000 )
1003def split_attr_name(attr_name: str) -> Tuple[str, Optional[str]]:
1004 """
1005 Split an attribute name into a first-order attribute name and remainder.
1007 Args:
1008 attr_name: Attribute name (a)
1010 Returns:
1011 Tuple containing:
1012 first_order_attr_name: Name of the first order attribute (a)
1013 remainder: The remainder (b.c)
1015 """
1016 first_order_attr_name, *remaining = attr_name.split(".", maxsplit=1)
1017 remainder = remaining[0] if remaining else None
1018 return first_order_attr_name, remainder
1021def get_fields(attr_name: str, *, members: Mapping[str, Any] = None, class_obj=None) -> Dict[str, Any]:
1022 if not (bool(members) ^ bool(class_obj)): 1022 ↛ 1023line 1022 didn't jump to line 1023, because the condition on line 1022 was never true
1023 raise ValueError("Either members or class_obj is required.")
1024 first_order_attr_name, remainder = split_attr_name(attr_name)
1025 fields = members[first_order_attr_name] if members else dict(vars(class_obj)).get(first_order_attr_name, {})
1026 if remainder:
1027 fields = attrgetter(remainder)(fields)
1029 if callable(fields):
1030 fields = fields()
1032 if not isinstance(fields, dict):
1033 # Support Django models
1034 fields = {getattr(f, "name", str(f)): f for f in fields if not getattr(f, "auto_created", False)}
1036 return fields