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

fix(commands): Fix application command checks' typing #1048

Merged
merged 25 commits into from
Nov 4, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
08c6e1b
Add '.pdm-python' to .gitignore
elenakrittik Jun 14, 2023
3f6d9fa
fix(commands): Fix incorrect check typings, add missing decorators
elenakrittik Jun 14, 2023
a0b3886
docs(changelog): Add changelog entry
elenakrittik Jun 14, 2023
1d8ddc2
Merge branch 'master' of https://github.com/DisnakeDev/disnake into f…
elenakrittik Jun 14, 2023
61d1aa2
Merge branch 'master' into fix/check-typings
elenakrittik Jun 19, 2023
70d3666
Merge branch 'master' of https://github.com/DisnakeDev/disnake into f…
elenakrittik Jun 22, 2023
e77e270
Merge branch 'fix/check-typings' of https://github.com/elenakrittik/d…
elenakrittik Jun 22, 2023
c8acd35
Merge branch 'master' into fix/check-typings
elenakrittik Jul 25, 2023
e5b36a5
refactor: Change new functions to use same impl.
elenakrittik Aug 26, 2023
6b81ae1
docs: Fix wrong references in new functions.
elenakrittik Aug 26, 2023
d6da2ce
docs: Interlink new and old functions in docstrings.
elenakrittik Aug 26, 2023
52a198a
docs: Add new functions to checks.rst.
elenakrittik Aug 26, 2023
ccd95a5
docs: Fix wrong references in InteractionBotBase's methods.
elenakrittik Aug 26, 2023
1c83bf7
Apply suggestions from code review
elenakrittik Aug 26, 2023
876b4e7
Merge branch 'master' into fix/check-typings
elenakrittik Aug 26, 2023
6f31656
docs: Shorten new functions' docstrings.
elenakrittik Aug 26, 2023
b278390
docs: Add additional comments for future readers.
elenakrittik Aug 26, 2023
fdffe0d
Merge branch 'fix/check-typings' of https://github.com/elenakrittik/d…
elenakrittik Aug 26, 2023
192d03f
fix: Docs build.
elenakrittik Aug 26, 2023
0f6794f
Merge branch 'master' of https://github.com/DisnakeDev/disnake into f…
elenakrittik Nov 4, 2023
a83ab39
fix: Remove wrong warning.
elenakrittik Nov 4, 2023
755f02d
fix: Change check-related exceptions to support application command c…
elenakrittik Nov 4, 2023
9d8be27
fix: Properly replace exception
elenakrittik Nov 4, 2023
ea7bccf
fix: Doc link.
elenakrittik Nov 4, 2023
c7f44f9
Merge branch 'fix/check-typings' of https://github.com/elenakrittik/d…
elenakrittik Nov 4, 2023
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ venvs/
coverage.xml
__pypackages__/
.pdm.toml
.pdm-python
pdm.lock

