Add a timedelta converter (#2736)

* Add a timedelta converter

This reuses a lot of logic from @mikeshardmind 's scheduler cog with permission

It adds a timedelta converter
It keeps it generalized as requested
It keeps the function available for non converter use as requested

* Handle feedback

* style fix
This commit is contained in:
DiscordLiz 2019-05-28 12:43:55 -04:00 committed by Michael H
parent 56161c0a88
commit 1581604f71

View File

@ -1,5 +1,6 @@
import re import re
import functools import functools
from datetime import timedelta
from typing import TYPE_CHECKING, Optional, List, Dict from typing import TYPE_CHECKING, Optional, List, Dict
import discord import discord
@ -7,17 +8,105 @@ from discord.ext import commands as dpy_commands
from . import BadArgument from . import BadArgument
from ..i18n import Translator from ..i18n import Translator
from ..utils.chat_formatting import humanize_timedelta
if TYPE_CHECKING: if TYPE_CHECKING:
from .context import Context from .context import Context
__all__ = ["GuildConverter", "APIToken", "DictConverter", "get_dict_converter"] __all__ = [
"APIToken",
"DictConverter",
"GuildConverter",
"TimedeltaConverter",
"get_dict_converter",
"get_timedelta_converter",
"parse_timedelta",
]
_ = Translator("commands.converter", __file__) _ = Translator("commands.converter", __file__)
ID_REGEX = re.compile(r"([0-9]{15,21})") ID_REGEX = re.compile(r"([0-9]{15,21})")
# Taken with permission from
# https://github.com/mikeshardmind/SinbadCogs/blob/816f3bc2ba860243f75112904b82009a8a9e1f99/scheduler/time_utils.py#L9-L19
TIME_RE_STRING = r"\s?".join(
[
r"((?P<weeks>\d+?)\s?(weeks?|w))?",
r"((?P<days>\d+?)\s?(days?|d))?",
r"((?P<hours>\d+?)\s?(hours?|hrs|hr?))?",
r"((?P<minutes>\d+?)\s?(minutes?|mins?|m(?!o)))?", # prevent matching "months"
r"((?P<seconds>\d+?)\s?(seconds?|secs?|s))?",
]
)
TIME_RE = re.compile(TIME_RE_STRING, re.I)
def parse_timedelta(
argument: str,
*,
maximum: Optional[timedelta] = None,
minimum: Optional[timedelta] = None,
allowed_units: Optional[List[str]] = None,
) -> Optional[timedelta]:
"""
This converts a user provided string into a timedelta
The units should be in order from largest to smallest.
This works with or without whitespace.
Parameters
----------
argument : str
The user provided input
maximum : Optional[timedelta]
If provided, any parsed value higher than this will raise an exception
minimum : Optional[timedelta]
If provided, any parsed value lower than this will raise an exception
allowed_units : Optional[List[str]]
If provided, you can constrain a user to expressing the amount of time
in specific units. The units you can chose to provide are the same as the
parser understands. `weeks` `days` `hours` `minutes` `seconds`
Returns
-------
Optional[timedelta]
If matched, the timedelta which was parsed. This can return `None`
Raises
------
BadArgument
If the argument passed uses a unit not allowed, but understood
or if the value is out of bounds.
"""
matches = TIME_RE.match(argument)
allowed_units = allowed_units or ["weeks", "days", "hours", "minutes", "seconds"]
if matches:
params = {k: int(v) for k, v in matches.groupdict().items() if v is not None}
for k in params.keys():
if k not in allowed_units:
raise BadArgument(
_("`{unit}` is not a valid unit of time for this command").format(unit=k)
)
if params:
delta = timedelta(**params)
if maximum and maximum < delta:
raise BadArgument(
_(
"This amount of time is too large for this command. (maximum: {maximum})"
).format(maximum=humanize_timedelta(timedelta=maximum))
)
if minimum and delta < minimum:
raise BadArgument(
_(
"This amount of time is too small for this command. (minimum: {minimum})"
).format(minimum=humanize_timedelta(timedelta=minimum))
)
return delta
return None
class GuildConverter(discord.Guild): class GuildConverter(discord.Guild):
"""Converts to a `discord.Guild` object. """Converts to a `discord.Guild` object.
@ -116,3 +205,80 @@ def get_dict_converter(*expected_keys: str, delims: Optional[List[str]] = None)
pass pass
return ValidatedConverter return ValidatedConverter
class TimedeltaConverter(dpy_commands.Converter):
"""
This is a converter for timedeltas.
The units should be in order from largest to smallest.
This works with or without whitespace.
See `parse_timedelta` for more information about how this functions.
Attributes
----------
maximum : Optional[timedelta]
If provided, any parsed value higher than this will raise an exception
minimum : Optional[timedelta]
If provided, any parsed value lower than this will raise an exception
allowed_units : Optional[List[str]]
If provided, you can constrain a user to expressing the amount of time
in specific units. The units you can chose to provide are the same as the
parser understands. `weeks` `days` `hours` `minutes` `seconds`
"""
def __init__(self, *, minimum=None, maximum=None, allowed_units=None):
self.allowed_units = allowed_units
self.minimum = minimum
self.maximum = maximum
async def convert(self, ctx: "Context", argument: str) -> timedelta:
delta = parse_timedelta(
argument, minimum=self.minimum, maximum=self.maximum, allowed_units=self.allowed_units
)
if delta is not None:
return delta
raise BadArgument() # This allows this to be a required argument.
def get_timedelta_converter(
*,
maximum: Optional[timedelta] = None,
minimum: Optional[timedelta] = None,
allowed_units: Optional[List[str]] = None,
) -> type:
"""
This creates a type suitable for typechecking which works with discord.py's
commands.
See `parse_timedelta` for more information about how this functions.
Parameters
----------
maximum : Optional[timedelta]
If provided, any parsed value higher than this will raise an exception
minimum : Optional[timedelta]
If provided, any parsed value lower than this will raise an exception
allowed_units : Optional[List[str]]
If provided, you can constrain a user to expressing the amount of time
in specific units. The units you can chose to provide are the same as the
parser understands. `weeks` `days` `hours` `minutes` `seconds`
Returns
-------
type
The converter class, which will be a subclass of `TimedeltaConverter`
"""
class PartialMeta(type(TimedeltaConverter)):
__call__ = functools.partialmethod(
type(DictConverter).__call__,
allowed_units=allowed_units,
minimum=minimum,
maximum=maximum,
)
class ValidatedConverter(TimedeltaConverter, metaclass=PartialMeta):
pass
return ValidatedConverter