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: handle typing.Annotated form annotations in slash commands option parser. #2124

Merged
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ These changes are available on the `master` branch, but have not yet been releas
([#2102](https://github.com/Pycord-Development/pycord/pull/2102))
- Added `bridge.Context` as a shortcut to `Union` of subclasses.
([#2106](https://github.com/Pycord-Development/pycord/pull/2106))
- Added Annotated forms support for typehinting slash command options.
([#2124](https://github.com/Pycord-Development/pycord/pull/2124))

### Changed

Expand Down
18 changes: 18 additions & 0 deletions discord/commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
Union,
)

from typing_extensions import Annotated, get_args, get_origin

from ..channel import _threaded_guild_channel_factory
from ..enums import Enum as DiscordEnum
from ..enums import MessageType, SlashCommandOptionType, try_enum
Expand Down Expand Up @@ -732,6 +734,19 @@ def _parse_options(self, params, *, check_params: bool = True) -> list[Option]:
if option == inspect.Parameter.empty:
option = str

if self._is_typing_annotated(option):
type_hint = get_args(option)[0]
metadata = option.__metadata__
# If multiple Options in metadata, the first will be used.
option_gen = (elem for elem in metadata if isinstance(elem, Option))
option = next(option_gen, Option())
# Handle Optional
if self._is_typing_optional(type_hint):
option.input_type = get_args(type_hint)[0]
option.default = None
else:
option.input_type = type_hint

if self._is_typing_union(option):
if self._is_typing_optional(option):
option = Option(option.__args__[0], default=None)
Expand Down Expand Up @@ -820,6 +835,9 @@ def _is_typing_union(self, annotation):
def _is_typing_optional(self, annotation):
return self._is_typing_union(annotation) and type(None) in annotation.__args__ # type: ignore

def _is_typing_annotated(self, annotation):
return get_origin(annotation) is Annotated

@property
def cog(self):
return getattr(self, "_cog", MISSING)
Expand Down
86 changes: 86 additions & 0 deletions tests/test_typing_annotated.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from typing import Optional

import pytest
from typing_extensions import Annotated

import discord
from discord import ApplicationContext
from discord.commands.core import SlashCommand, slash_command


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)


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)


def test_typing_annotated_cog():
class echoCog(discord.Cog):
def __init__(self, bot_) -> None:
self.bot = bot_
super().__init__()

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

bot = discord.Bot()
bot.add_cog(echoCog(bot))


def test_typing_annotated_cog_slashgroup():
class echoCog(discord.Cog):
grp = discord.commands.SlashCommandGroup("echo")

def __init__(self, bot_) -> None:
self.bot = bot_
super().__init__()

@grp.command()
async def echo(
self, ctx, txt: Annotated[str, discord.Option(description="Some text")]
):
await ctx.respond(txt)

bot = discord.Bot()
bot.add_cog(echoCog(bot))


def test_typing_annotated_optional():
async def echo(ctx, txt: Annotated[Optional[str], discord.Option()]):
await ctx.respond(txt)

cmd = SlashCommand(echo)
bot = discord.Bot()
bot.add_application_command(cmd)


def test_no_annotation():
async def echo(ctx, txt: str):
await ctx.respond(txt)

cmd = SlashCommand(echo)
bot = discord.Bot()
bot.add_application_command(cmd)


def test_annotated_no_option():
async def echo(ctx, txt: Annotated[str, "..."]):
await ctx.respond(txt)

cmd = SlashCommand(echo)
bot = discord.Bot()
bot.add_application_command(cmd)