mirror of
				https://github.com/Rapptz/discord.py.git
				synced 2025-10-25 02:23:04 +00:00 
			
		
		
		
	[commands] Initial support for FlagConverter
The name is currently pending and there's no command.signature hook for it yet since this requires bikeshedding.
This commit is contained in:
		| @@ -16,3 +16,4 @@ from .help import * | ||||
| from .converter import * | ||||
| from .cooldowns import * | ||||
| from .cog import * | ||||
| from .flags import * | ||||
|   | ||||
| @@ -75,6 +75,10 @@ __all__ = ( | ||||
|     'ExtensionFailed', | ||||
|     'ExtensionNotFound', | ||||
|     'CommandRegistrationError', | ||||
|     'BadFlagArgument', | ||||
|     'MissingFlagArgument', | ||||
|     'TooManyFlags', | ||||
|     'MissingRequiredFlag', | ||||
| ) | ||||
|  | ||||
| class CommandError(DiscordException): | ||||
| @@ -855,3 +859,76 @@ class CommandRegistrationError(ClientException): | ||||
|         self.alias_conflict = alias_conflict | ||||
|         type_ = 'alias' if alias_conflict else 'command' | ||||
|         super().__init__(f'The {type_} {name} is already an existing command or alias.') | ||||
|  | ||||
| class FlagError(BadArgument): | ||||
|     """The base exception type for all flag parsing related errors. | ||||
|  | ||||
|     This inherits from :exc:`BadArgument`. | ||||
|  | ||||
|     .. versionadded:: 2.0 | ||||
|     """ | ||||
|     pass | ||||
|  | ||||
| class TooManyFlags(FlagError): | ||||
|     """An exception raised when a flag has received too many values. | ||||
|  | ||||
|     This inherits from :exc:`FlagError`. | ||||
|  | ||||
|     .. versionadded:: 2.0 | ||||
|  | ||||
|     Attributes | ||||
|     ------------ | ||||
|     flag: :class:`~discord.ext.commands.Flag` | ||||
|         The flag that received too many values. | ||||
|     values: List[:class:`str`] | ||||
|         The values that were passed. | ||||
|     """ | ||||
|     def __init__(self, flag, values): | ||||
|         self.flag = flag | ||||
|         self.values = values | ||||
|         super().__init__(f'Too many flag values, expected {flag.max_args} but received {len(values)}.') | ||||
|  | ||||
| class BadFlagArgument(FlagError): | ||||
|     """An exception raised when a flag failed to convert a value. | ||||
|  | ||||
|     """ | ||||
|     def __init__(self, flag): | ||||
|         self.flag = flag | ||||
|         try: | ||||
|             name = flag.annotation.__name__ | ||||
|         except AttributeError: | ||||
|             name = flag.annotation.__class__.__name__ | ||||
|  | ||||
|         super().__init__(f'Could not convert to {name!r} for flag {flag.name!r}') | ||||
|  | ||||
| class MissingRequiredFlag(FlagError): | ||||
|     """An exception raised when a required flag was not given. | ||||
|  | ||||
|     This inherits from :exc:`FlagError` | ||||
|  | ||||
|     .. versionadded:: 2.0 | ||||
|  | ||||
|     Attributes | ||||
|     ----------- | ||||
|     flag: :class:`~discord.ext.commands.Flag` | ||||
|         The required flag that was not found. | ||||
|     """ | ||||
|     def __init__(self, flag): | ||||
|         self.flag = flag | ||||
|         super().__init__(f'Flag {flag.name!r} is required and missing') | ||||
|  | ||||
| class MissingFlagArgument(FlagError): | ||||
|     """An exception raised when a flag did not get a value. | ||||
|  | ||||
|     This inherits from :exc:`FlagError` | ||||
|  | ||||
|     .. versionadded:: 2.0 | ||||
|  | ||||
|     Attributes | ||||
|     ----------- | ||||
|     flag: :class:`~discord.ext.commands.Flag` | ||||
|         The flag that did not get a value. | ||||
|     """ | ||||
|     def __init__(self, flag): | ||||
|         self.flag = flag | ||||
|         super().__init__(f'Flag {flag.name!r} does not have an argument') | ||||
|   | ||||
							
								
								
									
										530
									
								
								discord/ext/commands/flags.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										530
									
								
								discord/ext/commands/flags.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,530 @@ | ||||
