Skip to content
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6dbe118
Client & Bot
Soheab May 16, 2025
b4a10c5
fix: intents should actually be required
Soheab May 16, 2025
38b72da
Remove unused imports
Soheab May 16, 2025
79e426f
flags: Intents
Soheab May 17, 2025
3ff6628
ext.commands: cog, core
Soheab May 17, 2025
13848f0
Remove unneeded total=False
Soheab May 17, 2025
edb32b6
Run "black discord"
Soheab May 17, 2025
7e8a254
ext.commands: hybrid
Soheab May 17, 2025
89a210b
Remove unusued imports
Soheab May 17, 2025
f2700a9
flags: MemberCacheFlags
Soheab May 17, 2025
624ce4d
Permission[s](Overwrite)
Soheab May 17, 2025
2353a82
AutoSharded(Client/Bot)
Soheab May 17, 2025
574235c
Remove unnecessary comments
Soheab May 17, 2025
0145ecb
permissions
Soheab May 17, 2025
62a59ba
ext.commands: help
Soheab May 17, 2025
21b07a2
ext.commands: core
Soheab May 17, 2025
62e6b8e
ext.commands: bot
Soheab May 17, 2025
b2173bf
ext.commands: checks
Soheab May 17, 2025
a4aeab9
abc
Soheab May 17, 2025
a79a693
app_commands: commands
Soheab May 17, 2025
621af5f
Add missing kwargs for (AutoSharded)Bot
Soheab May 18, 2025
b52ded6
Use total=False over NotRequired everything
Soheab May 20, 2025
6224fb5
Cleanup types for Permission(s)(Overwrite)
Soheab May 23, 2025
63e799d
Category.create_text/voice/stage/forum(_channel)
Soheab May 23, 2025
33dae2e
Remove unused classes
Soheab Aug 14, 2025
8a004a1
Remove from Command.__new__
Soheab Aug 14, 2025
999b619
Merge remote-tracking branch 'upstream/master' into feat/typed-kwargs
Soheab Aug 14, 2025
332f1ae
Add comment to type-ignore
Soheab Aug 14, 2025
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
7 changes: 4 additions & 3 deletions discord/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
T = TypeVar('T', bound=VoiceProtocol)

if TYPE_CHECKING:
from typing_extensions import Self
from typing_extensions import Self, Unpack

Check warning on line 80 in discord/abc.py

View workflow job for this annotation

GitHub Actions / check 3.x

Import "typing_extensions" could not be resolved from source (reportMissingModuleSource)

from .client import Client
from .user import ClientUser
Expand Down Expand Up @@ -112,6 +112,7 @@
from .types.snowflake import (
SnowflakeList,
)
from .permissions import _PermissionOverwriteKwargs

PartialMessageableChannel = Union[TextChannel, VoiceChannel, StageChannel, Thread, DMChannel, PartialMessageable]
MessageableChannel = Union[PartialMessageableChannel, GroupChannel]
Expand Down Expand Up @@ -915,7 +916,7 @@
target: Union[Member, Role],
*,
reason: Optional[str] = ...,
**permissions: Optional[bool],
**permissions: Unpack[_PermissionOverwriteKwargs],
) -> None:
...

Expand All @@ -925,7 +926,7 @@
*,
overwrite: Any = _undefined,
reason: Optional[str] = None,
**permissions: Optional[bool],
**permissions: Unpack[_PermissionOverwriteKwargs],
) -> None:
r"""|coro|

Expand Down
7 changes: 4 additions & 3 deletions discord/app_commands/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@
T = TypeVar('T')

if TYPE_CHECKING:
from typing_extensions import Self
from typing_extensions import Self, Unpack
from ..interactions import Interaction
from ..permissions import _PermissionsKwargs

CooldownFunction = Union[
Callable[[Interaction[Any]], Coroutine[Any, Any, T]],
Expand Down Expand Up @@ -286,7 +287,7 @@ def predicate(interaction: Interaction) -> bool:
return check(predicate)


def has_permissions(**perms: bool) -> Callable[[T], T]:
def has_permissions(**perms: Unpack[_PermissionsKwargs]) -> Callable[[T], T]:
r"""A :func:`~discord.app_commands.check` that is added that checks if the member
has all of the permissions necessary.

