Coverage for packages / griffelib / src / griffe / _internal / mixins.py: 85.64%

150 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-02-11 11:48 +0100

1# This module contains some mixins classes that hold shared methods 

2# of the different kinds of objects, and aliases. 

3 

4from __future__ import annotations 

5 

6import json 

7from contextlib import suppress 

8from typing import TYPE_CHECKING, Any, TypeVar 

9 

10from griffe._internal.enumerations import Kind 

11from griffe._internal.exceptions import AliasResolutionError, BuiltinModuleError, CyclicAliasError 

12from griffe._internal.merger import merge_stubs 

13 

14if TYPE_CHECKING: 

15 from collections.abc import Sequence 

16 

17 from griffe._internal.models import Alias, Attribute, Class, Function, Module, Object, TypeAlias 

18 

19_ObjType = TypeVar("_ObjType") 

20 

21 

22def _get_parts(key: str | Sequence[str]) -> Sequence[str]: 

23 if isinstance(key, str): 

24 if not key: 24 ↛ 25line 24 didn't jump to line 25 because the condition on line 24 was never true

25 raise ValueError("Empty strings are not supported") 

26 parts = key.split(".") 

27 else: 

28 parts = list(key) 

29 if not parts: 29 ↛ 30line 29 didn't jump to line 30 because the condition on line 29 was never true

30 raise ValueError("Empty tuples are not supported") 

31 return parts 

32 

33 

34class GetMembersMixin: 

35 """Mixin class to share methods for accessing members.""" 

36 

37 def __getitem__(self, key: str | Sequence[str]) -> Any: 

38 """Get a member with its name or path. 

39 

40 This method is part of the consumer API: 

41 do not use when producing Griffe trees! 

42 

43 Members will be looked up in both declared members and inherited ones, 

44 triggering computation of the latter. 

45 

46 Parameters: 

47 key: The name or path of the member. 

48 

49 Examples: 

50 >>> foo = griffe_object["foo"] 

51 >>> bar = griffe_object["path.to.bar"] 

52 >>> qux = griffe_object[("path", "to", "qux")] 

53 """ 

54 parts = _get_parts(key) 

55 if len(parts) == 1: 

56 return self.all_members[parts[0]] # ty:ignore[unresolved-attribute] 

57 return self.all_members[parts[0]][parts[1:]] # ty:ignore[unresolved-attribute] 

58 

59 def get_member(self, key: str | Sequence[str]) -> Any: 

60 """Get a member with its name or path. 

61 

62 This method is part of the producer API: 

63 you can use it safely while building Griffe trees 

64 (for example in Griffe extensions). 

65 

66 Members will be looked up in declared members only, not inherited ones. 

67 

68 Parameters: 

69 key: The name or path of the member. 

70 

71 Examples: 

72 >>> foo = griffe_object["foo"] 

73 >>> bar = griffe_object["path.to.bar"] 

74 >>> bar = griffe_object[("path", "to", "bar")] 

75 """ 

76 parts = _get_parts(key) 

77 if len(parts) == 1: 

78 return self.members[parts[0]] # ty:ignore[unresolved-attribute] 

79 return self.members[parts[0]].get_member(parts[1:]) # ty:ignore[unresolved-attribute] 

80 

81 

82# FIXME: Are `aliases` in other objects correctly updated when we delete a member? 

83# Would weak references be useful there? 

84class DelMembersMixin: 

85 """Mixin class to share methods for deleting members.""" 

86 

87 def __delitem__(self, key: str | Sequence[str]) -> None: 

88 """Delete a member with its name or path. 

89 

90 This method is part of the consumer API: 

91 do not use when producing Griffe trees! 

92 

93 Members will be looked up in both declared members and inherited ones, 

94 triggering computation of the latter. 

95 

96 Parameters: 

97 key: The name or path of the member. 

98 

99 Examples: 

100 >>> del griffe_object["foo"] 

101 >>> del griffe_object["path.to.bar"] 

102 >>> del griffe_object[("path", "to", "qux")] 

103 """ 

104 parts = _get_parts(key) 

105 if len(parts) == 1: 

106 name = parts[0] 

107 try: 

108 del self.members[name] # ty:ignore[unresolved-attribute] 

109 except KeyError: 

110 del self.inherited_members[name] # ty:ignore[unresolved-attribute] 

111 else: 

112 del self.all_members[parts[0]][parts[1:]] # ty:ignore[unresolved-attribute] 

113 

114 def del_member(self, key: str | Sequence[str]) -> None: 

115 """Delete a member with its name or path. 

116 

117 This method is part of the producer API: 

118 you can use it safely while building Griffe trees 

119 (for example in Griffe extensions). 

120 

121 Members will be looked up in declared members only, not inherited ones. 

122 

123 Parameters: 

124 key: The name or path of the member. 

125 

126 Examples: 

127 >>> griffe_object.del_member("foo") 

128 >>> griffe_object.del_member("path.to.bar") 

129 >>> griffe_object.del_member(("path", "to", "qux")) 

130 """ 

131 parts = _get_parts(key) 

132 if len(parts) == 1: 132 ↛ 136line 132 didn't jump to line 136 because the condition on line 132 was always true

133 name = parts[0] 

134 del self.members[name] # ty:ignore[unresolved-attribute] 

135 else: 

136 self.members[parts[0]].del_member(parts[1:]) # ty:ignore[unresolved-attribute] 

137 

138 

139class SetMembersMixin: 

140 """Mixin class to share methods for setting members.""" 

141 

142 def __setitem__(self, key: str | Sequence[str], value: Object | Alias) -> None: 

143 """Set a member with its name or path. 

144 

145 This method is part of the consumer API: 

146 do not use when producing Griffe trees! 

147 

148 Parameters: 

149 key: The name or path of the member. 

150 value: The member. 

151 

152 Examples: 

153 >>> griffe_object["foo"] = foo 

154 >>> griffe_object["path.to.bar"] = bar 

155 >>> griffe_object[("path", "to", "qux")] = qux 

156 """ 

157 parts = _get_parts(key) 

158 if len(parts) == 1: 158 ↛ 166line 158 didn't jump to line 166 because the condition on line 158 was always true

159 name = parts[0] 

160 self.members[name] = value # ty:ignore[unresolved-attribute] 

161 if self.is_collection: # ty:ignore[unresolved-attribute] 

162 value._modules_collection = self # ty:ignore[invalid-assignment] 

163 else: 

164 value.parent = self # ty:ignore[invalid-assignment] 

165 else: 

166 self.members[parts[0]][parts[1:]] = value # ty:ignore[unresolved-attribute] 

167 

168 def set_member(self, key: str | Sequence[str], value: Object | Alias) -> None: 

169 """Set a member with its name or path. 

170 

171 This method is part of the producer API: 

172 you can use it safely while building Griffe trees 

173 (for example in Griffe extensions). 

174 

175 Parameters: 

176 key: The name or path of the member. 

177 value: The member. 

178 

179 Examples: 

180 >>> griffe_object.set_member("foo", foo) 

181 >>> griffe_object.set_member("path.to.bar", bar) 

182 >>> griffe_object.set_member(("path", "to", "qux"), qux) 

183 """ 

184 parts = _get_parts(key) 

185 if len(parts) == 1: 185 ↛ 209line 185 didn't jump to line 209 because the condition on line 185 was always true

186 name = parts[0] 

187 if name in self.members: # ty:ignore[unresolved-attribute] 

188 member = self.members[name] # ty:ignore[unresolved-attribute] 

189 if not member.is_alias: 

190 # When reassigning a module to an existing one, 

191 # try to merge them as one regular and one stubs module 

192 # (implicit support for .pyi modules). 

193 if member.is_module and not (member.is_namespace_package or member.is_namespace_subpackage): 

194 # Accessing attributes of the value or member can trigger alias errors. 

195 # Accessing file paths can trigger a builtin module error. 

196 with suppress(AliasResolutionError, CyclicAliasError, BuiltinModuleError): 

197 if value.is_module and value.filepath != member.filepath: 197 ↛ 200line 197 didn't jump to line 200

198 with suppress(ValueError): 

199 value = merge_stubs(member, value) # ty:ignore[invalid-argument-type] 

200 for alias in member.aliases.values(): 200 ↛ 201line 200 didn't jump to line 201 because the loop on line 200 never started

201 with suppress(CyclicAliasError): 

202 alias.target = value 

203 self.members[name] = value # ty:ignore[unresolved-attribute] 

204 if self.is_collection: # ty:ignore[unresolved-attribute] 

205 value._modules_collection = self # ty:ignore[invalid-assignment] 

206 else: 

207 value.parent = self # ty:ignore[invalid-assignment] 

208 else: 

209 self.members[parts[0]].set_member(parts[1:], value) # ty:ignore[unresolved-attribute] 

210 

211 

212class SerializationMixin: 

213 """Mixin class to share methods for de/serializing objects.""" 

214 

215 def as_json(self, *, full: bool = False, **kwargs: Any) -> str: 

216 """Return this object's data as a JSON string. 

217 

218 Parameters: 

219 full: Whether to return full info, or just base info. 

220 **kwargs: Additional serialization options passed to encoder. 

221 

222 Returns: 

223 A JSON string. 

224 """ 

225 from griffe._internal.encoders import JSONEncoder # Avoid circular import. # noqa: PLC0415 

226 

227 return json.dumps(self, cls=JSONEncoder, full=full, **kwargs) 

228 

229 @classmethod 

230 def from_json(cls: type[_ObjType], json_string: str, **kwargs: Any) -> _ObjType: # noqa: PYI019 

231 """Create an instance of this class from a JSON string. 

232 

233 Parameters: 

234 json_string: JSON to decode into Object. 

235 **kwargs: Additional options passed to decoder. 

236 

237 Returns: 

238 An Object instance. 

239 

240 Raises: 

241 TypeError: When the json_string does not represent and object 

242 of the class from which this classmethod has been called. 

243 """ 

244 from griffe._internal.encoders import json_decoder # Avoid circular import. # noqa: PLC0415 

245 

246 kwargs.setdefault("object_hook", json_decoder) 

247 obj = json.loads(json_string, **kwargs) 

248 if not isinstance(obj, cls): 

249 raise TypeError(f"provided JSON object is not of type {cls}") 

250 return obj 

251 

252 

253class ObjectAliasMixin(GetMembersMixin, SetMembersMixin, DelMembersMixin, SerializationMixin): 

254 """Mixin class to share methods that appear both in objects and aliases, unchanged.""" 

255 

256 @property 

257 def all_members(self) -> dict[str, Object | Alias]: 

258 """All members (declared and inherited). 

259 

260 This method is part of the consumer API: 

261 do not use when producing Griffe trees! 

262 """ 

263 if self.is_class: # ty:ignore[unresolved-attribute] 

264 return {**self.inherited_members, **self.members} # ty:ignore[unresolved-attribute] 

265 return self.members # ty:ignore[unresolved-attribute] 

266 

267 @property 

268 def modules(self) -> dict[str, Module]: 

269 """The module members. 

270 

271 This method is part of the consumer API: 

272 do not use when producing Griffe trees! 

273 """ 

274 return {name: member for name, member in self.all_members.items() if member.kind is Kind.MODULE} # ty:ignore[invalid-return-type] 

275 

276 @property 

277 def classes(self) -> dict[str, Class]: 

278 """The class members. 

279 

280 This method is part of the consumer API: 

281 do not use when producing Griffe trees! 

282 """ 

283 return {name: member for name, member in self.all_members.items() if member.kind is Kind.CLASS} # ty:ignore[invalid-return-type] 

284 

285 @property 

286 def functions(self) -> dict[str, Function]: 

287 """The function members. 

288 

289 This method is part of the consumer API: 

290 do not use when producing Griffe trees! 

291 """ 

292 return {name: member for name, member in self.all_members.items() if member.kind is Kind.FUNCTION} # ty:ignore[invalid-return-type] 

293 

294 @property 

295 def attributes(self) -> dict[str, Attribute]: 

296 """The attribute members. 

297 

298 This method is part of the consumer API: 

299 do not use when producing Griffe trees! 

300 """ 

301 return {name: member for name, member in self.all_members.items() if member.kind is Kind.ATTRIBUTE} # ty:ignore[invalid-return-type] 

302 

303 @property 

304 def type_aliases(self) -> dict[str, TypeAlias]: 

305 """The type alias members. 

306 

307 This method is part of the consumer API: 

308 do not use when producing Griffe trees! 

309 """ 

310 return {name: member for name, member in self.all_members.items() if member.kind is Kind.TYPE_ALIAS} # ty:ignore[invalid-return-type] 

311 

312 @property 

313 def is_private(self) -> bool: 

314 """Whether this object/alias is private (starts with `_`) but not special.""" 

315 return self.name.startswith("_") and not self.is_special # ty:ignore[unresolved-attribute] 

316 

317 @property 

318 def is_special(self) -> bool: 

319 """Whether this object/alias is special ("dunder" attribute/method, starts and end with `__`).""" 

320 return self.name.startswith("__") and self.name.endswith("__") # ty:ignore[unresolved-attribute] 

321 

322 @property 

323 def is_class_private(self) -> bool: 

324 """Whether this object/alias is class-private (starts with `__` and is a class member).""" 

325 return ( 

326 bool(self.parent) and self.parent.is_class and self.name.startswith("__") and not self.name.endswith("__") # ty:ignore[unresolved-attribute] 

327 ) 

328 

329 @property 

330 def is_imported(self) -> bool: 

331 """Whether this object/alias was imported from another module.""" 

