Skip to content

Commit 7b8a46d

Browse files
authored
Merge branch 'Pycord-Development:master' into actx-responses
2 parents b19a745 + 0711ee7 commit 7b8a46d

File tree

8 files changed

+198
-32
lines changed

8 files changed

+198
-32
lines changed

.github/CONTRIBUTING.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,14 @@ deciding to ignore type checking warnings.
4343

4444
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.
4545

46-
### Git Commit Guidelines
46+
## Git Commit Styling
4747

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

54-
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.
50+
Some style guides we would recommed using in your pulls:
51+
52+
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.
53+
54+
The [gitmoji](https://gitmoji.dev) style guide would make your pull look more lively and different to others.
55+
56+
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.

discord/commands/commands.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
import re
3232
import types
3333
from collections import OrderedDict
34-
from typing import Any, Callable, Dict, List, Optional, Type, Union, TYPE_CHECKING
34+
from typing import Any, Callable, Dict, Generic, List, Optional, Type, TypeVar, Union, TYPE_CHECKING
35+
from typing_extensions import ParamSpec
3536

3637
from .context import ApplicationContext, AutocompleteContext
3738
from .errors import ApplicationCommandError, CheckFailure, ApplicationCommandInvokeError
@@ -61,9 +62,18 @@
6162
"MessageCommand",
6263
)
6364

64-
if TYPE_CHECKING:
65+
if TYPE_CHECKING:
66+
from ..cog import Cog
6567
from ..interactions import Interaction
6668

69+
T = TypeVar('T')
70+
CogT = TypeVar('CogT', bound='Cog')
71+
72+
if TYPE_CHECKING:
73+
P = ParamSpec('P')
74+
else:
75+
P = TypeVar('P')
76+
6777
def wrap_callback(coro):
6878
@functools.wraps(coro)
6979
async def wrapped(*args, **kwargs):
@@ -97,7 +107,7 @@ async def wrapped(arg):
97107
class _BaseCommand:
98108
__slots__ = ()
99109

100-
class ApplicationCommand(_BaseCommand):
110+
class ApplicationCommand(_BaseCommand, Generic[CogT, P, T]):
101111
cog = None
102112

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

532-
elif op.input_type == SlashCommandOptionType.string and op._converter is not None:
533-
arg = await op._converter.convert(ctx, arg)
542+
elif op.input_type == SlashCommandOptionType.string and (converter := op.converter) is not None:
543+
arg = await converter.convert(converter, ctx, arg)
534544

535545
kwargs[op._parameter_name] = arg
536546

@@ -626,11 +636,11 @@ def __init__(
626636
) -> None:
627637
self.name: Optional[str] = kwargs.pop("name", None)
628638
self.description = description or "No description provided"
629-
self._converter = None
639+
self.converter = None
630640
self.channel_types: List[SlashCommandOptionType] = kwargs.pop("channel_types", [])
631641
if not isinstance(input_type, SlashCommandOptionType):
632642
if hasattr(input_type, "convert"):
633-
self._converter = input_type
643+
self.converter = input_type
634644
input_type = SlashCommandOptionType.string
635645
else:
636646
_type = SlashCommandOptionType.from_datatype(input_type)

