Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement positional flags #2443

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ These changes are available on the `master` branch, but have not yet been releas

- Added `Guild.fetch_role` method.
([#2528](https://github.com/Pycord-Development/pycord/pull/2528))
- Added `positional` argument to `commands.Flag`.
([#2443](https://github.com/Pycord-Development/pycord/pull/2443))

## [2.6.0] - 2024-07-09

Expand Down
41 changes: 41 additions & 0 deletions discord/ext/commands/flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ class Flag:
max_args: :class:`int`
The maximum number of arguments the flag can accept.
A negative value indicates an unlimited amount of arguments.
positional: :class:`bool`
Whether the flag is positional.
A :class:`FlagConverter` can only handle one positional flag.
override: :class:`bool`
Whether multiple given values overrides the previous value.
"""
Expand All @@ -92,6 +95,7 @@ class Flag:
annotation: Any = _MISSING
default: Any = _MISSING
max_args: int = _MISSING
positional: bool = _MISSING
override: bool = _MISSING
cast_to_dict: bool = False

Expand All @@ -111,6 +115,7 @@ def flag(
default: Any = MISSING,
max_args: int = MISSING,
override: bool = MISSING,
positional: bool = MISSING,
) -> Any:
"""Override default functionality and parameters of the underlying :class:`FlagConverter`
class attributes.
Expand All @@ -132,13 +137,16 @@ class attributes.
override: :class:`bool`
Whether multiple given values overrides the previous value. The default
value depends on the annotation given.
positional: :class:`bool`
Whether the flag is positional or not. There can only be one positional flag.
"""
return Flag(
name=name,
aliases=aliases,
default=default,
max_args=max_args,
override=override,
positional=positional,
)


Expand All @@ -165,6 +173,7 @@ def get_flags(
flags: dict[str, Flag] = {}
cache: dict[str, Any] = {}
names: set[str] = set()
positional: Flag | None = None
for name, annotation in annotations.items():
flag = namespace.pop(name, MISSING)
if isinstance(flag, Flag):
Expand All @@ -176,6 +185,14 @@ def get_flags(
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
)
Expand Down Expand Up @@ -277,6 +294,7 @@ class FlagsMeta(type):
__commands_flag_case_insensitive__: bool
__commands_flag_delimiter__: str
__commands_flag_prefix__: str
__commands_flag_positional__: Flag | None
Vioshim marked this conversation as resolved.
Show resolved Hide resolved

def __new__(
cls: type[type],
Expand Down Expand Up @@ -337,9 +355,13 @@ def __new__(
delimiter = attrs.setdefault("__commands_flag_delimiter__", ":")
prefix = attrs.setdefault("__commands_flag_prefix__", "")

positional_flag: Flag | None = None
for flag_name, flag in get_flags(attrs, global_ns, local_ns).items():
flags[flag_name] = flag
if flag.positional:
positional_flag = flag
aliases.update({alias_name: flag_name for alias_name in flag.aliases})
attrs["__commands_flag_positional__"] = positional_flag

forbidden = set(delimiter).union(prefix)
for flag_name in flags:
Expand Down Expand Up @@ -539,10 +561,29 @@ def parse_flags(cls, argument: str) -> dict[str, list[str]]:
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: Flag | None = 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
21 changes: 20 additions & 1 deletion docs/ext/commands/commands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,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.

Flags can also be positional. This means that the flag does not require a corresponding
value to be passed in by the user. This is useful for flags that are either optional or have a default value.
For example, in the following code:

.. code-block:: python3

class BanFlags(commands.FlagConverter):
members: List[discord.Member] = commands.flag(name='member', positional=True)
reason: str = commands.flag(default='no reason')
days: int = commands.flag(default=1)

The ``members`` flag is marked as positional, meaning that the user can invoke the command without explicitly specifying the flag.

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 @@ -675,11 +688,17 @@ In order to customise the flag syntax we also have a few options that can be pas
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 a positional flag is provided.

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
Expand Down
Loading