diff --git a/aiogram/utils/keyboard.py b/aiogram/utils/keyboard.py index 84258e6d..9fcab689 100644 --- a/aiogram/utils/keyboard.py +++ b/aiogram/utils/keyboard.py @@ -236,6 +236,12 @@ class KeyboardBuilder(Generic[ButtonType]): return self def button(self, **kwargs: Any) -> "KeyboardBuilder[ButtonType]": + """ + Add button to markup + + :param kwargs: + :return: + """ if isinstance(callback_data := kwargs.get("callback_data", None), CallbackData): kwargs["callback_data"] = callback_data.pack() button = self._button_type(**kwargs) @@ -246,6 +252,17 @@ class KeyboardBuilder(Generic[ButtonType]): return ReplyKeyboardMarkup(keyboard=self.export(), **kwargs) return InlineKeyboardMarkup(inline_keyboard=self.export()) + def attach(self, builder: "KeyboardBuilder[ButtonType]") -> "KeyboardBuilder[ButtonType]": + if not isinstance(builder, KeyboardBuilder): + raise ValueError(f"Only KeyboardBuilder can be attached, not {type(builder).__name__}") + if builder._button_type is not self._button_type: + raise ValueError( + f"Only builders with same button type can be attached, " + f"not {self._button_type.__name__} and {builder._button_type.__name__}" + ) + self._markup.extend(builder.export()) + return self + def repeat_last(items: Iterable[T]) -> Generator[T, None, None]: items_iter = iter(items) @@ -303,6 +320,18 @@ class InlineKeyboardBuilder(KeyboardBuilder[InlineKeyboardButton]): """ return InlineKeyboardBuilder(markup=self.export()) + @classmethod + def from_markup( + cls: Type["InlineKeyboardBuilder"], markup: InlineKeyboardMarkup + ) -> "InlineKeyboardBuilder": + """ + Create builder from existing markup + + :param markup: + :return: + """ + return cls(markup=markup.inline_keyboard) + class ReplyKeyboardBuilder(KeyboardBuilder[KeyboardButton]): """ @@ -336,3 +365,13 @@ class ReplyKeyboardBuilder(KeyboardBuilder[KeyboardButton]): :return: """ return ReplyKeyboardBuilder(markup=self.export()) + + @classmethod + def from_markup(cls, markup: ReplyKeyboardMarkup) -> "ReplyKeyboardBuilder": + """ + Create builder from existing markup + + :param markup: + :return: + """ + return cls(markup=markup.keyboard) diff --git a/docs/utils/keyboard.rst b/docs/utils/keyboard.rst index 8988244e..35559fc0 100644 --- a/docs/utils/keyboard.rst +++ b/docs/utils/keyboard.rst @@ -14,6 +14,8 @@ Keyboard builder helps to dynamically generate markup. Usage example ============= +For example you want to generate inline keyboard with 10 buttons + .. code-block:: python builder = InlineKeyboardBuilder() @@ -21,22 +23,45 @@ Usage example for index in range(1, 11): builder.button(text=f"Set {index}", callback_data=f"set:{index}") + +then adjust this buttons to some grid, for example first line will have 3 buttons, the next lines will have 2 buttons + +.. code-block:: + builder.adjust(3, 2) +also you can attach another builder to this one + +.. code-block:: python + + another_builder = InlineKeyboardBuilder(...)... # Another builder with some buttons + builder.attach(another_builder) + +or you can attach some already generated markup + +.. code-block:: python + + markup = InlineKeyboardMarkup(inline_keyboard=[...]) # Some markup + builder.attach(InlineKeyboardBuilder.from_markup(markup)) + +and finally you can export this markup to use it in your message + +.. code-block:: python + await message.answer("Some text here", reply_markup=builder.as_markup()) +Reply keyboard builder has the same interface + +.. warning:: + + Note that you can't attach reply keyboard builder to inline keyboard builder and vice versa -Base builder -============ -.. autoclass:: aiogram.utils.keyboard.ReplyKeyboardBuilder - :members: __init__, buttons, copy, export, add, row, adjust, button, as_markup - :undoc-members: True Inline Keyboard =============== .. autoclass:: aiogram.utils.keyboard.InlineKeyboardBuilder - :noindex: + :members: __init__, buttons, copy, export, add, row, adjust, from_markup, attach .. method:: button(text: str, url: Optional[str] = None, login_url: Optional[LoginUrl] = None, callback_data: Optional[Union[str, CallbackData]] = None, switch_inline_query: Optional[str] = None, switch_inline_query_current_chat: Optional[str] = None, callback_game: Optional[CallbackGame] = None, pay: Optional[bool] = None, **kwargs: Any) -> aiogram.utils.keyboard.InlineKeyboardBuilder :noindex: @@ -52,7 +77,7 @@ Reply Keyboard ============== .. autoclass:: aiogram.utils.keyboard.ReplyKeyboardBuilder - :noindex: + :members: __init__, buttons, copy, export, add, row, adjust, from_markup, attach .. method:: button(text: str, request_contact: Optional[bool] = None, request_location: Optional[bool] = None, request_poll: Optional[KeyboardButtonPollType] = None, **kwargs: Any) -> aiogram.utils.keyboard.ReplyKeyboardBuilder :noindex: diff --git a/tests/test_api/test_types/test_chat_join_request.py b/tests/test_api/test_types/test_chat_join_request.py index a36dd935..ec442cb1 100644 --- a/tests/test_api/test_types/test_chat_join_request.py +++ b/tests/test_api/test_types/test_chat_join_request.py @@ -24,11 +24,7 @@ from aiogram.methods import ( SendVideoNote, SendVoice, ) -from aiogram.types import ( - Chat, - ChatJoinRequest, - User, -) +from aiogram.types import Chat, ChatJoinRequest, User class TestChatJoinRequest: