Skip to content

Commit

Permalink
Merge branch 'Pycord-Development:master' into actx-responses
Browse files Browse the repository at this point in the history
  • Loading branch information
krittick authored Dec 29, 2021
2 parents b19a745 + 0711ee7 commit 7b8a46d
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 32 deletions.
16 changes: 9 additions & 7 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,14 @@ deciding to ignore type checking warnings.

By submitting a pull request, you agree that; 1) You hold the copyright on all submitted code inside said pull request; 2) You agree to transfer all rights to the owner of this repository, and; 3) If you are found to be in fault with any of the above, we shall not be held responsible in any way after the pull request has been merged.

### Git Commit Guidelines
## Git Commit Styling

- Use present tense (e.g. "Add feature" not "Added feature")
- Limit all lines to 72 characters or less.
- Reference issues or pull requests outside of the first line.
- Please use the shorthand `#123` and not the full URL.
- Commits regarding the commands extension must be prefixed with `[commands]`
Not following this guideline could lead to your pull being squashed for a cleaner commit history

If you do not meet any of these guidelines, don't fret. Chances are they will be fixed upon rebasing but please do try to meet them to remove some of the workload.
Some style guides we would recommed using in your pulls:

The [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) style is a very widely used style and a good style to start with.

The [gitmoji](https://gitmoji.dev) style guide would make your pull look more lively and different to others.

We don't limit nor deny your pulls when you're using another style although, please make sure it is appropriate and makes sense in this library.
24 changes: 17 additions & 7 deletions discord/commands/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
import re
import types
from collections import OrderedDict
from typing import Any, Callable, Dict, List, Optional, Type, Union, TYPE_CHECKING
from typing import Any, Callable, Dict, Generic, List, Optional, Type, TypeVar, Union, TYPE_CHECKING
from typing_extensions import ParamSpec

from .context import ApplicationContext, AutocompleteContext
from .errors import ApplicationCommandError, CheckFailure, ApplicationCommandInvokeError
Expand Down Expand Up @@ -61,9 +62,18 @@
"MessageCommand",
)

if TYPE_CHECKING:
if TYPE_CHECKING:
from ..cog import Cog
from ..interactions import Interaction

T = TypeVar('T')
CogT = TypeVar('CogT', bound='Cog')

if TYPE_CHECKING:
P = ParamSpec('P')
else:
P = TypeVar('P')

def wrap_callback(coro):
@functools.wraps(coro)
async def wrapped(*args, **kwargs):
Expand Down Expand Up @@ -97,7 +107,7 @@ async def wrapped(arg):
class _BaseCommand:
__slots__ = ()

class ApplicationCommand(_BaseCommand):
class ApplicationCommand(_BaseCommand, Generic[CogT, P, T]):
cog = None

def __repr__(self):
Expand Down Expand Up @@ -529,8 +539,8 @@ async def _invoke(self, ctx: ApplicationContext) -> None:
if arg is None:
arg = ctx.guild.get_role(arg_id) or arg_id

elif op.input_type == SlashCommandOptionType.string and op._converter is not None:
arg = await op._converter.convert(ctx, arg)
elif op.input_type == SlashCommandOptionType.string and (converter := op.converter) is not None:
arg = await converter.convert(converter, ctx, arg)

kwargs[op._parameter_name] = arg

