Coverage for src/_griffe/mixins.py: 85.47%

142 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-08-16 15:54 +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, Sequence, TypeVar 

9 

10from _griffe.enumerations import Kind 

11from _griffe.exceptions import AliasResolutionError, CyclicAliasError 

12from _griffe.merger import merge_stubs 

13 

14if TYPE_CHECKING: 

15 from _griffe.models import Alias, Attribute, Class, Function, Module, Object 

16 

17_ObjType = TypeVar("_ObjType") 

18 

19 

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

21 if isinstance(key, str): 

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

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

24 parts = key.split(".") 

25 else: 

26 parts = list(key) 

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

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

29 return parts 

30 

31 

32class GetMembersMixin: 

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

34 

35 Methods: 

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

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

38 """ 

39 

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

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

42 

43 This method is part of the consumer API: 

44 do not use when producing Griffe trees! 

45 

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

47 triggering computation of the latter. 

48 

49 Parameters: 

50 key: The name or path of the member. 

51 

52 Examples: 

53 >>> foo = griffe_object["foo"] 

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

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

56 """ 

57 parts = _get_parts(key) 

58 if len(parts) == 1: 

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

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

61 

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

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

64 

65 This method is part of the producer API: 

66 you can use it safely while building Griffe trees 

67 (for example in Griffe extensions). 

68 

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

70 

71 Parameters: 

72 key: The name or path of the member. 

73 

74 Examples: 

75 >>> foo = griffe_object["foo"] 

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

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

78 """ 

79 parts = _get_parts(key) 

80 if len(parts) == 1: 

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

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

83 

84 

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

86# Would weak references be useful there? 

87class DelMembersMixin: 

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

89 

90 Methods: 

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

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

93 """ 

94 

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

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

97 

98 This method is part of the consumer API: 

99 do not use when producing Griffe trees! 

100 

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

102 triggering computation of the latter. 

103 

104 Parameters: 

105 key: The name or path of the member. 

106 

107 Examples: 

108 >>> del griffe_object["foo"] 

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

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

111 """ 

112 parts = _get_parts(key) 

113 if len(parts) == 1: 

114 name = parts[0] 

115 try: 

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

117 except KeyError: 

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

119 else: 

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

121 

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

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

124 

125 This method is part of the producer API: 

126 you can use it safely while building Griffe trees 

127 (for example in Griffe extensions). 

128 

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

130 

131 Parameters: 

132 key: The name or path of the member. 

133 

134 Examples: 

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

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

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

138 """ 

139 parts = _get_parts(key) 

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

141 name = parts[0] 

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

143 else: 

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

145 

146 

147class SetMembersMixin: 

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

149 

150 Methods: 

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

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

153 """ 

154 

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

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

157 

158 This method is part of the consumer API: 

159 do not use when producing Griffe trees! 

160 

161 Parameters: 

162 key: The name or path of the member. 

163 value: The member. 

164 

165 Examples: 

166 >>> griffe_object["foo"] = foo 

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

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

169 """ 

170 parts = _get_parts(key) 

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

172 name = parts[0] 

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

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

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

176 else: 

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

178 else: 

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

180 

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

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

183 

184 This method is part of the producer API: 

185 you can use it safely while building Griffe trees 

186 (for example in Griffe extensions). 

187 

188 Parameters: 

189 key: The name or path of the member. 

190 value: The member. 

191 

192 Examples: 

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

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

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

196 """ 

197 parts = _get_parts(key) 

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

199 name = parts[0] 

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

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

202 if not member.is_alias: 

203 # When reassigning a module to an existing one, 

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

205 # (implicit support for .pyi modules). 

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

207 with suppress(AliasResolutionError, CyclicAliasError): 

208 if value.is_module and value.filepath != member.filepath: 

209 with suppress(ValueError): 

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

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

212 with suppress(CyclicAliasError): 

213 alias.target = value 

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

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

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

217 else: 

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

219 else: 

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

221 

222 

223class SerializationMixin: 

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

225 

226 Methods: 

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

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

229 """ 

230 

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

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

233 

234 Parameters: 

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

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

237 

238 Returns: 

239 A JSON string. 

240 """ 

241 from _griffe.encoders import JSONEncoder # avoid circular import 

242 

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

244 

245 @classmethod 

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

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

248 

249 Parameters: 

250 json_string: JSON to decode into Object. 

251 **kwargs: Additional options passed to decoder. 

252 

253 Returns: 

254 An Object instance. 

255 

256 Raises: 

257 TypeError: When the json_string does not represent and object 

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

259 """ 

260 from _griffe.encoders import json_decoder # avoid circular import 

261 

262 kwargs.setdefault("object_hook", json_decoder) 

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

264 if not isinstance(obj, cls): 

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

266 return obj 

267 

268 

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

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

271 

272 Attributes: 

273 all_members: All members (declared and inherited). 

274 modules: The module members. 

275 classes: The class members. 

276 functions: The function members. 

277 attributes: The attribute members. 

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

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

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

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

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

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

284 is_public: Whether this object is considered public. 

285 is_deprecated: Whether this object is deprecated. 

286 """ 

287 

288 @property 

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

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

291 

292 This method is part of the consumer API: 

293 do not use when producing Griffe trees! 

294 """ 

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

296 

297 @property 

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

299 """The module members. 

300 

301 This method is part of the consumer API: 

302 do not use when producing Griffe trees! 

303 """ 

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

305 

306 @property 

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

308 """The class members. 

309 

310 This method is part of the consumer API: 

311 do not use when producing Griffe trees! 

312 """ 

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

314 

315 @property 

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

317 """The function members. 

318 

319 This method is part of the consumer API: 

320 do not use when producing Griffe trees! 

321 """ 

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

323 

324 @property 

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

326 """The attribute members. 

327 

328 This method is part of the consumer API: 

329 do not use when producing Griffe trees! 

330 """ 

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

332 

333 @property 

334 def is_private(self) -> bool: 

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

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

337 

338 @property 

339 def is_special(self) -> bool: 

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

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

342 

343 @property 

344 def is_class_private(self) -> bool: 

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

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

347 

348 @property 

349 def is_imported(self) -> bool: 

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

351 return self.parent and self.name in self.parent.imports # type: ignore[attr-defined] 

352 

353 @property 

354 def is_exported(self) -> bool: 

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

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

357 

358 @property 

359 def is_wildcard_exposed(self) -> bool: 

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

361 

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

363 

364 - be available at runtime 

365 - have a module as parent 

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

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

368 

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

370 

371 Returns: 

372 True or False. 

373 """ 

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

375 if not self.runtime or not self.parent.is_module: # type: ignore[attr-defined] 

376 return False 

377 

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

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

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

381 

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

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

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

385 return False 

386 

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

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

389 

390 @property 

391 def is_public(self) -> bool: 

392 """Whether this object is considered public. 

393 

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

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

396 

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

398 

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

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

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

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

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

404 - Otherwise, the object is public. 

405 """ 

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

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

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

409 

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

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

412 if not self.is_alias and self.is_module and not self.name.startswith("_"): # type: ignore[attr-defined] 412 ↛ 413line 412 didn't jump to line 413 because the condition on line 412 was never true

413 return True 

414 

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

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

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

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

419 

420 # Special objects are always considered public. 

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

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

423 if self.is_private: 

424 return False 

425 

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

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

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

429 if self.is_imported: # noqa: SIM103 

430 return False 

431 

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

433 return True 

434 

435 @property 

436 def is_deprecated(self) -> bool: 

437 """Whether this object is deprecated.""" 

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

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