Skip to content

Commit

Permalink
Merge pull request #94 from sushi-chaaaan/sushi-chaaaan/issue90
Browse files Browse the repository at this point in the history
Add sync internval option to ViewController Fixes #90
  • Loading branch information
sushichan044 authored Jun 23, 2024
2 parents a12167d + 7128387 commit 6a1ac9b
Show file tree
Hide file tree
Showing 8 changed files with 103 additions and 10 deletions.
1 change: 1 addition & 0 deletions .mise.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
settings.disable_tools = ["python"]
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
"editor.codeActionsOnSave": {
"source.fixAll": "explicit"
},
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff"
},
"python.analysis.typeCheckingMode": "basic",
"python.languageServer": "Pylance"
}
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,4 @@ line-ending = "auto"
allow-direct-references = true

[tool.hatch.build.targets.wheel]
packages = ["src/ductile_ui"]
packages = ["src/ductile"]
45 changes: 41 additions & 4 deletions src/ductile/controller/controller.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import asyncio
from typing import TYPE_CHECKING, Any, Literal, NamedTuple, overload

from ..internal import _InternalView # noqa: TID252
from ..state import State # noqa: TID252
from ..utils import wait_tasks_by_name # noqa: TID252
from ..utils import ( # noqa: TID252
debounce,
wait_tasks_by_name,
)

if TYPE_CHECKING:
from collections.abc import Generator
from collections.abc import Awaitable, Callable, Generator

from discord import Message

Expand All @@ -30,12 +34,16 @@ class ViewResult(NamedTuple):
class ViewController:
"""ViewController is a class that controls the view."""

def __init__(self, view: "View", *, timeout: float | None = 180) -> None:
def __init__(self, view: "View", *, timeout: float | None = 180, sync_interval: float | None = None) -> None:
self.__view = view
view._controller = self # noqa: SLF001
self.__raw_view = _InternalView(timeout=timeout, on_error=self.__view.on_error, on_timeout=self.__view.on_timeout)
self.__message: Message | None = None

# sync_fn is a function that syncs the message with the current view
self.__sync_fn = self.__create_sync_function(sync_interval=sync_interval)
self.__loop = asyncio.get_event_loop()

@property
def message(self) -> "Message | None":
"""
Expand Down Expand Up @@ -65,16 +73,45 @@ async def send(self) -> None:
raise NotImplementedError

async def sync(self) -> None:
"""Sync the message with current view."""
try:
return await self.__sync_fn()
except RuntimeError:
pass

def __create_sync_function(self, *, sync_interval: float | None) -> "Callable[[], Awaitable[None]]":
"""
Create a function that syncs the message with the current view.
Parameters
----------
sync_interval : `float | None`, optional
The interval to sync the message with the current view. If None, the message will be synced immediately.
Returns
-------
Callable[[], Awaitable[None]]
The function that syncs the message with the current view.
**Note that this function returns a same coroutine while debouncing.**
"""
if sync_interval is None:
return self.__sync_immediately

return debounce(wait=sync_interval)(self.__sync_immediately)

async def __sync_immediately(self) -> None:
"""Sync the message with current view."""
if self.message is None:
return

# maybe validation for self.__view is needed
d = self._process_view_for_discord("attachment")
self.message = await self.message.edit(**d)
await self.message.edit(**d)

def stop(self) -> None:
"""Stop the view and return the state of all states in the view."""
self.__loop.create_task(self.__sync_immediately()) # execute last sync before stop
self.__raw_view.stop()

async def wait(self) -> ViewResult:
Expand Down
5 changes: 3 additions & 2 deletions src/ductile/controller/interaction_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,16 @@
class InteractionController(ViewController):
"""InteractionController is a class that controls the view with `discord.abc.Messageable`."""

def __init__(
def __init__( # noqa: PLR0913
self,
view: "View",
*,
interaction: "Interaction",
timeout: float | None = 180,
ephemeral: bool = False,
sync_interval: float | None = None,
) -> None:
super().__init__(view, timeout=timeout)
super().__init__(view, timeout=timeout, sync_interval=sync_interval)
self.__interaction = interaction
self.__ephemeral = ephemeral

Expand Down
11 changes: 9 additions & 2 deletions src/ductile/controller/messageable_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,15 @@
class MessageableController(ViewController):
"""MessageableController is a class that controls the view with `discord.abc.Messageable`."""

def __init__(self, view: "View", *, messageable: "discord.abc.Messageable", timeout: float | None = 180) -> None:
super().__init__(view, timeout=timeout)
def __init__(
self,
view: "View",
*,
messageable: "discord.abc.Messageable",
timeout: float | None = 180,
sync_interval: float | None = None,
) -> None:
super().__init__(view, timeout=timeout, sync_interval=sync_interval)
self.__messageable = messageable

async def send(self) -> None:
Expand Down
4 changes: 3 additions & 1 deletion src/ductile/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
from .async_helper import get_all_tasks, wait_tasks_by_name
from .call import call_any_function
from .chunk import chunks
from .debounce import debounce
from .logger import get_logger
from .type_helper import is_async_func, is_sync_func

__all__ = [
"chunks",
"get_all_tasks",
"wait_tasks_by_name",
"call_any_function",
"chunks",
"debounce",
"get_logger",
"is_async_func",
"is_sync_func",
Expand Down
42 changes: 42 additions & 0 deletions src/ductile/utils/debounce.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import functools
import threading
from collections.abc import Callable
from typing import ParamSpec, TypeVar

P = ParamSpec("P")
R = TypeVar("R")


def debounce(*, wait: float = 10.0) -> Callable[[Callable[P, R]], Callable[P, R]]:
"""
Decorator that limits the execution of a specific function to once every specified seconds.
Parameters
----------
wait : `float`
Seconds to wait before the function can be called again. Defaults to 10.0.
"""

def decorator(fn: Callable[P, R]) -> Callable[P, R]:
last_called = threading.Event()
last_called.set() # set the event to allow the first call
last_result: R # must set by the first call

@functools.wraps(fn)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
nonlocal last_called
nonlocal last_result

if last_called.is_set():
last_called.clear() # clear the event to prevent the next call
result = fn(*args, **kwargs)
last_result = result
threading.Timer(wait, last_called.set).start() # set the event after the specified seconds
return result

# rate limited. returning the last result
return last_result

return wrapper

return decorator

0 comments on commit 6a1ac9b

Please sign in to comment.