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:
Zoë F
2021-09-05 18:50:21 -06:00
committed by GitHub
parent 86649e897f
commit ed9bb77eec
4 changed files with 165 additions and 36 deletions

View File

@@ -8,6 +8,7 @@ Some of the converters within are included provisionally and are marked as such.
import functools
import re
from datetime import timedelta
from dateutil.relativedelta import relativedelta
from typing import (
TYPE_CHECKING,
Generic,
@@ -37,9 +38,11 @@ __all__ = [
"DictConverter",
"UserInputOptional",
"NoParseOptional",
"RelativedeltaConverter",
"TimedeltaConverter",
"get_dict_converter",
"get_timedelta_converter",
"parse_relativedelta",
"parse_timedelta",
"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
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<days>\d+?)\s?(days?|d))?",
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)
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(
argument: str,
*,
@@ -81,9 +102,9 @@ def parse_timedelta(
----------
argument : str
The user provided input
maximum : Optional[timedelta]
maximum : Optional[datetime.timedelta]
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
allowed_units : Optional[List[str]]
If provided, you can constrain a user to expressing the amount of time
@@ -92,7 +113,7 @@ def parse_timedelta(
Returns
-------
Optional[timedelta]
Optional[datetime.timedelta]
If matched, the timedelta which was parsed. This can return `None`
Raises
@@ -101,35 +122,84 @@ def parse_timedelta(
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:
try:
delta = timedelta(**params)
except OverflowError:
raise BadArgument(
_("The time set is way too high, consider setting something reasonable.")
)
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
allowed_units = allowed_units or [
"weeks",
"days",
"hours",
"minutes",
"seconds",
]
params = _parse_and_match(argument, allowed_units)
if params:
try:
delta = timedelta(**params)
except OverflowError:
raise BadArgument(
_("The time set is way too high, consider setting something reasonable.")
)
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
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
@@ -250,9 +320,9 @@ else:
Attributes
----------
maximum : Optional[timedelta]
maximum : Optional[datetime.timedelta]
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
allowed_units : Optional[List[str]]
If provided, you can constrain a user to expressing the amount of time
@@ -315,9 +385,9 @@ else:
Parameters
----------
maximum : Optional[timedelta]
maximum : Optional[datetime.timedelta]
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
allowed_units : Optional[List[str]]
If provided, you can constrain a user to expressing the amount of time
@@ -349,6 +419,46 @@ else:
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:
class NoParseOptional: