# This module contains some mixins classes that hold shared methods # of the different kinds of objects, and aliases. from __future__ import annotations import json from contextlib import suppress from typing import TYPE_CHECKING, Any, TypeVar from _griffe.enumerations import Kind from _griffe.exceptions import AliasResolutionError, BuiltinModuleError, CyclicAliasError from _griffe.merger import merge_stubs if TYPE_CHECKING: from collections.abc import Sequence from _griffe.models import Alias, Attribute, Class, Function, Module, Object _ObjType = TypeVar("_ObjType") def _get_parts(key: str | Sequence[str]) -> Sequence[str]: if isinstance(key, str): if not key: raise ValueError("Empty strings are not supported") parts = key.split(".") else: parts = list(key) if not parts: raise ValueError("Empty tuples are not supported") return parts class GetMembersMixin: """Mixin class to share methods for accessing members. Methods: get_member: Get a member with its name or path. __getitem__: Same as `get_member`, with the item syntax `[]`. """ def __getitem__(self, key: str | Sequence[str]) -> Any: """Get a member with its name or path. This method is part of the consumer API: do not use when producing Griffe trees! Members will be looked up in both declared members and inherited ones, triggering computation of the latter. Parameters: key: The name or path of the member. Examples: >>> foo = griffe_object["foo"] >>> bar = griffe_object["path.to.bar"] >>> qux = griffe_object[("path", "to", "qux")] """ parts = _get_parts(key) if len(parts) == 1: return self.all_members[parts[0]] # type: ignore[attr-defined] return self.all_members[parts[0]][parts[1:]] # type: ignore[attr-defined] def get_member(self, key: str | Sequence[str]) -> Any: """Get a member with its name or path. This method is part of the producer API: you can use it safely while building Griffe trees (for example in Griffe extensions). Members will be looked up in declared members only, not inherited ones. Parameters: key: The name or path of the member. Examples: >>> foo = griffe_object["foo"] >>> bar = griffe_object["path.to.bar"] >>> bar = griffe_object[("path", "to", "bar")] """ parts = _get_parts(key) if len(parts) == 1: return self.members[parts[0]] # type: ignore[attr-defined] return self.members[parts[0]].get_member(parts[1:]) # type: ignore[attr-defined] # FIXME: Are `aliases` in other objects correctly updated when we delete a member? # Would weak references be useful there? class DelMembersMixin: """Mixin class to share methods for deleting members. Methods: del_member: Delete a member with its name or path. __delitem__: Same as `del_member`, with the item syntax `[]`. """ def __delitem__(self, key: str | Sequence[str]) -> None: """Delete a member with its name or path. This method is part of the consumer API: do not use when producing Griffe trees! Members will be looked up in both declared members and inherited ones, triggering computation of the latter. Parameters: key: The name or path of the member. Examples: >>> del griffe_object["foo"] >>> del griffe_object["path.to.bar"] >>> del griffe_object[("path", "to", "qux")] """ parts = _get_parts(key) if len(parts) == 1: name = parts[0] try: del self.members[name] # type: ignore[attr-defined] except KeyError: del self.inherited_members[name] # type: ignore[attr-defined] else: del self.all_members[parts[0]][parts[1:]] # type: ignore[attr-defined] def del_member(self, key: str | Sequence[str]) -> None: """Delete a member with its name or path. This method is part of the producer API: you can use it safely while building Griffe trees (for example in Griffe extensions). Members will be looked up in declared members only, not inherited ones. Parameters: key: The name or path of the member. Examples: >>> griffe_object.del_member("foo") >>> griffe_object.del_member("path.to.bar") >>> griffe_object.del_member(("path", "to", "qux")) """ parts = _get_parts(key) if len(parts) == 1: name = parts[0] del self.members[name] # type: ignore[attr-defined] else: self.members[parts[0]].del_member(parts[1:]) # type: ignore[attr-defined] class SetMembersMixin: """Mixin class to share methods for setting members. Methods: set_member: Set a member with its name or path. __setitem__: Same as `set_member`, with the item syntax `[]`. """ def __setitem__(self, key: str | Sequence[str], value: Object | Alias) -> None: """Set a member with its name or path. This method is part of the consumer API: do not use when producing Griffe trees! Parameters: key: The name or path of the member. value: The member. Examples: >>> griffe_object["foo"] = foo >>> griffe_object["path.to.bar"] = bar >>> griffe_object[("path", "to", "qux")] = qux """ parts = _get_parts(key) if len(parts) == 1: name = parts[0] self.members[name] = value # type: ignore[attr-defined] if self.is_collection: # type: ignore[attr-defined] value._modules_collection = self # type: ignore[union-attr] else: value.parent = self # type: ignore[assignment] else: self.members[parts[0]][parts[1:]] = value # type: ignore[attr-defined] def set_member(self, key: str | Sequence[str], value: Object | Alias) -> None: """Set a member with its name or path. This method is part of the producer API: you can use it safely while building Griffe trees (for example in Griffe extensions). Parameters: key: The name or path of the member. value: The member. Examples: >>> griffe_object.set_member("foo", foo) >>> griffe_object.set_member("path.to.bar", bar) >>> griffe_object.set_member(("path", "to", "qux"), qux) """ parts = _get_parts(key) if len(parts) == 1: name = parts[0] if name in self.members: # type: ignore[attr-defined] member = self.members[name] # type: ignore[attr-defined] if not member.is_alias: # When reassigning a module to an existing one, # try to merge them as one regular and one stubs module # (implicit support for .pyi modules). if member.is_module and not (member.is_namespace_package or member.is_namespace_subpackage): # Accessing attributes of the value or member can trigger alias errors. # Accessing file paths can trigger a builtin module error. with suppress(AliasResolutionError, CyclicAliasError, BuiltinModuleError): if value.is_module and value.filepath != member.filepath: with suppress(ValueError): value = merge_stubs(member, value) # type: ignore[arg-type] for alias in member.aliases.values(): with suppress(CyclicAliasError): alias.target = value self.members[name] = value # type: ignore[attr-defined] if self.is_collection: # type: ignore[attr-defined] value._modules_collection = self # type: ignore[union-attr] else: value.parent = self # type: ignore[assignment] else: self.members[parts[0]].set_member(parts[1:], value) # type: ignore[attr-defined] class SerializationMixin: """Mixin class to share methods for de/serializing objects. Methods: as_json: Return this object's data as a JSON string. from_json: Create an instance of this class from a JSON string. """ def as_json(self, *, full: bool = False, **kwargs: Any) -> str: """Return this object's data as a JSON string. Parameters: full: Whether to return full info, or just base info. **kwargs: Additional serialization options passed to encoder. Returns: A JSON string. """ from _griffe.encoders import JSONEncoder # Avoid circular import. return json.dumps(self, cls=JSONEncoder, full=full, **kwargs) @classmethod def from_json(cls: type[_ObjType], json_string: str, **kwargs: Any) -> _ObjType: # noqa: PYI019 """Create an instance of this class from a JSON string. Parameters: json_string: JSON to decode into Object. **kwargs: Additional options passed to decoder. Returns: An Object instance. Raises: TypeError: When the json_string does not represent and object of the class from which this classmethod has been called. """ from _griffe.encoders import json_decoder # Avoid circular import. kwargs.setdefault("object_hook", json_decoder) obj = json.loads(json_string, **kwargs) if not isinstance(obj, cls): raise TypeError(f"provided JSON object is not of type {cls}") return obj class ObjectAliasMixin(GetMembersMixin, SetMembersMixin, DelMembersMixin, SerializationMixin): """Mixin class to share methods that appear both in objects and aliases, unchanged. Attributes: all_members: All members (declared and inherited). modules: The module members. classes: The class members. functions: The function members. attributes: The attribute members. is_private: Whether this object/alias is private (starts with `_`) but not special. is_class_private: Whether this object/alias is class-private (starts with `__` and is a class member). is_special: Whether this object/alias is special ("dunder" attribute/method, starts and end with `__`). is_imported: Whether this object/alias was imported from another module. is_exported: Whether this object/alias is exported (listed in `__all__`). is_wildcard_exposed: Whether this object/alias is exposed to wildcard imports. is_public: Whether this object is considered public. is_deprecated: Whether this object is deprecated. """ @property def all_members(self) -> dict[str, Object | Alias]: """All members (declared and inherited). This method is part of the consumer API: do not use when producing Griffe trees! """ if self.is_class: # type: ignore[attr-defined] return {**self.inherited_members, **self.members} # type: ignore[attr-defined] return self.members # type: ignore[attr-defined] @property def modules(self) -> dict[str, Module]: """The module members. This method is part of the consumer API: do not use when producing Griffe trees! """ return {name: member for name, member in self.all_members.items() if member.kind is Kind.MODULE} # type: ignore[misc] @property def classes(self) -> dict[str, Class]: """The class members. This method is part of the consumer API: do not use when producing Griffe trees! """ return {name: member for name, member in self.all_members.items() if member.kind is Kind.CLASS} # type: ignore[misc] @property def functions(self) -> dict[str, Function]: """The function members. This method is part of the consumer API: do not use when producing Griffe trees! """ return {name: member for name, member in self.all_members.items() if member.kind is Kind.FUNCTION} # type: ignore[misc] @property def attributes(self) -> dict[str, Attribute]: """The attribute members. This method is part of the consumer API: do not use when producing Griffe trees! """ return {name: member for name, member in self.all_members.items() if member.kind is Kind.ATTRIBUTE} # type: ignore[misc] @property def is_private(self) -> bool: """Whether this object/alias is private (starts with `_`) but not special.""" return self.name.startswith("_") and not self.is_special # type: ignore[attr-defined] @property def is_special(self) -> bool: """Whether this object/alias is special ("dunder" attribute/method, starts and end with `__`).""" return self.name.startswith("__") and self.name.endswith("__") # type: ignore[attr-defined] @property def is_class_private(self) -> bool: """Whether this object/alias is class-private (starts with `__` and is a class member).""" return self.parent and self.parent.is_class and self.name.startswith("__") and not self.name.endswith("__") # type: ignore[attr-defined] @property def is_imported(self) -> bool: """Whether this object/alias was imported from another module.""" return self.parent and self.name in self.parent.imports # type: ignore[attr-defined] @property def is_exported(self) -> bool: """Whether this object/alias is exported (listed in `__all__`).""" return self.parent.is_module and bool(self.parent.exports and self.name in self.parent.exports) # type: ignore[attr-defined] @property def is_wildcard_exposed(self) -> bool: """Whether this object/alias is exposed to wildcard imports. To be exposed to wildcard imports, an object/alias must: - be available at runtime - have a module as parent - be listed in `__all__` if `__all__` is defined - or not be private (having a name starting with an underscore) Special case for Griffe trees: a submodule is only exposed if its parent imports it. Returns: True or False. """ # If the object is not available at runtime or is not defined at the module level, it is not exposed. if not self.runtime or not self.parent.is_module: # type: ignore[attr-defined] return False # If the parent module defines `__all__`, the object is exposed if it is listed in it. if self.parent.exports is not None: # type: ignore[attr-defined] return self.name in self.parent.exports # type: ignore[attr-defined] # If the object's name starts with an underscore, it is not exposed. # We don't use `is_private` or `is_special` here to avoid redundant string checks. if self.name.startswith("_"): # type: ignore[attr-defined] return False # Special case for Griffe trees: a submodule is only exposed if its parent imports it. return self.is_alias or not self.is_module or self.is_imported # type: ignore[attr-defined] @property def is_public(self) -> bool: """Whether this object is considered public. In modules, developers can mark objects as public thanks to the `__all__` variable. In classes however, there is no convention or standard to do so. Therefore, to decide whether an object is public, we follow this algorithm: - If the object's `public` attribute is set (boolean), return its value. - If the object is listed in its parent's (a module) `__all__` attribute, it is public. - If the parent (module) defines `__all__` and the object is not listed in, it is private. - If the object has a private name, it is private. - If the object was imported from another module, it is private. - Otherwise, the object is public. """ # Give priority to the `public` attribute if it is set. if self.public is not None: # type: ignore[attr-defined] return self.public # type: ignore[attr-defined] # If the object is a module and its name does not start with an underscore, it is public. # Modules are not subject to the `__all__` convention, only the underscore prefix one. if not self.is_alias and self.is_module and not self.name.startswith("_"): # type: ignore[attr-defined] return True # If the object is defined at the module-level and is listed in `__all__`, it is public. # If the parent module defines `__all__` but does not list the object, it is private. if self.parent and self.parent.is_module and bool(self.parent.exports): # type: ignore[attr-defined] return self.name in self.parent.exports # type: ignore[attr-defined] # Special objects are always considered public. # Even if we don't access them directly, they are used through different *public* means # like instantiating classes (`__init__`), using operators (`__eq__`), etc.. if self.is_private: return False # TODO: In a future version, we will support two conventions regarding imports: # - `from a import x as x` marks `x` as public. # - `from a import *` marks all wildcard imported objects as public. if self.is_imported: # noqa: SIM103 return False # If we reached this point, the object is public. return True @property def is_deprecated(self) -> bool: """Whether this object is deprecated.""" # NOTE: We might want to add more ways to detect deprecations in the future. return bool(self.deprecated) # type: ignore[attr-defined]