From 060bad105dc2569fc485adb03b985aa2ab5d367e Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 22 Feb 2022 23:47:09 +0000 Subject: Move new utilities to the util namespace --- botcore/utils/caching.py | 65 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 botcore/utils/caching.py (limited to 'botcore/utils/caching.py') diff --git a/botcore/utils/caching.py b/botcore/utils/caching.py new file mode 100644 index 00000000..ea71ed1d --- /dev/null +++ b/botcore/utils/caching.py @@ -0,0 +1,65 @@ +"""Utilities related to custom caches.""" + +import functools +import typing +from collections import OrderedDict + + +class AsyncCache: + """ + LRU cache implementation for coroutines. + + Once the cache exceeds the maximum size, keys are deleted in FIFO order. + + An offset may be optionally provided to be applied to the coroutine's arguments when creating the cache key. + """ + + def __init__(self, max_size: int = 128): + """ + Initialise a new AsyncCache instance. + + Args: + max_size: How many items to store in the cache. + """ + self._cache = OrderedDict() + self._max_size = max_size + + def __call__(self, arg_offset: int = 0) -> typing.Callable: + """ + Decorator for async cache. + + Args: + arg_offset: The offset for the position of the key argument. + + Returns: + A decorator to wrap the target function. + """ + + def decorator(function: typing.Callable) -> typing.Callable: + """ + Define the async cache decorator. + + Args: + function: The function to wrap. + + Returns: + The wrapped function. + """ + + @functools.wraps(function) + async def wrapper(*args) -> typing.Any: + """Decorator wrapper for the caching logic.""" + key = args[arg_offset:] + + if key not in self._cache: + if len(self._cache) > self._max_size: + self._cache.popitem(last=False) + + self._cache[key] = await function(*args) + return self._cache[key] + return wrapper + return decorator + + def clear(self) -> None: + """Clear cache instance.""" + self._cache.clear() -- cgit v1.2.3 From 54e3d222deb92ba89072589477114d7d9ceec382 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Thu, 24 Feb 2022 16:12:21 +0000 Subject: Consistently use double backticks when referring to a variable name. Also add sphix-style docstrings to functions that were previously missing them. --- botcore/utils/caching.py | 2 +- botcore/utils/channel.py | 23 +++++++++++++++++++-- botcore/utils/logging.py | 2 +- botcore/utils/scheduling.py | 50 +++++++++++++++++++++++---------------------- 4 files changed, 49 insertions(+), 28 deletions(-) (limited to 'botcore/utils/caching.py') diff --git a/botcore/utils/caching.py b/botcore/utils/caching.py index ea71ed1d..ac34bb9b 100644 --- a/botcore/utils/caching.py +++ b/botcore/utils/caching.py @@ -16,7 +16,7 @@ class AsyncCache: def __init__(self, max_size: int = 128): """ - Initialise a new AsyncCache instance. + Initialise a new :obj:`AsyncCache` instance. Args: max_size: How many items to store in the cache. diff --git a/botcore/utils/channel.py b/botcore/utils/channel.py index db155911..17e70a2a 100644 --- a/botcore/utils/channel.py +++ b/botcore/utils/channel.py @@ -10,7 +10,7 @@ log = logging.get_logger(__name__) def is_in_category(channel: discord.TextChannel, category_id: int) -> bool: """ - Return whether the given channel has the category_id. + Return whether the given ``channel`` in the the category with the id ``category_id``. Args: channel: The channel to check. @@ -23,7 +23,26 @@ def is_in_category(channel: discord.TextChannel, category_id: int) -> bool: async def get_or_fetch_channel(bot: Bot, channel_id: int) -> discord.abc.GuildChannel: - """Attempt to get or fetch a channel and return it.""" + """ + Attempt to get or fetch the given ``channel_id`` from the bots cache, and return it. + + Args: + bot: The :obj:`discord.ext.commands.Bot` instance to use for getting/fetching. + channel_id: The channel to get/fetch. + + Raises: + :exc:`discord.InvalidData` + An unknown channel type was received from Discord. + :exc:`discord.HTTPException` + Retrieving the channel failed. + :exc:`discord.NotFound` + Invalid Channel ID. + :exc:`discord.Forbidden` + You do not have permission to fetch this channel. + + Returns: + The channel from the ID. + """ log.trace(f"Getting the channel {channel_id}.") channel = bot.get_channel(channel_id) diff --git a/botcore/utils/logging.py b/botcore/utils/logging.py index 740c20d4..71ce4e66 100644 --- a/botcore/utils/logging.py +++ b/botcore/utils/logging.py @@ -1,4 +1,4 @@ -"""Custom :obj:`logging.Logger` class that implements a new ``"TRACE"`` level.""" +"""Common logging related functions.""" import logging import typing diff --git a/botcore/utils/scheduling.py b/botcore/utils/scheduling.py index e2952e6c..164f6b10 100644 --- a/botcore/utils/scheduling.py +++ b/botcore/utils/scheduling.py @@ -14,25 +14,26 @@ class Scheduler: """ Schedule the execution of coroutines and keep track of them. - When instantiating a Scheduler, a name must be provided. This name is used to distinguish the + When instantiating a :obj:`Scheduler`, a name must be provided. This name is used to distinguish the instance's log messages from other instances. Using the name of the class or module containing the instance is suggested. - Coroutines can be scheduled immediately with `schedule` or in the future with `schedule_at` - or `schedule_later`. A unique ID is required to be given in order to keep track of the - resulting Tasks. Any scheduled task can be cancelled prematurely using `cancel` by providing - the same ID used to schedule it. The `in` operator is supported for checking if a task with a - given ID is currently scheduled. + Coroutines can be scheduled immediately with :obj:`schedule` or in the future with :obj:`schedule_at` + or :obj:`schedule_later`. A unique ID is required to be given in order to keep track of the + resulting Tasks. Any scheduled task can be cancelled prematurely using :obj:`cancel` by providing + the same ID used to schedule it. + + The ``in`` operator is supported for checking if a task with a given ID is currently scheduled. Any exception raised in a scheduled task is logged when the task is done. """ def __init__(self, name: str): """ - Initialize a new Scheduler instance. + Initialize a new :obj:`Scheduler` instance. Args: - name: The name of the scheduler. Used in logging, and namespacing. + name: The name of the :obj:`Scheduler`. Used in logging, and namespacing. """ self.name = name @@ -41,21 +42,21 @@ class Scheduler: def __contains__(self, task_id: typing.Hashable) -> bool: """ - Return True if a task with the given `task_id` is currently scheduled. + Return :obj:`True` if a task with the given ``task_id`` is currently scheduled. Args: task_id: The task to look for. Returns: - True if the task was found. + :obj:`True` if the task was found. """ return task_id in self._scheduled_tasks def schedule(self, task_id: typing.Hashable, coroutine: typing.Coroutine) -> None: """ - Schedule the execution of a `coroutine`. + Schedule the execution of a ``coroutine``. - If a task with `task_id` already exists, close `coroutine` instead of scheduling it. This + If a task with ``task_id`` already exists, close ``coroutine`` instead of scheduling it. This prevents unawaited coroutine warnings. Don't pass a coroutine that'll be re-used elsewhere. Args: @@ -80,14 +81,14 @@ class Scheduler: def schedule_at(self, time: datetime, task_id: typing.Hashable, coroutine: typing.Coroutine) -> None: """ - Schedule `coroutine` to be executed at the given `time`. + Schedule ``coroutine`` to be executed at the given ``time``. - If `time` is timezone aware, then use that timezone to calculate now() when subtracting. - If `time` is naïve, then use UTC. + If ``time`` is timezone aware, then use that timezone to calculate now() when subtracting. + If ``time`` is naïve, then use UTC. - If `time` is in the past, schedule `coroutine` immediately. + If ``time`` is in the past, schedule ``coroutine`` immediately. - If a task with `task_id` already exists, close `coroutine` instead of scheduling it. This + If a task with ``task_id`` already exists, close ``coroutine`` instead of scheduling it. This prevents unawaited coroutine warnings. Don't pass a coroutine that'll be re-used elsewhere. Args: @@ -109,9 +110,9 @@ class Scheduler: coroutine: typing.Coroutine ) -> None: """ - Schedule `coroutine` to be executed after `delay` seconds. + Schedule ``coroutine`` to be executed after ``delay`` seconds. - If a task with `task_id` already exists, close `coroutine` instead of scheduling it. This + If a task with ``task_id`` already exists, close ``coroutine`` instead of scheduling it. This prevents unawaited coroutine warnings. Don't pass a coroutine that'll be re-used elsewhere. Args: @@ -123,7 +124,7 @@ class Scheduler: def cancel(self, task_id: typing.Hashable) -> None: """ - Unschedule the task identified by `task_id`. Log a warning if the task doesn't exist. + Unschedule the task identified by ``task_id``. Log a warning if the task doesn't exist. Args: task_id: The task's unique ID. @@ -152,7 +153,7 @@ class Scheduler: task_id: typing.Hashable, coroutine: typing.Coroutine ) -> None: - """Await `coroutine` after `delay` seconds.""" + """Await ``coroutine`` after ``delay`` seconds.""" try: self._log.trace(f"Waiting {delay} seconds before awaiting coroutine for #{task_id}.") await asyncio.sleep(delay) @@ -176,7 +177,7 @@ class Scheduler: """ Delete the task and raise its exception if one exists. - If `done_task` and the task associated with `task_id` are different, then the latter + If ``done_task`` and the task associated with ``task_id`` are different, then the latter will not be deleted. In this case, a new task was likely rescheduled with the same ID. """ self._log.trace(f"Performing done callback for task #{task_id} {id(done_task)}.") @@ -217,7 +218,8 @@ def create_task( """ Wrapper for creating an :obj:`asyncio.Task` which logs exceptions raised in the task. - If the loop kwarg is provided, the task is created from that event loop, otherwise the running loop is used. + If the ``event_loop`` kwarg is provided, the task is created from that event loop, + otherwise the running loop is used. Args: coro: The function to call. @@ -237,7 +239,7 @@ def create_task( def _log_task_exception(task: asyncio.Task, *, suppressed_exceptions: typing.Tuple[typing.Type[Exception]]) -> None: - """Retrieve and log the exception raised in `task` if one exists.""" + """Retrieve and log the exception raised in ``task`` if one exists.""" with contextlib.suppress(asyncio.CancelledError): exception = task.exception() # Log the exception if one exists. -- cgit v1.2.3