Repository: hikari-py/hikari
Pull request #2329 (2025-04-25 21:00:17)

Set button builder specifics in the sub-builders
This should improve typecheckers ability to infer the types properly

This has no user impact



Patch:
diff --git a/hikari/impl/special_endpoints.py b/hikari/impl/special_endpoints.py
index 097cacd8..cc02f94a 100644
--- a/hikari/impl/special_endpoints.py
+++ b/hikari/impl/special_endpoints.py
@@ -56,6 +56,7 @@
     "TypingIndicator",
 )
 
+import abc
 import asyncio
 import typing
 
@@ -1315,11 +1316,8 @@ def _build_emoji(
 
 @attrs_extensions.with_copy
 @attrs.define(kw_only=True, weakref_slot=False)
-class _ButtonBuilder(special_endpoints.ButtonBuilder):
+class _ButtonBuilder(special_endpoints.ButtonBuilder, abc.ABC):
     _id: undefined.UndefinedOr[int] = attrs.field(alias="id", default=undefined.UNDEFINED)
-    _style: int | component_models.ButtonStyle = attrs.field(alias="style")
-    _custom_id: undefined.UndefinedOr[str] = attrs.field()
-    _url: undefined.UndefinedOr[str] = attrs.field()
     _emoji: snowflakes.Snowflakeish | emojis.Emoji | str | undefined.UndefinedType = attrs.field(
         alias="emoji", default=undefined.UNDEFINED
     )
@@ -1341,11 +1339,6 @@ def type(self) -> typing.Literal[component_models.ComponentType.BUTTON]:
     def id(self) -> undefined.UndefinedOr[int]:
         return self._id
 
-    @property
-    @typing_extensions.override
-    def style(self) -> int | component_models.ButtonStyle:
-        return self._style
-
     @property
     @typing_extensions.override
     def emoji(self) -> snowflakes.Snowflakeish | emojis.Emoji | str | undefined.UndefinedType:
@@ -1384,7 +1377,7 @@ def build(
         data = data_binding.JSONObjectBuilder()
 
         data["type"] = component_models.ComponentType.BUTTON
-        data["style"] = self._style
+        data["style"] = self.style
         data["disabled"] = self._is_disabled
         data.put("id", self._id)
         data.put("label", self._label)
@@ -1395,9 +1388,6 @@ def build(
         elif self._emoji_name is not undefined.UNDEFINED:
             data["emoji"] = {"name": self._emoji_name}
 
-        data.put("custom_id", self._custom_id)
-        data.put("url", self._url)
-
         return data, []
 
 
@@ -1406,9 +1396,6 @@ class LinkButtonBuilder(_ButtonBuilder, special_endpoints.LinkButtonBuilder):
     """Builder class for link buttons."""
 
     _custom_id: undefined.UndefinedType = attrs.field(init=False, default=undefined.UNDEFINED)
-    _style: typing.Literal[component_models.ButtonStyle.LINK] = attrs.field(
-        init=False, default=component_models.ButtonStyle.LINK
-    )
     _url: str = attrs.field(alias="url")
 
     @property
@@ -1416,13 +1403,33 @@ class LinkButtonBuilder(_ButtonBuilder, special_endpoints.LinkButtonBuilder):
     def url(self) -> str:
         return self._url
 
+    @property
+    @typing_extensions.override
+    def style(self) -> typing.Literal[component_models.ButtonStyle.LINK]:
+        return component_models.ButtonStyle.LINK
+
+    @typing_extensions.override
+    def build(
+        self,
+    ) -> tuple[typing.MutableMapping[str, typing.Any], typing.Sequence[files.Resource[files.AsyncReader]]]:
+        data, attachments = super().build()
+
+        data["url"] = self._url
+
+        return data, attachments
+
 
 @attrs.define(kw_only=True, weakref_slot=False)
 class InteractiveButtonBuilder(_ButtonBuilder, special_endpoints.InteractiveButtonBuilder):
     """Builder class for interactive buttons."""
 
+    _style: int | component_models.ButtonStyle = attrs.field(alias="style")
     _custom_id: str = attrs.field(alias="custom_id")
-    _url: undefined.UndefinedType = attrs.field(init=False, default=undefined.UNDEFINED)
+
+    @property
+    @typing_extensions.override
+    def style(self) -> int | component_models.ButtonStyle:
+        return self._style
 
     @property
     @typing_extensions.override
@@ -1434,6 +1441,16 @@ def set_custom_id(self, custom_id: str, /) -> Self:
         self._custom_id = custom_id
         return self
 
+    @typing_extensions.override
+    def build(
+        self,
+    ) -> tuple[typing.MutableMapping[str, typing.Any], typing.Sequence[files.Resource[files.AsyncReader]]]:
+        data, attachments = super().build()
+
+        data["custom_id"] = self._custom_id
+
+        return data, attachments
+
 
 @attrs_extensions.with_copy
 @attrs.define(weakref_slot=False)


Test patch:
diff --git a/tests/hikari/impl/test_special_endpoints.py b/tests/hikari/impl/test_special_endpoints.py
index 89ceba48..e42f4419 100644
--- a/tests/hikari/impl/test_special_endpoints.py
+++ b/tests/hikari/impl/test_special_endpoints.py
@@ -1259,16 +1259,14 @@ def test__build_emoji_when_undefined():
 
 
 class Test_ButtonBuilder:
+    class ButtonBuilder(special_endpoints._ButtonBuilder):
+        @property
+        def style(self) -> components.ButtonStyle:
+            return components.ButtonStyle.DANGER
+
     @pytest.fixture
     def button(self):
-        return special_endpoints._ButtonBuilder(
-            style=components.ButtonStyle.DANGER,
-            custom_id="sfdasdasd",
-            url="hi there",
-            emoji=543123,
-            label="a lebel",
-            is_disabled=True,
-        )
+        return Test_ButtonBuilder.ButtonBuilder(id=5855932, emoji=543123, label="a lebel", is_disabled=True)
 
     def test_type_property(self, button):
         assert button.type is components.ComponentType.BUTTON
@@ -1313,27 +1311,15 @@ def test_set_is_disabled(self, button):
         assert button.set_is_disabled(False)
         assert button.is_disabled is False
 
-    def test_build(self):
-        button = special_endpoints._ButtonBuilder(
-            id=5855932,
-            style=components.ButtonStyle.DANGER,
-            url="https://example.com",
-            label="no u",
-            custom_id="ooga booga",
-            emoji="emoji_name",
-            is_disabled=True,
-        )
-
+    def test_build(self, button):
         payload, attachments = button.build()
 
         assert payload == {
             "id": 5855932,
             "type": components.ComponentType.BUTTON,
             "style": components.ButtonStyle.DANGER,
-            "url": "https://example.com",
-            "emoji": {"name": "emoji_name"},
-            "label": "no u",
-            "custom_id": "ooga booga",
+            "emoji": {"id": "543123"},
+            "label": "a lebel",
             "disabled": True,
         }
 
@@ -1341,9 +1327,7 @@ def test_build(self):
 
     @pytest.mark.parametrize("emoji", [123321, emojis.CustomEmoji(id=123321, name="", is_animated=True)])
     def test_build_with_custom_emoji(self, emoji: typing.Union[int, emojis.Emoji]):
-        button = special_endpoints._ButtonBuilder(
-            style=components.ButtonStyle.DANGER, emoji=emoji, url=undefined.UNDEFINED, custom_id=undefined.UNDEFINED
-        )
+        button = Test_ButtonBuilder.ButtonBuilder(emoji=emoji)
 
         payload, attachments = button.build()
 
@@ -1357,15 +1341,13 @@ def test_build_with_custom_emoji(self, emoji: typing.Union[int, emojis.Emoji]):
         assert attachments == []
 
     def test_build_without_optional_fields(self):
-        button = special_endpoints._ButtonBuilder(
-            style=components.ButtonStyle.LINK, url=undefined.UNDEFINED, custom_id=undefined.UNDEFINED
-        )
+        button = Test_ButtonBuilder.ButtonBuilder()
 
         payload, attachments = button.build()
 
         assert payload == {
             "type": components.ComponentType.BUTTON,
-            "style": components.ButtonStyle.LINK,
+            "style": components.ButtonStyle.DANGER,
             "disabled": False,
         }
 
@@ -1378,6 +1360,24 @@ def test_url_property(self):
 
         assert button.url == "hihihihi"
 
+    def test_build(self):
+        button = special_endpoints.LinkButtonBuilder(
+            id=5855932, url="hihihihi", label="no u", emoji="emoji_name", is_disabled=True
+        )
+
+        payload, attachments = button.build()
+
+        assert payload == {
+            "id": 5855932,
+            "style": components.ButtonStyle.LINK,
+            "type": components.ComponentType.BUTTON,
+            "disabled": True,
+            "emoji": {"name": "emoji_name"},
+            "label": "no u",
+            "url": "hihihihi",
+        }
+        assert attachments == []
+
 
 class TestInteractiveButtonBuilder:
     def test_custom_id_property(self):
@@ -1387,6 +1387,29 @@ def test_custom_id_property(self):
 
         assert button.custom_id == "eeeeee"
 
+    def test_build(self):
+        button = special_endpoints.InteractiveButtonBuilder(
+            id=5855932,
+            style=components.ButtonStyle.PRIMARY,
+            label="no u",
+            emoji="emoji_name",
+            is_disabled=True,
+            custom_id="oogie",
+        )
+
+        payload, attachments = button.build()
+
+        assert payload == {
+            "id": 5855932,
+            "style": components.ButtonStyle.PRIMARY,
+            "type": components.ComponentType.BUTTON,
+            "custom_id": "oogie",
+            "disabled": True,
+            "emoji": {"name": "emoji_name"},
+            "label": "no u",
+        }
+        assert attachments == []
+
 
 class TestSelectOptionBuilder:
     @pytest.fixture