Skip to content

Commit 9f265b0

Browse files
Vioshimpre-commit-ci[bot]LulalabyDorukyumJustaSqu1d
authored
feat: implement positional flags (#2443)
* Implements positional flags * style(pre-commit): auto fixes from pre-commit.com hooks * Documentation for positional argument in commands.Flag * style(pre-commit): auto fixes from pre-commit.com hooks * Apply suggestions from code review Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Co-authored-by: JustaSqu1d <89910983+JustaSqu1d@users.noreply.github.com> Signed-off-by: Lala Sabathil <aiko@aitsys.dev> * chore: Update typing import in flags.py The typing import in flags.py has been updated to include the Optional module. This change ensures that the __commands_flag_positional__ attribute can accept a value of None. * style(pre-commit): auto fixes from pre-commit.com hooks * style(pre-commit): auto fixes from pre-commit.com hooks * Apply suggestion from @Paillat-dev Signed-off-by: Paillat <jeremiecotti@ik.me> --------- Signed-off-by: Lala Sabathil <aiko@aitsys.dev> Signed-off-by: Lala Sabathil <lala@pycord.dev> Signed-off-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Signed-off-by: Paillat <paillat@pycord.dev> Signed-off-by: Paillat <jeremiecotti@ik.me> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Lala Sabathil <aiko@aitsys.dev> Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Co-authored-by: JustaSqu1d <89910983+JustaSqu1d@users.noreply.github.com> Co-authored-by: Lala Sabathil <lala@pycord.dev> Co-authored-by: Paillat <paillat@pycord.dev> Co-authored-by: Paillat <jeremiecotti@ik.me>
1 parent 30912d6 commit 9f265b0

File tree

3 files changed

+63
-1
lines changed

3 files changed

+63
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ These changes are available on the `master` branch, but have not yet been releas
104104

105105
### Added
106106

107+
- Added `positional` argument to `commands.Flag`.
108+
([#2443](https://github.com/Pycord-Development/pycord/pull/2443))
107109
- Added `Guild.fetch_role` method.
108110
([#2528](https://github.com/Pycord-Development/pycord/pull/2528))
109111
- Added the following `AppInfo` attributes: `approximate_guild_count`,

discord/ext/commands/flags.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ class Flag:
8585
max_args: :class:`int`
8686
The maximum number of arguments the flag can accept.
8787
A negative value indicates an unlimited amount of arguments.
88+
positional: :class:`bool`
89+
Whether the flag is positional.
90+
A :class:`FlagConverter` can only handle one positional flag.
8891
override: :class:`bool`
8992
Whether multiple given values overrides the previous value.
9093
"""
@@ -95,6 +98,7 @@ class Flag:
9598
annotation: Any = _missing_field_factory()
9699
default: Any = _missing_field_factory()
97100
max_args: int = _missing_field_factory()
101+
positional: bool = _missing_field_factory()
98102
override: bool = _missing_field_factory()
99103
cast_to_dict: bool = False
100104

@@ -114,6 +118,7 @@ def flag(
114118
default: Any = MISSING,
115119
max_args: int = MISSING,
116120
override: bool = MISSING,
121+
positional: bool = MISSING,
117122
) -> Any:
118123
"""Override default functionality and parameters of the underlying :class:`FlagConverter`
119124
class attributes.
@@ -135,13 +140,16 @@ class attributes.
135140
override: :class:`bool`
136141
Whether multiple given values overrides the previous value. The default
137142
value depends on the annotation given.
143+
positional: :class:`bool`
144+
Whether the flag is positional or not. There can only be one positional flag.
138145
"""
139146
return Flag(
140147
name=name,
141148
aliases=aliases,
142149
default=default,
143150
max_args=max_args,
144151
override=override,
152+
positional=positional,
145153
)
146154

147155

@@ -168,6 +176,7 @@ def get_flags(
168176
flags: dict[str, Flag] = {}
169177
cache: dict[str, Any] = {}
170178
names: set[str] = set()
179+
positional: Flag | None = None
171180
for name, annotation in annotations.items():
172181
flag = namespace.pop(name, MISSING)
173182
if isinstance(flag, Flag):
@@ -179,6 +188,14 @@ def get_flags(
179188
if flag.name is MISSING:
180189
flag.name = name
181190

191+
if flag.positional:
192+
if positional is not None:
193+
raise TypeError(
194+
f"{flag.name!r} positional flag conflicts with {positional.name!r} flag."
195+
)
196+
197+
positional = flag
198+
182199
annotation = flag.annotation = resolve_annotation(
183200
flag.annotation, globals, locals, cache
184201
)
@@ -280,6 +297,7 @@ class FlagsMeta(type):
280297
__commands_flag_case_insensitive__: bool
281298
__commands_flag_delimiter__: str
282299
__commands_flag_prefix__: str
300+
__commands_flag_positional__: Flag | None
283301

284302
def __new__(
285303
cls: type[type],
@@ -340,9 +358,13 @@ def __new__(
340358
delimiter = attrs.setdefault("__commands_flag_delimiter__", ":")
341359
prefix = attrs.setdefault("__commands_flag_prefix__", "")
342360

361+
positional_flag: Flag | None = None
343362
for flag_name, flag in get_flags(attrs, global_ns, local_ns).items():
344363
flags[flag_name] = flag
364+
if flag.positional:
365+
positional_flag = flag
345366
aliases.update({alias_name: flag_name for alias_name in flag.aliases})
367+
attrs["__commands_flag_positional__"] = positional_flag
346368

347369
forbidden = set(delimiter).union(prefix)
348370
for flag_name in flags:
@@ -542,10 +564,29 @@ def parse_flags(cls, argument: str) -> dict[str, list[str]]:
542564
result: dict[str, list[str]] = {}
543565
flags = cls.__commands_flags__
544566
aliases = cls.__commands_flag_aliases__
567+
positional_flag = cls.__commands_flag_positional__
545568
last_position = 0
546569
last_flag: Flag | None = None
547570

548571
case_insensitive = cls.__commands_flag_case_insensitive__
572+
573+
if positional_flag is not None:
574+
match = cls.__commands_flag_regex__.search(argument)
575+
if match is not None:
576+
begin, end = match.span(0)
577+
value = argument[:begin].strip()
578+
else:
579+
value = argument.strip()
580+
last_position = len(argument)
581+
582+
if value:
583+
name = (
584+
positional_flag.name.casefold()
585+
if case_insensitive
586+
else positional_flag.name
587+
)
588+
result[name] = [value]
589+
549590
for match in cls.__commands_flag_regex__.finditer(argument):
550591
begin, end = match.span(0)
551592
key = match.group("flag")

docs/ext/commands/commands.rst

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,19 @@ This tells the parser that the ``members`` attribute is mapped to a flag named `
656656
the default value is an empty list. For greater customisability, the default can either be a value or a callable
657657
that takes the :class:`~ext.commands.Context` as a sole parameter. This callable can either be a function or a coroutine.
658658

659+
Flags can also be positional. This means that the flag does not require a corresponding
660+
value to be passed in by the user. This is useful for flags that are either optional or have a default value.
661+
For example, in the following code:
662+
663+
.. code-block:: python3
664+
665+
class BanFlags(commands.FlagConverter):
666+
members: List[discord.Member] = commands.flag(name='member', positional=True)
667+
reason: str = commands.flag(default='no reason')
668+
days: int = commands.flag(default=1)
669+
670+
The ``members`` flag is marked as positional, meaning that the user can invoke the command without explicitly specifying the flag.
671+
659672
In order to customise the flag syntax we also have a few options that can be passed to the class parameter list:
660673

661674
.. code-block:: python3
@@ -675,11 +688,17 @@ In order to customise the flag syntax we also have a few options that can be pas
675688
nsfw: Optional[bool]
676689
slowmode: Optional[int]
677690
691+
# Hello there --bold True
692+
class Greeting(commands.FlagConverter):
693+
text: str = commands.flag(positional=True)
694+
bold: bool = False
695+
696+
678697
.. note::
679698

680699
Despite the similarities in these examples to command like arguments, the syntax and parser is not
681700
a command line parser. The syntax is mainly inspired by Discord's search bar input and as a result
682-
all flags need a corresponding value.
701+
all flags need a corresponding value unless a positional flag is provided.
683702

684703
The flag converter is similar to regular commands and allows you to use most types of converters
685704
(with the exception of :class:`~ext.commands.Greedy`) as the type annotation. Some extra support is added for specific

0 commit comments

Comments
 (0)