mirror of
https://github.com/aiogram/aiogram.git
synced 2025-12-12 18:19:34 +00:00
Upgrade architecture + 5.0 Bot API (#469)
Upgrade architecture + 5.0 Bot API (#469) * Moved `methods`, `types` and `client` to root package * Removed update handler from routers to dispatcher * Reworked events propagation mechanism to handlers * Reworked inner middlewares logic (very small change) * Updated to Bot API 5.0 * Initial migration from MkDocs to Sphinx + config for readthedocs
This commit is contained in:
parent
566b7ff282
commit
4008a3114d
608 changed files with 12537 additions and 6427 deletions
|
|
@ -2,30 +2,47 @@ import asyncio
|
|||
import datetime
|
||||
import time
|
||||
import warnings
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from aiogram import Bot
|
||||
from aiogram.api.methods import GetMe, GetUpdates, SendMessage
|
||||
from aiogram.api.types import Chat, Message, Update, User
|
||||
from aiogram.dispatcher.dispatcher import Dispatcher
|
||||
from aiogram.dispatcher.event.bases import NOT_HANDLED
|
||||
from aiogram.dispatcher.event.bases import UNHANDLED, SkipHandler
|
||||
from aiogram.dispatcher.middlewares.user_context import UserContextMiddleware
|
||||
from aiogram.dispatcher.router import Router
|
||||
from aiogram.methods import GetMe, GetUpdates, SendMessage
|
||||
from aiogram.types import (
|
||||
CallbackQuery,
|
||||
Chat,
|
||||
ChosenInlineResult,
|
||||
InlineQuery,
|
||||
Message,
|
||||
Poll,
|
||||
PollAnswer,
|
||||
PollOption,
|
||||
PreCheckoutQuery,
|
||||
ShippingAddress,
|
||||
ShippingQuery,
|
||||
Update,
|
||||
User,
|
||||
)
|
||||
from tests.mocked_bot import MockedBot
|
||||
|
||||
try:
|
||||
from asynctest import CoroutineMock, patch
|
||||
except ImportError:
|
||||
from unittest.mock import AsyncMock as CoroutineMock, patch # type: ignore
|
||||
from unittest.mock import AsyncMock as CoroutineMock # type: ignore
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
async def simple_message_handler(message: Message):
|
||||
await asyncio.sleep(1.5)
|
||||
await asyncio.sleep(0.2)
|
||||
return message.answer("ok")
|
||||
|
||||
|
||||
async def invalid_message_handler(message: Message):
|
||||
await asyncio.sleep(1.5)
|
||||
await asyncio.sleep(0.2)
|
||||
raise Exception(42)
|
||||
|
||||
|
||||
|
|
@ -43,6 +60,13 @@ UPDATE = Update(**RAW_UPDATE)
|
|||
|
||||
|
||||
class TestDispatcher:
|
||||
def test_init(self):
|
||||
dp = Dispatcher()
|
||||
|
||||
assert dp.update.handlers
|
||||
assert dp.update.handlers[0].callback == dp._listen_update
|
||||
assert dp.update.outer_middlewares
|
||||
|
||||
def test_parent_router(self):
|
||||
dp = Dispatcher()
|
||||
with pytest.raises(RuntimeError):
|
||||
|
|
@ -145,6 +169,290 @@ class TestDispatcher:
|
|||
|
||||
assert await dispatcher._process_update(bot=bot, update=Update(update_id=42))
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"event_type,update,has_chat,has_user",
|
||||
[
|
||||
pytest.param(
|
||||
"message",
|
||||
Update(
|
||||
update_id=42,
|
||||
message=Message(
|
||||
message_id=42,
|
||||
date=datetime.datetime.now(),
|
||||
text="test",
|
||||
chat=Chat(id=42, type="private"),
|
||||
from_user=User(id=42, is_bot=False, first_name="Test"),
|
||||
),
|
||||
),
|
||||
True,
|
||||
True,
|
||||
),
|
||||
pytest.param(
|
||||
"edited_message",
|
||||
Update(
|
||||
update_id=42,
|
||||
edited_message=Message(
|
||||
message_id=42,
|
||||
date=datetime.datetime.now(),
|
||||
text="edited test",
|
||||
chat=Chat(id=42, type="private"),
|
||||
from_user=User(id=42, is_bot=False, first_name="Test"),
|
||||
),
|
||||
),
|
||||
True,
|
||||
True,
|
||||
),
|
||||
pytest.param(
|
||||
"channel_post",
|
||||
Update(
|
||||
update_id=42,
|
||||
channel_post=Message(
|
||||
message_id=42,
|
||||
date=datetime.datetime.now(),
|
||||
text="test",
|
||||
chat=Chat(id=-42, type="private"),
|
||||
),
|
||||
),
|
||||
True,
|
||||
False,
|
||||
),
|
||||
pytest.param(
|
||||
"edited_channel_post",
|
||||
Update(
|
||||
update_id=42,
|
||||
edited_channel_post=Message(
|
||||
message_id=42,
|
||||
date=datetime.datetime.now(),
|
||||
text="test",
|
||||
chat=Chat(id=-42, type="private"),
|
||||
),
|
||||
),
|
||||
True,
|
||||
False,
|
||||
),
|
||||
pytest.param(
|
||||
"inline_query",
|
||||
Update(
|
||||
update_id=42,
|
||||
inline_query=InlineQuery(
|
||||
id="query id",
|
||||
from_user=User(id=42, is_bot=False, first_name="Test"),
|
||||
query="query",
|
||||
offset="offser",
|
||||
),
|
||||
),
|
||||
False,
|
||||
True,
|
||||
),
|
||||
pytest.param(
|
||||
"chosen_inline_result",
|
||||
Update(
|
||||
update_id=42,
|
||||
chosen_inline_result=ChosenInlineResult(
|
||||
result_id="result id",
|
||||
from_user=User(id=42, is_bot=False, first_name="Test"),
|
||||
query="query",
|
||||
),
|
||||
),
|
||||
False,
|
||||
True,
|
||||
),
|
||||
pytest.param(
|
||||
"callback_query",
|
||||
Update(
|
||||
update_id=42,
|
||||
callback_query=CallbackQuery(
|
||||
id="query id",
|
||||
from_user=User(id=42, is_bot=False, first_name="Test"),
|
||||
chat_instance="instance",
|
||||
data="placeholder",
|
||||
),
|
||||
),
|
||||
False,
|
||||
True,
|
||||
),
|
||||
pytest.param(
|
||||
"callback_query",
|
||||
Update(
|
||||
update_id=42,
|
||||
callback_query=CallbackQuery(
|
||||
id="query id",
|
||||
from_user=User(id=42, is_bot=False, first_name="Test"),
|
||||
chat_instance="instance",
|
||||
data="placeholder",
|
||||
message=Message(
|
||||
message_id=42,
|
||||
date=datetime.datetime.now(),
|
||||
text="test",
|
||||
chat=Chat(id=42, type="private"),
|
||||
from_user=User(id=42, is_bot=False, first_name="Test"),
|
||||
),
|
||||
),
|
||||
),
|
||||
True,
|
||||
True,
|
||||
),
|
||||
pytest.param(
|
||||
"shipping_query",
|
||||
Update(
|
||||
update_id=42,
|
||||
shipping_query=ShippingQuery(
|
||||
id="id",
|
||||
from_user=User(id=42, is_bot=False, first_name="Test"),
|
||||
invoice_payload="payload",
|
||||
shipping_address=ShippingAddress(
|
||||
country_code="placeholder",
|
||||
state="placeholder",
|
||||
city="placeholder",
|
||||
street_line1="placeholder",
|
||||
street_line2="placeholder",
|
||||
post_code="placeholder",
|
||||
),
|
||||
),
|
||||
),
|
||||
False,
|
||||
True,
|
||||
),
|
||||
pytest.param(
|
||||
"pre_checkout_query",
|
||||
Update(
|
||||
update_id=42,
|
||||
pre_checkout_query=PreCheckoutQuery(
|
||||
id="query id",
|
||||
from_user=User(id=42, is_bot=False, first_name="Test"),
|
||||
currency="BTC",
|
||||
total_amount=1,
|
||||
invoice_payload="payload",
|
||||
),
|
||||
),
|
||||
False,
|
||||
True,
|
||||
),
|
||||
pytest.param(
|
||||
"poll",
|
||||
Update(
|
||||
update_id=42,
|
||||
poll=Poll(
|
||||
id="poll id",
|
||||
question="Q?",
|
||||
options=[
|
||||
PollOption(text="A1", voter_count=2),
|
||||
PollOption(text="A2", voter_count=3),
|
||||
],
|
||||
is_closed=False,
|
||||
is_anonymous=False,
|
||||
type="quiz",
|
||||
allows_multiple_answers=False,
|
||||
total_voter_count=0,
|
||||
correct_option_id=1,
|
||||
),
|
||||
),
|
||||
False,
|
||||
False,
|
||||
),
|
||||
pytest.param(
|
||||
"poll_answer",
|
||||
Update(
|
||||
update_id=42,
|
||||
poll_answer=PollAnswer(
|
||||
poll_id="poll id",
|
||||
user=User(id=42, is_bot=False, first_name="Test"),
|
||||
option_ids=[42],
|
||||
),
|
||||
),
|
||||
False,
|
||||
True,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_listen_update(
|
||||
self, event_type: str, update: Update, has_chat: bool, has_user: bool
|
||||
):
|
||||
router = Dispatcher()
|
||||
observer = router.observers[event_type]
|
||||
|
||||
@observer()
|
||||
async def my_handler(event: Any, **kwargs: Any):
|
||||
assert event == getattr(update, event_type)
|
||||
if has_chat:
|
||||
assert Chat.get_current(False)
|
||||
if has_user:
|
||||
assert User.get_current(False)
|
||||
return kwargs
|
||||
|
||||
result = await router.update.trigger(update, test="PASS")
|
||||
assert isinstance(result, dict)
|
||||
assert result["event_update"] == update
|
||||
assert result["event_router"] == router
|
||||
assert result["test"] == "PASS"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_listen_unknown_update(self):
|
||||
dp = Dispatcher()
|
||||
|
||||
with pytest.raises(SkipHandler):
|
||||
await dp._listen_update(Update(update_id=42))
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_listen_unhandled_update(self):
|
||||
dp = Dispatcher()
|
||||
observer = dp.observers["message"]
|
||||
|
||||
@observer(lambda event: False)
|
||||
async def handler(event: Any):
|
||||
pass
|
||||
|
||||
response = await dp._listen_update(
|
||||
Update(
|
||||
update_id=42,
|
||||
poll=Poll(
|
||||
id="poll id",
|
||||
question="Q?",
|
||||
options=[
|
||||
PollOption(text="A1", voter_count=2),
|
||||
PollOption(text="A2", voter_count=3),
|
||||
],
|
||||
is_closed=False,
|
||||
is_anonymous=False,
|
||||
type="quiz",
|
||||
allows_multiple_answers=False,
|
||||
total_voter_count=0,
|
||||
correct_option_id=0,
|
||||
),
|
||||
)
|
||||
)
|
||||
assert response is UNHANDLED
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_nested_router_listen_update(self):
|
||||
dp = Dispatcher()
|
||||
router0 = Router()
|
||||
router1 = Router()
|
||||
dp.include_router(router0)
|
||||
router0.include_router(router1)
|
||||
observer = router1.message
|
||||
|
||||
@observer()
|
||||
async def my_handler(event: Message, **kwargs: Any):
|
||||
return kwargs
|
||||
|
||||
update = Update(
|
||||
update_id=42,
|
||||
message=Message(
|
||||
message_id=42,
|
||||
date=datetime.datetime.now(),
|
||||
text="test",
|
||||
chat=Chat(id=42, type="private"),
|
||||
from_user=User(id=42, is_bot=False, first_name="Test"),
|
||||
),
|
||||
)
|
||||
result = await dp._listen_update(update, test="PASS")
|
||||
assert isinstance(result, dict)
|
||||
assert result["event_update"] == update
|
||||
assert result["event_router"] == router1
|
||||
assert result["test"] == "PASS"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_process_update_call_request(self, bot: MockedBot):
|
||||
dispatcher = Dispatcher()
|
||||
|
|
@ -192,6 +500,45 @@ class TestDispatcher:
|
|||
await dispatcher._polling(bot=bot)
|
||||
mocked_process_update.assert_awaited()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exception_handler_catch_exceptions(self):
|
||||
dp = Dispatcher()
|
||||
router = Router()
|
||||
dp.include_router(router)
|
||||
|
||||
@router.message()
|
||||
async def message_handler(message: Message):
|
||||
raise Exception("KABOOM")
|
||||
|
||||
update = Update(
|
||||
update_id=42,
|
||||
message=Message(
|
||||
message_id=42,
|
||||
date=datetime.datetime.now(),
|
||||
text="test",
|
||||
chat=Chat(id=42, type="private"),
|
||||
from_user=User(id=42, is_bot=False, first_name="Test"),
|
||||
),
|
||||
)
|
||||
with pytest.raises(Exception, match="KABOOM"):
|
||||
await dp.update.trigger(update)
|
||||
|
||||
@router.errors()
|
||||
async def error_handler(event: Update, exception: Exception):
|
||||
return "KABOOM"
|
||||
|
||||
response = await dp.update.trigger(update)
|
||||
assert response == "KABOOM"
|
||||
|
||||
@dp.errors()
|
||||
async def root_error_handler(event: Update, exception: Exception):
|
||||
return exception
|
||||
|
||||
response = await dp.update.trigger(update)
|
||||
|
||||
assert isinstance(response, Exception)
|
||||
assert str(response) == "KABOOM"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_polling(self, bot: MockedBot):
|
||||
dispatcher = Dispatcher()
|
||||
|
|
@ -231,21 +578,11 @@ class TestDispatcher:
|
|||
dispatcher = Dispatcher()
|
||||
dispatcher.message.register(simple_message_handler)
|
||||
|
||||
response = await dispatcher.feed_webhook_update(bot, RAW_UPDATE, _timeout=2)
|
||||
response = await dispatcher.feed_webhook_update(bot, RAW_UPDATE, _timeout=0.3)
|
||||
assert isinstance(response, dict)
|
||||
assert response["method"] == "sendMessage"
|
||||
assert response["text"] == "ok"
|
||||
|
||||
# @pytest.mark.asyncio
|
||||
# async def test_feed_webhook_update_fast_process_error(self, bot: MockedBot):
|
||||
# dispatcher = Dispatcher()
|
||||
# dispatcher.message_handler.register(invalid_message_handler)
|
||||
#
|
||||
# response = await dispatcher.feed_webhook_update(bot, RAW_UPDATE, _timeout=2)
|
||||
# assert isinstance(response, dict)
|
||||
# assert response["method"] == "sendMessage"
|
||||
# assert response["text"] == "ok"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_feed_webhook_update_slow_process(self, bot: MockedBot, recwarn):
|
||||
warnings.simplefilter("always")
|
||||
|
|
@ -257,9 +594,9 @@ class TestDispatcher:
|
|||
"aiogram.dispatcher.dispatcher.Dispatcher._silent_call_request",
|
||||
new_callable=CoroutineMock,
|
||||
) as mocked_silent_call_request:
|
||||
response = await dispatcher.feed_webhook_update(bot, RAW_UPDATE, _timeout=1)
|
||||
response = await dispatcher.feed_webhook_update(bot, RAW_UPDATE, _timeout=0.1)
|
||||
assert response is None
|
||||
await asyncio.sleep(1)
|
||||
await asyncio.sleep(0.2)
|
||||
mocked_silent_call_request.assert_awaited()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -269,9 +606,9 @@ class TestDispatcher:
|
|||
dispatcher = Dispatcher()
|
||||
dispatcher.message.register(invalid_message_handler)
|
||||
|
||||
response = await dispatcher.feed_webhook_update(bot, RAW_UPDATE, _timeout=1)
|
||||
response = await dispatcher.feed_webhook_update(bot, RAW_UPDATE, _timeout=0.1)
|
||||
assert response is None
|
||||
await asyncio.sleep(1)
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
log_records = [rec.message for rec in caplog.records]
|
||||
assert "Cause exception while process update" in log_records[0]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue