Source code for term_image.renderable._types

"""
.. Custom data types for the Renderable API
"""

from __future__ import annotations

__all__ = (
    "Frame",
    "RenderArgs",
    "ArgsNamespace",
    "RenderData",
    "DataNamespace",
    "RenderArgsDataError",
    "RenderArgsError",
    "RenderDataError",
    "IncompatibleArgsNamespaceError",
    "IncompatibleRenderArgsError",
    "NoArgsNamespaceError",
    "NoDataNamespaceError",
    "UnassociatedNamespaceError",
    "UninitializedDataFieldError",
    "UnknownArgsFieldError",
    "UnknownDataFieldError",
)

from collections.abc import Iterator, Mapping, Sequence
from inspect import Parameter, signature
from types import MappingProxyType

from typing_extensions import (
    TYPE_CHECKING,
    Any,
    ClassVar,
    NamedTuple,
    Never,
    Self,
    TypeVar,
    dataclass_transform,
    overload,
)

from .. import geometry
from ..utils import arg_type_error
from ._exceptions import RenderableError

# Type Variables, Aliases, etc =================================================

if TYPE_CHECKING:
    # `RenderableMeta` is set from `._renderable` later on, as a top-level import
    # will result in a circular import and localized imports are just unnecessarily
    # costly (no matter how minimal).
    from ._renderable import Renderable, RenderableMeta

ArgsDataNamespaceMetaT = TypeVar(
    "ArgsDataNamespaceMetaT", bound="ArgsDataNamespaceMeta"
)
ArgsNamespaceMetaT = TypeVar("ArgsNamespaceMetaT", bound="ArgsNamespaceMeta")
DataNamespaceMetaT = TypeVar("DataNamespaceMetaT", bound="DataNamespaceMeta")


# Exceptions ===================================================================


