Repository: hikari-py/hikari
Pull request #1572 (2023-04-02 01:52:31)

Fix sticker pack handling and typing
### Summary
<!-- Small summary of the merge request -->

### Checklist
<!-- Make sure to tick all the following boxes by putting an `x` in between (like this `[x]`) -->
- [ ] I have run `nox` and all the pipelines have passed.
- [ ] I have made unittests according to the code I have added/modified/deleted.

### Related issues
<!--
To mention an issue use `#issue-id` and to mention a merge request use `!merge-request-id`
To close/fix an issue use `Close #issue-id` or `Fix #issue-id` (depending on the merge request)
-->



Patch:
diff --git a/changes/1572.breaking.md b/changes/1572.breaking.md
new file mode 100644
index 00000000..97a442cb
--- /dev/null
+++ b/changes/1572.breaking.md
@@ -0,0 +1,1 @@
+Renamed `StickerPack.banner_hash` to `StickerPack.banner_asset_id`.
diff --git a/changes/1572.bugfix.md b/changes/1572.bugfix.md
new file mode 100644
index 00000000..c94172d3
--- /dev/null
+++ b/changes/1572.bugfix.md
@@ -0,0 +1,5 @@
+Fixed sticker pack handling and typing:
+
+* Fixed deserialization raising when `"banner_asset_id"` or `"cover_sticker_id"` weren't included in the payload.
+* `StickerPack.banner_asset_id` is now correctly typed as `Optional[Snowflake]`.
+* `StickerPack.banner_url` and `StickerPack.make_banner_url` both now correctly return `None` when `StickerPack.banner_asset_id` is `None`.
diff --git a/hikari/api/rest.py b/hikari/api/rest.py
index b2fb50ec..b6e4fc06 100644
--- a/hikari/api/rest.py
+++ b/hikari/api/rest.py
@@ -7292,6 +7292,7 @@ def interaction_autocomplete_builder(
         choices: typing.Union[
             typing.Sequence[commands.CommandChoice], typing.Sequence[special_endpoints.AutocompleteChoiceBuilder]
         ],
+        _stack_level: int = 0,
     ) -> special_endpoints.InteractionAutocompleteBuilder:
         """Create a builder for an autocomplete interaction response.
 
@@ -7674,6 +7675,7 @@ async def create_autocomplete_response(
         choices: typing.Union[
             typing.Sequence[commands.CommandChoice], typing.Sequence[special_endpoints.AutocompleteChoiceBuilder]
         ],
+        _stack_level: int = 0,
     ) -> None:
         """Create the initial response for an autocomplete interaction.
 
diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py
index ad952a1a..7d5b81bb 100644
--- a/hikari/impl/entity_factory.py
+++ b/hikari/impl/entity_factory.py
@@ -2759,14 +2759,22 @@ def deserialize_sticker_pack(self, payload: data_binding.JSONObject) -> sticker_
         for sticker_payload in payload["stickers"]:
             pack_stickers.append(self.deserialize_standard_sticker(sticker_payload))
 
+        cover_sticker_id: typing.Optional[snowflakes.Snowflake] = None
+        if raw_cover_sticker_id := payload.get("cover_sticker_id"):
+            cover_sticker_id = snowflakes.Snowflake(raw_cover_sticker_id)
+
+        banner_asset_id: typing.Optional[snowflakes.Snowflake] = None
+        if raw_banner_asset_id := payload.get("banner_asset_id"):
+            banner_asset_id = snowflakes.Snowflake(raw_banner_asset_id)
+
         return sticker_models.StickerPack(
             id=snowflakes.Snowflake(payload["id"]),
             name=payload["name"],
             description=payload["description"],
-            cover_sticker_id=snowflakes.Snowflake(payload["cover_sticker_id"]),
+            cover_sticker_id=cover_sticker_id,
             stickers=pack_stickers,
             sku_id=snowflakes.Snowflake(payload["sku_id"]),
-            banner_hash=payload["banner_asset_id"],
+            banner_asset_id=banner_asset_id,
         )
 
     def deserialize_partial_sticker(self, payload: data_binding.JSONObject) -> sticker_models.PartialSticker:
diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py
index 9b819e50..a6c1818f 100644
--- a/hikari/impl/rest.py
+++ b/hikari/impl/rest.py
@@ -3948,8 +3948,9 @@ def interaction_autocomplete_builder(
         choices: typing.Union[
             typing.Sequence[commands.CommandChoice], typing.Sequence[special_endpoints.AutocompleteChoiceBuilder]
         ],
+        _stack_level: int = 0,
     ) -> special_endpoints.InteractionAutocompleteBuilder:
-        return special_endpoints_impl.InteractionAutocompleteBuilder(choices, _stack_level=1)
+        return special_endpoints_impl.InteractionAutocompleteBuilder(choices, _stack_level=_stack_level + 1)
 
     def interaction_message_builder(
         self, type_: typing.Union[base_interactions.ResponseType, int], /
@@ -4082,6 +4083,7 @@ async def create_autocomplete_response(
         choices: typing.Union[
             typing.Sequence[commands.CommandChoice], typing.Sequence[special_endpoints.AutocompleteChoiceBuilder]
         ],
+        _stack_level: int = 0,
     ) -> None:
         route = routes.POST_INTERACTION_RESPONSE.compile(interaction=interaction, token=token)
 
@@ -4099,6 +4101,7 @@ async def create_autocomplete_response(
                     removal_version="2.0.0.dev119",
                     additional_info="Use AutocompleteChoiceBuilder instead",
                     quote=False,
+                    stack_level=3 + _stack_level,
                 )
                 warned = True
 
diff --git a/hikari/interactions/command_interactions.py b/hikari/interactions/command_interactions.py
index eff6ce0f..5ceef418 100644
--- a/hikari/interactions/command_interactions.py
+++ b/hikari/interactions/command_interactions.py
@@ -383,7 +383,10 @@ class AutocompleteInteraction(BaseCommandInteraction):
     """Parameter values provided by the user invoking this command."""
 
     def build_response(
-        self, choices: typing.Sequence[commands.CommandChoice]
+        self,
+        choices: typing.Union[
+            typing.Sequence[commands.CommandChoice], typing.Sequence[special_endpoints.AutocompleteChoiceBuilder]
+        ],
     ) -> special_endpoints.InteractionAutocompleteBuilder:
         """Get a message response builder for use in the REST server flow.
 
@@ -392,6 +395,10 @@ def build_response(
             `AutocompleteInteraction.create_response` should be used to set
             the interaction response.
 
+        .. deprecated:: 2.0.0.dev118
+            Passing `hikari.commands.CommandChoice`s here instead of
+            `hikari.api.special_endpoints.AutocompleteChoiceBuilder`s.
+
         Examples
         --------
         .. code-block:: python
@@ -401,9 +408,9 @@ async def handle_autocomplete_interaction(interaction: AutocompleteInteraction)
                     interaction
                     .build_response(
                         [
-                            CommandChoice(name="foo", value="a"),
-                            CommandChoice(name="bar", value="b"),
-                            CommandChoice(name="baz", value="c"),
+                            AutocompleteChoiceBuilder(name="foo", value="a"),
+                            AutocompleteChoiceBuilder(name="bar", value="b"),
+                            AutocompleteChoiceBuilder(name="baz", value="c"),
                         ]
                     )
                 )
@@ -413,12 +420,18 @@ async def handle_autocomplete_interaction(interaction: AutocompleteInteraction)
         hikari.api.special_endpoints.InteractionAutocompleteBuilder
             Interaction autocomplete response builder object.
         """
-        return self.app.rest.interaction_autocomplete_builder(choices)
-
-    async def create_response(self, choices: typing.Sequence[commands.CommandChoice]) -> None:
-        """Create a response for this autocomplete interaction."""
-        await self.app.rest.create_autocomplete_response(
-            self.id,
-            self.token,
-            choices,
-        )
+        return self.app.rest.interaction_autocomplete_builder(choices, _stack_level=1)
+
+    async def create_response(
+        self,
+        choices: typing.Union[
+            typing.Sequence[commands.CommandChoice], typing.Sequence[special_endpoints.AutocompleteChoiceBuilder]
+        ],
+    ) -> None:
+        """Create a response for this autocomplete interaction.
+
+        .. deprecated:: 2.0.0.dev118
+            Passing `hikari.commands.CommandChoice`s here instead of
+            `hikari.api.special_endpoints.AutocompleteChoiceBuilder`s.
+        """
+        await self.app.rest.create_autocomplete_response(self.id, self.token, choices, _stack_level=1)
diff --git a/hikari/stickers.py b/hikari/stickers.py
index e41dd6b9..34c1bd58 100644
--- a/hikari/stickers.py
+++ b/hikari/stickers.py
@@ -107,17 +107,16 @@ class StickerPack(snowflakes.Unique):
     sku_id: snowflakes.Snowflake = attr.field(eq=False, hash=False, repr=False)
     """The ID of the packs SKU."""
 
-    # This is not exactly how Discord documents it, but we need to keep consistency
-    banner_hash: str = attr.field(eq=False, hash=False, repr=False)
-    """The hash for the pack's banner."""
+    banner_asset_id: typing.Optional[snowflakes.Snowflake] = attr.field(eq=False, hash=False, repr=False)
+    """ID of the sticker pack's banner image, if set."""
 
     @property
-    def banner_url(self) -> files.URL:
-        """Banner URL for the pack."""
+    def banner_url(self) -> typing.Optional[files.URL]:
+        """Banner URL for the pack, if set."""
         return self.make_banner_url()
 
-    def make_banner_url(self, *, ext: str = "png", size: int = 4096) -> files.URL:
-        """Generate the pack's banner image URL.
+    def make_banner_url(self, *, ext: str = "png", size: int = 4096) -> typing.Optional[files.URL]:
+        """Generate the pack's banner image URL, if set.
 
         Parameters
         ----------
@@ -130,17 +129,20 @@ def make_banner_url(self, *, ext: str = "png", size: int = 4096) -> files.URL:
 
         Returns
         -------
-        hikari.files.URL
-            The URL of the banner.
+        typing.Optional[hikari.files.URL]
+            The URL of the banner, if set.
 
         Raises
         ------
         ValueError
             If `size` is not a power of two or not between 16 and 4096.
         """
-        return routes.CDN_STICKER_PACK_BANNER.compile_to_file(
-            urls.CDN_URL, hash=self.banner_hash, file_format=ext, size=size
-        )
+        if self.banner_asset_id is not None:
+            return routes.CDN_STICKER_PACK_BANNER.compile_to_file(
+                urls.CDN_URL, hash=self.banner_asset_id, file_format=ext, size=size
+            )
+
+        return None
 
 
 @attr_extensions.with_copy


Test patch:
diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py
index 5cd96387..74b23183 100644
--- a/tests/hikari/impl/test_entity_factory.py
+++ b/tests/hikari/impl/test_entity_factory.py
@@ -5051,7 +5051,7 @@ def sticker_pack_payload(self, standard_sticker_payload):
             "cover_sticker_id": "456",
             "stickers": [standard_sticker_payload],
             "sku_id": "789",
-            "banner_asset_id": "hash123",
+            "banner_asset_id": "342123321",
         }
 
     def test_deserialize_partial_sticker(self, entity_factory_impl, partial_sticker_payload):
@@ -5099,7 +5099,7 @@ def test_deserialize_sticker_pack(self, entity_factory_impl, sticker_pack_payloa
         assert pack.description == "My sticker pack description"
         assert pack.cover_sticker_id == 456
         assert pack.sku_id == 789
-        assert pack.banner_hash == "hash123"
+        assert pack.banner_asset_id == 342123321
 
         assert len(pack.stickers) == 1
         sticker = pack.stickers[0]
@@ -5111,6 +5111,15 @@ def test_deserialize_sticker_pack(self, entity_factory_impl, sticker_pack_payloa
         assert sticker.sort_value == 96
         assert sticker.tags == ["thinking", "thonkang"]
 
+    def test_deserialize_sticker_pack_with_optional_fields(self, entity_factory_impl, sticker_pack_payload):
+        del sticker_pack_payload["cover_sticker_id"]
+        del sticker_pack_payload["banner_asset_id"]
+
+        pack = entity_factory_impl.deserialize_sticker_pack(sticker_pack_payload)
+
+        assert pack.cover_sticker_id is None
+        assert pack.banner_asset_id is None
+
     def test_stickers(self, entity_factory_impl, guild_sticker_payload):
         guild_definition = entity_factory_impl.deserialize_gateway_guild(
             {"id": "265828729970753537", "stickers": [guild_sticker_payload]}, user_id=123321
diff --git a/tests/hikari/interactions/test_command_interactions.py b/tests/hikari/interactions/test_command_interactions.py
index fa7d975d..523c218a 100644
--- a/tests/hikari/interactions/test_command_interactions.py
+++ b/tests/hikari/interactions/test_command_interactions.py
@@ -23,9 +23,9 @@
 import pytest
 
 from hikari import channels
-from hikari import commands
 from hikari import snowflakes
 from hikari import traits
+from hikari.impl import special_endpoints
 from hikari.interactions import base_interactions
 from hikari.interactions import command_interactions
 
@@ -124,14 +124,17 @@ def mock_autocomplete_interaction(self, mock_app):
 
     @pytest.fixture()
     def mock_command_choices(self):
-        return [commands.CommandChoice(name="a", value="b"), commands.CommandChoice(name="foo", value="bar")]
+        return [
+            special_endpoints.AutocompleteChoiceBuilder(name="a", value="b"),
+            special_endpoints.AutocompleteChoiceBuilder(name="foo", value="bar"),
+        ]
 
     def test_build_response(self, mock_autocomplete_interaction, mock_app, mock_command_choices):
         mock_app.rest.interaction_autocomplete_builder = mock.Mock()
         builder = mock_autocomplete_interaction.build_response(mock_command_choices)
 
         assert builder is mock_app.rest.interaction_autocomplete_builder.return_value
-        mock_app.rest.interaction_autocomplete_builder.assert_called_once_with(mock_command_choices)
+        mock_app.rest.interaction_autocomplete_builder.assert_called_once_with(mock_command_choices, _stack_level=1)
 
     @pytest.mark.asyncio()
     async def test_create_response(
@@ -143,7 +146,5 @@ async def test_create_response(
         await mock_autocomplete_interaction.create_response(mock_command_choices)
 
         mock_app.rest.create_autocomplete_response.assert_awaited_once_with(
-            2312312,
-            "httptptptptptptptp",
-            mock_command_choices,
+            2312312, "httptptptptptptptp", mock_command_choices, _stack_level=1
         )
diff --git a/tests/hikari/test_stickers.py b/tests/hikari/test_stickers.py
index 9c52fed5..0bb4238e 100644
--- a/tests/hikari/test_stickers.py
+++ b/tests/hikari/test_stickers.py
@@ -22,6 +22,7 @@
 import mock
 import pytest
 
+from hikari import snowflakes
 from hikari import stickers
 from hikari import urls
 from hikari.internal import routes
@@ -34,10 +35,10 @@ def model(self):
             id=123,
             name="testing",
             description="testing description",
-            cover_sticker_id=None,
+            cover_sticker_id=snowflakes.Snowflake(6541234),
             stickers=[],
             sku_id=123,
-            banner_hash="abc123",
+            banner_asset_id=snowflakes.Snowflake(541231),
         )
 
     def test_banner_url(self, model):
@@ -54,11 +55,16 @@ def test_make_banner_url(self, model):
 
         route.compile_to_file.assert_called_once_with(
             urls.CDN_URL,
-            hash="abc123",
+            hash=541231,
             size=512,
             file_format="url",
         )
 
+    def test_make_banner_url_when_no_banner_asset(self, model):
+        model.banner_asset_id = None
+
+        assert model.make_banner_url(ext="url", size=512) is None
+
 
 class TestPartialSticker:
     @pytest.fixture()