| """ | ||||
| The MIT License (MIT) | ||||
|  | ||||
| Copyright (c) 2015-present Rapptz | ||||
|  | ||||
| Permission is hereby granted, free of charge, to any person obtaining a | ||||
| copy of this software and associated documentation files (the "Software"), | ||||
| to deal in the Software without restriction, including without limitation | ||||
| the rights to use, copy, modify, merge, publish, distribute, sublicense, | ||||
| and/or sell copies of the Software, and to permit persons to whom the | ||||
| Software is furnished to do so, subject to the following conditions: | ||||
|  | ||||
| The above copyright notice and this permission notice shall be included in | ||||
| all copies or substantial portions of the Software. | ||||
|  | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | ||||
| OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | ||||
| FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | ||||
| DEALINGS IN THE SOFTWARE. | ||||
| """ | ||||
|  | ||||
| from __future__ import annotations | ||||
|  | ||||
| from .errors import ( | ||||
|     BadFlagArgument, | ||||
|     CommandError, | ||||
|     MissingFlagArgument, | ||||
|     TooManyFlags, | ||||
|     MissingRequiredFlag, | ||||
| ) | ||||
|  | ||||
| from .core import resolve_annotation | ||||
| from .view import StringView | ||||
| from .converter import run_converters | ||||
|  | ||||
| from discord.utils import maybe_coroutine | ||||
| from dataclasses import dataclass | ||||
| from typing import ( | ||||
|     Dict, | ||||
|     Optional, | ||||
|     Pattern, | ||||
|     Set, | ||||
|     TYPE_CHECKING, | ||||
|     Tuple, | ||||
|     List, | ||||
|     Any, | ||||
|     Type, | ||||
|     TypeVar, | ||||
|     Union, | ||||
| ) | ||||
|  | ||||
| import inspect | ||||
| import sys | ||||
| import re | ||||
|  | ||||
| __all__ = ( | ||||
|     'Flag', | ||||
|     'flag', | ||||
|     'FlagConverter', | ||||
| ) | ||||
|  | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from .context import Context | ||||
|  | ||||
|  | ||||
| class _MissingSentinel: | ||||
|     def __repr__(self): | ||||
|         return 'MISSING' | ||||
|  | ||||
|  | ||||
| MISSING: Any = _MissingSentinel() | ||||
|  | ||||
|  | ||||
| @dataclass | ||||
| class Flag: | ||||
|     """Represents a flag parameter for :class:`FlagConverter`. | ||||
|  | ||||
|     The :func:`~discord.ext.commands.flag` function helps | ||||
|     create these flag objects, but it is not necessary to | ||||
|     do so. These cannot be constructed manually. | ||||
|  | ||||
|     Attributes | ||||
|     ------------ | ||||
|     name: :class:`str` | ||||
|         The name of the flag. | ||||
|     attribute: :class:`str` | ||||
|         The attribute in the class that corresponds to this flag. | ||||
|     default: Any | ||||
|         The default value of the flag, if available. | ||||
|     annotation: Any | ||||
|         The underlying evaluated annotation of the flag. | ||||
|     max_args: :class:`int` | ||||
|         The maximum number of arguments the flag can accept. | ||||
|         A negative value indicates an unlimited amount of arguments. | ||||
|     override: :class:`bool` | ||||
|         Whether multiple given values overrides the previous value. | ||||
|     """ | ||||
|  | ||||
|     name: str = MISSING | ||||
|     attribute: str = MISSING | ||||
|     annotation: Any = MISSING | ||||
|     default: Any = MISSING | ||||
|     max_args: int = MISSING | ||||
|     override: bool = MISSING | ||||
|     cast_to_dict: bool = False | ||||
|  | ||||
|     @property | ||||
|     def required(self) -> bool: | ||||
|         """:class:`bool`: Whether the flag is required. | ||||
|  | ||||
|         A required flag has no default value. | ||||
|         """ | ||||
|         return self.default is MISSING | ||||
|  | ||||
|  | ||||
| def flag( | ||||
|     *, | ||||
|     name: str = MISSING, | ||||
|     default: Any = MISSING, | ||||
|     max_args: int = MISSING, | ||||
|     override: bool = MISSING, | ||||
| ) -> Any: | ||||
|     """Override default functionality and parameters of the underlying :class:`FlagConverter` | ||||
|     class attributes. | ||||
|  | ||||
|     Parameters | ||||
|     ------------ | ||||
|     name: :class:`str` | ||||
|         The flag name. If not given, defaults to the attribute name. | ||||
|     default: Any | ||||
|         The default parameter. This could be either a value or a callable that takes | ||||
|         :class:`Context` as its sole parameter. If not given then it defaults to | ||||
|         the default value given to the attribute. | ||||
|     max_args: :class:`int` | ||||
|         The maximum number of arguments the flag can accept. | ||||
|         A negative value indicates an unlimited amount of arguments. | ||||
|         The default value depends on the annotation given. | ||||
|     override: :class:`bool` | ||||
|         Whether multiple given values overrides the previous value. The default | ||||
|         value depends on the annotation given. | ||||
|     """ | ||||
|     return Flag(name=name, default=default, max_args=max_args, override=override) | ||||
|  | ||||
|  | ||||
| def validate_flag_name(name: str, forbidden: Set[str]): | ||||
|     if not name: | ||||
|         raise ValueError('flag names should not be empty') | ||||
|  | ||||
|     for ch in name: | ||||
|         if ch.isspace(): | ||||
|             raise ValueError(f'flag name {name!r} cannot have spaces') | ||||
|         if ch == '\\': | ||||
|             raise ValueError(f'flag name {name!r} cannot have backslashes') | ||||
|         if ch in forbidden: | ||||
|             raise ValueError(f'flag name {name!r} cannot have any of {forbidden!r} within them') | ||||
|  | ||||
|  | ||||
| def get_flags(namespace: Dict[str, Any], globals: Dict[str, Any], locals: Dict[str, Any]) -> Dict[str, Flag]: | ||||
|     annotations = namespace.get('__annotations__', {}) | ||||
|     flags: Dict[str, Flag] = {} | ||||
|     cache: Dict[str, Any] = {} | ||||
|     for name, annotation in annotations.items(): | ||||
|         flag = namespace.pop(name, MISSING) | ||||
|         if isinstance(flag, Flag): | ||||
|             flag.annotation = annotation | ||||
|         else: | ||||
|             flag = Flag(name=name, annotation=annotation, default=flag) | ||||
|  | ||||
|         flag.attribute = name | ||||
|         if flag.name is MISSING: | ||||
|             flag.name = name | ||||
|  | ||||
|         annotation = flag.annotation = resolve_annotation(flag.annotation, globals, locals, cache) | ||||
|  | ||||
|         # Add sensible defaults based off of the type annotation | ||||
|         # <type> -> (max_args=1) | ||||
|         # List[str] -> (max_args=-1) | ||||
|         # Tuple[int, ...] -> (max_args=1) | ||||
|         # Dict[K, V] -> (max_args=-1, override=True) | ||||
|         # Optional[str] -> (default=None, max_args=1) | ||||
|  | ||||
|         try: | ||||
|             origin = annotation.__origin__ | ||||
|         except AttributeError: | ||||
|             # A regular type hint | ||||
|             if flag.max_args is MISSING: | ||||
|                 flag.max_args = 1 | ||||
|         else: | ||||
|             if origin is Union and annotation.__args__[-1] is type(None): | ||||
|                 # typing.Optional | ||||
|                 if flag.max_args is MISSING: | ||||
|                     flag.max_args = 1 | ||||
|                 if flag.default is MISSING: | ||||
|                     flag.default = None | ||||
|             elif origin is tuple: | ||||
|                 # typing.Tuple | ||||
|                 # tuple parsing is e.g. `flag: peter 20` | ||||
|                 # for Tuple[str, int] would give you flag: ('peter', 20) | ||||
|                 if flag.max_args is MISSING: | ||||
|                     flag.max_args = 1 | ||||
|             elif origin is list: | ||||
|                 # typing.List | ||||
|                 if flag.max_args is MISSING: | ||||
|                     flag.max_args = -1 | ||||
|             elif origin is dict: | ||||
|                 # typing.Dict[K, V] | ||||
|                 # Equivalent to: | ||||
|                 # typing.List[typing.Tuple[K, V]] | ||||
|                 flag.cast_to_dict = True | ||||
|                 if flag.max_args is MISSING: | ||||
|                     flag.max_args = -1 | ||||
|                 if flag.override is MISSING: | ||||
|                     flag.override = True | ||||
|             else: | ||||
|                 raise TypeError(f'Unsupported typing annotation {annotation!r} for {flag.name!r} flag') | ||||
|  | ||||
|         if flag.override is MISSING: | ||||
|             flag.override = False | ||||
|  | ||||
|         flags[flag.name] = flag | ||||
|  | ||||
|     return flags | ||||
|  | ||||
|  | ||||
| class FlagsMeta(type): | ||||
|     if TYPE_CHECKING: | ||||
|         __commands_is_flag__: bool | ||||
|         __commands_flags__: Dict[str, Flag] | ||||
|         __commands_flag_regex__: Pattern[str] | ||||
|         __commands_flag_case_insensitive__: bool | ||||
|         __commands_flag_delimiter__: str | ||||
|         __commands_flag_prefix__: str | ||||
|  | ||||
|     def __new__( | ||||
|         cls: Type[type], | ||||
|         name: str, | ||||
|         bases: Tuple[type, ...], | ||||
|         attrs: Dict[str, Any], | ||||
|         *, | ||||
|         case_insensitive: bool = False, | ||||
|         delimiter: str = ':', | ||||
|         prefix: str = '', | ||||
|     ): | ||||
|         attrs['__commands_is_flag__'] = True | ||||
|         attrs['__commands_flag_case_insensitive__'] = case_insensitive | ||||
|         attrs['__commands_flag_delimiter__'] = delimiter | ||||
|         attrs['__commands_flag_prefix__'] = prefix | ||||
|  | ||||
|         if not prefix and not delimiter: | ||||
|             raise TypeError('Must have either a delimiter or a prefix set') | ||||
|  | ||||
|         try: | ||||
|             global_ns = sys.modules[attrs['__module__']].__dict__ | ||||
|         except KeyError: | ||||
|             global_ns = {} | ||||
|  | ||||
|         frame = inspect.currentframe() | ||||
|         try: | ||||
|             if frame is None: | ||||
|                 local_ns = {} | ||||
|             else: | ||||
|                 if frame.f_back is None: | ||||
|                     local_ns = frame.f_locals | ||||
|                 else: | ||||
|                     local_ns = frame.f_back.f_locals | ||||
|         finally: | ||||
|             del frame | ||||
|  | ||||
|         flags: Dict[str, Flag] = {} | ||||
|         for base in reversed(bases): | ||||
|             if base.__dict__.get('__commands_is_flag__', False): | ||||
|                 flags.update(base.__dict__['__commands_flags__']) | ||||
|  | ||||
|         flags.update(get_flags(attrs, global_ns, local_ns)) | ||||
|         forbidden = set(delimiter).union(prefix) | ||||
|         for flag_name in flags: | ||||
|             validate_flag_name(flag_name, forbidden) | ||||
|  | ||||
|         regex_flags = 0 | ||||
|         if case_insensitive: | ||||
|             flags = {key.casefold(): value for key, value in flags.items()} | ||||
|             regex_flags = re.IGNORECASE | ||||
|  | ||||
|         keys = sorted((re.escape(k) for k in flags), key=lambda t: len(t), reverse=True) | ||||
|         joined = '|'.join(keys) | ||||
|         pattern = re.compile(f'(({re.escape(prefix)})(?P<flag>{joined}){re.escape(delimiter)})', regex_flags) | ||||
|         attrs['__commands_flag_regex__'] = pattern | ||||
|         attrs['__commands_flags__'] = flags | ||||
|  | ||||
|         return type.__new__(cls, name, bases, attrs) | ||||
|  | ||||
|  | ||||
| async def tuple_convert_all(ctx: Context, argument: str, flag: Flag, converter: Any) -> Tuple[Any, ...]: | ||||
|     view = StringView(argument) | ||||
|     results = [] | ||||
|     param: inspect.Parameter = ctx.current_parameter  # type: ignore | ||||
|     while not view.eof: | ||||
|         view.skip_ws() | ||||
|         if view.eof: | ||||
|             break | ||||
|  | ||||
|         word = view.get_quoted_word() | ||||
|         if word is None: | ||||
|             break | ||||
|  | ||||
|         try: | ||||
|             converted = await run_converters(ctx, converter, word, param) | ||||
|         except CommandError: | ||||
|             raise | ||||
|         except Exception as e: | ||||
|             raise BadFlagArgument(flag) from e | ||||
|         else: | ||||
|             results.append(converted) | ||||
|  | ||||
|     return tuple(results) | ||||
|  | ||||
|  | ||||
| async def tuple_convert_flag(ctx: Context, argument: str, flag: Flag, converters: Any) -> Tuple[Any, ...]: | ||||
|     view = StringView(argument) | ||||
|     results = [] | ||||
|     param: inspect.Parameter = ctx.current_parameter  # type: ignore | ||||
|     for converter in converters: | ||||
|         view.skip_ws() | ||||
|         if view.eof: | ||||
|             break | ||||
|  | ||||
|         word = view.get_quoted_word() | ||||
|         if word is None: | ||||
|             break | ||||
|  | ||||
|         try: | ||||
|             converted = await run_converters(ctx, converter, word, param) | ||||
|         except CommandError: | ||||
|             raise | ||||
|         except Exception as e: | ||||
|             raise BadFlagArgument(flag) from e | ||||
|         else: | ||||
|             results.append(converted) | ||||
|  | ||||
|     if len(results) != len(converters): | ||||
|         raise BadFlagArgument(flag) | ||||
|  | ||||
|     return tuple(results) | ||||
|  | ||||
|  | ||||
| async def convert_flag(ctx, argument: str, flag: Flag, annotation: Any = None) -> Any: | ||||
|     param: inspect.Parameter = ctx.current_parameter  # type: ignore | ||||
|     annotation = annotation or flag.annotation | ||||
|     try: | ||||
|         origin = annotation.__origin__ | ||||
|     except AttributeError: | ||||
|         pass | ||||
|     else: | ||||
|         if origin is tuple: | ||||
|             if annotation.__args__[-1] is Ellipsis: | ||||
|                 return await tuple_convert_all(ctx, argument, flag, annotation.__args__[0]) | ||||
|             else: | ||||
|                 return await tuple_convert_flag(ctx, argument, flag, annotation.__args__) | ||||
|         elif origin is list or origin is Union and annotation.__args__[-1] is type(None): | ||||
|             # typing.List[x] or typing.Optional[x] | ||||
|             annotation = annotation.__args__[0] | ||||
|             return await convert_flag(ctx, argument, flag, annotation) | ||||
|         elif origin is dict: | ||||
|             # typing.Dict[K, V] -> typing.Tuple[K, V] | ||||
|             return await tuple_convert_flag(ctx, argument, flag, annotation.__args__) | ||||
|  | ||||
|     try: | ||||
|         return await run_converters(ctx, annotation, argument, param) | ||||
|     except CommandError: | ||||
|         raise | ||||
|     except Exception as e: | ||||
|         raise BadFlagArgument(flag) from e | ||||
|  | ||||
|  | ||||
| F = TypeVar('F', bound='FlagConverter') | ||||
|  | ||||
|  | ||||
| class FlagConverter(metaclass=FlagsMeta): | ||||
|     """A converter that allows for a user-friendly flag syntax. | ||||
|  | ||||
|     The flags are defined using :pep:`526` type annotations similar | ||||
|     to the :mod:`dataclasses` Python module. For more information on | ||||
|     how this converter works, check the appropriate | ||||
|     :ref:`documentation <ext_commands_flag_converter>`. | ||||
|  | ||||
|     .. versionadded:: 2.0 | ||||
|  | ||||
|     Parameters | ||||
|     ----------- | ||||
|     case_insensitive: :class:`bool` | ||||
|         A class parameter to toggle case insensitivity of the flag parsing. | ||||
|         If ``True`` then flags are parsed in a case insensitive manner. | ||||
|         Defaults to ``False``. | ||||
|     prefix: :class:`str` | ||||
|         The prefix that all flags must be prefixed with. By default | ||||
|         there is no prefix. | ||||
|     delimiter: :class:`str` | ||||
|         The delimiter that separates a flag's argument from the flag's name. | ||||
|         By default this is ``:``. | ||||
|     """ | ||||
|  | ||||
|     @classmethod | ||||
|     def get_flags(cls) -> Dict[str, Flag]: | ||||
|         """Dict[:class:`str`, :class:`Flag`]: A mapping of flag name to flag object this converter has.""" | ||||
|         return cls.__commands_flags__.copy() | ||||
|  | ||||
|     def __repr__(self) -> str: | ||||
|         pairs = ' '.join([f'{flag.attribute}={getattr(self, flag.attribute)!r}' for flag in self.get_flags().values()]) | ||||
|         return f'<{self.__class__.__name__} {pairs}>' | ||||
|  | ||||
|     @classmethod | ||||
|     def parse_flags(cls, argument: str) -> Dict[str, List[str]]: | ||||
|         result: Dict[str, List[str]] = {} | ||||
|         flags = cls.get_flags() | ||||
|         last_position = 0 | ||||
|         last_flag: Optional[Flag] = None | ||||
|  | ||||
|         case_insensitive = cls.__commands_flag_case_insensitive__ | ||||
|         for match in cls.__commands_flag_regex__.finditer(argument): | ||||
|             begin, end = match.span(0) | ||||
|             key = match.group('flag') | ||||
|             if case_insensitive: | ||||
|                 key = key.casefold() | ||||
|  | ||||
|             flag = flags.get(key) | ||||
|             if last_position and last_flag is not None: | ||||
|                 value = argument[last_position : begin - 1].lstrip() | ||||
|                 if not value: | ||||
|                     raise MissingFlagArgument(last_flag) | ||||
|  | ||||
|                 try: | ||||
|                     values = result[last_flag.name] | ||||
|                 except KeyError: | ||||
|                     result[last_flag.name] = [value] | ||||
|                 else: | ||||
|                     values.append(value) | ||||
|  | ||||
|             last_position = end | ||||
|             last_flag = flag | ||||
|  | ||||
|         # Add the remaining string to the last available flag | ||||
|         if last_position and last_flag is not None: | ||||
|             value = argument[last_position:].strip() | ||||
|             if not value: | ||||
|                 raise MissingFlagArgument(last_flag) | ||||
|  | ||||
|             try: | ||||
|                 values = result[last_flag.name] | ||||
|             except KeyError: | ||||
|                 result[last_flag.name] = [value] | ||||
|             else: | ||||
|                 values.append(value) | ||||
|  | ||||
|         # Verification of values will come at a later stage | ||||
|         return result | ||||
|  | ||||
|     @classmethod | ||||
|     async def convert(cls: Type[F], ctx: Context, argument: str) -> F: | ||||
|         """|coro| | ||||
|  | ||||
|         The method that actually converters an argument to the flag mapping. | ||||
|  | ||||
|         Parameters | ||||
|         ---------- | ||||
|         cls: Type[:class:`FlagConverter`] | ||||
|             The flag converter class. | ||||
|         ctx: :class:`Context` | ||||
|             The invocation context. | ||||
|         argument: :class:`str` | ||||
|             The argument to convert from. | ||||
|  | ||||
|         Raises | ||||
|         -------- | ||||
|         FlagError | ||||
|             A flag related parsing error. | ||||
|         CommandError | ||||
|             A command related error. | ||||
|  | ||||
|         Returns | ||||
|         -------- | ||||
|         :class:`FlagConverter` | ||||
|             The flag converter instance with all flags parsed. | ||||
|         """ | ||||
|         arguments = cls.parse_flags(argument) | ||||
|         flags = cls.get_flags() | ||||
|  | ||||
|         self: F = cls.__new__(cls) | ||||
|         for name, flag in flags.items(): | ||||
|             try: | ||||
|                 values = arguments[name] | ||||
|             except KeyError: | ||||
|                 if flag.required: | ||||
|                     raise MissingRequiredFlag(flag) | ||||
|                 else: | ||||
|                     if callable(flag.default): | ||||
|                         default = await maybe_coroutine(flag.default, ctx) | ||||
|                         setattr(self, flag.attribute, default) | ||||
|                     else: | ||||
|                         setattr(self, flag.attribute, flag.default) | ||||
|                     continue | ||||
|  | ||||
|             if flag.max_args > 0 and len(values) > flag.max_args: | ||||
|                 if flag.override: | ||||
|                     values = values[-flag.max_args :] | ||||
|                 else: | ||||
|                     raise TooManyFlags(flag, values) | ||||
|  | ||||
|             # Special case: | ||||
|             if flag.max_args == 1: | ||||
|                 value = await convert_flag(ctx, values[0], flag) | ||||
|                 setattr(self, flag.attribute, value) | ||||
|                 continue | ||||
|  | ||||
|             # Another special case, tuple parsing. | ||||
|             # Tuple parsing is basically converting arguments within the flag | ||||
|             # So, given flag: hello 20 as the input and Tuple[str, int] as the type hint | ||||
|             # We would receive ('hello', 20) as the resulting value | ||||
|             # This uses the same whitespace and quoting rules as regular parameters. | ||||
|             values = [await convert_flag(ctx, value, flag) for value in values] | ||||
|  | ||||
|             if flag.cast_to_dict: | ||||
|                 values = dict(values)  # type: ignore | ||||
|  | ||||
|             setattr(self, flag.attribute, values) | ||||
|  | ||||
|         return self | ||||
| @@ -331,6 +331,17 @@ Converters | ||||
|  | ||||
| .. autofunction:: discord.ext.commands.run_converters | ||||
|  | ||||
| Flag Converter | ||||
| ~~~~~~~~~~~~~~~ | ||||
|  | ||||
| .. autoclass:: discord.ext.commands.FlagConverter | ||||
|     :members: | ||||
|  | ||||
| .. autoclass:: discord.ext.commands.Flag() | ||||
|     :members: | ||||
|  | ||||
| .. autofunction:: discord.ext.commands.flag | ||||
|  | ||||
| .. _ext_commands_api_errors: | ||||
|  | ||||
| Exceptions | ||||
| @@ -456,6 +467,18 @@ Exceptions | ||||
| .. autoexception:: discord.ext.commands.NSFWChannelRequired | ||||
|     :members: | ||||
|  | ||||
| .. autoexception:: discord.ext.commands.BadFlagArgument | ||||
|     :members: | ||||
|  | ||||
| .. autoexception:: discord.ext.commands.MissingFlagArgument | ||||
|     :members: | ||||
|  | ||||
| .. autoexception:: discord.ext.commands.TooManyFlags | ||||
|     :members: | ||||
|  | ||||
| .. autoexception:: discord.ext.commands.MissingRequiredFlag | ||||
|     :members: | ||||
|  | ||||
| .. autoexception:: discord.ext.commands.ExtensionError | ||||
|     :members: | ||||
|  | ||||
| @@ -501,6 +524,10 @@ Exception Hierarchy | ||||
|                     - :exc:`~.commands.EmojiNotFound` | ||||
|                     - :exc:`~.commands.PartialEmojiConversionFailure` | ||||
|                     - :exc:`~.commands.BadBoolArgument` | ||||
|                     - :exc:`~.commands.BadFlagArgument` | ||||
|                         - :exc:`~.commands.MissingFlagArgument` | ||||
|                         - :exc:`~.commands.TooManyFlags` | ||||
|                         - :exc:`~.commands.MissingRequiredFlag` | ||||
|                 - :exc:`~.commands.BadUnionArgument` | ||||
|                 - :exc:`~.commands.ArgumentParsingError` | ||||
|                     - :exc:`~.commands.UnexpectedQuoteError` | ||||
|   | ||||
| @@ -594,6 +594,157 @@ This command can be invoked any of the following ways: | ||||
|     To help aid with some parsing ambiguities, :class:`str`, ``None``, :data:`typing.Optional` and | ||||
|     :class:`~ext.commands.Greedy` are forbidden as parameters for the :class:`~ext.commands.Greedy` converter. | ||||
|  | ||||
| .. _ext_commands_flag_converter: | ||||
|  | ||||
| FlagConverter | ||||
| ++++++++++++++ | ||||
|  | ||||
| .. versionadded:: 2.0 | ||||
|  | ||||
| A :class:`~ext.commands.FlagConverter` allows the user to specify user-friendly "flags" using :pep:`526` type annotations | ||||
| or a syntax more reminiscent of the :mod:`py:dataclasses` module. | ||||
|  | ||||
| For example, the following code: | ||||
|  | ||||
| .. code-block:: python3 | ||||
|  | ||||
|     from discord.ext import commands | ||||
|     import discord | ||||
|  | ||||
|     class BanFlags(commands.FlagConverter): | ||||
|         member: discord.Member | ||||
|         reason: str | ||||
|         days: int = 1 | ||||
|  | ||||
|     @commands.command() | ||||
|     async def ban(ctx, *, flags: BanFlags): | ||||
|         plural = f'{flags.days} days' if flags.days != 1 else f'{flags.days} day' | ||||
|         await ctx.send(f'Banned {flags.member} for {flags.reason!r} (deleted {plural} worth of messages)') | ||||
|  | ||||
| Allows the user to invoke the command using a simple flag-like syntax: | ||||
|  | ||||
| .. image:: /images/commands/flags1.png | ||||
|  | ||||
| Flags use a syntax that allows the user to not require quotes when passing flags. The goal of the flag syntax is to be as | ||||
| user-friendly as possible. This makes flags a good choice for complicated commands that can have multiple knobs. | ||||
| **It is recommended to use keyword-only parameters with the flag converter**. This ensures proper parsing and | ||||
| behaviour with quoting. | ||||
|  | ||||
| The :class:`~ext.commands.FlagConverter` class examines the class to find flags. A flag can either be a | ||||
| class variable with a type annotation or a class variable that's been assigned the result of the :func:`~ext.commands.flag` | ||||
| function. | ||||
|  | ||||
| For most use cases, no extra work is required to define flags. However, if customisation is needed to control the flag name | ||||
| or the default value then the :func:`~ext.commands.flag` function can come in handy: | ||||
|  | ||||
| .. code-block:: python3 | ||||
|  | ||||
|     from typing import List | ||||
|  | ||||
|     class BanFlags(commands.FlagConverter): | ||||
|         members: List[discord.Member] = commands.flag(name='member', default=lambda ctx: []) | ||||
|  | ||||
| This tells the parser that the ``members`` attribute is mapped to a flag named ``member`` and that | ||||
| the default value is an empty list. For greater customisability, the default can either be a value or a callable | ||||
| that takes the :class:`~ext.commands.Context` as a sole parameter. This callable can either be a function or a coroutine. | ||||
|  | ||||
| In order to customise the flag syntax we also have a few options that can be passed to the class parameter list: | ||||
|  | ||||
| .. code-block:: python3 | ||||
|  | ||||
|     # --hello=world syntax | ||||
|     class PosixLikeFlags(commands.FlagConverter, delimiter='=', prefix='--'): | ||||
|         hello: str | ||||
|  | ||||
|  | ||||
|     # /make food | ||||
|     class WindowsLikeFlags(commands.FlagConverter, prefix='/', delimiter=''): | ||||
|         make: str | ||||
|  | ||||
|     # TOPIC: not allowed nsfw: yes Slowmode: 100 | ||||
|     class Settings(commands.FlagConverter, case_insentitive=True): | ||||
|         topic: Optional[str] | ||||
|         nsfw: Optional[bool] | ||||
|         slowmode: Optional[int] | ||||
|  | ||||
| The flag converter is similar to regular commands and allows you to use most types of converters | ||||
| (with the exception of :class:`~ext.commands.Greedy`) as the type annotation. Some extra support is added for specific | ||||
| annotations as described below. | ||||
|  | ||||
| typing.List | ||||
| ^^^^^^^^^^^^^ | ||||
|  | ||||
| If a list is given as a flag annotation it tells the parser that the argument can be passed multiple times. | ||||
|  | ||||
| For example, augmenting the example above: | ||||
|  | ||||
| .. code-block:: python3 | ||||
|  | ||||
|     from discord.ext import commands | ||||
|     from typing import List | ||||
|     import discord | ||||
|  | ||||
|     class BanFlags(commands.FlagConverter): | ||||
|         members: List[discord.Member] = commands.flag(name='member') | ||||
|         reason: str | ||||
|         days: int = 1 | ||||
|  | ||||
|     @commands.command() | ||||
|     async def ban(ctx, *, flags: BanFlags): | ||||
|         for member in flags.members: | ||||
|             await member.ban(reason=flags.reason, delete_message_days=flags.days) | ||||
|  | ||||
|         members = ', '.join(str(member) for member in flags.members) | ||||
|         plural = f'{flags.days} days' if flags.days != 1 else f'{flags.days} day' | ||||
|         await ctx.send(f'Banned {members} for {flags.reason!r} (deleted {plural} worth of messages)') | ||||
|  | ||||
| This is called by repeatedly specifying the flag: | ||||
|  | ||||
| .. image:: /images/commands/flags2.png | ||||
|  | ||||
| typing.Tuple | ||||
| ^^^^^^^^^^^^^ | ||||
|  | ||||
| Since the above syntax can be a bit repetitive when specifying a flag many times, the :class:`py:tuple` type annotation | ||||
| allows for "greedy-like" semantics using a variadic tuple: | ||||
|  | ||||
| .. code-block:: python3 | ||||
|  | ||||
|     from discord.ext import commands | ||||
|     from typing import Tuple | ||||
|     import discord | ||||
|  | ||||
|     class BanFlags(commands.FlagConverter): | ||||
|         members: Tuple[discord.Member, ...] | ||||
|         reason: str | ||||
|         days: int = 1 | ||||
|  | ||||
| This allows the previous ``ban`` command to be called like this: | ||||
|  | ||||
| .. image:: /images/commands/flags3.png | ||||
|  | ||||
| The :class:`py:tuple` annotation also allows for parsing of pairs. For example, given the following code: | ||||
|  | ||||
| .. code-block:: python3 | ||||
|  | ||||
|     # point: 10 11 point: 12 13 | ||||
|     class Coordinates(commands.FlagConverter): | ||||
|         point: Tuple[int, int] | ||||
|  | ||||
|  | ||||
| .. warning:: | ||||
|  | ||||
|     Due to potential parsing ambiguities, the parser expects tuple arguments to be quoted | ||||
|     if they require spaces. So if one of the inner types is :class:`str` and the argument requires spaces | ||||
|     then quotes should be used to disambiguate it from the other element of the tuple. | ||||
|  | ||||
| typing.Dict | ||||
| ^^^^^^^^^^^^^ | ||||
|  | ||||
| A :class:`dict` annotation is functionally equivalent to ``List[Tuple[K, V]]`` except with the return type | ||||
| given as a :class:`dict` rather than a :class:`list`. | ||||
|  | ||||
|  | ||||
| .. _ext_commands_error_handler: | ||||
|  | ||||
| Error Handling | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								docs/images/commands/flags1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/images/commands/flags1.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 25 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/images/commands/flags2.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/images/commands/flags2.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 28 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/images/commands/flags3.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/images/commands/flags3.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 27 KiB | 
		Reference in New Issue
	
	Block a user