Expand Down Expand Up @@ -341,7 +342,7 @@ def predicate(interaction: Interaction) -> bool:
return check(predicate)


def bot_has_permissions(**perms: bool) -> Callable[[T], T]:
def bot_has_permissions(**perms: Unpack[_PermissionsKwargs]) -> Callable[[T], T]:
"""Similar to :func:`has_permissions` except checks if the bot itself has
the permissions listed. This relies on :attr:`discord.Interaction.app_permissions`.

Expand Down
5 changes: 3 additions & 2 deletions discord/app_commands/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
from ..utils import resolve_annotation, MISSING, is_inside_class, maybe_coroutine, async_all, _shorten, _to_kebab_case

if TYPE_CHECKING:
from typing_extensions import ParamSpec, Concatenate
from typing_extensions import ParamSpec, Concatenate, Unpack
from ..interactions import Interaction
from ..abc import Snowflake
from .namespace import Namespace
Expand All @@ -73,6 +73,7 @@
# However, for type hinting purposes it's unfortunately necessary for one to
# reference the other to prevent type checking errors in callbacks
from discord.ext import commands
from discord.permissions import _PermissionsKwargs

ErrorFunc = Callable[[Interaction, AppCommandError], Coroutine[Any, Any, None]]

Expand Down Expand Up @@ -2840,7 +2841,7 @@ def inner(f: T) -> T:
return inner


def default_permissions(perms_obj: Optional[Permissions] = None, /, **perms: bool) -> Callable[[T], T]:
def default_permissions(perms_obj: Optional[Permissions] = None, /, **perms: Unpack[_PermissionsKwargs]) -> Callable[[T], T]:
r"""A decorator that sets the default permissions needed to execute this command.

When this decorator is used, by default users must have these permissions to execute the command.
Expand Down
49 changes: 44 additions & 5 deletions discord/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
Sequence,
Tuple,
TypeVar,
TypedDict,
Union,
overload,
)
Expand Down Expand Up @@ -85,7 +86,7 @@
)

if TYPE_CHECKING:
from typing_extensions import Self
from typing_extensions import Self, Unpack

Check warning on line 89 in discord/channel.py

View workflow job for this annotation

GitHub Actions / check 3.x

Import "typing_extensions" could not be resolved from source (reportMissingModuleSource)

from .types.threads import ThreadArchiveDuration
from .role import Role
Expand Down Expand Up @@ -120,6 +121,44 @@

OverwriteKeyT = TypeVar('OverwriteKeyT', Role, BaseUser, Object, Union[Role, Member, Object])

class _BaseCreateChannelOptions(TypedDict, total=False):
reason: Optional[str]
position: int

class _CreateTextChannelOptions(_BaseCreateChannelOptions, total=False):
topic: str
slowmode_delay: int
nsfw: bool
overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite]
default_auto_archive_duration: int
default_thread_slowmode_delay: int

class _CreateVoiceChannelOptions(_BaseCreateChannelOptions, total=False):
bitrate: int
user_limit: int
rtc_region: Optional[str]
video_quality_mode: VideoQualityMode
overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite]

class _CreateStageChannelOptions(_CreateVoiceChannelOptions, total=False):
bitrate: int
user_limit: int
rtc_region: Optional[str]
video_quality_mode: VideoQualityMode
overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite]

class _CreateForumChannelOptions(_CreateTextChannelOptions, total=False):
topic: str
slowmode_delay: int
nsfw: bool
overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite]
default_auto_archive_duration: int
default_thread_slowmode_delay: int
default_sort_order: ForumOrderType
default_reaction_emoji: EmojiInputType
default_layout: ForumLayoutType
available_tags: Sequence[ForumTag]


class ThreadWithMessage(NamedTuple):
thread: Thread
Expand Down Expand Up @@ -2194,7 +2233,7 @@
r.sort(key=lambda c: (c.position, c.id))
return r

async def create_text_channel(self, name: str, **options: Any) -> TextChannel:
async def create_text_channel(self, name: str, **options: Unpack[_CreateTextChannelOptions]) -> TextChannel:
"""|coro|

A shortcut method to :meth:`Guild.create_text_channel` to create a :class:`TextChannel` in the category.
Expand All @@ -2206,7 +2245,7 @@
"""
return await self.guild.create_text_channel(name, category=self, **options)

async def create_voice_channel(self, name: str, **options: Any) -> VoiceChannel:
async def create_voice_channel(self, name: str, **options: Unpack[_CreateVoiceChannelOptions]) -> VoiceChannel:
"""|coro|

A shortcut method to :meth:`Guild.create_voice_channel` to create a :class:`VoiceChannel` in the category.
Expand All @@ -2218,7 +2257,7 @@
"""
return await self.guild.create_voice_channel(name, category=self, **options)

async def create_stage_channel(self, name: str, **options: Any) -> StageChannel:
async def create_stage_channel(self, name: str, **options: Unpack[_CreateStageChannelOptions]) -> StageChannel:
"""|coro|

A shortcut method to :meth:`Guild.create_stage_channel` to create a :class:`StageChannel` in the category.
Expand All @@ -2232,7 +2271,7 @@
"""
return await self.guild.create_stage_channel(name, category=self, **options)

async def create_forum(self, name: str, **options: Any) -> ForumChannel:
async def create_forum(self, name: str, **options: Unpack[_CreateForumChannelOptions]) -> ForumChannel:
"""|coro|

A shortcut method to :meth:`Guild.create_forum` to create a :class:`ForumChannel` in the category.
Expand Down
27 changes: 25 additions & 2 deletions discord/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
Tuple,
Type,
TypeVar,
TypedDict,
Union,
overload,
)
Expand Down Expand Up @@ -82,7 +83,7 @@
if TYPE_CHECKING:
from types import TracebackType

from typing_extensions import Self
from typing_extensions import Self, Unpack

Check warning on line 86 in discord/client.py

View workflow job for this annotation

GitHub Actions / check 3.x

Import "typing_extensions" could not be resolved from source (reportMissingModuleSource)

from .abc import Messageable, PrivateChannel, Snowflake, SnowflakeTime
from .app_commands import Command, ContextMenu
Expand Down Expand Up @@ -120,6 +121,28 @@
from .audit_logs import AuditLogEntry
from .poll import PollAnswer
from .subscription import Subscription
from .flags import MemberCacheFlags

class _ClientOptions(TypedDict, total=False):
max_messages: int
proxy: str
proxy_auth: aiohttp.BasicAuth
shard_id: int
shard_count: int
application_id: int
member_cache_flags: MemberCacheFlags
chunk_guilds_at_startup: bool
status: Status
activity: BaseActivity
allowed_mentions: AllowedMentions
heartbeat_timeout: float
guild_ready_timeout: float
assume_unsync_clock: bool
enable_debug_events: bool
enable_raw_presences: bool
http_trace: aiohttp.TraceConfig
max_ratelimit_timeout: float
connector: aiohttp.BaseConnector


# fmt: off
Expand Down Expand Up @@ -272,7 +295,7 @@
The websocket gateway the client is currently connected to. Could be ``None``.
"""

def __init__(self, *, intents: Intents, **options: Any) -> None:
def __init__(self, *, intents: Intents, **options: Unpack[_ClientOptions]) -> None:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, the reason why I didn't do this before for Client is because as part of the contract that you can inherit from Client this means that any extra args would cause type errors even though they might use the extra kwargs themselves.

