Skip to content

Commit

Permalink
Improve timedelta conversions (#6349)
Browse files Browse the repository at this point in the history
Co-authored-by: zephyrkul <[email protected]>
  • Loading branch information
Zephyrkul and Zephyrkul authored Apr 14, 2024
1 parent afb4f60 commit 00e41d3
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 49 deletions.
81 changes: 47 additions & 34 deletions redbot/cogs/mutes/converters.py
Original file line number Diff line number Diff line change
@@ -1,63 +1,76 @@
from __future__ import annotations

import logging
import re
from typing import Union, Dict
from typing import Optional, TypedDict
from datetime import timedelta
from typing_extensions import Annotated

from discord.ext.commands.converter import Converter
from redbot.core import commands
from redbot.core import i18n
from redbot.core.commands.converter import TIME_RE

_ = i18n.Translator("Mutes", __file__)
log = logging.getLogger("red.cogs.mutes")

# the following regex is slightly modified from Red
# it's changed to be slightly more strict on matching with finditer
# this is to prevent "empty" matches when parsing the full reason
# This is also designed more to allow time interval at the beginning or the end of the mute
# to account for those times when you think of adding time *after* already typing out the reason
# https://github.com/Cog-Creators/Red-DiscordBot/blob/V3/develop/redbot/core/commands/converter.py#L55
TIME_RE_STRING = r"|".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)
TIME_SPLIT = re.compile(r"t(?:ime)?=")
TIME_SPLIT = re.compile(r"t(?:ime\s?)?=\s*")

_ = i18n.Translator("Mutes", __file__)

def _edgematch(pattern: re.Pattern[str], argument: str) -> Optional[re.Match[str]]:
"""Internal utility to match at either end of the argument string"""
# precondition: pattern does not end in $
# precondition: argument does not end in whitespace
return pattern.match(argument) or re.search(
pattern.pattern + "$", argument, flags=pattern.flags
)


class MuteTime(Converter):
class _MuteTime(TypedDict, total=False):
duration: timedelta
reason: str


class _MuteTimeConverter(Converter):
"""
This will parse my defined multi response pattern and provide usable formats
to be used in multiple responses
"""

async def convert(
self, ctx: commands.Context, argument: str
) -> Dict[str, Union[timedelta, str, None]]:
time_split = TIME_SPLIT.split(argument)
result: Dict[str, Union[timedelta, str, None]] = {}
async def convert(self, ctx: commands.Context, argument: str) -> _MuteTime:
time_split = TIME_SPLIT.search(argument)
result: _MuteTime = {}
if time_split:
maybe_time = time_split[-1]
maybe_time = argument[time_split.end() :]
strategy = re.match
else:
maybe_time = argument
strategy = _edgematch

time_data = {}
for time in TIME_RE.finditer(maybe_time):
argument = argument.replace(time[0], "")
for k, v in time.groupdict().items():
if v:
time_data[k] = int(v)
if time_data:
match = strategy(TIME_RE, maybe_time)
if match:
time_data = {k: int(v) for k, v in match.groupdict().items() if v is not None}
for k in time_data:
if k in ("years", "months"):
raise commands.BadArgument(
_("`{unit}` is not a valid unit of time for this command").format(unit=k)
)
try:
result["duration"] = timedelta(**time_data)
result["duration"] = duration = timedelta(**time_data)
except OverflowError:
raise commands.BadArgument(
_("The time provided is too long; use a more reasonable time.")
)
if duration <= timedelta(seconds=0):
raise commands.BadArgument(_("The time provided must not be in the past."))
if time_split:
start, end = time_split.span()
end += match.end()
else:
start, end = match.span()
argument = argument[:start] + argument[end:]
result["reason"] = argument.strip()
return result


MuteTime = Annotated[_MuteTime, _MuteTimeConverter]
33 changes: 18 additions & 15 deletions redbot/core/commands/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,20 +57,22 @@

# 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<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?))?",
r"((?P<minutes>\d+?)\s?(minutes?|mins?|m(?!o)))?", # prevent matching "months"
r"((?P<seconds>\d+?)\s?(seconds?|secs?|s))?",
]
# with modifications
TIME_RE = re.compile(
r"""
(\s?( # match deliminators here to make word border below unambiguous
(?P<years>[\+-]?\d+)\s?(years?|y)
| (?P<months>[\+-]?\d+)\s?(months?|mo)
| (?P<weeks>[\+-]?\d+)\s?(weeks?|w)
| (?P<days>[\+-]?\d+)\s?(days?|d)
| (?P<hours>[\+-]?\d+)\s?(hours?|hrs|hr?)
| (?P<minutes>[\+-]?\d+)\s?(minutes?|mins?|m)
| (?P<seconds>[\+-]?\d+)\s?(seconds?|secs?|s)
))+\b
""",
flags=re.IGNORECASE | re.VERBOSE,
)

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]]:
"""
Expand All @@ -92,13 +94,13 @@ def parse_timedelta(
argument: str,
*,
maximum: Optional[timedelta] = None,
minimum: Optional[timedelta] = None,
minimum: Optional[timedelta] = timedelta(seconds=0),
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.
If a unit is specified multiple times, only the last is considered.
This works with or without whitespace.
Parameters
Expand All @@ -109,6 +111,7 @@ def parse_timedelta(
If provided, any parsed value higher than this will raise an exception
minimum : Optional[datetime.timedelta]
If provided, any parsed value lower than this will raise an exception
Defaults to 0 seconds, pass None explicitly to allow negative values
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
Expand Down Expand Up @@ -162,7 +165,7 @@ def parse_relativedelta(
"""
This converts a user provided string into a datetime with offset from NOW
The units should be in order from largest to smallest.
If a unit is specified multiple times, only the last is considered.
This works with or without whitespace.
Parameters
Expand Down

0 comments on commit 00e41d3

Please sign in to comment.