diff --git a/docs/dispatcher/router.rst b/docs/dispatcher/router.rst index 40d91769..3550ea8f 100644 --- a/docs/dispatcher/router.rst +++ b/docs/dispatcher/router.rst @@ -7,6 +7,8 @@ Router :show-inheritance: +.. _Event observers: + Event observers =============== @@ -24,13 +26,17 @@ Update .. code-block:: python - @router.update() + @dispatcher.update() async def message_handler(update: types.Update) -> Any: pass .. note:: By default Router already has an update handler which route all event types to another observers. +.. warning:: + + The only root Router (Dispatcher) can handle this type of event. + Message ------- @@ -146,6 +152,7 @@ Errors Is useful for handling errors from other handlers +.. _Nested routers: Nested routers ============== diff --git a/docs/index.rst b/docs/index.rst index 2d007e88..7a4481b2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -12,6 +12,7 @@ Contents :maxdepth: 3 install + migration_2_to_3 api/index dispatcher/index utils/index diff --git a/docs/migration_2_to_3.rst b/docs/migration_2_to_3.rst new file mode 100644 index 00000000..a22e1162 --- /dev/null +++ b/docs/migration_2_to_3.rst @@ -0,0 +1,347 @@ +========================== +Migration FAQ (2.x -> 3.0) +========================== + +aiogram v3 introduced many breaking changes. This guide will help you to migrate from your existing v2.x code to new v3.0 + +.. danger:: + + This guide is still in progress. + +Dispatcher +========== + +- :class:`Dispatcher` class no longer accepts the `Bot` instance into the initializer, it should be passed to dispatcher only for starting polling or handling event from webhook. +- :class:`Dispatcher` now can be extended with another Dispatcher-like thing named :class:`Router` (:ref:`Read more » `) +- Removed the **_handler** suffix from all event handler decorators and registering methods. (:ref:`Read more » `) +- Executor entirely removed, now you can use Dispatcher directly to start polling or webhook. + +Filtering events +================ + +- Keyword filters can no more be used, use filters explicitly. (`Read more » `_) +- Most of common filters is replaced by "magic filter". (:ref:`Read more » `) +- Now by default message handler receives any content type, if you want specific one just add the filters (Magic or any other) + + +Middlewares +=========== + +(:ref:`Read more » `) + +- Middlewares is now can control a execution context +- + +Markups +======= + +- + +Callbacks data +============== + +- + +Finite State machine +==================== + +- State filter will no more added to all handlers, you will need to specify state if you want + + + +Sending Files +============= + +- + +Webhook +======= + +- + + + +Draft +===== + +This part of document will be removed + +.. code-block:: markdown + + ## Keyboards and filters (part 1) + + - `(Reply|Inline)KeyboardMarkup` is no longer used for building keyboards via `add`/`insert`/`row`, use `(Reply|Inline)KeyboardBuilder` and `button` instead. + - `CallbackData` is now a base class, not a factory. + - Integrate `[magic-filter](https://pypi.org/project/magic-filter/)` into aiogram. + - Code for 2.x + + ```python + from secrets import token_urlsafe + + from aiogram import Bot, Dispatcher + from aiogram.types import ( + CallbackQuery, + InlineKeyboardButton, + InlineKeyboardMarkup, + Message, + ) + from aiogram.utils.callback_data import CallbackData + + dp = Dispatcher(Bot(TOKEN)) + + vote_cb = CallbackData("vote", "action", "id", sep="_") + votes = {} # For demo purposes only! Use database in real code! + + @dp.message_handler(commands="start") + async def post(message: Message) -> None: + vote_id = token_urlsafe(8) # Lazy way to generate a random string + kb = ( + InlineKeyboardMarkup(row_width=2) + .insert(InlineKeyboardButton(text="+1", callback_data=vote_cb.new(action="up", id=vote_id))) + .insert(InlineKeyboardButton(text="-1", callback_data=vote_cb.new(action="down", id=vote_id))) + .insert(InlineKeyboardButton(text="?", callback_data=vote_cb.new(action="count", id=vote_id))) + ) + await message.reply("Vote on this post", reply_markup=kb) + + @dp.callback_query_handler(vote_cb.filter(action="count")) + async def show_voters_count(query: CallbackQuery, callback_data: dict) -> None: + vote_id = int(callback_data["id"]) + votes[vote_id] = votes.setdefault(vote_id, 0) + 1 + await query.answer(votes[vote_id], cache_time=1) + + @dp.callback_query_handler(vote_cb.filter()) # all other actions + async def vote(query: CallbackQuery, callback_data: dict) -> None: + if (action := callback_data["action"]) == "up": + d = 1 + elif action == "down": + d = -1 + else: + raise AssertionError(f"action action!r} is not implemented") + votes[int(callback_data["id"])] += d + await query.answer(f"{action.capitalize()}voted!") + ``` + + - Code for 3.0 + + ```python + from enum import Enum + from secrets import token_urlsafe + + from aiogram import Dispatcher, F + from aiogram.types import CallbackQuery, Message + from aiogram.dispatcher.filters.callback_data import CallbackData + from aiogram.utils.keyboard import InlineKeyboardBuilder + + dp = Dispatcher() + votes = {} # For demo purposes only! Use database in real code! + + class VoteAction(Enum): + UP = "up" + DOWN = "down" + COUNT = "count" + + class VoteCallback(CallbackData, prefix="vote", sep="_"): + action: VoteAction # Yes, it also supports `Enum`s + id: str + + @dp.message(commands="start") + async def post(message: Message) -> None: + vote_id = token_urlsafe(8) # Lazy way to generate a random string + kb = ( + InlineKeyboardBuilder() + .button(text="+1", callback_data=VoteCallback(action=VoteAction.UP, id=vote_id)) + .button(text="-1", callback_data=VoteCallback(action=VoteAction.DOWN, id=vote_id)) + .button(text="?", callback_data=VoteCallback(action=VoteAction.COUNT, id=vote_id)) + .adjust(2) # row_width=2 + ) + await message.reply("Vote on this post", reply_markup=kb.as_markup()) + + # `F` is a `MagicFilter` instance, see docs for `magic-filter` for more info + @dp.callback_query(VoteCallback.filter(F.action == VoteAction.COUNT)) + async def show_voters_count( + query: CallbackQuery, + callback_data: VoteCallback, # Now it is the class itself, not a mysterious `dict` + ) -> None: + vote_id = callback_data.id + votes[vote_id] = votes.setdefault(vote_id, 0) + 1 + await query.answer(votes[vote_id], cache_time=1) + + @dp.callback_query(VoteCallback.filter()) # all other actions + async def vote(query: CallbackQuery, callback_data: VoteCallback) -> None: + if callback_data.action == VoteAction.UP: + d = 1 + elif callback_data.action == VoteAction.DOWN: + d = -1 + else: + raise AssertionError(f"action {callback_data.action!r} is not implemented") + votes[callback_data.id] += d + await query.answer(f"{action.capitalize()}voted!") + ``` + + + ## Code style + + - Allow the code to be split into several files in a convenient way with `Router`s. + - Make `Dispatcher` a router with some special abilities. + - Remove `_handler` in favor of `` (e.g. `dp.message()` instead of `dp.message_handler()`) + - Code for 2.x (one of possible ways) + + ```python + from aiogram import Bot, Dispatcher, executor + from mybot import handlers + + dp = Dispatcher(Bot(TOKEN)) + + handlers.hello.setup(dp) + ... + + executor.start_polling(dp, ...) + ``` + + ```python + from aiogram import Dispatcher + from aiogram.types import Message + + # No way to use decorators :( + async def hello(message: Message) -> None: + await message.reply("Hello!") + + async def goodbye(message: Message) -> None: + await message.reply("Bye!") + + def setup(dp: Dispatcher) -> None: + dp.register_message_handler(hello, commands=["hello", "hi"]) + dp.register_message_handler(goodbye, commands=["goodbye", "bye"]) + # This list can become huge in a time, may be inconvenient + ``` + + - Code for 3.0 + + ```python + from aiogram import Bot, Dispatcher + from mybot import handlers + + dp = Dispatcher() + + # Any router can include a sub-router + dp.include_router(handlers.hello.router) # `Dispatcher` is a `Router` too + ... + + dp.run_polling(Bot(TOKEN)) # But it's special, e.g. it can `run_polling` + ``` + + ```python + from aiogram import Router + from aiogram.types import Message + + router = Router() + + # Event handler decorator is an event type itself without `_handler` suffix + @router.message(commands=["hello", "hi"]) # Yay, decorators! + async def hello(message: Message) -> None: + await message.reply("Hello!") + + async def goodbye(message: Message) -> None: + await message.reply("Bye!") + + # If you still prefer registering handlers without decorators, use this + router.message.register(goodbye, commands=["goodbye", "bye"]) + ``` + + + ## Webhooks and API methods + + - All methods are classes now. + - Allow using Reply into webhook with polling. *Check whether it worked in 2.x* + - Webhook setup is more flexible ~~and complicated xd~~ + + ## Exceptions + + - No more specific exceptions, only by status code. + - Code for 2.x [todo] + + ```python + from asyncio import sleep + + from aiogram import Bot, Dispatcher + from aiogram.dispatcher.filters import Command + from aiogram.types import Message + from aiogram.utils.exceptions import ( + BadRequest, + BotBlocked, + RestartingTelegram, + RetryAfter, + ) + + dp = Dispatcher(Bot(TOKEN)) + chats = set() + + async def broadcaster(bot: Bot, chat: int, text: str) -> bool: + """Broadcasts a message and returns whether it was sent""" + while True: + try: + await bot.send_message(chat, text) + except BotBlocked: + chats.discard(chat) + log.warning("Remove chat %d because bot was blocked", chat) + return False + except RetryAfter as e: + log.info("Sleeping %d due to flood wait", e.retry_after) + await sleep(e.retry_after) + continue + except RestartingTelegram: + log.info("Telegram is restarting, sleeping for 1 sec") + await sleep(1) + continue + except BadRequest as e: + log.warning("Remove chat %d because of bad request", chat) + chats.discard(chat) + return False + else: + return True + + @dp.message_handler(commands="broadcast") + async def broadcast(message: Message, command: Command.CommandObj) -> None: + # TODO ... + ``` + + - Code for 3.x [todo] + + ```python + ... + ``` + + + ## Filters (part 2) + + - Remove the majority of filters in favor of `MagicFilter` (aka `F`). + - Deprecate usage of bound filters in favor of classes, functions and `F`. + - Message handler defaults to any content type. + - Per-router filters. + + ## Middlewares and app state + + - Rework middleware logic. + - Pass `**kwargs` from `start_polling` to handlers and filters. + - No more global `bot` and `message.bot`. + - `bot["foo"]` → `dp["foo"]`. + + ## FSM + + - FSMStrategy. + - Default to any state. + - States are also callable filters. + - No more `next` and `proxy`. + - No state filtering is done by default: + + [Default state is not None · Issue #954 · aiogram/aiogram](https://github.com/aiogram/aiogram/issues/954#issuecomment-1172967490) + + + ## Misc + + - No more unrelated attributes and methods in types. + - `get_args()` + - `get_(full_)command()` + - …? + - Add handler flags. + - ???