Skip to content

Commit

Permalink
[commands] Add support for positional flag parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
Vioshim committed May 5, 2024
1 parent 9fab99a commit 71358b8
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 1 deletion.
37 changes: 37 additions & 0 deletions discord/ext/commands/flags.py
Expand Up @@ -79,6 +79,10 @@ class Flag:
description: :class:`str`
The description of the flag. Shown for hybrid commands when they're
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
Expand All @@ -89,6 +93,7 @@ class Flag:
max_args: int = MISSING
override: bool = MISSING
description: str = MISSING
positional: bool = MISSING
cast_to_dict: bool = False

@property
Expand All @@ -109,6 +114,7 @@ def flag(
override: bool = MISSING,
converter: Any = MISSING,
description: str = MISSING,
positional: bool = MISSING,
) -> Any:
"""Override default functionality and parameters of the underlying :class:`FlagConverter`
class attributes.
Expand Down Expand Up @@ -136,6 +142,10 @@ class attributes.
description: :class:`str`
The description of the flag. Shown for hybrid commands when they're
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(
name=name,
Expand All @@ -145,6 +155,7 @@ class attributes.
override=override,
annotation=converter,
description=description,
positional=positional,
)


Expand All @@ -171,6 +182,7 @@ def get_flags(namespace: Dict[str, Any], globals: Dict[str, Any], locals: Dict[s
flags: Dict[str, Flag] = {}
cache: Dict[str, Any] = {}
names: Set[str] = set()
positional: Optional[Flag] = None
for name, annotation in annotations.items():
flag = namespace.pop(name, MISSING)
if isinstance(flag, Flag):
Expand All @@ -183,6 +195,11 @@ def get_flags(namespace: Dict[str, Any], globals: Dict[str, Any], locals: Dict[s
if flag.name is MISSING:
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)

if flag.default is MISSING and hasattr(annotation, '__commands_is_flag__') and annotation._can_be_constructible():
Expand Down Expand Up @@ -270,6 +287,7 @@ class FlagsMeta(type):
__commands_flag_case_insensitive__: bool
__commands_flag_delimiter__: str
__commands_flag_prefix__: str
__commands_flag_positional__: Optional[Flag]

def __new__(
cls,
Expand Down Expand Up @@ -324,9 +342,13 @@ def __new__(
delimiter = attrs.setdefault('__commands_flag_delimiter__', ':')
prefix = attrs.setdefault('__commands_flag_prefix__', '')

positional: Optional[Flag] = None
for flag_name, flag in get_flags(attrs, global_ns, local_ns).items():
flags[flag_name] = flag
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)
for flag_name in flags:
Expand Down Expand Up @@ -500,10 +522,25 @@ def parse_flags(cls, argument: str, *, ignore_extra: bool = True) -> Dict[str, L
result: Dict[str, List[str]] = {}
flags = cls.__commands_flags__
aliases = cls.__commands_flag_aliases__
positional_flag = cls.__commands_flag_positional__
last_position = 0
last_flag: Optional[Flag] = None

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):
begin, end = match.span(0)
key = match.group('flag')
Expand Down
20 changes: 19 additions & 1 deletion docs/ext/commands/commands.rst
Expand Up @@ -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
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:

.. code-block:: python3
Expand All @@ -796,12 +809,17 @@ In order to customise the flag syntax we also have a few options that can be pas
topic: Optional[str]
nsfw: Optional[bool]
slowmode: Optional[int]
# Hello there --bold True
class Greeting(commands.FlagConverter):
text: str = commands.flag(positional=True)
bold: bool = False
.. note::

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
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
converting a flag, :exc:`~ext.commands.BadFlagArgument` is raised instead and the original exception
Expand Down

0 comments on commit 71358b8

Please sign in to comment.