From c85c7f0c23f5ad7b46d103bdc00bac69f1da0fdf Mon Sep 17 00:00:00 2001 From: Vioshim Date: Tue, 30 Apr 2024 13:05:14 -0500 Subject: [PATCH 1/8] Implements positional flags --- discord/ext/commands/flags.py | 36 ++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/discord/ext/commands/flags.py b/discord/ext/commands/flags.py index 54e7e0c37c..32a3d295ea 100644 --- a/discord/ext/commands/flags.py +++ b/discord/ext/commands/flags.py @@ -29,7 +29,7 @@ import re import sys from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any, Iterator, Literal, Pattern, TypeVar, Union +from typing import TYPE_CHECKING, Any, Iterator, Literal, Optional, Pattern, TypeVar, Union from discord.utils import MISSING, MissingField, maybe_coroutine, resolve_annotation @@ -82,6 +82,8 @@ 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 or not. There can only be one positional flag. override: :class:`bool` Whether multiple given values overrides the previous value. """ @@ -92,6 +94,7 @@ class Flag: annotation: Any = _MISSING default: Any = _MISSING max_args: int = _MISSING + positional: bool = _MISSING override: bool = _MISSING cast_to_dict: bool = False @@ -111,6 +114,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. @@ -132,6 +136,8 @@ 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, @@ -139,6 +145,7 @@ class attributes. default=default, max_args=max_args, override=override, + positional=positional, ) @@ -165,6 +172,7 @@ def get_flags( 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): @@ -176,6 +184,12 @@ 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 ) @@ -277,6 +291,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: type[type], @@ -337,9 +352,13 @@ def __new__( delimiter = attrs.setdefault("__commands_flag_delimiter__", ":") prefix = attrs.setdefault("__commands_flag_prefix__", "") + positional_flag: Optional[Flag] = 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: @@ -539,10 +558,25 @@ 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") From 96fe65ed6f811ed06147232abdcb92ef7ed25e15 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 30 Apr 2024 18:14:54 +0000 Subject: [PATCH 2/8] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ext/commands/flags.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/discord/ext/commands/flags.py b/discord/ext/commands/flags.py index 32a3d295ea..318a67d482 100644 --- a/discord/ext/commands/flags.py +++ b/discord/ext/commands/flags.py @@ -29,7 +29,16 @@ import re import sys from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any, Iterator, Literal, Optional, Pattern, TypeVar, Union +from typing import ( + TYPE_CHECKING, + Any, + Iterator, + Literal, + Optional, + Pattern, + TypeVar, + Union, +) from discord.utils import MISSING, MissingField, maybe_coroutine, resolve_annotation @@ -172,7 +181,7 @@ def get_flags( flags: dict[str, Flag] = {} cache: dict[str, Any] = {} names: set[str] = set() - positional: Optional[Flag] = None + positional: Flag | None = None for name, annotation in annotations.items(): flag = namespace.pop(name, MISSING) if isinstance(flag, Flag): @@ -186,7 +195,9 @@ def get_flags( if flag.positional: if positional is not None: - raise TypeError(f"{flag.name!r} positional flag conflicts with {positional.name!r} flag.") + raise TypeError( + f"{flag.name!r} positional flag conflicts with {positional.name!r} flag." + ) positional = flag @@ -291,7 +302,7 @@ class FlagsMeta(type): __commands_flag_case_insensitive__: bool __commands_flag_delimiter__: str __commands_flag_prefix__: str - __commands_flag_positional__: Optional[Flag] + __commands_flag_positional__: Flag | None def __new__( cls: type[type], @@ -352,7 +363,7 @@ def __new__( delimiter = attrs.setdefault("__commands_flag_delimiter__", ":") prefix = attrs.setdefault("__commands_flag_prefix__", "") - positional_flag: Optional[Flag] = None + 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: @@ -574,7 +585,11 @@ def parse_flags(cls, argument: str) -> dict[str, list[str]]: last_position = len(argument) if value: - name = positional_flag.name.casefold() if case_insensitive else positional_flag.name + name = ( + positional_flag.name.casefold() + if case_insensitive + else positional_flag.name + ) result[name] = [value] for match in cls.__commands_flag_regex__.finditer(argument): From 539a9a065d509959275d81348a63347fa179a274 Mon Sep 17 00:00:00 2001 From: Vioshim Date: Tue, 30 Apr 2024 14:16:16 -0500 Subject: [PATCH 3/8] Documentation for positional argument in commands.Flag --- CHANGELOG.md | 2 ++ docs/ext/commands/commands.rst | 21 ++++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39df9290f2..6b117f33cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2421](https://github.com/Pycord-Development/pycord/pull/2421)) - Added `member` data to the `raw_reaction_remove` event. ([#2412](https://github.com/Pycord-Development/pycord/pull/2412)) +- Added `positional` argument to `commands.Flag` + ([#2443](https://github.com/Pycord-Development/pycord/pull/2443)) ### Fixed diff --git a/docs/ext/commands/commands.rst b/docs/ext/commands/commands.rst index c907e2a1b4..04807bdb42 100644 --- a/docs/ext/commands/commands.rst +++ b/docs/ext/commands/commands.rst @@ -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 have one of its flags be marked as 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 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 @@ -674,12 +687,18 @@ 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 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 From 409ec1aee67dea8e565ecdf748e5667a2aabc086 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 30 Apr 2024 19:17:26 +0000 Subject: [PATCH 4/8] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ext/commands/flags.py | 11 +---------- docs/ext/commands/commands.rst | 2 +- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/discord/ext/commands/flags.py b/discord/ext/commands/flags.py index 318a67d482..99b1e0030b 100644 --- a/discord/ext/commands/flags.py +++ b/discord/ext/commands/flags.py @@ -29,16 +29,7 @@ import re import sys from dataclasses import dataclass, field -from typing import ( - TYPE_CHECKING, - Any, - Iterator, - Literal, - Optional, - Pattern, - TypeVar, - Union, -) +from typing import TYPE_CHECKING, Any, Iterator, Literal, Pattern, TypeVar, Union from discord.utils import MISSING, MissingField, maybe_coroutine, resolve_annotation diff --git a/docs/ext/commands/commands.rst b/docs/ext/commands/commands.rst index 04807bdb42..6205bd3e74 100644 --- a/docs/ext/commands/commands.rst +++ b/docs/ext/commands/commands.rst @@ -687,7 +687,7 @@ 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) From 29401b122d996055784589f60d5200e717fbe9f6 Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Mon, 1 Jul 2024 15:13:57 +0200 Subject: [PATCH 5/8] 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 --- CHANGELOG.md | 2 +- discord/ext/commands/flags.py | 3 ++- docs/ext/commands/commands.rst | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b117f33cc..9c21f543eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ These changes are available on the `master` branch, but have not yet been releas ([#2421](https://github.com/Pycord-Development/pycord/pull/2421)) - Added `member` data to the `raw_reaction_remove` event. ([#2412](https://github.com/Pycord-Development/pycord/pull/2412)) -- Added `positional` argument to `commands.Flag` +- Added `positional` argument to `commands.Flag`. ([#2443](https://github.com/Pycord-Development/pycord/pull/2443)) ### Fixed diff --git a/discord/ext/commands/flags.py b/discord/ext/commands/flags.py index 99b1e0030b..9836f66c4e 100644 --- a/discord/ext/commands/flags.py +++ b/discord/ext/commands/flags.py @@ -83,7 +83,8 @@ class Flag: 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 or not. There can only be one positional flag. + 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. """ diff --git a/docs/ext/commands/commands.rst b/docs/ext/commands/commands.rst index 6205bd3e74..f21c9f24cb 100644 --- a/docs/ext/commands/commands.rst +++ b/docs/ext/commands/commands.rst @@ -656,7 +656,7 @@ 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 have one of its flags be marked as positional. This means that the flag does not require a corresponding +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: @@ -667,7 +667,7 @@ For example, in the following code: 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 specifying the flag +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: From dfdc4f3e814a2d4fcf0a5161e8d838e9b345d502 Mon Sep 17 00:00:00 2001 From: Vioshim Date: Wed, 21 Aug 2024 14:39:46 -0500 Subject: [PATCH 6/8] 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. --- discord/ext/commands/flags.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/ext/commands/flags.py b/discord/ext/commands/flags.py index 9836f66c4e..9d5ae74b90 100644 --- a/discord/ext/commands/flags.py +++ b/discord/ext/commands/flags.py @@ -29,7 +29,7 @@ import re import sys from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any, Iterator, Literal, Pattern, TypeVar, Union +from typing import TYPE_CHECKING, Any, Iterator, Literal, Optional, Pattern, TypeVar, Union from discord.utils import MISSING, MissingField, maybe_coroutine, resolve_annotation @@ -294,7 +294,7 @@ class FlagsMeta(type): __commands_flag_case_insensitive__: bool __commands_flag_delimiter__: str __commands_flag_prefix__: str - __commands_flag_positional__: Flag | None + __commands_flag_positional__: Optional[Flag] def __new__( cls: type[type], From 4f752e6b9bd9d68838b9f2cc355d6dfc14f99c9f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 21 Aug 2024 19:41:18 +0000 Subject: [PATCH 7/8] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ext/commands/flags.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/discord/ext/commands/flags.py b/discord/ext/commands/flags.py index 9d5ae74b90..96e6a9857c 100644 --- a/discord/ext/commands/flags.py +++ b/discord/ext/commands/flags.py @@ -29,7 +29,16 @@ import re import sys from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any, Iterator, Literal, Optional, Pattern, TypeVar, Union +from typing import ( + TYPE_CHECKING, + Any, + Iterator, + Literal, + Optional, + Pattern, + TypeVar, + Union, +) from discord.utils import MISSING, MissingField, maybe_coroutine, resolve_annotation @@ -294,7 +303,7 @@ class FlagsMeta(type): __commands_flag_case_insensitive__: bool __commands_flag_delimiter__: str __commands_flag_prefix__: str - __commands_flag_positional__: Optional[Flag] + __commands_flag_positional__: Flag | None def __new__( cls: type[type], From ecf1698070888a8c0f19260f8a32dd8b4bb976fa Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 22 Aug 2024 16:50:29 +0000 Subject: [PATCH 8/8] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ext/commands/flags.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/discord/ext/commands/flags.py b/discord/ext/commands/flags.py index 96e6a9857c..9836f66c4e 100644 --- a/discord/ext/commands/flags.py +++ b/discord/ext/commands/flags.py @@ -29,16 +29,7 @@ import re import sys from dataclasses import dataclass, field -from typing import ( - TYPE_CHECKING, - Any, - Iterator, - Literal, - Optional, - Pattern, - TypeVar, - Union, -) +from typing import TYPE_CHECKING, Any, Iterator, Literal, Pattern, TypeVar, Union from discord.utils import MISSING, MissingField, maybe_coroutine, resolve_annotation