Skip to content

fix!: support registering component to multiple managers #39

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Mar 4, 2025
1 change: 1 addition & 0 deletions changelog/39.breaking.0.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Remove the ``RichComponent.manager`` class variable and replace it with new :meth:`.RichComponent.get_manager` and :meth:`.RichComponent.set_manager` instance methods.
1 change: 1 addition & 0 deletions changelog/39.breaking.1.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Remove the ``RichComponent.factory`` class variable and replace it with new :meth:`.RichComponent.get_factory` and :meth:`.RichComponent.set_factory` class methods.
1 change: 1 addition & 0 deletions changelog/39.deprecate.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Deprecate ``ComponentManager.parse_message_interaction`` in favour of :meth:`.ComponentManager.parse_raw_component`.
1 change: 1 addition & 0 deletions changelog/39.feature.0.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support :obj:`~.ComponentManager.register`\ing the same component to multiple :class:`.ComponentManager`\s.
1 change: 1 addition & 0 deletions changelog/39.feature.1.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Make the :class:`.RichComponent` :class:`~typing.Protocol` runtime-checkable.
1 change: 1 addition & 0 deletions changelog/39.feature.2.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a new :obj:`~.FieldType.META` :class:`.FieldType` for component fields that should *not* be stored in the custom ID (:obj:`.FieldType.CUSTOM_ID`) and should *not* be inferred from the raw disnake component (:obj:`.FieldType.INTERNAL`).
2 changes: 1 addition & 1 deletion docs/source/api_ref/impl/component/button.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ Classes
.. attributetable:: disnake_compass.impl.component.button.RichButton

.. autoclass:: disnake_compass.impl.component.button.RichButton
:members:
:members: disabled, emoji, label, style, as_ui_component, callback, get_factory, get_manager, make_custom_id, set_factory, set_manager
12 changes: 6 additions & 6 deletions docs/source/api_ref/impl/component/select.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,29 @@ Classes
.. attributetable:: disnake_compass.impl.component.select.BaseSelect

.. autoclass:: disnake_compass.impl.component.select.BaseSelect
:members: disabled, factory, max_values, min_values, placeholder, as_ui_component, callback
:members: disabled, max_values, min_values, placeholder, as_ui_component, callback, get_factory, get_manager, make_custom_id, set_factory, set_manager

.. attributetable:: disnake_compass.impl.component.select.RichStringSelect

.. autoclass:: disnake_compass.impl.component.select.RichStringSelect
:members: disabled, factory, options, max_values, min_values, placeholder, as_ui_component, callback
:members: disabled, options, max_values, min_values, placeholder, as_ui_component, callback, get_factory, get_manager, make_custom_id, set_factory, set_manager

.. attributetable:: disnake_compass.impl.component.select.RichUserSelect

.. autoclass:: disnake_compass.impl.component.select.RichUserSelect
:members: disabled, factory, max_values, min_values, placeholder, as_ui_component, callback
:members: disabled, max_values, min_values, placeholder, as_ui_component, callback, get_factory, get_manager, make_custom_id, set_factory, set_manager

.. attributetable:: disnake_compass.impl.component.select.RichRoleSelect

.. autoclass:: disnake_compass.impl.component.select.RichRoleSelect
:members: disabled, factory, max_values, min_values, placeholder, as_ui_component, callback
:members: disabled, max_values, min_values, placeholder, as_ui_component, callback, get_factory, get_manager, make_custom_id, set_factory, set_manager

.. attributetable:: disnake_compass.impl.component.select.RichMentionableSelect

.. autoclass:: disnake_compass.impl.component.select.RichMentionableSelect
:members: disabled, factory, max_values, min_values, placeholder, as_ui_component, callback
:members: disabled, max_values, min_values, placeholder, as_ui_component, callback, get_factory, get_manager, make_custom_id, set_factory, set_manager

.. attributetable:: disnake_compass.impl.component.select.RichChannelSelect

.. autoclass:: disnake_compass.impl.component.select.RichChannelSelect
:members: channel_types, disabled, factory, max_values, min_values, placeholder, as_ui_component, callback
:members: channel_types, disabled, max_values, min_values, placeholder, as_ui_component, callback, get_factory, get_manager, make_custom_id, set_factory, set_manager
2 changes: 1 addition & 1 deletion docs/source/api_ref/impl/factory.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.. currentmodule:: disnake_compass

Component Manager Implementation
Component Factory Implementation
================================

.. automodule:: disnake_compass.impl.factory
Expand Down
1 change: 1 addition & 0 deletions docs/source/api_ref/impl/manager.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ Classes

.. autoclass:: disnake_compass.impl.manager.ComponentManager
:members:
:exclude-members: parse_message_interaction
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ git = "https://github.com/sharp-eyes/furo"
typeCheckingMode = "strict"
pythonVersion = "3.10"
reportMissingTypeStubs = false
# Disabling this only disables the use of bare `type: ignore` comments.
enableTypeIgnoreComments = false
exclude = [
# Default options
"**/node_modules",
Expand Down
67 changes: 30 additions & 37 deletions src/disnake_compass/api/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,23 @@ class RichComponent(typing.Protocol):

__slots__: typing.Sequence[str] = ()

event: typing.ClassVar[str]
factory: typing.ClassVar[ComponentFactory[RichComponent]]
manager: typing.ClassVar[ComponentManager | None]
def get_manager(self) -> ComponentManager:
"""Get the manager that was responsible for parsing this component instance."""
...

def set_manager(self, manager: ComponentManager, /) -> None:
"""Set the manager that was responsible for parsing this component instance."""
...

@classmethod
def get_factory(cls) -> ComponentFactory[typing_extensions.Self]:
"""Get the factory that built this component instance."""
...

@classmethod
def set_factory(cls, factory: ComponentFactory[typing_extensions.Self]) -> None:
"""Set the factory that built this component instance."""
...

async def callback(self, interaction: disnake.Interaction[disnake.Client], /) -> None:
"""Run the component callback.
Expand All @@ -57,7 +71,9 @@ async def callback(self, interaction: disnake.Interaction[disnake.Client], /) ->
"""
...

async def as_ui_component(self) -> disnake.ui.WrappedComponent:
async def as_ui_component(
self, manager: ComponentManager | None = None, /
) -> disnake.ui.WrappedComponent:
"""Convert this component into a component that can be sent by disnake.

Returns
Expand Down Expand Up @@ -98,7 +114,9 @@ class RichButton(RichComponent, typing.Protocol):
Disabled buttons can therefore not cause any interactions, either.
"""

async def as_ui_component(self) -> disnake.ui.Button[None]: # noqa: D102
async def as_ui_component( # noqa: D102
self, manager: ComponentManager | None = None, /
) -> disnake.ui.Button[None]:
# <<Docstring inherited from RichComponent>>
...

Expand Down Expand Up @@ -133,12 +151,13 @@ class RichSelect(RichComponent, typing.Protocol):
"""

async def as_ui_component( # noqa: D102
self,
self, manager: ComponentManager | None = None, /
) -> disnake.ui.BaseSelect[typing.Any, typing.Any, None]:
# <<Docstring inherited from RichComponent>>
...


@typing.runtime_checkable
class ComponentManager(typing.Protocol):
"""The baseline protocol for component managers.

Expand Down Expand Up @@ -186,7 +205,7 @@ def parent(self) -> ComponentManager | None:
"""
...

def make_identifier(self, component_type: type[RichComponent]) -> str:
def make_identifier(self, component_type: type[RichComponent], /) -> str:
"""Make an identifier for the provided component class.

This is used to store the component in :attr:`components`, and to
Expand All @@ -206,7 +225,7 @@ def make_identifier(self, component_type: type[RichComponent]) -> str:
"""
...

def get_identifier(self, custom_id: str) -> tuple[str, typing.Sequence[str]]:
def get_identifier(self, custom_id: str, /) -> tuple[str, typing.Sequence[str]]:
"""Extract the identifier and parameters from a custom id.

This is used to check whether the identifier is registered in
Expand All @@ -220,7 +239,7 @@ def get_identifier(self, custom_id: str) -> tuple[str, typing.Sequence[str]]:
"""
...

async def make_custom_id(self, component: RichComponent) -> str:
async def make_custom_id(self, component: RichComponent, /) -> str:
"""Make a custom id from the provided component.

This can then be used later to reconstruct the component without any
Expand All @@ -239,36 +258,10 @@ async def make_custom_id(self, component: RichComponent) -> str:
"""
...

async def parse_message_interaction(
self,
interaction: disnake.MessageInteraction[disnake.Client],
) -> RichComponent | None:
"""Parse an interaction and construct a rich component from it.

In case the interaction does not match any component registered to this
manager, this method will simply return :data:`None`.

Parameters
----------
interaction
The interaction to parse. This should, under normal circumstances,
be either a :class:`disnake.MessageInteraction` or
:class:`disnake.ModalInteraction`.

Returns
-------
:class:`RichComponent`
The component if the interaction was caused by a component
registered to this manager.
:data:`None`
The interaction was not related to this manager.

"""
...

def register_component(
self,
component_type: type[ComponentT],
/,
) -> type[ComponentT]:
r"""Register a component to this component manager.

Expand All @@ -288,7 +281,7 @@ def register_component(
"""
...

def deregister_component(self, component_type: type[RichComponent]) -> None:
def deregister_component(self, component_type: type[RichComponent], /) -> None:
"""Deregister a component from this component manager.

After deregistration, the component will no be tracked, and its
Expand Down
30 changes: 15 additions & 15 deletions src/disnake_compass/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from __future__ import annotations

import enum
import functools
import typing

import attrs
Expand Down Expand Up @@ -34,26 +33,19 @@ class FieldType(enum.Flag):
the sole reason of facilitating unions in lookups using :func:`get_fields`.
"""

META = enum.auto()
"""Internal field that facilitates disnake-compass functionality."""
INTERNAL = enum.auto()
"""Internal field that does not show up in the component's init signature."""
"""Internal field that interfaces with disnake ui component fields."""
CUSTOM_ID = enum.auto()
"""Field parsed into/from the component's custom id."""
SELECT = enum.auto()
"""Field parsed from a select component's selected values."""
MODAL = enum.auto()
"""Field parsed from a modal component's modal values."""

@classmethod
def ALL(cls) -> FieldType: # noqa: N802
"""Meta-value for all field types.

Mainly intended for use in :func:`get_fields`.
"""
return _ALL_FIELD_TYPES


_ALL_FIELD_TYPES = functools.reduce(FieldType.__or__, FieldType)
_ALL_FIELD_TYPES._name_ = "ALL()" # This makes it render nicer in docs.
ALL = META | INTERNAL | CUSTOM_ID | SELECT | MODAL
"""Meta-value to facilitate checking for any field type."""


def get_parser(
Expand Down Expand Up @@ -144,7 +136,7 @@ def get_fields(
cls: type,
/,
*,
kind: FieldType = _ALL_FIELD_TYPES,
kind: FieldType = FieldType.ALL,
) -> typing.Sequence[attrs.Attribute[typing.Any]]:
r"""Get the attributes of an attrs class.

Expand Down Expand Up @@ -208,7 +200,7 @@ def field(


def internal(
default: _T,
default: _T = attrs.NOTHING,
*,
frozen: bool = False,
) -> _T:
Expand Down Expand Up @@ -242,3 +234,11 @@ def internal(
on_setattr=attrs.setters.frozen if frozen else None,
metadata={FieldMetadata.FIELDTYPE: FieldType.INTERNAL},
)


def meta(*, init: bool = False) -> typing.Any: # noqa: ANN401
return attrs.field(
metadata={FieldMetadata.FIELDTYPE: FieldType.META},
init=init,
repr=False,
)
Loading