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:
Alex Root Junior 2021-09-22 00:52:38 +03:00 committed by GitHub
parent 5bd1162f57
commit e4046095d7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
223 changed files with 1909 additions and 1121 deletions

View 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 == "тест"

View 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)