Coverage for packages / griffelib / src / griffe / _internal / agents / nodes / runtime.py: 87.88%
198 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-11 11:48 +0100
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-11 11:48 +0100
1# This module contains utilities for extracting information from runtime objects.
3from __future__ import annotations
5import inspect
6import sys
7import typing
8from functools import cached_property
9from types import GetSetDescriptorType, ModuleType
10from typing import TYPE_CHECKING, Any, ClassVar
12from griffe._internal.enumerations import ObjectKind
13from griffe._internal.logger import logger
15if TYPE_CHECKING:
16 import types
17 from collections.abc import Sequence
19_TYPING_MODULES: tuple[types.ModuleType, ...]
20try:
21 import typing_extensions
22except ImportError:
23 _TYPING_MODULES = (typing,)
24else:
25 _TYPING_MODULES = (typing, typing_extensions)
28_builtin_module_names = {_.lstrip("_") for _ in sys.builtin_module_names}
29_cyclic_relationships = {
30 ("os", "nt"),
31 ("os", "posix"),
32 ("numpy.core._multiarray_umath", "numpy.core.multiarray"),
33}
36def _same_components(a: str, b: str, /) -> bool:
37 return [cpn.lstrip("_") for cpn in a.split(".")] == [cpn.lstrip("_") for cpn in b.split(".")]
40class ObjectNode:
41 """Helper class to represent an object tree.
43 It's not really a tree but more a backward-linked list:
44 each node has a reference to its parent, but not to its child (for simplicity purposes and to avoid bugs).
46 Each node stores an object, its name, and a reference to its parent node.
47 """
49 exclude_specials: ClassVar[set[str]] = {"__builtins__", "__loader__", "__spec__"}
50 """Low level attributes known to cause issues when resolving aliases."""
52 def __init__(self, obj: Any, name: str, parent: ObjectNode | None = None) -> None:
53 """Initialize the object.
55 Parameters:
56 obj: A Python object.
57 name: The object's name.
58 parent: The object's parent node.
59 """
60 # Unwrap object.
61 try:
62 obj = inspect.unwrap(obj)
63 except Exception as error: # noqa: BLE001
64 # `inspect.unwrap` at some point runs `hasattr(obj, "__wrapped__")`,
65 # which triggers the `__getattr__` method of the object, which in
66 # turn can raise various exceptions. Probably not just `__getattr__`.
67 # See https://github.com/pawamoy/pytkdocs/issues/45.
68 logger.debug("Could not unwrap %s: %r", name, error)
70 # Unwrap cached properties (`inspect.unwrap` doesn't do that).
71 if isinstance(obj, cached_property):
72 is_cached_property = True
73 obj = obj.func
74 else:
75 is_cached_property = False
77 self.obj: Any = obj
78 """The actual Python object."""
79 self.name: str = name
80 """The Python object's name."""
81 self.parent: ObjectNode | None = parent
82 """The parent node."""
83 self.is_cached_property: bool = is_cached_property
84 """Whether this node's object is a cached property."""
86 def __repr__(self) -> str:
87 return f"ObjectNode(name={self.name!r})"
89 @property
90 def path(self) -> str:
91 """The object's (Python) path."""
92 if self.parent is None:
93 return self.name
94 return f"{self.parent.path}.{self.name}"
96 @property
97 def module(self) -> ObjectNode:
98 """The object's module, fetched from the node tree."""
99 if self.is_module:
100 return self
101 if self.parent is not None: 101 ↛ 103line 101 didn't jump to line 103 because the condition on line 101 was always true
102 return self.parent.module
103 raise ValueError(f"Object node {self.path} does not have a parent module")
105 @property
106 def module_path(self) -> str | None:
107 """The object's module path."""
108 try:
109 module = self.obj.__module__
110 except AttributeError:
111 try:
112 module = inspect.getmodule(self.obj) or self.module.obj
113 except ValueError:
114 return None
115 try:
116 return module.__spec__.name # ty:ignore[possibly-missing-attribute]
117 except AttributeError:
118 return getattr(module, "__name__", None)
119 else:
120 # Some libraries mistakenly set `__module__` to a module object instead of a string.
121 # Handle this case specifically, but do not cast any other object to a string.
122 if isinstance(module, ModuleType): 122 ↛ 123line 122 didn't jump to line 123 because the condition on line 122 was never true
123 logger.debug(
124 "Object %s has its `__module__` attribute set to a module object: "
125 "it must be a string instead (the module fully qualified name). "
126 "Please report to the maintainers of this library.",
127 self.path,
128 )
129 return getattr(module, "__qualname__", getattr(module, "__name__", None))
130 if isinstance(module, str):
131 return module
132 logger.debug(
133 "Object %s has its `__module__` attribute set to a %s object: "
134 "it must be a string instead (the module fully qualified name). "
135 "Please report to the maintainers of this library.",
136 self.path,
137 getattr(type(module), "__name__", str(type(module))),
138 )
139 return None
141 @property
142 def kind(self) -> ObjectKind:
143 """The kind of this node."""
144 if self.is_module:
145 return ObjectKind.MODULE
146 if self.is_class:
147 return ObjectKind.CLASS
148 if self.is_staticmethod:
149 return ObjectKind.STATICMETHOD
150 if self.is_classmethod: 150 ↛ 151line 150 didn't jump to line 151 because the condition on line 150 was never true
151 return ObjectKind.CLASSMETHOD
152 if self.is_cached_property:
153 return ObjectKind.CACHED_PROPERTY
154 if self.is_method:
155 return ObjectKind.METHOD
156 if self.is_builtin_method:
157 return ObjectKind.BUILTIN_METHOD
158 if self.is_coroutine: 158 ↛ 159line 158 didn't jump to line 159 because the condition on line 158 was never true
159 return ObjectKind.COROUTINE
160 if self.is_builtin_function:
161 return ObjectKind.BUILTIN_FUNCTION
162 if self.is_method_descriptor:
163 return ObjectKind.METHOD_DESCRIPTOR
164 if self.is_function:
165 return ObjectKind.FUNCTION
166 if self.is_getset_descriptor:
167 return ObjectKind.GETSET_DESCRIPTOR
168 if self.is_property:
169 return ObjectKind.PROPERTY
170 if self.is_type_alias:
171 return ObjectKind.TYPE_ALIAS
172 return ObjectKind.ATTRIBUTE
174 @cached_property
175 def children(self) -> Sequence[ObjectNode]:
176 """The children of this node."""
177 children = []
178 for name, member in inspect.getmembers(self.obj):
179 if self._pick_member(name, member):
180 children.append(ObjectNode(member, name, parent=self))
181 return children
183 @cached_property
184 def is_module(self) -> bool:
185 """Whether this node's object is a module."""
186 return inspect.ismodule(self.obj)
188 @cached_property
189 def is_class(self) -> bool:
190 """Whether this node's object is a class."""
191 return inspect.isclass(self.obj)
193 @cached_property
194 def is_function(self) -> bool:
195 """Whether this node's object is a function."""
196 # `inspect.isfunction` returns `False` for partials.
197 return inspect.isfunction(self.obj) or (callable(self.obj) and not self.is_class)
199 @cached_property
200 def is_type_alias(self) -> bool:
201 """Whether this node's object is a type alias."""
202 return isinstance(
203 self.obj,
204 tuple(module.TypeAliasType for module in _TYPING_MODULES if hasattr(module, "TypeAliasType")),
205 )
207 @cached_property
208 def is_builtin_function(self) -> bool:
209 """Whether this node's object is a builtin function."""
210 return inspect.isbuiltin(self.obj)
212 @cached_property
213 def is_coroutine(self) -> bool:
214 """Whether this node's object is a coroutine."""
215 return inspect.iscoroutinefunction(self.obj)
217 @cached_property
218 def is_getset_descriptor(self) -> bool:
219 """Whether this node's object is a get/set descriptor."""
220 return isinstance(self.obj, GetSetDescriptorType)
222 @cached_property
223 def is_property(self) -> bool:
224 """Whether this node's object is a property."""
225 return isinstance(self.obj, property) or self.is_cached_property
227 @cached_property
228 def parent_is_class(self) -> bool:
229 """Whether the object of this node's parent is a class."""
230 return bool(self.parent and self.parent.is_class)
232 @cached_property
233 def is_method(self) -> bool:
234 """Whether this node's object is a method."""
235 function_type = type(lambda: None)
236 return self.parent_is_class and isinstance(self.obj, function_type)
238 @cached_property
239 def is_method_descriptor(self) -> bool:
240 """Whether this node's object is a method descriptor.
242 Built-in methods (e.g. those implemented in C/Rust) are often
243 method descriptors, rather than normal methods.
244 """
245 return inspect.ismethoddescriptor(self.obj)
247 @cached_property
248 def is_builtin_method(self) -> bool:
249 """Whether this node's object is a builtin method."""
250 return self.is_builtin_function and self.parent_is_class
252 @cached_property
253 def is_staticmethod(self) -> bool:
254 """Whether this node's object is a staticmethod."""
255 if self.parent is None: 255 ↛ 256line 255 didn't jump to line 256 because the condition on line 255 was never true
256 return False
257 try:
258 self_from_parent = self.parent.obj.__dict__.get(self.name, None)
259 except AttributeError:
260 return False
261 return self.parent_is_class and isinstance(self_from_parent, staticmethod)
263 @cached_property
264 def is_classmethod(self) -> bool:
265 """Whether this node's object is a classmethod."""
266 if self.parent is None: 266 ↛ 267line 266 didn't jump to line 267 because the condition on line 266 was never true
267 return False
268 try:
269 self_from_parent = self.parent.obj.__dict__.get(self.name, None)
270 except AttributeError:
271 return False
272 return self.parent_is_class and isinstance(self_from_parent, classmethod)
274 @cached_property
275 def is_attribute(self) -> bool:
276 """Whether this node's object is an attribute."""
277 return self.kind is ObjectKind.ATTRIBUTE
279 @cached_property
280 def _ids(self) -> set[int]:
281 if self.parent is None:
282 return {id(self.obj)}
283 return {id(self.obj)} | self.parent._ids
285 def _pick_member(self, name: str, member: Any) -> bool:
286 return (
287 name not in self.exclude_specials
288 and member is not type
289 and member is not object
290 and id(member) not in self._ids
291 and name in vars(self.obj)
292 )
294 @cached_property
295 def alias_target_path(self) -> str | None:
296 """Alias target path of this node, if the node should be an alias."""
297 # Top-level objects can't have been imported.
298 if self.parent is None: 298 ↛ 299line 298 didn't jump to line 299 because the condition on line 298 was never true
299 return None
301 # We can't ever know if an attribute was imported.
302 if self.is_attribute:
303 return None
305 # Get the path of the module the child object was declared in.
306 child_module_path = self.module_path
307 if not child_module_path:
308 return None
310 # Get the path of the module the parent object was declared in.
311 parent_module_path = self.parent.module_path
312 if not parent_module_path: 312 ↛ 313line 312 didn't jump to line 313 because the condition on line 312 was never true
313 return None
315 # Special cases: break cycles.
316 if (parent_module_path, child_module_path) in _cyclic_relationships: 316 ↛ 317line 316 didn't jump to line 317 because the condition on line 316 was never true
317 return None
319 # If the current object was declared in the same module as its parent,
320 # or in a module with the same path components but starting/not starting with underscores,
321 # we don't want to alias it. Examples: (a, a), (a, _a), (_a, a), (_a, _a),
322 # (a.b, a.b), (a.b, _a.b), (_a._b, a.b), (a._b, _a.b), etc..
323 if _same_components(parent_module_path, child_module_path):
324 return None
326 # If the current object was declared in any other module, we alias it.
327 # We remove the leading underscore from the child module path
328 # if it's a built-in module (e.g. _io -> io). That's because objects
329 # in built-in modules inconsistently lie about their module path,
330 # so we prefer to use the non-underscored (public) version,
331 # as users most likely import from the public module and not the private one.
332 if child_module_path.lstrip("_") in _builtin_module_names:
333 child_module_path = child_module_path.lstrip("_")
335 # Child object is a module, return its path directly.
336 if self.is_module:
337 return child_module_path
339 # Rebuild the child object path.
340 child_name = getattr(self.obj, "__qualname__", self.path[len(self.module.path) + 1 :])
341 return f"{child_module_path}.{child_name}"