diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index ed9651b025..84f6a9bf34 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "3.11" cache: "pip" @@ -26,7 +26,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "3.11" cache: "pip" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 65da74cfb0..4fbb2b5b8a 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -45,7 +45,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -56,7 +56,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -70,4 +70,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 35cb6d490d..44e5c35799 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "3.11" cache: "pip" @@ -32,7 +32,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "3.11" cache: "pip" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9a2b03aa55..effb760e9f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,7 +22,7 @@ jobs: PYTHON: ${{ matrix.python-version }} steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: "pip" @@ -50,7 +50,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.8" cache: "pip" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ed38b0797c..f35687365b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ ci: repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -19,16 +19,16 @@ repos: # - --remove-duplicate-keys # - --remove-unused-variables - repo: https://github.com/asottile/pyupgrade - rev: v3.13.0 + rev: v3.15.0 hooks: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/PyCQA/isort - rev: 5.12.0 + rev: 5.13.0 hooks: - id: isort - repo: https://github.com/psf/black - rev: 23.9.1 + rev: 23.11.0 hooks: - id: black args: [--safe, --quiet] @@ -77,7 +77,7 @@ repos: # - id: mypy - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.3 + rev: v4.0.0-alpha.4 hooks: - id: prettier args: [--prose-wrap=always, --print-width=88] diff --git a/CHANGELOG.md b/CHANGELOG.md index 41f9e96c11..6928a6bac4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,13 @@ These changes are available on the `master` branch, but have not yet been releas ([#2206](https://github.com/Pycord-Development/pycord/pull/2206)) - Added function `Guild.delete_auto_moderation_rule`. ([#2153](https://github.com/Pycord-Development/pycord/pull/2153)) +- Added `VoiceChannel.slowmode_delay`. + ([#2112](https://github.com/Pycord-Development/pycord/pull/2112)) +- Added `ForumChannel.default_reaction_emoji` attribute. + ([#2178](https://github.com/Pycord-Development/pycord/pull/2178)) +- Added `default_reaction_emoji` parameter to `Guild.create_forum_channel()` and + `ForumChannel.edit()` methods. + ([#2178](https://github.com/Pycord-Development/pycord/pull/2178)) ### Changed @@ -180,6 +187,20 @@ These changes are available on the `master` branch, but have not yet been releas ([#2192](https://github.com/Pycord-Development/pycord/pull/2192)) - Fixed `DMChannel.recipient` being `None` and consequently `User.dm_channel` also being `None`. ([#2219](https://github.com/Pycord-Development/pycord/pull/2219)) +- Fixed ffmpeg being terminated prematurely when piping audio stream. + ([#2240](https://github.com/Pycord-Development/pycord/pull/2240)) +- Fixed tasks looping infinitely when `tzinfo` is neither `None` nor UTC. + ([#2196](https://github.com/Pycord-Development/pycord/pull/2196)) +- Fixed `AttributeError` when running permission checks without the `bot` scope. + ([#2113](https://github.com/Pycord-Development/pycord/issues/2113)) +- Fixed `Option` not working on bridge commands because `ext.commands.Command` doesn't + recognize them. ([#2256](https://github.com/Pycord-Development/pycord/pull/2256)) +- Fixed offset-aware tasks causing `TypeError` when being prepared. + ([#2271](https://github.com/Pycord-Development/pycord/pull/2271)) +- Fixed `AttributeError` when serializing commands with `Annotated` type hints. + ([#2243](https://github.com/Pycord-Development/pycord/pull/2243)) +- Fixed `Intents.all()` returning the wrong value. + ([#2257](https://github.com/Pycord-Development/pycord/issues/2257)) ## [2.4.1] - 2023-03-20 diff --git a/discord/abc.py b/discord/abc.py index 71a307ab87..ce811492bc 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -50,6 +50,7 @@ from .invite import Invite from .iterators import HistoryIterator from .mentions import AllowedMentions +from .partial_emoji import PartialEmoji, _EmojiTag from .permissions import PermissionOverwrite, Permissions from .role import Role from .scheduled_events import ScheduledEvent @@ -507,6 +508,28 @@ async def _edit( raise InvalidArgument("type field must be of type ChannelType") options["type"] = ch_type.value + try: + default_reaction_emoji = options["default_reaction_emoji"] + except KeyError: + pass + else: + if isinstance(default_reaction_emoji, _EmojiTag): # Emoji, PartialEmoji + default_reaction_emoji = default_reaction_emoji._to_partial() + elif isinstance(default_reaction_emoji, int): + default_reaction_emoji = PartialEmoji( + name=None, id=default_reaction_emoji + ) + elif isinstance(default_reaction_emoji, str): + default_reaction_emoji = PartialEmoji.from_str(default_reaction_emoji) + else: + raise InvalidArgument( + "default_reaction_emoji must be of type: Emoji | int | str" + ) + + options[ + "default_reaction_emoji" + ] = default_reaction_emoji._to_forum_reaction_payload() + if options: return await self._state.http.edit_channel( self.id, reason=reason, **options @@ -712,7 +735,7 @@ def permissions_for(self, obj: Member | Role, /) -> Permissions: return Permissions.all() default = self.guild.default_role - base = Permissions(default.permissions.value) + base = Permissions(default.permissions.value if default else 0) # Handle the role case first if isinstance(obj, Role): diff --git a/discord/bot.py b/discord/bot.py index 5587aef34b..545c11bb8d 100644 --- a/discord/bot.py +++ b/discord/bot.py @@ -781,8 +781,8 @@ async def on_connect(): lambda cmd: cmd.name == i["name"] and cmd.type == i.get("type") and cmd.guild_ids is not None - # TODO: fix this type error (guild_id is not defined in ApplicationCommand Typed Dict) - and int(i["guild_id"]) in cmd.guild_ids, # type: ignore + and (guild_id := i.get("guild_id")) + and guild_id in cmd.guild_ids, self.pending_application_commands, ) if not cmd: diff --git a/discord/channel.py b/discord/channel.py index 6121dd5890..93925b29a9 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -32,6 +32,7 @@ from . import utils from .asset import Asset +from .emoji import Emoji from .enums import ( ChannelType, EmbeddedActivity, @@ -171,7 +172,7 @@ def to_dict(self) -> dict[str, Any]: payload: dict[str, Any] = { "name": self.name, "moderated": self.moderated, - } | self.emoji._to_forum_tag_payload() + } | self.emoji._to_forum_reaction_payload() if self.id: payload["id"] = self.id @@ -195,6 +196,7 @@ class _TextChannel(discord.abc.GuildChannel, Hashable): "last_message_id", "default_auto_archive_duration", "default_thread_slowmode_delay", + "default_reaction_emoji", "default_sort_order", "available_tags", "flags", @@ -228,7 +230,6 @@ def _update( self.name: str = data["name"] self.category_id: int | None = utils._get_as_snowflake(data, "parent_id") self._type: int = data["type"] - # This data may be missing depending on how this object is being created/updated if not data.pop("_invoke_flag", False): self.topic: str | None = data.get("topic") @@ -1008,6 +1009,10 @@ class ForumChannel(_TextChannel): The initial slowmode delay to set on newly created threads in this channel. .. versionadded:: 2.3 + default_reaction_emoji: Optional[:class:`str` | :class:`discord.Emoji`] + The default forum reaction emoji. + + .. versionadded:: 2.5 """ def __init__( @@ -1022,6 +1027,15 @@ def _update(self, guild: Guild, data: ForumChannelPayload) -> None: for tag in (data.get("available_tags") or []) ] self.default_sort_order: SortOrder | None = data.get("default_sort_order", None) + reaction_emoji_ctx: dict = data.get("default_reaction_emoji") + if reaction_emoji_ctx is not None: + emoji_name = reaction_emoji_ctx.get("emoji_name") + if emoji_name is not None: + self.default_reaction_emoji = reaction_emoji_ctx["emoji_name"] + else: + self.default_reaction_emoji = self._state.get_emoji( + utils._get_as_snowflake(reaction_emoji_ctx, "emoji_id") + ) @property def guidelines(self) -> str | None: @@ -1061,6 +1075,7 @@ async def edit( default_auto_archive_duration: ThreadArchiveDuration = ..., default_thread_slowmode_delay: int = ..., default_sort_order: SortOrder = ..., + default_reaction_emoji: Emoji | int | str | None = ..., available_tags: list[ForumTag] = ..., require_tag: bool = ..., overwrites: Mapping[Role | Member | Snowflake, PermissionOverwrite] = ..., @@ -1113,6 +1128,12 @@ async def edit(self, *, reason=None, **options): The default sort order type to use to order posts in this channel. .. versionadded:: 2.3 + default_reaction_emoji: Optional[:class:`discord.Emoji` | :class:`int` | :class:`str`] + The default reaction emoji. + Can be a unicode emoji or a custom emoji in the forms: + :class:`Emoji`, snowflake ID, string representation (eg. ''). + + .. versionadded:: 2.5 available_tags: List[:class:`ForumTag`] The set of tags that can be used in this channel. Must be less than `20`. @@ -1329,6 +1350,7 @@ class VocalGuildChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hasha "user_limit", "_state", "position", + "slowmode_delay", "_overwrites", "category_id", "rtc_region", @@ -1376,6 +1398,7 @@ def _update( data, "last_message_id" ) self.position: int = data.get("position") + self.slowmode_delay = data.get("rate_limit_per_user", 0) self.bitrate: int = data.get("bitrate") self.user_limit: int = data.get("user_limit") self.flags: ChannelFlags = ChannelFlags._from_value(data.get("flags", 0)) @@ -1483,6 +1506,13 @@ class VoiceChannel(discord.abc.Messageable, VocalGuildChannel): The ID of the last message sent to this channel. It may not always point to an existing or valid message. .. versionadded:: 2.0 + slowmode_delay: :class:`int` + The number of seconds a member must wait between sending messages + in this channel. A value of `0` denotes that it is disabled. + Bots and users with :attr:`~Permissions.manage_channels` or + :attr:`~Permissions.manage_messages` bypass slowmode. + + .. versionadded:: 2.5 flags: :class:`ChannelFlags` Extra features of the channel. @@ -1791,6 +1821,7 @@ async def edit( overwrites: Mapping[Role | Member, PermissionOverwrite] = ..., rtc_region: VoiceRegion | None = ..., video_quality_mode: VideoQualityMode = ..., + slowmode_delay: int = ..., reason: str | None = ..., ) -> VoiceChannel | None: ... diff --git a/discord/commands/core.py b/discord/commands/core.py index d53bac0d71..c7bd4efd40 100644 --- a/discord/commands/core.py +++ b/discord/commands/core.py @@ -746,10 +746,12 @@ def _parse_options(self, params, *, check_params: bool = True) -> list[Option]: option = next(option_gen, Option()) # Handle Optional if self._is_typing_optional(type_hint): - option.input_type = get_args(type_hint)[0] + option.input_type = SlashCommandOptionType.from_datatype( + get_args(type_hint)[0] + ) option.default = None else: - option.input_type = type_hint + option.input_type = SlashCommandOptionType.from_datatype(type_hint) if self._is_typing_union(option): if self._is_typing_optional(option): diff --git a/discord/ext/commands/core.py b/discord/ext/commands/core.py index d3ce2e07db..c2282d28bf 100644 --- a/discord/ext/commands/core.py +++ b/discord/ext/commands/core.py @@ -45,6 +45,7 @@ from ...commands import ( ApplicationCommand, + Option, _BaseCommand, message_command, slash_command, @@ -562,7 +563,13 @@ async def dispatch_error(self, ctx: Context, error: Exception) -> None: ctx.bot.dispatch("command_error", ctx, error) async def transform(self, ctx: Context, param: inspect.Parameter) -> Any: - required = param.default is param.empty + if isinstance(param.annotation, Option): + default = param.annotation.default + required = param.annotation.required + else: + default = param.default + required = default is param.empty + converter = get_converter(param) consume_rest_is_special = ( param.kind == param.KEYWORD_ONLY and not self.rest_is_raw @@ -599,7 +606,7 @@ async def transform(self, ctx: Context, param: inspect.Parameter) -> Any: ): return await converter._construct_default(ctx) raise MissingRequiredArgument(param) - return param.default + return default previous = view.index if consume_rest_is_special: diff --git a/discord/ext/tasks/__init__.py b/discord/ext/tasks/__init__.py index 400786d8a4..638bd831c6 100644 --- a/discord/ext/tasks/__init__.py +++ b/discord/ext/tasks/__init__.py @@ -575,7 +575,7 @@ def _get_next_sleep_time(self) -> datetime.datetime: if self._current_loop == 0: # if we're at the last index on the first iteration, we need to sleep until tomorrow return datetime.datetime.combine( - datetime.datetime.now(datetime.timezone.utc) + datetime.datetime.now(self._time[0].tzinfo or datetime.timezone.utc) + datetime.timedelta(days=1), self._time[0], ) @@ -584,18 +584,26 @@ def _get_next_sleep_time(self) -> datetime.datetime: if self._current_loop == 0: self._time_index += 1 - if next_time > datetime.datetime.now(datetime.timezone.utc).timetz(): + if ( + next_time + > datetime.datetime.now( + next_time.tzinfo or datetime.timezone.utc + ).timetz() + ): return datetime.datetime.combine( - datetime.datetime.now(datetime.timezone.utc), next_time + datetime.datetime.now(next_time.tzinfo or datetime.timezone.utc), + next_time, ) else: return datetime.datetime.combine( - datetime.datetime.now(datetime.timezone.utc) + datetime.datetime.now(next_time.tzinfo or datetime.timezone.utc) + datetime.timedelta(days=1), next_time, ) - next_date = cast(datetime.datetime, self._last_iteration) + next_date = cast( + datetime.datetime, self._last_iteration.astimezone(next_time.tzinfo) + ) if next_time < next_date.timetz(): next_date += datetime.timedelta(days=1) @@ -611,9 +619,9 @@ def _prepare_time_index(self, now: datetime.datetime = MISSING) -> None: now if now is not MISSING else datetime.datetime.now(datetime.timezone.utc).replace(microsecond=0) - ).timetz() + ) for idx, time in enumerate(self._time): - if time >= time_now: + if time >= time_now.astimezone(time.tzinfo).timetz(): self._time_index = idx break else: diff --git a/discord/flags.py b/discord/flags.py index 308083d673..9f687ff9bc 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -636,8 +636,8 @@ def __init__(self, **kwargs: bool): @classmethod def all(cls: type[Intents]) -> Intents: """A factory method that creates a :class:`Intents` with everything enabled.""" - bits = max(cls.VALID_FLAGS.values()).bit_length() - value = (1 << bits) - 1 + value = sum({1 << (flag.bit_length() - 1) for flag in cls.VALID_FLAGS.values()}) + self = cls.__new__(cls) self.value = value return self diff --git a/discord/guild.py b/discord/guild.py index 7ce2a4d45b..98053fe27c 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -47,7 +47,7 @@ from .channel import * from .channel import _guild_channel_factory, _threaded_guild_channel_factory from .colour import Colour -from .emoji import Emoji +from .emoji import Emoji, PartialEmoji, _EmojiTag from .enums import ( AuditLogAction, AutoModEventType, @@ -1395,6 +1395,7 @@ async def create_forum_channel( slowmode_delay: int = MISSING, nsfw: bool = MISSING, overwrites: dict[Role | Member, PermissionOverwrite] = MISSING, + default_reaction_emoji: Emoji | int | str = MISSING, ) -> ForumChannel: """|coro| @@ -1436,6 +1437,12 @@ async def create_forum_channel( To mark the channel as NSFW or not. reason: Optional[:class:`str`] The reason for creating this channel. Shows up on the audit log. + default_reaction_emoji: Optional[:class:`Emoji` | :class:`int` | :class:`str`] + The default reaction emoji. + Can be a unicode emoji or a custom emoji in the forms: + :class:`Emoji`, snowflake ID, string representation (eg. ''). + + .. versionadded:: v2.5 Returns ------- @@ -1449,7 +1456,7 @@ async def create_forum_channel( HTTPException Creating the channel failed. InvalidArgument - The permission overwrite information is not in proper form. + The argument is not in proper form. Examples -------- @@ -1485,6 +1492,24 @@ async def create_forum_channel( if nsfw is not MISSING: options["nsfw"] = nsfw + if default_reaction_emoji is not MISSING: + if isinstance(default_reaction_emoji, _EmojiTag): # Emoji, PartialEmoji + default_reaction_emoji = default_reaction_emoji._to_partial() + elif isinstance(default_reaction_emoji, int): + default_reaction_emoji = PartialEmoji( + name=None, id=default_reaction_emoji + ) + elif isinstance(default_reaction_emoji, str): + default_reaction_emoji = PartialEmoji.from_str(default_reaction_emoji) + else: + raise InvalidArgument( + "default_reaction_emoji must be of type: Emoji | int | str" + ) + + options[ + "default_reaction_emoji" + ] = default_reaction_emoji._to_forum_reaction_payload() + data = await self._create_channel( name, overwrites=overwrites, diff --git a/discord/http.py b/discord/http.py index 40d9ebd928..30184b665a 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1096,6 +1096,7 @@ def create_channel( "rtc_region", "video_quality_mode", "auto_archive_duration", + "default_reaction_emoji", ) payload.update( {k: v for k, v in options.items() if k in valid_keys and v is not None} diff --git a/discord/interactions.py b/discord/interactions.py index 83ca14f128..19b5363082 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -149,6 +149,8 @@ class Interaction: "custom_id", "_channel_data", "_message_data", + "_guild_data", + "_guild", "_permissions", "_app_permissions", "_state", @@ -188,6 +190,11 @@ def _from_data(self, data: InteractionPayload): self.user: User | Member | None = None self._permissions: int = 0 + self._guild: Guild | None = None + self._guild_data = data.get("guild") + if self.guild is None and self._guild_data: + self._guild = Guild(data=self._guild_data, state=self) + # TODO: there's a potential data loss here if self.guild_id: guild = ( @@ -246,6 +253,8 @@ def client(self) -> Client: @property def guild(self) -> Guild | None: """The guild the interaction was sent from.""" + if self._guild: + return self._guild return self._state and self._state._get_guild(self.guild_id) def is_command(self) -> bool: diff --git a/discord/partial_emoji.py b/discord/partial_emoji.py index ec5495c7af..171d6390c5 100644 --- a/discord/partial_emoji.py +++ b/discord/partial_emoji.py @@ -160,11 +160,11 @@ def to_dict(self) -> dict[str, Any]: def _to_partial(self) -> PartialEmoji: return self - def _to_forum_tag_payload( + def _to_forum_reaction_payload( self, - ) -> TypedDict("TagPayload", {"emoji_id": int, "emoji_name": None}) | TypedDict( - "TagPayload", {"emoji_id": None, "emoji_name": str} - ): + ) -> TypedDict( + "ReactionPayload", {"emoji_id": int, "emoji_name": None} + ) | TypedDict("ReactionPayload", {"emoji_id": None, "emoji_name": str}): if self.id is None: return {"emoji_id": None, "emoji_name": self.name} else: diff --git a/discord/player.py b/discord/player.py index 87b0f5718a..0b4dc417c3 100644 --- a/discord/player.py +++ b/discord/player.py @@ -227,7 +227,7 @@ def _pipe_writer(self, source: io.BufferedIOBase) -> None: # arbitrarily large read size data = source.read(8192) if not data: - self._process.terminate() + self._stdin.close() return try: self._stdin.write(data) diff --git a/discord/raw_models.py b/discord/raw_models.py index 39eab47443..79c8091d98 100644 --- a/discord/raw_models.py +++ b/discord/raw_models.py @@ -256,7 +256,7 @@ def __init__( self.burst: bool = data.get("burst") self.burst_colours: list = data.get("burst_colors", []) self.burst_colors: list = self.burst_colours - self.type: ReactionType = try_enum(data.get("type", 0)) + self.type: ReactionType = try_enum(ReactionType, data.get("type", 0)) try: self.guild_id: int | None = int(data["guild_id"]) @@ -333,7 +333,7 @@ def __init__(self, data: ReactionClearEmojiEvent, emoji: PartialEmoji) -> None: self.burst: bool = data.get("burst") self.burst_colours: list = data.get("burst_colors", []) self.burst_colors: list = self.burst_colours - self.type: ReactionType = try_enum(data.get("type", 0)) + self.type: ReactionType = try_enum(ReactionType, data.get("type", 0)) try: self.guild_id: int | None = int(data["guild_id"]) diff --git a/discord/types/interactions.py b/discord/types/interactions.py index ad891e3203..db66489067 100644 --- a/discord/types/interactions.py +++ b/discord/types/interactions.py @@ -47,35 +47,45 @@ class ApplicationCommand(TypedDict): - options: NotRequired[list[ApplicationCommandOption]] - type: NotRequired[ApplicationCommandType] - name_localized: NotRequired[str] - name_localizations: NotRequired[dict[str, str]] - description_localized: NotRequired[str] - description_localizations: NotRequired[dict[str, str]] id: Snowflake + type: NotRequired[ApplicationCommandType] application_id: Snowflake + guild_id: NotRequired[Snowflake] name: str + name_localizations: NotRequired[dict[str, str] | None] description: str + description_localizations: NotRequired[dict[str, str] | None] + options: NotRequired[list[ApplicationCommandOption]] + default_member_permissions: str | None + dm_permission: NotRequired[bool] + default_permission: NotRequired[bool | None] + nsfw: NotRequired[bool] + version: Snowflake ApplicationCommandOptionType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] class ApplicationCommandOption(TypedDict): - choices: NotRequired[list[ApplicationCommandOptionChoice]] - options: NotRequired[list[ApplicationCommandOption]] - name_localizations: NotRequired[dict[str, str]] - description_localizations: NotRequired[dict[str, str]] type: ApplicationCommandOptionType name: str + name_localizations: NotRequired[dict[str, str] | None] description: str + description_localizations: NotRequired[dict[str, str] | None] required: bool + options: NotRequired[list[ApplicationCommandOption]] + choices: NotRequired[list[ApplicationCommandOptionChoice]] + channel_types: NotRequired[list[ChannelType]] + min_value: NotRequired[int | float] + max_value: NotRequired[int | float] + min_length: NotRequired[int] + max_length: NotRequired[int] + autocomplete: NotRequired[bool] class ApplicationCommandOptionChoice(TypedDict): - name_localizations: NotRequired[dict[str, str]] name: str + name_localizations: NotRequired[dict[str, str] | None] value: str | int diff --git a/requirements/dev.txt b/requirements/dev.txt index 1831a6fd24..3c71378e73 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,11 +1,11 @@ -r _.txt -pylint~=2.17.5 +pylint~=3.0.3 pytest~=7.4.3 -pytest-asyncio~=0.21.1 +pytest-asyncio~=0.23.2 # pytest-order~=1.0.1 -mypy~=1.5.1 +mypy~=1.7.1 coverage~=7.3 pre-commit==3.5.0 codespell==2.2.6 -bandit==1.7.5 +bandit==1.7.6 flake8==6.1.0 diff --git a/requirements/docs.txt b/requirements/docs.txt index 453c4fcf7b..605916439c 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -2,7 +2,7 @@ sphinx==5.3.0 sphinxcontrib_trio==1.1.2 sphinxcontrib-websupport==1.2.4 myst-parser==1.0.0 -sphinxext-opengraph==0.8.2 +sphinxext-opengraph==0.9.1 sphinx-copybutton==0.5.2 furo@ git+https://github.com/pradyunsg/furo@193643f sphinx-autodoc-typehints==1.23.0 diff --git a/requirements/speed.txt b/requirements/speed.txt index 5664d336cf..d4ce69e7e6 100644 --- a/requirements/speed.txt +++ b/requirements/speed.txt @@ -1,2 +1,2 @@ -msgspec~=0.18.4 +msgspec~=0.18.5 aiohttp[speedups] diff --git a/tests/test_typing_annotated.py b/tests/test_typing_annotated.py index 582bd4f8a0..7091a241ff 100644 --- a/tests/test_typing_annotated.py +++ b/tests/test_typing_annotated.py @@ -1,10 +1,9 @@ from typing import Optional -import pytest from typing_extensions import Annotated import discord -from discord import ApplicationContext +from discord import SlashCommandOptionType from discord.commands.core import SlashCommand, slash_command @@ -15,6 +14,10 @@ async def echo(ctx, txt: Annotated[str, discord.Option()]): cmd = SlashCommand(echo) bot = discord.Bot() bot.add_application_command(cmd) + dict_result = cmd.to_dict() + assert ( + dict_result.get("options")[0].get("type") == SlashCommandOptionType.string.value + ) def test_typing_annotated_decorator(): @@ -24,6 +27,12 @@ def test_typing_annotated_decorator(): async def echo(ctx, txt: Annotated[str, discord.Option(description="Some text")]): await ctx.respond(txt) + dict_result = echo.to_dict() + + option = dict_result.get("options")[0] + assert option.get("type") == SlashCommandOptionType.string.value + assert option.get("description") == "Some text" + def test_typing_annotated_cog(): class echoCog(discord.Cog): @@ -38,7 +47,14 @@ async def echo( await ctx.respond(txt) bot = discord.Bot() - bot.add_cog(echoCog(bot)) + cog = echoCog(bot) + bot.add_cog(cog) + + dict_result = cog.echo.to_dict() + + option = dict_result.get("options")[0] + assert option.get("type") == SlashCommandOptionType.string.value + assert option.get("description") == "Some text" def test_typing_annotated_cog_slashgroup(): @@ -56,7 +72,14 @@ async def echo( await ctx.respond(txt) bot = discord.Bot() - bot.add_cog(echoCog(bot)) + cog = echoCog(bot) + bot.add_cog(cog) + + dict_result = cog.echo.to_dict() + + option = dict_result.get("options")[0] + assert option.get("type") == SlashCommandOptionType.string.value + assert option.get("description") == "Some text" def test_typing_annotated_optional(): @@ -67,6 +90,11 @@ async def echo(ctx, txt: Annotated[Optional[str], discord.Option()]): bot = discord.Bot() bot.add_application_command(cmd) + dict_result = cmd.to_dict() + + option = dict_result.get("options")[0] + assert option.get("type") == SlashCommandOptionType.string.value + def test_no_annotation(): async def echo(ctx, txt: str): @@ -76,6 +104,11 @@ async def echo(ctx, txt: str): bot = discord.Bot() bot.add_application_command(cmd) + dict_result = cmd.to_dict() + + option = dict_result.get("options")[0] + assert option.get("type") == SlashCommandOptionType.string.value + def test_annotated_no_option(): async def echo(ctx, txt: Annotated[str, "..."]): @@ -84,3 +117,8 @@ async def echo(ctx, txt: Annotated[str, "..."]): cmd = SlashCommand(echo) bot = discord.Bot() bot.add_application_command(cmd) + + dict_result = cmd.to_dict() + + option = dict_result.get("options")[0] + assert option.get("type") == SlashCommandOptionType.string.value