Repository: hikari-py/hikari
Pull request #1740 (2023-10-29 18:20:46)

Add missing clear methods for InteractionMessageBuilder
### Checklist
<!-- Make sure to tick all the following boxes by putting an `x` in between (like this `[x]`) -->
- [x] I have run `nox` and all the pipelines have passed.
- [ ] I have made unittests according to the code I have added/modified/deleted.



Patch:
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/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/sphinx.txt b/dev-requirements/sphinx.txt
index 48d9b407..135ce3d7 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
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/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/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/shard.py b/hikari/impl/shard.py
index 5738941c..7bbe4241 100644
--- a/hikari/impl/shard.py
+++ b/hikari/impl/shard.py
@@ -818,7 +818,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 +892,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 +957,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)
 


Test patch:
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):