Repository: hikari-py/hikari
Pull request #2353 (2025-05-05 09:33:16)

Add `old_thread` field to `GuildThreadUpdateEvent`
### Summary
Add `old_thread` to `GuildThreadUpdateEvent` to match the behavior of `old_channel` and `GuildChannelUpdateEvent`.

### 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.
- [x] I have made unittests according to the code I have added/modified/deleted.



Patch:
diff --git a/changes/2353.feature.md b/changes/2353.feature.md
new file mode 100644
index 00000000..9938b670
--- /dev/null
+++ b/changes/2353.feature.md
@@ -0,0 +1,1 @@
+Add `old_thread` field to `GuildThreadUpdateEvent`.
diff --git a/hikari/api/event_factory.py b/hikari/api/event_factory.py
index 87460e65..3a271d15 100644
--- a/hikari/api/event_factory.py
+++ b/hikari/api/event_factory.py
@@ -214,7 +214,11 @@ def deserialize_guild_thread_access_event(
 
     @abc.abstractmethod
     def deserialize_guild_thread_update_event(
-        self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
+        self,
+        shard: gateway_shard.GatewayShard,
+        payload: data_binding.JSONObject,
+        *,
+        old_thread: channel_models.GuildThreadChannel | None = None,
     ) -> channel_events.GuildThreadUpdateEvent:
         """Parse a raw payload from Discord into a guild thread update event object.
 
@@ -224,6 +228,8 @@ def deserialize_guild_thread_update_event(
             The shard that emitted this event.
         payload
             The dict payload to parse.
+        old_thread
+            The guild thread channel object or [`None`][].
 
         Returns
         -------
diff --git a/hikari/events/channel_events.py b/hikari/events/channel_events.py
index bd0432df..1efa07c7 100644
--- a/hikari/events/channel_events.py
+++ b/hikari/events/channel_events.py
@@ -811,6 +811,12 @@ class GuildThreadUpdateEvent(GuildThreadEvent):
     shard: gateway_shard.GatewayShard = attrs.field(metadata={attrs_extensions.SKIP_DEEP_COPY: True})
     # <<inherited docstring from ShardEvent>>.
 
+    old_thread: channels.GuildThreadChannel | None = attrs.field(repr=True)
+    """The old thread object.
+
+    This will be [`None`][] if the thread is missing from the cache.
+    """
+
     thread: channels.GuildThreadChannel = attrs.field()
     """The thread that was updated."""
 
diff --git a/hikari/impl/event_factory.py b/hikari/impl/event_factory.py
index dc1c5cfb..201865c3 100644
--- a/hikari/impl/event_factory.py
+++ b/hikari/impl/event_factory.py
@@ -180,10 +180,14 @@ def deserialize_guild_thread_access_event(
 
     @typing_extensions.override
     def deserialize_guild_thread_update_event(
-        self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
+        self,
+        shard: gateway_shard.GatewayShard,
+        payload: data_binding.JSONObject,
+        *,
+        old_thread: channel_models.GuildThreadChannel | None = None,
     ) -> channel_events.GuildThreadUpdateEvent:
         channel = self._app.entity_factory.deserialize_guild_thread(payload)
-        return channel_events.GuildThreadUpdateEvent(shard=shard, thread=channel)
+        return channel_events.GuildThreadUpdateEvent(shard=shard, thread=channel, old_thread=old_thread)
 
     @typing_extensions.override
     def deserialize_guild_thread_delete_event(
diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py
index 00c9958b..350064c4 100644
--- a/hikari/impl/event_manager.py
+++ b/hikari/impl/event_manager.py
@@ -207,7 +207,8 @@ async def on_thread_create(self, shard: gateway_shard.GatewayShard, payload: dat
     @event_manager_base.filtered(channel_events.GuildThreadUpdateEvent, config.CacheComponents.GUILD_THREADS)
     async def on_thread_update(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
         """See https://discord.com/developers/docs/topics/gateway-events#thread-update for more info."""
-        event = self._event_factory.deserialize_guild_thread_update_event(shard, payload)
+        old = self._cache.get_thread(snowflakes.Snowflake(payload["id"])) if self._cache else None
+        event = self._event_factory.deserialize_guild_thread_update_event(shard, payload, old_thread=old)
 
         if self._cache:
             self._cache.update_thread(event.thread)


Test patch:
diff --git a/tests/hikari/events/test_channel_events.py b/tests/hikari/events/test_channel_events.py
index 191d6853..1a490e14 100644
--- a/tests/hikari/events/test_channel_events.py
+++ b/tests/hikari/events/test_channel_events.py
@@ -264,7 +264,7 @@ def test_thread_id_property(self, event: channel_events.GuildThreadCreateEvent):
 class TestGuildThreadUpdateEvent:
     @pytest.fixture
     def event(self) -> channel_events.GuildThreadUpdateEvent:
-        return channel_events.GuildThreadUpdateEvent(shard=mock.Mock(), thread=mock.Mock())
+        return channel_events.GuildThreadUpdateEvent(shard=mock.Mock(), thread=mock.Mock(), old_thread=mock.Mock())
 
     def test_app_property(self, event: channel_events.GuildThreadUpdateEvent):
         assert event.app is event.thread.app
diff --git a/tests/hikari/impl/test_event_factory.py b/tests/hikari/impl/test_event_factory.py
index 38129023..fa262dfa 100644
--- a/tests/hikari/impl/test_event_factory.py
+++ b/tests/hikari/impl/test_event_factory.py
@@ -189,14 +189,18 @@ def test_deserialize_guild_thread_access_event(
     def test_deserialize_guild_thread_update_event(
         self, event_factory: event_factory_.EventFactoryImpl, mock_app: mock.Mock, mock_shard: shard.GatewayShard
     ):
+        mock_old_thread = object()
         mock_payload = mock.Mock()
 
-        event = event_factory.deserialize_guild_thread_update_event(mock_shard, mock_payload)
+        event = event_factory.deserialize_guild_thread_update_event(
+            mock_shard, mock_payload, old_thread=mock_old_thread
+        )
 
-        assert event.shard is mock_shard
-        assert event.thread is mock_app.entity_factory.deserialize_guild_thread.return_value
         mock_app.entity_factory.deserialize_guild_thread.assert_called_once_with(mock_payload)
         assert isinstance(event, channel_events.GuildThreadUpdateEvent)
+        assert event.shard is mock_shard
+        assert event.thread is mock_app.entity_factory.deserialize_guild_thread.return_value
+        assert event.old_thread is mock_old_thread
 
     def test_deserialize_guild_thread_delete_event(
         self, event_factory: event_factory_.EventFactoryImpl, mock_app: mock.Mock, mock_shard: shard.GatewayShard
diff --git a/tests/hikari/impl/test_event_manager.py b/tests/hikari/impl/test_event_manager.py
index e915f3c7..023fdc60 100644
--- a/tests/hikari/impl/test_event_manager.py
+++ b/tests/hikari/impl/test_event_manager.py
@@ -298,25 +298,34 @@ async def test_on_thread_create_for_access_stateless(
     async def test_on_thread_update_stateful(
         self, event_manager_impl: event_manager.EventManagerImpl, shard: mock.Mock, event_factory: mock.Mock
     ):
-        mock_payload = mock.Mock()
-        await event_manager_impl.on_thread_update(shard, mock_payload)
+        payload = {"id": 123}
+        old_thread = mock.Mock()
+        event = mock.Mock(thread=mock.Mock(channels.GuildThreadChannel))
+
+        event_factory.deserialize_guild_thread_update_event.return_value = event
+        event_manager_impl._cache.get_thread.return_value = old_thread
+
+        await event_manager_impl.on_thread_update(shard, payload)
 
-        event = event_factory.deserialize_guild_thread_update_event.return_value
+        event_manager_impl._cache.get_thread.assert_called_once_with(123)
         event_manager_impl._cache.update_thread.assert_called_once_with(event.thread)
+        event_factory.deserialize_guild_thread_update_event.assert_called_once_with(
+            shard, payload, old_thread=old_thread
+        )
         event_manager_impl.dispatch.assert_awaited_once_with(event)
-        event_factory.deserialize_guild_thread_update_event.assert_called_once_with(shard, mock_payload)
 
     @pytest.mark.asyncio
     async def test_on_thread_update_stateless(
         self, stateless_event_manager_impl: event_manager.EventManagerImpl, shard: mock.Mock, event_factory: mock.Mock
     ):
-        mock_payload = mock.Mock()
-        await stateless_event_manager_impl.on_thread_update(shard, mock_payload)
+        payload = {"id": 123}
+
+        await stateless_event_manager_impl.on_thread_update(shard, payload)
 
         stateless_event_manager_impl.dispatch.assert_awaited_once_with(
             event_factory.deserialize_guild_thread_update_event.return_value
         )
-        event_factory.deserialize_guild_thread_update_event.assert_called_once_with(shard, mock_payload)
+        event_factory.deserialize_guild_thread_update_event.assert_called_once_with(shard, payload, old_thread=None)
 
     @pytest.mark.asyncio
     async def test_on_thread_delete_stateful(
diff --git a/tests/hikari/test_invites.py b/tests/hikari/test_invites.py
index 4c7f9507..ae1f4ec1 100644
--- a/tests/hikari/test_invites.py
+++ b/tests/hikari/test_invites.py
@@ -94,12 +94,7 @@ def test_make_banner_url_format_set_to_deprecated_ext_argument_if_provided(self,
             assert model.make_banner_url(ext="JPEG") == "file"
 
         route.compile_to_file.assert_called_once_with(
-            urls.CDN_URL,
-            guild_id=123321,
-            hash="fofoof",
-            size=4096,
-            file_format="JPEG",
-            lossless=True,
+            urls.CDN_URL, guild_id=123321, hash="fofoof", size=4096, file_format="JPEG", lossless=True
         )
 
     def test_banner_url(self, model: invites.InviteGuild):
@@ -115,12 +110,7 @@ def test_make_banner_url_when_hash(self, model: invites.InviteGuild):
             assert model.make_banner_url(ext="url", size=512) == "file"
 
         route.compile_to_file.assert_called_once_with(
-            urls.CDN_URL,
-            guild_id=123321,
-            hash="fofoof",
-            size=512,
-            file_format="URL",
-            lossless=True,
+            urls.CDN_URL, guild_id=123321, hash="fofoof", size=512, file_format="URL", lossless=True
         )
 
     def test_make_banner_url_when_format_is_None_and_banner_hash_is_for_gif(self, model: invites.InviteGuild):
@@ -132,12 +122,7 @@ def test_make_banner_url_when_format_is_None_and_banner_hash_is_for_gif(self, mo
             assert model.make_banner_url(ext=None, size=4096) == "file"
 
         route.compile_to_file.assert_called_once_with(
-            urls.CDN_URL,
-            guild_id=model.id,
-            hash="a_18dnf8dfbakfdh",
-            size=4096,
-            file_format="GIF",
-            lossless=True,
+            urls.CDN_URL, guild_id=model.id, hash="a_18dnf8dfbakfdh", size=4096, file_format="GIF", lossless=True
         )
 
     def test_make_banner_url_when_format_is_None_and_banner_hash_is_not_for_gif(self, model: invites.InviteGuild):
@@ -149,12 +134,7 @@ def test_make_banner_url_when_format_is_None_and_banner_hash_is_not_for_gif(self
             assert model.make_banner_url(ext=None, size=4096) == "file"
 
         route.compile_to_file.assert_called_once_with(
-            urls.CDN_URL,
-            guild_id=model.id,
-            hash=model.banner_hash,
-            size=4096,
-            file_format="PNG",
-            lossless=True,
+            urls.CDN_URL, guild_id=model.id, hash=model.banner_hash, size=4096, file_format="PNG", lossless=True
         )
 
     def test_make_banner_url_when_no_hash(self, model: invites.InviteGuild):
diff --git a/tests/hikari/test_webhooks.py b/tests/hikari/test_webhooks.py
index e4e290ba..0bf0d576 100644
--- a/tests/hikari/test_webhooks.py
+++ b/tests/hikari/test_webhooks.py
@@ -229,12 +229,7 @@ def test_make_avatar_url_format_set_to_deprecated_ext_argument_if_provided(self,
             assert webhook.make_avatar_url(ext="JPEG") == "file"
 
         route.compile_to_file.assert_called_once_with(
-            urls.CDN_URL,
-            user_id=987654321,
-            hash="hook",
-            size=4096,
-            file_format="JPEG",
-            lossless=True,
+            urls.CDN_URL, user_id=987654321, hash="hook", size=4096, file_format="JPEG", lossless=True
         )
 
     def test_avatar_url_property(self, webhook):