mirror of
https://github.com/aiogram/aiogram.git
synced 2025-12-12 02:03:04 +00:00
Dev 3.x i18n & improvements (#696)
* Added base code and make code improvements * Auto-exclude coverage for `if TYPE_CHECKING:` * Fixed current coverage * Cover I18n module * Update pipeline * Fixed annotations * Added docs * Move exceptions * Added tests for KeyboardBuilder and initial docs * Remove help generator (removed from sources tree, requires rewrite) * Added patch-notes #698, #699, #700, #701, #702, #703
This commit is contained in:
parent
5bd1162f57
commit
e4046095d7
223 changed files with 1909 additions and 1121 deletions
|
|
@ -1,3 +1,5 @@
|
|||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from _pytest.config import UsageError
|
||||
from aioredis.connection import parse_url as parse_redis_url
|
||||
|
|
@ -7,6 +9,8 @@ from aiogram.dispatcher.fsm.storage.memory import MemoryStorage
|
|||
from aiogram.dispatcher.fsm.storage.redis import RedisStorage
|
||||
from tests.mocked_bot import MockedBot
|
||||
|
||||
DATA_DIR = Path(__file__).parent / "data"
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption("--redis", default=None, help="run tests which require redis connection")
|
||||
|
|
|
|||
BIN
tests/data/locales/en/LC_MESSAGES/messages.mo
Normal file
BIN
tests/data/locales/en/LC_MESSAGES/messages.mo
Normal file
Binary file not shown.
2
tests/data/locales/en/LC_MESSAGES/messages.po
Normal file
2
tests/data/locales/en/LC_MESSAGES/messages.po
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
msgid "test"
|
||||
msgstr ""
|
||||
2
tests/data/locales/messages.pot
Normal file
2
tests/data/locales/messages.pot
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
msgid "test"
|
||||
msgstr ""
|
||||
BIN
tests/data/locales/uk/LC_MESSAGES/messages.mo
Normal file
BIN
tests/data/locales/uk/LC_MESSAGES/messages.mo
Normal file
Binary file not shown.
2
tests/data/locales/uk/LC_MESSAGES/messages.po
Normal file
2
tests/data/locales/uk/LC_MESSAGES/messages.po
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
msgid "test"
|
||||
msgstr "тест"
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
import io
|
||||
import os
|
||||
from tempfile import mkstemp
|
||||
|
||||
import aiofiles
|
||||
import pytest
|
||||
|
|
@ -6,6 +8,7 @@ from aresponses import ResponsesMockServer
|
|||
|
||||
from aiogram import Bot
|
||||
from aiogram.client.session.aiohttp import AiohttpSession
|
||||
from aiogram.client.telegram import TelegramAPIServer
|
||||
from aiogram.methods import GetFile, GetMe
|
||||
from aiogram.types import File, PhotoSize
|
||||
from tests.mocked_bot import MockedBot
|
||||
|
|
@ -128,3 +131,15 @@ class TestBot:
|
|||
await bot.download(
|
||||
[PhotoSize(file_id="file id", file_unique_id="file id", width=123, height=123)]
|
||||
)
|
||||
|
||||
async def test_download_local_file(self, bot: MockedBot):
|
||||
bot.session.api = TelegramAPIServer.from_base("http://localhost:8081", is_local=True)
|
||||
fd, tmp = mkstemp(prefix="test-", suffix=".txt")
|
||||
value = b"KABOOM"
|
||||
try:
|
||||
with open(fd, "wb") as f:
|
||||
f.write(value)
|
||||
content = await bot.download_file(tmp)
|
||||
assert content.getvalue() == value
|
||||
finally:
|
||||
os.unlink(tmp)
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ from aresponses import ResponsesMockServer
|
|||
from aiogram import Bot
|
||||
from aiogram.client.session import aiohttp
|
||||
from aiogram.client.session.aiohttp import AiohttpSession
|
||||
from aiogram.exceptions import TelegramNetworkError
|
||||
from aiogram.methods import Request, TelegramMethod
|
||||
from aiogram.types import UNSET, InputFile
|
||||
from aiogram.utils.exceptions.network import NetworkError
|
||||
from tests.mocked_bot import MockedBot
|
||||
|
||||
try:
|
||||
|
|
@ -187,7 +187,7 @@ class TestAiohttpSession:
|
|||
new_callable=CoroutineMock,
|
||||
side_effect=side_effect,
|
||||
):
|
||||
with pytest.raises(NetworkError):
|
||||
with pytest.raises(TelegramNetworkError):
|
||||
await bot.get_me()
|
||||
|
||||
async def test_stream_content(self, aresponses: ResponsesMockServer):
|
||||
|
|
|
|||
|
|
@ -7,16 +7,21 @@ import pytest
|
|||
from aiogram import Bot
|
||||
from aiogram.client.session.base import BaseSession, TelegramType
|
||||
from aiogram.client.telegram import PRODUCTION, TelegramAPIServer
|
||||
from aiogram.exceptions import (
|
||||
RestartingTelegram,
|
||||
TelegramAPIError,
|
||||
TelegramBadRequest,
|
||||
TelegramConflictError,
|
||||
TelegramEntityTooLarge,
|
||||
TelegramForbiddenError,
|
||||
TelegramMigrateToChat,
|
||||
TelegramNotFound,
|
||||
TelegramRetryAfter,
|
||||
TelegramServerError,
|
||||
TelegramUnauthorizedError,
|
||||
)
|
||||
from aiogram.methods import DeleteMessage, GetMe, TelegramMethod
|
||||
from aiogram.types import UNSET, User
|
||||
from aiogram.utils.exceptions.bad_request import BadRequest
|
||||
from aiogram.utils.exceptions.base import TelegramAPIError
|
||||
from aiogram.utils.exceptions.conflict import ConflictError
|
||||
from aiogram.utils.exceptions.network import EntityTooLarge
|
||||
from aiogram.utils.exceptions.not_found import NotFound
|
||||
from aiogram.utils.exceptions.server import RestartingTelegram, ServerError
|
||||
from aiogram.utils.exceptions.special import MigrateToChat, RetryAfter
|
||||
from aiogram.utils.exceptions.unauthorized import UnauthorizedError
|
||||
from tests.mocked_bot import MockedBot
|
||||
|
||||
try:
|
||||
|
|
@ -153,25 +158,25 @@ class TestBaseSession:
|
|||
"status_code,content,error",
|
||||
[
|
||||
[200, '{"ok":true,"result":true}', None],
|
||||
[400, '{"ok":false,"description":"test"}', BadRequest],
|
||||
[400, '{"ok":false,"description":"test"}', TelegramBadRequest],
|
||||
[
|
||||
400,
|
||||
'{"ok":false,"description":"test", "parameters": {"retry_after": 1}}',
|
||||
RetryAfter,
|
||||
TelegramRetryAfter,
|
||||
],
|
||||
[
|
||||
400,
|
||||
'{"ok":false,"description":"test", "parameters": {"migrate_to_chat_id": -42}}',
|
||||
MigrateToChat,
|
||||
TelegramMigrateToChat,
|
||||
],
|
||||
[404, '{"ok":false,"description":"test"}', NotFound],
|
||||
[401, '{"ok":false,"description":"test"}', UnauthorizedError],
|
||||
[403, '{"ok":false,"description":"test"}', UnauthorizedError],
|
||||
[409, '{"ok":false,"description":"test"}', ConflictError],
|
||||
[413, '{"ok":false,"description":"test"}', EntityTooLarge],
|
||||
[404, '{"ok":false,"description":"test"}', TelegramNotFound],
|
||||
[401, '{"ok":false,"description":"test"}', TelegramUnauthorizedError],
|
||||
[403, '{"ok":false,"description":"test"}', TelegramForbiddenError],
|
||||
[409, '{"ok":false,"description":"test"}', TelegramConflictError],
|
||||
[413, '{"ok":false,"description":"test"}', TelegramEntityTooLarge],
|
||||
[500, '{"ok":false,"description":"restarting"}', RestartingTelegram],
|
||||
[500, '{"ok":false,"description":"test"}', ServerError],
|
||||
[502, '{"ok":false,"description":"test"}', ServerError],
|
||||
[500, '{"ok":false,"description":"test"}', TelegramServerError],
|
||||
[502, '{"ok":false,"description":"test"}', TelegramServerError],
|
||||
[499, '{"ok":false,"description":"test"}', TelegramAPIError],
|
||||
[499, '{"ok":false,"description":"test"}', TelegramAPIError],
|
||||
],
|
||||
|
|
|
|||
49
tests/test_dispatcher/test_filters/test_state.py
Normal file
49
tests/test_dispatcher/test_filters/test_state.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
from inspect import isclass
|
||||
|
||||
import pytest
|
||||
|
||||
from aiogram.dispatcher.filters import StateFilter
|
||||
from aiogram.dispatcher.fsm.state import State, StatesGroup
|
||||
from aiogram.types import Update
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
class MyGroup(StatesGroup):
|
||||
state = State()
|
||||
|
||||
|
||||
class TestStateFilter:
|
||||
@pytest.mark.parametrize(
|
||||
"state", [None, State("test"), MyGroup, MyGroup(), "state", ["state"]]
|
||||
)
|
||||
def test_validator(self, state):
|
||||
f = StateFilter(state=state)
|
||||
assert isinstance(f.state, list)
|
||||
value = f.state[0]
|
||||
assert (
|
||||
isinstance(value, (State, str, MyGroup))
|
||||
or (isclass(value) and issubclass(value, StatesGroup))
|
||||
or value is None
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"state,current_state,result",
|
||||
[
|
||||
[State("state"), "@:state", True],
|
||||
[[State("state")], "@:state", True],
|
||||
[MyGroup, "MyGroup:state", True],
|
||||
[[MyGroup], "MyGroup:state", True],
|
||||
[MyGroup(), "MyGroup:state", True],
|
||||
[[MyGroup()], "MyGroup:state", True],
|
||||
["*", "state", True],
|
||||
[None, None, True],
|
||||
[[None], None, True],
|
||||
[None, "state", False],
|
||||
[[], "state", False],
|
||||
],
|
||||
)
|
||||
@pytestmark
|
||||
async def test_filter(self, state, current_state, result):
|
||||
f = StateFilter(state=state)
|
||||
assert bool(await f(obj=Update(update_id=42), raw_state=current_state)) is result
|
||||
0
tests/test_dispatcher/test_middlewares/__init__.py
Normal file
0
tests/test_dispatcher/test_middlewares/__init__.py
Normal file
14
tests/test_dispatcher/test_middlewares/test_user_context.py
Normal file
14
tests/test_dispatcher/test_middlewares/test_user_context.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import pytest
|
||||
|
||||
from aiogram.dispatcher.middlewares.user_context import UserContextMiddleware
|
||||
|
||||
|
||||
async def next_handler(*args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class TestUserContextMiddleware:
|
||||
@pytest.mark.asyncio
|
||||
async def test_unexpected_event_type(self):
|
||||
with pytest.raises(RuntimeError):
|
||||
await UserContextMiddleware()(next_handler, object(), {})
|
||||
139
tests/test_utils/test_i18n.py
Normal file
139
tests/test_utils/test_i18n.py
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
from typing import Any, Dict
|
||||
|
||||
import pytest
|
||||
|
||||
from aiogram import Dispatcher
|
||||
from aiogram.dispatcher.fsm.context import FSMContext
|
||||
from aiogram.dispatcher.fsm.storage.memory import MemoryStorage
|
||||
from aiogram.types import Update, User
|
||||
from aiogram.utils.i18n import ConstI18nMiddleware, FSMI18nMiddleware, I18n, SimpleI18nMiddleware
|
||||
from aiogram.utils.i18n.context import ctx_i18n, get_i18n, gettext, lazy_gettext
|
||||
from tests.conftest import DATA_DIR
|
||||
from tests.mocked_bot import MockedBot
|
||||
|
||||
|
||||
@pytest.fixture(name="i18n")
|
||||
def i18n_fixture() -> I18n:
|
||||
return I18n(path=DATA_DIR / "locales")
|
||||
|
||||
|
||||
class TestI18nCore:
|
||||
def test_init(self, i18n: I18n):
|
||||
assert set(i18n.available_locales) == {"en", "uk"}
|
||||
|
||||
def test_reload(self, i18n: I18n):
|
||||
i18n.reload()
|
||||
assert set(i18n.available_locales) == {"en", "uk"}
|
||||
|
||||
def test_current_locale(self, i18n: I18n):
|
||||
assert i18n.current_locale == "en"
|
||||
i18n.current_locale = "uk"
|
||||
assert i18n.current_locale == "uk"
|
||||
assert i18n.ctx_locale.get() == "uk"
|
||||
|
||||
def test_get_i18n(self, i18n: I18n):
|
||||
with pytest.raises(LookupError):
|
||||
get_i18n()
|
||||
|
||||
token = ctx_i18n.set(i18n)
|
||||
assert get_i18n() == i18n
|
||||
ctx_i18n.reset(token)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"locale,case,result",
|
||||
[
|
||||
[None, dict(singular="test"), "test"],
|
||||
[None, dict(singular="test", locale="uk"), "тест"],
|
||||
["en", dict(singular="test", locale="uk"), "тест"],
|
||||
["uk", dict(singular="test", locale="uk"), "тест"],
|
||||
["uk", dict(singular="test"), "тест"],
|
||||
["it", dict(singular="test"), "test"],
|
||||
[None, dict(singular="test", n=2), "test"],
|
||||
[None, dict(singular="test", n=2, locale="uk"), "тест"],
|
||||
["en", dict(singular="test", n=2, locale="uk"), "тест"],
|
||||
["uk", dict(singular="test", n=2, locale="uk"), "тест"],
|
||||
["uk", dict(singular="test", n=2), "тест"],
|
||||
["it", dict(singular="test", n=2), "test"],
|
||||
[None, dict(singular="test", plural="test2", n=2), "test2"],
|
||||
[None, dict(singular="test", plural="test2", n=2, locale="uk"), "test2"],
|
||||
["en", dict(singular="test", plural="test2", n=2, locale="uk"), "test2"],
|
||||
["uk", dict(singular="test", plural="test2", n=2, locale="uk"), "test2"],
|
||||
["uk", dict(singular="test", plural="test2", n=2), "test2"],
|
||||
["it", dict(singular="test", plural="test2", n=2), "test2"],
|
||||
],
|
||||
)
|
||||
def test_gettext(self, i18n: I18n, locale: str, case: Dict[str, Any], result: str):
|
||||
if locale is not None:
|
||||
i18n.current_locale = locale
|
||||
token = ctx_i18n.set(i18n)
|
||||
try:
|
||||
assert i18n.gettext(**case) == result
|
||||
assert str(i18n.lazy_gettext(**case)) == result
|
||||
assert gettext(**case) == result
|
||||
assert str(lazy_gettext(**case)) == result
|
||||
finally:
|
||||
ctx_i18n.reset(token)
|
||||
|
||||
|
||||
async def next_call(event, data):
|
||||
assert "i18n" in data
|
||||
assert "i18n_middleware" in data
|
||||
return gettext("test")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestSimpleI18nMiddleware:
|
||||
@pytest.mark.parametrize(
|
||||
"event_from_user,result",
|
||||
[
|
||||
[None, "test"],
|
||||
[User(id=42, is_bot=False, language_code="uk", first_name="Test"), "тест"],
|
||||
[User(id=42, is_bot=False, language_code="it", first_name="Test"), "test"],
|
||||
],
|
||||
)
|
||||
async def test_middleware(self, i18n: I18n, event_from_user, result):
|
||||
middleware = SimpleI18nMiddleware(i18n=i18n)
|
||||
result = await middleware(
|
||||
next_call,
|
||||
Update(update_id=42),
|
||||
{"event_from_user": event_from_user},
|
||||
)
|
||||
assert result == result
|
||||
|
||||
async def test_setup(self, i18n: I18n):
|
||||
dp = Dispatcher()
|
||||
middleware = SimpleI18nMiddleware(i18n=i18n)
|
||||
middleware.setup(router=dp)
|
||||
|
||||
assert middleware not in dp.update.outer_middlewares
|
||||
assert middleware in dp.message.outer_middlewares
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestConstI18nMiddleware:
|
||||
async def test_middleware(self, i18n: I18n):
|
||||
middleware = ConstI18nMiddleware(i18n=i18n, locale="uk")
|
||||
result = await middleware(
|
||||
next_call,
|
||||
Update(update_id=42),
|
||||
{"event_from_user": User(id=42, is_bot=False, language_code="it", first_name="Test")},
|
||||
)
|
||||
assert result == "тест"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestFSMI18nMiddleware:
|
||||
async def test_middleware(self, i18n: I18n, bot: MockedBot):
|
||||
middleware = FSMI18nMiddleware(i18n=i18n)
|
||||
storage = MemoryStorage()
|
||||
state = FSMContext(bot=bot, storage=storage, user_id=42, chat_id=42)
|
||||
data = {
|
||||
"event_from_user": User(id=42, is_bot=False, language_code="it", first_name="Test"),
|
||||
"state": state,
|
||||
}
|
||||
result = await middleware(next_call, Update(update_id=42), data)
|
||||
assert result == "test"
|
||||
await middleware.set_locale(state, "uk")
|
||||
assert i18n.current_locale == "uk"
|
||||
result = await middleware(next_call, Update(update_id=42), data)
|
||||
assert result == "тест"
|
||||
226
tests/test_utils/test_keyboard.py
Normal file
226
tests/test_utils/test_keyboard.py
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
import pytest
|
||||
|
||||
from aiogram.dispatcher.filters.callback_data import CallbackData
|
||||
from aiogram.types import (
|
||||
InlineKeyboardButton,
|
||||
InlineKeyboardMarkup,
|
||||
KeyboardButton,
|
||||
ReplyKeyboardMarkup,
|
||||
)
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder, KeyboardBuilder, ReplyKeyboardBuilder
|
||||
|
||||
|
||||
class MyCallback(CallbackData, prefix="test"):
|
||||
value: str
|
||||
|
||||
|
||||
class TestKeyboardBuilder:
|
||||
def test_init(self):
|
||||
with pytest.raises(ValueError):
|
||||
KeyboardBuilder(button_type=object)
|
||||
|
||||
def test_init_success(self):
|
||||
builder = KeyboardBuilder(button_type=KeyboardButton)
|
||||
assert builder._button_type is KeyboardButton
|
||||
builder = InlineKeyboardBuilder()
|
||||
assert builder._button_type is InlineKeyboardButton
|
||||
builder = ReplyKeyboardBuilder()
|
||||
assert builder._button_type is KeyboardButton
|
||||
|
||||
def test_validate_button(self):
|
||||
builder = InlineKeyboardBuilder()
|
||||
with pytest.raises(ValueError):
|
||||
builder._validate_button(button=object())
|
||||
with pytest.raises(ValueError):
|
||||
builder._validate_button(button=KeyboardButton(text="test"))
|
||||
assert builder._validate_button(
|
||||
button=InlineKeyboardButton(text="test", callback_data="callback")
|
||||
)
|
||||
|
||||
def test_validate_buttons(self):
|
||||
builder = InlineKeyboardBuilder()
|
||||
with pytest.raises(ValueError):
|
||||
builder._validate_buttons(object(), object())
|
||||
with pytest.raises(ValueError):
|
||||
builder._validate_buttons(KeyboardButton(text="test"))
|
||||
with pytest.raises(ValueError):
|
||||
builder._validate_buttons(
|
||||
InlineKeyboardButton(text="test", callback_data="callback"),
|
||||
KeyboardButton(text="test"),
|
||||
)
|
||||
assert builder._validate_button(
|
||||
InlineKeyboardButton(text="test", callback_data="callback")
|
||||
)
|
||||
|
||||
def test_validate_row(self):
|
||||
builder = ReplyKeyboardBuilder()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
assert builder._validate_row(
|
||||
row=(KeyboardButton(text=f"test {index}") for index in range(10))
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
assert builder._validate_row(
|
||||
row=[KeyboardButton(text=f"test {index}") for index in range(10)]
|
||||
)
|
||||
|
||||
for count in range(9):
|
||||
assert builder._validate_row(
|
||||
row=[KeyboardButton(text=f"test {index}") for index in range(count)]
|
||||
)
|
||||
|
||||
def test_validate_markup(self):
|
||||
builder = ReplyKeyboardBuilder()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
builder._validate_markup(markup=())
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
builder._validate_markup(
|
||||
markup=[
|
||||
[KeyboardButton(text=f"{row}.{col}") for col in range(8)] for row in range(15)
|
||||
]
|
||||
)
|
||||
|
||||
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):
|
||||
builder = ReplyKeyboardBuilder()
|
||||
with pytest.raises(ValueError):
|
||||
builder._validate_size(None)
|
||||
with pytest.raises(ValueError):
|
||||
builder._validate_size(2.0)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
builder._validate_size(0)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
builder._validate_size(10)
|
||||
for size in range(1, 9):
|
||||
builder._validate_size(size)
|
||||
|
||||
def test_export(self):
|
||||
builder = ReplyKeyboardBuilder(markup=[[KeyboardButton(text="test")]])
|
||||
markup = builder.export()
|
||||
assert id(builder._markup) != id(markup)
|
||||
|
||||
markup.clear()
|
||||
assert len(builder._markup) == 1
|
||||
assert len(markup) == 0
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"builder,button",
|
||||
[
|
||||
[
|
||||
ReplyKeyboardBuilder(markup=[[KeyboardButton(text="test")]]),
|
||||
KeyboardButton(text="test2"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardBuilder(markup=[[InlineKeyboardButton(text="test")]]),
|
||||
InlineKeyboardButton(text="test2"),
|
||||
],
|
||||
[
|
||||
KeyboardBuilder(
|
||||
button_type=InlineKeyboardButton, markup=[[InlineKeyboardButton(text="test")]]
|
||||
),
|
||||
InlineKeyboardButton(text="test2"),
|
||||
],
|
||||
],
|
||||
)
|
||||
def test_copy(self, builder, button):
|
||||
builder1 = builder
|
||||
builder2 = builder1.copy()
|
||||
assert builder1 != builder2
|
||||
|
||||
builder1.add(button)
|
||||
builder2.row(button)
|
||||
|
||||
markup1 = builder1.export()
|
||||
markup2 = builder2.export()
|
||||
assert markup1 != markup2
|
||||
|
||||
assert len(markup1) == 1
|
||||
assert len(markup2) == 2
|
||||
assert len(markup1[0]) == 2
|
||||
assert len(markup2[0]) == 1
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"count,rows,last_columns",
|
||||
[[0, 0, 0], [3, 1, 3], [8, 1, 8], [9, 2, 1], [16, 2, 8], [19, 3, 3]],
|
||||
)
|
||||
def test_add(self, count: int, rows: int, last_columns: int):
|
||||
builder = ReplyKeyboardBuilder()
|
||||
|
||||
for index in range(count):
|
||||
builder.add(KeyboardButton(text=f"btn-{index}"))
|
||||
markup = builder.export()
|
||||
|
||||
assert len(list(builder.buttons)) == count
|
||||
assert len(markup) == rows
|
||||
if last_columns:
|
||||
assert len(markup[-1]) == last_columns
|
||||
|
||||
def test_row(
|
||||
self,
|
||||
):
|
||||
builder = ReplyKeyboardBuilder(markup=[[KeyboardButton(text="test")]])
|
||||
builder.row(*(KeyboardButton(text=f"test-{index}") for index in range(10)), width=3)
|
||||
markup = builder.export()
|
||||
assert len(markup) == 5
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"count,repeat,sizes,shape",
|
||||
[
|
||||
[0, False, [], []],
|
||||
[0, False, [2], []],
|
||||
[1, False, [2], [1]],
|
||||
[3, False, [2], [2, 1]],
|
||||
[10, False, [], [8, 2]],
|
||||
[10, False, [3, 2, 1], [3, 2, 1, 1, 1, 1, 1]],
|
||||
[12, True, [3, 2, 1], [3, 2, 1, 3, 2, 1]],
|
||||
],
|
||||
)
|
||||
def test_adjust(self, count, repeat, sizes, shape):
|
||||
builder = ReplyKeyboardBuilder()
|
||||
builder.row(*(KeyboardButton(text=f"test-{index}") for index in range(count)))
|
||||
builder.adjust(*sizes, repeat=repeat)
|
||||
markup = builder.export()
|
||||
|
||||
assert len(markup) == len(shape)
|
||||
for row, expected_size in zip(markup, shape):
|
||||
assert len(row) == expected_size
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"builder_type,kwargs,expected",
|
||||
[
|
||||
[ReplyKeyboardBuilder, dict(text="test"), KeyboardButton(text="test")],
|
||||
[
|
||||
InlineKeyboardBuilder,
|
||||
dict(text="test", callback_data="callback"),
|
||||
InlineKeyboardButton(text="test", callback_data="callback"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardBuilder,
|
||||
dict(text="test", callback_data=MyCallback(value="test")),
|
||||
InlineKeyboardButton(text="test", callback_data="test:test"),
|
||||
],
|
||||
],
|
||||
)
|
||||
def test_button(self, builder_type, kwargs, expected):
|
||||
builder = builder_type()
|
||||
builder.button(**kwargs)
|
||||
markup = builder.export()
|
||||
assert markup[0][0] == expected
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"builder,expected",
|
||||
[
|
||||
[ReplyKeyboardBuilder(), ReplyKeyboardMarkup],
|
||||
[InlineKeyboardBuilder(), InlineKeyboardMarkup],
|
||||
],
|
||||
)
|
||||
def test_as_markup(self, builder, expected):
|
||||
assert isinstance(builder.as_markup(), expected)
|
||||
Loading…
Add table
Add a link
Reference in a new issue