!test_bot/locale/*.json
1 change: 1 addition & 0 deletions changelog/1045.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
|commands| Fix incorrect typings of :meth:`~disnake.ext.commands.InvokableApplicationCommand.add_check`, :meth:`~disnake.ext.commands.InvokableApplicationCommand.remove_check`, :meth:`~disnake.ext.commands.InteractionBotBase.add_app_command_check` and :meth:`~disnake.ext.commands.InteractionBotBase.remove_app_command_check`.
1 change: 1 addition & 0 deletions changelog/1045.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
|commands| Implement :func:`~disnake.ext.commands.app_check` decorator and :func:`~disnake.ext.commands.app_check_any` function.
elenakrittik marked this conversation as resolved.
Show resolved Hide resolved
6 changes: 6 additions & 0 deletions disnake/ext/commands/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from typing import TYPE_CHECKING, Any, Callable, Coroutine, TypeVar, Union

if TYPE_CHECKING:
from disnake import ApplicationCommandInteraction

from .cog import Cog
from .context import Context
from .errors import CommandError
Expand All @@ -16,6 +18,10 @@
Check = Union[
Callable[["Cog", "Context[Any]"], MaybeCoro[bool]], Callable[["Context[Any]"], MaybeCoro[bool]]
]
AppCheck = Union[
Callable[["Cog", "ApplicationCommandInteraction"], MaybeCoro[bool]],
Callable[["ApplicationCommandInteraction"], MaybeCoro[bool]],
]
Hook = Union[Callable[["Cog", "Context[Any]"], Coro[Any]], Callable[["Context[Any]"], Coro[Any]]]
Error = Union[
Callable[["Cog", "Context[Any]", "CommandError"], Coro[Any]],
Expand Down
8 changes: 4 additions & 4 deletions disnake/ext/commands/base_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

from disnake.interactions import ApplicationCommandInteraction

from ._types import Check, Coro, Error, Hook
from ._types import AppCheck, Coro, Error, Hook
from .cog import Cog

ApplicationCommandInteractionT = TypeVar(
Expand Down Expand Up @@ -155,7 +155,7 @@ def __init__(self, func: CommandCallback, *, name: Optional[str] = None, **kwarg
except AttributeError:
checks = kwargs.get("checks", [])

self.checks: List[Check] = checks
self.checks: List[AppCheck] = checks

try:
cooldown = func.__commands_cooldown__
Expand Down Expand Up @@ -253,7 +253,7 @@ def default_member_permissions(self) -> Optional[Permissions]:
def callback(self) -> CommandCallback:
return self._callback

def add_check(self, func: Check) -> None:
def add_check(self, func: AppCheck) -> None:
"""Adds a check to the application command.

This is the non-decorator interface to :func:`.check`.
elenakrittik marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -265,7 +265,7 @@ def add_check(self, func: Check) -> None:
"""
self.checks.append(func)

def remove_check(self, func: Check) -> None:
def remove_check(self, func: AppCheck) -> None:
"""Removes a check from the application command.

This function is idempotent and will not raise an exception
Expand Down
24 changes: 12 additions & 12 deletions disnake/ext/commands/cog.py
Original file line number Diff line number Diff line change
Expand Up @@ -787,30 +787,30 @@ def _inject(self, bot: AnyBot) -> Self:

# Add application command checks
if cls.bot_slash_command_check is not Cog.bot_slash_command_check:
bot.add_app_command_check(self.bot_slash_command_check, slash_commands=True) # type: ignore
bot.add_app_command_check(self.bot_slash_command_check, slash_commands=True)

if cls.bot_user_command_check is not Cog.bot_user_command_check:
bot.add_app_command_check(self.bot_user_command_check, user_commands=True) # type: ignore
bot.add_app_command_check(self.bot_user_command_check, user_commands=True)

if cls.bot_message_command_check is not Cog.bot_message_command_check:
bot.add_app_command_check(self.bot_message_command_check, message_commands=True) # type: ignore
bot.add_app_command_check(self.bot_message_command_check, message_commands=True)

# Add app command one-off checks
if cls.bot_slash_command_check_once is not Cog.bot_slash_command_check_once:
bot.add_app_command_check(
self.bot_slash_command_check_once, # type: ignore
self.bot_slash_command_check_once,
call_once=True,
slash_commands=True,
)

if cls.bot_user_command_check_once is not Cog.bot_user_command_check_once:
bot.add_app_command_check(
self.bot_user_command_check_once, call_once=True, user_commands=True # type: ignore
self.bot_user_command_check_once, call_once=True, user_commands=True
)

if cls.bot_message_command_check_once is not Cog.bot_message_command_check_once:
bot.add_app_command_check(
self.bot_message_command_check_once, # type: ignore
self.bot_message_command_check_once,
call_once=True,
message_commands=True,
)
Expand Down Expand Up @@ -857,32 +857,32 @@ def _eject(self, bot: AnyBot) -> None:

# Remove application command checks
if cls.bot_slash_command_check is not Cog.bot_slash_command_check:
bot.remove_app_command_check(self.bot_slash_command_check, slash_commands=True) # type: ignore
bot.remove_app_command_check(self.bot_slash_command_check, slash_commands=True)

if cls.bot_user_command_check is not Cog.bot_user_command_check:
bot.remove_app_command_check(self.bot_user_command_check, user_commands=True) # type: ignore
bot.remove_app_command_check(self.bot_user_command_check, user_commands=True)

if cls.bot_message_command_check is not Cog.bot_message_command_check:
bot.remove_app_command_check(self.bot_message_command_check, message_commands=True) # type: ignore
bot.remove_app_command_check(self.bot_message_command_check, message_commands=True)

# Remove app command one-off checks
if cls.bot_slash_command_check_once is not Cog.bot_slash_command_check_once:
bot.remove_app_command_check(
self.bot_slash_command_check_once, # type: ignore
self.bot_slash_command_check_once,
call_once=True,
slash_commands=True,
)

if cls.bot_user_command_check_once is not Cog.bot_user_command_check_once:
bot.remove_app_command_check(
self.bot_user_command_check_once, # type: ignore
self.bot_user_command_check_once,
call_once=True,
user_commands=True,
)

if cls.bot_message_command_check_once is not Cog.bot_message_command_check_once:
bot.remove_app_command_check(
self.bot_message_command_check_once, # type: ignore
self.bot_message_command_check_once,
call_once=True,
message_commands=True,
)
Expand Down
167 changes: 166 additions & 1 deletion disnake/ext/commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@

from disnake.message import Message

from ._types import Check, Coro, CoroFunc, Error, Hook
from ._types import AppCheck, Check, Coro, CoroFunc, Error, Hook
from .base_core import InvokableApplicationCommand


__all__ = (
Expand All @@ -76,6 +77,8 @@
"has_any_role",
"check",
"check_any",
"app_check",
"app_check_any",
"before_invoke",
"after_invoke",
"bot_has_role",
Expand Down Expand Up @@ -1850,6 +1853,168 @@ async def predicate(ctx: AnyContext) -> bool:
return check(predicate)


def app_check(predicate: AppCheck) -> Callable[[T], T]:
elenakrittik marked this conversation as resolved.
Show resolved Hide resolved
elenakrittik marked this conversation as resolved.
Show resolved Hide resolved
"""A decorator that adds a check to the :class:`disnake.ext.commands.InvokableApplicationCommand` or its
elenakrittik marked this conversation as resolved.
Show resolved Hide resolved
elenakrittik marked this conversation as resolved.
Show resolved Hide resolved
subclasses. These checks could be accessed via :attr:`.Command.checks`.

These checks should be predicates that take in a single parameter taking
a :class:`disnake.ApplicationCommandInteraction`. If the check returns a
``False``-like value then during invocation a :exc:`.CheckFailure`
exception is raised and sent to the event corresponding to your application
command's type.

If an exception should be thrown in the predicate then it should be a
subclass of :exc:`.CommandError`. Any exception not subclassed from it
will be propagated while those subclassed will be sent to
the event corresponding to your application command's type.

A special attribute named ``predicate`` is bound to the value
returned by this decorator to retrieve the predicate passed to the
decorator. This allows the following introspection and chaining to be done:

.. code-block:: python3

def owner_or_permissions(**perms):
original = commands.has_permissions(**perms).predicate
async def extended_check(inter):
if inter.guild is None:
return False
return inter.guild.owner_id == inter.author.id or await original(ctx)
return commands.check(extended_check)

.. note::

The function returned by ``predicate`` is **always** a coroutine,
even if the original function was not a coroutine.

.. versionadded:: 2.10

Examples
--------
Creating a basic check to see if the command invoker is you.

.. code-block:: python3

def check_if_it_is_me(inter):
return ctx.message.author.id == 85309593344815104

@bot.slash_command()
@commands.app_check(check_if_it_is_me)
async def only_for_me(inter):
await inter.send('I know you!')

Transforming common checks into its own decorator:

.. code-block:: python3

def is_me():
def predicate(inter):
return ctx.message.author.id == 85309593344815104
return commands.check(predicate)

@bot.slash_command()
@is_me()
async def only_me(inter):
await inter.send('Only you!')

Parameters
----------
predicate: Callable[[:class:`disnake.ApplicationCommandInteraction`], :class:`bool`]
The predicate to check if the command should be invoked.
"""

def decorator(func: Union[InvokableApplicationCommand, CoroFunc]) -> Union[Command, CoroFunc]:
if hasattr(func, "__command_flag__"):
func.checks.append(predicate)
else:
if not hasattr(func, "__commands_checks__"):
func.__commands_checks__ = [] # type: ignore

func.__commands_checks__.append(predicate) # type: ignore

return func

if asyncio.iscoroutinefunction(predicate):
decorator.predicate = predicate
else:

@functools.wraps(predicate)
async def wrapper(ctx):
return predicate(ctx) # type: ignore

decorator.predicate = wrapper

return decorator # type: ignore


def app_check_any(*checks: AppCheck) -> Callable[[T], T]:
elenakrittik marked this conversation as resolved.
Show resolved Hide resolved
"""A :func:`check` that is added that checks if any of the checks passed
will pass, i.e. using logical OR.

If all checks fail then :exc:`.CheckAnyFailure` is raised to signal the failure.
It inherits from :exc:`.CheckFailure`.

.. note::

The ``predicate`` attribute for this function **is** a coroutine.

.. versionadded:: 2.10

Parameters
----------
*checks: Callable[[:class:`disnake.ApplicationCommandInteraction
`], :class:`bool`]
An argument list of checks that have been decorated with
the :func:`check` decorator.

Raises
------
TypeError
A check passed has not been decorated with the :func:`check`
decorator.

Examples
--------
Creating a basic check to see if it's the bot owner or
the server owner:

.. code-block:: python3

def is_guild_owner():
def predicate(inter):
return inter.guild is not None and inter.guild.owner_id == inter.author.id
return commands.check(predicate)

@bot.slash_command()
@commands.check_any(commands.is_owner(), is_guild_owner())
async def only_for_owners(inter):
await inter.send('Hello mister owner!')
"""
unwrapped = []
for wrapped in checks:
try:
pred = wrapped.predicate
except AttributeError:
raise TypeError(f"{wrapped!r} must be wrapped by commands.check decorator") from None
else:
unwrapped.append(pred)

async def predicate(ctx: AnyContext) -> bool:
errors = []
for func in unwrapped:
try:
value = await func(ctx)
except CheckFailure as e:
errors.append(e)
else:
if value:
return True
# if we're here, all checks failed
raise CheckAnyFailure(unwrapped, errors)

return check(predicate)


def has_role(item: Union[int, str]) -> Callable[[T], T]:
"""A :func:`.check` that is added that checks if the member invoking the
command has the role specified via the name or ID specified.
Expand Down
8 changes: 4 additions & 4 deletions disnake/ext/commands/interaction_bot_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
)
from disnake.permissions import Permissions

from ._types import Check, CoroFunc
from ._types import AppCheck, CoroFunc
from .base_core import CogT, CommandCallback, InteractionCommandCallback

P = ParamSpec("P")
Expand Down Expand Up @@ -991,7 +991,7 @@ async def on_message_command_error(

def add_app_command_check(
elenakrittik marked this conversation as resolved.
Show resolved Hide resolved
self,
func: Check,
func: AppCheck,
*,
call_once: bool = False,
slash_commands: bool = False,
Expand Down Expand Up @@ -1039,7 +1039,7 @@ def add_app_command_check(

def remove_app_command_check(
self,
func: Check,
func: AppCheck,
*,
call_once: bool = False,
slash_commands: bool = False,
Expand Down Expand Up @@ -1179,7 +1179,7 @@ def decorator(
) -> Callable[[ApplicationCommandInteraction], Any]:
elenakrittik marked this conversation as resolved.
Show resolved Hide resolved
# T was used instead of Check to ensure the type matches on return
self.add_app_command_check(
func, # type: ignore
func,
call_once=call_once,
slash_commands=slash_commands,
user_commands=user_commands,
Expand Down