332 return bool(self.parent) and self.name in self.parent.imports # ty:ignore[unresolved-attribute] 

333 

334 @property 

335 def is_exported(self) -> bool: 

336 """Whether this object/alias is exported (listed in `__all__`).""" 

337 return ( 

338 bool(self.parent) # ty:ignore[unresolved-attribute] 

339 and self.parent.is_module # ty:ignore[unresolved-attribute] 

340 and bool(self.parent.exports and self.name in self.parent.exports) # ty:ignore[unresolved-attribute] 

341 ) 

342 

343 @property 

344 def is_wildcard_exposed(self) -> bool: 

345 """Whether this object/alias is exposed to wildcard imports. 

346 

347 To be exposed to wildcard imports, an object/alias must: 

348 

349 - be available at runtime 

350 - have a module as parent 

351 - be listed in `__all__` if `__all__` is defined 

352 - or not be private (having a name starting with an underscore) 

353 

354 Special case for Griffe trees: a submodule is only exposed if its parent imports it. 

355 

356 Returns: 

357 True or False. 

358 """ 

359 # If the object is not available at runtime or is not defined at the module level, it is not exposed. 

360 if not self.runtime or not (bool(self.parent) and self.parent.is_module): # ty:ignore[unresolved-attribute] 

361 return False 

362 

363 # If the parent module defines `__all__`, the object is exposed if it is listed in it. 

364 if self.parent.exports is not None: # ty:ignore[unresolved-attribute] 

365 return self.name in self.parent.exports # ty:ignore[unresolved-attribute] 

366 

367 # If the object's name starts with an underscore, it is not exposed. 

368 # We don't use `is_private` or `is_special` here to avoid redundant string checks. 

369 if self.name.startswith("_"): # ty:ignore[unresolved-attribute] 

370 return False 

371 

372 # Special case for Griffe trees: a submodule is only exposed if its parent imports it. 

373 return self.is_alias or not self.is_module or self.is_imported # ty:ignore[unresolved-attribute] 

374 

375 @property 

376 def is_public(self) -> bool: 

377 """Whether this object is considered public. 

378 

379 In modules, developers can mark objects as public thanks to the `__all__` variable. 

380 In classes however, there is no convention or standard to do so. 

381 

382 Therefore, to decide whether an object is public, we follow this algorithm: 

383 

384 - If the object's `public` attribute is set (boolean), return its value. 

385 - If the object is listed in its parent's (a module) `__all__` attribute, it is public. 

386 - If the parent (module) defines `__all__` and the object is not listed in, it is private. 

387 - If the object has a private name, it is private. 

388 - If the object was imported from another module, it is private. 

389 - Otherwise, the object is public. 

390 """ 

391 # Give priority to the `public` attribute if it is set. 

392 if self.public is not None: # ty:ignore[unresolved-attribute] 392 ↛ 393line 392 didn't jump to line 393 because the condition on line 392 was never true

393 return self.public # ty:ignore[unresolved-attribute] 

394 

395 # If the object is a module and its name does not start with an underscore, it is public. 

396 # Modules are not subject to the `__all__` convention, only the underscore prefix one. 

397 if not self.is_alias and self.is_module and not self.name.startswith("_"): # ty:ignore[unresolved-attribute] 

398 return True 

399 

400 # If the object is defined at the module-level and is listed in `__all__`, it is public. 

401 # If the parent module defines `__all__` but does not list the object, it is private. 

402 if self.parent and self.parent.is_module and bool(self.parent.exports): # ty:ignore[unresolved-attribute] 

403 return self.name in self.parent.exports # ty:ignore[unresolved-attribute] 

404 

405 # Special objects are always considered public. 

406 # Even if we don't access them directly, they are used through different *public* means 

407 # like instantiating classes (`__init__`), using operators (`__eq__`), etc.. 

408 if self.is_private: 

409 return False 

410 

411 # TODO: In a future version, we will support two conventions regarding imports: 

412 # - `from a import x as x` marks `x` as public. 

413 # - `from a import *` marks all wildcard imported objects as public. 

414 if self.is_imported: # noqa: SIM103 

415 return False 

416 

417 # If we reached this point, the object is public. 

418 return True 

419 

420 @property 

421 def is_deprecated(self) -> bool: 

422 """Whether this object is deprecated.""" 

423 # NOTE: We might want to add more ways to detect deprecations in the future. 

424 return bool(self.deprecated) # ty:ignore[unresolved-attribute] 

425 

426 @property 

427 def is_generic(self) -> bool: 

428 """Whether this object is generic.""" 

429 return bool(self.type_parameters) # ty:ignore[unresolved-attribute]