Source code for flare.components.base

from __future__ import annotations

import abc
import copy
import hashlib
import typing as t

import hikari
import hikari.components

from flare import dataclass
from flare.exceptions import CustomIDNotSetError, SerializerError
from flare.internal import bootstrap

if t.TYPE_CHECKING:
    from flare import row
    from flare.components.button import Button
    from flare.components.select import (
        ChannelSelect,
        MentionableSelect,
        RoleSelect,
        TextSelect,
        UserSelect,
    )
    from flare.context import MessageContext, PartialContext

__all__: t.Final[t.Sequence[str]] = ("Component", "SupportsCookie", "CallbackComponent")

ComponentBuilderT = t.TypeVar("ComponentBuilderT", bound=hikari.api.ComponentBuilder)
PartialContextT = t.TypeVar("PartialContextT", bound="PartialContext[t.Any]", contravariant=True)
P = t.ParamSpec("P")

CallbackComponentT = t.TypeVar("CallbackComponentT", bound="CallbackComponent")


[docs] class Component(abc.ABC, t.Generic[ComponentBuilderT]):
[docs] @abc.abstractmethod def build(self, action_row: ComponentBuilderT) -> t.Any: """Build and append a flare component to a hikari action row.""" ...
@property @abc.abstractmethod def width(self) -> int: """ The width of the component. """ ... @property @abc.abstractmethod def custom_id(self) -> str: """A custom_id for a component.""" ...
[docs] class SupportsCookie(abc.ABC): @property @abc.abstractmethod def cookie(self) -> str: """A unique identifier for a component.""" ...
class SupportsCallback(t.Protocol[PartialContextT]): async def callback(self, ctx: PartialContextT) -> None: raise NotImplementedError def write_cookie(s: str) -> str: return hashlib.blake2s(s.encode("latin1"), digest_size=8).digest().decode("latin1")
[docs] class CallbackComponent( Component[hikari.api.MessageActionRowBuilder], SupportsCallback["MessageContext"], SupportsCookie, dataclass.Dataclass, ): """ An abstract class that all components with callbacks are derive from. """ _cookie: t.ClassVar[str] def __init_subclass__( cls, cookie: str | None = None, _dataclass_fields: list[dataclass.Field] | None = None, ) -> None: super().__init_subclass__(_dataclass_fields) cls._cookie = cookie or write_cookie(f"{cls.__name__}.{cls.__module__}") bootstrap.components[cls._cookie] = cls def __post_init__(self) -> None: self._custom_id: str | None = None @property def custom_id(self) -> str: """ The custom ID of the component. """ if not self._custom_id: raise CustomIDNotSetError(f"The row containing `{self.__class__.__name__}` must be awaited.") return self._custom_id async def set_custom_id(self): self._custom_id = await bootstrap.active_serde.serialize( self._cookie, self._dataclass_annotations, self._dataclass_values ) @property def cookie(self) -> str: return self._cookie
[docs] @staticmethod async def from_partial(component: hikari.PartialComponent) -> CallbackComponent: """ Build a flare component from `hikari.PartialComponent`. Args: component: A partial component. The component type must be `hikari ComponentType.BUTTON` or `hikari.ComponentType.SELECT_MENU`. Returns: A component. Raises: SerializerError: The component could not be deserialized. """ if not isinstance(component, (hikari.ButtonComponent, hikari.components.SelectMenuComponent)): raise SerializerError(f"Flare component type can not be {component.type}") assert component.custom_id try: flare_component, kwargs = await bootstrap.active_serde.deserialize( component.custom_id, bootstrap.components ) except SerializerError: raise component_inst = flare_component(**kwargs) if isinstance(component, hikari.ButtonComponent): if t.TYPE_CHECKING: assert isinstance(component_inst, Button) component_inst.set_label(component.label).set_emoji(component.emoji).set_style( hikari.ButtonStyle(component.style) ).set_disabled(component.is_disabled) else: if t.TYPE_CHECKING: assert isinstance( component_inst, TextSelect | RoleSelect | UserSelect | MentionableSelect | ChannelSelect ) ( component_inst.set_max_values(component.max_values) .set_min_values(component.min_values) .set_placeholder(component.placeholder or hikari.UNDEFINED) .set_disabled(component.is_disabled) ) if isinstance(component, hikari.components.TextSelectMenuComponent): if t.TYPE_CHECKING: assert isinstance(component_inst, TextSelect) component_inst.set_options(*((option.label, option.value) for option in component.options)) if isinstance(component, hikari.components.ChannelSelectMenuComponent): if t.TYPE_CHECKING: assert isinstance(component_inst, ChannelSelect) component_inst.set_channel_types(*(hikari.ChannelType(c) for c in component.channel_types)) return component_inst
def _clone(self: CallbackComponentT) -> CallbackComponentT: return copy.copy(self)
[docs] def get_from(self: CallbackComponentT, rows: t.Sequence[row.Row]) -> t.Sequence[CallbackComponentT]: """ Return all instances of this component that appear in :class:`typing.Sequence[flare.row.Row]`. Args: rows: The rows to search through. Returns: A list of all components of this type that appear in ``rows``. """ out: list[CallbackComponentT] = [] for row in rows: for component in row: if isinstance(component, type(self)) and component.cookie == self.cookie: out.append(component) return out
# MIT License # # Copyright (c) 2022-present Lunarmagpie # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE.