diff --git a/discord/abc.py b/discord/abc.py index 692472f8f..b38f6ffac 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -73,7 +73,7 @@ __all__ = ( T = TypeVar('T', bound=VoiceProtocol) if TYPE_CHECKING: - from typing_extensions import Self + from typing_extensions import Self, Unpack from .client import Client from .user import ClientUser @@ -108,6 +108,7 @@ if TYPE_CHECKING: from .types.snowflake import ( SnowflakeList, ) + from .permissions import _PermissionOverwriteKwargs PartialMessageableChannel = Union[TextChannel, VoiceChannel, StageChannel, Thread, DMChannel, PartialMessageable] MessageableChannel = Union[PartialMessageableChannel, GroupChannel] @@ -886,7 +887,7 @@ class GuildChannel: target: Union[Member, Role], *, reason: Optional[str] = ..., - **permissions: Optional[bool], + **permissions: Unpack[_PermissionOverwriteKwargs], ) -> None: ... @@ -896,7 +897,7 @@ class GuildChannel: *, overwrite: Any = _undefined, reason: Optional[str] = None, - **permissions: Optional[bool], + **permissions: Unpack[_PermissionOverwriteKwargs], ) -> None: r"""|coro| diff --git a/discord/app_commands/checks.py b/discord/app_commands/checks.py index 5c17b951c..3fbd677c3 100644 --- a/discord/app_commands/checks.py +++ b/discord/app_commands/checks.py @@ -55,8 +55,9 @@ from ..utils import get as utils_get, MISSING, maybe_coroutine 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]], @@ -286,7 +287,7 @@ def has_any_role(*items: Union[int, str]) -> Callable[[T], T]: 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. @@ -341,7 +342,7 @@ def has_permissions(**perms: bool) -> Callable[[T], T]: 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`. diff --git a/discord/app_commands/commands.py b/discord/app_commands/commands.py index d5b8d93b2..41cfd4172 100644 --- a/discord/app_commands/commands.py +++ b/discord/app_commands/commands.py @@ -61,7 +61,7 @@ from ..permissions import Permissions 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 @@ -73,6 +73,7 @@ if TYPE_CHECKING: # 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]] @@ -2821,7 +2822,7 @@ def allowed_installs( 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. diff --git a/discord/channel.py b/discord/channel.py index a306707d6..8f6f9546b 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -39,6 +39,7 @@ from typing import ( Sequence, Tuple, TypeVar, + TypedDict, Union, overload, ) @@ -85,7 +86,7 @@ __all__ = ( ) if TYPE_CHECKING: - from typing_extensions import Self + from typing_extensions import Self, Unpack from .types.threads import ThreadArchiveDuration from .role import Role @@ -120,6 +121,50 @@ if TYPE_CHECKING: OverwriteKeyT = TypeVar('OverwriteKeyT', Role, BaseUser, Object, Union[Role, Member, Object]) + class _CreateChannelWithCategory(TypedDict, total=False): + category: Optional[CategoryChannel] + + class _CreateNewsChannel(TypedDict, total=False): + news: bool + + 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 @@ -2194,7 +2239,7 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable): 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. @@ -2206,7 +2251,7 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable): """ 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. @@ -2218,7 +2263,7 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable): """ 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. @@ -2232,7 +2277,7 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable): """ 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. diff --git a/discord/client.py b/discord/client.py index b997bd96f..67e42410b 100644 --- a/discord/client.py +++ b/discord/client.py @@ -42,6 +42,7 @@ from typing import ( Tuple, Type, TypeVar, + TypedDict, Union, overload, ) @@ -82,7 +83,7 @@ from .soundboard import SoundboardDefaultSound, SoundboardSound if TYPE_CHECKING: from types import TracebackType - from typing_extensions import Self + from typing_extensions import Self, Unpack from .abc import Messageable, PrivateChannel, Snowflake, SnowflakeTime from .app_commands import Command, ContextMenu @@ -120,6 +121,28 @@ if TYPE_CHECKING: 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 @@ -272,7 +295,7 @@ class Client: 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: self.loop: asyncio.AbstractEventLoop = _loop # self.ws is set in the connect method self.ws: DiscordWebSocket = None # type: ignore diff --git a/discord/ext/commands/bot.py b/discord/ext/commands/bot.py index 8ce872f1a..29b1f045e 100644 --- a/discord/ext/commands/bot.py +++ b/discord/ext/commands/bot.py @@ -64,7 +64,7 @@ from .cog import Cog 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 @@ -80,12 +80,24 @@ if TYPE_CHECKING: 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', @@ -169,7 +181,7 @@ class BotBase(GroupMixin[None]): 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 @@ -281,7 +293,7 @@ class BotBase(GroupMixin[None]): 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`. @@ -293,8 +305,8 @@ class BotBase(GroupMixin[None]): """ 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 @@ -305,7 +317,7 @@ class BotBase(GroupMixin[None]): 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`. @@ -317,8 +329,8 @@ class BotBase(GroupMixin[None]): """ 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 @@ -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], + ) -> None: + ... diff --git a/discord/ext/commands/cog.py b/discord/ext/commands/cog.py index 659d69ebb..1b8a24e66 100644 --- a/discord/ext/commands/cog.py +++ b/discord/ext/commands/cog.py @@ -44,18 +44,30 @@ from typing import ( 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', @@ -169,7 +181,7 @@ class CogMeta(type): __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: name, bases, attrs = args if any(issubclass(base, app_commands.Group) for base in bases): raise TypeError( diff --git a/discord/ext/commands/core.py b/discord/ext/commands/core.py index 372fcbedf..9107a3d22 100644 --- a/discord/ext/commands/core.py +++ b/discord/ext/commands/core.py @@ -43,6 +43,7 @@ from typing import ( TypeVar, Union, overload, + TypedDict, ) import re @@ -58,10 +59,39 @@ from .parameters import Parameter, Signature from discord.app_commands.commands import NUMPY_DOCSTRING_ARG_REGEX if TYPE_CHECKING: - from typing_extensions import Concatenate, ParamSpec, Self + from typing_extensions import Concatenate, ParamSpec, Self, Unpack from ._types import BotT, Check, ContextT, Coro, CoroFunc, Error, Hook, UserCheck + from discord.permissions import _PermissionsKwargs + + class _CommandDecoratorKwargs(TypedDict, total=False): + enabled: bool + help: str + brief: str + usage: str + rest_is_raw: bool + aliases: List[str] + description: str + hidden: bool + checks: List[UserCheck[Context[Any]]] + cooldown: CooldownMapping[Context[Any]] + max_concurrency: MaxConcurrency + require_var_positional: bool + cooldown_after_parsing: bool + ignore_extra: bool + extras: Dict[Any, Any] + + class _CommandKwargs(_CommandDecoratorKwargs, total=False): + name: str + + class _GroupDecoratorKwargs(_CommandDecoratorKwargs, total=False): + invoke_without_command: bool + case_insensitive: bool + + class _GroupKwargs(_GroupDecoratorKwargs, total=False): + name: str + __all__ = ( 'Command', @@ -368,9 +398,9 @@ class Command(_BaseCommand, Generic[CogT, P, T]): .. versionadded:: 2.0 """ - __original_kwargs__: Dict[str, Any] + __original_kwargs__: _CommandKwargs - def __new__(cls, *args: Any, **kwargs: Any) -> Self: + def __new__(cls, *args: Any, **kwargs: Unpack[_CommandKwargs]) -> Self: # if you're wondering why this is done, it's because we need to ensure # we have a complete original copy of **kwargs even for classes that # mess with it by popping before delegating to the subclass __init__. @@ -393,7 +423,7 @@ class Command(_BaseCommand, Generic[CogT, P, T]): Callable[Concatenate[Context[Any], P], Coro[T]], ], /, - **kwargs: Any, + **kwargs: Unpack[_CommandKwargs], ) -> None: if not asyncio.iscoroutinefunction(func): raise TypeError('Callback must be a coroutine.') @@ -556,7 +586,7 @@ class Command(_BaseCommand, Generic[CogT, P, T]): except ValueError: pass - def update(self, **kwargs: Any) -> None: + def update(self, **kwargs: Unpack[_CommandKwargs]) -> None: """Updates :class:`Command` instance with updated attribute. This works similarly to the :func:`~discord.ext.commands.command` decorator in terms @@ -564,7 +594,7 @@ class Command(_BaseCommand, Generic[CogT, P, T]): subclass constructors, sans the name and callback. """ cog = self.cog - self.__init__(self.callback, **dict(self.__original_kwargs__, **kwargs)) + self.__init__(self.callback, **dict(self.__original_kwargs__, **kwargs)) # type: ignore # it's a typeddict self.cog = cog async def __call__(self, context: Context[BotT], /, *args: P.args, **kwargs: P.kwargs) -> T: @@ -1468,7 +1498,7 @@ class GroupMixin(Generic[CogT]): self: GroupMixin[CogT], name: str = ..., *args: Any, - **kwargs: Any, + **kwargs: Unpack[_CommandDecoratorKwargs], ) -> Callable[ [ Union[ @@ -1486,7 +1516,7 @@ class GroupMixin(Generic[CogT]): name: str = ..., cls: Type[CommandT] = ..., # type: ignore # previous overload handles case where cls is not set *args: Any, - **kwargs: Any, + **kwargs: Unpack[_CommandDecoratorKwargs], ) -> Callable[ [ Union[ @@ -1503,7 +1533,7 @@ class GroupMixin(Generic[CogT]): name: str = MISSING, cls: Type[Command[Any, ..., Any]] = MISSING, *args: Any, - **kwargs: Any, + **kwargs: Unpack[_CommandDecoratorKwargs], ) -> Any: """A shortcut decorator that invokes :func:`~discord.ext.commands.command` and adds it to the internal command list via :meth:`~.GroupMixin.add_command`. @@ -1515,8 +1545,7 @@ class GroupMixin(Generic[CogT]): """ def decorator(func): - - kwargs.setdefault('parent', self) + kwargs.setdefault('parent', self) # type: ignore # the parent kwarg is not for users to set. result = command(name=name, cls=cls, *args, **kwargs)(func) self.add_command(result) return result @@ -1528,7 +1557,7 @@ class GroupMixin(Generic[CogT]): self: GroupMixin[CogT], name: str = ..., *args: Any, - **kwargs: Any, + **kwargs: Unpack[_GroupDecoratorKwargs], ) -> Callable[ [ Union[ @@ -1546,7 +1575,7 @@ class GroupMixin(Generic[CogT]): name: str = ..., cls: Type[GroupT] = ..., # type: ignore # previous overload handles case where cls is not set *args: Any, - **kwargs: Any, + **kwargs: Unpack[_GroupDecoratorKwargs], ) -> Callable[ [ Union[ @@ -1563,7 +1592,7 @@ class GroupMixin(Generic[CogT]): name: str = MISSING, cls: Type[Group[Any, ..., Any]] = MISSING, *args: Any, - **kwargs: Any, + **kwargs: Unpack[_GroupDecoratorKwargs], ) -> Any: """A shortcut decorator that invokes :func:`.group` and adds it to the internal command list via :meth:`~.GroupMixin.add_command`. @@ -1575,7 +1604,7 @@ class GroupMixin(Generic[CogT]): """ def decorator(func): - kwargs.setdefault('parent', self) + kwargs.setdefault('parent', self) # type: ignore # the parent kwarg is not for users to set. result = group(name=name, cls=cls, *args, **kwargs)(func) self.add_command(result) return result @@ -1606,7 +1635,7 @@ class Group(GroupMixin[CogT], Command[CogT, P, T]): Defaults to ``False``. """ - def __init__(self, *args: Any, **attrs: Any) -> None: + def __init__(self, *args: Any, **attrs: Unpack[_GroupKwargs]) -> None: self.invoke_without_command: bool = attrs.pop('invoke_without_command', False) super().__init__(*args, **attrs) @@ -1728,7 +1757,7 @@ if TYPE_CHECKING: @overload def command( name: str = ..., - **attrs: Any, + **attrs: Unpack[_CommandDecoratorKwargs], ) -> _CommandDecorator: ... @@ -1737,7 +1766,7 @@ def command( def command( name: str = ..., cls: Type[CommandT] = ..., # type: ignore # previous overload handles case where cls is not set - **attrs: Any, + **attrs: Unpack[_CommandDecoratorKwargs], ) -> Callable[ [ Union[ @@ -1753,7 +1782,7 @@ def command( def command( name: str = MISSING, cls: Type[Command[Any, ..., Any]] = MISSING, - **attrs: Any, + **attrs: Unpack[_CommandDecoratorKwargs], ) -> Any: """A decorator that transforms a function into a :class:`.Command` or if called with :func:`.group`, :class:`.Group`. @@ -1798,7 +1827,7 @@ def command( @overload def group( name: str = ..., - **attrs: Any, + **attrs: Unpack[_GroupDecoratorKwargs], ) -> _GroupDecorator: ... @@ -1807,7 +1836,7 @@ def group( def group( name: str = ..., cls: Type[GroupT] = ..., # type: ignore # previous overload handles case where cls is not set - **attrs: Any, + **attrs: Unpack[_GroupDecoratorKwargs], ) -> Callable[ [ Union[ @@ -1823,7 +1852,7 @@ def group( def group( name: str = MISSING, cls: Type[Group[Any, ..., Any]] = MISSING, - **attrs: Any, + **attrs: Unpack[_GroupDecoratorKwargs], ) -> Any: """A decorator that transforms a function into a :class:`.Group`. @@ -2165,7 +2194,7 @@ def bot_has_any_role(*items: int) -> Callable[[T], T]: return check(predicate) -def has_permissions(**perms: bool) -> Check[Any]: +def has_permissions(**perms: Unpack[_PermissionsKwargs]) -> Check[Any]: """A :func:`.check` that is added that checks if the member has all of the permissions necessary. @@ -2212,7 +2241,7 @@ def has_permissions(**perms: bool) -> Check[Any]: return check(predicate) -def bot_has_permissions(**perms: bool) -> Check[Any]: +def bot_has_permissions(**perms: Unpack[_PermissionsKwargs]) -> Check[Any]: """Similar to :func:`.has_permissions` except checks if the bot itself has the permissions listed. @@ -2237,7 +2266,7 @@ def bot_has_permissions(**perms: bool) -> Check[Any]: return check(predicate) -def has_guild_permissions(**perms: bool) -> Check[Any]: +def has_guild_permissions(**perms: Unpack[_PermissionsKwargs]) -> Check[Any]: """Similar to :func:`.has_permissions`, but operates on guild wide permissions instead of the current channel permissions. @@ -2266,7 +2295,7 @@ def has_guild_permissions(**perms: bool) -> Check[Any]: return check(predicate) -def bot_has_guild_permissions(**perms: bool) -> Check[Any]: +def bot_has_guild_permissions(**perms: Unpack[_PermissionsKwargs]) -> Check[Any]: """Similar to :func:`.has_guild_permissions`, but checks the bot members guild permissions. diff --git a/discord/ext/commands/help.py b/discord/ext/commands/help.py index d06fbd8bf..90b44d16a 100644 --- a/discord/ext/commands/help.py +++ b/discord/ext/commands/help.py @@ -42,6 +42,7 @@ from typing import ( Iterable, Sequence, Mapping, + TypedDict, ) import discord.utils @@ -50,7 +51,7 @@ from .core import Group, Command, get_signature_parameters from .errors import CommandError if TYPE_CHECKING: - from typing_extensions import Self + from typing_extensions import Self, Unpack import discord.abc @@ -58,6 +59,7 @@ if TYPE_CHECKING: from .context import Context from .cog import Cog from .parameters import Parameter + from .core import _CommandKwargs from ._types import ( UserCheck, @@ -65,6 +67,30 @@ if TYPE_CHECKING: _Bot, ) + class _HelpCommandOptions(TypedDict, total=False): + show_hidden: bool + verify_checks: bool + command_attrs: _CommandKwargs + + class _BaseHelpCommandOptions(_HelpCommandOptions, total=False): + sort_commands: bool + dm_help: bool + dm_help_threshold: int + no_category: str + paginator: Paginator + commands_heading: str + + class _DefaultHelpCommandOptions(_BaseHelpCommandOptions, total=False): + width: int + indent: int + arguments_heading: str + default_argument_description: str + show_parameter_descriptions: bool + + class _MinimalHelpCommandOptions(_BaseHelpCommandOptions, total=False): + aliases_heading: str + + __all__ = ( 'Paginator', 'HelpCommand', @@ -224,7 +250,7 @@ def _not_overridden(f: FuncT) -> FuncT: class _HelpCommandImpl(Command): - def __init__(self, inject: HelpCommand, *args: Any, **kwargs: Any) -> None: + def __init__(self, inject: HelpCommand, *args: Any, **kwargs: Unpack[_CommandKwargs]) -> None: super().__init__(inject.command_callback, *args, **kwargs) self._original: HelpCommand = inject self._injected: HelpCommand = inject @@ -299,7 +325,7 @@ class _HelpCommandImpl(Command): def update(self, **kwargs: Any) -> None: cog = self.cog - self.__init__(self._original, **dict(self.__original_kwargs__, **kwargs)) + self.__init__(self._original, **dict(self.__original_kwargs__, **kwargs)) # type: ignore self.cog = cog @@ -366,10 +392,9 @@ class HelpCommand: self.__original_args__ = deepcopy(args) return self - def __init__(self, **options: Any) -> None: + def __init__(self, **options: Unpack[_HelpCommandOptions]) -> None: self.show_hidden: bool = options.pop('show_hidden', False) self.verify_checks: bool = options.pop('verify_checks', True) - self.command_attrs: Dict[str, Any] self.command_attrs = attrs = options.pop('command_attrs', {}) attrs.setdefault('name', 'help') attrs.setdefault('help', 'Shows this message') @@ -1041,7 +1066,7 @@ class DefaultHelpCommand(HelpCommand): The paginator used to paginate the help command output. """ - def __init__(self, **options: Any) -> None: + def __init__(self, **options: Unpack[_DefaultHelpCommandOptions]) -> None: self.width: int = options.pop('width', 80) self.indent: int = options.pop('indent', 2) self.sort_commands: bool = options.pop('sort_commands', True) @@ -1051,11 +1076,13 @@ class DefaultHelpCommand(HelpCommand): self.commands_heading: str = options.pop('commands_heading', 'Commands:') self.default_argument_description: str = options.pop('default_argument_description', 'No description given') self.no_category: str = options.pop('no_category', 'No Category') - self.paginator: Paginator = options.pop('paginator', None) self.show_parameter_descriptions: bool = options.pop('show_parameter_descriptions', True) - if self.paginator is None: + paginator = options.pop('paginator', None) + if paginator is None: self.paginator: Paginator = Paginator() + else: + self.paginator: Paginator = paginator super().__init__(**options) @@ -1334,17 +1361,19 @@ class MinimalHelpCommand(HelpCommand): The paginator used to paginate the help command output. """ - def __init__(self, **options: Any) -> None: + def __init__(self, **options: Unpack[_MinimalHelpCommandOptions]) -> None: self.sort_commands: bool = options.pop('sort_commands', True) self.commands_heading: str = options.pop('commands_heading', 'Commands') self.dm_help: bool = options.pop('dm_help', False) self.dm_help_threshold: int = options.pop('dm_help_threshold', 1000) self.aliases_heading: str = options.pop('aliases_heading', 'Aliases:') self.no_category: str = options.pop('no_category', 'No Category') - self.paginator: Paginator = options.pop('paginator', None) - if self.paginator is None: + paginator = options.pop('paginator', None) + if paginator is None: self.paginator: Paginator = Paginator(suffix=None, prefix=None) + else: + self.paginator: Paginator = paginator super().__init__(**options) diff --git a/discord/ext/commands/hybrid.py b/discord/ext/commands/hybrid.py index 0857003fa..78e2f43bb 100644 --- a/discord/ext/commands/hybrid.py +++ b/discord/ext/commands/hybrid.py @@ -24,19 +24,7 @@ DEALINGS IN THE SOFTWARE. from __future__ import annotations -from typing import ( - TYPE_CHECKING, - Any, - Callable, - ClassVar, - Dict, - List, - Tuple, - Type, - TypeVar, - Union, - Optional, -) +from typing import TYPE_CHECKING, Any, Callable, ClassVar, Dict, List, Tuple, Type, TypeVar, Union, Optional import discord import inspect @@ -51,7 +39,7 @@ from .cog import Cog from .view import StringView if TYPE_CHECKING: - from typing_extensions import Self, ParamSpec, Concatenate + from typing_extensions import Self, ParamSpec, Concatenate, Unpack from ._types import ContextT, Coro, BotT from .bot import Bot from .context import Context @@ -60,6 +48,29 @@ if TYPE_CHECKING: AutocompleteCallback, ChoiceT, ) + from .core import _CommandKwargs + + class _HybridCommandKwargs(_CommandKwargs, total=False): + guild_ids: list[int] + guild_only: bool + default_permissions: bool + nsfw: bool + with_app_command: bool + + class _HybridCommandDecoratorKwargs(_HybridCommandKwargs, total=False): + description: Union[str, app_commands.locale_str] + + class _HybridGroupKwargs(_HybridCommandDecoratorKwargs, total=False): + with_app_command: bool + guild_ids: list[int] + guild_only: bool + default_permissions: bool + nsfw: bool + description: str + + class _HybridGroupDecoratorKwargs(_HybridGroupKwargs, total=False): + description: Union[str, app_commands.locale_str] + fallback: Union[str, app_commands.locale_str] __all__ = ( @@ -501,7 +512,7 @@ class HybridCommand(Command[CogT, P, T]): *, name: Union[str, app_commands.locale_str] = MISSING, description: Union[str, app_commands.locale_str] = MISSING, - **kwargs: Any, + **kwargs: Unpack[_HybridCommandKwargs], # type: ignore # name, description ) -> None: name, name_locale = (name.message, name) if isinstance(name, app_commands.locale_str) else (name, None) if name is not MISSING: @@ -621,7 +632,7 @@ class HybridGroup(Group[CogT, P, T]): name: Union[str, app_commands.locale_str] = MISSING, description: Union[str, app_commands.locale_str] = MISSING, fallback: Optional[Union[str, app_commands.locale_str]] = None, - **attrs: Any, + **attrs: Unpack[_HybridGroupKwargs], # type: ignore # name, description ) -> None: name, name_locale = (name.message, name) if isinstance(name, app_commands.locale_str) else (name, None) if name is not MISSING: @@ -825,7 +836,7 @@ class HybridGroup(Group[CogT, P, T]): name: Union[str, app_commands.locale_str] = MISSING, *args: Any, with_app_command: bool = True, - **kwargs: Any, + **kwargs: Unpack[_HybridCommandDecoratorKwargs], # type: ignore # name, with_app_command ) -> Callable[[CommandCallback[CogT, ContextT, P2, U]], HybridCommand[CogT, P2, U]]: """A shortcut decorator that invokes :func:`~discord.ext.commands.hybrid_command` and adds it to the internal command list via :meth:`add_command`. @@ -837,8 +848,8 @@ class HybridGroup(Group[CogT, P, T]): """ def decorator(func: CommandCallback[CogT, ContextT, P2, U]): - 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 users 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 @@ -849,7 +860,7 @@ class HybridGroup(Group[CogT, P, T]): name: Union[str, app_commands.locale_str] = MISSING, *args: Any, with_app_command: bool = True, - **kwargs: Any, + **kwargs: Unpack[_HybridGroupDecoratorKwargs], # type: ignore # name, with_app_command ) -> Callable[[CommandCallback[CogT, ContextT, P2, U]], HybridGroup[CogT, P2, U]]: """A shortcut decorator that invokes :func:`~discord.ext.commands.hybrid_group` and adds it to the internal command list via :meth:`~.GroupMixin.add_command`. @@ -861,8 +872,8 @@ class HybridGroup(Group[CogT, P, T]): """ def decorator(func: CommandCallback[CogT, ContextT, P2, U]): - 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 users 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 @@ -873,7 +884,7 @@ def hybrid_command( name: Union[str, app_commands.locale_str] = MISSING, *, with_app_command: bool = True, - **attrs: Any, + **attrs: Unpack[_HybridCommandDecoratorKwargs], # type: ignore # name, with_app_command ) -> Callable[[CommandCallback[CogT, ContextT, P, T]], HybridCommand[CogT, P, T]]: r"""A decorator that transforms a function into a :class:`.HybridCommand`. @@ -916,7 +927,7 @@ def hybrid_command( if isinstance(func, Command): raise TypeError('Callback is already a command.') # Pyright does not allow Command[Any] to be assigned to Command[CogT] despite it being okay here - return HybridCommand(func, name=name, with_app_command=with_app_command, **attrs) # type: ignore + return HybridCommand(func, name=name, with_app_command=with_app_command, **attrs) # type: ignore # name, with_app_command return decorator @@ -925,7 +936,7 @@ def hybrid_group( name: Union[str, app_commands.locale_str] = MISSING, *, with_app_command: bool = True, - **attrs: Any, + **attrs: Unpack[_HybridGroupDecoratorKwargs], # type: ignore # name, with_app_command ) -> Callable[[CommandCallback[CogT, ContextT, P, T]], HybridGroup[CogT, P, T]]: """A decorator that transforms a function into a :class:`.HybridGroup`. @@ -949,6 +960,6 @@ def hybrid_group( def decorator(func: CommandCallback[CogT, ContextT, P, T]) -> HybridGroup[CogT, P, T]: if isinstance(func, Command): raise TypeError('Callback is already a command.') - return HybridGroup(func, name=name, with_app_command=with_app_command, **attrs) + return HybridGroup(func, name=name, with_app_command=with_app_command, **attrs) # type: ignore # name, with_app_command return decorator diff --git a/discord/flags.py b/discord/flags.py index 20f8c5470..82a430482 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -40,12 +40,48 @@ from typing import ( Type, TypeVar, overload, + TypedDict, ) from .enums import UserFlags if TYPE_CHECKING: - from typing_extensions import Self + from typing_extensions import Self, Unpack + + class _IntentsFlagsKwargs(TypedDict, total=False): + guilds: bool + members: bool + moderation: bool + bans: bool + emojis: bool + emojis_and_stickers: bool + expressions: bool + integrations: bool + webhooks: bool + invites: bool + voice_states: bool + presences: bool + messages: bool + guild_messages: bool + dm_messages: bool + reactions: bool + guild_reactions: bool + dm_reactions: bool + typing: bool + guild_typing: bool + dm_typing: bool + message_content: bool + guild_scheduled_events: bool + auto_moderation: bool + auto_moderation_configuration: bool + auto_moderation_execution: bool + polls: bool + guild_polls: bool + dm_polls: bool + + class _MemberCacheFlagsKwargs(TypedDict, total=False): + voice: bool + joined: bool __all__ = ( @@ -754,12 +790,12 @@ class Intents(BaseFlags): __slots__ = () - def __init__(self, value: int = 0, **kwargs: bool) -> None: + def __init__(self, value: int = 0, **kwargs: Unpack[_IntentsFlagsKwargs]) -> None: self.value: int = value - for key, value in kwargs.items(): + for key, kwvalue in kwargs.items(): if key not in self.VALID_FLAGS: raise TypeError(f'{key!r} is not a valid flag name.') - setattr(self, key, value) + setattr(self, key, kwvalue) @classmethod def all(cls: Type[Intents]) -> Intents: @@ -1415,7 +1451,7 @@ class MemberCacheFlags(BaseFlags): __slots__ = () - def __init__(self, **kwargs: bool): + def __init__(self, **kwargs: Unpack[_MemberCacheFlagsKwargs]) -> None: bits = max(self.VALID_FLAGS.values()).bit_length() self.value: int = (1 << bits) - 1 for key, value in kwargs.items(): diff --git a/discord/permissions.py b/discord/permissions.py index b553e2578..6a28c6821 100644 --- a/discord/permissions.py +++ b/discord/permissions.py @@ -24,7 +24,7 @@ DEALINGS IN THE SOFTWARE. from __future__ import annotations -from typing import Callable, Any, ClassVar, Dict, Iterator, Set, TYPE_CHECKING, Tuple, Optional +from typing import Callable, Any, ClassVar, Dict, Iterator, Set, TYPE_CHECKING, Tuple, Optional, TypedDict, Generic, TypeVar from .flags import BaseFlags, flag_value, fill_with_flags, alias_flag_value __all__ = ( @@ -33,7 +33,73 @@ __all__ = ( ) if TYPE_CHECKING: - from typing_extensions import Self + from typing_extensions import Self, Unpack + + BoolOrNoneT = TypeVar('BoolOrNoneT', bound=Optional[bool]) + + class _BasePermissionsKwargs(Generic[BoolOrNoneT], TypedDict, total=False): + create_instant_invite: BoolOrNoneT + kick_members: BoolOrNoneT + ban_members: BoolOrNoneT + administrator: BoolOrNoneT + manage_channels: BoolOrNoneT + manage_guild: BoolOrNoneT + add_reactions: BoolOrNoneT + view_audit_log: BoolOrNoneT + priority_speaker: BoolOrNoneT + stream: BoolOrNoneT + read_messages: BoolOrNoneT + view_channel: BoolOrNoneT + send_messages: BoolOrNoneT + send_tts_messages: BoolOrNoneT + manage_messages: BoolOrNoneT + embed_links: BoolOrNoneT + attach_files: BoolOrNoneT + read_message_history: BoolOrNoneT + mention_everyone: BoolOrNoneT + external_emojis: BoolOrNoneT + use_external_emojis: BoolOrNoneT + view_guild_insights: BoolOrNoneT + connect: BoolOrNoneT + speak: BoolOrNoneT + mute_members: BoolOrNoneT + deafen_members: BoolOrNoneT + move_members: BoolOrNoneT + use_voice_activation: BoolOrNoneT + change_nickname: BoolOrNoneT + manage_nicknames: BoolOrNoneT + manage_roles: BoolOrNoneT + manage_permissions: BoolOrNoneT + manage_webhooks: BoolOrNoneT + manage_expressions: BoolOrNoneT + manage_emojis: BoolOrNoneT + manage_emojis_and_stickers: BoolOrNoneT + use_application_commands: BoolOrNoneT + request_to_speak: BoolOrNoneT + manage_events: BoolOrNoneT + manage_threads: BoolOrNoneT + create_public_threads: BoolOrNoneT + create_private_threads: BoolOrNoneT + send_messages_in_threads: BoolOrNoneT + external_stickers: BoolOrNoneT + use_external_stickers: BoolOrNoneT + use_embedded_activities: BoolOrNoneT + moderate_members: BoolOrNoneT + use_soundboard: BoolOrNoneT + use_external_sounds: BoolOrNoneT + send_voice_messages: BoolOrNoneT + create_expressions: BoolOrNoneT + create_events: BoolOrNoneT + send_polls: BoolOrNoneT + create_polls: BoolOrNoneT + use_external_apps: BoolOrNoneT + + class _PermissionsKwargs(_BasePermissionsKwargs[bool]): + ... + + class _PermissionOverwriteKwargs(_BasePermissionsKwargs[Optional[bool]]): + ... + # A permission alias works like a regular flag but is marked # So the PermissionOverwrite knows to work with it @@ -135,18 +201,18 @@ class Permissions(BaseFlags): __slots__ = () - def __init__(self, permissions: int = 0, **kwargs: bool): + def __init__(self, permissions: int = 0, **kwargs: Unpack[_PermissionsKwargs]): if not isinstance(permissions, int): raise TypeError(f'Expected int parameter, received {permissions.__class__.__name__} instead.') self.value = permissions - for key, value in kwargs.items(): + for key, kwvalue in kwargs.items(): try: flag = self.VALID_FLAGS[key] except KeyError: raise TypeError(f'{key!r} is not a valid permission name.') from None else: - self._set_flag(flag, value) + self._set_flag(flag, kwvalue) # type: ignore def is_subset(self, other: Permissions) -> bool: """Returns ``True`` if self has the same or fewer permissions as other.""" @@ -381,7 +447,7 @@ class Permissions(BaseFlags): """ return cls(1 << 3) - def update(self, **kwargs: bool) -> None: + def update(self, **kwargs: Unpack[_PermissionsKwargs]) -> None: r"""Bulk updates this permission object. Allows you to set multiple attributes by using keyword @@ -396,7 +462,7 @@ class Permissions(BaseFlags): for key, value in kwargs.items(): flag = self.VALID_FLAGS.get(key) if flag is not None: - self._set_flag(flag, value) + self._set_flag(flag, value) # type: ignore def handle_overwrite(self, allow: int, deny: int) -> None: # Basically this is what's happening here. @@ -908,7 +974,7 @@ class PermissionOverwrite: create_polls: Optional[bool] use_external_apps: Optional[bool] - def __init__(self, **kwargs: Optional[bool]): + def __init__(self, **kwargs: Unpack[_PermissionOverwriteKwargs]) -> None: self._values: Dict[str, Optional[bool]] = {} for key, value in kwargs.items(): @@ -970,7 +1036,7 @@ class PermissionOverwrite: """ return len(self._values) == 0 - def update(self, **kwargs: Optional[bool]) -> None: + def update(self, **kwargs: Unpack[_PermissionOverwriteKwargs]) -> None: r"""Bulk updates this permission overwrite object. Allows you to set multiple attributes by using keyword diff --git a/discord/shard.py b/discord/shard.py index 454fd5e28..3725324d4 100644 --- a/discord/shard.py +++ b/discord/shard.py @@ -52,6 +52,12 @@ if TYPE_CHECKING: from .activity import BaseActivity from .flags import Intents from .types.gateway import SessionStartLimit + from .client import _ClientOptions + + class _AutoShardedClientOptions(_ClientOptions, total=False): + shard_ids: List[int] + shard_connect_timeout: Optional[float] + __all__ = ( 'AutoShardedClient', @@ -365,7 +371,7 @@ class AutoShardedClient(Client): if TYPE_CHECKING: _connection: AutoShardedConnectionState - def __init__(self, *args: Any, intents: Intents, **kwargs: Any) -> None: + def __init__(self, *args: Any, intents: Intents, **kwargs: Unpack[_AutoShardedClientOptions]) -> None: kwargs.pop('shard_id', None) self.shard_ids: Optional[List[int]] = kwargs.pop('shard_ids', None) self.shard_connect_timeout: Optional[float] = kwargs.pop('shard_connect_timeout', 180.0)