mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-06 03:08:55 -05:00
Add RelativedeltaConverter and parse_relativedelta (#5000)
* Added years and months to parse_timedelta * Added new parse_datetimedelta along with classes for relative dates * Switched datetime as dt to just datetime for clarity * Changed to returning relativedelta instead of datetime * Fixed single char typo * After some digging, removed min and max from relative delta b/c of https://github.com/dateutil/dateutil/issues/350 * Add dateutil to intersphinx mapping * Change uppercase D in RelativeDeltaConverter to a lowercase D * Fix cross-references in docstrings * Add new class and methods to __all__ * Remove get_relativedelta_converter() * style * Fix name of parse_relativedelta test * more style * Re-export new class and function in `redbot.core.commands` Co-authored-by: jack1142 <6032823+jack1142@users.noreply.github.com>
This commit is contained in:
parent
86649e897f
commit
ed9bb77eec
@ -218,6 +218,7 @@ intersphinx_mapping = {
|
|||||||
"dpy": (f"https://discordpy.readthedocs.io/en/v{dpy_version}/", None),
|
"dpy": (f"https://discordpy.readthedocs.io/en/v{dpy_version}/", None),
|
||||||
"motor": ("https://motor.readthedocs.io/en/stable/", None),
|
"motor": ("https://motor.readthedocs.io/en/stable/", None),
|
||||||
"babel": ("http://babel.pocoo.org/en/stable/", None),
|
"babel": ("http://babel.pocoo.org/en/stable/", None),
|
||||||
|
"dateutil": ("https://dateutil.readthedocs.io/en/stable/", None),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Extlinks
|
# Extlinks
|
||||||
|
|||||||
@ -21,9 +21,11 @@ from .commands import (
|
|||||||
from .context import Context as Context, GuildContext as GuildContext, DMContext as DMContext
|
from .context import Context as Context, GuildContext as GuildContext, DMContext as DMContext
|
||||||
from .converter import (
|
from .converter import (
|
||||||
DictConverter as DictConverter,
|
DictConverter as DictConverter,
|
||||||
|
RelativedeltaConverter as RelativedeltaConverter,
|
||||||
TimedeltaConverter as TimedeltaConverter,
|
TimedeltaConverter as TimedeltaConverter,
|
||||||
get_dict_converter as get_dict_converter,
|
get_dict_converter as get_dict_converter,
|
||||||
get_timedelta_converter as get_timedelta_converter,
|
get_timedelta_converter as get_timedelta_converter,
|
||||||
|
parse_relativedelta as parse_relativedelta,
|
||||||
parse_timedelta as parse_timedelta,
|
parse_timedelta as parse_timedelta,
|
||||||
NoParseOptional as NoParseOptional,
|
NoParseOptional as NoParseOptional,
|
||||||
UserInputOptional as UserInputOptional,
|
UserInputOptional as UserInputOptional,
|
||||||
|
|||||||
@ -8,6 +8,7 @@ Some of the converters within are included provisionally and are marked as such.
|
|||||||
import functools
|
import functools
|
||||||
import re
|
import re
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Generic,
|
Generic,
|
||||||
@ -37,9 +38,11 @@ __all__ = [
|
|||||||
"DictConverter",
|
"DictConverter",
|
||||||
"UserInputOptional",
|
"UserInputOptional",
|
||||||
"NoParseOptional",
|
"NoParseOptional",
|
||||||
|
"RelativedeltaConverter",
|
||||||
"TimedeltaConverter",
|
"TimedeltaConverter",
|
||||||
"get_dict_converter",
|
"get_dict_converter",
|
||||||
"get_timedelta_converter",
|
"get_timedelta_converter",
|
||||||
|
"parse_relativedelta",
|
||||||
"parse_timedelta",
|
"parse_timedelta",
|
||||||
"Literal",
|
"Literal",
|
||||||
]
|
]
|
||||||
@ -53,6 +56,8 @@ ID_REGEX = re.compile(r"([0-9]{15,20})")
|
|||||||
# https://github.com/mikeshardmind/SinbadCogs/blob/816f3bc2ba860243f75112904b82009a8a9e1f99/scheduler/time_utils.py#L9-L19
|
# https://github.com/mikeshardmind/SinbadCogs/blob/816f3bc2ba860243f75112904b82009a8a9e1f99/scheduler/time_utils.py#L9-L19
|
||||||
TIME_RE_STRING = r"\s?".join(
|
TIME_RE_STRING = r"\s?".join(
|
||||||
[
|
[
|
||||||
|
r"((?P<years>\d+?)\s?(years?|y))?",
|
||||||
|
r"((?P<months>\d+?)\s?(months?|mo))?",
|
||||||
r"((?P<weeks>\d+?)\s?(weeks?|w))?",
|
r"((?P<weeks>\d+?)\s?(weeks?|w))?",
|
||||||
r"((?P<days>\d+?)\s?(days?|d))?",
|
r"((?P<days>\d+?)\s?(days?|d))?",
|
||||||
r"((?P<hours>\d+?)\s?(hours?|hrs|hr?))?",
|
r"((?P<hours>\d+?)\s?(hours?|hrs|hr?))?",
|
||||||
@ -64,6 +69,22 @@ TIME_RE_STRING = r"\s?".join(
|
|||||||
TIME_RE = re.compile(TIME_RE_STRING, re.I)
|
TIME_RE = re.compile(TIME_RE_STRING, re.I)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_and_match(string_to_match: str, allowed_units: List[str]) -> Optional[Dict[str, int]]:
|
||||||
|
"""
|
||||||
|
Local utility function to match TIME_RE string above to user input for both parse_timedelta and parse_relativedelta
|
||||||
|
"""
|
||||||
|
matches = TIME_RE.match(string_to_match)
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
return params
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def parse_timedelta(
|
def parse_timedelta(
|
||||||
argument: str,
|
argument: str,
|
||||||
*,
|
*,
|
||||||
@ -81,9 +102,9 @@ def parse_timedelta(
|
|||||||
----------
|
----------
|
||||||
argument : str
|
argument : str
|
||||||
The user provided input
|
The user provided input
|
||||||
maximum : Optional[timedelta]
|
maximum : Optional[datetime.timedelta]
|
||||||
If provided, any parsed value higher than this will raise an exception
|
If provided, any parsed value higher than this will raise an exception
|
||||||
minimum : Optional[timedelta]
|
minimum : Optional[datetime.timedelta]
|
||||||
If provided, any parsed value lower than this will raise an exception
|
If provided, any parsed value lower than this will raise an exception
|
||||||
allowed_units : Optional[List[str]]
|
allowed_units : Optional[List[str]]
|
||||||
If provided, you can constrain a user to expressing the amount of time
|
If provided, you can constrain a user to expressing the amount of time
|
||||||
@ -92,7 +113,7 @@ def parse_timedelta(
|
|||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
Optional[timedelta]
|
Optional[datetime.timedelta]
|
||||||
If matched, the timedelta which was parsed. This can return `None`
|
If matched, the timedelta which was parsed. This can return `None`
|
||||||
|
|
||||||
Raises
|
Raises
|
||||||
@ -101,35 +122,84 @@ def parse_timedelta(
|
|||||||
If the argument passed uses a unit not allowed, but understood
|
If the argument passed uses a unit not allowed, but understood
|
||||||
or if the value is out of bounds.
|
or if the value is out of bounds.
|
||||||
"""
|
"""
|
||||||
matches = TIME_RE.match(argument)
|
allowed_units = allowed_units or [
|
||||||
allowed_units = allowed_units or ["weeks", "days", "hours", "minutes", "seconds"]
|
"weeks",
|
||||||
if matches:
|
"days",
|
||||||
params = {k: int(v) for k, v in matches.groupdict().items() if v is not None}
|
"hours",
|
||||||
for k in params.keys():
|
"minutes",
|
||||||
if k not in allowed_units:
|
"seconds",
|
||||||
raise BadArgument(
|
]
|
||||||
_("`{unit}` is not a valid unit of time for this command").format(unit=k)
|
params = _parse_and_match(argument, allowed_units)
|
||||||
)
|
if params:
|
||||||
if params:
|
try:
|
||||||
try:
|
delta = timedelta(**params)
|
||||||
delta = timedelta(**params)
|
except OverflowError:
|
||||||
except OverflowError:
|
raise BadArgument(
|
||||||
raise BadArgument(
|
_("The time set is way too high, consider setting something reasonable.")
|
||||||
_("The time set is way too high, consider setting something reasonable.")
|
)
|
||||||
)
|
if maximum and maximum < delta:
|
||||||
if maximum and maximum < delta:
|
raise BadArgument(
|
||||||
raise BadArgument(
|
_(
|
||||||
_(
|
"This amount of time is too large for this command. (Maximum: {maximum})"
|
||||||
"This amount of time is too large for this command. (Maximum: {maximum})"
|
).format(maximum=humanize_timedelta(timedelta=maximum))
|
||||||
).format(maximum=humanize_timedelta(timedelta=maximum))
|
)
|
||||||
)
|
if minimum and delta < minimum:
|
||||||
if minimum and delta < minimum:
|
raise BadArgument(
|
||||||
raise BadArgument(
|
_(
|
||||||
_(
|
"This amount of time is too small for this command. (Minimum: {minimum})"
|
||||||
"This amount of time is too small for this command. (Minimum: {minimum})"
|
).format(minimum=humanize_timedelta(timedelta=minimum))
|
||||||
).format(minimum=humanize_timedelta(timedelta=minimum))
|
)
|
||||||
)
|
return delta
|
||||||
return delta
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_relativedelta(
|
||||||
|
argument: str, *, allowed_units: Optional[List[str]] = None
|
||||||
|
) -> Optional[relativedelta]:
|
||||||
|
"""
|
||||||
|
This converts a user provided string into a datetime with offset from NOW
|
||||||
|
|
||||||
|
The units should be in order from largest to smallest.
|
||||||
|
This works with or without whitespace.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
argument : str
|
||||||
|
The user provided input
|
||||||
|
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. (``years``, ``months``, ``weeks``, ``days``, ``hours``, ``minutes``, ``seconds``)
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Optional[dateutil.relativedelta.relativedelta]
|
||||||
|
If matched, the relativedelta 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.
|
||||||
|
"""
|
||||||
|
allowed_units = allowed_units or [
|
||||||
|
"years",
|
||||||
|
"months",
|
||||||
|
"weeks",
|
||||||
|
"days",
|
||||||
|
"hours",
|
||||||
|
"minutes",
|
||||||
|
"seconds",
|
||||||
|
]
|
||||||
|
params = _parse_and_match(argument, allowed_units)
|
||||||
|
if params:
|
||||||
|
try:
|
||||||
|
delta = relativedelta(**params)
|
||||||
|
except OverflowError:
|
||||||
|
raise BadArgument(
|
||||||
|
_("The time set is way too high, consider setting something reasonable.")
|
||||||
|
)
|
||||||
|
return delta
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@ -250,9 +320,9 @@ else:
|
|||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
----------
|
----------
|
||||||
maximum : Optional[timedelta]
|
maximum : Optional[datetime.timedelta]
|
||||||
If provided, any parsed value higher than this will raise an exception
|
If provided, any parsed value higher than this will raise an exception
|
||||||
minimum : Optional[timedelta]
|
minimum : Optional[datetime.timedelta]
|
||||||
If provided, any parsed value lower than this will raise an exception
|
If provided, any parsed value lower than this will raise an exception
|
||||||
allowed_units : Optional[List[str]]
|
allowed_units : Optional[List[str]]
|
||||||
If provided, you can constrain a user to expressing the amount of time
|
If provided, you can constrain a user to expressing the amount of time
|
||||||
@ -315,9 +385,9 @@ else:
|
|||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
maximum : Optional[timedelta]
|
maximum : Optional[datetime.timedelta]
|
||||||
If provided, any parsed value higher than this will raise an exception
|
If provided, any parsed value higher than this will raise an exception
|
||||||
minimum : Optional[timedelta]
|
minimum : Optional[datetime.timedelta]
|
||||||
If provided, any parsed value lower than this will raise an exception
|
If provided, any parsed value lower than this will raise an exception
|
||||||
allowed_units : Optional[List[str]]
|
allowed_units : Optional[List[str]]
|
||||||
If provided, you can constrain a user to expressing the amount of time
|
If provided, you can constrain a user to expressing the amount of time
|
||||||
@ -349,6 +419,46 @@ else:
|
|||||||
return ValidatedConverter
|
return ValidatedConverter
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
RelativedeltaConverter = relativedelta
|
||||||
|
else:
|
||||||
|
|
||||||
|
class RelativedeltaConverter(dpy_commands.Converter):
|
||||||
|
"""
|
||||||
|
This is a converter for relative deltas.
|
||||||
|
|
||||||
|
The units should be in order from largest to smallest.
|
||||||
|
This works with or without whitespace.
|
||||||
|
|
||||||
|
See `parse_relativedelta` for more information about how this functions.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
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 choose to provide are the same as the
|
||||||
|
parser understands: (``years``, ``months``, ``weeks``, ``days``, ``hours``, ``minutes``, ``seconds``)
|
||||||
|
default_unit : Optional[str]
|
||||||
|
If provided, it will additionally try to match integer-only input into
|
||||||
|
a timedelta, using the unit specified. Same units as in ``allowed_units``
|
||||||
|
apply.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *, allowed_units=None, default_unit=None):
|
||||||
|
self.allowed_units = allowed_units
|
||||||
|
self.default_unit = default_unit
|
||||||
|
|
||||||
|
async def convert(self, ctx: "Context", argument: str) -> relativedelta:
|
||||||
|
if self.default_unit and argument.isdecimal():
|
||||||
|
argument = argument + self.default_unit
|
||||||
|
|
||||||
|
delta = parse_relativedelta(argument, allowed_units=self.allowed_units)
|
||||||
|
|
||||||
|
if delta is not None:
|
||||||
|
return delta
|
||||||
|
raise BadArgument() # This allows this to be a required argument.
|
||||||
|
|
||||||
|
|
||||||
if not TYPE_CHECKING:
|
if not TYPE_CHECKING:
|
||||||
|
|
||||||
class NoParseOptional:
|
class NoParseOptional:
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
import inspect
|
import inspect
|
||||||
|
import datetime
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from discord.ext import commands as dpy_commands
|
from discord.ext import commands as dpy_commands
|
||||||
|
|
||||||
from redbot.core import commands
|
from redbot.core import commands
|
||||||
|
from redbot.core.commands import converter
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
@ -49,3 +52,16 @@ def test_dpy_commands_reexports():
|
|||||||
missing_attrs = dpy_attrs - set(commands.__dict__.keys())
|
missing_attrs = dpy_attrs - set(commands.__dict__.keys())
|
||||||
|
|
||||||
assert not missing_attrs
|
assert not missing_attrs
|
||||||
|
|
||||||
|
|
||||||
|
def test_converter_timedelta():
|
||||||
|
assert converter.parse_timedelta("1 day") == datetime.timedelta(days=1)
|
||||||
|
assert converter.parse_timedelta("1 minute") == datetime.timedelta(minutes=1)
|
||||||
|
assert converter.parse_timedelta("13 days 5 minutes") == datetime.timedelta(days=13, minutes=5)
|
||||||
|
|
||||||
|
|
||||||
|
def test_converter_relativedelta():
|
||||||
|
assert converter.parse_relativedelta("1 year") == relativedelta(years=1)
|
||||||
|
assert converter.parse_relativedelta("1 year 10 days 3 seconds") == relativedelta(
|
||||||
|
years=1, days=10, seconds=3
|
||||||
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user