diff --git a/CHANGELOG.md b/CHANGELOG.md index af98289db1..6704cc29e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2659](https://github.com/Pycord-Development/pycord/pull/2659)) - Added `VoiceMessage` subclass of `File` to allow voice messages to be sent. ([#2579](https://github.com/Pycord-Development/pycord/pull/2579)) +- 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 54e7e0c37c..9836f66c4e 100644 --- a/discord/ext/commands/flags.py +++ b/discord/ext/commands/flags.py @@ -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. """ @@ -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 @@ -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. @@ -132,6 +137,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 +146,7 @@ class attributes. default=default, max_args=max_args, override=override, + positional=positional, ) @@ -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): @@ -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 ) @@ -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 def __new__( cls: type[type], @@ -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: @@ -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") diff --git a/docs/ext/commands/commands.rst b/docs/ext/commands/commands.rst index 686f95f047..ed2046fe10 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 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 @@ -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