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

142 statements  

« prev     ^ index     » next       coverage.py v7.6.2, created at 2024-10-12 01:34 +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.enumerations import Kind 

11from _griffe.exceptions import AliasResolutionError, BuiltinModuleError, CyclicAliasError 

12from _griffe.merger import merge_stubs 

13 

14if TYPE_CHECKING: 

15 from collections.abc import Sequence 

16 

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

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: 

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.encoders import JSONEncoder # avoid circular import 

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.encoders import json_decoder # avoid circular import 

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 is_private: Whether this object/alias is private (starts with `_`) but not special. 

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

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

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

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

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

288 is_public: Whether this object is considered public. 

289 is_deprecated: Whether this object is deprecated. 

290 """ 

291 

292 @property 

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

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

295 

296 This method is part of the consumer API: 

297 do not use when producing Griffe trees! 

298 """ 

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

300 

301 @property 

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

303 """The module members. 

304 

305 This method is part of the consumer API: 

306 do not use when producing Griffe trees! 

307 """ 

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

309 

310 @property 

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

312 """The class members. 

313 

314 This method is part of the consumer API: 

315 do not use when producing Griffe trees! 

316 """ 

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

318 

319 @property 

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

321 """The function members. 

322 

323 This method is part of the consumer API: 

324 do not use when producing Griffe trees! 

325 """ 

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

327 

328 @property 

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

330 """The attribute members. 

331 

332 This method is part of the consumer API: 

333 do not use when producing Griffe trees! 

334 """ 

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

336 

337 @property 

338 def is_private(self) -> bool: 

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

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

341 

342 @property 

343 def is_special(self) -> bool: 

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

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

346 

347 @property 

348 def is_class_private(self) -> bool: 

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

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

351 

352 @property 

353 def is_imported(self) -> bool: 

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

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

356 

357 @property 

358 def is_exported(self) -> bool: 

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

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

361 

362 @property 

363 def is_wildcard_exposed(self) -> bool: 

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

365 

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

367 

368 - be available at runtime 

369 - have a module as parent 

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

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

372 

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

374 

375 Returns: 

376 True or False. 

377 """ 

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

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

380 return False 

381 

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

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

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

385 

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

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

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

389 return False 

390 

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

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

393 

394 @property 

395 def is_public(self) -> bool: 

396 """Whether this object is considered public. 

397 

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

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

400 

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

402 

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

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

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

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

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

408 - Otherwise, the object is public. 

409 """ 

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

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

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

413 

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

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

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

417 return True 

418 

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

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

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

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

423 

424 # Special objects are always considered public. 

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

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

427 if self.is_private: 

428 return False 

429 

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

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

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

433 if self.is_imported: # noqa: SIM103 

434 return False 

435 

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

437 return True 

438 

439 @property 

440 def is_deprecated(self) -> bool: 

441 """Whether this object is deprecated.""" 

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

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