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
« 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.
4from __future__ import annotations
6import json
7from contextlib import suppress
8from typing import TYPE_CHECKING, Any, TypeVar
10from _griffe.enumerations import Kind
11from _griffe.exceptions import AliasResolutionError, BuiltinModuleError, CyclicAliasError
12from _griffe.merger import merge_stubs
14if TYPE_CHECKING:
15 from collections.abc import Sequence
17 from _griffe.models import Alias, Attribute, Class, Function, Module, Object
19_ObjType = TypeVar("_ObjType")
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
34class GetMembersMixin:
35 """Mixin class to share methods for accessing members.
37 Methods:
38 get_member: Get a member with its name or path.
39 __getitem__: Same as `get_member`, with the item syntax `[]`.
40 """
42 def __getitem__(self, key: str | Sequence[str]) -> Any:
43 """Get a member with its name or path.
45 This method is part of the consumer API:
46 do not use when producing Griffe trees!
48 Members will be looked up in both declared members and inherited ones,
49 triggering computation of the latter.
51 Parameters:
52 key: The name or path of the member.
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]
64 def get_member(self, key: str | Sequence[str]) -> Any:
65 """Get a member with its name or path.
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).
71 Members will be looked up in declared members only, not inherited ones.
73 Parameters:
74 key: The name or path of the member.
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]
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.
92 Methods:
93 del_member: Delete a member with its name or path.
94 __delitem__: Same as `del_member`, with the item syntax `[]`.
95 """
97 def __delitem__(self, key: str | Sequence[str]) -> None:
98 """Delete a member with its name or path.
100 This method is part of the consumer API:
101 do not use when producing Griffe trees!
103 Members will be looked up in both declared members and inherited ones,
104 triggering computation of the latter.
106 Parameters:
107 key: The name or path of the member.
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]
124 def del_member(self, key: str | Sequence[str]) -> None:
125 """Delete a member with its name or path.
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).
131 Members will be looked up in declared members only, not inherited ones.
133 Parameters:
134 key: The name or path of the member.
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]
149class SetMembersMixin:
150 """Mixin class to share methods for setting members.
152 Methods:
153 set_member: Set a member with its name or path.
154 __setitem__: Same as `set_member`, with the item syntax `[]`.
155 """
157 def __setitem__(self, key: str | Sequence[str], value: Object | Alias) -> None:
158 """Set a member with its name or path.
160 This method is part of the consumer API:
161 do not use when producing Griffe trees!
163 Parameters:
164 key: The name or path of the member.
165 value: The member.
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]
183 def set_member(self, key: str | Sequence[str], value: Object | Alias) -> None:
184 """Set a member with its name or path.
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).
190 Parameters:
191 key: The name or path of the member.
192 value: The member.
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]
227class SerializationMixin:
228 """Mixin class to share methods for de/serializing objects.
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 """
235 def as_json(self, *, full: bool = False, **kwargs: Any) -> str:
236 """Return this object's data as a JSON string.
238 Parameters:
239 full: Whether to return full info, or just base info.
240 **kwargs: Additional serialization options passed to encoder.
242 Returns:
243 A JSON string.
244 """
245 from _griffe.encoders import JSONEncoder # avoid circular import
247 return json.dumps(self, cls=JSONEncoder, full=full, **kwargs)
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.
253 Parameters:
254 json_string: JSON to decode into Object.
255 **kwargs: Additional options passed to decoder.
257 Returns:
258 An Object instance.
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
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
273class ObjectAliasMixin(GetMembersMixin, SetMembersMixin, DelMembersMixin, SerializationMixin):
274 """Mixin class to share methods that appear both in objects and aliases, unchanged.
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 """
292 @property
293 def all_members(self) -> dict[str, Object | Alias]:
294 """All members (declared and inherited).
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]
301 @property
302 def modules(self) -> dict[str, Module]:
303 """The module members.
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]
310 @property
311 def classes(self) -> dict[str, Class]:
312 """The class members.
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]
319 @property
320 def functions(self) -> dict[str, Function]:
321 """The function members.
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]
328 @property
329 def attributes(self) -> dict[str, Attribute]:
330 """The attribute members.
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]
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]
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]
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]
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]
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]
362 @property
363 def is_wildcard_exposed(self) -> bool:
364 """Whether this object/alias is exposed to wildcard imports.
366 To be exposed to wildcard imports, an object/alias must:
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)
373 Special case for Griffe trees: a submodule is only exposed if its parent imports it.
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
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]
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
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]
394 @property
395 def is_public(self) -> bool:
396 """Whether this object is considered public.
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.
401 Therefore, to decide whether an object is public, we follow this algorithm:
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]
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
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]
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
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
436 # If we reached this point, the object is public.
437 return True
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]