Repository: hikari-py/hikari
Pull request #1357 (2022-11-13 09:30:41)

Add Python 3.12 support
one day:tm:

(This will fail, just opening this as a place to track the compatibility issues)

davfsa: Readme :)
davfsa: Re-trigger CI

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7cf52380..88b80e87 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -17,7 +17,7 @@ jobs:
       fail-fast: false
       matrix:
         os: [ubuntu-latest, macos-latest, windows-latest]
-        python-version: [3.8, 3.9, "3.10", 3.11]
+        python-version: [3.8, 3.9, "3.10", 3.11, 3.12]
 
     runs-on: ${{ matrix.os }}
 
diff --git a/README.md b/README.md
index 1ad27581..98645aeb 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ Gateway APIs.
 Built on good intentions and the hope that it will be extendable and reusable, rather than an obstacle for future
 development.
 
-Python 3.8, 3.9, 3.10 and 3.11 are currently supported.
+Python 3.8, 3.9, 3.10, 3.11 and 3.12 are currently supported.
 
 ## Installation
 
diff --git a/changes/1357.feature.md b/changes/1357.feature.md
new file mode 100644
index 00000000..0593ed25
--- /dev/null
+++ b/changes/1357.feature.md
@@ -0,0 +1,1 @@
+Add Python 3.12 support.
diff --git a/changes/1740.feature.md b/changes/1740.feature.md
new file mode 100644
index 00000000..5f6ea640
--- /dev/null
+++ b/changes/1740.feature.md
@@ -0,0 +1,2 @@
+Add missing `clear_x` methods to `InteractionMessageBuilder`
+- This brings the functionality more in-line with other message edit APIs
diff --git a/changes/1744.bugfix.md b/changes/1744.bugfix.md
new file mode 100644
index 00000000..3596046f
--- /dev/null
+++ b/changes/1744.bugfix.md
@@ -0,0 +1,1 @@
+Ensure shard connect and disconnect always get sent in pairs and properly waited for
diff --git a/changes/1745.bugfix.md b/changes/1745.bugfix.md
new file mode 100644
index 00000000..36a7ee3d
--- /dev/null
+++ b/changes/1745.bugfix.md
@@ -0,0 +1,3 @@
+Improve handing of force exiting a bot (double interrupt)
+- Improve exception message
+- Reset signal handlers to original ones after no longer capturing signals
diff --git a/dev-requirements/build.txt b/dev-requirements/build.txt
index 5e743662..34318c8c 100644
--- a/dev-requirements/build.txt
+++ b/dev-requirements/build.txt
@@ -1,2 +1,2 @@
 setuptools>=65.2.0
-wheel==0.41.2
+wheel==0.41.3
diff --git a/dev-requirements/flake8.txt b/dev-requirements/flake8.txt
index 54cecdf0..b86a5bd3 100644
--- a/dev-requirements/flake8.txt
+++ b/dev-requirements/flake8.txt
@@ -5,7 +5,7 @@ flake8==6.1.0
 
 flake8-bandit~=4.1.1                    # runs bandit
 flake8-black==0.3.6                     # runs black
-flake8-builtins==2.1.0                  # builtin shadowing checks
+flake8-builtins==2.2.0                  # builtin shadowing checks
 flake8-coding==1.3.2                    # coding magic-comment detection
 flake8-comprehensions==3.14.0           # comprehension checks
 flake8-docstrings==1.7.0                # pydocstyle support
@@ -13,9 +13,9 @@ flake8-executable==2.1.3                # shebangs
 flake8-fixme==1.1.1                     # "fix me" counter
 flake8-functions==0.0.8                 # function linting
 flake8-html==0.4.3                      # html output
-flake8-isort==6.1.0                     # runs isort
+flake8-isort==6.1.1                     # runs isort
 flake8-mutable==1.2.0                   # mutable default argument detection
-flake8-pep3101==2.0.0                   # new-style format strings only
+flake8-pep3101==2.1.0                   # new-style format strings only
 flake8-print==5.0.0                     # complain about print statements in code
 flake8-printf-formatting==1.1.2         # forbey printf-style python2 string formatting
 flake8-pytest-style==1.7.2              # pytest checks
diff --git a/dev-requirements/formatting.txt b/dev-requirements/formatting.txt
index 996ed1b9..1176c7b7 100644
--- a/dev-requirements/formatting.txt
+++ b/dev-requirements/formatting.txt
@@ -1,2 +1,2 @@
-black==23.10.1
+black==23.11.0
 isort==5.12.0
diff --git a/dev-requirements/pyright.txt b/dev-requirements/pyright.txt
index f3a3b12b..6f43e36b 100644
--- a/dev-requirements/pyright.txt
+++ b/dev-requirements/pyright.txt
@@ -1,1 +1,1 @@
-pyright==1.1.333
+pyright==1.1.336
diff --git a/dev-requirements/slotscheck.txt b/dev-requirements/slotscheck.txt
index ea1c50f9..084deacb 100644
--- a/dev-requirements/slotscheck.txt
+++ b/dev-requirements/slotscheck.txt
@@ -1,1 +1,1 @@
-slotscheck==0.17.0
+slotscheck==0.17.1
diff --git a/dev-requirements/sphinx.txt b/dev-requirements/sphinx.txt
index 48d9b407..2be21400 100644
--- a/dev-requirements/sphinx.txt
+++ b/dev-requirements/sphinx.txt
@@ -11,6 +11,6 @@ numpydoc==1.6.0
 myst-parser==2.0.0
 
 # Misc extensions
-sphinxext.opengraph==0.8.2
+sphinxext.opengraph==0.9.0
 sphinx-copybutton==0.5.2
