diff --git a/discord/ext/commands/converter.py b/discord/ext/commands/converter.py index 6c559009dbae..35d8b20d9216 100644 --- a/discord/ext/commands/converter.py +++ b/discord/ext/commands/converter.py @@ -85,6 +85,7 @@ 'clean_content', 'Greedy', 'Range', + 'RegexConverter', 'run_converters', ) @@ -951,6 +952,82 @@ async def convert(self, ctx: Context[BotT], argument: str) -> discord.ScheduledE return result +class RegexConverter(Converter[str]): + """Converter that checks if the argument matches a custom regular expression. + + This converter is useful for creating custom validation logic. + + .. versionadded:: 2.5 + + Parameters + ----------- + pattern: Union[:class:`str`, :class:`re.Pattern`] + The pattern to match against. If a string is provided, it will be compiled into a regular expression. + case_insensitive: :class:`bool` + Whether the pattern should be case insensitive. + + This sets the :attr:`re.IGNORECASE` flag. + This cannot be used with a pre-compiled pattern. + + Defaults to ``False``. + force_ascii: :class:`bool` + Whether to force ASCII encoding on the pattern. + + This sets the :attr:`re.ASCII` flag. + This cannot be used with a pre-compiled pattern. + + Defaults to ``False``. + multiline: :class:`bool` + Whether the pattern should match across multiple lines. + + This sets the :attr:`re.MULTILINE` flag. + This cannot be used with a pre-compiled pattern. + + Defaults to ``False``. + flags: Union[:class:`int`, :class:`re.RegexFlag`] + The flags to pass to the regular expression compiler. + + :attr:`re.IGNORECASE`, :attr:`re.ASCII` and :attr:`re.MULTILINE` + are set to this if the respective parameters are set to ``True``. + + This cannot be used with a pre-compiled pattern. + + Defaults to ``0``. + + Attributes + ------------ + pattern: :class:`re.Pattern` + The compiled regular expression pattern. + """ + + def __init__( + self, + pattern: Union[str, re.Pattern[str]], + case_insensitive: bool = False, + force_ascii: bool = False, + multiline: bool = False, + flags: Union[int, re.RegexFlag] = 0, + ) -> None: + flags = flags or 0 + if case_insensitive: + flags |= re.IGNORECASE + if force_ascii: + flags |= re.ASCII + if multiline: + flags |= re.MULTILINE + + if flags and isinstance(pattern, re.Pattern): + raise ValueError('Cannot specify flags with a pre-compiled pattern') + + self.pattern: re.Pattern[str] = pattern if isinstance(pattern, re.Pattern) else re.compile(pattern, flags) + + async def convert(self, ctx: Context[BotT], argument: str) -> re.Match[str]: + match_ = self.pattern.fullmatch(argument) + if match_ is None: + raise NoMatch(self.pattern, argument) + + return match_ + class clean_content(Converter[str]): """Converts the argument to mention scrubbed version of said content. diff --git a/discord/ext/commands/errors.py b/discord/ext/commands/errors.py index 0c1e0f2d0db4..96dd7224e5d1 100644 --- a/discord/ext/commands/errors.py +++ b/discord/ext/commands/errors.py @@ -24,6 +24,7 @@ from __future__ import annotations +import re from typing import TYPE_CHECKING, Any, Callable, List, Optional, Tuple, Union from discord.errors import ClientException, DiscordException @@ -105,6 +106,7 @@ 'MissingRequiredFlag', 'HybridCommandError', 'RangeError', + 'NoMatch', ) @@ -1186,6 +1188,28 @@ def __init__(self, flag: Flag) -> None: self.flag: Flag = flag super().__init__(f'Flag {flag.name!r} does not have an argument') +class NoMatch(ArgumentParsingError): + """An exception raised when the parser fails to match a user's input to a pattern. + + This inherits from :exc:`ArgumentParsingError`. + + .. versionadded:: 2.5 + + Attributes + ----------- + pattern: :class:`re.Pattern` + The pattern that failed to match. + argument: :class:`str` + The argument that failed to match. + """ + + def __init__(self, pattern: re.Pattern, argument: str) -> None: + self.pattern: re.Pattern = pattern + self.argument: str = argument + super().__init__( + f'{argument!r} did not match the required pattern: {pattern.pattern}' + ) + class HybridCommandError(CommandError): """An exception raised when a :class:`~discord.ext.commands.HybridCommand` raises