Coverage for src/_griffe/agents/nodes/runtime.py: 89.55%
168 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-08-15 16:47 +0200
« prev ^ index » next coverage.py v7.6.1, created at 2024-08-15 16:47 +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 Any, ClassVar, Sequence
10from _griffe.enumerations import ObjectKind
11from _griffe.logger import logger
13_builtin_module_names = {_.lstrip("_") for _ in sys.builtin_module_names}
14_cyclic_relationships = {
15 ("os", "nt"),
16 ("os", "posix"),
17 ("numpy.core._multiarray_umath", "numpy.core.multiarray"),
18}
21def _same_components(a: str, b: str, /) -> bool:
22 # YORE: EOL 3.8: Replace `lstrip` with `removeprefix` within line.
23 return [cpn.lstrip("_") for cpn in a.split(".")] == [cpn.lstrip("_") for cpn in b.split(".")]
26class ObjectNode:
27 """Helper class to represent an object tree.
29 It's not really a tree but more a backward-linked list:
30 each node has a reference to its parent, but not to its child (for simplicity purposes and to avoid bugs).
32 Each node stores an object, its name, and a reference to its parent node.
33 """
35 exclude_specials: ClassVar[set[str]] = {"__builtins__", "__loader__", "__spec__"}
36 """Low level attributes known to cause issues when resolving aliases."""
38 def __init__(self, obj: Any, name: str, parent: ObjectNode | None = None) -> None:
39 """Initialize the object.
41 Parameters:
42 obj: A Python object.
43 name: The object's name.
44 parent: The object's parent node.
45 """
46 # Unwrap object.
47 try:
48 obj = inspect.unwrap(obj)
49 except Exception as error: # noqa: BLE001
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 logger.debug(f"Could not unwrap {name}: {error!r}")
56 # Unwrap cached properties (`inspect.unwrap` doesn't do that).
57 if isinstance(obj, cached_property):
58 is_cached_property = True
59 obj = obj.func
60 else:
61 is_cached_property = False
63 self.obj: Any = obj
64 """The actual Python object."""
65 self.name: str = name
66 """The Python object's name."""
67 self.parent: ObjectNode | None = parent
68 """The parent node."""
69 self.is_cached_property: bool = is_cached_property
70 """Whether this node's object is a cached property."""
72 def __repr__(self) -> str:
73 return f"ObjectNode(name={self.name!r})"
75 @property
76 def path(self) -> str:
77 """The object's (Python) path."""
78 if self.parent is None:
79 return self.name
80 return f"{self.parent.path}.{self.name}"
82 @property
83 def module(self) -> ObjectNode:
84 """The object's module, fetched from the node tree."""
85 if self.is_module:
86 return self
87 if self.parent is not None: 87 ↛ 89line 87 didn't jump to line 89 because the condition on line 87 was always true
88 return self.parent.module
89 raise ValueError(f"Object node {self.path} does not have a parent module")
91 @property
92 def module_path(self) -> str | None:
93 """The object's module path."""
94 try:
95 return self.obj.__module__
96 except AttributeError:
97 try:
98 module = inspect.getmodule(self.obj) or self.module.obj
99 except ValueError:
100 return None
101 try:
102 return module.__spec__.name # type: ignore[union-attr]
103 except AttributeError:
104 return getattr(module, "__name__", None)
106 @property
107 def kind(self) -> ObjectKind:
108 """The kind of this node."""
109 if self.is_module:
110 return ObjectKind.MODULE
111 if self.is_class:
112 return ObjectKind.CLASS
113 if self.is_staticmethod:
114 return ObjectKind.STATICMETHOD
115 if self.is_classmethod: 115 ↛ 116line 115 didn't jump to line 116 because the condition on line 115 was never true
116 return ObjectKind.CLASSMETHOD
117 if self.is_cached_property:
118 return ObjectKind.CACHED_PROPERTY
119 if self.is_method:
120 return ObjectKind.METHOD
121 if self.is_builtin_method:
122 return ObjectKind.BUILTIN_METHOD
123 if self.is_coroutine: 123 ↛ 124line 123 didn't jump to line 124 because the condition on line 123 was never true
124 return ObjectKind.COROUTINE
125 if self.is_builtin_function:
126 return ObjectKind.BUILTIN_FUNCTION
127 if self.is_method_descriptor:
128 return ObjectKind.METHOD_DESCRIPTOR
129 if self.is_function:
130 return ObjectKind.FUNCTION
131 if self.is_property:
132 return ObjectKind.PROPERTY
133 return ObjectKind.ATTRIBUTE
135 @cached_property
136 def children(self) -> Sequence[ObjectNode]:
137 """The children of this node."""
138 children = []
139 for name, member in inspect.getmembers(self.obj):
140 if self._pick_member(name, member):
141 children.append(ObjectNode(member, name, parent=self))
142 return children
144 @cached_property
145 def is_module(self) -> bool:
146 """Whether this node's object is a module."""
147 return inspect.ismodule(self.obj)
149 @cached_property
150 def is_class(self) -> bool:
151 """Whether this node's object is a class."""
152 return inspect.isclass(self.obj)
154 @cached_property
155 def is_function(self) -> bool:
156 """Whether this node's object is a function."""
157 # `inspect.isfunction` returns `False` for partials.
158 return inspect.isfunction(self.obj) or (callable(self.obj) and not self.is_class)
160 @cached_property
161 def is_builtin_function(self) -> bool:
162 """Whether this node's object is a builtin function."""
163 return inspect.isbuiltin(self.obj)
165 @cached_property
166 def is_coroutine(self) -> bool:
167 """Whether this node's object is a coroutine."""
168 return inspect.iscoroutinefunction(self.obj)
170 @cached_property
171 def is_property(self) -> bool:
172 """Whether this node's object is a property."""
173 return isinstance(self.obj, property) or self.is_cached_property
175 @cached_property
176 def parent_is_class(self) -> bool:
177 """Whether the object of this node's parent is a class."""
178 return bool(self.parent and self.parent.is_class)
180 @cached_property
181 def is_method(self) -> bool:
182 """Whether this node's object is a method."""
183 function_type = type(lambda: None) 183 ↛ exitline 183 didn't run the lambda on line 183
184 return self.parent_is_class and isinstance(self.obj, function_type)
186 @cached_property
187 def is_method_descriptor(self) -> bool:
188 """Whether this node's object is a method descriptor.
190 Built-in methods (e.g. those implemented in C/Rust) are often
191 method descriptors, rather than normal methods.
192 """
193 return inspect.ismethoddescriptor(self.obj)
195 @cached_property
196 def is_builtin_method(self) -> bool:
197 """Whether this node's object is a builtin method."""
198 return self.is_builtin_function and self.parent_is_class
200 @cached_property
201 def is_staticmethod(self) -> bool:
202 """Whether this node's object is a staticmethod."""
203 if self.parent is None: 203 ↛ 204line 203 didn't jump to line 204 because the condition on line 203 was never true
204 return False
205 try:
206 self_from_parent = self.parent.obj.__dict__.get(self.name, None)
207 except AttributeError:
208 return False
209 return self.parent_is_class and isinstance(self_from_parent, staticmethod)
211 @cached_property
212 def is_classmethod(self) -> bool:
213 """Whether this node's object is a classmethod."""
214 if self.parent is None: 214 ↛ 215line 214 didn't jump to line 215 because the condition on line 214 was never true
215 return False
216 try:
217 self_from_parent = self.parent.obj.__dict__.get(self.name, None)
218 except AttributeError:
219 return False
220 return self.parent_is_class and isinstance(self_from_parent, classmethod)
222 @cached_property
223 def _ids(self) -> set[int]:
224 if self.parent is None:
225 return {id(self.obj)}
226 return {id(self.obj)} | self.parent._ids
228 def _pick_member(self, name: str, member: Any) -> bool:
229 return (
230 name not in self.exclude_specials
231 and member is not type
232 and member is not object
233 and id(member) not in self._ids
234 and name in vars(self.obj)
235 )
237 @cached_property
238 def alias_target_path(self) -> str | None:
239 """Alias target path of this node, if the node should be an alias."""
240 if self.parent is None: 240 ↛ 241line 240 didn't jump to line 241 because the condition on line 240 was never true
241 return None
243 # Get the path of the module the child was declared in.
244 child_module_path = self.module_path
245 if not child_module_path:
246 return None
248 # Get the module the parent object was declared in.
249 parent_module_path = self.parent.module_path
250 if not parent_module_path: 250 ↛ 251line 250 didn't jump to line 251 because the condition on line 250 was never true
251 return None
253 # Special cases: break cycles.
254 if (parent_module_path, child_module_path) in _cyclic_relationships: 254 ↛ 255line 254 didn't jump to line 255 because the condition on line 254 was never true
255 return None
257 # If the current object was declared in the same module as its parent,
258 # or in a module with the same path components but starting/not starting with underscores,
259 # we don't want to alias it. Examples: (a, a), (a, _a), (_a, a), (_a, _a),
260 # (a.b, a.b), (a.b, _a.b), (_a._b, a.b), (a._b, _a.b), etc..
261 if _same_components(parent_module_path, child_module_path):
262 return None
264 # If the current object was declared in any other module, we alias it.
265 # We remove the leading underscore from the child module path
266 # if it's a built-in module (e.g. _io -> io). That's because objects
267 # in built-in modules inconsistently lie about their module path,
268 # so we prefer to use the non-underscored (public) version,
269 # as users most likely import from the public module and not the private one.
270 if child_module_path.lstrip("_") in _builtin_module_names:
271 child_module_path = child_module_path.lstrip("_")
272 if self.is_module:
273 return child_module_path
274 child_name = getattr(self.obj, "__qualname__", self.path[len(self.module.path) + 1 :])
275 return f"{child_module_path}.{child_name}"