[docs] class RenderArgsDataError(RenderableError): """Base exception class for errors specific to :py:class:`RenderArgs` and :py:class:`RenderData`. Raised for errors that occur during the creation of render argument/data namespace classes. """
[docs] class RenderArgsError(RenderArgsDataError): """Base exception class for errors specific to :py:class:`RenderArgs`. Raised for errors that occur during the creation of render argument namespace classes. """
[docs] class RenderDataError(RenderArgsDataError): """Base exception class for errors specific to :py:class:`RenderData`."""
[docs] class IncompatibleArgsNamespaceError(RenderArgsError): """Raised when a given render argument namespace is incompatible [#an-com]_ with a certain :term:`render class`. """
[docs] class IncompatibleRenderArgsError(RenderArgsError): """Raised when a given set of render arguments is incompatible [#ra-com]_ with a certain :term:`render class`. """
[docs] class NoArgsNamespaceError(RenderArgsError): """Raised when an attempt is made to get a render argument namespace for a :term:`render class` that has no render arguments. """
[docs] class NoDataNamespaceError(RenderDataError): """Raised when an attempt is made to get a render data namespace for a :term:`render class` that has no render data. """
[docs] class UnassociatedNamespaceError(RenderArgsDataError): """Raised when certain operations are attempted on a render argument/data namespace class that hasn't been associated [#an-ass]_ [#dn-ass]_ with a :term:`render class`. """
[docs] class UninitializedDataFieldError(RenderDataError, AttributeError): """Raised when an attempt is made to access a render data field that hasn't been initialized i.e for which a value hasn't been set. """
[docs] class UnknownArgsFieldError(RenderArgsError, AttributeError): """Raised when an attempt is made to access or modify an unknown render argument field. """
[docs] class UnknownDataFieldError(RenderDataError, AttributeError): """Raised when an attempt is made to access or modify an unknown render data field. """
# Classes ====================================================================== class ArgsDataNamespaceMeta(type): """Metaclass of render argument/data namespaces.""" _associated: bool = False _FIELDS: MappingProxyType[str, Any] = MappingProxyType({}) _RENDER_CLS: type[Renderable] def __new__( cls: type[ArgsDataNamespaceMetaT], name: str, bases: tuple[type[ArgsDataNamespaceMeta]], namespace: dict[str, Any], *, render_cls: type[Renderable] | None = None, _base: bool = False, **kwargs: Any, ) -> ArgsDataNamespaceMetaT: if _base: namespace["__slots__"] = () else: # Assumes the metaclass is never used directly without `_base=True` if len(bases) > 1: raise RenderArgsDataError("Multiple base classes") base = bases[0] fields: Sequence[str] = namespace.get("__annotations__", ()) if base._FIELDS: if fields: raise RenderArgsDataError("Cannot both inherit and define fields") elif fields: for field_name in fields: namespace.pop(field_name, None) namespace["_FIELDS"] = MappingProxyType(dict.fromkeys(fields)) namespace["__slots__"] = tuple(fields) if render_cls: if base._associated: raise RenderArgsDataError( "Cannot reassociate a namespace subclass; the base class " f"is already associated with {base._RENDER_CLS.__name__!r}" ) if not fields: raise RenderArgsDataError( "Cannot associate a namespace class that has no fields" ) if not isinstance(render_cls, RenderableMeta): raise arg_type_error("render_cls", render_cls) namespace["_associated"] = True namespace["_RENDER_CLS"] = render_cls elif fields: raise RenderArgsDataError("Unassociated namespace class with fields") new_cls = super().__new__(cls, name, bases, namespace, **kwargs) if new_cls._FIELDS: for method_name in ("__new__", "__init__"): method = getattr(new_cls, method_name) parameters = iter(signature(method).parameters.items()) next(parameters) # skip first parameter (cls, self) for param_name, param in parameters: if param.default is Parameter.empty and ( Parameter.VAR_POSITIONAL is not param.kind is not Parameter.VAR_KEYWORD ): raise TypeError( f"'{new_cls.__qualname__}.{method_name}' has a " f"required parameter {param_name!r}" ) return new_cls class ArgsDataNamespace(metaclass=ArgsDataNamespaceMeta, _base=True): """:term:`Render class`\\ -specific argument/data namespace.""" _associated: ClassVar[bool] _FIELDS: ClassVar[MappingProxyType[str, Any]] _RENDER_CLS: ClassVar[type[Renderable]] def __new__(cls, *args: Any, **kwargs: Any) -> Self: if not cls._associated: raise UnassociatedNamespaceError( "Cannot instantiate a render argument/data namespace class " "that hasn't been associated with a render class" ) return super().__new__(cls) def __init__(self, fields: Mapping[str, Any]) -> None: # Subclass(es) redefine `__setattr__()` setattr_ = ArgsDataNamespace.__setattr__ for name in type(self)._FIELDS: setattr_(self, name, fields[name]) def __delattr__(self, name: str) -> Never: raise AttributeError("Cannot delete field") @classmethod def get_render_cls(cls) -> type[Renderable]: """Returns the associated :term:`render class`. Returns: The associated render class, if the namespace class has been associated. Raises: UnassociatedNamespaceError: The namespace class hasn't been associated with a render class. """ if not cls._associated: raise UnassociatedNamespaceError( "This namespace class hasn't been associated with a render class" ) return cls._RENDER_CLS class ArgsNamespaceMeta(ArgsDataNamespaceMeta): """Metaclass of render argument namespaces.""" def __new__( cls: type[ArgsNamespaceMetaT], name: str, bases: tuple[type[ArgsNamespaceMeta]], namespace: dict[str, Any], *, _base: bool = False, **kwargs: Any, ) -> ArgsNamespaceMetaT: if not _base: try: defaults = { name: namespace[name] for name in namespace.get("__annotations__", ()) } except KeyError as e: raise RenderArgsError( f"Field {e.args[0]!r} has no default value" ) from None args_cls = super().__new__(cls, name, bases, namespace, _base=_base, **kwargs) if not _base: if "_FIELDS" in args_cls.__dict__: args_cls._FIELDS = MappingProxyType(defaults) if render_cls := args_cls.__dict__.get("_RENDER_CLS"): if render_cls.Args: raise RenderArgsError( f"{render_cls.__name__!r} already has an associated render " f"argument namespace class {render_cls.Args.__name__!r}" ) render_cls.Args = args_cls render_cls._ALL_DEFAULT_ARGS = MappingProxyType( {render_cls: args_cls(), **render_cls._ALL_DEFAULT_ARGS} ) return args_cls
[docs] @dataclass_transform(eq_default=True, frozen_default=True) class ArgsNamespace(ArgsDataNamespace, metaclass=ArgsNamespaceMeta, _base=True): """ArgsNamespace(*values, **fields) :term:`Render class`\\ -specific render argument namespace. Args: values: Render argument field values. The values are mapped to fields in the order in which the fields were defined. fields: Render argument fields. The keywords must be names of render argument fields for the associated [#an-ass]_ render class. Raises: UnassociatedNamespaceError: The namespace class hasn't been associated [#an-ass]_ with a render class. TypeError: More values (positional arguments) than there are fields. UnknownArgsFieldError: Unknown field name(s). TypeError: Multiple values given for a field. If no value is given for a field, its default value is used. NOTE: * Fields are exposed as instance attributes. * Instances are immutable but updated copies can be created via :py:meth:`update`. .. Completed in /docs/source/api/renderable.rst """ def __init__(self, *values: Any, **fields: Any) -> None: default_fields = type(self)._FIELDS if len(values) > len(default_fields): raise TypeError( f"{type(self).__name__!r} defines {len(default_fields)} render " f"argument field(s) but {len(values)} values were given" ) value_fields = dict(zip(default_fields, values)) unknown = fields.keys() - default_fields.keys() if unknown: raise UnknownArgsFieldError( f"Unknown render argument fields {tuple(unknown)} for " f"{type(self)._RENDER_CLS.__name__!r}" ) multiple = fields.keys() & value_fields.keys() if multiple: raise TypeError( f"Got multiple values for render argument fields " f"{tuple(multiple)} of {type(self)._RENDER_CLS.__name__!r}" ) super().__init__({**default_fields, **value_fields, **fields}) def __repr__(self) -> str: return "".join( ( f"{type(self).__name__}(", ", ".join( f"{name}={getattr(self, name)!r}" for name in type(self)._FIELDS ), ")", ) ) def __getattr__(self, name: str) -> Never: raise UnknownArgsFieldError( f"Unknown render argument field {name!r} for " f"{type(self)._RENDER_CLS.__name__!r}" ) def __setattr__(self, name: str, value: Never) -> Never: raise AttributeError( "Cannot modify render argument fields, use the `update()` method " "of the namespace or the containing `RenderArgs` instance, as " "applicable, instead" )
[docs] def __eq__(self, other: object) -> bool: """Compares the namespace with another. Args: other: Another render argument namespace. Returns: ``True`` if both operands are associated with the same :term:`render class` and have equal field values. Otherwise, ``False``. """ if isinstance(other, ArgsNamespace): return self is other or ( type(self)._RENDER_CLS is type(other)._RENDER_CLS and all( getattr(self, name) == getattr(other, name) for name in type(self)._FIELDS ) ) return NotImplemented
[docs] def __hash__(self) -> int: """Computes the hash of the namespace. Returns: The computed hash. IMPORTANT: Like tuples, an instance is hashable if and only if the field values are hashable. """ # Field names and their order is the same for all instances associated # with the same render class. return hash( ( type(self)._RENDER_CLS, tuple([getattr(self, field) for field in type(self)._FIELDS]), ) )
[docs] def __or__(self, other: ArgsNamespace | RenderArgs) -> RenderArgs: """Derives a set of render arguments from the combination of both operands. Args: other: Another render argument namespace or a set of render arguments. Returns: A set of render arguments associated with the **most derived** one of the associated :term:`render classes` of both operands. Raises: IncompatibleArgsNamespaceError: *other* is a render argument namespace and neither operand is compatible [#ra-com]_ [#an-com]_ with the associated :term:`render class` of the other. IncompatibleRenderArgsError: *other* is a set of render arguments and neither operand is compatible [#ra-com]_ [#an-com]_ with the associated :term:`render class` of the other. NOTE: * If *other* is a render argument namespace associated with the same :term:`render class` as *self*, *other* takes precedence. * If *other* is a set of render arguments that contains a namespace associated with the same :term:`render class` as *self*, *self* takes precedence. """ self_render_cls = type(self)._RENDER_CLS if isinstance(other, ArgsNamespace): other_render_cls = type(other)._RENDER_CLS if self_render_cls is other_render_cls: return RenderArgs(other_render_cls, other) if issubclass(self_render_cls, other_render_cls): return RenderArgs(self_render_cls, self, other) if issubclass(other_render_cls, self_render_cls): return RenderArgs(other_render_cls, self, other) raise IncompatibleArgsNamespaceError( f"A render argument namespace for {other_render_cls.__name__!r} " "cannot be combined with a render argument namespace for " f"{self_render_cls.__name__!r}." ) if isinstance(other, RenderArgs): other_render_cls = other.render_cls if issubclass(self_render_cls, other_render_cls): return RenderArgs(self_render_cls, other, self) if issubclass(other_render_cls, self_render_cls): return RenderArgs(other_render_cls, other, self) raise IncompatibleRenderArgsError( f"A set of render arguments for {other_render_cls.__name__!r} " "cannot be combined with a render argument namespace for " f"{self_render_cls.__name__!r}." ) return NotImplemented
[docs] def __pos__(self) -> RenderArgs: """Creates a set of render arguments from the namespace. Returns: A set of render arguments associated with the same :term:`render class` as the namespace and initialized with the namespace. TIP: ``+namespace`` is shorthand for :py:meth:`namespace.to_render_args() <to_render_args>`. """ return RenderArgs(type(self)._RENDER_CLS, self)
[docs] def __ror__(self, other: ArgsNamespace | RenderArgs) -> RenderArgs: """Same as :py:meth:`__or__` but with reflected operands. NOTE: Unlike :py:meth:`__or__`, if *other* is a render argument namespace associated with the same :term:`render class` as *self*, *self* takes precedence. """ # Not commutative if isinstance(other, ArgsNamespace) and ( type(self)._RENDER_CLS is type(other)._RENDER_CLS ): return RenderArgs(type(self)._RENDER_CLS, self) return type(self).__or__(self, other) # All other cases are commutative
[docs] def as_dict(self) -> dict[str, Any]: """Copies the namespace as a dictionary. Returns: A dictionary mapping field names to their values. WARNING: The number and order of fields are guaranteed to be the same for a namespace class that defines fields, its subclasses, and all their instances; but beyond this, should not be relied upon as the details (such as the specific number or order) may change without notice. The order is an implementation detail of the Render Arguments/Data API and the number should be considered an implementation detail of the specific namespace subclass. """ return {name: getattr(self, name) for name in type(self)._FIELDS}
[docs] @classmethod def get_fields(cls) -> Mapping[str, Any]: """Returns the field definitions. Returns: A mapping of field names to their default values. WARNING: The number and order of fields are guaranteed to be the same for a namespace class that defines fields, its subclasses, and all their instances; but beyond this, should not be relied upon as the details (such as the specific number or order) may change without notice. The order is an implementation detail of the Render Arguments/Data API and the number should be considered an implementation detail of the specific namespace subclass. """ return cls._FIELDS
[docs] def to_render_args(self, render_cls: type[Renderable] | None = None) -> RenderArgs: """Creates a set of render arguments from the namespace. Args: render_cls: A :term:`render class`, with which the namespace is compatible [#an-com]_. Returns: A set of render arguments associated with *render_cls* (or the associated [#an-ass]_ render class of the namespace, if ``None``) and initialized with the namespace. Propagates exceptions raised by the :py:class:`~term_image.renderable.RenderArgs` constructor, as applicable. .. seealso:: :py:meth:`__pos__`. """ return RenderArgs(render_cls or type(self)._RENDER_CLS, self)
[docs] def update(self, **fields: Any) -> Self: """Updates render argument fields. Args: fields: Render argument fields. Returns: A namespace with the given fields updated. Raises: UnknownArgsFieldError: Unknown field name(s). """ if not fields: return self unknown = fields.keys() - type(self)._FIELDS.keys() if unknown: raise UnknownArgsFieldError( f"Unknown render argument field(s) {tuple(unknown)} for " f"{type(self)._RENDER_CLS.__name__!r}" ) new = type(self).__new__(type(self)) new_fields = self.as_dict() new_fields.update(fields) super(ArgsNamespace, new).__init__(new_fields) return new
class DataNamespaceMeta(ArgsDataNamespaceMeta): """Metaclass of render data namespaces.""" _FIELDS: MappingProxyType[str, None] def __new__( cls: type[DataNamespaceMetaT], name: str, bases: tuple[type[DataNamespaceMeta]], namespace: dict[str, Any], *, _base: bool = False, **kwargs: Any, ) -> DataNamespaceMetaT: data_cls = super().__new__(cls, name, bases, namespace, _base=_base, **kwargs) if not _base and (render_cls := data_cls.__dict__.get("_RENDER_CLS")): if render_cls._Data_: raise RenderDataError( f"{render_cls.__name__!r} already has an associated render " f"data namespace class {render_cls._Data_.__name__!r}" ) render_cls._Data_ = data_cls render_cls._RENDER_DATA_MRO = MappingProxyType( {render_cls: data_cls, **render_cls._RENDER_DATA_MRO} ) return data_cls
[docs] class DataNamespace(ArgsDataNamespace, metaclass=DataNamespaceMeta, _base=True): """DataNamespace() :term:`Render class`\\ -specific render data namespace. Raises: UnassociatedNamespaceError: The namespace class hasn't been associated [#dn-ass]_ with a render class. Subclassing, defining (and inheriting) fields and associating with a render class are just as they are for :ref:`args-namespace`, except that values assigned to class attributes are neither required nor used. Every field of a namespace is **uninitialized** immediately after instantiation. The fields are expected to be initialized within the :py:meth:`~term_image.renderable.Renderable._get_render_data_` method of the render class with which the namespace is associated [#dn-ass]_ or at some other point during a render operation, if necessary. NOTE: * Fields are exposed as instance attributes. * Instances are mutable and fields can be updated **in-place**, either individually by assignment to an attribute reference or in batch via :py:meth:`update`. * An instance shouldn't be copied by any means because finalizing its containing :py:class:`RenderData` instance may invalidate all copies of the namespace. .. Completed in /docs/source/api/renderable.rst """ _FIELDS: ClassVar[MappingProxyType[str, None]] def __init__(self) -> None: pass def __repr__(self) -> str: fields_repr = {} for name in type(self)._FIELDS: try: fields_repr[name] = repr(getattr(self, name)) except UninitializedDataFieldError: fields_repr[name] = "<uninitialized>" return "".join( ( f"<{type(self).__name__}: ", ", ".join( f"{name}={value_repr}" for name, value_repr in fields_repr.items() ), ">", ) ) def __getattr__(self, name: str) -> Never: if name in type(self)._FIELDS: raise UninitializedDataFieldError( f"The render data field {name!r} of " f"{type(self)._RENDER_CLS.__name__!r} has not been initialized" ) raise UnknownDataFieldError( f"Unknown render data field {name!r} for " f"{type(self)._RENDER_CLS.__name__!r}" ) def __setattr__(self, name: str, value: Any) -> None: try: super().__setattr__(name, value) except AttributeError: raise UnknownDataFieldError( f"Unknown render data field {name!r} for " f"{type(self)._RENDER_CLS.__name__!r}" ) from None
[docs] def as_dict(self) -> dict[str, Any]: """Copies the namespace as a dictionary. Returns: A dictionary mapping field names to their current values. Raises: UninitializedDataFieldError: A field has not been initialized. WARNING: The number and order of fields are guaranteed to be the same for a namespace class that defines fields, its subclasses, and all their instances; but beyond this, should not be relied upon as the details (such as the specific number or order) may change without notice. The order is an implementation detail of the Render Arguments/Data API and the number should be considered an implementation detail of the specific namespace subclass. """ return {name: getattr(self, name) for name in type(self)._FIELDS}
[docs] @classmethod def get_fields(cls) -> tuple[str, ...]: """Returns the field names. Returns: A tuple of field names. WARNING: The number and order of fields are guaranteed to be the same for a namespace class that defines fields, its subclasses, and all their instances; but beyond this, should not be relied upon as the details (such as the specific number or order) may change without notice. The order is an implementation detail of the Render Arguments/Data API and the number should be considered an implementation detail of the specific namespace subclass. """ return tuple(cls._FIELDS)
[docs] def update(self, **fields: Any) -> None: """Updates render data fields. Args: fields: Render data fields. Raises: UnknownDataFieldError: Unknown field name(s). """ if fields: unknown = fields.keys() - type(self)._FIELDS.keys() if unknown: raise UnknownDataFieldError( f"Unknown render data field(s) {tuple(unknown)} for " f"{type(self)._RENDER_CLS.__name__!r}" ) setattr_ = super().__setattr__ for field in fields.items(): setattr_(*field)
[docs] class Frame(NamedTuple): """A rendered frame. TIP: - Instances are immutable and hashable. - Instances with equal fields compare equal. WARNING: Even though this class inherits from :py:class:`tuple`, the class and its instances should not be used as such, because: * this is an implementation detail, * the number or order of fields may change. Any change to this aspect of the interface may happen without notice and will not be considered a breaking change. """ number: int """Frame number The number of the rendered frame (a non-negative integer), if the frame was rendered by a renderable with *definite* frame count. Otherwise, the value range and meaning of this field is unspecified. """ duration: int """Frame duration (in **milliseconds**) The duration of the rendered frame (a non-negative integer), if the frame was rendered by an **animated** renderable. Otherwise, the value range and meaning of this field is unspecified. HINT: For animated renderables, a zero value indicates that the next frame should be displayed immediately after (without any delay), except stated otherwise. """ render_size: geometry.Size """Frame :term:`render size`""" render_output: str """Frame :term:`render output`"""
[docs] def __str__(self) -> str: """Returns the frame :term:`render output`. Returns: The frame render output, :py:attr:`render_output`. """ return self.render_output
class RenderArgsData: """Render arguments/data baseclass.""" __slots__ = ("render_cls", "_namespaces") render_cls: type[Renderable] _namespaces: MappingProxyType[type[Renderable], ArgsDataNamespace] def __init__( self, render_cls: type[Renderable], namespaces: Mapping[type[Renderable], ArgsDataNamespace], ) -> None: self.render_cls = render_cls self._namespaces = MappingProxyType(namespaces)
[docs] class RenderArgs(RenderArgsData): """RenderArgs(render_cls, /, *namespaces) RenderArgs(render_cls, init_render_args, /, *namespaces) Render arguments. Args: render_cls: A :term:`render class`. init_render_args (RenderArgs | None): A set of render arguments. If not ``None``, * it must be compatible [#ra-com]_ with *render_cls*, * it'll be used to initialize the render arguments. namespaces: Render argument namespaces compatible [#an-com]_ with *render_cls*. .. note:: If multiple namespaces associated with the same :term:`render class` are given, the last of them takes precedence. Raises: IncompatibleRenderArgsError: *init_render_args* is incompatible [#ra-com]_ with *render_cls*. IncompatibleArgsNamespaceError: Incompatible [#an-com]_ render argument namespace. A set of render arguments (an instance of this class) is basically a container of render argument namespaces (instances of :py:class:`~term_image.renderable.ArgsNamespace`); one for each :term:`render class`, which has render arguments, in the Method Resolution Order of its associated [#ra-ass]_ render class. The namespace for each render class is derived from the following sources, in [descending] order of precedence: * *namespaces* * *init_render_args* * default render argument namespaces NOTE: Instances are immutable but updated copies can be created via :py:meth:`update`. .. seealso:: :py:attr:`~term_image.renderable.Renderable.Args` Render class-specific render arguments. .. Completed in /docs/source/api/renderable.rst """ # Class Attributes ========================================================= __slots__ = () _interned: ClassVar[dict[type[Renderable], Self]] = {} _namespaces: MappingProxyType[type[Renderable], ArgsNamespace] # Instance Attributes ====================================================== render_cls: type[Renderable] """The associated :term:`render class`""" # Special Methods ========================================================== def __new__( cls, render_cls: type[Renderable], init_or_namespace: RenderArgs | ArgsNamespace | None = None, *namespaces: ArgsNamespace, ) -> Self: if init_or_namespace is None: init_render_args = None elif isinstance(init_or_namespace, RenderArgs): init_render_args = init_or_namespace else: init_render_args = None namespaces = (init_or_namespace, *namespaces) if init_render_args and not issubclass(render_cls, init_render_args.render_cls): raise IncompatibleRenderArgsError( f"'init_render_args' (associated with " f"{init_render_args.render_cls.__name__!r}) is incompatible with " f"{render_cls.__name__!r} " ) if not namespaces: # has default namespaces only if ( not init_render_args or init_render_args is BASE_RENDER_ARGS or cls._interned.get(init_render_args.render_cls) is init_render_args ): try: return cls._interned[render_cls] # type: ignore[return-value] except KeyError: pass if ( init_render_args and type(init_render_args) is cls and init_render_args.render_cls is render_cls ): return init_render_args return super().__new__(cls) def __init__( self, render_cls: type[Renderable], init_or_namespace: RenderArgs | ArgsNamespace | None = None, *namespaces: ArgsNamespace, ) -> None: if init_or_namespace is None: init_render_args = None elif isinstance(init_or_namespace, RenderArgs): init_render_args = init_or_namespace else: init_render_args = None namespaces = (init_or_namespace, *namespaces) # has default namespaces only if not namespaces and ( not init_render_args or init_render_args is BASE_RENDER_ARGS or ( type(self)._interned.get(init_render_args.render_cls) is init_render_args ) ): if render_cls in type(self)._interned: # has been initialized return intern = True else: intern = False if init_render_args is self: return namespaces_dict = render_cls._ALL_DEFAULT_ARGS.copy() # if `init_render_args` is non-default if init_render_args and BASE_RENDER_ARGS is not init_render_args is not ( type(self)._interned.get(init_render_args.render_cls) ): namespaces_dict.update(init_render_args._namespaces) for index, namespace in enumerate(namespaces): if namespace._RENDER_CLS not in namespaces_dict: raise IncompatibleArgsNamespaceError( f"'namespaces[{index}]' (associated with " f"{namespace._RENDER_CLS.__name__!r}) is incompatible with " f"{render_cls.__name__!r} " ) namespaces_dict[namespace._RENDER_CLS] = namespace super().__init__(render_cls, namespaces_dict) if intern: type(self)._interned[render_cls] = self def __init_subclass__(cls, **kwargs: Any) -> None: cls._interned = {} super().__init_subclass__(**kwargs)
[docs] def __contains__(self, namespace: ArgsNamespace) -> bool: """Checks if a render argument namespace is contained in this set of render arguments. Args: namespace: A render argument namespace. Returns: ``True`` if the given namespace is equal to any of the namespaces contained in this set of render arguments. Otherwise, ``False``. """ return None is not self._namespaces.get(namespace._RENDER_CLS) == namespace
[docs] def __eq__(self, other: object) -> bool: """Compares this set of render arguments with another. Args: other: Another set of render arguments. Returns: ``True`` if both are associated with the same :term:`render class` and have equal argument values. Otherwise, ``False``. """ if isinstance(other, RenderArgs): return ( self is other or self.render_cls is other.render_cls and self._namespaces == other._namespaces ) return NotImplemented
[docs] def __getitem__(self, render_cls: type[Renderable]) -> Any: """Returns a constituent namespace. Args: render_cls: A :term:`render class` of which :py:attr:`render_cls` is a subclass (which may be :py:attr:`render_cls` itself) and which has render arguments. Returns: The constituent namespace associated with *render_cls*. Raises: TypeError: *render_cls* is not a render class. ValueError: :py:attr:`render_cls` is not a subclass of *render_cls*. NoArgsNamespaceError: *render_cls* has no render arguments. NOTE: The return type hint is :py:class:`~typing.Any` only for compatibility with static type checking, as this aspect of the interface seems too dynamic to be correctly expressed with the exisiting type system. Regardless, the return value is guaranteed to always be an instance of a render argument namespace class associated with *render_cls*. For instance, the following would always be valid for :py:class:`~term_image.renderable.Renderable`:: renderable_args: RenderableArgs = render_args[Renderable] and similar idioms are encouraged to be used for other render classes. """ try: return self._namespaces[render_cls] except TypeError: raise arg_type_error("render_cls", render_cls) from None except KeyError: if not isinstance(render_cls, RenderableMeta): raise arg_type_error("render_cls", render_cls) from None if issubclass(self.render_cls, render_cls): raise NoArgsNamespaceError( f"{render_cls.__name__!r} has no render arguments" ) from None raise ValueError( f"{self.render_cls.__name__!r} is not a subclass of " f"{render_cls.__name__!r}" ) from None
[docs] def __hash__(self) -> int: """Computes the hash of the render arguments. Returns: The computed hash. IMPORTANT: Like tuples, an instance is hashable if and only if the constituent namespaces are hashable. """ # Namespaces are always in the same order, wrt their respective associated # render classes, for all instances associated with the same render class. return hash((self.render_cls, tuple(self._namespaces.values())))
[docs] def __iter__(self) -> Iterator[ArgsNamespace]: """Returns an iterator that yields the constituent namespaces. Returns: An iterator that yields the constituent namespaces. WARNING: The number and order of namespaces is guaranteed to be the same across all instances associated [#ra-ass]_ with the same :term:`render class` but beyond this, should not be relied upon as the details (such as the specific number or order) may change without notice. The order is an implementation detail of the Renderable API and the number should be considered alike with respect to the associated render class. """ return iter(self._namespaces.values())
def __repr__(self) -> str: return "".join( ( f"{type(self).__name__}({self.render_cls.__name__}", ", " if self._namespaces else "", ", ".join(map(repr, self._namespaces.values())), ")", ) ) # Public Methods ===========================================================
[docs] def convert(self, render_cls: type[Renderable]) -> RenderArgs: """Converts the set of render arguments to one for a related render class. Args: render_cls: A :term:`render class` of which :py:attr:`render_cls` is a parent or child (which may be :py:attr:`render_cls` itself). Returns: A set of render arguments associated [#ra-ass]_ with *render_cls* and initialized with all constituent namespaces (of this set of render arguments, *self*) that are compatible [#an-com]_ with *render_cls*. Raises: ValueError: *render_cls* is not a parent or child of :py:attr:`render_cls`. """ if render_cls is self.render_cls: return self if issubclass(render_cls, self.render_cls): return RenderArgs(render_cls, self) if issubclass(self.render_cls, render_cls): render_cls_args_mro = render_cls._ALL_DEFAULT_ARGS return RenderArgs( render_cls, *[ namespace for cls, namespace in self._namespaces.items() if cls in render_cls_args_mro ], ) raise ValueError( f"{render_cls.__name__!r} is not a parent or child of " f"{self.render_cls.__name__!r}" )
@overload def update( self, namespace: ArgsNamespace, /, *namespaces: ArgsNamespace, ) -> RenderArgs: ... @overload def update( self, render_cls: type[Renderable], /, **fields: Any, ) -> RenderArgs: ...
[docs] def update( self, render_cls_or_namespace: type[Renderable] | ArgsNamespace, /, *namespaces: ArgsNamespace, **fields: Any, ) -> RenderArgs: """update(namespace, /, *namespaces) -> RenderArgs update(render_cls, /, **fields) -> RenderArgs Replaces or updates render argument namespaces. Args: namespace (ArgsNamespace): Prepended to *namespaces*. namespaces: Render argument namespaces compatible [#an-com]_ with :py:attr:`render_cls`. .. note:: If multiple namespaces associated with the same :term:`render class` are given, the last of them takes precedence. render_cls (type[Renderable]): A :term:`render class` of which :py:attr:`render_cls` is a subclass (which may be :py:attr:`render_cls` itself) and which has render arguments. fields: Render argument fields. The keywords must be names of render argument fields for *render_cls*. Returns: For the **first** form, an instance with the namespaces for the respective associated :term:`render classes` of the given namespaces replaced. For the **second** form, an instance with the given render argument fields for *render_cls* updated, if any. Raises: TypeError: The arguments given do not conform to any of the expected forms. Propagates exceptions raised by: * the class constructor, for the **first** form. * :py:meth:`__getitem__` and :py:meth:`ArgsNamespace.update() <term_image.renderable.ArgsNamespace.update>`, for the **second** form. """ if isinstance(render_cls_or_namespace, RenderableMeta): if namespaces: raise TypeError( "No other positional argument is expected when the first argument " "is a render class" ) render_cls = render_cls_or_namespace else: if fields: raise TypeError( "No keyword argument is expected when the first argument is " "a render argument namespace" ) render_cls = None namespaces = (render_cls_or_namespace, *namespaces) return RenderArgs( self.render_cls, self, *((self[render_cls].update(**fields),) if render_cls else namespaces), )
[docs] class RenderData(RenderArgsData): """Render data. Args: render_cls: A :term:`render class`. An instance of this class is basically a container of render data namespaces (instances of :py:class:`~term_image.renderable.DataNamespace`); one for each :term:`render class`, which has render data, in the Method Resolution Order of its associated [#rd-ass]_ render class. NOTE: * Instances are immutable but the constituent namespaces are mutable. * Instances and their contents shouldn't be copied by any means because finalizing an instance may invalidate all other copies. * Instances should always be explicitly finalized as soon as they're no longer needed. .. seealso:: :py:attr:`~term_image.renderable.Renderable._Data_` Render class-specific render data. """ # Class Attributes ========================================================= __slots__ = ("finalized",) # Instance Attributes ====================================================== finalized: bool """Finalization status""" render_cls: type[Renderable] """The associated :term:`render class`""" _namespaces: MappingProxyType[type[Renderable], DataNamespace] # Special Methods ========================================================== def __init__(self, render_cls: type[Renderable]) -> None: super().__init__( render_cls, {cls: data_cls() for cls, data_cls in render_cls._RENDER_DATA_MRO.items()}, ) self.finalized = False def __del__(self) -> None: try: self.finalize() except AttributeError: # Unsuccessful initialization pass
[docs] def __getitem__(self, render_cls: type[Renderable]) -> Any: """Returns a constituent namespace. Args: render_cls: A :term:`render class` of which :py:attr:`render_cls` is a subclass (which may be :py:attr:`render_cls` itself) and which has render data. Returns: The constituent namespace associated with *render_cls*. Raises: TypeError: *render_cls* is not a render class. ValueError: :py:attr:`render_cls` is not a subclass of *render_cls*. NoDataNamespaceError: *render_cls* has no render data. NOTE: The return type hint is :py:class:`~typing.Any` only for compatibility with static type checking, as this aspect of the interface seems too dynamic to be correctly expressed with the exisiting type system. Regardless, the return value is guaranteed to always be an instance of a render data namespace class associated with *render_cls*. For instance, the following would always be valid for :py:class:`~term_image.renderable.Renderable`:: renderable_data: RenderableData = render_data[Renderable] and similar idioms are encouraged to be used for other render classes. """ try: return self._namespaces[render_cls] except TypeError: raise arg_type_error("render_cls", render_cls) from None except KeyError: if not isinstance(render_cls, RenderableMeta): raise arg_type_error("render_cls", render_cls) from None if issubclass(self.render_cls, render_cls): raise NoDataNamespaceError( f"{render_cls.__name__!r} has no render data" ) from None raise ValueError( f"{self.render_cls.__name__!r} is not a subclass of " f"{render_cls.__name__!r}" ) from None
[docs] def __iter__(self) -> Iterator[DataNamespace]: """Returns an iterator that yields the constituent namespaces. Returns: An iterator that yields the constituent namespaces. WARNING: The number and order of namespaces is guaranteed to be the same across all instances associated [#rd-ass]_ with the same :term:`render class` but beyond this, should not be relied upon as the details (such as the specific number or order) may change without notice. The order is an implementation detail of the Renderable API and the number should be considered alike with respect to the associated render class. """ return iter(self._namespaces.values())
def __repr__(self) -> str: return "".join( ( f"<{type(self).__name__}({self.render_cls.__name__})", ": " if self._namespaces else "", ", ".join(map(repr, self._namespaces.values())), ">", ) ) # Public Methods ===========================================================
[docs] def finalize(self) -> None: """Finalizes the render data. Calls :py:meth:`~term_image.renderable.Renderable._finalize_render_data_` of :py:attr:`render_cls`. NOTE: This method is safe for multiple invocations on the same instance. """ if not self.finalized: try: self.render_cls._finalize_render_data_(self) finally: self.finalized = True
# Variables ==================================================================== BASE_RENDER_ARGS = RenderArgs.__new__(RenderArgs, None) # type: ignore[arg-type]