discord/commands/context.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@
2424
"""
2525
from __future__ import annotations
2626

27-
from typing import TYPE_CHECKING, Optional, Union
27+
from typing import Callable, TYPE_CHECKING, Optional, TypeVar, Union
2828

2929
import discord.abc
3030

3131
if TYPE_CHECKING:
32+
from typing_extensions import ParamSpec
33+
3234
import discord
3335
from discord import Bot
3436
from discord.state import ConnectionState
@@ -44,6 +46,15 @@
4446
from ..message import Message
4547
from ..user import User
4648
from ..utils import cached_property
49+
from ..webhook import Webhook
50+
51+
T = TypeVar('T')
52+
CogT = TypeVar('CogT', bound="Cog")
53+
54+
if TYPE_CHECKING:
55+
P = ParamSpec('P')
56+
else:
57+
P = TypeVar('P')
4758

4859
__all__ = ("ApplicationContext", "AutocompleteContext")
4960

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

95+
async def invoke(self, command: ApplicationCommand[CogT, P, T], /, *args: P.args, **kwargs: P.kwargs) -> T:
96+
r"""|coro|
97+
Calls a command with the arguments given.
98+
This is useful if you want to just call the callback that a
99+
:class:`.ApplicationCommand` holds internally.
100+
.. note::
101+
This does not handle converters, checks, cooldowns, pre-invoke,
102+
or after-invoke hooks in any matter. It calls the internal callback
103+
directly as-if it was a regular function.
104+
You must take care in passing the proper arguments when
105+
using this function.
106+
Parameters
107+
-----------
108+
command: :class:`.ApplicationCommand`
109+
The command that is going to be called.
110+
\*args
111+
The arguments to use.
112+
\*\*kwargs
113+
The keyword arguments to use.
114+
Raises
115+
-------
116+
TypeError
117+
The command argument to invoke is missing.
118+
"""
119+
return await command(self, *args, **kwargs)
120+
84121
@cached_property
85122
def channel(self):
86123
return self.interaction.channel

discord/ext/pages/pagination.py

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -291,14 +291,14 @@ def update_buttons(self) -> Dict:
291291

292292
return self.buttons
293293

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

311-
if not isinstance(messageable, abc.Messageable):
312-
raise TypeError("messageable should be a subclass of abc.Messageable")
313-
314311
page = self.pages[0]
315312

316-
if isinstance(messageable, (ApplicationContext, Context)):
317-
self.user = messageable.author
313+
self.user = ctx.author
318314

319-
if isinstance(messageable, ApplicationContext):
320-
msg = await messageable.respond(
315+
if isinstance(ctx, ApplicationContext):
316+
msg = await ctx.respond(
321317
content=page if isinstance(page, str) else None,
322318
embed=page if isinstance(page, discord.Embed) else None,
323319
view=self,
324320
ephemeral=ephemeral,
325321
)
326322

327323
else:
328-
msg = await messageable.send(
324+
msg = await ctx.send(
329325
content=page if isinstance(page, str) else None,
330326
embed=page if isinstance(page, discord.Embed) else None,
331327
view=self,
@@ -370,3 +366,49 @@ async def respond(self, interaction: discord.Interaction, ephemeral: bool = Fals
370366
elif isinstance(msg, discord.Interaction):
371367
self.message = await msg.original_message()
372368
return self.message
369+
370+
async def update(
371+
self,
372+
interaction: discord.Interaction,
373+
pages: List[Union[str, discord.Embed]],
374+
show_disabled: Optional[bool] = None,
375+
show_indicator: Optional[bool] = None,
376+
author_check: Optional[bool] = None,
377+
disable_on_timeout: Optional[bool] = None,
378+
custom_view: Optional[discord.ui.View] = None,
379+
timeout: Optional[float] = None
380+
):
381+
"""Updates the paginator. This might be useful if you use a view with :class:`discord.SelectOption`
382+
and you have a different amount of pages depending on the selected option.
383+
384+
Parameters
385+
----------
386+
pages: List[Union[:class:`str`, :class:`discord.Embed`]]
387+
The list of strings and/or embeds to paginate.
388+
show_disabled: Optional[:class:`bool`]
389+
Whether to show disabled buttons.
390+
show_indicator: Optional[:class:`bool`]
391+
Whether to show the page indicator.
392+
author_check: Optional[:class:`bool`]
393+
Whether only the original user of the command can change pages.
394+
disable_on_timeout: Optional[:class:`bool`]
395+
Whether the buttons get disabled when the paginator view times out.
396+
custom_view: Optional[:class:`discord.ui.View`]
397+
A custom view whose items are appended below the pagination buttons.
398+
timeout: Optional[:class:`float`]
399+
Timeout in seconds from last interaction with the paginator before no longer accepting input.
400+
"""
401+
402+
# Update pages and reset current_page to 0 (default)
403+
self.pages = pages
404+
self.page_count = len(self.pages) - 1
405+
self.current_page = 0
406+
# Apply config changes, if specified
407+
self.show_disabled = show_disabled if show_disabled else self.show_disabled
408+
self.show_indicator = show_indicator if show_indicator else self.show_indicator
409+
self.usercheck = author_check if author_check else self.usercheck
410+
self.disable_on_timeout = disable_on_timeout if disable_on_timeout else self.disable_on_timeout
411+
self.custom_view = custom_view if custom_view else self.custom_view
412+
self.timeout = timeout if timeout else self.timeout
413+
414+
await self.goto_page(interaction, self.current_page)

discord/threads.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,36 @@ async def edit(
589589
data = await self._state.http.edit_channel(self.id, **payload)
590590
# The data payload will always be a Thread payload
591591
return Thread(data=data, state=self._state, guild=self.guild) # type: ignore
592+
593+
async def archive(self, locked: bool = MISSING) -> Thread:
594+
"""|coro|
595+
596+
Archives the thread. This is a shorthand of :meth:`.edit`.
597+
598+
Parameters
599+
------------
600+
locked: :class:`bool`
601+
Whether to lock the thread on archive, Defaults to ``False``.
602+
603+
604+
Returns
605+
--------
606+
:class:`.Thread`
607+
The updated thread.
608+
"""
609+
return await self.edit(archived=True, locked=locked)
610+
611+
async def unarchive(self) -> Thread:
612+
"""|coro|
613+
614+
Unarchives the thread. This is a shorthand of :meth:`.edit`.
615+
616+
Returns
617+
--------
618+
:class:`.Thread`
619+
The updated thread.
620+
"""
621+
return await self.edit(archived=False)
592622

593623
async def join(self):
594624
"""|coro|

discord/utils.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,13 +1106,13 @@ async def autocomplete(ctx):
11061106
11071107
Parameters
11081108
-----------
1109-
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`]]]]
1109+
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`]]]]
11101110
Possible values for the option. Accepts an iterable of :class:`str`, a callable (sync or async) that takes a
11111111
single argument of :class:`AutocompleteContext`, or a coroutine. Must resolve to an iterable of :class:`str`.
11121112
11131113
Returns
11141114
--------
1115-
Callable[[:class:`AutocompleteContext`], Awaitable[Union[Iterable[:class:`str`], Iterable[:class:`int`], Iterable[:class:`float`]]]]
1115+
Callable[[:class:`AutocompleteContext`], Awaitable[Union[Iterable[:class:`OptionChoice`], Iterable[:class:`str`], Iterable[:class:`int`], Iterable[:class:`float`]]]]
11161116
A wrapped callback for the autocomplete.
11171117
"""
11181118
async def autocomplete_callback(ctx: AutocompleteContext) -> V:
@@ -1123,7 +1123,11 @@ async def autocomplete_callback(ctx: AutocompleteContext) -> V:
11231123
if asyncio.iscoroutine(_values):
11241124
_values = await _values
11251125

1126-
gen = (val for val in _values if str(val).lower().startswith(str(ctx.value or "").lower()))
1126+
def check(item: Any) -> bool:
1127+
item = getattr(item, "name", item)
1128+
return str(item).lower().startswith(str(ctx.value or "").lower())
1129+
1130+
gen = (val for val in _values if check(val))
11271131
return iter(itertools.islice(gen, 25))
11281132

11291133
return autocomplete_callback

docs/quickstart.rst

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
.. currentmodule:: discord
66

77
Quickstart
8-
============
8+
==========
99

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

1313
A Minimal Bot
14-
---------------
14+
-------------
1515

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

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

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

4545
1. The first line just imports the library, if this raises a `ModuleNotFoundError` or `ImportError`
4646
then head on over to :ref:`installing` section to properly install.
@@ -77,3 +77,41 @@ On other systems:
7777
$ python3 example_bot.py
7878
7979
Now you can try playing around with your basic bot.
80+
81+
A Minimal Bot with Slash Commands
82+
---------------------------------
83+
84+
As a continuation, let's create a bot that registers a simple slash command!
85+
86+
It looks something like this:
87+
88+
.. code-block:: python3
89+
90+
import discord
91+
92+
bot = discord.Bot()
93+
94+
@bot.event
95+
async def on_ready():
96+
print(f"We have logged in as {bot.user}")
97+
98+
@bot.slash_command(guild_ids=[your, guild_ids, here])
99+
async def hello(ctx):
100+
await ctx.respond("Hello!")
101+
102+
bot.run("your token here")
103+
104+
Let's look at the differences compared to the previous example, step-by-step:
105+
106+
1. The first line remains unchanged.
107+
2. Next, we create an instance of :class:`.Bot`. This is different from :class:`.Client`, as it supports
108+
slash command creation and other features, while inheriting all the features of :class:`.Client`.
109+
3. We then use the :meth:`.Bot.slash_command` decorator to register a new slash command.
110+
The ``guild_ids`` attribute contains a list of guilds where this command will be active.
111+
If you omit it, the command will be globally available, and may take up to an hour to register.
112+
4. Afterwards, we trigger a response to the slash command in the form of a text reply. Please note that
113+
all slash commands must have some form of response, otherwise they will fail.
114+
6. Finally, we, once again, run the bot with our login token.
115+
116+
117+
Congratulations! Now you have created your first slash command!

examples/app_commands/slash_basic.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
async def hello(ctx):
1515
"""Say hello to the bot""" # the command description can be supplied as the docstring
1616
await ctx.respond(f"Hello {ctx.author}!")
17+
# Please note that you MUST respond with ctx.respond(), ctx.defer(), or any other
18+
# interaction response within 3 seconds in your slash command code, otherwise the
19+
# interaction will fail.
1720

1821

1922
@bot.slash_command(

0 commit comments

Comments
 (0)