From ad2edb506505dc7a1bda936d7dc1ef474aecb2b0 Mon Sep 17 00:00:00 2001 From: Mehedi Hasan Shojib Date: Sun, 8 Feb 2026 23:05:59 +0600 Subject: [PATCH 1/7] feat: implement button styles in high-level API (#53) This change adds support for the new button styles introduced by Telegram. Users can now use the `style` parameter in `InlineKeyboardButton`, `KeyboardButton`, and `InlineKeyboardButtonBuy`. New type `KeyboardButtonStyle` is added to support: - `bg_primary` - `bg_danger` - `bg_success` - `icon` The `KeyboardButton.read` method maintains backward compatibility by returning a string for plain text buttons without styles. The `LoginUrl.write` method was also updated to support passing styles. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- pyrogram/types/bots_and_keyboards/__init__.py | 2 + .../inline_keyboard_button.py | 47 +++++++---- .../inline_keyboard_button_buy.py | 14 +++- .../bots_and_keyboards/keyboard_button.py | 29 +++++-- .../keyboard_button_style.py | 57 +++++++++++++ .../types/bots_and_keyboards/login_url.py | 8 +- tests/test_keyboards.py | 84 +++++++++++++++++++ 7 files changed, 216 insertions(+), 25 deletions(-) create mode 100644 pyrogram/types/bots_and_keyboards/keyboard_button_style.py create mode 100644 tests/test_keyboards.py diff --git a/pyrogram/types/bots_and_keyboards/__init__.py b/pyrogram/types/bots_and_keyboards/__init__.py index a939d97e..90078fda 100644 --- a/pyrogram/types/bots_and_keyboards/__init__.py +++ b/pyrogram/types/bots_and_keyboards/__init__.py @@ -30,6 +30,7 @@ from .inline_keyboard_button_buy import InlineKeyboardButtonBuy from .inline_keyboard_markup import InlineKeyboardMarkup from .keyboard_button import KeyboardButton +from .keyboard_button_style import KeyboardButtonStyle from .login_url import LoginUrl from .menu_button import MenuButton from .menu_button_commands import MenuButtonCommands @@ -69,6 +70,7 @@ "InlineKeyboardButtonBuy", "InlineKeyboardMarkup", "KeyboardButton", + "KeyboardButtonStyle", "LoginUrl", "MenuButton", "MenuButtonCommands", diff --git a/pyrogram/types/bots_and_keyboards/inline_keyboard_button.py b/pyrogram/types/bots_and_keyboards/inline_keyboard_button.py index 24e6f28a..7f8dc1ef 100644 --- a/pyrogram/types/bots_and_keyboards/inline_keyboard_button.py +++ b/pyrogram/types/bots_and_keyboards/inline_keyboard_button.py @@ -56,6 +56,9 @@ class InlineKeyboardButton(Object): copy_text (``str``, *optional*): A button that copies the text to the clipboard. + + style (:obj:`~pyrogram.types.KeyboardButtonStyle`, *optional*): + Button style. """ def __init__( @@ -71,6 +74,7 @@ def __init__( callback_game: types.CallbackGame | None = None, requires_password: bool | None = None, copy_text: str | None = None, + style: types.KeyboardButtonStyle | None = None, ) -> None: super().__init__() @@ -85,9 +89,12 @@ def __init__( self.callback_game = callback_game self.requires_password = requires_password self.copy_text = copy_text + self.style = style @staticmethod def read(b: raw.base.KeyboardButton): + style = types.KeyboardButtonStyle.read(getattr(b, "style", None)) + if isinstance(b, raw.types.KeyboardButtonCallback): # Try decode data to keep it as string, but if fails, fallback to bytes so we don't lose any information, # instead of decoding by ignoring/replacing errors. @@ -100,48 +107,55 @@ def read(b: raw.base.KeyboardButton): text=b.text, callback_data=data, requires_password=getattr(b, "requires_password", None), + style=style, ) if isinstance(b, raw.types.KeyboardButtonUrl): - return InlineKeyboardButton(text=b.text, url=b.url) + return InlineKeyboardButton(text=b.text, url=b.url, style=style) if isinstance(b, raw.types.KeyboardButtonUrlAuth): return InlineKeyboardButton( text=b.text, login_url=types.LoginUrl.read(b), + style=style, ) if isinstance(b, raw.types.KeyboardButtonUserProfile): - return InlineKeyboardButton(text=b.text, user_id=b.user_id) + return InlineKeyboardButton(text=b.text, user_id=b.user_id, style=style) if isinstance(b, raw.types.KeyboardButtonSwitchInline): if b.same_peer: return InlineKeyboardButton( text=b.text, switch_inline_query_current_chat=b.query, + style=style, ) - return InlineKeyboardButton(text=b.text, switch_inline_query=b.query) + return InlineKeyboardButton( + text=b.text, switch_inline_query=b.query, style=style + ) if isinstance(b, raw.types.KeyboardButtonGame): return InlineKeyboardButton( - text=b.text, - callback_game=types.CallbackGame(), + text=b.text, callback_game=types.CallbackGame(), style=style ) if isinstance(b, raw.types.KeyboardButtonWebView): return InlineKeyboardButton( - text=b.text, - web_app=types.WebAppInfo(url=b.url), + text=b.text, web_app=types.WebAppInfo(url=b.url), style=style ) if isinstance(b, raw.types.KeyboardButtonCopy): - return types.InlineKeyboardButton(text=b.text, copy_text=b.copy_text) + return types.InlineKeyboardButton( + text=b.text, copy_text=b.copy_text, style=style + ) if isinstance(b, raw.types.KeyboardButtonBuy): return types.InlineKeyboardButtonBuy.read(b) return None async def write(self, client: pyrogram.Client): + style = self.style.write() if self.style else None + if self.callback_data is not None: # Telegram only wants bytes, but we are allowed to pass strings too, for convenience. data = ( @@ -154,27 +168,33 @@ async def write(self, client: pyrogram.Client): text=self.text, data=data, requires_password=self.requires_password, + style=style, ) if self.url is not None: - return raw.types.KeyboardButtonUrl(text=self.text, url=self.url) + return raw.types.KeyboardButtonUrl( + text=self.text, url=self.url, style=style + ) if self.login_url is not None: return self.login_url.write( text=self.text, bot=await client.resolve_peer(self.login_url.bot_username or "self"), + style=style, ) if self.user_id is not None: return raw.types.InputKeyboardButtonUserProfile( text=self.text, user_id=await client.resolve_peer(self.user_id), + style=style, ) if self.switch_inline_query is not None: return raw.types.KeyboardButtonSwitchInline( text=self.text, query=self.switch_inline_query, + style=style, ) if self.switch_inline_query_current_chat is not None: @@ -182,19 +202,18 @@ async def write(self, client: pyrogram.Client): text=self.text, query=self.switch_inline_query_current_chat, same_peer=True, + style=style, ) if self.callback_game is not None: - return raw.types.KeyboardButtonGame(text=self.text) + return raw.types.KeyboardButtonGame(text=self.text, style=style) if self.web_app is not None: return raw.types.KeyboardButtonWebView( - text=self.text, - url=self.web_app.url, + text=self.text, url=self.web_app.url, style=style ) if self.copy_text is not None: return raw.types.KeyboardButtonCopy( - text=self.text, - copy_text=self.copy_text, + text=self.text, copy_text=self.copy_text, style=style ) return None diff --git a/pyrogram/types/bots_and_keyboards/inline_keyboard_button_buy.py b/pyrogram/types/bots_and_keyboards/inline_keyboard_button_buy.py index 7cb3b1bd..5b9e83b3 100644 --- a/pyrogram/types/bots_and_keyboards/inline_keyboard_button_buy.py +++ b/pyrogram/types/bots_and_keyboards/inline_keyboard_button_buy.py @@ -13,16 +13,24 @@ class InlineKeyboardButtonBuy(Object): text (``str``): Text of the button. If none of the optional fields are used, it will be sent as a message when the button is pressed. + + style (:obj:`~pyrogram.types.KeyboardButtonStyle`, *optional*): + Button style. """ - def __init__(self, text: str) -> None: + def __init__(self, text: str, style: types.KeyboardButtonStyle | None = None) -> None: super().__init__() self.text = str(text) + self.style = style @staticmethod def read(b): - return InlineKeyboardButtonBuy(text=b.text) + return InlineKeyboardButtonBuy( + text=b.text, style=types.KeyboardButtonStyle.read(getattr(b, "style", None)) + ) async def write(self, _: pyrogram.Client): - return raw.types.KeyboardButtonBuy(text=self.text) + return raw.types.KeyboardButtonBuy( + text=self.text, style=self.style.write() if self.style else None + ) diff --git a/pyrogram/types/bots_and_keyboards/keyboard_button.py b/pyrogram/types/bots_and_keyboards/keyboard_button.py index 8cdf3b78..1ecd233d 100644 --- a/pyrogram/types/bots_and_keyboards/keyboard_button.py +++ b/pyrogram/types/bots_and_keyboards/keyboard_button.py @@ -35,6 +35,8 @@ class KeyboardButton(Object): button is pressed. The Web App will be able to send a “web_app_data” service message. Available in private chats only. + style (:obj:`~pyrogram.types.KeyboardButtonStyle`, *optional*): + Button style. """ def __init__( @@ -46,6 +48,7 @@ def __init__( | types.RequestPeerTypeChannel = None, request_user: types.RequestPeerTypeUser = None, web_app: types.WebAppInfo = None, + style: types.KeyboardButtonStyle = None, ) -> None: super().__init__() @@ -55,20 +58,25 @@ def __init__( self.request_chat = request_chat self.request_user = request_user self.web_app = web_app + self.style = style @staticmethod def read(b): + style = types.KeyboardButtonStyle.read(getattr(b, "style", None)) + if isinstance(b, raw.types.KeyboardButton): - return b.text + return KeyboardButton(text=b.text, style=style) if style else b.text if isinstance(b, raw.types.KeyboardButtonRequestPhone): - return KeyboardButton(text=b.text, request_contact=True) + return KeyboardButton(text=b.text, request_contact=True, style=style) if isinstance(b, raw.types.KeyboardButtonRequestGeoLocation): - return KeyboardButton(text=b.text, request_location=True) + return KeyboardButton(text=b.text, request_location=True, style=style) if isinstance(b, raw.types.KeyboardButtonSimpleWebView): - return KeyboardButton(text=b.text, web_app=types.WebAppInfo(url=b.url)) + return KeyboardButton( + text=b.text, web_app=types.WebAppInfo(url=b.url), style=style + ) if isinstance(b, raw.types.KeyboardButtonRequestPeer): if isinstance(b.peer_type, raw.types.RequestPeerTypeBroadcast): @@ -79,6 +87,7 @@ def read(b): is_username=b.peer_type.has_username, max=b.max_quantity, ), + style=style, ) if isinstance(b.peer_type, raw.types.RequestPeerTypeChat): return KeyboardButton( @@ -90,6 +99,7 @@ def read(b): is_forum=b.peer_type.forum, max=b.max_quantity, ), + style=style, ) if isinstance(b.peer_type, raw.types.RequestPeerTypeUser): @@ -100,17 +110,21 @@ def read(b): is_premium=b.peer_type.premium, max=b.max_quantity, ), + style=style, ) return None return None def write(self): + style = self.style.write() if self.style else None + if self.request_contact: - return raw.types.KeyboardButtonRequestPhone(text=self.text) + return raw.types.KeyboardButtonRequestPhone(text=self.text, style=style) if self.request_location: - return raw.types.KeyboardButtonRequestGeoLocation(text=self.text) + return raw.types.KeyboardButtonRequestGeoLocation(text=self.text, style=style) if self.request_chat: if isinstance(self.request_chat, types.RequestPeerTypeChannel): + # Note: InputKeyboardButtonRequestPeer doesn't have style in schema return raw.types.InputKeyboardButtonRequestPeer( text=self.text, button_id=self.request_chat.button_id, @@ -154,5 +168,6 @@ def write(self): return raw.types.KeyboardButtonSimpleWebView( text=self.text, url=self.web_app.url, + style=style, ) - return raw.types.KeyboardButton(text=self.text) + return raw.types.KeyboardButton(text=self.text, style=style) diff --git a/pyrogram/types/bots_and_keyboards/keyboard_button_style.py b/pyrogram/types/bots_and_keyboards/keyboard_button_style.py new file mode 100644 index 00000000..abc25d85 --- /dev/null +++ b/pyrogram/types/bots_and_keyboards/keyboard_button_style.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +from pyrogram import raw +from pyrogram.types.object import Object + + +class KeyboardButtonStyle(Object): + """Button style. + + Parameters: + bg_primary (``bool``, *optional*): + Whether the button should be primary. + + bg_danger (``bool``, *optional*): + Whether the button should be danger. + + bg_success (``bool``, *optional*): + Whether the button should be success. + + icon (``int``, *optional*): + Custom icon for the button. + """ + + def __init__( + self, + *, + bg_primary: bool | None = None, + bg_danger: bool | None = None, + bg_success: bool | None = None, + icon: int | None = None, + ) -> None: + super().__init__() + + self.bg_primary = bg_primary + self.bg_danger = bg_danger + self.bg_success = bg_success + self.icon = icon + + @staticmethod + def read(b: raw.types.KeyboardButtonStyle) -> KeyboardButtonStyle | None: + if not b: + return None + + return KeyboardButtonStyle( + bg_primary=b.bg_primary, + bg_danger=b.bg_danger, + bg_success=b.bg_success, + icon=b.icon, + ) + + def write(self) -> raw.types.KeyboardButtonStyle: + return raw.types.KeyboardButtonStyle( + bg_primary=self.bg_primary, + bg_danger=self.bg_danger, + bg_success=self.bg_success, + icon=self.icon, + ) diff --git a/pyrogram/types/bots_and_keyboards/login_url.py b/pyrogram/types/bots_and_keyboards/login_url.py index 16619421..42f9eba5 100644 --- a/pyrogram/types/bots_and_keyboards/login_url.py +++ b/pyrogram/types/bots_and_keyboards/login_url.py @@ -60,11 +60,17 @@ def __init__( def read(b: raw.types.KeyboardButtonUrlAuth) -> LoginUrl: return LoginUrl(url=b.url, forward_text=b.fwd_text, button_id=b.button_id) - def write(self, text: str, bot: raw.types.InputUser): + def write( + self, + text: str, + bot: raw.types.InputUser, + style: raw.types.KeyboardButtonStyle | None = None, + ): return raw.types.InputKeyboardButtonUrlAuth( text=text, url=self.url, bot=bot, fwd_text=self.forward_text, request_write_access=self.request_write_access, + style=style, ) diff --git a/tests/test_keyboards.py b/tests/test_keyboards.py new file mode 100644 index 00000000..97884110 --- /dev/null +++ b/tests/test_keyboards.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +import pytest +from pyrogram import types, raw + +def test_keyboard_button_style(): + style = types.KeyboardButtonStyle(bg_primary=True, bg_danger=False, bg_success=True, icon=123) + assert style.bg_primary is True + assert style.bg_danger is False + assert style.bg_success is True + assert style.icon == 123 + + raw_style = style.write() + assert isinstance(raw_style, raw.types.KeyboardButtonStyle) + assert raw_style.bg_primary is True + assert raw_style.bg_danger is False + assert raw_style.bg_success is True + assert raw_style.icon == 123 + + style2 = types.KeyboardButtonStyle.read(raw_style) + assert style2.bg_primary is True + assert style2.bg_danger is False + assert style2.bg_success is True + assert style2.icon == 123 + +def test_inline_keyboard_button_with_style(): + style = types.KeyboardButtonStyle(bg_primary=True) + button = types.InlineKeyboardButton("Test", url="https://example.com", style=style) + assert button.style == style + + raw_button = raw.types.KeyboardButtonUrl(text="Test", url="https://example.com", style=style.write()) + button2 = types.InlineKeyboardButton.read(raw_button) + assert button2.text == "Test" + assert button2.url == "https://example.com" + assert button2.style.bg_primary is True + +def test_keyboard_button_with_style(): + style = types.KeyboardButtonStyle(bg_success=True) + button = types.KeyboardButton("Test", style=style) + assert button.style == style + + raw_button = raw.types.KeyboardButton(text="Test", style=style.write()) + button2 = types.KeyboardButton.read(raw_button) + assert isinstance(button2, types.KeyboardButton) + assert button2.text == "Test" + assert button2.style.bg_success is True + +def test_keyboard_button_backward_compatibility(): + raw_button = raw.types.KeyboardButton(text="Test") + button = types.KeyboardButton.read(raw_button) + assert button == "Test" + assert isinstance(button, str) + +@pytest.mark.asyncio +async def test_inline_keyboard_button_write(): + # Mock client for resolve_peer + class MockClient: + async def resolve_peer(self, peer_id): + return raw.types.InputPeerSelf() + + client = MockClient() + style = types.KeyboardButtonStyle(bg_danger=True) + button = types.InlineKeyboardButton("Test", callback_data="data", style=style) + + raw_button = await button.write(client) + assert isinstance(raw_button, raw.types.KeyboardButtonCallback) + assert raw_button.style.bg_danger is True + assert raw_button.data == b"data" + +@pytest.mark.asyncio +async def test_login_url_write_with_style(): + class MockClient: + async def resolve_peer(self, peer_id): + return raw.types.InputUserSelf() + + client = MockClient() + style = types.KeyboardButtonStyle(bg_primary=True) + login_url = types.LoginUrl(url="https://example.com") + button = types.InlineKeyboardButton("Login", login_url=login_url, style=style) + + raw_button = await button.write(client) + assert isinstance(raw_button, raw.types.InputKeyboardButtonUrlAuth) + assert raw_button.style.bg_primary is True + assert raw_button.url == "https://example.com" From f8da922ea2f85e87927d8fe6a231766f643777d6 Mon Sep 17 00:00:00 2001 From: 5hojib Date: Sun, 8 Feb 2026 17:06:19 +0000 Subject: [PATCH 2/7] InkyPinkyPonky [no ci] Signed-off-by: 5hojib --- .../inline_keyboard_button.py | 28 ++++++++++++++----- .../inline_keyboard_button_buy.py | 10 +++++-- .../bots_and_keyboards/keyboard_button.py | 8 ++++-- tests/test_keyboards.py | 21 +++++++++++--- 4 files changed, 51 insertions(+), 16 deletions(-) diff --git a/pyrogram/types/bots_and_keyboards/inline_keyboard_button.py b/pyrogram/types/bots_and_keyboards/inline_keyboard_button.py index 7f8dc1ef..f65e19e2 100644 --- a/pyrogram/types/bots_and_keyboards/inline_keyboard_button.py +++ b/pyrogram/types/bots_and_keyboards/inline_keyboard_button.py @@ -131,22 +131,30 @@ def read(b: raw.base.KeyboardButton): style=style, ) return InlineKeyboardButton( - text=b.text, switch_inline_query=b.query, style=style + text=b.text, + switch_inline_query=b.query, + style=style, ) if isinstance(b, raw.types.KeyboardButtonGame): return InlineKeyboardButton( - text=b.text, callback_game=types.CallbackGame(), style=style + text=b.text, + callback_game=types.CallbackGame(), + style=style, ) if isinstance(b, raw.types.KeyboardButtonWebView): return InlineKeyboardButton( - text=b.text, web_app=types.WebAppInfo(url=b.url), style=style + text=b.text, + web_app=types.WebAppInfo(url=b.url), + style=style, ) if isinstance(b, raw.types.KeyboardButtonCopy): return types.InlineKeyboardButton( - text=b.text, copy_text=b.copy_text, style=style + text=b.text, + copy_text=b.copy_text, + style=style, ) if isinstance(b, raw.types.KeyboardButtonBuy): @@ -173,7 +181,9 @@ async def write(self, client: pyrogram.Client): if self.url is not None: return raw.types.KeyboardButtonUrl( - text=self.text, url=self.url, style=style + text=self.text, + url=self.url, + style=style, ) if self.login_url is not None: @@ -210,10 +220,14 @@ async def write(self, client: pyrogram.Client): if self.web_app is not None: return raw.types.KeyboardButtonWebView( - text=self.text, url=self.web_app.url, style=style + text=self.text, + url=self.web_app.url, + style=style, ) if self.copy_text is not None: return raw.types.KeyboardButtonCopy( - text=self.text, copy_text=self.copy_text, style=style + text=self.text, + copy_text=self.copy_text, + style=style, ) return None diff --git a/pyrogram/types/bots_and_keyboards/inline_keyboard_button_buy.py b/pyrogram/types/bots_and_keyboards/inline_keyboard_button_buy.py index 5b9e83b3..218c9db7 100644 --- a/pyrogram/types/bots_and_keyboards/inline_keyboard_button_buy.py +++ b/pyrogram/types/bots_and_keyboards/inline_keyboard_button_buy.py @@ -18,7 +18,9 @@ class InlineKeyboardButtonBuy(Object): Button style. """ - def __init__(self, text: str, style: types.KeyboardButtonStyle | None = None) -> None: + def __init__( + self, text: str, style: types.KeyboardButtonStyle | None = None + ) -> None: super().__init__() self.text = str(text) @@ -27,10 +29,12 @@ def __init__(self, text: str, style: types.KeyboardButtonStyle | None = None) -> @staticmethod def read(b): return InlineKeyboardButtonBuy( - text=b.text, style=types.KeyboardButtonStyle.read(getattr(b, "style", None)) + text=b.text, + style=types.KeyboardButtonStyle.read(getattr(b, "style", None)), ) async def write(self, _: pyrogram.Client): return raw.types.KeyboardButtonBuy( - text=self.text, style=self.style.write() if self.style else None + text=self.text, + style=self.style.write() if self.style else None, ) diff --git a/pyrogram/types/bots_and_keyboards/keyboard_button.py b/pyrogram/types/bots_and_keyboards/keyboard_button.py index 1ecd233d..9edcb1c6 100644 --- a/pyrogram/types/bots_and_keyboards/keyboard_button.py +++ b/pyrogram/types/bots_and_keyboards/keyboard_button.py @@ -75,7 +75,9 @@ def read(b): if isinstance(b, raw.types.KeyboardButtonSimpleWebView): return KeyboardButton( - text=b.text, web_app=types.WebAppInfo(url=b.url), style=style + text=b.text, + web_app=types.WebAppInfo(url=b.url), + style=style, ) if isinstance(b, raw.types.KeyboardButtonRequestPeer): @@ -121,7 +123,9 @@ def write(self): if self.request_contact: return raw.types.KeyboardButtonRequestPhone(text=self.text, style=style) if self.request_location: - return raw.types.KeyboardButtonRequestGeoLocation(text=self.text, style=style) + return raw.types.KeyboardButtonRequestGeoLocation( + text=self.text, style=style + ) if self.request_chat: if isinstance(self.request_chat, types.RequestPeerTypeChannel): # Note: InputKeyboardButtonRequestPeer doesn't have style in schema diff --git a/tests/test_keyboards.py b/tests/test_keyboards.py index 97884110..28673c80 100644 --- a/tests/test_keyboards.py +++ b/tests/test_keyboards.py @@ -1,10 +1,14 @@ from __future__ import annotations import pytest -from pyrogram import types, raw + +from pyrogram import raw, types + def test_keyboard_button_style(): - style = types.KeyboardButtonStyle(bg_primary=True, bg_danger=False, bg_success=True, icon=123) + style = types.KeyboardButtonStyle( + bg_primary=True, bg_danger=False, bg_success=True, icon=123 + ) assert style.bg_primary is True assert style.bg_danger is False assert style.bg_success is True @@ -23,17 +27,23 @@ def test_keyboard_button_style(): assert style2.bg_success is True assert style2.icon == 123 + def test_inline_keyboard_button_with_style(): style = types.KeyboardButtonStyle(bg_primary=True) - button = types.InlineKeyboardButton("Test", url="https://example.com", style=style) + button = types.InlineKeyboardButton( + "Test", url="https://example.com", style=style + ) assert button.style == style - raw_button = raw.types.KeyboardButtonUrl(text="Test", url="https://example.com", style=style.write()) + raw_button = raw.types.KeyboardButtonUrl( + text="Test", url="https://example.com", style=style.write() + ) button2 = types.InlineKeyboardButton.read(raw_button) assert button2.text == "Test" assert button2.url == "https://example.com" assert button2.style.bg_primary is True + def test_keyboard_button_with_style(): style = types.KeyboardButtonStyle(bg_success=True) button = types.KeyboardButton("Test", style=style) @@ -45,12 +55,14 @@ def test_keyboard_button_with_style(): assert button2.text == "Test" assert button2.style.bg_success is True + def test_keyboard_button_backward_compatibility(): raw_button = raw.types.KeyboardButton(text="Test") button = types.KeyboardButton.read(raw_button) assert button == "Test" assert isinstance(button, str) + @pytest.mark.asyncio async def test_inline_keyboard_button_write(): # Mock client for resolve_peer @@ -67,6 +79,7 @@ async def resolve_peer(self, peer_id): assert raw_button.style.bg_danger is True assert raw_button.data == b"data" + @pytest.mark.asyncio async def test_login_url_write_with_style(): class MockClient: From 543ff3cae44d538c96fc46ccd12988ef35985773 Mon Sep 17 00:00:00 2001 From: 5hojib Date: Sun, 8 Feb 2026 23:08:12 +0600 Subject: [PATCH 3/7] update --- .../inline_keyboard_button_buy.py | 2 +- tests/test_keyboards.py | 97 ------------------- 2 files changed, 1 insertion(+), 98 deletions(-) delete mode 100644 tests/test_keyboards.py diff --git a/pyrogram/types/bots_and_keyboards/inline_keyboard_button_buy.py b/pyrogram/types/bots_and_keyboards/inline_keyboard_button_buy.py index 218c9db7..1fa7a289 100644 --- a/pyrogram/types/bots_and_keyboards/inline_keyboard_button_buy.py +++ b/pyrogram/types/bots_and_keyboards/inline_keyboard_button_buy.py @@ -1,7 +1,7 @@ from __future__ import annotations import pyrogram -from pyrogram import raw +from pyrogram import raw, types from pyrogram.types.object import Object diff --git a/tests/test_keyboards.py b/tests/test_keyboards.py deleted file mode 100644 index 28673c80..00000000 --- a/tests/test_keyboards.py +++ /dev/null @@ -1,97 +0,0 @@ -from __future__ import annotations - -import pytest - -from pyrogram import raw, types - - -def test_keyboard_button_style(): - style = types.KeyboardButtonStyle( - bg_primary=True, bg_danger=False, bg_success=True, icon=123 - ) - assert style.bg_primary is True - assert style.bg_danger is False - assert style.bg_success is True - assert style.icon == 123 - - raw_style = style.write() - assert isinstance(raw_style, raw.types.KeyboardButtonStyle) - assert raw_style.bg_primary is True - assert raw_style.bg_danger is False - assert raw_style.bg_success is True - assert raw_style.icon == 123 - - style2 = types.KeyboardButtonStyle.read(raw_style) - assert style2.bg_primary is True - assert style2.bg_danger is False - assert style2.bg_success is True - assert style2.icon == 123 - - -def test_inline_keyboard_button_with_style(): - style = types.KeyboardButtonStyle(bg_primary=True) - button = types.InlineKeyboardButton( - "Test", url="https://example.com", style=style - ) - assert button.style == style - - raw_button = raw.types.KeyboardButtonUrl( - text="Test", url="https://example.com", style=style.write() - ) - button2 = types.InlineKeyboardButton.read(raw_button) - assert button2.text == "Test" - assert button2.url == "https://example.com" - assert button2.style.bg_primary is True - - -def test_keyboard_button_with_style(): - style = types.KeyboardButtonStyle(bg_success=True) - button = types.KeyboardButton("Test", style=style) - assert button.style == style - - raw_button = raw.types.KeyboardButton(text="Test", style=style.write()) - button2 = types.KeyboardButton.read(raw_button) - assert isinstance(button2, types.KeyboardButton) - assert button2.text == "Test" - assert button2.style.bg_success is True - - -def test_keyboard_button_backward_compatibility(): - raw_button = raw.types.KeyboardButton(text="Test") - button = types.KeyboardButton.read(raw_button) - assert button == "Test" - assert isinstance(button, str) - - -@pytest.mark.asyncio -async def test_inline_keyboard_button_write(): - # Mock client for resolve_peer - class MockClient: - async def resolve_peer(self, peer_id): - return raw.types.InputPeerSelf() - - client = MockClient() - style = types.KeyboardButtonStyle(bg_danger=True) - button = types.InlineKeyboardButton("Test", callback_data="data", style=style) - - raw_button = await button.write(client) - assert isinstance(raw_button, raw.types.KeyboardButtonCallback) - assert raw_button.style.bg_danger is True - assert raw_button.data == b"data" - - -@pytest.mark.asyncio -async def test_login_url_write_with_style(): - class MockClient: - async def resolve_peer(self, peer_id): - return raw.types.InputUserSelf() - - client = MockClient() - style = types.KeyboardButtonStyle(bg_primary=True) - login_url = types.LoginUrl(url="https://example.com") - button = types.InlineKeyboardButton("Login", login_url=login_url, style=style) - - raw_button = await button.write(client) - assert isinstance(raw_button, raw.types.InputKeyboardButtonUrlAuth) - assert raw_button.style.bg_primary is True - assert raw_button.url == "https://example.com" From df273a0ba382e6b861a8e9d4c9ca49d552544ea4 Mon Sep 17 00:00:00 2001 From: 5hojib Date: Sun, 8 Feb 2026 17:08:35 +0000 Subject: [PATCH 4/7] InkyPinkyPonky [no ci] Signed-off-by: 5hojib --- .../types/bots_and_keyboards/inline_keyboard_button_buy.py | 4 +++- pyrogram/types/bots_and_keyboards/keyboard_button.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pyrogram/types/bots_and_keyboards/inline_keyboard_button_buy.py b/pyrogram/types/bots_and_keyboards/inline_keyboard_button_buy.py index 1fa7a289..72603dd1 100644 --- a/pyrogram/types/bots_and_keyboards/inline_keyboard_button_buy.py +++ b/pyrogram/types/bots_and_keyboards/inline_keyboard_button_buy.py @@ -19,7 +19,9 @@ class InlineKeyboardButtonBuy(Object): """ def __init__( - self, text: str, style: types.KeyboardButtonStyle | None = None + self, + text: str, + style: types.KeyboardButtonStyle | None = None, ) -> None: super().__init__() diff --git a/pyrogram/types/bots_and_keyboards/keyboard_button.py b/pyrogram/types/bots_and_keyboards/keyboard_button.py index 9edcb1c6..6d8ea4b8 100644 --- a/pyrogram/types/bots_and_keyboards/keyboard_button.py +++ b/pyrogram/types/bots_and_keyboards/keyboard_button.py @@ -124,7 +124,8 @@ def write(self): return raw.types.KeyboardButtonRequestPhone(text=self.text, style=style) if self.request_location: return raw.types.KeyboardButtonRequestGeoLocation( - text=self.text, style=style + text=self.text, + style=style, ) if self.request_chat: if isinstance(self.request_chat, types.RequestPeerTypeChannel): From 46fa0fc804484131df7fce4cff38c272b377ecd7 Mon Sep 17 00:00:00 2001 From: 5hojib Date: Mon, 9 Feb 2026 08:20:37 +0600 Subject: [PATCH 5/7] update compiler.py --- compiler/docs/compiler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/compiler/docs/compiler.py b/compiler/docs/compiler.py index d240e322..9a4efc8c 100644 --- a/compiler/docs/compiler.py +++ b/compiler/docs/compiler.py @@ -555,6 +555,7 @@ def get_title_list(s: str) -> list: Bot keyboards ReplyKeyboardMarkup KeyboardButton + KeyboardButtonStyle ReplyKeyboardRemove InlineKeyboardMarkup InlineKeyboardButton From f843b71f3bbd3b8a6fa4530a5a622b9dd986a83f Mon Sep 17 00:00:00 2001 From: Mehedi Hasan Shojib Date: Mon, 9 Feb 2026 08:29:43 +0600 Subject: [PATCH 6/7] Update __init__.py --- pyrogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrogram/__init__.py b/pyrogram/__init__.py index 4d0651fc..57883ee1 100644 --- a/pyrogram/__init__.py +++ b/pyrogram/__init__.py @@ -1,6 +1,6 @@ from __future__ import annotations -__version__ = "v0.2.224.1" +__version__ = "v0.2.224.2" __license__ = "MIT License" from concurrent.futures.thread import ThreadPoolExecutor From d2cac30fa2ddc5058f55c238e39126148c55f2d6 Mon Sep 17 00:00:00 2001 From: Mehedi Hasan Shojib Date: Mon, 9 Feb 2026 08:30:59 +0600 Subject: [PATCH 7/7] Update __init__.py --- pyrogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrogram/__init__.py b/pyrogram/__init__.py index 57883ee1..bd4ed922 100644 --- a/pyrogram/__init__.py +++ b/pyrogram/__init__.py @@ -1,6 +1,6 @@ from __future__ import annotations -__version__ = "v0.2.224.2" +__version__ = "v0.2.224.3" __license__ = "MIT License" from concurrent.futures.thread import ThreadPoolExecutor