Browse Source

[commands] Add support for positional flag parameters

pull/9556/merge
Vioshim 11 months ago
committed by GitHub
parent
commit
71358b8dce
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 37
      discord/ext/commands/flags.py
  2. 20
      docs/ext/commands/commands.rst

37
discord/ext/commands/flags.py

@ -79,6 +79,10 @@ class Flag:
description: :class:`str` description: :class:`str`
The description of the flag. Shown for hybrid commands when they're The description of the flag. Shown for hybrid commands when they're
used as application commands. used as application commands.
positional: :class:`bool`
Whether the flag is positional or not. There can only be one positional flag.
.. versionadded:: 2.4
""" """
name: str = MISSING name: str = MISSING
@ -89,6 +93,7 @@ class Flag:
max_args: int = MISSING max_args: int = MISSING
override: bool = MISSING override: bool = MISSING
description: str = MISSING description: str = MISSING
positional: bool = MISSING
cast_to_dict: bool = False cast_to_dict: bool = False
@property @property
@ -109,6 +114,7 @@ def flag(
override: bool = MISSING, override: bool = MISSING,
converter: Any = MISSING, converter: Any = MISSING,
description: str = MISSING, description: str = MISSING,
positional: bool = MISSING,
) -> Any: ) -> Any:
"""Override default functionality and parameters of the underlying :class:`FlagConverter` """Override default functionality and parameters of the underlying :class:`FlagConverter`
class attributes. class attributes.
@ -136,6 +142,10 @@ def flag(
description: :class:`str` description: :class:`str`
The description of the flag. Shown for hybrid commands when they're The description of the flag. Shown for hybrid commands when they're
used as application commands. used as application commands.
positional: :class:`bool`
Whether the flag is positional or not. There can only be one positional flag.
.. versionadded:: 2.4
""" """
return Flag( return Flag(
name=name, name=name,
@ -145,6 +155,7 @@ def flag(
override=override, override=override,
annotation=converter, annotation=converter,
description=description, description=description,
positional=positional,
) )
@ -171,6 +182,7 @@ def get_flags(namespace: Dict[str, Any], globals: Dict[str, Any], locals: Dict[s
flags: Dict[str, Flag] = {} flags: Dict[str, Flag] = {}
cache: Dict[str, Any] = {} cache: Dict[str, Any] = {}
names: Set[str] = set() names: Set[str] = set()
positional: Optional[Flag] = None
for name, annotation in annotations.items(): for name, annotation in annotations.items():
flag = namespace.pop(name, MISSING) flag = namespace.pop(name, MISSING)
if isinstance(flag, Flag): if isinstance(flag, Flag):
@ -183,6 +195,11 @@ def get_flags(namespace: Dict[str, Any], globals: Dict[str, Any], locals: Dict[s
if flag.name is MISSING: if flag.name is MISSING:
flag.name = name flag.name = name
if flag.positional:
if positional is not None:
raise TypeError(f"{flag.name!r} positional flag conflicts with {positional.name!r} flag.")
positional = flag
annotation = flag.annotation = resolve_annotation(flag.annotation, globals, locals, cache) annotation = flag.annotation = resolve_annotation(flag.annotation, globals, locals, cache)
if flag.default is MISSING and hasattr(annotation, '__commands_is_flag__') and annotation._can_be_constructible(): if flag.default is MISSING and hasattr(annotation, '__commands_is_flag__') and annotation._can_be_constructible():
@ -270,6 +287,7 @@ class FlagsMeta(type):
__commands_flag_case_insensitive__: bool __commands_flag_case_insensitive__: bool
__commands_flag_delimiter__: str __commands_flag_delimiter__: str
__commands_flag_prefix__: str __commands_flag_prefix__: str
__commands_flag_positional__: Optional[Flag]
def __new__( def __new__(
cls, cls,
@ -324,9 +342,13 @@ class FlagsMeta(type):
delimiter = attrs.setdefault('__commands_flag_delimiter__', ':') delimiter = attrs.setdefault('__commands_flag_delimiter__', ':')
prefix = attrs.setdefault('__commands_flag_prefix__', '') prefix = attrs.setdefault('__commands_flag_prefix__', '')
positional: Optional[Flag] = None
for flag_name, flag in get_flags(attrs, global_ns, local_ns).items(): for flag_name, flag in get_flags(attrs, global_ns, local_ns).items():
flags[flag_name] = flag flags[flag_name] = flag
aliases.update({alias_name: flag_name for alias_name in flag.aliases}) aliases.update({alias_name: flag_name for alias_name in flag.aliases})
if flag.positional:
positional = flag
attrs['__commands_flag_positional__'] = positional
forbidden = set(delimiter).union(prefix) forbidden = set(delimiter).union(prefix)
for flag_name in flags: for flag_name in flags:
@ -500,10 +522,25 @@ class FlagConverter(metaclass=FlagsMeta):
result: Dict[str, List[str]] = {} result: Dict[str, List[str]] = {}
flags = cls.__commands_flags__ flags = cls.__commands_flags__
aliases = cls.__commands_flag_aliases__ aliases = cls.__commands_flag_aliases__
positional_flag = cls.__commands_flag_positional__
last_position = 0 last_position = 0
last_flag: Optional[Flag] = None last_flag: Optional[Flag] = None
case_insensitive = cls.__commands_flag_case_insensitive__ case_insensitive = cls.__commands_flag_case_insensitive__
if positional_flag is not None:
match = cls.__commands_flag_regex__.search(argument)
if match is not None:
begin, end = match.span(0)
value = argument[:begin].strip()
else:
value = argument.strip()
last_position = len(argument)
if value:
name = positional_flag.name.casefold() if case_insensitive else positional_flag.name
result[name] = [value]
for match in cls.__commands_flag_regex__.finditer(argument): for match in cls.__commands_flag_regex__.finditer(argument):
begin, end = match.span(0) begin, end = match.span(0)
key = match.group('flag') key = match.group('flag')

20
docs/ext/commands/commands.rst

@ -778,6 +778,19 @@ This tells the parser that the ``members`` attribute is mapped to a flag named `
the default value is an empty list. For greater customisability, the default can either be a value or a callable 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. that takes the :class:`~ext.commands.Context` as a sole parameter. This callable can either be a function or a coroutine.
A positional flag can be defined by setting the :attr:`~ext.commands.Flag.positional` attribute to ``True``. This
tells the parser that the content provided before the parsing occurs is part of the flag. This is useful for commands that
require a parameter to be used first and the flags are optional, such as the following:
.. code-block:: python3
class BanFlags(commands.FlagConverter):
members: List[discord.Member] = commands.flag(name='member', positional=True, default=lambda ctx: [])
reason: Optional[str] = None
.. note::
Only one positional flag is allowed in a flag converter.
In order to customise the flag syntax we also have a few options that can be passed to the class parameter list: 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 .. code-block:: python3
@ -796,12 +809,17 @@ In order to customise the flag syntax we also have a few options that can be pas
topic: Optional[str] topic: Optional[str]
nsfw: Optional[bool] nsfw: Optional[bool]
slowmode: Optional[int] slowmode: Optional[int]
# Hello there --bold True
class Greeting(commands.FlagConverter):
text: str = commands.flag(positional=True)
bold: bool = False
.. note:: .. note::
Despite the similarities in these examples to command like arguments, the syntax and parser is not Despite the similarities in these examples to command like arguments, the syntax and parser is not
a command line parser. The syntax is mainly inspired by Discord's search bar input and as a result a command line parser. The syntax is mainly inspired by Discord's search bar input and as a result
all flags need a corresponding value. all flags need a corresponding value unless part of a positional flag.
Flag converters will only raise :exc:`~ext.commands.FlagError` derived exceptions. If an error is raised while Flag converters will only raise :exc:`~ext.commands.FlagError` derived exceptions. If an error is raised while
converting a flag, :exc:`~ext.commands.BadFlagArgument` is raised instead and the original exception converting a flag, :exc:`~ext.commands.BadFlagArgument` is raised instead and the original exception

Loading…
Cancel
Save