Coverage for src/_griffe/agents/nodes/runtime.py: 87.95%
168 statements
« prev ^ index » next coverage.py v7.6.2, created at 2024-10-12 01:34 +0200
« prev ^ index » next coverage.py v7.6.2, created at 2024-10-12 01:34 +0200
1# This module contains utilities for extracting information from runtime objects.
3from __future__ import annotations
5import inspect
6import sys
7from functools import cached_property
8from typing import TYPE_CHECKING, Any, ClassVar
10from _griffe.enumerations import ObjectKind
11from _griffe.logger import logger
13if TYPE_CHECKING:
14 from collections.abc import Sequence
16_builtin_module_names = {_.lstrip("_") for _ in sys.builtin_module_names}
17_cyclic_relationships = {
18 ("os", "nt"),
19 ("os", "posix"),
20 ("numpy.core._multiarray_umath", "numpy.core.multiarray"),
21}
24def _same_components(a: str, b: str, /) -> bool:
25 return [cpn.lstrip("_") for cpn in a.split(".")] == [cpn.lstrip("_") for cpn in b.split(".")]
28class ObjectNode:
29 """Helper class to represent an object tree.
31 It's not really a tree but more a backward-linked list:
32 each node has a reference to its parent, but not to its child (for simplicity purposes and to avoid bugs).
34 Each node stores an object, its name, and a reference to its parent node.
35 """
37 exclude_specials: ClassVar[set[str]] = {"__builtins__", "__loader__", "__spec__"}
38 """Low level attributes known to cause issues when resolving aliases."""
40 def __init__(self, obj: Any, name: str, parent: ObjectNode | None = None) -> None:
41 """Initialize the object.
43 Parameters:
44 obj: A Python object.
45 name: The object's name.
46 parent: The object's parent node.
47 """
48 # Unwrap object.
49 try:
50 obj = inspect.unwrap(obj)
51 except Exception as error: # noqa: BLE001
52 # inspect.unwrap at some point runs hasattr(obj, "__wrapped__"),
53 # which triggers the __getattr__ method of the object, which in
54 # turn can raise various exceptions. Probably not just __getattr__.
55 # See https://github.com/pawamoy/pytkdocs/issues/45
56 logger.debug("Could not unwrap %s: %r", name, error)
58 # Unwrap cached properties (`inspect.unwrap` doesn't do that).
59 if isinstance(obj, cached_property):
60 is_cached_property = True
61 obj = obj.func
62 else:
63 is_cached_property = False
65 self.obj: Any = obj
66 """The actual Python object."""
67 self.name: str = name
68 """The Python object's name."""
69 self.parent: ObjectNode | None = parent
70 """The parent node."""
71 self.is_cached_property: bool = is_cached_property
72 """Whether this node's object is a cached property."""
74 def __repr__(self) -> str:
75 return f"ObjectNode(name={self.name!r})"
77 @property
78 def path(self) -> str:
79 """The object's (Python) path."""
80 if self.parent is None:
81 return self.name
82 return f"{self.parent.path}.{self.name}"
84 @property
85 def module(self) -> ObjectNode:
86 """The object's module, fetched from the node tree."""
87 if self.is_module:
88 return self
89 if self.parent is not None: 89 ↛ 91line 89 didn't jump to line 91 because the condition on line 89 was always true
90 return self.parent.module
91 raise ValueError(f"Object node {self.path} does not have a parent module")
93 @property
94 def module_path(self) -> str | None:
95 """The object's module path."""
96 try:
97 return self.obj.__module__
98 except AttributeError:
99 try:
100 module = inspect.getmodule(self.obj) or self.module.obj
101 except ValueError:
102 return None
103 try:
104 return module.__spec__.name # type: ignore[union-attr]
105 except AttributeError:
106 return getattr(module, "__name__", None)
108 @property
109 def kind(self) -> ObjectKind:
110 """The kind of this node."""
111 if self.is_module:
112 return ObjectKind.MODULE
113 if self.is_class:
114 return ObjectKind.CLASS
115 if self.is_staticmethod:
116 return ObjectKind.STATICMETHOD
117 if self.is_classmethod: 117 ↛ 118line 117 didn't jump to line 118 because the condition on line 117 was never true
118 return ObjectKind.CLASSMETHOD
119 if self.is_cached_property:
120 return ObjectKind.CACHED_PROPERTY
121 if self.is_method:
122 return ObjectKind.METHOD
123 if self.is_builtin_method:
124 return ObjectKind.BUILTIN_METHOD
125 if self.is_coroutine: 125 ↛ 126line 125 didn't jump to line 126 because the condition on line 125 was never true
126 return ObjectKind.COROUTINE
127 if self.is_builtin_function:
128 return ObjectKind.BUILTIN_FUNCTION
129 if self.is_method_descriptor:
130 return ObjectKind.METHOD_DESCRIPTOR
131 if self.is_function:
132 return ObjectKind.FUNCTION
133 if self.is_property:
134 return ObjectKind.PROPERTY
135 return ObjectKind.ATTRIBUTE
137 @cached_property
138 def children(self) -> Sequence[ObjectNode]:
139 """The children of this node."""
140 children = []
141 for name, member in inspect.getmembers(self.obj):
142 if self._pick_member(name, member):
143 children.append(ObjectNode(member, name, parent=self))
144 return children
146 @cached_property
147 def is_module(self) -> bool:
148 """Whether this node's object is a module."""
149 return inspect.ismodule(self.obj)
151 @cached_property
152 def is_class(self) -> bool:
153 """Whether this node's object is a class."""
154 return inspect.isclass(self.obj)
156 @cached_property
157 def is_function(self) -> bool:
158 """Whether this node's object is a function."""
159 # `inspect.isfunction` returns `False` for partials.
160 return inspect.isfunction(self.obj) or (callable(self.obj) and not self.is_class)
162 @cached_property
163 def is_builtin_function(self) -> bool:
164 """Whether this node's object is a builtin function."""
165 return inspect.isbuiltin(self.obj)
167 @cached_property
168 def is_coroutine(self) -> bool:
169 """Whether this node's object is a coroutine."""
170 return inspect.iscoroutinefunction(self.obj)
172 @cached_property
173 def is_property(self) -> bool:
174 """Whether this node's object is a property."""
175 return isinstance(self.obj, property) or self.is_cached_property
177 @cached_property
178 def parent_is_class(self) -> bool:
179 """Whether the object of this node's parent is a class."""
180 return bool(self.parent and self.parent.is_class)
182 @cached_property
183 def is_method(self) -> bool:
184 """Whether this node's object is a method."""
185 function_type = type(lambda: None)
186 return self.parent_is_class and isinstance(self.obj, function_type)
188 @cached_property
189 def is_method_descriptor(self) -> bool:
190 """Whether this node's object is a method descriptor.
192 Built-in methods (e.g. those implemented in C/Rust) are often
193 method descriptors, rather than normal methods.
194 """
195 return inspect.ismethoddescriptor(self.obj)
197 @cached_property
198 def is_builtin_method(self) -> bool:
199 """Whether this node's object is a builtin method."""
200 return self.is_builtin_function and self.parent_is_class
202 @cached_property
203 def is_staticmethod(self) -> bool:
204 """Whether this node's object is a staticmethod."""
205 if self.parent is None: 205 ↛ 206line 205 didn't jump to line 206 because the condition on line 205 was never true
206 return False
207 try:
208 self_from_parent = self.parent.obj.__dict__.get(self.name, None)
209 except AttributeError:
210 return False
211 return self.parent_is_class and isinstance(self_from_parent, staticmethod)
213 @cached_property
214 def is_classmethod(self) -> bool:
215 """Whether this node's object is a classmethod."""
216 if self.parent is None: 216 ↛ 217line 216 didn't jump to line 217 because the condition on line 216 was never true
217 return False
218 try:
219 self_from_parent = self.parent.obj.__dict__.get(self.name, None)
220 except AttributeError:
221 return False
222 return self.parent_is_class and isinstance(self_from_parent, classmethod)
224 @cached_property
225 def _ids(self) -> set[int]:
226 if self.parent is None:
227 return {id(self.obj)}
228 return {id(self.obj)} | self.parent._ids
230 def _pick_member(self, name: str, member: Any) -> bool:
231 return (
232 name not in self.exclude_specials
233 and member is not type
234 and member is not object
235 and id(member) not in self._ids
236 and name in vars(self.obj)
237 )
239 @cached_property
240 def alias_target_path(self) -> str | None:
241 """Alias target path of this node, if the node should be an alias."""
242 if self.parent is None: 242 ↛ 243line 242 didn't jump to line 243 because the condition on line 242 was never true
243 return None
245 # Get the path of the module the child was declared in.
246 child_module_path = self.module_path
247 if not child_module_path:
248 return None
250 # Get the module the parent object was declared in.
251 parent_module_path = self.parent.module_path
252 if not parent_module_path: 252 ↛ 253line 252 didn't jump to line 253 because the condition on line 252 was never true
253 return None
255 # Special cases: break cycles.
256 if (parent_module_path, child_module_path) in _cyclic_relationships: 256 ↛ 257line 256 didn't jump to line 257 because the condition on line 256 was never true
257 return None
259 # If the current object was declared in the same module as its parent,
260 # or in a module with the same path components but starting/not starting with underscores,
261 # we don't want to alias it. Examples: (a, a), (a, _a), (_a, a), (_a, _a),
262 # (a.b, a.b), (a.b, _a.b), (_a._b, a.b), (a._b, _a.b), etc..
263 if _same_components(parent_module_path, child_module_path):
264 return None
266 # If the current object was declared in any other module, we alias it.
267 # We remove the leading underscore from the child module path
268 # if it's a built-in module (e.g. _io -> io). That's because objects
269 # in built-in modules inconsistently lie about their module path,
270 # so we prefer to use the non-underscored (public) version,
271 # as users most likely import from the public module and not the private one.
272 if child_module_path.lstrip("_") in _builtin_module_names:
273 child_module_path = child_module_path.lstrip("_")
274 if self.is_module:
275 return child_module_path
276 child_name = getattr(self.obj, "__qualname__", self.path[len(self.module.path) + 1 :])
277 return f"{child_module_path}.{child_name}"