Expand Down Expand Up @@ -626,11 +636,11 @@ def __init__(
) -> None:
self.name: Optional[str] = kwargs.pop("name", None)
self.description = description or "No description provided"
self._converter = None
self.converter = None
self.channel_types: List[SlashCommandOptionType] = kwargs.pop("channel_types", [])
if not isinstance(input_type, SlashCommandOptionType):
if hasattr(input_type, "convert"):
self._converter = input_type
self.converter = input_type
input_type = SlashCommandOptionType.string
else:
_type = SlashCommandOptionType.from_datatype(input_type)
Expand Down
39 changes: 38 additions & 1 deletion discord/commands/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@
"""
from __future__ import annotations

from typing import TYPE_CHECKING, Optional, Union
from typing import Callable, TYPE_CHECKING, Optional, TypeVar, Union

import discord.abc

if TYPE_CHECKING:
from typing_extensions import ParamSpec

import discord
from discord import Bot
from discord.state import ConnectionState
Expand All @@ -44,6 +46,15 @@
from ..message import Message
from ..user import User
from ..utils import cached_property
from ..webhook import Webhook

T = TypeVar('T')
CogT = TypeVar('CogT', bound="Cog")

if TYPE_CHECKING:
P = ParamSpec('P')
else:
P = TypeVar('P')

__all__ = ("ApplicationContext", "AutocompleteContext")

Expand Down Expand Up @@ -81,6 +92,32 @@ def __init__(self, bot: Bot, interaction: Interaction):
async def _get_channel(self) -> discord.abc.Messageable:
return self.channel

async def invoke(self, command: ApplicationCommand[CogT, P, T], /, *args: P.args, **kwargs: P.kwargs) -> T:
r"""|coro|
Calls a command with the arguments given.
This is useful if you want to just call the callback that a
:class:`.ApplicationCommand` holds internally.
.. note::
This does not handle converters, checks, cooldowns, pre-invoke,
or after-invoke hooks in any matter. It calls the internal callback
directly as-if it was a regular function.
You must take care in passing the proper arguments when
using this function.
Parameters
-----------
command: :class:`.ApplicationCommand`
The command that is going to be called.
\*args
The arguments to use.
\*\*kwargs
The keyword arguments to use.
Raises
-------
TypeError
The command argument to invoke is missing.
"""
return await command(self, *args, **kwargs)

@cached_property
def channel(self):
return self.interaction.channel
Expand Down
64 changes: 53 additions & 11 deletions discord/ext/pages/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,14 +291,14 @@ def update_buttons(self) -> Dict:

return self.buttons

async def send(self, messageable: abc.Messageable, ephemeral: bool = False) -> Union[discord.Message, discord.WebhookMessage]:
async def send(self, ctx: Union[ApplicationContext, Context], ephemeral: bool = False) -> Union[discord.Message, discord.WebhookMessage]:
"""Sends a message with the paginated items.
Parameters
------------
messageable: :class:`discord.abc.Messageable`
The messageable channel to send to.
ctx: Union[:class:`~discord.ext.commands.Context`, :class:`~discord.ApplicationContext`]
A command's invocation context.
ephemeral: :class:`bool`
Choose whether the message is ephemeral or not. Only works with slash commands.
Expand All @@ -308,24 +308,20 @@ async def send(self, messageable: abc.Messageable, ephemeral: bool = False) -> U
The message that was sent with the paginator.
"""

if not isinstance(messageable, abc.Messageable):
raise TypeError("messageable should be a subclass of abc.Messageable")

page = self.pages[0]

if isinstance(messageable, (ApplicationContext, Context)):
self.user = messageable.author
self.user = ctx.author

if isinstance(messageable, ApplicationContext):
msg = await messageable.respond(
if isinstance(ctx, ApplicationContext):
msg = await ctx.respond(
content=page if isinstance(page, str) else None,
embed=page if isinstance(page, discord.Embed) else None,
view=self,
ephemeral=ephemeral,
)

else:
msg = await messageable.send(
msg = await ctx.send(
content=page if isinstance(page, str) else None,
embed=page if isinstance(page, discord.Embed) else None,
view=self,
Expand Down Expand Up @@ -370,3 +366,49 @@ async def respond(self, interaction: discord.Interaction, ephemeral: bool = Fals
elif isinstance(msg, discord.Interaction):
self.message = await msg.original_message()
return self.message

async def update(
self,
interaction: discord.Interaction,
pages: List[Union[str, discord.Embed]],
show_disabled: Optional[bool] = None,
show_indicator: Optional[bool] = None,
author_check: Optional[bool] = None,
disable_on_timeout: Optional[bool] = None,
custom_view: Optional[discord.ui.View] = None,
timeout: Optional[float] = None
):
"""Updates the paginator. This might be useful if you use a view with :class:`discord.SelectOption`
and you have a different amount of pages depending on the selected option.
Parameters
----------
pages: List[Union[:class:`str`, :class:`discord.Embed`]]
The list of strings and/or embeds to paginate.
show_disabled: Optional[:class:`bool`]
Whether to show disabled buttons.
show_indicator: Optional[:class:`bool`]
Whether to show the page indicator.
author_check: Optional[:class:`bool`]
Whether only the original user of the command can change pages.
disable_on_timeout: Optional[:class:`bool`]
Whether the buttons get disabled when the paginator view times out.
custom_view: Optional[:class:`discord.ui.View`]
A custom view whose items are appended below the pagination buttons.
timeout: Optional[:class:`float`]
Timeout in seconds from last interaction with the paginator before no longer accepting input.
"""

# Update pages and reset current_page to 0 (default)
self.pages = pages
self.page_count = len(self.pages) - 1
self.current_page = 0
# Apply config changes, if specified
self.show_disabled = show_disabled if show_disabled else self.show_disabled
self.show_indicator = show_indicator if show_indicator else self.show_indicator
self.usercheck = author_check if author_check else self.usercheck
self.disable_on_timeout = disable_on_timeout if disable_on_timeout else self.disable_on_timeout
self.custom_view = custom_view if custom_view else self.custom_view
self.timeout = timeout if timeout else self.timeout

await self.goto_page(interaction, self.current_page)
30 changes: 30 additions & 0 deletions discord/threads.py
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,36 @@ async def edit(
data = await self._state.http.edit_channel(self.id, **payload)
# The data payload will always be a Thread payload
return Thread(data=data, state=self._state, guild=self.guild) # type: ignore

async def archive(self, locked: bool = MISSING) -> Thread:
"""|coro|
Archives the thread. This is a shorthand of :meth:`.edit`.
Parameters
------------
locked: :class:`bool`
Whether to lock the thread on archive, Defaults to ``False``.
Returns
--------
:class:`.Thread`
The updated thread.
"""
return await self.edit(archived=True, locked=locked)

async def unarchive(self) -> Thread:
"""|coro|
Unarchives the thread. This is a shorthand of :meth:`.edit`.
Returns
--------
:class:`.Thread`
The updated thread.
"""
return await self.edit(archived=False)

async def join(self):
"""|coro|
Expand Down
10 changes: 7 additions & 3 deletions discord/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1106,13 +1106,13 @@ async def autocomplete(ctx):
Parameters
-----------
values: Union[Union[Iterable[:class:`str`], Iterable[:class:`int`], Iterable[:class:`float`]], Callable[[:class:`AutocompleteContext`], Union[Union[Iterable[:class:`str`], Iterable[:class:`int`], Iterable[:class:`float`]], Awaitable[Union[Iterable[:class:`str`], Iterable[:class:`int`], Iterable[:class:`float`]]]]], Awaitable[Union[Iterable[:class:`str`], Iterable[:class:`int`], Iterable[:class:`float`]]]]
values: Union[Union[Iterable[:class:`OptionChoice`], Iterable[:class:`str`], Iterable[:class:`int`], Iterable[:class:`float`]], Callable[[:class:`AutocompleteContext`], Union[Union[Iterable[:class:`str`], Iterable[:class:`int`], Iterable[:class:`float`]], Awaitable[Union[Iterable[:class:`str`], Iterable[:class:`int`], Iterable[:class:`float`]]]]], Awaitable[Union[Iterable[:class:`str`], Iterable[:class:`int`], Iterable[:class:`float`]]]]
Possible values for the option. Accepts an iterable of :class:`str`, a callable (sync or async) that takes a
single argument of :class:`AutocompleteContext`, or a coroutine. Must resolve to an iterable of :class:`str`.
Returns
--------
Callable[[:class:`AutocompleteContext`], Awaitable[Union[Iterable[:class:`str`], Iterable[:class:`int`], Iterable[:class:`float`]]]]
Callable[[:class:`AutocompleteContext`], Awaitable[Union[Iterable[:class:`OptionChoice`], Iterable[:class:`str`], Iterable[:class:`int`], Iterable[:class:`float`]]]]
A wrapped callback for the autocomplete.
"""
async def autocomplete_callback(ctx: AutocompleteContext) -> V:
Expand All @@ -1123,7 +1123,11 @@ async def autocomplete_callback(ctx: AutocompleteContext) -> V:
if asyncio.iscoroutine(_values):
_values = await _values

gen = (val for val in _values if str(val).lower().startswith(str(ctx.value or "").lower()))
def check(item: Any) -> bool:
item = getattr(item, "name", item)
return str(item).lower().startswith(str(ctx.value or "").lower())

gen = (val for val in _values if check(val))
return iter(itertools.islice(gen, 25))

return autocomplete_callback
44 changes: 41 additions & 3 deletions docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
.. currentmodule:: discord

Quickstart
============
==========

This page gives a brief introduction to the library. It assumes you have the library installed,
if you don't check the :ref:`installing` portion.

A Minimal Bot
---------------
-------------

Let's make a bot that responds to a specific message and walk you through it.

Expand Down Expand Up @@ -40,7 +40,7 @@ It looks something like this:
Let's name this file ``example_bot.py``. Make sure not to name it ``discord.py`` as that'll conflict
with the library.

There's a lot going on here, so let's walk you through it step by step.
There's a lot going on here, so let's walk you through it step by step:

1. The first line just imports the library, if this raises a `ModuleNotFoundError` or `ImportError`
then head on over to :ref:`installing` section to properly install.
Expand Down Expand Up @@ -77,3 +77,41 @@ On other systems:
$ python3 example_bot.py
Now you can try playing around with your basic bot.

A Minimal Bot with Slash Commands
---------------------------------

As a continuation, let's create a bot that registers a simple slash command!

It looks something like this:

.. code-block:: python3
import discord
bot = discord.Bot()
@bot.event
async def on_ready():
print(f"We have logged in as {bot.user}")
@bot.slash_command(guild_ids=[your, guild_ids, here])
async def hello(ctx):
await ctx.respond("Hello!")
bot.run("your token here")
Let's look at the differences compared to the previous example, step-by-step:

1. The first line remains unchanged.
2. Next, we create an instance of :class:`.Bot`. This is different from :class:`.Client`, as it supports
slash command creation and other features, while inheriting all the features of :class:`.Client`.
3. We then use the :meth:`.Bot.slash_command` decorator to register a new slash command.
The ``guild_ids`` attribute contains a list of guilds where this command will be active.
If you omit it, the command will be globally available, and may take up to an hour to register.
4. Afterwards, we trigger a response to the slash command in the form of a text reply. Please note that
all slash commands must have some form of response, otherwise they will fail.
6. Finally, we, once again, run the bot with our login token.


Congratulations! Now you have created your first slash command!
3 changes: 3 additions & 0 deletions examples/app_commands/slash_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
async def hello(ctx):
"""Say hello to the bot""" # the command description can be supplied as the docstring
await ctx.respond(f"Hello {ctx.author}!")
# Please note that you MUST respond with ctx.respond(), ctx.defer(), or any other
# interaction response within 3 seconds in your slash command code, otherwise the
# interaction will fail.


@bot.slash_command(
Expand Down

0 comments on commit 7b8a46d

Please sign in to comment.