From 4da720d80abe1228c4ee086b32fcf23bfd8cf702 Mon Sep 17 00:00:00 2001 From: shiftinv <8530778+shiftinv@users.noreply.github.com> Date: Sat, 20 Jan 2024 17:38:46 +0100 Subject: [PATCH 1/4] feat(webhook): improve thread support for webhook messages (#1077) --- changelog/1077.feature.rst | 1 + disnake/webhook/async_.py | 91 ++++++++++++++++++++++++++++++-------- disnake/webhook/sync.py | 71 +++++++++++++++++++++++------ 3 files changed, 131 insertions(+), 32 deletions(-) create mode 100644 changelog/1077.feature.rst diff --git a/changelog/1077.feature.rst b/changelog/1077.feature.rst new file mode 100644 index 0000000000..2ba901b3f9 --- /dev/null +++ b/changelog/1077.feature.rst @@ -0,0 +1 @@ +Add support for threads in :meth:`Webhook.fetch_message`, :meth:`~Webhook.edit_message`, and :meth:`~Webhook.delete_message`, as well as their sync counterparts. diff --git a/disnake/webhook/async_.py b/disnake/webhook/async_.py index 5c252d9663..cf25ca8fd6 100644 --- a/disnake/webhook/async_.py +++ b/disnake/webhook/async_.py @@ -37,6 +37,7 @@ from ..http import Route, set_attachments, to_multipart, to_multipart_with_attachments from ..message import Message from ..mixins import Hashable +from ..object import Object from ..ui.action_row import MessageUIComponent, components_to_dict from ..user import BaseUser, User @@ -293,6 +294,7 @@ def execute_webhook( params = {"wait": int(wait)} if thread_id: params["thread_id"] = thread_id + route = Route( "POST", "/webhooks/{webhook_id}/{webhook_token}", @@ -310,7 +312,12 @@ def get_webhook_message( message_id: int, *, session: aiohttp.ClientSession, + thread_id: Optional[int] = None, ) -> Response[MessagePayload]: + params: Dict[str, Any] = {} + if thread_id is not None: + params["thread_id"] = thread_id + route = Route( "GET", "/webhooks/{webhook_id}/{webhook_token}/messages/{message_id}", @@ -318,7 +325,7 @@ def get_webhook_message( webhook_token=token, message_id=message_id, ) - return self.request(route, session) + return self.request(route, session, params=params) def edit_webhook_message( self, @@ -330,7 +337,12 @@ def edit_webhook_message( payload: Optional[Dict[str, Any]] = None, multipart: Optional[List[Dict[str, Any]]] = None, files: Optional[List[File]] = None, + thread_id: Optional[int] = None, ) -> Response[Message]: + params: Dict[str, Any] = {} + if thread_id is not None: + params["thread_id"] = thread_id + route = Route( "PATCH", "/webhooks/{webhook_id}/{webhook_token}/messages/{message_id}", @@ -338,7 +350,9 @@ def edit_webhook_message( webhook_token=token, message_id=message_id, ) - return self.request(route, session, payload=payload, multipart=multipart, files=files) + return self.request( + route, session, payload=payload, multipart=multipart, files=files, params=params + ) def delete_webhook_message( self, @@ -347,7 +361,12 @@ def delete_webhook_message( message_id: int, *, session: aiohttp.ClientSession, + thread_id: Optional[int] = None, ) -> Response[None]: + params: Dict[str, Any] = {} + if thread_id is not None: + params["thread_id"] = thread_id + route = Route( "DELETE", "/webhooks/{webhook_id}/{webhook_token}/messages/{message_id}", @@ -355,7 +374,7 @@ def delete_webhook_message( webhook_token=token, message_id=message_id, ) - return self.request(route, session) + return self.request(route, session, params=params) def fetch_webhook( self, @@ -691,10 +710,14 @@ def __getattr__(self, attr) -> NoReturn: class _WebhookState(Generic[WebhookT]): - __slots__ = ("_parent", "_webhook") + __slots__ = ("_parent", "_webhook", "_thread") def __init__( - self, webhook: WebhookT, parent: Optional[Union[ConnectionState, _WebhookState]] + self, + webhook: WebhookT, + parent: Optional[Union[ConnectionState, _WebhookState]], + *, + thread: Optional[Snowflake] = None, ) -> None: self._webhook: WebhookT = webhook @@ -704,6 +727,8 @@ def __init__( else: self._parent = parent + self._thread: Optional[Snowflake] = thread + def _get_guild(self, guild_id): if self._parent is not None: return self._parent._get_guild(guild_id) @@ -861,6 +886,7 @@ async def edit( view=view, components=components, allowed_mentions=allowed_mentions, + thread=self._state._thread, ) async def delete(self, *, delay: Optional[float] = None) -> None: @@ -888,13 +914,13 @@ async def delete(self, *, delay: Optional[float] = None) -> None: async def inner_call(delay: float = delay) -> None: await asyncio.sleep(delay) try: - await self._state._webhook.delete_message(self.id) + await self._state._webhook.delete_message(self.id, thread=self._state._thread) except HTTPException: pass asyncio.create_task(inner_call()) else: - await self._state._webhook.delete_message(self.id) + await self._state._webhook.delete_message(self.id, thread=self._state._thread) class BaseWebhook(Hashable): @@ -1422,19 +1448,30 @@ async def edit( return Webhook(data=data, session=self.session, token=self.auth_token, state=self._state) - def _create_message(self, data): - state = _WebhookState(self, parent=self._state) - # state may be artificial (unlikely at this point...) + def _create_message( + self, data, *, thread: Optional[Snowflake] = None, thread_name: Optional[str] = None + ): channel_id = int(data["channel_id"]) - # if the channel ID does not match, a new thread was created + + # If channel IDs don't match, a new thread was most likely created; + # if the user passed a `thread_name`, assume this is the case and + # create a `thread` object for the state + if self.channel_id != channel_id and thread_name: + thread = Object(id=channel_id) + + state = _WebhookState(self, parent=self._state, thread=thread) + + # If the channel IDs don't match, the message was created in a thread if self.channel_id != channel_id: guild = self.guild msg_channel = guild and guild.get_channel_or_thread(channel_id) else: msg_channel = self.channel + if not msg_channel: # state may be artificial (unlikely at this point...) msg_channel = PartialMessageable(state=self._state, id=channel_id) # type: ignore + # state is artificial return WebhookMessage(data=data, state=state, channel=msg_channel) # type: ignore @@ -1588,7 +1625,7 @@ async def send( .. versionadded:: 2.4 thread: :class:`~disnake.abc.Snowflake` - The thread to send this webhook to. + The thread to send this message to. .. versionadded:: 2.0 @@ -1732,7 +1769,7 @@ async def send( msg = None if wait: - msg = self._create_message(data) + msg = self._create_message(data, thread=thread, thread_name=thread_name) if delete_after is not MISSING: await msg.delete(delay=delete_after) @@ -1742,7 +1779,7 @@ async def send( return msg - async def fetch_message(self, id: int) -> WebhookMessage: + async def fetch_message(self, id: int, *, thread: Optional[Snowflake] = None) -> WebhookMessage: """|coro| Retrieves a single :class:`WebhookMessage` owned by this webhook. @@ -1756,6 +1793,10 @@ async def fetch_message(self, id: int) -> WebhookMessage: ---------- id: :class:`int` The message ID to look for. + thread: Optional[:class:`~disnake.abc.Snowflake`] + The thread the message is in, if any. + + .. versionadded:: 2.10 Raises ------ @@ -1782,8 +1823,9 @@ async def fetch_message(self, id: int) -> WebhookMessage: self.token, id, session=self.session, + thread_id=thread.id if thread else None, ) - return self._create_message(data) + return self._create_message(data, thread=thread) async def edit_message( self, @@ -1798,6 +1840,7 @@ async def edit_message( view: Optional[View] = MISSING, components: Optional[Components[MessageUIComponent]] = MISSING, allowed_mentions: Optional[AllowedMentions] = None, + thread: Optional[Snowflake] = None, ) -> WebhookMessage: """|coro| @@ -1873,6 +1916,10 @@ async def edit_message( allowed_mentions: :class:`AllowedMentions` Controls the mentions being processed in this message. See :meth:`.abc.Messageable.send` for more information. + thread: Optional[:class:`~disnake.abc.Snowflake`] + The thread the message is in, if any. + + .. versionadded:: 2.10 Raises ------ @@ -1905,7 +1952,7 @@ async def edit_message( # if no attachment list was provided but we're uploading new files, # use current attachments as the base if attachments is MISSING and (file or files): - attachments = (await self.fetch_message(message_id)).attachments + attachments = (await self.fetch_message(message_id, thread=thread)).attachments previous_mentions: Optional[AllowedMentions] = getattr( self._state, "allowed_mentions", None @@ -1929,6 +1976,7 @@ async def edit_message( self.token, message_id, session=self.session, + thread_id=thread.id if thread else None, payload=params.payload, multipart=params.multipart, files=params.files, @@ -1938,12 +1986,14 @@ async def edit_message( for f in params.files: f.close() - message = self._create_message(data) + message = self._create_message(data, thread=thread) if view and not view.is_finished(): self._state.store_view(view, message_id) return message - async def delete_message(self, message_id: int, /) -> None: + async def delete_message( + self, message_id: int, /, *, thread: Optional[Snowflake] = None + ) -> None: """|coro| Deletes a message owned by this webhook. @@ -1960,6 +2010,10 @@ async def delete_message(self, message_id: int, /) -> None: ---------- message_id: :class:`int` The ID of the message to delete. + thread: Optional[:class:`~disnake.abc.Snowflake`] + The thread the message is in, if any. + + .. versionadded:: 2.10 Raises ------ @@ -1979,4 +2033,5 @@ async def delete_message(self, message_id: int, /) -> None: self.token, message_id, session=self.session, + thread_id=thread.id if thread else None, ) diff --git a/disnake/webhook/sync.py b/disnake/webhook/sync.py index 2f8e9d7377..410016a68b 100644 --- a/disnake/webhook/sync.py +++ b/disnake/webhook/sync.py @@ -34,6 +34,7 @@ from ..flags import MessageFlags from ..http import Route from ..message import Message +from ..object import Object from .async_ import BaseWebhook, _WebhookState, handle_message_parameters __all__ = ( @@ -283,6 +284,7 @@ def execute_webhook( params = {"wait": int(wait)} if thread_id: params["thread_id"] = thread_id + route = Route( "POST", "/webhooks/{webhook_id}/{webhook_token}", @@ -300,7 +302,12 @@ def get_webhook_message( message_id: int, *, session: Session, + thread_id: Optional[int] = None, ): + params: Dict[str, Any] = {} + if thread_id is not None: + params["thread_id"] = thread_id + route = Route( "GET", "/webhooks/{webhook_id}/{webhook_token}/messages/{message_id}", @@ -308,7 +315,7 @@ def get_webhook_message( webhook_token=token, message_id=message_id, ) - return self.request(route, session) + return self.request(route, session, params=params) def edit_webhook_message( self, @@ -320,7 +327,12 @@ def edit_webhook_message( payload: Optional[Dict[str, Any]] = None, multipart: Optional[List[Dict[str, Any]]] = None, files: Optional[List[File]] = None, + thread_id: Optional[int] = None, ): + params: Dict[str, Any] = {} + if thread_id is not None: + params["thread_id"] = thread_id + route = Route( "PATCH", "/webhooks/{webhook_id}/{webhook_token}/messages/{message_id}", @@ -328,7 +340,9 @@ def edit_webhook_message( webhook_token=token, message_id=message_id, ) - return self.request(route, session, payload=payload, multipart=multipart, files=files) + return self.request( + route, session, payload=payload, multipart=multipart, files=files, params=params + ) def delete_webhook_message( self, @@ -337,7 +351,12 @@ def delete_webhook_message( message_id: int, *, session: Session, + thread_id: Optional[int] = None, ): + params: Dict[str, Any] = {} + if thread_id is not None: + params["thread_id"] = thread_id + route = Route( "DELETE", "/webhooks/{webhook_id}/{webhook_token}/messages/{message_id}", @@ -345,7 +364,7 @@ def delete_webhook_message( webhook_token=token, message_id=message_id, ) - return self.request(route, session) + return self.request(route, session, params=params) def fetch_webhook( self, @@ -487,6 +506,7 @@ def edit( files=files, attachments=attachments, allowed_mentions=allowed_mentions, + thread=self._state._thread, ) def delete(self, *, delay: Optional[float] = None) -> None: @@ -514,7 +534,7 @@ def delete(self, *, delay: Optional[float] = None) -> None: """ if delay is not None: time.sleep(delay) - self._state._webhook.delete_message(self.id) + self._state._webhook.delete_message(self.id, thread=self._state._thread) class SyncWebhook(BaseWebhook): @@ -857,11 +877,16 @@ def edit( data=data, session=self.session, token=self.auth_token, state=self._state ) - def _create_message(self, data): - state = _WebhookState(self, parent=self._state) - # state may be artificial (unlikely at this point...) - channel = self.channel + def _create_message( + self, data, *, thread: Optional[Snowflake] = None, thread_name: Optional[str] = None + ): + # see async webhook's _create_message for details channel_id = int(data["channel_id"]) + if self.channel_id != channel_id and thread_name: + thread = Object(id=channel_id) + + state = _WebhookState(self, parent=self._state, thread=thread) + channel = self.channel if not channel or self.channel_id != channel_id: channel = PartialMessageable(state=self._state, id=channel_id) # type: ignore # state is artificial @@ -1088,9 +1113,11 @@ def send( for f in params.files: f.close() if wait: - return self._create_message(data) + return self._create_message(data, thread=thread, thread_name=thread_name) - def fetch_message(self, id: int, /) -> SyncWebhookMessage: + def fetch_message( + self, id: int, /, *, thread: Optional[Snowflake] = None + ) -> SyncWebhookMessage: """Retrieves a single :class:`SyncWebhookMessage` owned by this webhook. .. versionadded:: 2.0 @@ -1102,6 +1129,10 @@ def fetch_message(self, id: int, /) -> SyncWebhookMessage: ---------- id: :class:`int` The message ID to look for. + thread: Optional[:class:`~disnake.abc.Snowflake`] + The thread the message is in, if any. + + .. versionadded:: 2.10 Raises ------ @@ -1128,8 +1159,9 @@ def fetch_message(self, id: int, /) -> SyncWebhookMessage: self.token, id, session=self.session, + thread_id=thread.id if thread else None, ) - return self._create_message(data) + return self._create_message(data, thread=thread) def edit_message( self, @@ -1142,6 +1174,7 @@ def edit_message( files: List[File] = MISSING, attachments: Optional[List[Attachment]] = MISSING, allowed_mentions: Optional[AllowedMentions] = None, + thread: Optional[Snowflake] = None, ) -> SyncWebhookMessage: """Edits a message owned by this webhook. @@ -1194,6 +1227,10 @@ def edit_message( allowed_mentions: :class:`AllowedMentions` Controls the mentions being processed in this message. See :meth:`.abc.Messageable.send` for more information. + thread: Optional[:class:`~disnake.abc.Snowflake`] + The thread the message is in, if any. + + .. versionadded:: 2.10 Raises ------ @@ -1214,7 +1251,7 @@ def edit_message( # if no attachment list was provided but we're uploading new files, # use current attachments as the base if attachments is MISSING and (file or files): - attachments = self.fetch_message(message_id).attachments + attachments = self.fetch_message(message_id, thread=thread).attachments previous_mentions: Optional[AllowedMentions] = getattr( self._state, "allowed_mentions", None @@ -1236,6 +1273,7 @@ def edit_message( self.token, message_id, session=self.session, + thread_id=thread.id if thread else None, payload=params.payload, multipart=params.multipart, files=params.files, @@ -1244,9 +1282,9 @@ def edit_message( if params.files: for f in params.files: f.close() - return self._create_message(data) + return self._create_message(data, thread=thread) - def delete_message(self, message_id: int, /) -> None: + def delete_message(self, message_id: int, /, *, thread: Optional[Snowflake] = None) -> None: """Deletes a message owned by this webhook. This is a lower level interface to :meth:`WebhookMessage.delete` in case @@ -1261,6 +1299,10 @@ def delete_message(self, message_id: int, /) -> None: ---------- message_id: :class:`int` The ID of the message to delete. + thread: Optional[:class:`~disnake.abc.Snowflake`] + The thread the message is in, if any. + + .. versionadded:: 2.10 Raises ------ @@ -1280,4 +1322,5 @@ def delete_message(self, message_id: int, /) -> None: self.token, message_id, session=self.session, + thread_id=thread.id if thread else None, ) From a24abf43297215b6fe70e0bc61b2ec1dada84b9f Mon Sep 17 00:00:00 2001 From: shiftinv <8530778+shiftinv@users.noreply.github.com> Date: Sat, 20 Jan 2024 17:44:58 +0100 Subject: [PATCH 2/4] fix(shard): handle `RECONNECT` opcode during initial connection flow (#1155) --- changelog/1155.bugfix.rst | 1 + disnake/shard.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 changelog/1155.bugfix.rst diff --git a/changelog/1155.bugfix.rst b/changelog/1155.bugfix.rst new file mode 100644 index 0000000000..9dabc40f9a --- /dev/null +++ b/changelog/1155.bugfix.rst @@ -0,0 +1 @@ +Handle unexpected ``RECONNECT`` opcode where ``HELLO`` is expected during initial shard connection. diff --git a/disnake/shard.py b/disnake/shard.py index a82ae13efd..3903d413aa 100644 --- a/disnake/shard.py +++ b/disnake/shard.py @@ -190,6 +190,16 @@ async def reidentify(self, exc: ReconnectWebSocket) -> None: gateway=self.ws.resume_gateway if exc.resume else None, ) self.ws = await asyncio.wait_for(coro, timeout=60.0) + # n.b. this is the same error handling as for the actual worker, but for the initial connect steps + except ReconnectWebSocket as e: + _log.debug( + "Unexpectedly received request to %s shard ID %s while attempting to %s", + e.op, + self.id, + exc.op, + ) + etype = EventType.resume if e.resume else EventType.identify + self._queue_put(EventItem(etype, self, e)) except self._handled_exceptions as e: await self._handle_disconnect(e) except asyncio.CancelledError: @@ -204,6 +214,14 @@ async def reconnect(self) -> None: try: coro = DiscordWebSocket.from_client(self._client, shard_id=self.id) self.ws = await asyncio.wait_for(coro, timeout=60.0) + except ReconnectWebSocket as e: + _log.debug( + "Unexpectedly received request to %s shard ID %s while attempting to reconnect", + e.op, + self.id, + ) + etype = EventType.resume if e.resume else EventType.identify + self._queue_put(EventItem(etype, self, e)) except self._handled_exceptions as e: await self._handle_disconnect(e) except asyncio.CancelledError: From 699ffc40fb4749d583cadca0390693c4bda5b4de Mon Sep 17 00:00:00 2001 From: shiftinv <8530778+shiftinv@users.noreply.github.com> Date: Fri, 2 Feb 2024 13:02:16 +0100 Subject: [PATCH 3/4] docs: misc fixes/clarifications (#1137) --- changelog/1045.bugfix.rst | 2 +- changelog/1111.bugfix.rst | 2 +- disnake/abc.py | 6 ------ disnake/ext/commands/common_bot_base.py | 8 ++++++++ disnake/threads.py | 2 +- disnake/ui/select/role.py | 2 +- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/changelog/1045.bugfix.rst b/changelog/1045.bugfix.rst index baa41c937a..47de7a7283 100644 --- a/changelog/1045.bugfix.rst +++ b/changelog/1045.bugfix.rst @@ -1 +1 @@ -|commands| Fix incorrect typings of :meth:`~disnake.ext.commands.InvokableApplicationCommand.add_check`, :meth:`~disnake.ext.commands.InvokableApplicationCommand.remove_check`, :meth:`~disnake.ext.commands.InteractionBotBase.add_app_command_check` and :meth:`~disnake.ext.commands.InteractionBotBase.remove_app_command_check`. +|commands| Fix incorrect typings of :meth:`InvokableApplicationCommand.add_check <.ext.commands.InvokableApplicationCommand.add_check>`, :meth:`InvokableApplicationCommand.remove_check <.ext.commands.InvokableApplicationCommand.remove_check>`, :meth:`Bot.add_app_command_check <.ext.commands.Bot.add_app_command_check>` and :meth:`Bot.remove_app_command_check <.ext.commands.Bot.remove_app_command_check>`. diff --git a/changelog/1111.bugfix.rst b/changelog/1111.bugfix.rst index 4efa8693ed..e4db7af3b1 100644 --- a/changelog/1111.bugfix.rst +++ b/changelog/1111.bugfix.rst @@ -1 +1 @@ -Allow ``cls`` argument in select menu decorators (e.g. :func`ui.string_select`) to be specified by keyword instead of being positional-only. +Allow ``cls`` argument in select menu decorators (e.g. :func:`ui.string_select`) to be specified by keyword instead of being positional-only. diff --git a/disnake/abc.py b/disnake/abc.py index fda31ec5fb..55c9ee3f78 100644 --- a/disnake/abc.py +++ b/disnake/abc.py @@ -1297,12 +1297,6 @@ async def create_invite( max_age: :class:`int` How long the invite should last in seconds. If set to ``0``, then the invite doesn't expire. Defaults to ``0``. - - .. warning:: - - If the guild is not a Community guild (has ``COMMUNITY`` in :attr:`.Guild.features`), - this must be set to a number between ``1`` and ``2592000`` seconds. - max_uses: :class:`int` How many uses the invite could be used for. If it's 0 then there are unlimited uses. Defaults to ``0``. diff --git a/disnake/ext/commands/common_bot_base.py b/disnake/ext/commands/common_bot_base.py index 737736c170..8658aa369a 100644 --- a/disnake/ext/commands/common_bot_base.py +++ b/disnake/ext/commands/common_bot_base.py @@ -162,6 +162,10 @@ def add_cog(self, cog: Cog, *, override: bool = False) -> None: A cog is a class that has its own event listeners and commands. + This automatically re-syncs application commands, provided that + :attr:`command_sync_flags.sync_on_cog_actions <.CommandSyncFlags.sync_on_cog_actions>` + isn't disabled. + .. versionchanged:: 2.0 :exc:`.ClientException` is raised when a cog with the same name @@ -228,6 +232,10 @@ def remove_cog(self, name: str) -> Optional[Cog]: If no cog is found then this method has no effect. + This automatically re-syncs application commands, provided that + :attr:`command_sync_flags.sync_on_cog_actions <.CommandSyncFlags.sync_on_cog_actions>` + isn't disabled. + Parameters ---------- name: :class:`str` diff --git a/disnake/threads.py b/disnake/threads.py index fb0b2add92..52810b9f64 100644 --- a/disnake/threads.py +++ b/disnake/threads.py @@ -831,7 +831,7 @@ async def remove_user(self, user: Snowflake) -> None: Parameters ---------- user: :class:`abc.Snowflake` - The user to add to the thread. + The user to remove from the thread. Raises ------ diff --git a/disnake/ui/select/role.py b/disnake/ui/select/role.py index fe2da2f97a..f3dbec4b17 100644 --- a/disnake/ui/select/role.py +++ b/disnake/ui/select/role.py @@ -23,7 +23,7 @@ class RoleSelect(BaseSelect[RoleSelectMenu, "Role", V_co]): - """Represents a UI user select menu. + """Represents a UI role select menu. This is usually represented as a drop down menu. From 0b1a047bbf8462e2203aa66b1394072251ed25ab Mon Sep 17 00:00:00 2001 From: shiftinv <8530778+shiftinv@users.noreply.github.com> Date: Fri, 2 Feb 2024 13:54:16 +0100 Subject: [PATCH 4/4] build(meta): add Python 3.12 package classifier (#1135) --- .github/workflows/lint-test.yml | 10 +++------- changelog/1117.feature.rst | 1 + changelog/{1128.feature.rst => 1128.feature.0.rst} | 0 changelog/1128.feature.1.rst | 1 + changelog/1135.feature.rst | 1 + pyproject.toml | 1 + 6 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 changelog/1117.feature.rst rename changelog/{1128.feature.rst => 1128.feature.0.rst} (100%) create mode 100644 changelog/1128.feature.1.rst create mode 100644 changelog/1135.feature.rst diff --git a/.github/workflows/lint-test.yml b/.github/workflows/lint-test.yml index 731f771f5e..f78b75ffff 100644 --- a/.github/workflows/lint-test.yml +++ b/.github/workflows/lint-test.yml @@ -72,9 +72,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies - run: | - pdm update --pre aiohttp # XXX: temporarily install aiohttp prerelease for 3.12 - pdm install -d -Gspeed -Gdocs -Gvoice + run: pdm install -d -Gspeed -Gdocs -Gvoice - name: Add .venv/bin to PATH run: dirname "$(pdm info --python)" >> $GITHUB_PATH @@ -171,13 +169,11 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies - run: | - pdm update --pre aiohttp # XXX: temporarily install aiohttp prerelease for 3.12 - pdm install -dG test # needed for coverage + run: pdm install -dG test # needed for coverage - name: Test package install run: | - python -m pip install --pre . # XXX: temporarily install aiohttp prerelease for 3.12; remove --pre flag again later + python -m pip install . - name: Run pytest id: run_tests diff --git a/changelog/1117.feature.rst b/changelog/1117.feature.rst new file mode 100644 index 0000000000..5b9263547d --- /dev/null +++ b/changelog/1117.feature.rst @@ -0,0 +1 @@ +Support Python 3.12. diff --git a/changelog/1128.feature.rst b/changelog/1128.feature.0.rst similarity index 100% rename from changelog/1128.feature.rst rename to changelog/1128.feature.0.rst diff --git a/changelog/1128.feature.1.rst b/changelog/1128.feature.1.rst new file mode 100644 index 0000000000..5b9263547d --- /dev/null +++ b/changelog/1128.feature.1.rst @@ -0,0 +1 @@ +Support Python 3.12. diff --git a/changelog/1135.feature.rst b/changelog/1135.feature.rst new file mode 100644 index 0000000000..5b9263547d --- /dev/null +++ b/changelog/1135.feature.rst @@ -0,0 +1 @@ +Support Python 3.12. diff --git a/pyproject.toml b/pyproject.toml index 73f3ad1b9f..c103f1ee4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Internet", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules",