From d42d4e97f4904b1d21d1fb95cedab583348b1646 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 12 Mar 2023 03:03:02 +0200 Subject: [PATCH] Update migration guide --- docs/api/methods/set_sticker_set_thumb.rst | 44 -- docs/dispatcher/filters/callback_data.rst | 2 + docs/dispatcher/middlewares.rst | 2 + docs/migration_2_to_3.rst | 581 +++++++++++---------- docs/utils/keyboard.rst | 1 + 5 files changed, 311 insertions(+), 319 deletions(-) delete mode 100644 docs/api/methods/set_sticker_set_thumb.rst diff --git a/docs/api/methods/set_sticker_set_thumb.rst b/docs/api/methods/set_sticker_set_thumb.rst deleted file mode 100644 index 133fad94..00000000 --- a/docs/api/methods/set_sticker_set_thumb.rst +++ /dev/null @@ -1,44 +0,0 @@ -################## -setStickerSetThumb -################## - -Returns: :obj:`bool` - -.. automodule:: aiogram.methods.set_sticker_set_thumb - :members: - :member-order: bysource - :undoc-members: True - - -Usage -===== - -As bot method -------------- - -.. code-block:: - - result: bool = await bot.set_sticker_set_thumb(...) - - -Method as object ----------------- - -Imports: - -- :code:`from aiogram.methods.set_sticker_set_thumb import SetStickerSetThumb` -- alias: :code:`from aiogram.methods import SetStickerSetThumb` - -With specific bot -~~~~~~~~~~~~~~~~~ - -.. code-block:: python - - result: bool = await bot(SetStickerSetThumb(...)) - -As reply into Webhook in handler -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: python - - return SetStickerSetThumb(...) diff --git a/docs/dispatcher/filters/callback_data.rst b/docs/dispatcher/filters/callback_data.rst index 9ba67331..cbf51a79 100644 --- a/docs/dispatcher/filters/callback_data.rst +++ b/docs/dispatcher/filters/callback_data.rst @@ -1,3 +1,5 @@ +.. _callback-data-factory + ============================== Callback Data Factory & Filter ============================== diff --git a/docs/dispatcher/middlewares.rst b/docs/dispatcher/middlewares.rst index 55875eb2..56d07ef1 100644 --- a/docs/dispatcher/middlewares.rst +++ b/docs/dispatcher/middlewares.rst @@ -1,3 +1,5 @@ +.. _middlewares: + =========== Middlewares =========== diff --git a/docs/migration_2_to_3.rst b/docs/migration_2_to_3.rst index a22e1162..a9d476cd 100644 --- a/docs/migration_2_to_3.rst +++ b/docs/migration_2_to_3.rst @@ -2,346 +2,377 @@ 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. +This version introduces much many breaking changes and architectural improvements, +helping to reduce global variables count in your code, provides mechanisms useful mechanisms +to separate your code to modules or just make sharable modules via packages on the PyPi, +makes middlewares and filters more controllable and others. + +On this page you can read about points that changed corresponding to last stable 2.x version. + 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 » `) +- :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. + Also this way adds possibility to use multiple bot instances at the same time ("multibot") +- :class:`Dispatcher` now can be extended with another Dispatcher-like + thing named :class:`Router` (:ref:`Read more » `). + With routes you can easily separate your code to multiple modules + and maybe share this modules between projects. +- 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 » `_) +- In due to keyword filters was removed all enabled by default filters (state and content_type now is not enabled) - 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) +- State filter is not enabled by default, that's mean if you using :code:`state="*"` in v2 + then you should not pass any state filter in v3, and vice versa, + if the state in v2 is not specified now you should specify the state. +- Added possibility to register per-router global filters, that helps to reduces + the number of repetitions in the code and makes easily way to control + for what each router will be used. +Bot API Methods +=============== + +- Now all API methods is classes with validation (via `pydantic `_) + (all API calls is also available as methods in the Bot class) +- Increased pre-defined Enums count and moved into `aiogram.enums` sub-package. +- Separated HTTP client session into container that can be reused between different + Bot instances in the application. Middlewares =========== -(:ref:`Read more » `) +- Middlewares can now control a execution context, e.g. using context managers (:ref:`Read more » `) +- All contextual data now is shared between middlewares, filters and handlers to end-to-end use. + For example now you can easily pass some data into context inside middleware and + get it in the filters layer as the same way as in the handlers via keyword arguments. +- Added mechanism named **flags**, that helps to customize handler behavior + in conjunction with middlewares. -- Middlewares is now can control a execution context -- +Keyboard Markup +=============== -Markups -======= +- Now :class:`aiogram.types.inline_keyboard_markup.InlineKeyboardMarkup` + and :class:`aiogram.types.reply_keyboard_markup.ReplyKeyboardMarkup` has no methods to extend it, + instead you have to use markup builders :class:`aiogram.utils.keyboard.ReplyKeyboardBuilder` + and :class:`aiogram.utils.keyboard.KeyboardBuilder` respectively + (:ref:`Read more » `) -- Callbacks data ============== -- +- Callback data factory now is strictly typed via `pydantic `_ models + (:ref:`Read more » `) Finite State machine ==================== - State filter will no more added to all handlers, you will need to specify state if you want - - Sending Files ============= -- +- From now you should wrap sending files into InputFile object before send instead of passing + IO object directly to the API method. (:ref:`Read more » `) Webhook ======= -- +- Simplified aiohttp web app configuration +- By default added possibility to upload files when you use reply into webhook +.. + This part of document will be removed -Draft -===== + .. code-block:: markdown -This part of document will be removed + ## Keyboards and filters (part 1) -.. code-block:: markdown + - `(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 - ## Keyboards and filters (part 1) + ```python + from secrets import token_urlsafe - - `(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))) + from aiogram import Bot, Dispatcher + from aiogram.types import ( + CallbackQuery, + InlineKeyboardButton, + InlineKeyboardMarkup, + Message, ) - await message.reply("Vote on this post", reply_markup=kb) + from aiogram.utils.callback_data import CallbackData - @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 = Dispatcher(Bot(TOKEN)) - @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!") - ``` + vote_cb = CallbackData("vote", "action", "id", sep="_") + votes = {} # For demo purposes only! Use database in real code! - - Code for 3.0 + @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) - ```python - from enum import Enum - from secrets import token_urlsafe + @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) - 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 + @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: - return True + raise AssertionError(f"action action!r} is not implemented") + votes[int(callback_data["id"])] += d + await query.answer(f"{action.capitalize()}voted!") + ``` - @dp.message_handler(commands="broadcast") - async def broadcast(message: Message, command: Command.CommandObj) -> None: - # TODO ... - ``` + - Code for 3.0 - - Code for 3.x [todo] + ```python + from enum import Enum + from secrets import token_urlsafe - ```python - ... - ``` + 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!") + ``` - ## Filters (part 2) + ## Code style - - 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. + - 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) - ## Middlewares and app state + ```python + from aiogram import Bot, Dispatcher, executor + from mybot import handlers - - Rework middleware logic. - - Pass `**kwargs` from `start_polling` to handlers and filters. - - No more global `bot` and `message.bot`. - - `bot["foo"]` → `dp["foo"]`. + dp = Dispatcher(Bot(TOKEN)) - ## FSM + handlers.hello.setup(dp) + ... - - FSMStrategy. - - Default to any state. - - States are also callable filters. - - No more `next` and `proxy`. - - No state filtering is done by default: + executor.start_polling(dp, ...) + ``` - [Default state is not None · Issue #954 · aiogram/aiogram](https://github.com/aiogram/aiogram/issues/954#issuecomment-1172967490) + ```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"]) + ``` - ## Misc + ## Webhooks and API methods - - No more unrelated attributes and methods in types. - - `get_args()` - - `get_(full_)command()` - - …? - - Add handler flags. - - ??? + - 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. + - ??? diff --git a/docs/utils/keyboard.rst b/docs/utils/keyboard.rst index 752ffb0e..8988244e 100644 --- a/docs/utils/keyboard.rst +++ b/docs/utils/keyboard.rst @@ -1,3 +1,4 @@ +.. _keyboard-builder ================ Keyboard builder ================