-sphinxcontrib-towncrier==0.3.2a0
+sphinxcontrib-towncrier==0.4.0a0
diff --git a/dev-requirements/towncrier.txt b/dev-requirements/towncrier.txt
index 65670dc4..409f3a34 100644
--- a/dev-requirements/towncrier.txt
+++ b/dev-requirements/towncrier.txt
@@ -1,1 +1,1 @@
-towncrier==23.10.0
+towncrier==23.11.0
diff --git a/hikari/api/special_endpoints.py b/hikari/api/special_endpoints.py
index 113c0a5e..a33f4462 100644
--- a/hikari/api/special_endpoints.py
+++ b/hikari/api/special_endpoints.py
@@ -683,19 +683,19 @@ def attachments(self) -> undefined.UndefinedNoneOr[typing.Sequence[files.Resourc
 
     @property
     @abc.abstractmethod
-    def components(self) -> undefined.UndefinedOr[typing.Sequence[ComponentBuilder]]:
+    def components(self) -> undefined.UndefinedNoneOr[typing.Sequence[ComponentBuilder]]:
         """Sequence of up to 5 component builders to send in this response."""
 
     @property
     @abc.abstractmethod
-    def embeds(self) -> undefined.UndefinedOr[typing.Sequence[embeds_.Embed]]:
+    def embeds(self) -> undefined.UndefinedNoneOr[typing.Sequence[embeds_.Embed]]:
         """Sequence of up to 10 of the embeds included in this response."""
 
     # Settable fields
 
     @property
     @abc.abstractmethod
-    def content(self) -> undefined.UndefinedOr[str]:
+    def content(self) -> undefined.UndefinedNoneOr[str]:
         """Response's message content."""
 
     @property
diff --git a/hikari/errors.py b/hikari/errors.py
index 1bdacaf7..aa33d3d4 100644
--- a/hikari/errors.py
+++ b/hikari/errors.py
@@ -102,6 +102,9 @@ class HikariInterrupt(KeyboardInterrupt, HikariError):
     signame: str = attrs.field()
     """The signal name that was raised."""
 
+    def __str__(self) -> str:
+        return f"Signal {self.signum} ({self.signame}) received"
+
 
 @attrs.define(auto_exc=True, repr=False, slots=False)
 class ComponentStateConflictError(HikariError):
diff --git a/hikari/events/shard_events.py b/hikari/events/shard_events.py
index e427ac53..4c79024d 100644
--- a/hikari/events/shard_events.py
+++ b/hikari/events/shard_events.py
@@ -100,7 +100,7 @@ class ShardStateEvent(ShardEvent, abc.ABC):
 @attrs_extensions.with_copy
 @attrs.define(kw_only=True, weakref_slot=False)
 class ShardConnectedEvent(ShardStateEvent):
-    """Event fired when a shard connects."""
+    """Event fired when a shard successfully connects."""
 
     app: traits.RESTAware = attrs.field(metadata={attrs_extensions.SKIP_DEEP_COPY: True})
     # <<inherited docstring from Event>>.
diff --git a/hikari/impl/gateway_bot.py b/hikari/impl/gateway_bot.py
index debe57bb..6ff48a1c 100644
--- a/hikari/impl/gateway_bot.py
+++ b/hikari/impl/gateway_bot.py
@@ -805,10 +805,10 @@ def run(
             except AttributeError:
                 _LOGGER.log(ux.TRACE, "cannot set coroutine tracking depth for sys, no functionality exists for this")
 
-        try:
-            with signals.handle_interrupts(
-                enabled=enable_signal_handlers, loop=loop, propagate_interrupts=propagate_interrupts
-            ):
+        with signals.handle_interrupts(
+            enabled=enable_signal_handlers, loop=loop, propagate_interrupts=propagate_interrupts
+        ):
+            try:
                 loop.run_until_complete(
                     self.start(
                         activity=activity,
@@ -825,22 +825,27 @@ def run(
 
                 loop.run_until_complete(self.join())
 
-        finally:
-            if self._closing_event:
-                if self._closing_event.is_set():
-                    loop.run_until_complete(self._closing_event.wait())
-                else:
-                    loop.run_until_complete(self.close())
+            finally:
+                try:
+                    if self._closing_event:
+                        if self._closing_event.is_set():
+                            loop.run_until_complete(self._closing_event.wait())
+                        else:
+                            loop.run_until_complete(self.close())
+
+                    if close_passed_executor and self._executor is not None:
+                        _LOGGER.debug("shutting down executor %s", self._executor)
+                        self._executor.shutdown(wait=True)
+                        self._executor = None
 
-            if close_passed_executor and self._executor is not None:
-                _LOGGER.debug("shutting down executor %s", self._executor)
-                self._executor.shutdown(wait=True)
-                self._executor = None
+                    if close_loop:
+                        aio.destroy_loop(loop, _LOGGER)
 
-            if close_loop:
-                aio.destroy_loop(loop, _LOGGER)
+                    _LOGGER.info("successfully terminated")
 
-            _LOGGER.info("successfully terminated")
+                except errors.HikariInterrupt:
+                    _LOGGER.warning("forcefully terminated")
+                    raise
 
     async def start(
         self,
diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py
index fbf8ae43..a24ef7bf 100644
--- a/hikari/impl/rest.py
+++ b/hikari/impl/rest.py
@@ -731,7 +731,7 @@ async def _request(
 
         await aio.first_completed(request_task, self._close_event.wait())
 
-        if not self._close_event.is_set():
+        if not request_task.cancelled():
             return request_task.result()
 
         raise errors.ComponentStateConflictError("The REST client was closed mid-request")
diff --git a/hikari/impl/rest_bot.py b/hikari/impl/rest_bot.py
index 0613d80c..eeb10f93 100644
--- a/hikari/impl/rest_bot.py
+++ b/hikari/impl/rest_bot.py
@@ -568,10 +568,10 @@ def run(
             except AttributeError:
                 _LOGGER.log(ux.TRACE, "cannot set coroutine tracking depth for sys, no functionality exists for this")
 
-        try:
-            with signals.handle_interrupts(
-                enabled=enable_signal_handlers, loop=loop, propagate_interrupts=propagate_interrupts
-            ):
+        with signals.handle_interrupts(
+            enabled=enable_signal_handlers, loop=loop, propagate_interrupts=propagate_interrupts
+        ):
+            try:
                 loop.run_until_complete(
                     self.start(
                         backlog=backlog,
@@ -588,22 +588,27 @@ def run(
                 )
                 loop.run_until_complete(self.join())
 
-        finally:
-            if self._close_event:
-                if self._is_closing:
-                    loop.run_until_complete(self._close_event.wait())
-                else:
-                    loop.run_until_complete(self.close())
+            finally:
+                try:
+                    if self._close_event:
+                        if self._is_closing:
+                            loop.run_until_complete(self._close_event.wait())
+                        else:
+                            loop.run_until_complete(self.close())
+
+                    if close_passed_executor and self._executor:
+                        _LOGGER.debug("shutting down executor %s", self._executor)
+                        self._executor.shutdown(wait=True)
+                        self._executor = None
 
-            if close_passed_executor and self._executor:
-                _LOGGER.debug("shutting down executor %s", self._executor)
-                self._executor.shutdown(wait=True)
-                self._executor = None
+                    if close_loop:
+                        aio.destroy_loop(loop, _LOGGER)
 
-            if close_loop:
-                aio.destroy_loop(loop, _LOGGER)
+                    _LOGGER.info("successfully terminated")
 
-            _LOGGER.info("successfully terminated")
+                except errors.HikariInterrupt:
+                    _LOGGER.warning("forcefully terminated")
+                    raise
 
     async def start(
         self,
diff --git a/hikari/impl/shard.py b/hikari/impl/shard.py
index 5738941c..48f3b9b4 100644
--- a/hikari/impl/shard.py
+++ b/hikari/impl/shard.py
@@ -219,7 +219,8 @@ def _handle_other_message(self, message: aiohttp.WSMessage, /) -> typing.NoRetur
             close_code = int(message.data)
 
             can_reconnect = close_code < 4000 or close_code in _RECONNECTABLE_CLOSE_CODES
-            raise errors.GatewayServerClosedConnectionError(message.extra, close_code, can_reconnect)
+            # str(message.extra) is used to cast the possible None to a string
+            raise errors.GatewayServerClosedConnectionError(str(message.extra), close_code, can_reconnect)
 
         if message.type == aiohttp.WSMsgType.CLOSING or message.type == aiohttp.WSMsgType.CLOSED:
             # May be caused by the server shutting us down.
@@ -818,7 +819,6 @@ async def _connect(self) -> typing.Tuple[asyncio.Task[None], ...]:
             dumps=self._dumps,
             url=url,
         )
-        self._event_manager.dispatch(self._event_factory.deserialize_connected_event(self))
 
         # Expect initial HELLO
         hello_payload = await self._ws.receive_json()
@@ -893,6 +893,7 @@ async def _keep_alive(self) -> None:
                 if not self._handshake_event.is_set():
                     continue
 
+                await self._event_manager.dispatch(self._event_factory.deserialize_connected_event(self))
                 await aio.first_completed(*lifetime_tasks)
 
                 # Since nothing went wrong, we can reset the backoff and try again
@@ -957,7 +958,9 @@ async def _keep_alive(self) -> None:
                     else:
                         await ws.send_close(code=_RESUME_CLOSE_CODE, message=b"shard disconnecting temporarily")
 
-                    self._event_manager.dispatch(self._event_factory.deserialize_disconnected_event(self))
+                    if self._handshake_event.is_set():
+                        # We dispatched the connected event, so we can dispatch the disconnected one too
+                        await self._event_manager.dispatch(self._event_factory.deserialize_disconnected_event(self))
 
     def _serialize_and_store_presence_payload(
         self,
diff --git a/hikari/impl/special_endpoints.py b/hikari/impl/special_endpoints.py
index c2a3eb99..4ac7df60 100644
--- a/hikari/impl/special_endpoints.py
+++ b/hikari/impl/special_endpoints.py
@@ -1011,7 +1011,7 @@ class InteractionMessageBuilder(special_endpoints.InteractionMessageBuilder):
     )
 
     # Not-required arguments.
-    _content: undefined.UndefinedOr[str] = attrs.field(alias="content", default=undefined.UNDEFINED)
+    _content: undefined.UndefinedNoneOr[str] = attrs.field(alias="content", default=undefined.UNDEFINED)
 
     # Key-word only not-required arguments.
     _flags: typing.Union[int, messages.MessageFlag, undefined.UndefinedType] = attrs.field(
@@ -1030,10 +1030,10 @@ class InteractionMessageBuilder(special_endpoints.InteractionMessageBuilder):
     _attachments: undefined.UndefinedNoneOr[typing.List[files.Resourceish]] = attrs.field(
         alias="attachments", default=undefined.UNDEFINED, kw_only=True
     )
-    _components: undefined.UndefinedOr[typing.List[special_endpoints.ComponentBuilder]] = attrs.field(
+    _components: undefined.UndefinedNoneOr[typing.List[special_endpoints.ComponentBuilder]] = attrs.field(
         alias="components", default=undefined.UNDEFINED, kw_only=True
     )
-    _embeds: undefined.UndefinedOr[typing.List[embeds_.Embed]] = attrs.field(
+    _embeds: undefined.UndefinedNoneOr[typing.List[embeds_.Embed]] = attrs.field(
         alias="embeds", default=undefined.UNDEFINED, kw_only=True
     )
 
@@ -1042,16 +1042,16 @@ def attachments(self) -> undefined.UndefinedNoneOr[typing.Sequence[files.Resourc
         return self._attachments.copy() if self._attachments else self._attachments
 
     @property
-    def content(self) -> undefined.UndefinedOr[str]:
+    def content(self) -> undefined.UndefinedNoneOr[str]:
         return self._content
 
     @property
-    def components(self) -> undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]]:
-        return self._components.copy() if self._components is not undefined.UNDEFINED else undefined.UNDEFINED
+    def components(self) -> undefined.UndefinedNoneOr[typing.Sequence[special_endpoints.ComponentBuilder]]:
+        return self._components.copy() if self._components else self._components
 
     @property
-    def embeds(self) -> undefined.UndefinedOr[typing.Sequence[embeds_.Embed]]:
-        return self._embeds.copy() if self._embeds is not undefined.UNDEFINED else undefined.UNDEFINED
+    def embeds(self) -> undefined.UndefinedNoneOr[typing.Sequence[embeds_.Embed]]:
+        return self._embeds.copy() if self._embeds else self._embeds
 
     @property
     def flags(self) -> typing.Union[undefined.UndefinedType, int, messages.MessageFlag]:
@@ -1081,10 +1081,6 @@ def user_mentions(
     ) -> undefined.UndefinedOr[typing.Union[snowflakes.SnowflakeishSequence[users.PartialUser], bool]]:
         return self._user_mentions
 
-    def clear_attachments(self, /) -> Self:
-        self._attachments = None
-        return self
-
     def add_attachment(self, attachment: files.Resourceish, /) -> Self:
         if not self._attachments:
             self._attachments = []
@@ -1092,24 +1088,40 @@ def add_attachment(self, attachment: files.Resourceish, /) -> Self:
         self._attachments.append(attachment)
         return self
 
+    def clear_attachments(self, /) -> Self:
+        self._attachments = None
+        return self
+
     def add_component(self, component: special_endpoints.ComponentBuilder, /) -> Self:
-        if self._components is undefined.UNDEFINED:
+        if not self._components:
             self._components = []
 
         self._components.append(component)
         return self
 
+    def clear_components(self, /) -> Self:
+        self._components = None
+        return self
+
     def add_embed(self, embed: embeds_.Embed, /) -> Self:
-        if self._embeds is undefined.UNDEFINED:
+        if not self._embeds:
             self._embeds = []
 
         self._embeds.append(embed)
         return self
 
+    def clear_embeds(self, /) -> Self:
+        self._embeds = None
+        return self
+
     def set_content(self, content: undefined.UndefinedOr[str], /) -> Self:
         self._content = str(content) if content is not undefined.UNDEFINED else undefined.UNDEFINED
         return self
 
+    def clear_content(self, /) -> Self:
+        self._content = None
+        return self
+
     def set_flags(self, flags: typing.Union[undefined.UndefinedType, int, messages.MessageFlag], /) -> Self:
         self._flags = flags
         return self
@@ -1165,15 +1177,21 @@ def build(
         elif self._attachments is None:
             data.put("attachments", None)
 
-        if self._embeds is not undefined.UNDEFINED:
+        if self._embeds:
             embeds: typing.List[data_binding.JSONObject] = []
             for embed, attachments in map(entity_factory.serialize_embed, self._embeds):
                 final_attachments.extend(attachments)
                 embeds.append(embed)
 
             data["embeds"] = embeds
+        elif self._embeds is None:
+            data.put("embeds", None)
+
+        if self._components:
+            data.put_array("components", self._components, conversion=lambda component: component.build())
+        elif self._components is None:
+            data.put("components", None)
 
-        data.put_array("components", self._components, conversion=lambda component: component.build())
         data.put("flags", self.flags)
         data.put("tts", self.is_tts)
 
diff --git a/hikari/internal/aio.py b/hikari/internal/aio.py
index 489e300b..3b7b75e6 100644
--- a/hikari/internal/aio.py
+++ b/hikari/internal/aio.py
@@ -259,7 +259,7 @@ async def murder(future: asyncio.Future[typing.Any]) -> None:
     remaining_tasks = tuple(t for t in asyncio.all_tasks(loop) if not t.done())
 
     if remaining_tasks:
-        logger.debug("terminating %s remaining tasks forcefully", len(remaining_tasks))
+        logger.warning("terminating %s remaining tasks forcefully", len(remaining_tasks))
         loop.run_until_complete(_gather((murder(task) for task in remaining_tasks)))
     else:
         logger.debug("No remaining tasks exist, good job!")
diff --git a/hikari/internal/fast_protocol.py b/hikari/internal/fast_protocol.py
index a3135780..2e9f3b6f 100644
--- a/hikari/internal/fast_protocol.py
+++ b/hikari/internal/fast_protocol.py
@@ -32,7 +32,7 @@
     from typing_extensions import Self
 
 _Protocol: FastProtocolChecking = NotImplemented
-_IGNORED_ATTRS = typing.EXCLUDED_ATTRIBUTES + ["__qualname__", "__slots__"]
+_IGNORED_ATTRS = frozenset(typing.EXCLUDED_ATTRIBUTES) | {"__qualname__", "__slots__"}
 
 
 def _check_if_ignored(name: str) -> bool:
diff --git a/hikari/internal/signals.py b/hikari/internal/signals.py
index 48220c74..11dcfdca 100644
--- a/hikari/internal/signals.py
+++ b/hikari/internal/signals.py
@@ -38,6 +38,9 @@
 from hikari import errors
 from hikari.internal import ux
 
+if typing.TYPE_CHECKING:
+    _SignalHandlerT = typing.Callable[[int, typing.Optional[types.FrameType]], None]
+
 _INTERRUPT_SIGNALS: typing.Tuple[str, ...] = ("SIGINT", "SIGTERM")
 _LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari.signals")
 
@@ -49,9 +52,7 @@ def _raise_interrupt(signum: int) -> typing.NoReturn:
     raise errors.HikariInterrupt(signum, signame)
 
 
-def _interrupt_handler(
-    loop: asyncio.AbstractEventLoop,
-) -> typing.Callable[[int, typing.Optional[types.FrameType]], None]:
+def _interrupt_handler(loop: asyncio.AbstractEventLoop) -> _SignalHandlerT:
     loop_thread_id = threading.get_native_id()
 
     def handler(signum: int, frame: typing.Optional[types.FrameType]) -> None:
@@ -102,13 +103,16 @@ def handle_interrupts(
         return
 
     interrupt_handler = _interrupt_handler(loop)
+    original_handlers: typing.Dict[int, typing.Union[int, _SignalHandlerT, None]] = {}
 
     for sig in _INTERRUPT_SIGNALS:
         try:
             signum = getattr(signal, sig)
-            signal.signal(signum, interrupt_handler)
         except AttributeError:
             _LOGGER.log(ux.TRACE, "signal %s is not implemented on your platform; skipping", sig)
+        else:
+            original_handlers[signum] = signal.getsignal(signum)
+            signal.signal(signum, interrupt_handler)
 
     try:
         yield
@@ -118,10 +122,5 @@ def handle_interrupts(
             raise
 
     finally:
-        for sig in _INTERRUPT_SIGNALS:
-            try:
-                signum = getattr(signal, sig)
-                signal.signal(signum, signal.SIG_DFL)
-            except AttributeError:
-                # Signal not implemented. We already logged this earlier.
-                pass
+        for signum, handler in original_handlers.items():
+            signal.signal(signum, handler)
diff --git a/requirements.txt b/requirements.txt
index 1526161d..44ae1bfe 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
-aiohttp~=3.8
+aiohttp~=3.9
 attrs~=23.1
 colorlog~=6.7
 multidict~=6.0
diff --git a/setup.py b/setup.py
index 6ec507f6..e70e11c8 100644
--- a/setup.py
+++ b/setup.py
@@ -65,7 +65,7 @@ def parse_requirements_file(path):
     maintainer_email=metadata.email,
     license=metadata.license,
     url=metadata.url,
-    python_requires=">=3.8.0,<3.12",
+    python_requires=">=3.8.0,<3.13",
     packages=setuptools.find_namespace_packages(include=["hikari*"]),
     entry_points={"console_scripts": ["hikari = hikari.cli:main"]},
     install_requires=parse_requirements_file("requirements.txt"),
@@ -95,6 +95,7 @@ def parse_requirements_file(path):
         "Programming Language :: Python :: 3.9",
         "Programming Language :: Python :: 3.10",
         "Programming Language :: Python :: 3.11",
+        "Programming Language :: Python :: 3.12",
         "Programming Language :: Python :: Implementation :: CPython",
         "Topic :: Communications :: Chat",
         "Topic :: Internet :: WWW/HTTP",


Test patch:
diff --git a/tests/hikari/impl/test_event_manager_base.py b/tests/hikari/impl/test_event_manager_base.py
index ebba1c2e..89935f66 100644
--- a/tests/hikari/impl/test_event_manager_base.py
+++ b/tests/hikari/impl/test_event_manager_base.py
@@ -572,6 +572,10 @@ async def test_handle_dispatch_ignores_cancelled_errors(self, event_manager, eve
 
     @pytest.mark.asyncio()
     async def test_handle_dispatch_handles_exceptions(self, event_manager, event_loop):
+        mock_task = mock.Mock()
+        # On Python 3.12+ Asyncio uses this to get the task's context if set to call the
+        # error handler in. We want to avoid for this test for simplicity.
+        mock_task.get_context.return_value = None
         event_manager._enabled_for_consumer = mock.Mock(return_value=True)
         exc = Exception("aaaa!")
         consumer = mock.Mock(callback=mock.AsyncMock(side_effect=exc))
@@ -580,12 +584,12 @@ async def test_handle_dispatch_handles_exceptions(self, event_manager, event_loo
         shard = object()
         pl = {"i like": "cats"}
 
-        with mock.patch.object(asyncio, "current_task") as current_task:
+        with mock.patch.object(asyncio, "current_task", return_value=mock_task):
             await event_manager._handle_dispatch(consumer, shard, pl)
 
         error_handler.assert_called_once_with(
             event_loop,
-            {"exception": exc, "message": "Exception occurred in raw event dispatch conduit", "task": current_task()},
+            {"exception": exc, "message": "Exception occurred in raw event dispatch conduit", "task": mock_task},
         )
 
     @pytest.mark.asyncio()
diff --git a/tests/hikari/impl/test_shard.py b/tests/hikari/impl/test_shard.py
index 5b10ab1b..af1bc377 100644
--- a/tests/hikari/impl/test_shard.py
+++ b/tests/hikari/impl/test_shard.py
@@ -1014,10 +1014,6 @@ async def test__connect_when_not_reconnecting(self, client, http_settings, proxy
             dumps=client._dumps,
             url="wss://somewhere.com?somewhere=true&v=400&encoding=json",
         )
-        client._event_factory.deserialize_connected_event.assert_called_once_with(client)
-        client._event_manager.dispatch.assert_called_once_with(
-            client._event_factory.deserialize_connected_event.return_value
-        )
 
         assert create_task.call_count == 2
         create_task.assert_has_calls(
@@ -1103,10 +1099,6 @@ async def test__connect_when_reconnecting(self, client, http_settings, proxy_set
             transport_compression=True,
             url="wss://notsomewhere.com?somewhere=true&v=400&encoding=json&compress=zlib-stream",
         )
-        client._event_factory.deserialize_connected_event.assert_called_once_with(client)
-        client._event_manager.dispatch.assert_called_once_with(
-            client._event_factory.deserialize_connected_event.return_value
-        )
 
         assert create_task.call_count == 2
         create_task.assert_has_calls(
diff --git a/tests/hikari/impl/test_special_endpoints.py b/tests/hikari/impl/test_special_endpoints.py
index 836eb1fb..a046504c 100644
--- a/tests/hikari/impl/test_special_endpoints.py
+++ b/tests/hikari/impl/test_special_endpoints.py
@@ -927,10 +927,7 @@ def test_build_for_partial_when_empty_lists(self):
         result, attachments = builder.build(mock_entity_factory)
 
         mock_entity_factory.serialize_embed.assert_not_called()
-        assert result == {
-            "type": base_interactions.ResponseType.MESSAGE_UPDATE,
-            "data": {"components": [], "embeds": []},
-        }
+        assert result == {"type": base_interactions.ResponseType.MESSAGE_UPDATE, "data": {}}
         assert attachments == []
 
     def test_build_handles_attachments(self):
diff --git a/tests/hikari/internal/test_signals.py b/tests/hikari/internal/test_signals.py
index e32a911b..23a331c7 100644
--- a/tests/hikari/internal/test_signals.py
+++ b/tests/hikari/internal/test_signals.py
@@ -70,7 +70,9 @@ def test_behaviour(self):
                 register_signal_handler.reset_mock()
 
         assert register_signal_handler.call_count == 2
-        register_signal_handler.assert_has_calls([mock.call(2, signal.SIG_DFL), mock.call(15, signal.SIG_DFL)])
+        register_signal_handler.assert_has_calls(
+            [mock.call(2, signal.default_int_handler), mock.call(15, signal.SIG_DFL)]
+        )
 
     def test_when_disabled(self):
         with mock.patch.object(signal, "signal") as register_signal_handler: