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

Allow typing.Annotated hints for defining options. #2120

Closed
afroemming opened this issue Jun 14, 2023 · 1 comment · Fixed by #2124
Closed

Allow typing.Annotated hints for defining options. #2120

afroemming opened this issue Jun 14, 2023 · 1 comment · Fixed by #2124
Labels
feature request New feature request priority: low Low Priority
Milestone

Comments

@afroemming
Copy link
Contributor

Summary

Allow typing.Annotated hints for defining options.

What is the feature request for?

The core library

The Problem

Pycord allows commands to have options defined through parameter annotations, as in:

async def echo(ctx, txt: Option(str, description="Text to echo")):
  await ctx.respond(txt)

however, this use of annotations precludes a static type checker from being able to be used on the associated parameter (that is, mypy for example gives a warning: "Invalid type comment or annotation [valid-type]". The standard library has introduced with PEP 593 a way for an annotation to provide both a type hint and metadata for that parameter with typing.Annotated. However, this library's command parsing methods are not able to interpret an annotation that is a typing.Annotated.

(As an aside, using decorators to define options is not a work around for this because while then the option's associated parameter in the callback function can have a type hint, the decorator hides from statis type analysis the callback function's parameter types/ return when called)

The Ideal Solution

Add to option parsing the ability to read annotations of the form,

Annotated[<type>, Option(<args>)

(ie. something like this would be valid.

async def square(ctx, num: Annotated[int, Option(description="Text to echo")]):
  await ctx.respond(num**2)

) by for instance, by checking if option (as in commands/core.py:731) is an instance of typing.Annotated, picking the input type for the option from option.__args__[0], and iterating through option.__metadata__ trying to find an instance of discord.Option.

The Current Solution

As far as I'm aware, the only way to avoid static type checker warning in a command function definition is ignoring them.

Additional Context

A brief test for the above ideal solution is

import discord

def test_typing_annotated_decorator():
    bot = discord.Bot()
    @bot.slash_command()
    async def echo(ctx, txt: Annotated[str, discord.Option(description='Some text')]):
        await ctx.respond(txt)

which fails with

============================= test session starts ==============================
platform linux -- Python 3.11.3, pytest-7.3.2, pluggy-1.0.0
rootdir: /home/amelia/pycord
configfile: pyproject.toml
collected 1 item

tests/test_typing_annotated.py F                                         [100%]

=================================== FAILURES ===================================
____________________________ test_typing_annotated _____________________________

    def test_typing_annotated():
        async def echo(ctx, txt: Annotated[str, discord.Option()]):
            await ctx.respond(txt)
        cmd = SlashCommand(echo)
        bot = discord.Bot()
>       bot.add_application_command(cmd)

tests/test_typing_annotated.py:14: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
discord/bot.py:127: in add_application_command
    command._set_cog(None)
discord/commands/core.py:603: in _set_cog
    self.cog = cog
discord/commands/core.py:830: in cog
    self._validate_parameters()
discord/commands/core.py:708: in _validate_parameters
    self.options: list[Option] = self._parse_options(params)
discord/commands/core.py:748: in _parse_options
    option = Option(option)
discord/commands/options.py:231: in __init__
    raise exc
discord/commands/options.py:226: in __init__
    self.input_type = SlashCommandOptionType.from_datatype(input_type)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

cls = <enum SlashCommandOptionType>
datatype = typing.Annotated[str, <discord.commands.Option name=None>]

    @classmethod
    def from_datatype(cls, datatype):
        if isinstance(datatype, tuple):  # typing.Union has been used
            datatypes = [cls.from_datatype(op) for op in datatype]
            if all(x == cls.channel for x in datatypes):
                return cls.channel
            elif set(datatypes) <= {cls.role, cls.user}:
                return cls.mentionable
            else:
                raise TypeError("Invalid usage of typing.Union")
    
        py_3_10_union_type = hasattr(types, "UnionType") and isinstance(
            datatype, types.UnionType
        )
    
        if py_3_10_union_type or getattr(datatype, "__origin__", None) is Union:
            # Python 3.10+ "|" operator or typing.Union has been used. The __args__ attribute is a tuple of the types.
            # Type checking fails for this case, so ignore it.
            return cls.from_datatype(datatype.__args__)  # type: ignore
    
        if datatype.__name__ in ["Member", "User"]:
            return cls.user
        if datatype.__name__ in [
            "GuildChannel",
            "TextChannel",
            "VoiceChannel",
            "StageChannel",
            "CategoryChannel",
            "ThreadOption",
            "Thread",
            "ForumChannel",
            "DMChannel",
        ]:
            return cls.channel
        if datatype.__name__ == "Role":
            return cls.role
        if datatype.__name__ == "Attachment":
            return cls.attachment
        if datatype.__name__ == "Mentionable":
            return cls.mentionable
    
>       if issubclass(datatype, str):
E       TypeError: issubclass() arg 1 must be a class

discord/enums.py:796: TypeError
=============================== warnings summary ===============================
.venv/lib64/python3.11/site-packages/_pytest/config/__init__.py:1314
  /home/amelia/pycord/.venv/lib64/python3.11/site-packages/_pytest/config/__init__.py:1314: PytestConfigWarning: Unknown config option: asyncio_mode
  
    self._warn_or_fail_if_strict(f"Unknown config option: {key}\n")

discord/player.py:28
  /home/amelia/pycord/discord/player.py:28: DeprecationWarning: 'audioop' is deprecated and slated for removal in Python 3.13
    import audioop

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_typing_annotated.py::test_typing_annotated - TypeError: iss...
======================== 1 failed, 2 warnings in 0.18s =========================
@afroemming afroemming added the feature request New feature request label Jun 14, 2023
@Lulalaby
Copy link
Member

Feel free to open up a pull request

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature request New feature request priority: low Low Priority
Projects
Status: Done
Development

Successfully merging a pull request may close this issue.

2 participants