mirror of
https://github.com/aiogram/aiogram.git
synced 2025-12-14 19:00:23 +00:00
Update KeyboardBuilder utility, fixed type-hints for button method, a… (#1399)
* Update KeyboardBuilder utility, fixed type-hints for button method, adjusted limits of the different markup types to real world values. * Added changelog * Fixed coverage * Update aiogram/utils/keyboard.py Co-authored-by: Suren Khorenyan <surenkhorenyan@gmail.com> * Fixed codestyle --------- Co-authored-by: Suren Khorenyan <surenkhorenyan@gmail.com>
This commit is contained in:
parent
d3c63797b0
commit
844d6f58f5
3 changed files with 108 additions and 76 deletions
1
CHANGES/1399.bugfix.rst
Normal file
1
CHANGES/1399.bugfix.rst
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Update KeyboardBuilder utility, fixed type-hints for button method, adjusted limits of the different markup types to real world values.
|
||||||
|
|
@ -16,7 +16,6 @@ from typing import (
|
||||||
TypeVar,
|
TypeVar,
|
||||||
Union,
|
Union,
|
||||||
cast,
|
cast,
|
||||||
no_type_check,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from aiogram.filters.callback_data import CallbackData
|
from aiogram.filters.callback_data import CallbackData
|
||||||
|
|
@ -26,6 +25,8 @@ from aiogram.types import (
|
||||||
InlineKeyboardMarkup,
|
InlineKeyboardMarkup,
|
||||||
KeyboardButton,
|
KeyboardButton,
|
||||||
KeyboardButtonPollType,
|
KeyboardButtonPollType,
|
||||||
|
KeyboardButtonRequestChat,
|
||||||
|
KeyboardButtonRequestUsers,
|
||||||
LoginUrl,
|
LoginUrl,
|
||||||
ReplyKeyboardMarkup,
|
ReplyKeyboardMarkup,
|
||||||
SwitchInlineQueryChosenChat,
|
SwitchInlineQueryChosenChat,
|
||||||
|
|
@ -34,9 +35,6 @@ from aiogram.types import (
|
||||||
|
|
||||||
ButtonType = TypeVar("ButtonType", InlineKeyboardButton, KeyboardButton)
|
ButtonType = TypeVar("ButtonType", InlineKeyboardButton, KeyboardButton)
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
MAX_WIDTH = 8
|
|
||||||
MIN_WIDTH = 1
|
|
||||||
MAX_BUTTONS = 100
|
|
||||||
|
|
||||||
|
|
||||||
class KeyboardBuilder(Generic[ButtonType], ABC):
|
class KeyboardBuilder(Generic[ButtonType], ABC):
|
||||||
|
|
@ -46,6 +44,10 @@ class KeyboardBuilder(Generic[ButtonType], ABC):
|
||||||
Works both of InlineKeyboardMarkup and ReplyKeyboardMarkup.
|
Works both of InlineKeyboardMarkup and ReplyKeyboardMarkup.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
max_width: int = 0
|
||||||
|
min_width: int = 0
|
||||||
|
max_buttons: int = 0
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, button_type: Type[ButtonType], markup: Optional[List[List[ButtonType]]] = None
|
self, button_type: Type[ButtonType], markup: Optional[List[List[ButtonType]]] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
@ -103,8 +105,8 @@ class KeyboardBuilder(Generic[ButtonType], ABC):
|
||||||
f"Row {row!r} should be type 'List[{self._button_type.__name__}]' "
|
f"Row {row!r} should be type 'List[{self._button_type.__name__}]' "
|
||||||
f"not type {type(row).__name__}"
|
f"not type {type(row).__name__}"
|
||||||
)
|
)
|
||||||
if len(row) > MAX_WIDTH:
|
if len(row) > self.max_width:
|
||||||
raise ValueError(f"Row {row!r} is too long (MAX_WIDTH={MAX_WIDTH})")
|
raise ValueError(f"Row {row!r} is too long (max width: {self.max_width})")
|
||||||
self._validate_buttons(*row)
|
self._validate_buttons(*row)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
@ -125,8 +127,8 @@ class KeyboardBuilder(Generic[ButtonType], ABC):
|
||||||
for row in markup:
|
for row in markup:
|
||||||
self._validate_row(row)
|
self._validate_row(row)
|
||||||
count += len(row)
|
count += len(row)
|
||||||
if count > MAX_BUTTONS:
|
if count > self.max_buttons:
|
||||||
raise ValueError(f"Too much buttons detected Max allowed count - {MAX_BUTTONS}")
|
raise ValueError(f"Too much buttons detected Max allowed count - {self.max_buttons}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _validate_size(self, size: Any) -> int:
|
def _validate_size(self, size: Any) -> int:
|
||||||
|
|
@ -138,18 +140,12 @@ class KeyboardBuilder(Generic[ButtonType], ABC):
|
||||||
"""
|
"""
|
||||||
if not isinstance(size, int):
|
if not isinstance(size, int):
|
||||||
raise ValueError("Only int sizes are allowed")
|
raise ValueError("Only int sizes are allowed")
|
||||||
if size not in range(MIN_WIDTH, MAX_WIDTH + 1):
|
if size not in range(self.min_width, self.max_width + 1):
|
||||||
raise ValueError(f"Row size {size} are not allowed")
|
raise ValueError(
|
||||||
|
f"Row size {size} is not allowed, range: [{self.min_width}, {self.max_width}]"
|
||||||
|
)
|
||||||
return size
|
return size
|
||||||
|
|
||||||
def copy(self: "KeyboardBuilder[ButtonType]") -> "KeyboardBuilder[ButtonType]":
|
|
||||||
"""
|
|
||||||
Make full copy of current builder with markup
|
|
||||||
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return self.__class__(self._button_type, markup=self.export())
|
|
||||||
|
|
||||||
def export(self) -> List[List[ButtonType]]:
|
def export(self) -> List[List[ButtonType]]:
|
||||||
"""
|
"""
|
||||||
Export configured markup as list of lists of buttons
|
Export configured markup as list of lists of buttons
|
||||||
|
|
@ -175,21 +171,23 @@ class KeyboardBuilder(Generic[ButtonType], ABC):
|
||||||
markup = self.export()
|
markup = self.export()
|
||||||
|
|
||||||
# Try to add new buttons to the end of last row if it possible
|
# Try to add new buttons to the end of last row if it possible
|
||||||
if markup and len(markup[-1]) < MAX_WIDTH:
|
if markup and len(markup[-1]) < self.max_width:
|
||||||
last_row = markup[-1]
|
last_row = markup[-1]
|
||||||
pos = MAX_WIDTH - len(last_row)
|
pos = self.max_width - len(last_row)
|
||||||
head, buttons = buttons[:pos], buttons[pos:]
|
head, buttons = buttons[:pos], buttons[pos:]
|
||||||
last_row.extend(head)
|
last_row.extend(head)
|
||||||
|
|
||||||
# Separate buttons to exclusive rows with max possible row width
|
# Separate buttons to exclusive rows with max possible row width
|
||||||
while buttons:
|
while buttons:
|
||||||
row, buttons = buttons[:MAX_WIDTH], buttons[MAX_WIDTH:]
|
row, buttons = buttons[: self.max_width], buttons[self.max_width :]
|
||||||
markup.append(list(row))
|
markup.append(list(row))
|
||||||
|
|
||||||
self._markup = markup
|
self._markup = markup
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def row(self, *buttons: ButtonType, width: int = MAX_WIDTH) -> "KeyboardBuilder[ButtonType]":
|
def row(
|
||||||
|
self, *buttons: ButtonType, width: Optional[int] = None
|
||||||
|
) -> "KeyboardBuilder[ButtonType]":
|
||||||
"""
|
"""
|
||||||
Add row to markup
|
Add row to markup
|
||||||
|
|
||||||
|
|
@ -199,6 +197,9 @@ class KeyboardBuilder(Generic[ButtonType], ABC):
|
||||||
:param width:
|
:param width:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
if width is None:
|
||||||
|
width = self.max_width
|
||||||
|
|
||||||
self._validate_size(width)
|
self._validate_size(width)
|
||||||
self._validate_buttons(*buttons)
|
self._validate_buttons(*buttons)
|
||||||
self._markup.extend(
|
self._markup.extend(
|
||||||
|
|
@ -220,7 +221,7 @@ class KeyboardBuilder(Generic[ButtonType], ABC):
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
if not sizes:
|
if not sizes:
|
||||||
sizes = (MAX_WIDTH,)
|
sizes = (self.max_width,)
|
||||||
|
|
||||||
validated_sizes = map(self._validate_size, sizes)
|
validated_sizes = map(self._validate_size, sizes)
|
||||||
sizes_iter = repeat_all(validated_sizes) if repeat else repeat_last(validated_sizes)
|
sizes_iter = repeat_all(validated_sizes) if repeat else repeat_last(validated_sizes)
|
||||||
|
|
@ -239,7 +240,7 @@ class KeyboardBuilder(Generic[ButtonType], ABC):
|
||||||
self._markup = markup
|
self._markup = markup
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def button(self, **kwargs: Any) -> "KeyboardBuilder[ButtonType]":
|
def _button(self, **kwargs: Any) -> "KeyboardBuilder[ButtonType]":
|
||||||
"""
|
"""
|
||||||
Add button to markup
|
Add button to markup
|
||||||
|
|
||||||
|
|
@ -293,25 +294,40 @@ class InlineKeyboardBuilder(KeyboardBuilder[InlineKeyboardButton]):
|
||||||
Inline keyboard builder inherits all methods from generic builder
|
Inline keyboard builder inherits all methods from generic builder
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
max_width: int = 8
|
||||||
|
min_width: int = 1
|
||||||
|
max_buttons: int = 100
|
||||||
|
|
||||||
@no_type_check
|
def button(
|
||||||
def button(
|
self,
|
||||||
self,
|
*,
|
||||||
*,
|
text: str,
|
||||||
text: str,
|
url: Optional[str] = None,
|
||||||
url: Optional[str] = None,
|
callback_data: Optional[Union[str, CallbackData]] = None,
|
||||||
callback_data: Optional[Union[str, CallbackData]] = None,
|
web_app: Optional[WebAppInfo] = None,
|
||||||
web_app: Optional[WebAppInfo] = None,
|
login_url: Optional[LoginUrl] = None,
|
||||||
login_url: Optional[LoginUrl] = None,
|
switch_inline_query: Optional[str] = None,
|
||||||
switch_inline_query: Optional[str] = None,
|
switch_inline_query_current_chat: Optional[str] = None,
|
||||||
switch_inline_query_current_chat: Optional[str] = None,
|
switch_inline_query_chosen_chat: Optional[SwitchInlineQueryChosenChat] = None,
|
||||||
switch_inline_query_chosen_chat: Optional[SwitchInlineQueryChosenChat] = None,
|
callback_game: Optional[CallbackGame] = None,
|
||||||
callback_game: Optional[CallbackGame] = None,
|
pay: Optional[bool] = None,
|
||||||
pay: Optional[bool] = None,
|
**kwargs: Any,
|
||||||
**kwargs: Any,
|
) -> "KeyboardBuilder[InlineKeyboardButton]":
|
||||||
) -> "KeyboardBuilder[InlineKeyboardButton]":
|
return self._button(
|
||||||
...
|
text=text,
|
||||||
|
url=url,
|
||||||
|
callback_data=callback_data,
|
||||||
|
web_app=web_app,
|
||||||
|
login_url=login_url,
|
||||||
|
switch_inline_query=switch_inline_query,
|
||||||
|
switch_inline_query_current_chat=switch_inline_query_current_chat,
|
||||||
|
switch_inline_query_chosen_chat=switch_inline_query_chosen_chat,
|
||||||
|
callback_game=callback_game,
|
||||||
|
pay=pay,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
|
||||||
def as_markup(self, **kwargs: Any) -> InlineKeyboardMarkup:
|
def as_markup(self, **kwargs: Any) -> InlineKeyboardMarkup:
|
||||||
"""Construct an InlineKeyboardMarkup"""
|
"""Construct an InlineKeyboardMarkup"""
|
||||||
|
|
@ -346,22 +362,34 @@ class ReplyKeyboardBuilder(KeyboardBuilder[KeyboardButton]):
|
||||||
Reply keyboard builder inherits all methods from generic builder
|
Reply keyboard builder inherits all methods from generic builder
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
max_width: int = 10
|
||||||
|
min_width: int = 1
|
||||||
|
max_buttons: int = 300
|
||||||
|
|
||||||
@no_type_check
|
def button(
|
||||||
def button(
|
self,
|
||||||
self,
|
*,
|
||||||
*,
|
text: str,
|
||||||
text: str,
|
request_users: Optional[KeyboardButtonRequestUsers] = None,
|
||||||
request_user: Optional[bool] = None,
|
request_chat: Optional[KeyboardButtonRequestChat] = None,
|
||||||
request_chat: Optional[bool] = None,
|
request_contact: Optional[bool] = None,
|
||||||
request_contact: Optional[bool] = None,
|
request_location: Optional[bool] = None,
|
||||||
request_location: Optional[bool] = None,
|
request_poll: Optional[KeyboardButtonPollType] = None,
|
||||||
request_poll: Optional[KeyboardButtonPollType] = None,
|
web_app: Optional[WebAppInfo] = None,
|
||||||
web_app: Optional[WebAppInfo] = None,
|
**kwargs: Any,
|
||||||
**kwargs: Any,
|
) -> "KeyboardBuilder[KeyboardButton]":
|
||||||
) -> "KeyboardBuilder[KeyboardButton]":
|
return self._button(
|
||||||
...
|
text=text,
|
||||||
|
request_users=request_users,
|
||||||
|
request_chat=request_chat,
|
||||||
|
request_contact=request_contact,
|
||||||
|
request_location=request_location,
|
||||||
|
request_poll=request_poll,
|
||||||
|
web_app=web_app,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
|
||||||
def as_markup(self, **kwargs: Any) -> ReplyKeyboardMarkup:
|
def as_markup(self, **kwargs: Any) -> ReplyKeyboardMarkup:
|
||||||
...
|
...
|
||||||
|
|
|
||||||
|
|
@ -61,36 +61,38 @@ class TestKeyboardBuilder:
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
assert builder._validate_row(
|
assert builder._validate_row(
|
||||||
row=(KeyboardButton(text=f"test {index}") for index in range(10))
|
row=(
|
||||||
|
KeyboardButton(text=f"test {index}") for index in range(builder.max_width + 5)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
assert builder._validate_row(
|
assert builder._validate_row(
|
||||||
row=[KeyboardButton(text=f"test {index}") for index in range(10)]
|
row=[
|
||||||
|
KeyboardButton(text=f"test {index}") for index in range(builder.max_width + 5)
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
for count in range(9):
|
for count in range(11):
|
||||||
assert builder._validate_row(
|
assert builder._validate_row(
|
||||||
row=[KeyboardButton(text=f"test {index}") for index in range(count)]
|
row=[KeyboardButton(text=f"test {index}") for index in range(count)]
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_validate_markup(self):
|
def test_validate_markup_invalid_type(self):
|
||||||
builder = ReplyKeyboardBuilder()
|
builder = ReplyKeyboardBuilder()
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
builder._validate_markup(markup=())
|
builder._validate_markup(markup=())
|
||||||
|
|
||||||
|
def test_validate_markup_too_many_buttons(self):
|
||||||
|
builder = ReplyKeyboardBuilder()
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
builder._validate_markup(
|
builder._validate_markup(
|
||||||
markup=[
|
markup=[
|
||||||
[KeyboardButton(text=f"{row}.{col}") for col in range(8)] for row in range(15)
|
[KeyboardButton(text=f"{row}.{col}") for col in range(builder.max_width)]
|
||||||
|
for row in range(builder.max_buttons)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
assert builder._validate_markup(
|
|
||||||
markup=[[KeyboardButton(text=f"{row}.{col}") for col in range(8)] for row in range(8)]
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_validate_size(self):
|
def test_validate_size(self):
|
||||||
builder = ReplyKeyboardBuilder()
|
builder = ReplyKeyboardBuilder()
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
|
|
@ -102,7 +104,7 @@ class TestKeyboardBuilder:
|
||||||
builder._validate_size(0)
|
builder._validate_size(0)
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
builder._validate_size(10)
|
builder._validate_size(builder.max_width + 5)
|
||||||
for size in range(1, 9):
|
for size in range(1, 9):
|
||||||
builder._validate_size(size)
|
builder._validate_size(size)
|
||||||
|
|
||||||
|
|
@ -126,12 +128,6 @@ class TestKeyboardBuilder:
|
||||||
InlineKeyboardBuilder(markup=[[InlineKeyboardButton(text="test")]]),
|
InlineKeyboardBuilder(markup=[[InlineKeyboardButton(text="test")]]),
|
||||||
InlineKeyboardButton(text="test2"),
|
InlineKeyboardButton(text="test2"),
|
||||||
],
|
],
|
||||||
[
|
|
||||||
KeyboardBuilder(
|
|
||||||
button_type=InlineKeyboardButton, markup=[[InlineKeyboardButton(text="test")]]
|
|
||||||
),
|
|
||||||
InlineKeyboardButton(text="test2"),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_copy(self, builder, button):
|
def test_copy(self, builder, button):
|
||||||
|
|
@ -153,7 +149,14 @@ class TestKeyboardBuilder:
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"count,rows,last_columns",
|
"count,rows,last_columns",
|
||||||
[[0, 0, 0], [3, 1, 3], [8, 1, 8], [9, 2, 1], [16, 2, 8], [19, 3, 3]],
|
[
|
||||||
|
[0, 0, 0],
|
||||||
|
[3, 1, 3],
|
||||||
|
[8, 1, 8],
|
||||||
|
[12, 2, 2],
|
||||||
|
[16, 2, 6],
|
||||||
|
[22, 3, 2],
|
||||||
|
],
|
||||||
)
|
)
|
||||||
def test_add(self, count: int, rows: int, last_columns: int):
|
def test_add(self, count: int, rows: int, last_columns: int):
|
||||||
builder = ReplyKeyboardBuilder()
|
builder = ReplyKeyboardBuilder()
|
||||||
|
|
@ -182,8 +185,8 @@ class TestKeyboardBuilder:
|
||||||
[0, False, [2], []],
|
[0, False, [2], []],
|
||||||
[1, False, [2], [1]],
|
[1, False, [2], [1]],
|
||||||
[3, False, [2], [2, 1]],
|
[3, False, [2], [2, 1]],
|
||||||
[10, False, [], [8, 2]],
|
|
||||||
[10, False, [3, 2, 1], [3, 2, 1, 1, 1, 1, 1]],
|
[10, False, [3, 2, 1], [3, 2, 1, 1, 1, 1, 1]],
|
||||||
|
[12, False, [], [10, 2]],
|
||||||
[12, True, [3, 2, 1], [3, 2, 1, 3, 2, 1]],
|
[12, True, [3, 2, 1], [3, 2, 1, 3, 2, 1]],
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue