Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/1431.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
|commands| Support :data:`typing.Annotated` for specifying converters in prefix commands in a more type-safe way. See :ref:`Special Converters <ext_commands_converters_annotated>` for details.
2 changes: 1 addition & 1 deletion disnake/ext/commands/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ def __or__(self, other):

if TYPE_CHECKING:
# aliased import since mypy doesn't understand `Range = Annotated`
from typing_extensions import Annotated as Range, Annotated as String
from typing import Annotated as Range, Annotated as String
else:

@dataclass(frozen=True, repr=False)
Expand Down
6 changes: 6 additions & 0 deletions disnake/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from operator import attrgetter
from typing import (
TYPE_CHECKING,
Annotated,
Any,
AsyncIterator,
Awaitable,
Expand Down Expand Up @@ -1212,9 +1213,14 @@ def evaluate_annotation(
cache[tp] = evaluated
return evaluated

# Annotated[X, Y], where Y is the converter we need
if get_origin(tp) is Annotated:
return evaluate_annotation(tp.__metadata__[0], globals, locals, cache)

# GenericAlias / UnionType
if hasattr(tp, "__args__"):
if not hasattr(tp, "__origin__"):
# n.b. this became obsolete in Python 3.14+, as `UnionType` and `Union` are the same thing now.
if tp.__class__ is UnionType:
converted = Union[tp.__args__]
return evaluate_annotation(converted, globals, locals, cache)
Expand Down
30 changes: 30 additions & 0 deletions docs/ext/commands/commands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,8 @@ This can get tedious, so an inline advanced converter is possible through a :fun
else:
await ctx.send("Hm you're not so new.")

.. _ext_commands_discord_converters:

Discord Converters
++++++++++++++++++

Expand Down Expand Up @@ -552,6 +554,34 @@ The ``buy_sell`` parameter must be either the literal string ``"buy"`` or ``"sel

Note that ``typing.Literal[True]`` and ``typing.Literal[False]`` still follow the :class:`bool` converter rules.

.. _ext_commands_converters_annotated:

typing.Annotated
^^^^^^^^^^^^^^^^

.. versionadded:: |vnext|

With :data:`typing.Annotated`, you can use converters in a more type-safe way.
Taking the example from :ref:`ext_commands_basic_converters` above, ``content`` is annotated
as ``to_upper`` (i.e. a converter function), while it would naturally be a :class:`str` at runtime;
this will likely trip up type-checkers such as pyright/mypy.

To avoid this, you can use :data:`typing.Annotated`, such that type-checkers consider the parameter
a :class:`str` while disnake will use the converter passed as the second argument to :data:`~typing.Annotated` at runtime:

.. code-block:: python3

from typing import Annotated

def to_upper(argument: str):
return argument.upper()

@bot.command()
async def up(ctx, *, content: Annotated[str, to_upper]):
await ctx.send(content)

This works with all types of converters mentioned on this page.

Greedy
^^^^^^

Expand Down
2 changes: 1 addition & 1 deletion docs/ext/commands/slash_commands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ Discord itself supports only a few built-in types which are guaranteed to be enf
- :class:`disnake.Attachment`
- :class:`disnake.abc.Snowflake`\*\*\*

All the other types may be converted implicitly, similarly to :ref:`ext_commands_basic_converters`
Other types may be converted implicitly, using the builtin :ref:`ext_commands_discord_converters`:

.. code-block:: python3

Expand Down
3 changes: 3 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from datetime import timedelta, timezone
from typing import (
TYPE_CHECKING,
Annotated,
Any,
Callable,
Dict,
Expand Down Expand Up @@ -791,6 +792,8 @@ def test_normalise_optional_params(params, expected) -> None:
# forward refs
("bool", bool, True),
("Tuple[dict, List[Literal[42, 99]]]", Tuple[dict, List[Literal[42, 99]]], True),
# Annotated[X, Y] -> Y
(Annotated[str, str.casefold], str.casefold, False),
# 3.10 union syntax
pytest.param(
"int | float",
Expand Down