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

1# This module contains utilities for extracting information from runtime objects. 

2 

3from __future__ import annotations 

4 

5import inspect 

6import sys 

7import typing 

8from functools import cached_property 

9from types import GetSetDescriptorType, ModuleType 

10from typing import TYPE_CHECKING, Any, ClassVar 

11 

12from griffe._internal.enumerations import ObjectKind 

13from griffe._internal.logger import logger 

14 

15if TYPE_CHECKING: 

16 import types 

17 from collections.abc import Sequence 

18 

19_TYPING_MODULES: tuple[types.ModuleType, ...] 

20try: 

21 import typing_extensions 

22except ImportError: 

23 _TYPING_MODULES = (typing,) 

24else: 

25 _TYPING_MODULES = (typing, typing_extensions) 

26 

27 

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} 

34 

35 

36def _same_components(a: str, b: str, /) -> bool: 

37 return [cpn.lstrip("_") for cpn in a.split(".")] == [cpn.lstrip("_") for cpn in b.split(".")] 

38 

39 

40class ObjectNode: 

41 """Helper class to represent an object tree. 

42 

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). 

45 

46 Each node stores an object, its name, and a reference to its parent node. 

47 """ 

48 

49 exclude_specials: ClassVar[set[str]] = {"__builtins__", "__loader__", "__spec__"} 

50 """Low level attributes known to cause issues when resolving aliases.""" 

51 

52 def __init__(self, obj: Any, name: str, parent: ObjectNode | None = None) -> None: 

53 """Initialize the object. 

54 

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) 

69 

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 

76 

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.""" 

85 

86 def __repr__(self) -> str: 

87 return f"ObjectNode(name={self.name!r})" 

88 

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}" 

95 

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") 

104 

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 

140 

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 

173 

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 

182 

183 @cached_property 

184 def is_module(self) -> bool: 

185 """Whether this node's object is a module.""" 

186 return inspect.ismodule(self.obj) 

187 

188 @cached_property 

189 def is_class(self) -> bool: 

190 """Whether this node's object is a class.""" 

191 return inspect.isclass(self.obj) 

192 

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) 

198 

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 ) 

206 

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) 

211 

212 @cached_property 

213 def is_coroutine(self) -> bool: 

214 """Whether this node's object is a coroutine.""" 

215 return inspect.iscoroutinefunction(self.obj) 

216 

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) 

221 

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 

226 

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) 

231 

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) 

237 

238 @cached_property 

239 def is_method_descriptor(self) -> bool: 

240 """Whether this node's object is a method descriptor. 

241 

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) 

246 

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 

251 

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) 

262 

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) 

273 

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 

278 

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 

284 

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 ) 

293 

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 

300 

301 # We can't ever know if an attribute was imported. 

302 if self.is_attribute: 

303 return None 

304 

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 

309 

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 

314 

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 

318 

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 

325 

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("_") 

334 

335 # Child object is a module, return its path directly. 

336 if self.is_module: 

337 return child_module_path 

338 

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}"