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

150 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2025-08-11 13:44 +0200

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 Methods: 

38 get_member: Get a member with its name or path. 

39 __getitem__: Same as `get_member`, with the item syntax `[]`. 

40 """ 

41 

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

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

44 

45 This method is part of the consumer API: 

46 do not use when producing Griffe trees! 

47 

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

49 triggering computation of the latter. 

50 

51 Parameters: 

52 key: The name or path of the member. 

53 

54 Examples: 

55 >>> foo = griffe_object["foo"] 

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

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

58 """ 

59 parts = _get_parts(key) 

60 if len(parts) == 1: 

61 return self.all_members[parts[0]] # type: ignore[attr-defined] 

62 return self.all_members[parts[0]][parts[1:]] # type: ignore[attr-defined] 

63 

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

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

66 

67 This method is part of the producer API: 

68 you can use it safely while building Griffe trees 

69 (for example in Griffe extensions). 

70 

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

72 

73 Parameters: 

74 key: The name or path of the member. 

75 

76 Examples: 

77 >>> foo = griffe_object["foo"] 

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

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

80 """ 

81 parts = _get_parts(key) 

82 if len(parts) == 1: 

83 return self.members[parts[0]] # type: ignore[attr-defined] 

84 return self.members[parts[0]].get_member(parts[1:]) # type: ignore[attr-defined] 

85 

86 

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

88# Would weak references be useful there? 

89class DelMembersMixin: 

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

91 

92 Methods: 

93 del_member: Delete a member with its name or path. 

94 __delitem__: Same as `del_member`, with the item syntax `[]`. 

95 """ 

96 

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

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

99 

100 This method is part of the consumer API: 

101 do not use when producing Griffe trees! 

102 

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

104 triggering computation of the latter. 

105 

106 Parameters: 

107 key: The name or path of the member. 

108 

109 Examples: 

110 >>> del griffe_object["foo"] 

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

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

113 """ 

114 parts = _get_parts(key) 

115 if len(parts) == 1: 

116 name = parts[0] 

117 try: 

118 del self.members[name] # type: ignore[attr-defined] 

119 except KeyError: 

120 del self.inherited_members[name] # type: ignore[attr-defined] 

121 else: 

122 del self.all_members[parts[0]][parts[1:]] # type: ignore[attr-defined] 

123 

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

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

126 

127 This method is part of the producer API: 

128 you can use it safely while building Griffe trees 

129 (for example in Griffe extensions). 

130 

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

132 

133 Parameters: 

134 key: The name or path of the member. 

135 

136 Examples: 

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

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

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

140 """ 

141 parts = _get_parts(key) 

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

143 name = parts[0] 

144 del self.members[name] # type: ignore[attr-defined] 

145 else: 

146 self.members[parts[0]].del_member(parts[1:]) # type: ignore[attr-defined] 

147 

148 

149class SetMembersMixin: 

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

151 

152 Methods: 

153 set_member: Set a member with its name or path. 

154 __setitem__: Same as `set_member`, with the item syntax `[]`. 

155 """ 

156 

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

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

159 

160 This method is part of the consumer API: 

161 do not use when producing Griffe trees! 

162 

163 Parameters: 

164 key: The name or path of the member. 

165 value: The member. 

166 

167 Examples: 

168 >>> griffe_object["foo"] = foo 

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

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

171 """ 

172 parts = _get_parts(key) 

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

174 name = parts[0] 

175 self.members[name] = value # type: ignore[attr-defined] 

176 if self.is_collection: # type: ignore[attr-defined] 

177 value._modules_collection = self # type: ignore[union-attr] 

178 else: 

179 value.parent = self # type: ignore[assignment] 

180 else: 

181 self.members[parts[0]][parts[1:]] = value # type: ignore[attr-defined] 

182 

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

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

185 

186 This method is part of the producer API: 

187 you can use it safely while building Griffe trees 

188 (for example in Griffe extensions). 

189 

190 Parameters: 

191 key: The name or path of the member. 

192 value: The member. 

193 

194 Examples: 

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

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

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

198 """ 

199 parts = _get_parts(key) 

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

201 name = parts[0] 

202 if name in self.members: # type: ignore[attr-defined] 

203 member = self.members[name] # type: ignore[attr-defined] 

204 if not member.is_alias: 

205 # When reassigning a module to an existing one, 

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

207 # (implicit support for .pyi modules). 

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

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

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

211 with suppress(AliasResolutionError, CyclicAliasError, BuiltinModuleError): 

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

213 with suppress(ValueError): 

214 value = merge_stubs(member, value) # type: ignore[arg-type] 

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

216 with suppress(CyclicAliasError): 

217 alias.target = value 

218 self.members[name] = value # type: ignore[attr-defined] 

219 if self.is_collection: # type: ignore[attr-defined] 

220 value._modules_collection = self # type: ignore[union-attr] 

221 else: 

222 value.parent = self # type: ignore[assignment] 

223 else: 

224 self.members[parts[0]].set_member(parts[1:], value) # type: ignore[attr-defined] 

225 

226 

227class SerializationMixin: 

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

229 

230 Methods: 

231 as_json: Return this object's data as a JSON string. 

232 from_json: Create an instance of this class from a JSON string. 

233 """ 

234 

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

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

237 

238 Parameters: 

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

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

241 

242 Returns: 

243 A JSON string. 

