Enhance keyboard utility, improved documentation page. (#1236)

* Enhance keyboard utility, improved documentation for this utility.

Updated the 'aiogram/utils/keyboard.py' file with new methods for integrating buttons and keyboard creation more seamlessly. Added functionality to create buttons from existing markup and attach another builder. This improvement aims to make the keyboard building process more user-friendly and flexible.

* Added changelog

* Cover by tests
This commit is contained in:
Alex Root Junior 2023-08-04 22:36:50 +03:00 committed by GitHub
parent 11dc7eaa31
commit 90654ac0fa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 146 additions and 7 deletions

3
CHANGES/1236.feature.rst Normal file
View file

@ -0,0 +1,3 @@
Updated keyboard builders with new methods for integrating buttons and keyboard creation more seamlessly.
Added functionality to create buttons from existing markup and attach another builder.
This improvement aims to make the keyboard building process more user-friendly and flexible.

View file

@ -238,6 +238,12 @@ class KeyboardBuilder(Generic[ButtonType], ABC):
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)
@ -250,6 +256,17 @@ class KeyboardBuilder(Generic[ButtonType], ABC):
inline_keyboard = cast(List[List[InlineKeyboardButton]], self.export()) # type: ignore
return InlineKeyboardMarkup(inline_keyboard=inline_keyboard)
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)
@ -307,6 +324,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]):
"""
@ -340,3 +369,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)

View file

@ -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:

View file

@ -228,3 +228,75 @@ class TestKeyboardBuilder:
)
def test_as_markup(self, builder, expected):
assert isinstance(builder.as_markup(), expected)
@pytest.mark.parametrize(
"markup,builder_type",
[
[
ReplyKeyboardMarkup(
keyboard=[
[KeyboardButton(text="test-1"), KeyboardButton(text="test-2")],
[KeyboardButton(text="test-3")],
]
),
ReplyKeyboardBuilder,
],
[
InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="test-1"), InlineKeyboardButton(text="test-2")],
[InlineKeyboardButton(text="test-3")],
]
),
InlineKeyboardBuilder,
],
],
)
def test_from_markup(self, markup, builder_type):
builder = builder_type.from_markup(markup)
assert len(builder.export()) == 2
assert len(list(builder.buttons)) == 3
def test_attach(self):
builder = ReplyKeyboardBuilder(
markup=[
[
KeyboardButton(text="test1"),
KeyboardButton(text="test2"),
KeyboardButton(text="test3"),
]
]
)
builder.adjust(2)
builder.attach(ReplyKeyboardBuilder(markup=[[KeyboardButton(text="test2")]]))
markup = builder.export()
assert len(markup) == 3
assert len(markup[0]) == 2
assert len(markup[1]) == 1
assert len(markup[2]) == 1
def test_attach_invalid_button_type(self):
builder = ReplyKeyboardBuilder(
markup=[
[
KeyboardButton(text="test1"),
KeyboardButton(text="test2"),
KeyboardButton(text="test3"),
]
]
)
with pytest.raises(ValueError):
builder.attach(InlineKeyboardBuilder(markup=[[InlineKeyboardButton(text="test2")]]))
def test_attach_not_builder(self):
builder = ReplyKeyboardBuilder(
markup=[
[
KeyboardButton(text="test1"),
KeyboardButton(text="test2"),
KeyboardButton(text="test3"),
]
]
)
with pytest.raises(ValueError):
builder.attach(KeyboardButton(text="test2"))