[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:
parent
1c64689807
commit
ddb71e2aed
@ -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 |
Loading…
x
Reference in New Issue
Block a user