244 """ 

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

246 

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

248 

249 @classmethod 

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

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

252 

253 Parameters: 

254 json_string: JSON to decode into Object. 

255 **kwargs: Additional options passed to decoder. 

256 

257 Returns: 

258 An Object instance. 

259 

260 Raises: 

261 TypeError: When the json_string does not represent and object 

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

263 """ 

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

265 

266 kwargs.setdefault("object_hook", json_decoder) 

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

268 if not isinstance(obj, cls): 

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

270 return obj 

271 

272 

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

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

275 

276 Attributes: 

277 all_members: All members (declared and inherited). 

278 modules: The module members. 

279 classes: The class members. 

280 functions: The function members. 

281 attributes: The attribute members. 

282 type_aliases: The type alias members. 

283 is_private: Whether this object/alias is private (starts with `_`) but not special. 

284 is_class_private: Whether this object/alias is class-private (starts with `__` and is a class member). 

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

286 is_imported: Whether this object/alias was imported from another module. 

287 is_exported: Whether this object/alias is exported (listed in `__all__`). 

288 is_wildcard_exposed: Whether this object/alias is exposed to wildcard imports. 

289 is_public: Whether this object is considered public. 

290 is_deprecated: Whether this object is deprecated. 

291 """ 

292 

293 @property 

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

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

296 

297 This method is part of the consumer API: 

298 do not use when producing Griffe trees! 

299 """ 

300 if self.is_class: # type: ignore[attr-defined] 

301 return {**self.inherited_members, **self.members} # type: ignore[attr-defined] 

302 return self.members # type: ignore[attr-defined] 

303 

304 @property 

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

306 """The module members. 

307 

308 This method is part of the consumer API: 

309 do not use when producing Griffe trees! 

310 """ 

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

312 

313 @property 

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

315 """The class members. 

316 

317 This method is part of the consumer API: 

318 do not use when producing Griffe trees! 

319 """ 

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

321 

322 @property 

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

324 """The function members. 

325 

326 This method is part of the consumer API: 

327 do not use when producing Griffe trees! 

328 """ 

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

330 

331 @property 

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

333 """The attribute members. 

334 

335 This method is part of the consumer API: 

336 do not use when producing Griffe trees! 

337 """ 

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

339 

340 @property 

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

342 """The type alias members. 

343 

344 This method is part of the consumer API: 

345 do not use when producing Griffe trees! 

346 """ 

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

348 

349 @property 

350 def is_private(self) -> bool: 

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

352 return self.name.startswith("_") and not self.is_special # type: ignore[attr-defined] 

353 

354 @property 

355 def is_special(self) -> bool: 

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

357 return self.name.startswith("__") and self.name.endswith("__") # type: ignore[attr-defined] 

358 

359 @property 

360 def is_class_private(self) -> bool: 

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

362 return ( 

363 bool(self.parent) and self.parent.is_class and self.name.startswith("__") and not self.name.endswith("__") # type: ignore[attr-defined] 

364 ) 

365 

366 @property 

367 def is_imported(self) -> bool: 

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

369 return bool(self.parent) and self.name in self.parent.imports # type: ignore[attr-defined] 

370 

371 @property 

372 def is_exported(self) -> bool: 

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

374 return ( 

375 bool(self.parent) # type: ignore[attr-defined] 

376 and self.parent.is_module # type: ignore[attr-defined] 

377 and bool(self.parent.exports and self.name in self.parent.exports) # type: ignore[attr-defined] 

378 ) 

379 

380 @property 

381 def is_wildcard_exposed(self) -> bool: 

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

383 

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

385 

386 - be available at runtime 

387 - have a module as parent 

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

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

390 

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

392 

393 Returns: 

394 True or False. 

395 """ 

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

397 if not self.runtime or not (bool(self.parent) and self.parent.is_module): # type: ignore[attr-defined] 

398 return False 

399 

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

401 if self.parent.exports is not None: # type: ignore[attr-defined] 

402 return self.name in self.parent.exports # type: ignore[attr-defined] 

403 

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

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

406 if self.name.startswith("_"): # type: ignore[attr-defined] 

407 return False 

408 

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

410 return self.is_alias or not self.is_module or self.is_imported # type: ignore[attr-defined] 

411 

412 @property 

413 def is_public(self) -> bool: 

414 """Whether this object is considered public. 

415 

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

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

418 

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

420 

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

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

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

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

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

426 - Otherwise, the object is public. 

427 """ 

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

429 if self.public is not None: # type: ignore[attr-defined] 429 ↛ 430line 429 didn't jump to line 430 because the condition on line 429 was never true

430 return self.public # type: ignore[attr-defined] 

431 

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

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

434 if not self.is_alias and self.is_module and not self.name.startswith("_"): # type: ignore[attr-defined] 

435 return True 

436 

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

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

439 if self.parent and self.parent.is_module and bool(self.parent.exports): # type: ignore[attr-defined] 

440 return self.name in self.parent.exports # type: ignore[attr-defined] 

441 

442 # Special objects are always considered public. 

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

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

445 if self.is_private: 

446 return False 

447 

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

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

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

451 if self.is_imported: # noqa: SIM103 

452 return False 

453 

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

455 return True 

456 

457 @property 

458 def is_deprecated(self) -> bool: 

459 """Whether this object is deprecated.""" 

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

461 return bool(self.deprecated) # type: ignore[attr-defined] 

462 

463 @property 

464 def is_generic(self) -> bool: 

465 """Whether this object is generic.""" 

466 return bool(self.type_parameters) # type: ignore[attr-defined]