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
« 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.
4from __future__ import annotations
6import json
7from contextlib import suppress
8from typing import TYPE_CHECKING, Any, TypeVar
10from griffe._internal.enumerations import Kind
11from griffe._internal.exceptions import AliasResolutionError, BuiltinModuleError, CyclicAliasError
12from griffe._internal.merger import merge_stubs
14if TYPE_CHECKING:
15 from collections.abc import Sequence
17 from griffe._internal.models import Alias, Attribute, Class, Function, Module, Object, TypeAlias
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 def __getitem__(self, key: str | Sequence[str]) -> Any:
38 """Get a member with its name or path.
40 This method is part of the consumer API:
41 do not use when producing Griffe trees!
43 Members will be looked up in both declared members and inherited ones,
44 triggering computation of the latter.
46 Parameters:
47 key: The name or path of the member.
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]
59 def get_member(self, key: str | Sequence[str]) -> Any:
60 """Get a member with its name or path.
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).
66 Members will be looked up in declared members only, not inherited ones.
68 Parameters:
69 key: The name or path of the member.
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]
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."""
87 def __delitem__(self, key: str | Sequence[str]) -> None:
88 """Delete a member with its name or path.
90 This method is part of the consumer API:
91 do not use when producing Griffe trees!
93 Members will be looked up in both declared members and inherited ones,
94 triggering computation of the latter.
96 Parameters:
97 key: The name or path of the member.
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]
114 def del_member(self, key: str | Sequence[str]) -> None:
115 """Delete a member with its name or path.
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).
121 Members will be looked up in declared members only, not inherited ones.
123 Parameters:
124 key: The name or path of the member.
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]
139class SetMembersMixin:
140 """Mixin class to share methods for setting members."""
142 def __setitem__(self, key: str | Sequence[str], value: Object | Alias) -> None:
143 """Set a member with its name or path.
145 This method is part of the consumer API:
146 do not use when producing Griffe trees!
148 Parameters:
149 key: The name or path of the member.
150 value: The member.
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]
168 def set_member(self, key: str | Sequence[str], value: Object | Alias) -> None:
169 """Set a member with its name or path.
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).
175 Parameters:
176 key: The name or path of the member.
177 value: The member.
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]
212class SerializationMixin:
213 """Mixin class to share methods for de/serializing objects."""
215 def as_json(self, *, full: bool = False, **kwargs: Any) -> str:
216 """Return this object's data as a JSON string.
218 Parameters:
219 full: Whether to return full info, or just base info.
220 **kwargs: Additional serialization options passed to encoder.
222 Returns:
223 A JSON string.
224 """
225 from griffe._internal.encoders import JSONEncoder # Avoid circular import. # noqa: PLC0415
227 return json.dumps(self, cls=JSONEncoder, full=full, **kwargs)
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.
233 Parameters:
234 json_string: JSON to decode into Object.
235 **kwargs: Additional options passed to decoder.
237 Returns:
238 An Object instance.
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
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
253class ObjectAliasMixin(GetMembersMixin, SetMembersMixin, DelMembersMixin, SerializationMixin):
254 """Mixin class to share methods that appear both in objects and aliases, unchanged."""
256 @property
257 def all_members(self) -> dict[str, Object | Alias]:
258 """All members (declared and inherited).
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]
267 @property
268 def modules(self) -> dict[str, Module]:
269 """The module members.
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]
276 @property
277 def classes(self) -> dict[str, Class]:
278 """The class members.
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]
285 @property
286 def functions(self) -> dict[str, Function]:
287 """The function members.
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]
294 @property
295 def attributes(self) -> dict[str, Attribute]:
296 """The attribute members.
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]
303 @property
304 def type_aliases(self) -> dict[str, TypeAlias]:
305 """The type alias members.
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]
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]
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]
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 )
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]
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 )
343 @property
344 def is_wildcard_exposed(self) -> bool:
345 """Whether this object/alias is exposed to wildcard imports.
347 To be exposed to wildcard imports, an object/alias must:
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)
354 Special case for Griffe trees: a submodule is only exposed if its parent imports it.
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
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]
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
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]
375 @property
376 def is_public(self) -> bool:
377 """Whether this object is considered public.
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.
382 Therefore, to decide whether an object is public, we follow this algorithm:
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]
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
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]
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
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
417 # If we reached this point, the object is public.
418 return True
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]
426 @property
427 def is_generic(self) -> bool:
428 """Whether this object is generic."""
429 return bool(self.type_parameters) # ty:ignore[unresolved-attribute]