diff --git a/discord/app_commands/errors.py b/discord/app_commands/errors.py index 42de35e4e..dc63f10e8 100644 --- a/discord/app_commands/errors.py +++ b/discord/app_commands/errors.py @@ -28,6 +28,7 @@ from typing import Any, TYPE_CHECKING, List, Optional, Sequence, Union from ..enums import AppCommandOptionType, AppCommandType, Locale from ..errors import DiscordException, HTTPException, _flatten_error_dict +from ..utils import _human_join __all__ = ( 'AppCommandError', @@ -242,13 +243,7 @@ class MissingAnyRole(CheckFailure): def __init__(self, missing_roles: SnowflakeList) -> None: self.missing_roles: SnowflakeList = missing_roles - missing = [f"'{role}'" for role in missing_roles] - - if len(missing) > 2: - fmt = '{}, or {}'.format(', '.join(missing[:-1]), missing[-1]) - else: - fmt = ' or '.join(missing) - + fmt = _human_join([f"'{role}'" for role in missing_roles]) message = f'You are missing at least one of the required roles: {fmt}' super().__init__(message) @@ -271,11 +266,7 @@ class MissingPermissions(CheckFailure): self.missing_permissions: List[str] = missing_permissions missing = [perm.replace('_', ' ').replace('guild', 'server').title() for perm in missing_permissions] - - if len(missing) > 2: - fmt = '{}, and {}'.format(", ".join(missing[:-1]), missing[-1]) - else: - fmt = ' and '.join(missing) + fmt = _human_join(missing, final='and') message = f'You are missing {fmt} permission(s) to run this command.' super().__init__(message, *args) @@ -298,11 +289,7 @@ class BotMissingPermissions(CheckFailure): self.missing_permissions: List[str] = missing_permissions missing = [perm.replace('_', ' ').replace('guild', 'server').title() for perm in missing_permissions] - - if len(missing) > 2: - fmt = '{}, and {}'.format(", ".join(missing[:-1]), missing[-1]) - else: - fmt = ' and '.join(missing) + fmt = _human_join(missing, final='and') message = f'Bot requires {fmt} permission(s) to run this command.' super().__init__(message, *args) diff --git a/discord/ext/commands/errors.py b/discord/ext/commands/errors.py index 736d0b5af..0c1e0f2d0 100644 --- a/discord/ext/commands/errors.py +++ b/discord/ext/commands/errors.py @@ -27,6 +27,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any, Callable, List, Optional, Tuple, Union from discord.errors import ClientException, DiscordException +from discord.utils import _human_join if TYPE_CHECKING: from discord.abc import GuildChannel @@ -758,12 +759,7 @@ class MissingAnyRole(CheckFailure): self.missing_roles: SnowflakeList = missing_roles missing = [f"'{role}'" for role in missing_roles] - - if len(missing) > 2: - fmt = '{}, or {}'.format(', '.join(missing[:-1]), missing[-1]) - else: - fmt = ' or '.join(missing) - + fmt = _human_join(missing) message = f'You are missing at least one of the required roles: {fmt}' super().__init__(message) @@ -788,12 +784,7 @@ class BotMissingAnyRole(CheckFailure): self.missing_roles: SnowflakeList = missing_roles missing = [f"'{role}'" for role in missing_roles] - - if len(missing) > 2: - fmt = '{}, or {}'.format(', '.join(missing[:-1]), missing[-1]) - else: - fmt = ' or '.join(missing) - + fmt = _human_join(missing) message = f'Bot is missing at least one of the required roles: {fmt}' super().__init__(message) @@ -832,11 +823,7 @@ class MissingPermissions(CheckFailure): self.missing_permissions: List[str] = missing_permissions missing = [perm.replace('_', ' ').replace('guild', 'server').title() for perm in missing_permissions] - - if len(missing) > 2: - fmt = '{}, and {}'.format(', '.join(missing[:-1]), missing[-1]) - else: - fmt = ' and '.join(missing) + fmt = _human_join(missing, final='and') message = f'You are missing {fmt} permission(s) to run this command.' super().__init__(message, *args) @@ -857,11 +844,7 @@ class BotMissingPermissions(CheckFailure): self.missing_permissions: List[str] = missing_permissions missing = [perm.replace('_', ' ').replace('guild', 'server').title() for perm in missing_permissions] - - if len(missing) > 2: - fmt = '{}, and {}'.format(', '.join(missing[:-1]), missing[-1]) - else: - fmt = ' and '.join(missing) + fmt = _human_join(missing, final='and') message = f'Bot requires {fmt} permission(s) to run this command.' super().__init__(message, *args) @@ -896,11 +879,7 @@ class BadUnionArgument(UserInputError): return x.__class__.__name__ to_string = [_get_name(x) for x in converters] - if len(to_string) > 2: - fmt = '{}, or {}'.format(', '.join(to_string[:-1]), to_string[-1]) - else: - fmt = ' or '.join(to_string) - + fmt = _human_join(to_string) super().__init__(f'Could not convert "{param.displayed_name or param.name}" into {fmt}.') @@ -933,11 +912,7 @@ class BadLiteralArgument(UserInputError): self.argument: str = argument to_string = [repr(l) for l in literals] - if len(to_string) > 2: - fmt = '{}, or {}'.format(', '.join(to_string[:-1]), to_string[-1]) - else: - fmt = ' or '.join(to_string) - + fmt = _human_join(to_string) super().__init__(f'Could not convert "{param.displayed_name or param.name}" into the literal {fmt}.') diff --git a/discord/ui/select.py b/discord/ui/select.py index b0807626f..094c6a32c 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -45,7 +45,7 @@ from .item import Item, ItemCallbackType from ..enums import ChannelType, ComponentType, SelectDefaultValueType from ..partial_emoji import PartialEmoji from ..emoji import Emoji -from ..utils import MISSING +from ..utils import MISSING, _human_join from ..components import ( SelectOption, SelectMenu, @@ -160,15 +160,7 @@ def _handle_select_defaults( object_type = obj.__class__ if not isinstance(obj, Object) else obj.type if not _is_valid_object_type(object_type, component_type, type_to_supported_classes): - # TODO: split this into a util function - supported_classes = [c.__name__ for c in type_to_supported_classes[component_type]] - if len(supported_classes) > 2: - supported_classes = ', '.join(supported_classes[:-1]) + f', or {supported_classes[-1]}' - elif len(supported_classes) == 2: - supported_classes = f'{supported_classes[0]} or {supported_classes[1]}' - else: - supported_classes = supported_classes[0] - + supported_classes = _human_join([c.__name__ for c in type_to_supported_classes[component_type]]) raise TypeError(f'Expected an instance of {supported_classes} not {object_type.__name__}') if object_type is Object: @@ -1042,8 +1034,8 @@ def select( raise TypeError('select function must be a coroutine function') callback_cls = getattr(cls, '__origin__', cls) if not issubclass(callback_cls, BaseSelect): - supported_classes = ", ".join(["ChannelSelect", "MentionableSelect", "RoleSelect", "Select", "UserSelect"]) - raise TypeError(f'cls must be one of {supported_classes} or a subclass of one of them, not {cls!r}.') + supported_classes = ', '.join(['ChannelSelect', 'MentionableSelect', 'RoleSelect', 'Select', 'UserSelect']) + raise TypeError(f'cls must be one of {supported_classes} or a subclass of one of them, not {cls.__name__}.') func.__discord_ui_model_type__ = callback_cls func.__discord_ui_model_kwargs__ = { diff --git a/discord/utils.py b/discord/utils.py index a3f830019..33a4020a2 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -1380,3 +1380,17 @@ CAMEL_CASE_REGEX = re.compile(r'(? str: return CAMEL_CASE_REGEX.sub('-', text).lower() + + +def _human_join(seq: Sequence[str], /, *, delimiter: str = ', ', final: str = 'or') -> str: + size = len(seq) + if size == 0: + return '' + + if size == 1: + return seq[0] + + if size == 2: + return f'{seq[0]} {final} {seq[1]}' + + return delimiter.join(seq[:-1]) + f' {final} {seq[-1]}'