diff --git a/redbot/cogs/cleanup/cleanup.py b/redbot/cogs/cleanup/cleanup.py index 090584bb3..783d9a315 100644 --- a/redbot/cogs/cleanup/cleanup.py +++ b/redbot/cogs/cleanup/cleanup.py @@ -7,13 +7,13 @@ import discord from redbot.core import commands, Config from redbot.core.bot import Red -from redbot.core.commands import RawUserIdConverter +from redbot.core.commands import positive_int, RawUserIdConverter from redbot.core.i18n import Translator, cog_i18n from redbot.core.utils.chat_formatting import humanize_number from redbot.core.utils.mod import slow_deletion, mass_purge from redbot.core.utils.predicates import MessagePredicate from .checks import check_self_permissions -from .converters import PositiveInt, RawMessageIds, positive_int +from .converters import RawMessageIds _ = Translator("Cleanup", __file__) @@ -78,9 +78,9 @@ class Cleanup(commands.Cog): channel: Union[ discord.TextChannel, discord.VoiceChannel, discord.DMChannel, discord.Thread ], - number: Optional[PositiveInt] = None, + number: Optional[int] = None, check: Callable[[discord.Message], bool] = lambda x: True, - limit: Optional[PositiveInt] = None, + limit: Optional[int] = None, before: Union[discord.Message, datetime] = None, after: Union[discord.Message, datetime] = None, delete_pinned: bool = False, @@ -684,9 +684,7 @@ class Cleanup(commands.Cog): @commands.guild_only() @commands.mod_or_permissions(manage_messages=True) @commands.bot_has_permissions(manage_messages=True) - async def cleanup_duplicates( - self, ctx: commands.Context, number: positive_int = PositiveInt(50) - ): + async def cleanup_duplicates(self, ctx: commands.Context, number: positive_int = 50): """Deletes duplicate messages in the channel from the last X messages and keeps only one copy. Defaults to 50. diff --git a/redbot/cogs/cleanup/converters.py b/redbot/cogs/cleanup/converters.py index 6cefc5c2d..be0c487da 100644 --- a/redbot/cogs/cleanup/converters.py +++ b/redbot/cogs/cleanup/converters.py @@ -1,8 +1,5 @@ -from typing import NewType, TYPE_CHECKING - from redbot.core.commands import BadArgument, Context, Converter from redbot.core.i18n import Translator -from redbot.core.utils.chat_formatting import inline _ = Translator("Cleanup", __file__) @@ -15,18 +12,3 @@ class RawMessageIds(Converter): return int(argument) raise BadArgument(_("{} doesn't look like a valid message ID.").format(argument)) - - -PositiveInt = NewType("PositiveInt", int) -if TYPE_CHECKING: - positive_int = PositiveInt -else: - - def positive_int(arg: str) -> int: - try: - ret = int(arg) - except ValueError: - raise BadArgument(_("{arg} is not an integer.").format(arg=inline(arg))) - if ret <= 0: - raise BadArgument(_("{arg} is not a positive integer.").format(arg=inline(arg))) - return ret diff --git a/redbot/cogs/economy/converters.py b/redbot/cogs/economy/converters.py deleted file mode 100644 index c24ae98be..000000000 --- a/redbot/cogs/economy/converters.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import NewType, TYPE_CHECKING - -from redbot.core.commands import BadArgument -from redbot.core.i18n import Translator -from redbot.core.utils.chat_formatting import inline - -_ = Translator("Economy", __file__) - -# Duplicate of redbot.cogs.cleanup.converters.PositiveInt -PositiveInt = NewType("PositiveInt", int) -if TYPE_CHECKING: - positive_int = PositiveInt -else: - - def positive_int(arg: str) -> int: - try: - ret = int(arg) - except ValueError: - raise BadArgument(_("{arg} is not an integer.").format(arg=inline(arg))) - if ret <= 0: - raise BadArgument(_("{arg} is not a positive integer.").format(arg=inline(arg))) - return ret diff --git a/redbot/cogs/economy/economy.py b/redbot/cogs/economy/economy.py index 5c8bccb9f..35f8c7680 100644 --- a/redbot/cogs/economy/economy.py +++ b/redbot/cogs/economy/economy.py @@ -5,18 +5,17 @@ from collections import defaultdict, deque, namedtuple from datetime import datetime, timezone, timedelta from enum import Enum from math import ceil -from typing import cast, Iterable, Union, Literal +from typing import cast, Iterable, Literal import discord from redbot.core import Config, bank, commands, errors -from redbot.core.commands.converter import TimedeltaConverter +from redbot.core.commands.converter import TimedeltaConverter, positive_int from redbot.core.bot import Red from redbot.core.i18n import Translator, cog_i18n from redbot.core.utils import AsyncIter from redbot.core.utils.chat_formatting import box, humanize_number -from redbot.core.utils.menus import close_menu, menu -from .converters import positive_int +from redbot.core.utils.menus import menu T_ = Translator("Economy", __file__) diff --git a/redbot/core/cog_manager.py b/redbot/core/cog_manager.py index 7b14383c3..75bfa034c 100644 --- a/redbot/core/cog_manager.py +++ b/redbot/core/cog_manager.py @@ -4,10 +4,10 @@ import pkgutil from importlib import import_module, invalidate_caches from importlib.machinery import ModuleSpec from pathlib import Path -from typing import TYPE_CHECKING, Union, List, Optional +from typing import Union, List, Optional import redbot.cogs -from redbot.core.commands import BadArgument +from redbot.core.commands import positive_int from redbot.core.utils import deduplicate_iterables import discord @@ -21,21 +21,6 @@ from .utils.chat_formatting import box, pagify, humanize_list, inline __all__ = ["CogManager"] -# Duplicate of redbot.cogs.cleanup.converters.positive_int -if TYPE_CHECKING: - positive_int = int -else: - - def positive_int(arg: str) -> int: - try: - ret = int(arg) - except ValueError: - raise BadArgument(_("{arg} is not an integer.").format(arg=inline(arg))) - if ret <= 0: - raise BadArgument(_("{arg} is not a positive integer.").format(arg=inline(arg))) - return ret - - class NoSuchCog(ImportError): """Thrown when a cog is missing. diff --git a/redbot/core/commands/__init__.py b/redbot/core/commands/__init__.py index a768c928f..5b51643aa 100644 --- a/redbot/core/commands/__init__.py +++ b/redbot/core/commands/__init__.py @@ -28,10 +28,12 @@ from .converter import ( DictConverter as DictConverter, RelativedeltaConverter as RelativedeltaConverter, TimedeltaConverter as TimedeltaConverter, + finite_float as finite_float, get_dict_converter as get_dict_converter, get_timedelta_converter as get_timedelta_converter, parse_relativedelta as parse_relativedelta, parse_timedelta as parse_timedelta, + positive_int as positive_int, NoParseOptional as NoParseOptional, UserInputOptional as UserInputOptional, RawUserIdConverter as RawUserIdConverter, diff --git a/redbot/core/commands/converter.py b/redbot/core/commands/converter.py index 2b3bbb690..94722cef2 100644 --- a/redbot/core/commands/converter.py +++ b/redbot/core/commands/converter.py @@ -6,6 +6,7 @@ This module contains useful functions and classes for command argument conversio Some of the converters within are included provisionally and are marked as such. """ import functools +import math import re from datetime import timedelta from dateutil.relativedelta import relativedelta @@ -37,10 +38,12 @@ __all__ = [ "NoParseOptional", "RelativedeltaConverter", "TimedeltaConverter", + "finite_float", "get_dict_converter", "get_timedelta_converter", "parse_relativedelta", "parse_timedelta", + "positive_int", "CommandConverter", "CogConverter", ] @@ -233,6 +236,26 @@ class RawUserIdConverter(dpy_commands.Converter): # which is *not* for type checking for the actual implementation # and ensure the lies stay correct for how the object should look as a typehint +positive_int = dpy_commands.Range[int, 0, None] + + +if TYPE_CHECKING: + finite_float = float +else: + + def finite_float(arg: str) -> float: + """ + This converts a user provided string into a finite float. + """ + try: + ret = float(arg) + except ValueError: + raise BadArgument(_("`{arg}` is not a number.").format(arg=arg)) + if not math.isfinite(ret): + raise BadArgument(_("`{arg}` is not a finite number.").format(arg=ret)) + return ret + + if TYPE_CHECKING: DictConverter = Dict[str, str] else: diff --git a/redbot/core/events.py b/redbot/core/events.py index d1651eadf..a096ba23f 100644 --- a/redbot/core/events.py +++ b/redbot/core/events.py @@ -2,7 +2,6 @@ import asyncio import contextlib import platform import sys -import codecs import logging import traceback from datetime import datetime, timedelta, timezone @@ -17,12 +16,9 @@ from redbot.core import data_manager from redbot.core.commands import RedHelpFormatter, HelpSettings from redbot.core.i18n import ( Translator, - set_contextual_locale, - set_contextual_regional_format, set_contextual_locales_from_guild, ) -from .utils import AsyncIter -from .. import __version__ as red_version, version_info as red_version_info, VersionInfo +from .. import __version__ as red_version, version_info as red_version_info from . import commands from .config import get_latest_confs from .utils._internal_utils import ( @@ -32,7 +28,7 @@ from .utils._internal_utils import ( fetch_latest_red_version_info, send_to_owners_with_prefix_replaced, ) -from .utils.chat_formatting import inline, format_perms_list, humanize_timedelta +from .utils.chat_formatting import inline, format_perms_list import rich from rich import box @@ -229,6 +225,8 @@ def init_events(bot, cli_flags): return if not isinstance(error, commands.CommandNotFound): asyncio.create_task(bot._delete_delay(ctx)) + converter = getattr(ctx.current_parameter, "converter", None) + argument = ctx.current_argument if isinstance(error, commands.MissingRequiredArgument): await ctx.send_help() @@ -241,10 +239,112 @@ def init_events(bot, cli_flags): await ctx.send(msg) if error.send_cmd_help: await ctx.send_help() + elif isinstance(error, commands.RangeError): + if isinstance(error.value, int): + if error.minimum == 0 and error.maximum is None: + message = _("Argument `{parameter_name}` must be a positive integer.") + elif error.minimum is None and error.maximum is not None: + message = _( + "Argument `{parameter_name}` must be an integer no more than {maximum}." + ) + elif error.minimum is not None and error.maximum is None: + message = _( + "Argument `{parameter_name}` must be an integer no less than {minimum}." + ) + elif error.maximum is not None and error.minimum is not None: + message = _( + "Argument `{parameter_name}` must be an integer between {minimum} and {maximum}." + ) + elif isinstance(error.value, float): + if error.minimum == 0 and error.maximum is None: + message = _("Argument `{parameter_name}` must be a positive number.") + elif error.minimum is None and error.maximum is not None: + message = _( + "Argument `{parameter_name}` must be a number no more than {maximum}." + ) + elif error.minimum is not None and error.maximum is None: + message = _( + "Argument `{parameter_name}` must be a number no less than {maximum}." + ) + elif error.maximum is not None and error.minimum is not None: + message = _( + "Argument `{parameter_name}` must be a number between {minimum} and {maximum}." + ) + elif isinstance(error.value, str): + if error.minimum is None and error.maximum is not None: + message = _( + "Argument `{parameter_name}` must be a string with a length of no more than {maximum}." + ) + elif error.minimum is not None and error.maximum is None: + message = _( + "Argument `{parameter_name}` must be a string with a length of no less than {minimum}." + ) + elif error.maximum is not None and error.minimum is not None: + message = _( + "Argument `{parameter_name}` must be a string with a length of between {minimum} and {maximum}." + ) + await ctx.send( + message.format( + maximum=error.maximum, + minimum=error.minimum, + parameter_name=ctx.current_parameter.name, + ) + ) + return elif isinstance(error, commands.BadArgument): + if isinstance(converter, commands.Range): + if converter.annotation is int: + if converter.min == 0 and converter.max is None: + message = _("Argument `{parameter_name}` must be a positive integer.") + elif converter.min is None and converter.max is not None: + message = _( + "Argument `{parameter_name}` must be an integer no more than {maximum}." + ) + elif converter.min is not None and converter.max is None: + message = _( + "Argument `{parameter_name}` must be an integer no less than {minimum}." + ) + elif converter.max is not None and converter.min is not None: + message = _( + "Argument `{parameter_name}` must be an integer between {minimum} and {maximum}." + ) + elif converter.annotation is float: + if converter.min == 0 and converter.max is None: + message = _("Argument `{parameter_name}` must be a positive number.") + elif converter.min is None and converter.max is not None: + message = _( + "Argument `{parameter_name}` must be a number no more than {maximum}." + ) + elif converter.min is not None and converter.max is None: + message = _( + "Argument `{parameter_name}` must be a number no less than {minimum}." + ) + elif converter.max is not None and converter.min is not None: + message = _( + "Argument `{parameter_name}` must be a number between {minimum} and {maximum}." + ) + elif converter.annotation is str: + if error.minimum is None and error.maximum is not None: + message = _( + "Argument `{parameter_name}` must be a string with a length of no more than {maximum}." + ) + elif error.minimum is not None and error.maximum is None: + message = _( + "Argument `{parameter_name}` must be a string with a length of no less than {minimum}." + ) + elif error.maximum is not None and error.minimum is not None: + message = _( + "Argument `{parameter_name}` must be a string with a length of between {minimum} and {maximum}." + ) + await ctx.send( + message.format( + maximum=converter.max, + minimum=converter.min, + parameter_name=ctx.current_parameter.name, + ) + ) + return if isinstance(error.__cause__, ValueError): - converter = ctx.current_parameter.converter - argument = ctx.current_argument if converter is int: await ctx.send(_('"{argument}" is not an integer.').format(argument=argument)) return