I'm unsure where I really stand on this from a usability POV, and I don't know if it's that annoying.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure what you mean as extra kwargs are ignored anyways and you lose the typed kwargs when subclassing and overriding the __init__ to handle the extra args anyways.

class MyClient(discord.Client):
    ...

# Argument missing for parameter "intents" (expected)
# No parameter named "hello" (expected but ignored anyways)
MyClient(hello="lol")


class ClientWithInit(discord.Client):
    # *args = tuple[Unknown, ...], **kwargs = dict[str, Unknown]
    def __init__(self, *args, **kwargs) -> None:

        # handling extra kwarg
        self.foo = kwargs.pop("foo")

        super().__init__(*args, **kwargs)
        


ClientWithInit(foo="bar") # works fine
Suggested change
def __init__(self, *, intents: Intents, **options: Unpack[_ClientOptions]) -> None:
def __init__(self, *, intents: Intents, **options: Unpack[_ClientOptions]) -> None:

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other issue is that it doesn't allow people who inherit to have the same ability, e.g. ClientWithInit(intents=None) passes despite it not meeting the type signature. I guess it's fine just unfortunate.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, there’s unfortunately no good way to make these public. The user can, if they want, import them in a TYPE_CHECKING block, but that’s probably not good advice to give.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not going to be supported -- and what I'm talking about is an actual supported way of doing this. I guess it has no impact on this PR. The most annoying thing right now is the duplication of all the flags/permissions.

self.loop: asyncio.AbstractEventLoop = _loop
# self.ws is set in the connect method
self.ws: DiscordWebSocket = None # type: ignore
Expand Down
46 changes: 36 additions & 10 deletions discord/ext/commands/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
from .hybrid import hybrid_command, hybrid_group, HybridCommand, HybridGroup

if TYPE_CHECKING:
from typing_extensions import Self
from typing_extensions import Self, Unpack

import importlib.machinery

Expand All @@ -80,12 +80,24 @@
MaybeAwaitableFunc,
)
from .core import Command
from .hybrid import CommandCallback, ContextT, P
from .hybrid import CommandCallback, ContextT, P, _HybridCommandDecoratorKwargs, _HybridGroupDecoratorKwargs
from discord.client import _ClientOptions
from discord.shard import _AutoShardedClientOptions

_Prefix = Union[Iterable[str], str]
_PrefixCallable = MaybeAwaitableFunc[[BotT, Message], _Prefix]
PrefixType = Union[_Prefix, _PrefixCallable[BotT]]

class _BotOptions(_ClientOptions, total=False):
owner_id: int
owner_ids: Collection[int]
strip_after_prefix: bool
case_insensitive: bool

class _AutoShardedBotOptions(_AutoShardedClientOptions, _BotOptions):
...


__all__ = (
'when_mentioned',
'when_mentioned_or',
Expand Down Expand Up @@ -169,7 +181,7 @@ def __init__(
allowed_contexts: app_commands.AppCommandContext = MISSING,
allowed_installs: app_commands.AppInstallationType = MISSING,
intents: discord.Intents,
**options: Any,
**options: Unpack[_BotOptions],
) -> None:
super().__init__(intents=intents, **options)
self.command_prefix: PrefixType[BotT] = command_prefix # type: ignore
Expand Down Expand Up @@ -281,7 +293,7 @@ def hybrid_command(
name: Union[str, app_commands.locale_str] = MISSING,
with_app_command: bool = True,
*args: Any,
**kwargs: Any,
**kwargs: Unpack[_HybridCommandDecoratorKwargs], # type: ignore # name, with_app_command
) -> Callable[[CommandCallback[Any, ContextT, P, T]], HybridCommand[Any, P, T]]:
"""A shortcut decorator that invokes :func:`~discord.ext.commands.hybrid_command` and adds it to
the internal command list via :meth:`add_command`.
Expand All @@ -293,8 +305,8 @@ def hybrid_command(
"""

def decorator(func: CommandCallback[Any, ContextT, P, T]):
kwargs.setdefault('parent', self)
result = hybrid_command(name=name, *args, with_app_command=with_app_command, **kwargs)(func)
kwargs.setdefault('parent', self) # type: ignore # parent is not for the user to set
result = hybrid_command(name=name, *args, with_app_command=with_app_command, **kwargs)(func) # type: ignore # name, with_app_command
self.add_command(result)
return result

Expand All @@ -305,7 +317,7 @@ def hybrid_group(
name: Union[str, app_commands.locale_str] = MISSING,
with_app_command: bool = True,
*args: Any,
**kwargs: Any,
**kwargs: Unpack[_HybridGroupDecoratorKwargs], # type: ignore # name, with_app_command
) -> Callable[[CommandCallback[Any, ContextT, P, T]], HybridGroup[Any, P, T]]:
"""A shortcut decorator that invokes :func:`~discord.ext.commands.hybrid_group` and adds it to
the internal command list via :meth:`add_command`.
Expand All @@ -317,8 +329,8 @@ def hybrid_group(
"""

def decorator(func: CommandCallback[Any, ContextT, P, T]):
kwargs.setdefault('parent', self)
result = hybrid_group(name=name, *args, with_app_command=with_app_command, **kwargs)(func)
kwargs.setdefault('parent', self) # type: ignore # parent is not for the user to set
result = hybrid_group(name=name, *args, with_app_command=with_app_command, **kwargs)(func) # type: ignore # name, with_app_command
self.add_command(result)
return result

Expand Down Expand Up @@ -1527,4 +1539,18 @@ class AutoShardedBot(BotBase, discord.AutoShardedClient):
.. versionadded:: 2.0
"""

pass
if TYPE_CHECKING:

def __init__(
self,
command_prefix: PrefixType[BotT],
*,
help_command: Optional[HelpCommand] = _default,
tree_cls: Type[app_commands.CommandTree[Any]] = app_commands.CommandTree,
description: Optional[str] = None,
allowed_contexts: app_commands.AppCommandContext = MISSING,
allowed_installs: app_commands.AppInstallationType = MISSING,
intents: discord.Intents,
**kwargs: Unpack[_AutoShardedBotOptions],
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same nit as before.

) -> None:
...
18 changes: 15 additions & 3 deletions discord/ext/commands/cog.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,30 @@
Tuple,
TypeVar,
Union,
TypedDict,
)

from ._types import _BaseCommand, BotT

if TYPE_CHECKING:
from typing_extensions import Self
from typing_extensions import Self, Unpack
from discord.abc import Snowflake
from discord._types import ClientT

from .bot import BotBase
from .context import Context
from .core import Command
from .core import Command, _CommandDecoratorKwargs

class _CogKwargs(TypedDict, total=False):
name: str
group_name: Union[str, app_commands.locale_str]
description: str
group_description: Union[str, app_commands.locale_str]
group_nsfw: bool
group_auto_locale_strings: bool
group_extras: Dict[Any, Any]
command_attrs: _CommandDecoratorKwargs


__all__ = (
'CogMeta',
Expand Down Expand Up @@ -169,7 +181,7 @@ async def bar(self, ctx):
__cog_app_commands__: List[Union[app_commands.Group, app_commands.Command[Any, ..., Any]]]
__cog_listeners__: List[Tuple[str, str]]

def __new__(cls, *args: Any, **kwargs: Any) -> CogMeta:
def __new__(cls, *args: Any, **kwargs: Unpack[_CogKwargs]) -> CogMeta:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this even work from a user's POV?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately not. It does error out when the user has typed the correct name but the wrong type.

name, bases, attrs = args
if any(issubclass(base, app_commands.Group) for base in bases):
raise TypeError(
Expand Down
Loading
Loading