diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 8a4bef99..00000000 --- a/.flake8 +++ /dev/null @@ -1,41 +0,0 @@ -[flake8] -count=True -statistics=True -show-source=True - -max-line-length=79 -import-order-style=smarkets -application-import-names=aiogram_dialog -exclude= - .venv, - docs, - benchmarks -docstring-convention=pep257 -ignore= - # A002 argument "object" is shadowing a python builtin - A002, - # A003 class attribute "id" is shadowing a python builtin - A003, - # A005 A module is shadowing a Python builtin module - A005, - # D100 Missing docstring in public module - D100, - # D101 Missing docstring in public class - D101, - # D102 Missing docstring in public method - D102, - # D103 Missing docstring in public function - D103, - # D104 Missing docstring in public package - D104, - # D105 Missing docstring in magic method - D105, - # D107 Missing docstring in __init__ - D107, - # W504 line break after binary operator - W504, -max-cognitive-complexity=10 -max-complexity=10 -per-file-ignores= - tests/**:D1,E800,A003 - **/__init__.py:F401 diff --git a/.github/workflows/setup.yml b/.github/workflows/setup.yml index 3f4871c8..993e3cf7 100644 --- a/.github/workflows/setup.yml +++ b/.github/workflows/setup.yml @@ -21,11 +21,11 @@ jobs: os: - ubuntu-latest python-version: - - "3.8" - "3.9" - "3.10" - "3.11" - "3.12" + - "3.13" steps: - uses: actions/checkout@v2 @@ -34,15 +34,16 @@ jobs: with: python-version: ${{ matrix.python-version }} + - name: Install uv + uses: astral-sh/setup-uv@v3 + - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install . -r requirements_dev.txt - pip install diagrams + uv pip install . -r requirements_dev.txt --system + uv pip install diagrams --system - - name: Run flake8 - run: | - python -m flake8 src/aiogram_dialog tests example + - name: Run Ruff + run: ruff check src/aiogram_dialog tests example - name: Run tests run: | diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 00000000..dfb7e3d4 --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,68 @@ +line-length = 79 +target-version="py39" +src = ["src"] + +include = ["src/**.py", "tests/**.py", "example/**.py"] +exclude = ["docs"] + +lint.select = ["ALL"] +lint.ignore = [ + "ANN", + "ARG", + "D", + "DTZ", + "TD", + "A002", + "ASYNC230", + "BLE001", + "EM101", + "EM102", + "FA100", + "FBT001", + "FBT002", + "FIX002", + "ISC002", + "ISC003", + "N818", + "PLR0913", + "PLW2901", + "PYI034", + "RET505", + "S311", + "SIM103", + "SIM108", + "SIM114", + "TCH001", + "TCH002", + "TCH003", + "TRY003", + "TRY201", + "TRY400", + "UP007", + "UP038", +] + + +[lint.per-file-ignores] +"src/aiogram_dialog/tools/**" = [ + "S101", + "SLF001", + "PTH", +] +"src/aiogram_dialog/test_tools/**"= [ + "S101", + "PTH" +] +"tests/**" = [ + "TID252", + "PLR2004", + "S101", + "INP001", + "FBT003", +] +"example/**" = [ + "INP001", + "ERA001", + "RUF001", + "PTH", +] \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 72a8a1fe..c8410d17 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,32 +12,29 @@ # # import os # import sys -# sys.path.insert(0, os.path.abspath('.')) - -import datetime - +# sys.path.insert(0, os.path.abspath(".")) # -- Project information ----------------------------------------------------- -project = 'aiogram-dialog' -copyright = f'{datetime.date.today().year}, Tishka17' -author = 'Tishka17' -master_doc = 'index' +project = "aiogram-dialog" +copyright = "%Y, Tishka17" +author = "Tishka17" +master_doc = "index" # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# extensions coming with Sphinx (named "sphinx.ext.*") or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx_copybutton', + "sphinx.ext.autodoc", + "sphinx_copybutton", ] autodoc_type_aliases = { } -autodoc_typehints = 'description' +autodoc_typehints = "description" # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -49,9 +46,9 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'furo' +html_theme = "furo" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] diff --git a/docs/widgets/custom_widgets/switch_inline_query_current_chat/example.py b/docs/widgets/custom_widgets/switch_inline_query_current_chat/example.py index d62182d4..ddd57de0 100644 --- a/docs/widgets/custom_widgets/switch_inline_query_current_chat/example.py +++ b/docs/widgets/custom_widgets/switch_inline_query_current_chat/example.py @@ -1,9 +1,7 @@ -from typing import Dict, List - -from aiogram.filters.state import StatesGroup, State +from aiogram.filters.state import State, StatesGroup from aiogram.types import InlineKeyboardButton -from aiogram_dialog import Dialog, Window -from aiogram_dialog import DialogManager + +from aiogram_dialog import Dialog, DialogManager, Window from aiogram_dialog.widgets.kbd import SwitchInlineQuery from aiogram_dialog.widgets.text import Const @@ -11,9 +9,9 @@ class SwitchInlineQueryCurrentChat(SwitchInlineQuery): async def _render_keyboard( self, - data: Dict, + data: dict, manager: DialogManager, - ) -> List[List[InlineKeyboardButton]]: + ) -> list[list[InlineKeyboardButton]]: return [ [ InlineKeyboardButton( @@ -34,8 +32,8 @@ class MySG(StatesGroup): Window( SwitchInlineQueryCurrentChat( Const("Some search"), # Button text - Const("query") # additional query. Optional + Const("query"), # additional query. Optional ), - state=MySG.main - ) + state=MySG.main, + ), ) diff --git a/docs/widgets/hiding/example.py b/docs/widgets/hiding/example.py index de3cd53a..5b1c777a 100644 --- a/docs/widgets/hiding/example.py +++ b/docs/widgets/hiding/example.py @@ -1,11 +1,9 @@ -from typing import Dict - -from aiogram.filters.state import StatesGroup, State +from aiogram.filters.state import State, StatesGroup from magic_filter import F -from aiogram_dialog import Window, DialogManager +from aiogram_dialog import DialogManager, Window from aiogram_dialog.widgets.common import Whenable -from aiogram_dialog.widgets.kbd import Button, Row, Group +from aiogram_dialog.widgets.kbd import Button, Group, Row from aiogram_dialog.widgets.text import Const, Format, Multi @@ -20,7 +18,7 @@ async def get_data(**kwargs): } -def is_tishka17(data: Dict, widget: Whenable, manager: DialogManager): +def is_tishka17(data: dict, widget: Whenable, manager: DialogManager): return data.get("name") == "Tishka17" @@ -28,7 +26,7 @@ def is_tishka17(data: Dict, widget: Whenable, manager: DialogManager): Multi( Const("Hello"), Format("{name}", when="extended"), - sep=" " + sep=" ", ), Group( Row( diff --git a/docs/widgets/index.rst b/docs/widgets/index.rst index c11c0664..5678ef7a 100644 --- a/docs/widgets/index.rst +++ b/docs/widgets/index.rst @@ -4,13 +4,14 @@ Widgets and Rendering Base information ******************** -Currently there are 4 kinds of widgets: :ref:`texts `, :ref:`keyboards `, -:ref:`input `, :ref:`media` and you can create your own :ref:`widgets`. +Currently there are 5 kinds of widgets: :ref:`texts `, :ref:`keyboards `, +:ref:`input `, :ref:`media`, :ref:`link preview` and you can create your own :ref:`widgets`. * **Texts** used to render text anywhere in dialog. It can be message text, button title and so on. * **Keyboards** represent parts of ``InlineKeyboard`` * **Media** represent media attachment to message * **Input** allows to process incoming messages from user. Is has no representation. +* **Link Preview** used to manage link previews in messages. Widgets can display static (e.g. ``Const``) and dynamic (e.g. ``Format``) content. To use dynamic data you have to set it. See :ref:`passing data `. @@ -37,5 +38,6 @@ Also there are 2 general types: keyboard/index input/index media/index + link_preview/index hiding/index - custom_widgets/index + custom_widgets/index \ No newline at end of file diff --git a/docs/widgets/keyboard/calendar/custom.py b/docs/widgets/keyboard/calendar/custom.py index d3eb30aa..efb8a1b0 100644 --- a/docs/widgets/keyboard/calendar/custom.py +++ b/docs/widgets/keyboard/calendar/custom.py @@ -1,17 +1,20 @@ -from typing import Dict - from aiogram_dialog import DialogManager from aiogram_dialog.widgets.kbd import ( - Calendar, CalendarScope, CalendarUserConfig, + Calendar, + CalendarScope, + CalendarUserConfig, ) from aiogram_dialog.widgets.kbd.calendar_kbd import ( - CalendarDaysView, CalendarMonthView, CalendarScopeView, CalendarYearsView, + CalendarDaysView, + CalendarMonthView, + CalendarScopeView, + CalendarYearsView, ) from aiogram_dialog.widgets.text import Const, Format class CustomCalendar(Calendar): - def _init_views(self) -> Dict[CalendarScope, CalendarScopeView]: + def _init_views(self) -> dict[CalendarScope, CalendarScopeView]: return { CalendarScope.DAYS: CalendarDaysView( self._item_callback_data, self.config, @@ -28,7 +31,7 @@ def _init_views(self) -> Dict[CalendarScope, CalendarScopeView]: async def _get_user_config( self, - data: Dict, + data: dict, manager: DialogManager, ) -> CalendarUserConfig: return CalendarUserConfig( diff --git a/docs/widgets/link_preview/example.py b/docs/widgets/link_preview/example.py new file mode 100644 index 00000000..3f9bbed4 --- /dev/null +++ b/docs/widgets/link_preview/example.py @@ -0,0 +1,27 @@ +from aiogram.filters.state import State, StatesGroup + +from aiogram_dialog import Window +from aiogram_dialog.widgets.link_preview import LinkPreview +from aiogram_dialog.widgets.text import Const + + +class SG(StatesGroup): + MAIN = State() + SECOND = State() + + +window = Window( + Const("https://nplus1.ru/news/2024/05/23/voyager-1-science-data"), + LinkPreview(is_disabled=True), + state=SG.MAIN, +) + +second_window = Window( + Const("some text"), + LinkPreview( + url=Const("https://nplus1.ru/news/2024/05/23/voyager-1-science-data"), + prefer_small_media=True, + show_above_text=True, + ), + state=SG.MAIN, +) diff --git a/docs/widgets/link_preview/index.rst b/docs/widgets/link_preview/index.rst new file mode 100644 index 00000000..0c552bfc --- /dev/null +++ b/docs/widgets/link_preview/index.rst @@ -0,0 +1,23 @@ +.. _link_preview: + +LinkPreview +************* + +The **LinkPreview** widget is used to manage link previews in messages. + +Parameters: + +* ``url``: A ``TextWidget`` with URL to be used in the link preview. If not provided, the first URL found in the message will be used. +* ``is_disabled``: that controls whether the link preview is displayed. If ``True``, the preview will be disabled. +* ``prefer_small_media``: that controls if the media in the link preview should be displayed in a smaller size. Ignored if media size change is not supported. +* ``prefer_large_media``: that controls if the media in the link preview should be enlarged. Ignored if media size change is not supported. +* ``show_above_text``: that specifies whether the link preview should be displayed above the message text. If ``True``, link preview be displayed above the message text. + + +Code example: + +.. literalinclude:: ./example.py + +.. autoclass:: aiogram_dialog.widgets.link_preview.LinkPreview + :special-members: __init__ + :members: render_link_preview, _render_link_preview diff --git a/docs/widgets/text/case/example.py b/docs/widgets/text/case/example.py index 0181477a..cbe72d5e 100644 --- a/docs/widgets/text/case/example.py +++ b/docs/widgets/text/case/example.py @@ -1,11 +1,11 @@ -from typing import Any, Dict +from typing import Any +from aiogram.filters.state import State, StatesGroup from magic_filter import F -from aiogram.filters.state import StatesGroup, State +from aiogram_dialog import Dialog, DialogManager, Window +from aiogram_dialog.widgets.text import Case, Const, Format -from aiogram_dialog import Window, DialogManager, Dialog -from aiogram_dialog.widgets.text import Const, Format, Case class MySG(StatesGroup): window1 = State() @@ -35,7 +35,7 @@ async def get_data(**kwargs): # The result of this function will be used to select wich option of ``Case`` widget to show. # # `text2` will produce text `42 is even!` -def parity_selector(data: Dict, case: Case, manager: DialogManager): +def parity_selector(data: dict, case: Case, manager: DialogManager): return data["number"] % 2 @@ -63,8 +63,8 @@ def parity_selector(data: Dict, case: Case, manager: DialogManager): async def on_dialog_start(start_data: Any, manager: DialogManager): - manager.dialog_data['user'] = { - 'test_result': True, + manager.dialog_data["user"] = { + "test_result": True, } @@ -74,7 +74,7 @@ async def on_dialog_start(start_data: Any, manager: DialogManager): text2, text3, state=MySG.window1, - getter=get_data + getter=get_data, ), - on_start=on_dialog_start -) \ No newline at end of file + on_start=on_dialog_start, +) diff --git a/example/custom_media_url.py b/example/custom_media_url.py index 1f169e95..67acc22b 100644 --- a/example/custom_media_url.py +++ b/example/custom_media_url.py @@ -4,17 +4,18 @@ from io import BytesIO from typing import Union - from aiogram import Bot, Dispatcher from aiogram.filters import CommandStart from aiogram.fsm.state import State, StatesGroup from aiogram.types import BufferedInputFile, ContentType, InputFile, Message from PIL import Image, ImageDraw, ImageFont - from aiogram_dialog import ( - Dialog, DialogManager, setup_dialogs, - StartMode, Window, + Dialog, + DialogManager, + StartMode, + Window, + setup_dialogs, ) from aiogram_dialog.api.entities import MediaAttachment from aiogram_dialog.manager.message_manager import MessageManager @@ -22,7 +23,6 @@ from aiogram_dialog.widgets.media import StaticMedia from aiogram_dialog.widgets.text import Const - src_dir = os.path.normpath(os.path.join(__file__, os.path.pardir)) API_TOKEN = os.getenv("BOT_TOKEN") @@ -115,5 +115,5 @@ async def main(): await dp.start_polling(bot) -if __name__ == '__main__': +if __name__ == "__main__": asyncio.run(main()) diff --git a/example/i18n/bot.py b/example/i18n/bot.py index d737e9d5..949d9445 100644 --- a/example/i18n/bot.py +++ b/example/i18n/bot.py @@ -18,7 +18,11 @@ from i18n_middleware import I18nMiddleware from aiogram_dialog import ( - Dialog, DialogManager, setup_dialogs, StartMode, Window, + Dialog, + DialogManager, + StartMode, + Window, + setup_dialogs, ) from aiogram_dialog.widgets.kbd import Button, Cancel, Row @@ -87,5 +91,5 @@ async def main(): await dp.start_polling(bot) -if __name__ == '__main__': +if __name__ == "__main__": asyncio.run(main()) diff --git a/example/i18n/i18n_format.py b/example/i18n/i18n_format.py index 2f4cd6f2..ddf44caa 100644 --- a/example/i18n/i18n_format.py +++ b/example/i18n/i18n_format.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, Protocol +from typing import Any, Protocol from aiogram_dialog.api.protocols import DialogManager from aiogram_dialog.widgets.common import WhenCondition @@ -21,7 +21,7 @@ def __init__(self, text: str, when: WhenCondition = None): super().__init__(when) self.text = text - async def _render_text(self, data: Dict, manager: DialogManager) -> str: + async def _render_text(self, data: dict, manager: DialogManager) -> str: format_text = manager.middleware_data.get( I18N_FORMAT_KEY, default_format_text, ) diff --git a/example/i18n/i18n_middleware.py b/example/i18n/i18n_middleware.py index f897cb46..e0cfaf5b 100644 --- a/example/i18n/i18n_middleware.py +++ b/example/i18n/i18n_middleware.py @@ -1,4 +1,5 @@ -from typing import Any, Awaitable, Callable, Dict, Union +from collections.abc import Awaitable, Callable +from typing import Any, Union from aiogram.dispatcher.middlewares.base import BaseMiddleware from aiogram.types import CallbackQuery, Message @@ -9,7 +10,7 @@ class I18nMiddleware(BaseMiddleware): def __init__( self, - l10ns: Dict[str, FluentLocalization], + l10ns: dict[str, FluentLocalization], default_lang: str, ): super().__init__() @@ -19,11 +20,11 @@ def __init__( async def __call__( self, handler: Callable[ - [Union[Message, CallbackQuery], Dict[str, Any]], + [Union[Message, CallbackQuery], dict[str, Any]], Awaitable[Any], ], event: Union[Message, CallbackQuery], - data: Dict[str, Any], + data: dict[str, Any], ) -> Any: # some language/locale retrieving logic if event.from_user: diff --git a/example/input_media_group.py b/example/input_media_group.py index e54efac5..ba608e7b 100644 --- a/example/input_media_group.py +++ b/example/input_media_group.py @@ -10,7 +10,11 @@ from aiogram.types import CallbackQuery, Message from aiogram_dialog import ( - Dialog, DialogManager, setup_dialogs, StartMode, Window, + Dialog, + DialogManager, + StartMode, + Window, + setup_dialogs, ) from aiogram_dialog.api.entities import MediaAttachment, MediaId from aiogram_dialog.widgets.common import ManagedScroll @@ -57,7 +61,7 @@ async def getter(dialog_manager: DialogManager, **kwargs) -> dict: ) else: media = MediaAttachment( - url="https://upload.wikimedia.org/wikipedia/commons/thumb/d/d1/Image_not_available.png/800px-Image_not_available.png?20210219185637", # noqa: E501 + url="https://upload.wikimedia.org/wikipedia/commons/thumb/d/d1/Image_not_available.png/800px-Image_not_available.png?20210219185637", type=ContentType.PHOTO, ) return { @@ -105,5 +109,5 @@ async def main(): await dp.start_polling(bot) -if __name__ == '__main__': +if __name__ == "__main__": asyncio.run(main()) diff --git a/example/launch_modes.py b/example/launch_modes.py index fb94bff3..78aab3ec 100644 --- a/example/launch_modes.py +++ b/example/launch_modes.py @@ -9,7 +9,12 @@ from aiogram.types import Message from aiogram_dialog import ( - Dialog, DialogManager, LaunchMode, setup_dialogs, StartMode, Window, + Dialog, + DialogManager, + LaunchMode, + StartMode, + Window, + setup_dialogs, ) from aiogram_dialog.widgets.kbd import Cancel, Row, Start from aiogram_dialog.widgets.text import Const, Format @@ -93,5 +98,5 @@ async def main(): await dp.start_polling(bot) -if __name__ == '__main__': +if __name__ == "__main__": asyncio.run(main()) diff --git a/example/list_group.py b/example/list_group.py index e791a24b..7104f8a1 100644 --- a/example/list_group.py +++ b/example/list_group.py @@ -1,7 +1,6 @@ import asyncio import logging import os -from typing import Dict from aiogram import Bot, Dispatcher from aiogram.filters import CommandStart @@ -10,13 +9,20 @@ from aiogram.types import Message from aiogram_dialog import ( - Dialog, DialogManager, LaunchMode, - setup_dialogs, StartMode, SubManager, + Dialog, + DialogManager, + LaunchMode, + StartMode, + SubManager, Window, + setup_dialogs, ) from aiogram_dialog.widgets.kbd import ( - Checkbox, ListGroup, - ManagedCheckbox, Radio, Row, + Checkbox, + ListGroup, + ManagedCheckbox, + Radio, + Row, ) from aiogram_dialog.widgets.text import Const, Format @@ -27,7 +33,7 @@ class DialogSG(StatesGroup): greeting = State() -def when_checked(data: Dict, widget, manager: SubManager) -> bool: +def when_checked(data: dict, widget, manager: SubManager) -> bool: # manager for our case is already adapted for current ListGroup row # so `.find` returns widget adapted for current row # if you need to find widgets outside the row, use `.find_in_parent` @@ -61,8 +67,8 @@ async def data_getter(*args, **kwargs): item_id_getter=str, items=["black", "white"], # Alternatives: - # items=F["data"]["colors"], # noqa: E800 - # items=lambda d: d["data"]["colors"], # noqa: E800 + # items=F["data"]["colors"], + # items=lambda d: d["data"]["colors"], when=when_checked, ), ), @@ -70,8 +76,8 @@ async def data_getter(*args, **kwargs): item_id_getter=str, items=["apple", "orange", "pear"], # Alternatives: - # items=F["fruits"], # noqa: E800 - # items=lambda d: d["fruits"], # noqa: E800 + # items=F["fruits"], + # items=lambda d: d["fruits"], ), state=DialogSG.greeting, getter=data_getter, @@ -98,5 +104,5 @@ async def main(): await dp.start_polling(bot) -if __name__ == '__main__': +if __name__ == "__main__": asyncio.run(main()) diff --git a/example/loading.py b/example/loading.py index fd810716..52f3394f 100644 --- a/example/loading.py +++ b/example/loading.py @@ -9,10 +9,15 @@ from aiogram.types import CallbackQuery, Message from aiogram_dialog import ( - BaseDialogManager, Dialog, DialogManager, - setup_dialogs, StartMode, Window, + BaseDialogManager, + Dialog, + DialogManager, + StartMode, + Window, + setup_dialogs, ) from aiogram_dialog.widgets.kbd import Button +from aiogram_dialog.widgets.link_preview import LinkPreview from aiogram_dialog.widgets.text import Const, Multi, Progress API_TOKEN = os.getenv("BOT_TOKEN") @@ -53,7 +58,7 @@ async def start_bg( manager: DialogManager, ): await manager.start(Bg.progress) - asyncio.create_task(background(callback, manager.bg())) + asyncio.create_task(background(callback, manager.bg())) # noqa: RUF006 async def background(callback: CallbackQuery, manager: BaseDialogManager): @@ -71,6 +76,7 @@ async def background(callback: CallbackQuery, manager: BaseDialogManager): Window( Const("Press button to start processing"), Button(Const("Start"), id="start", on_click=start_bg), + LinkPreview(url=Const("http://ya.ru")), state=MainSG.main, ), ) @@ -95,5 +101,5 @@ async def main(): await dp.start_polling(bot) -if __name__ == '__main__': +if __name__ == "__main__": asyncio.run(main()) diff --git a/example/mega/bot.py b/example/mega/bot.py index d373b80b..7ba54e8a 100644 --- a/example/mega/bot.py +++ b/example/mega/bot.py @@ -11,6 +11,7 @@ from bot_dialogs.calendar import calendar_dialog from bot_dialogs.counter import counter_dialog from bot_dialogs.layouts import layouts_dialog +from bot_dialogs.link_preview import link_preview_dialog from bot_dialogs.main import main_dialog from bot_dialogs.mutltiwidget import multiwidget_dialog from bot_dialogs.reply_buttons import reply_kbd_dialog @@ -18,7 +19,7 @@ from bot_dialogs.select import selects_dialog from bot_dialogs.switch import switch_dialog -from aiogram_dialog import DialogManager, setup_dialogs, ShowMode, StartMode +from aiogram_dialog import DialogManager, ShowMode, StartMode, setup_dialogs from aiogram_dialog.api.exceptions import UnknownIntent @@ -40,7 +41,7 @@ async def on_unknown_intent(event: ErrorEvent, dialog_manager: DialogManager): "Redirecting to main menu.", ) if event.update.callback_query.message: - try: + try: # noqa: SIM105 await event.update.callback_query.message.delete() except TelegramBadRequest: pass # whatever @@ -68,6 +69,7 @@ async def on_unknown_intent(event: ErrorEvent, dialog_manager: DialogManager): multiwidget_dialog, switch_dialog, reply_kbd_dialog, + link_preview_dialog, ) @@ -93,5 +95,5 @@ async def main(): await dp.start_polling(bot) -if __name__ == '__main__': +if __name__ == "__main__": asyncio.run(main()) diff --git a/example/mega/bot_dialogs/calendar.py b/example/mega/bot_dialogs/calendar.py index 5101b89c..b1e8a113 100644 --- a/example/mega/bot_dialogs/calendar.py +++ b/example/mega/bot_dialogs/calendar.py @@ -1,19 +1,25 @@ from datetime import date -from typing import Dict from aiogram import F from babel.dates import get_day_names, get_month_names from aiogram_dialog import ChatEvent, Dialog, DialogManager, Window from aiogram_dialog.widgets.kbd import ( - Calendar, CalendarScope, ManagedCalendar, SwitchTo, + Calendar, + CalendarScope, + ManagedCalendar, + SwitchTo, ) from aiogram_dialog.widgets.kbd.calendar_kbd import ( - CalendarDaysView, CalendarMonthView, - CalendarScopeView, CalendarYearsView, - DATE_TEXT, TODAY_TEXT, + DATE_TEXT, + TODAY_TEXT, + CalendarDaysView, + CalendarMonthView, + CalendarScopeView, + CalendarYearsView, ) from aiogram_dialog.widgets.text import Const, Format, Text + from . import states from .common import MAIN_MENU_BUTTON @@ -25,7 +31,7 @@ async def _render_text(self, data, manager: DialogManager) -> str: selected_date: date = data["date"] locale = manager.event.from_user.language_code return get_day_names( - width="short", context='stand-alone', locale=locale, + width="short", context="stand-alone", locale=locale, )[selected_date.weekday()].title() @@ -49,12 +55,12 @@ async def _render_text(self, data, manager: DialogManager) -> str: selected_date: date = data["date"] locale = manager.event.from_user.language_code return get_month_names( - 'wide', context='stand-alone', locale=locale, + "wide", context="stand-alone", locale=locale, )[selected_date.month].title() class CustomCalendar(Calendar): - def _init_views(self) -> Dict[CalendarScope, CalendarScopeView]: + def _init_views(self) -> dict[CalendarScope, CalendarScopeView]: return { CalendarScope.DAYS: CalendarDaysView( self._item_callback_data, diff --git a/example/mega/bot_dialogs/common.py b/example/mega/bot_dialogs/common.py index 89718bdd..3cf5ab2e 100644 --- a/example/mega/bot_dialogs/common.py +++ b/example/mega/bot_dialogs/common.py @@ -1,5 +1,6 @@ from aiogram_dialog.widgets.kbd import Start from aiogram_dialog.widgets.text import Const + from . import states MAIN_MENU_BUTTON = Start( diff --git a/example/mega/bot_dialogs/counter.py b/example/mega/bot_dialogs/counter.py index 445d8066..a5efad82 100644 --- a/example/mega/bot_dialogs/counter.py +++ b/example/mega/bot_dialogs/counter.py @@ -3,6 +3,7 @@ from aiogram_dialog import Dialog, DialogManager, Window from aiogram_dialog.widgets.kbd import Counter, ManagedCounter from aiogram_dialog.widgets.text import Const, Progress + from . import states from .common import MAIN_MENU_BUTTON diff --git a/example/mega/bot_dialogs/layouts.py b/example/mega/bot_dialogs/layouts.py index 6e7f7082..a56e04f2 100644 --- a/example/mega/bot_dialogs/layouts.py +++ b/example/mega/bot_dialogs/layouts.py @@ -1,10 +1,17 @@ from aiogram_dialog import ( - Dialog, Window, + Dialog, + Window, ) from aiogram_dialog.widgets.kbd import ( - Button, Column, Group, Row, Select, SwitchTo, + Button, + Column, + Group, + Row, + Select, + SwitchTo, ) from aiogram_dialog.widgets.text import Const, Format + from . import states from .common import MAIN_MENU_BUTTON diff --git a/example/mega/bot_dialogs/link_preview.py b/example/mega/bot_dialogs/link_preview.py new file mode 100644 index 00000000..d353fc43 --- /dev/null +++ b/example/mega/bot_dialogs/link_preview.py @@ -0,0 +1,83 @@ +from aiogram_dialog import ( + Dialog, + Window, +) +from aiogram_dialog.widgets.kbd import SwitchTo +from aiogram_dialog.widgets.link_preview import LinkPreview +from aiogram_dialog.widgets.text import Const, Format + +from . import states +from .common import MAIN_MENU_BUTTON + + +async def links_getter(**_): + return { + "main": "https://en.wikipedia.org/wiki/HTML_element", + "photo": "https://en.wikipedia.org/wiki/Hyperlink", + } + + +LinkPreview_MAIN_MENU_BUTTON = SwitchTo( + text=Const("Back"), id="back", state=states.LinkPreview.MAIN, +) +COMMON_TEXT = Format( + "This is demo of different link preview options.\n" + "Link in text: {main}\n" + "Link in preview can be different\n\n" + "Current mode is:", +) + +BACK = SwitchTo(Const("back"), "_back", states.LinkPreview.MAIN) + +link_preview_dialog = Dialog( + Window( + COMMON_TEXT, + Format("Default"), + SwitchTo( + Const("disable"), "_disable", states.LinkPreview.IS_DISABLED, + ), + SwitchTo( + Const("prefer small media"), "_prefer_small_media", + states.LinkPreview.SMALL_MEDIA, + ), + SwitchTo( + Const("prefer large media"), "_prefer_large_media", + states.LinkPreview.LARGE_MEDIA, + ), + SwitchTo( + Const("show above text"), "_show_above_text", + states.LinkPreview.SHOW_ABOVE_TEXT, + ), + MAIN_MENU_BUTTON, + state=states.LinkPreview.MAIN, + ), + Window( + COMMON_TEXT, + Const("is_disabled=True"), + LinkPreview(is_disabled=True), + LinkPreview_MAIN_MENU_BUTTON, + state=states.LinkPreview.IS_DISABLED, + ), + Window( + COMMON_TEXT, + Const("prefer_small_media=True"), + LinkPreview(Format("{photo}"), prefer_small_media=True), + LinkPreview_MAIN_MENU_BUTTON, + state=states.LinkPreview.SMALL_MEDIA, + ), + Window( + COMMON_TEXT, + Const("prefer_large_media=True"), + LinkPreview(Format("{photo}"), prefer_large_media=True), + LinkPreview_MAIN_MENU_BUTTON, + state=states.LinkPreview.LARGE_MEDIA, + ), + Window( + COMMON_TEXT, + Const("show_above_text=True"), + LinkPreview(Format("{photo}"), show_above_text=True), + LinkPreview_MAIN_MENU_BUTTON, + state=states.LinkPreview.SHOW_ABOVE_TEXT, + ), + getter=links_getter, +) diff --git a/example/mega/bot_dialogs/main.py b/example/mega/bot_dialogs/main.py index b6a3c8dd..0b541bf7 100644 --- a/example/mega/bot_dialogs/main.py +++ b/example/mega/bot_dialogs/main.py @@ -2,6 +2,7 @@ from aiogram_dialog.about import about_aiogram_dialog_button from aiogram_dialog.widgets.kbd import Start from aiogram_dialog.widgets.text import Const + from . import states main_dialog = Dialog( @@ -43,6 +44,11 @@ id="switch", state=states.Switch.MAIN, ), + Start( + text=Const("🔗 Link Preview"), + id="linkpreview", + state=states.LinkPreview.MAIN, + ), Start( text=Const("⌨️ Reply keyboard"), id="reply", diff --git a/example/mega/bot_dialogs/mutltiwidget.py b/example/mega/bot_dialogs/mutltiwidget.py index c9f07b03..3bc0f2a1 100644 --- a/example/mega/bot_dialogs/mutltiwidget.py +++ b/example/mega/bot_dialogs/mutltiwidget.py @@ -1,6 +1,7 @@ from aiogram_dialog import Dialog, Window from aiogram_dialog.widgets.kbd import Checkbox, Counter, Multiselect, Radio from aiogram_dialog.widgets.text import Const, Format + from . import states from .common import MAIN_MENU_BUTTON diff --git a/example/mega/bot_dialogs/reply_buttons.py b/example/mega/bot_dialogs/reply_buttons.py index 1d4a341c..a9024da1 100644 --- a/example/mega/bot_dialogs/reply_buttons.py +++ b/example/mega/bot_dialogs/reply_buttons.py @@ -1,10 +1,14 @@ from aiogram_dialog import Dialog, Window from aiogram_dialog.widgets.kbd import ( - Checkbox, Radio, RequestContact, - RequestLocation, Row, + Checkbox, + Radio, + RequestContact, + RequestLocation, + Row, ) from aiogram_dialog.widgets.markup.reply_keyboard import ReplyKeyboardFactory from aiogram_dialog.widgets.text import Const, Format + from . import states from .common import MAIN_MENU_BUTTON diff --git a/example/mega/bot_dialogs/scrolls.py b/example/mega/bot_dialogs/scrolls.py index 3e8c264a..cb474a30 100644 --- a/example/mega/bot_dialogs/scrolls.py +++ b/example/mega/bot_dialogs/scrolls.py @@ -4,13 +4,21 @@ from aiogram_dialog import Dialog, DialogManager, Window from aiogram_dialog.widgets.common import sync_scroll from aiogram_dialog.widgets.kbd import ( - CurrentPage, FirstPage, LastPage, - Multiselect, NextPage, NumberedPager, - PrevPage, Row, ScrollingGroup, - StubScroll, SwitchTo, + CurrentPage, + FirstPage, + LastPage, + Multiselect, + NextPage, + NumberedPager, + PrevPage, + Row, + ScrollingGroup, + StubScroll, + SwitchTo, ) from aiogram_dialog.widgets.media import StaticMedia from aiogram_dialog.widgets.text import Const, Format, List, ScrollingText + from . import states from .common import MAIN_MENU_BUTTON diff --git a/example/mega/bot_dialogs/select.py b/example/mega/bot_dialogs/select.py index 169e537f..f85767e2 100644 --- a/example/mega/bot_dialogs/select.py +++ b/example/mega/bot_dialogs/select.py @@ -5,10 +5,15 @@ from aiogram_dialog import Dialog, DialogManager, Window from aiogram_dialog.widgets.kbd import ( - Column, Multiselect, Radio, - Select, SwitchTo, Toggle, + Column, + Multiselect, + Radio, + Select, + SwitchTo, + Toggle, ) from aiogram_dialog.widgets.text import Const, Format, List + from . import states from .common import MAIN_MENU_BUTTON @@ -89,8 +94,8 @@ async def on_item_selected( field=Format("+ {item.emoji} {item.name} - {item.id}"), items=FRUITS_KEY, # Alternatives: - # items=lambda d: d[OTHER_KEY][FRUITS_KEY], # noqa: E800 - # items=F[OTHER_KEY][FRUITS_KEY], # noqa: E800 + # items=lambda d: d[OTHER_KEY][FRUITS_KEY], + # items=F[OTHER_KEY][FRUITS_KEY], ), Column( Select( @@ -98,8 +103,8 @@ async def on_item_selected( id="sel", items=FRUITS_KEY, # Alternatives: - # items=lambda d: d[OTHER_KEY][FRUITS_KEY], # noqa: E800 - # items=F[OTHER_KEY][FRUITS_KEY], # noqa: E800 + # items=lambda d: d[OTHER_KEY][FRUITS_KEY], + # items=F[OTHER_KEY][FRUITS_KEY], item_id_getter=fruit_id_getter, on_click=on_item_selected, ), @@ -118,8 +123,8 @@ async def on_item_selected( id="radio", items=FRUITS_KEY, # Alternatives: - # items=lambda d: d[OTHER_KEY][FRUITS_KEY], # noqa: E800 - # items=F[OTHER_KEY][FRUITS_KEY], # noqa: E800 + # items=lambda d: d[OTHER_KEY][FRUITS_KEY], + # items=F[OTHER_KEY][FRUITS_KEY], item_id_getter=fruit_id_getter, ), ), @@ -138,8 +143,8 @@ async def on_item_selected( id="multi", items=FRUITS_KEY, # Alternatives: - # items=lambda d: d[OTHER_KEY][FRUITS_KEY], # noqa: E800 - # items=F[OTHER_KEY][FRUITS_KEY], # noqa: E800 + # items=lambda d: d[OTHER_KEY][FRUITS_KEY], + # items=F[OTHER_KEY][FRUITS_KEY], item_id_getter=fruit_id_getter, ), ), diff --git a/example/mega/bot_dialogs/states.py b/example/mega/bot_dialogs/states.py index cb9d48a2..a18f1bd3 100644 --- a/example/mega/bot_dialogs/states.py +++ b/example/mega/bot_dialogs/states.py @@ -52,3 +52,11 @@ class Switch(StatesGroup): MAIN = State() INPUT = State() LAST = State() + + +class LinkPreview(StatesGroup): + MAIN = State() + IS_DISABLED = State() + SMALL_MEDIA = State() + LARGE_MEDIA = State() + SHOW_ABOVE_TEXT = State() diff --git a/example/mega/bot_dialogs/switch.py b/example/mega/bot_dialogs/switch.py index 80cbd0a2..4848e61a 100644 --- a/example/mega/bot_dialogs/switch.py +++ b/example/mega/bot_dialogs/switch.py @@ -1,8 +1,9 @@ -from typing import Any, Dict +from typing import Any from aiogram_dialog import Dialog, DialogManager, Window from aiogram_dialog.widgets.kbd import Back, Checkbox, Next, Radio, Row from aiogram_dialog.widgets.text import Case, Const, Format + from . import states from .common import MAIN_MENU_BUTTON @@ -14,7 +15,7 @@ async def data_getter( dialog_manager: DialogManager, **_kwargs, -) -> Dict[str, Any]: +) -> dict[str, Any]: return { "option": dialog_manager.find(CHECKBOX_ID).is_checked(), "emoji": dialog_manager.find(EMOJI_ID).get_checked(), diff --git a/example/multistack.py b/example/multistack.py index 14e70ca3..de1e5ca2 100644 --- a/example/multistack.py +++ b/example/multistack.py @@ -11,8 +11,11 @@ from aiogram.types import CallbackQuery, Message from aiogram_dialog import ( - Dialog, DialogManager, - setup_dialogs, StartMode, Window, + Dialog, + DialogManager, + StartMode, + Window, + setup_dialogs, ) from aiogram_dialog.widgets.input import MessageInput from aiogram_dialog.widgets.kbd import Button, Cancel, Multiselect, Start @@ -109,5 +112,5 @@ async def main(): await dp.start_polling(bot) -if __name__ == '__main__': +if __name__ == "__main__": asyncio.run(main()) diff --git a/example/scrolls.py b/example/scrolls.py index 349eacf6..d6e7bf95 100644 --- a/example/scrolls.py +++ b/example/scrolls.py @@ -11,14 +11,24 @@ from aiogram.types import Message from aiogram_dialog import ( - Dialog, DialogManager, - setup_dialogs, StartMode, Window, + Dialog, + DialogManager, + StartMode, + Window, + setup_dialogs, ) from aiogram_dialog.widgets.kbd import ( - CurrentPage, FirstPage, LastPage, - Multiselect, NextPage, NumberedPager, - PrevPage, Row, ScrollingGroup, - StubScroll, SwitchTo, + CurrentPage, + FirstPage, + LastPage, + Multiselect, + NextPage, + NumberedPager, + PrevPage, + Row, + ScrollingGroup, + StubScroll, + SwitchTo, ) from aiogram_dialog.widgets.text import Const, Format, List, ScrollingText @@ -223,5 +233,5 @@ async def main(): await dp.start_polling(bot) -if __name__ == '__main__': +if __name__ == "__main__": asyncio.run(main()) diff --git a/example/simple.py b/example/simple.py index 7a939a61..13bd9c41 100644 --- a/example/simple.py +++ b/example/simple.py @@ -11,9 +11,13 @@ from redis.asyncio.client import Redis from aiogram_dialog import ( - ChatEvent, Dialog, DialogManager, - setup_dialogs, ShowMode, - StartMode, Window, + ChatEvent, + Dialog, + DialogManager, + ShowMode, + StartMode, + Window, + setup_dialogs, ) from aiogram_dialog.api.exceptions import UnknownIntent, UnknownState from aiogram_dialog.widgets.input import MessageInput @@ -172,5 +176,5 @@ async def main(): await dp.start_polling(bot) -if __name__ == '__main__': +if __name__ == "__main__": asyncio.run(main()) diff --git a/example/subdialog.py b/example/subdialog.py index cb1a3102..ac8cab21 100644 --- a/example/subdialog.py +++ b/example/subdialog.py @@ -10,14 +10,23 @@ from aiogram.types import CallbackQuery, Message from aiogram_dialog import ( - Data, Dialog, DialogManager, - setup_dialogs, StartMode, Window, + Data, + Dialog, + DialogManager, + StartMode, + Window, + setup_dialogs, ) from aiogram_dialog.tools import render_preview, render_transitions from aiogram_dialog.widgets.input import MessageInput from aiogram_dialog.widgets.kbd import ( - Back, Button, Cancel, - Group, Next, Row, Start, + Back, + Button, + Cancel, + Group, + Next, + Row, + Start, ) from aiogram_dialog.widgets.text import Const, Format, Multi @@ -156,5 +165,5 @@ async def main(): await dp.start_polling(bot) -if __name__ == '__main__': +if __name__ == "__main__": asyncio.run(main()) diff --git a/example/wizard.py b/example/wizard.py index 8cea2a17..47e606dc 100644 --- a/example/wizard.py +++ b/example/wizard.py @@ -8,7 +8,11 @@ from aiogram.types import Message from aiogram_dialog import ( - Dialog, DialogManager, setup_dialogs, StartMode, Window, + Dialog, + DialogManager, + StartMode, + Window, + setup_dialogs, ) from aiogram_dialog.widgets.input import TextInput from aiogram_dialog.widgets.kbd import Checkbox, Next, SwitchTo @@ -123,5 +127,5 @@ async def main(): await dp.start_polling(bot) -if __name__ == '__main__': +if __name__ == "__main__": asyncio.run(main()) diff --git a/pyproject.toml b/pyproject.toml index c093da4b..0066f84b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=66.0"] +requires = ["setuptools==75.3.0"] build-backend = "setuptools.build_meta" [tool.setuptools] @@ -17,18 +17,22 @@ authors = [ ] license = { text = "Apache-2.0" } description = "Telegram bot UI framework on top of aiogram" -requires-python = ">=3.8" +requires-python = ">=3.9" classifiers = [ "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", ] dependencies = [ - 'aiogram>=3.5.0', - 'jinja2', - 'cachetools>=4.0.0,<6.0.0', - 'magic_filter', + "aiogram>=3.14.0", + "jinja2", + "cachetools>=4.0.0,<6.0.0", ] [project.optional-dependencies] tools = [ diff --git a/requirements_dev.txt b/requirements_dev.txt index faf62be7..543ae4f4 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,17 +1,5 @@ vulture -flake8==7.* -flake8-blind-except -flake8-bugbear -flake8-builtins -flake8-cognitive-complexity -flake8-comprehensions -flake8-docstrings -flake8-eradicate -flake8-import-order -flake8-mutable -flake8-polyfill -flake8-print - +ruff==0.7.2 pytest pytest-asyncio -pytest-repeat +pytest-repeat \ No newline at end of file diff --git a/requirements_doc.txt b/requirements_doc.txt index 54e78bcc..76386171 100644 --- a/requirements_doc.txt +++ b/requirements_doc.txt @@ -1,4 +1,4 @@ sphinx sphinx-autodocgen furo -sphinx-copybutton +sphinx-copybutton \ No newline at end of file diff --git a/src/aiogram_dialog/__init__.py b/src/aiogram_dialog/__init__.py index d6d649e5..474f5717 100644 --- a/src/aiogram_dialog/__init__.py +++ b/src/aiogram_dialog/__init__.py @@ -22,12 +22,21 @@ import importlib.metadata as _metadata from .api.entities import ( - AccessSettings, ChatEvent, Data, DEFAULT_STACK_ID, GROUP_STACK_ID, - LaunchMode, ShowMode, StartMode, + DEFAULT_STACK_ID, + GROUP_STACK_ID, + AccessSettings, + ChatEvent, + Data, + LaunchMode, + ShowMode, + StartMode, ) from .api.protocols import ( - BaseDialogManager, BgManagerFactory, CancelEventProcessing, - DialogManager, DialogProtocol, + BaseDialogManager, + BgManagerFactory, + CancelEventProcessing, + DialogManager, + DialogProtocol, UnsetId, ) from .dialog import Dialog diff --git a/src/aiogram_dialog/about.py b/src/aiogram_dialog/about.py index fe0baced..c4162512 100644 --- a/src/aiogram_dialog/about.py +++ b/src/aiogram_dialog/about.py @@ -33,7 +33,7 @@ def about_dialog(): "Author: {{metadata['Author-email']}}\n" "\n" "{% for name, url in urls%}" - "{{name}}: {{url}}\n" + '{{name}}: {{url}}\n' "{% endfor %}" "", ), diff --git a/src/aiogram_dialog/api/entities/__init__.py b/src/aiogram_dialog/api/entities/__init__.py index 38b04326..4626999a 100644 --- a/src/aiogram_dialog/api/entities/__init__.py +++ b/src/aiogram_dialog/api/entities/__init__.py @@ -12,13 +12,17 @@ from .access import AccessSettings from .context import Context, Data -from .events import ChatEvent, EVENT_CONTEXT_KEY, EventContext +from .events import EVENT_CONTEXT_KEY, ChatEvent, EventContext from .launch_mode import LaunchMode from .media import MediaAttachment, MediaId from .modes import ShowMode, StartMode from .new_message import MarkupVariant, NewMessage, OldMessage, UnknownText from .stack import DEFAULT_STACK_ID, GROUP_STACK_ID, Stack from .update_event import ( - DIALOG_EVENT_NAME, DialogAction, DialogStartEvent, DialogSwitchEvent, - DialogUpdate, DialogUpdateEvent, + DIALOG_EVENT_NAME, + DialogAction, + DialogStartEvent, + DialogSwitchEvent, + DialogUpdate, + DialogUpdateEvent, ) diff --git a/src/aiogram_dialog/api/entities/access.py b/src/aiogram_dialog/api/entities/access.py index bf49c19f..158b7926 100644 --- a/src/aiogram_dialog/api/entities/access.py +++ b/src/aiogram_dialog/api/entities/access.py @@ -1,8 +1,8 @@ from dataclasses import dataclass -from typing import Any, List +from typing import Any @dataclass class AccessSettings: - user_ids: List[int] + user_ids: list[int] custom: Any = None diff --git a/src/aiogram_dialog/api/entities/context.py b/src/aiogram_dialog/api/entities/context.py index b4882088..0f48af41 100644 --- a/src/aiogram_dialog/api/entities/context.py +++ b/src/aiogram_dialog/api/entities/context.py @@ -1,12 +1,12 @@ from dataclasses import dataclass, field -from typing import Dict, List, Optional, Union +from typing import Optional, Union from aiogram.fsm.state import State from .access import AccessSettings -Data = Union[Dict, List, int, str, float, None] -DataDict = Dict[str, Data] +Data = Union[dict, list, int, str, float, None] +DataDict = dict[str, Data] @dataclass(unsafe_hash=True) diff --git a/src/aiogram_dialog/api/entities/new_message.py b/src/aiogram_dialog/api/entities/new_message.py index a0f16221..6cc071cf 100644 --- a/src/aiogram_dialog/api/entities/new_message.py +++ b/src/aiogram_dialog/api/entities/new_message.py @@ -2,8 +2,13 @@ from enum import Enum from typing import Optional, Union +from aiogram.enums import ContentType from aiogram.types import ( - Chat, ForceReply, InlineKeyboardMarkup, ReplyKeyboardMarkup, + Chat, + ForceReply, + InlineKeyboardMarkup, + LinkPreviewOptions, + ReplyKeyboardMarkup, ReplyKeyboardRemove, ) @@ -27,6 +32,7 @@ class OldMessage: text: Union[str, None, UnknownText] = None has_reply_keyboard: bool = False business_connection_id: Optional[str] = None + content_type: Optional[ContentType] = None @dataclass @@ -38,5 +44,5 @@ class NewMessage: reply_markup: Optional[MarkupVariant] = None parse_mode: Optional[str] = None show_mode: ShowMode = ShowMode.AUTO - disable_web_page_preview: Optional[bool] = None media: Optional[MediaAttachment] = None + link_preview_options: Optional[LinkPreviewOptions] = None diff --git a/src/aiogram_dialog/api/entities/stack.py b/src/aiogram_dialog/api/entities/stack.py index 30fbd8cb..27b3f01f 100644 --- a/src/aiogram_dialog/api/entities/stack.py +++ b/src/aiogram_dialog/api/entities/stack.py @@ -2,11 +2,13 @@ import string import time from dataclasses import dataclass, field -from typing import List, Optional +from typing import Optional +from aiogram.enums import ContentType from aiogram.fsm.state import State from aiogram_dialog.api.exceptions import DialogStackOverflow + from .access import AccessSettings from .context import Context, Data @@ -38,7 +40,7 @@ def new_id(): @dataclass(unsafe_hash=True) class Stack: _id: str = field(compare=True, default_factory=new_id) - intents: List[str] = field(compare=False, default_factory=list) + intents: list[str] = field(compare=False, default_factory=list) last_message_id: Optional[int] = field(compare=False, default=None) last_reply_keyboard: bool = field(compare=False, default=False) last_media_id: Optional[str] = field(compare=False, default=None) @@ -46,6 +48,7 @@ class Stack: last_income_media_group_id: Optional[str] = field( compare=False, default=None, ) + content_type: Optional[ContentType] = field(compare=False, default=None) access_settings: Optional[AccessSettings] = None @property diff --git a/src/aiogram_dialog/api/internal/__init__.py b/src/aiogram_dialog/api/internal/__init__.py index c3e86d15..2dc28901 100644 --- a/src/aiogram_dialog/api/internal/__init__.py +++ b/src/aiogram_dialog/api/internal/__init__.py @@ -4,7 +4,7 @@ "CALLBACK_DATA_KEY", "CONTEXT_KEY", "EVENT_SIMULATED", "STACK_KEY", "STORAGE_KEY", "ButtonVariant", "DataGetter", "InputWidget", "KeyboardWidget", - "MediaWidget", "RawKeyboard", "TextWidget", "Widget", + "LinkPreviewWidget", "MediaWidget", "RawKeyboard", "TextWidget", "Widget", "WindowProtocol", ] @@ -13,10 +13,21 @@ DialogManagerFactory, ) from .middleware import ( - CALLBACK_DATA_KEY, CONTEXT_KEY, EVENT_SIMULATED, STACK_KEY, STORAGE_KEY, + CALLBACK_DATA_KEY, + CONTEXT_KEY, + EVENT_SIMULATED, + STACK_KEY, + STORAGE_KEY, ) from .widgets import ( - ButtonVariant, DataGetter, InputWidget, KeyboardWidget, - MediaWidget, RawKeyboard, TextWidget, Widget, + ButtonVariant, + DataGetter, + InputWidget, + KeyboardWidget, + LinkPreviewWidget, + MediaWidget, + RawKeyboard, + TextWidget, + Widget, ) from .window import WindowProtocol diff --git a/src/aiogram_dialog/api/internal/manager.py b/src/aiogram_dialog/api/internal/manager.py index 73e399c5..f5ed9d6c 100644 --- a/src/aiogram_dialog/api/internal/manager.py +++ b/src/aiogram_dialog/api/internal/manager.py @@ -1,18 +1,19 @@ from abc import abstractmethod -from typing import Dict, Protocol +from typing import Protocol from aiogram import Router from aiogram_dialog.api.entities import ChatEvent from aiogram_dialog.api.protocols import ( - DialogManager, DialogRegistryProtocol, + DialogManager, + DialogRegistryProtocol, ) class DialogManagerFactory(Protocol): @abstractmethod def __call__( - self, event: ChatEvent, data: Dict, + self, event: ChatEvent, data: dict, registry: DialogRegistryProtocol, router: Router, ) -> DialogManager: diff --git a/src/aiogram_dialog/api/internal/widgets.py b/src/aiogram_dialog/api/internal/widgets.py index df306af8..8472b216 100644 --- a/src/aiogram_dialog/api/internal/widgets.py +++ b/src/aiogram_dialog/api/internal/widgets.py @@ -1,11 +1,19 @@ from abc import abstractmethod +from collections.abc import Awaitable, Callable from typing import ( - Any, Awaitable, Callable, Dict, List, Optional, Protocol, - runtime_checkable, Union, + Any, + Optional, + Protocol, + Union, + runtime_checkable, ) from aiogram.types import ( - CallbackQuery, InlineKeyboardButton, KeyboardButton, Message, + CallbackQuery, + InlineKeyboardButton, + KeyboardButton, + LinkPreviewOptions, + Message, ) from aiogram_dialog import DialogManager @@ -28,21 +36,31 @@ def find(self, widget_id: str) -> Optional["Widget"]: class TextWidget(Widget, Protocol): @abstractmethod async def render_text( - self, data: Dict, manager: DialogManager, + self, data: dict, manager: DialogManager, ) -> str: """Create text.""" raise NotImplementedError +@runtime_checkable +class LinkPreviewWidget(Widget, Protocol): + @abstractmethod + async def render_link_preview( + self, data: dict, manager: DialogManager, + ) -> Optional[LinkPreviewOptions]: + """Create link preview.""" + raise NotImplementedError + + ButtonVariant = Union[InlineKeyboardButton, KeyboardButton] -RawKeyboard = List[List[ButtonVariant]] +RawKeyboard = list[list[ButtonVariant]] @runtime_checkable class KeyboardWidget(Widget, Protocol): @abstractmethod async def render_keyboard( - self, data: Dict, manager: DialogManager, + self, data: dict, manager: DialogManager, ) -> RawKeyboard: """Create Inline keyboard contents.""" raise NotImplementedError @@ -89,7 +107,7 @@ async def process_message( raise NotImplementedError -DataGetter = Callable[..., Awaitable[Dict]] +DataGetter = Callable[..., Awaitable[dict]] @runtime_checkable diff --git a/src/aiogram_dialog/api/internal/window.py b/src/aiogram_dialog/api/internal/window.py index 2dce844c..9deb8d59 100644 --- a/src/aiogram_dialog/api/internal/window.py +++ b/src/aiogram_dialog/api/internal/window.py @@ -9,6 +9,7 @@ from aiogram_dialog.api.entities import Data, NewMessage from aiogram_dialog.api.protocols import DialogProtocol + from .manager import DialogManager diff --git a/src/aiogram_dialog/api/protocols/__init__.py b/src/aiogram_dialog/api/protocols/__init__.py index 73f1ecb1..731dd00c 100644 --- a/src/aiogram_dialog/api/protocols/__init__.py +++ b/src/aiogram_dialog/api/protocols/__init__.py @@ -9,7 +9,10 @@ from .dialog import CancelEventProcessing, DialogProtocol from .manager import ( - BaseDialogManager, BgManagerFactory, DialogManager, UnsetId, + BaseDialogManager, + BgManagerFactory, + DialogManager, + UnsetId, ) from .media import MediaIdStorageProtocol from .message_manager import MessageManagerProtocol, MessageNotModified diff --git a/src/aiogram_dialog/api/protocols/dialog.py b/src/aiogram_dialog/api/protocols/dialog.py index 941566dd..024dbfff 100644 --- a/src/aiogram_dialog/api/protocols/dialog.py +++ b/src/aiogram_dialog/api/protocols/dialog.py @@ -1,11 +1,14 @@ from abc import abstractmethod -from typing import Any, Dict, List, Optional, Protocol, runtime_checkable, Type +from typing import Any, Optional, Protocol, runtime_checkable from aiogram.fsm.state import State, StatesGroup from aiogram_dialog.api.entities import ( - Data, LaunchMode, NewMessage, + Data, + LaunchMode, + NewMessage, ) + from .manager import DialogManager @@ -24,11 +27,11 @@ def states_group_name(self) -> str: raise NotImplementedError @abstractmethod - def states(self) -> List[State]: + def states(self) -> list[State]: raise NotImplementedError @abstractmethod - def states_group(self) -> Type[StatesGroup]: + def states_group(self) -> type[StatesGroup]: raise NotImplementedError @abstractmethod @@ -60,7 +63,7 @@ def find(self, widget_id) -> Any: @abstractmethod async def load_data( self, manager: DialogManager, - ) -> Dict: + ) -> dict: raise NotImplementedError @abstractmethod diff --git a/src/aiogram_dialog/api/protocols/manager.py b/src/aiogram_dialog/api/protocols/manager.py index 899c7e5e..cffd5bdb 100644 --- a/src/aiogram_dialog/api/protocols/manager.py +++ b/src/aiogram_dialog/api/protocols/manager.py @@ -1,13 +1,18 @@ from abc import abstractmethod from enum import Enum -from typing import Any, Dict, Optional, Protocol, Union +from typing import Any, Optional, Protocol, Union from aiogram import Bot from aiogram.fsm.state import State from aiogram_dialog.api.entities import ( AccessSettings, - ChatEvent, Context, Data, ShowMode, Stack, StartMode, + ChatEvent, + Context, + Data, + ShowMode, + Stack, + StartMode, ) @@ -46,7 +51,7 @@ async def switch_to( @abstractmethod async def update( self, - data: Dict, + data: dict, show_mode: Optional[ShowMode] = None, ) -> None: raise NotImplementedError @@ -91,13 +96,13 @@ async def mark_closed(self) -> None: @property @abstractmethod - def middleware_data(self) -> Dict: + def middleware_data(self) -> dict: """Middleware data.""" raise NotImplementedError @property @abstractmethod - def dialog_data(self) -> Dict: + def dialog_data(self) -> dict: """Dialog data for current context.""" raise NotImplementedError @@ -183,7 +188,7 @@ async def reset_stack(self, remove_keyboard: bool = True) -> None: raise NotImplementedError @abstractmethod - async def load_data(self) -> Dict: + async def load_data(self) -> dict: """Load data for current state.""" raise NotImplementedError diff --git a/src/aiogram_dialog/api/protocols/registry.py b/src/aiogram_dialog/api/protocols/registry.py index 5608c801..6a59095a 100644 --- a/src/aiogram_dialog/api/protocols/registry.py +++ b/src/aiogram_dialog/api/protocols/registry.py @@ -1,5 +1,5 @@ from abc import abstractmethod -from typing import Dict, Protocol, Type, Union +from typing import Protocol, Union from aiogram.fsm.state import State, StatesGroup @@ -12,5 +12,5 @@ def find_dialog(self, state: Union[State, str]) -> DialogProtocol: raise NotImplementedError @abstractmethod - def states_groups(self) -> Dict[str, Type[StatesGroup]]: + def states_groups(self) -> dict[str, type[StatesGroup]]: raise NotImplementedError diff --git a/src/aiogram_dialog/context/access_validator.py b/src/aiogram_dialog/context/access_validator.py index 1120be57..b9dbebe7 100644 --- a/src/aiogram_dialog/context/access_validator.py +++ b/src/aiogram_dialog/context/access_validator.py @@ -5,7 +5,8 @@ from aiogram_dialog import ChatEvent from aiogram_dialog.api.entities import ( - Context, Stack, + Context, + Stack, ) from aiogram_dialog.api.protocols import StackAccessValidator diff --git a/src/aiogram_dialog/context/intent_filter.py b/src/aiogram_dialog/context/intent_filter.py index 29309fdf..c38da0ec 100644 --- a/src/aiogram_dialog/context/intent_filter.py +++ b/src/aiogram_dialog/context/intent_filter.py @@ -1,4 +1,4 @@ -from typing import Optional, Type +from typing import Optional from aiogram.filters import BaseFilter from aiogram.fsm.state import StatesGroup @@ -9,7 +9,7 @@ class IntentFilter(BaseFilter): - def __init__(self, aiogd_intent_state_group: Optional[Type[StatesGroup]]): + def __init__(self, aiogd_intent_state_group: Optional[type[StatesGroup]]): self.aiogd_intent_state_group = aiogd_intent_state_group async def __call__(self, obj: TelegramObject, **kwargs) -> bool: diff --git a/src/aiogram_dialog/context/intent_middleware.py b/src/aiogram_dialog/context/intent_middleware.py index b781fa41..34de801d 100644 --- a/src/aiogram_dialog/context/intent_middleware.py +++ b/src/aiogram_dialog/context/intent_middleware.py @@ -1,5 +1,6 @@ +from collections.abc import Awaitable, Callable from logging import getLogger -from typing import Any, Awaitable, Callable, Dict, Optional +from typing import Any, Optional from aiogram import Router from aiogram.dispatcher.event.bases import UNHANDLED @@ -9,30 +10,40 @@ CallbackQuery, ChatJoinRequest, ChatMemberUpdated, - InaccessibleMessage, Message, + InaccessibleMessage, + Message, ) from aiogram.types.error_event import ErrorEvent from aiogram_dialog.api.entities import ( + DEFAULT_STACK_ID, + EVENT_CONTEXT_KEY, ChatEvent, Context, - DEFAULT_STACK_ID, DialogUpdateEvent, - EVENT_CONTEXT_KEY, EventContext, Stack, ) from aiogram_dialog.api.exceptions import ( - InvalidStackIdError, OutdatedIntent, UnknownIntent, UnknownState, + InvalidStackIdError, + OutdatedIntent, + UnknownIntent, + UnknownState, ) from aiogram_dialog.api.internal import ( - CALLBACK_DATA_KEY, CONTEXT_KEY, EVENT_SIMULATED, - ReplyCallbackQuery, STACK_KEY, STORAGE_KEY, + CALLBACK_DATA_KEY, + CONTEXT_KEY, + EVENT_SIMULATED, + STACK_KEY, + STORAGE_KEY, + ReplyCallbackQuery, ) from aiogram_dialog.api.protocols import ( - DialogRegistryProtocol, StackAccessValidator, + DialogRegistryProtocol, + StackAccessValidator, ) from aiogram_dialog.utils import remove_intent_id, split_reply_callback + from .storage import StorageProxy logger = getLogger(__name__) @@ -118,6 +129,7 @@ def event_context_from_error(event: ErrorEvent) -> EventContext: return event_context_from_chat_join(event.update.chat_join_request) elif event.update.callback_query: return event_context_from_callback(event.update.callback_query) + raise ValueError("Unsupported event type in ErrorEvent.update") class InaccessibleBusinessMessage(InaccessibleMessage): @@ -139,7 +151,7 @@ def __init__( def storage_proxy( self, event_context: EventContext, fsm_storage: BaseStorage, ) -> StorageProxy: - proxy = StorageProxy( + return StorageProxy( bot=event_context.bot, storage=fsm_storage, events_isolation=self.events_isolation, @@ -149,7 +161,6 @@ def storage_proxy( thread_id=event_context.thread_id, business_connection_id=event_context.business_connection_id, ) - return proxy def _check_outdated(self, intent_id: str, stack: Stack): """Check if intent id is outdated for stack.""" @@ -159,7 +170,7 @@ def _check_outdated(self, intent_id: str, stack: Stack): f"Outdated intent id ({intent_id}) " f"for stack ({stack.id})", ) - elif intent_id != stack.last_intent_id(): + if intent_id != stack.last_intent_id(): raise OutdatedIntent( stack.id, f"Outdated intent id ({intent_id}) " @@ -175,8 +186,7 @@ async def _load_stack( ) -> Optional[Stack]: if stack_id is None: raise InvalidStackIdError("Both stack id and intent id are None") - stack = await proxy.load_stack(stack_id) - return stack + return await proxy.load_stack(stack_id) async def _load_context_by_stack( self, @@ -198,7 +208,7 @@ async def _load_context_by_stack( else: try: context = await proxy.load_context(stack.last_intent_id()) - except: # noqa: B001,B901,E722 + except: await proxy.unlock() raise @@ -233,7 +243,7 @@ async def _load_context_by_intent( return try: self._check_outdated(intent_id, stack) - except: # noqa: B001,B901,E722 + except: await proxy.unlock() raise @@ -444,7 +454,7 @@ def __init__( self.access_validator = access_validator def _is_error_supported( - self, event: ErrorEvent, data: Dict[str, Any], + self, event: ErrorEvent, data: dict[str, Any], ) -> bool: if isinstance(event, InvalidStackIdError): return False @@ -487,10 +497,10 @@ async def _load_stack( async def __call__( self, handler: Callable[ - [ErrorEvent, Dict[str, Any]], Awaitable[Any], + [ErrorEvent, dict[str, Any]], Awaitable[Any], ], event: ErrorEvent, - data: Dict[str, Any], + data: dict[str, Any], ) -> Any: error = event.exception if not self._is_error_supported(event, data): diff --git a/src/aiogram_dialog/context/media_storage.py b/src/aiogram_dialog/context/media_storage.py index 9a972d2f..6ba290da 100644 --- a/src/aiogram_dialog/context/media_storage.py +++ b/src/aiogram_dialog/context/media_storage.py @@ -29,5 +29,5 @@ async def save_media_id( media_id: MediaId, ) -> None: if not path and not url: - return None + return self.cache[(path, url, type)] = media_id diff --git a/src/aiogram_dialog/context/storage.py b/src/aiogram_dialog/context/storage.py index 1269008a..f7766161 100644 --- a/src/aiogram_dialog/context/storage.py +++ b/src/aiogram_dialog/context/storage.py @@ -1,15 +1,20 @@ from contextlib import AsyncExitStack from copy import copy -from typing import Dict, Optional, Type +from typing import Optional from aiogram import Bot from aiogram.fsm.state import State, StatesGroup from aiogram.fsm.storage.base import ( - BaseEventIsolation, BaseStorage, StorageKey, + BaseEventIsolation, + BaseStorage, + StorageKey, ) from aiogram_dialog.api.entities import ( - AccessSettings, Context, DEFAULT_STACK_ID, Stack, + DEFAULT_STACK_ID, + AccessSettings, + Context, + Stack, ) from aiogram_dialog.api.exceptions import UnknownIntent, UnknownState @@ -24,7 +29,7 @@ def __init__( thread_id: Optional[int], business_connection_id: Optional[str], bot: Bot, - state_groups: Dict[str, Type[StatesGroup]], + state_groups: dict[str, type[StatesGroup]], ): self.storage = storage self.events_isolation = events_isolation @@ -137,9 +142,11 @@ def _fixed_stack_id(self, stack_id: str) -> str: if stack_id != DEFAULT_STACK_ID: return stack_id # private chat has chat_id=user_id and no business connection - if self.user_id in (None, self.chat_id): - if self.business_connection_id is None: - return stack_id + if ( + self.user_id in (None, self.chat_id) and + self.business_connection_id is None + ): + return stack_id return f"<{self.user_id}>" def _stack_key(self, stack_id: str) -> StorageKey: @@ -160,11 +167,11 @@ def _state(self, state: str) -> State: if real_state.state == state: return real_state except KeyError: - raise UnknownState(f"Unknown state group {group}") + raise UnknownState(f"Unknown state group {group}") from None raise UnknownState(f"Unknown state {state}") def _parse_access_settings( - self, raw: Optional[Dict], + self, raw: Optional[dict], ) -> Optional[AccessSettings]: if not raw: return None @@ -175,7 +182,7 @@ def _parse_access_settings( def _dump_access_settings( self, access_settings: Optional[AccessSettings], - ) -> Optional[Dict]: + ) -> Optional[dict]: if not access_settings: return None return { diff --git a/src/aiogram_dialog/dialog.py b/src/aiogram_dialog/dialog.py index 164d045c..a40891c9 100644 --- a/src/aiogram_dialog/dialog.py +++ b/src/aiogram_dialog/dialog.py @@ -1,12 +1,8 @@ +from collections.abc import Awaitable, Callable from logging import getLogger from typing import ( Any, - Awaitable, - Callable, - Dict, - List, Optional, - Type, TypeVar, Union, ) @@ -22,12 +18,15 @@ ) from aiogram_dialog.api.internal import Widget, WindowProtocol from aiogram_dialog.api.protocols import ( - CancelEventProcessing, DialogManager, DialogProtocol, + CancelEventProcessing, + DialogManager, + DialogProtocol, ) + from .context.intent_filter import IntentFilter from .utils import remove_intent_id from .widgets.data import PreviewAwareGetter -from .widgets.utils import ensure_data_getter, GetterVariant +from .widgets.utils import GetterVariant, ensure_data_getter logger = getLogger(__name__) @@ -51,7 +50,7 @@ def __init__( ): super().__init__(name=name or windows[0].get_state().group.__name__) self._states_group = windows[0].get_state().group - self._states: List[State] = [] + self._states: list[State] = [] for w in windows: if w.get_state().group != self._states_group: raise ValueError( @@ -61,7 +60,7 @@ def __init__( if state in self._states: raise ValueError(f"Multiple windows with state {state}") self._states.append(state) - self.windows: Dict[State, WindowProtocol] = dict( + self.windows: dict[State, WindowProtocol] = dict( zip(self._states, windows), ) self.on_start = on_start @@ -79,7 +78,7 @@ def __init__( def launch_mode(self) -> LaunchMode: return self._launch_mode - def states(self) -> List[State]: + def states(self) -> list[State]: return self._states async def process_start( @@ -113,7 +112,7 @@ async def _current_window( async def load_data( self, manager: DialogManager, - ) -> Dict: + ) -> dict: data = await manager.load_data() data.update(await self.getter(**manager.middleware_data)) return data @@ -121,8 +120,7 @@ async def load_data( async def render(self, manager: DialogManager) -> NewMessage: logger.debug("Dialog render (%s)", self) window = await self._current_window(manager) - new_message = await window.render(self, manager) - return new_message + return await window.render(self, manager) async def _message_handler( self, message: Message, dialog_manager: DialogManager, @@ -188,7 +186,7 @@ def _register_handlers(self) -> None: self.callback_query.register(self._callback_handler) self.message.register(self._message_handler) - def states_group(self) -> Type[StatesGroup]: + def states_group(self) -> type[StatesGroup]: return self._states_group def states_group_name(self) -> str: diff --git a/src/aiogram_dialog/manager/bg_manager.py b/src/aiogram_dialog/manager/bg_manager.py index 4e4adb34..2d9c4136 100644 --- a/src/aiogram_dialog/manager/bg_manager.py +++ b/src/aiogram_dialog/manager/bg_manager.py @@ -1,14 +1,14 @@ from logging import getLogger -from typing import Any, Dict, Optional, Union +from typing import Any, Optional, Union from aiogram import Bot, Router from aiogram.fsm.state import State from aiogram.types import Chat, User from aiogram_dialog.api.entities import ( + DEFAULT_STACK_ID, AccessSettings, Data, - DEFAULT_STACK_ID, DialogAction, DialogStartEvent, DialogSwitchEvent, @@ -19,10 +19,13 @@ StartMode, ) from aiogram_dialog.api.internal import ( - FakeChat, FakeUser, + FakeChat, + FakeUser, ) from aiogram_dialog.api.protocols import ( - BaseDialogManager, BgManagerFactory, UnsetId, + BaseDialogManager, + BgManagerFactory, + UnsetId, ) from aiogram_dialog.manager.updater import Updater from aiogram_dialog.utils import is_chat_loaded, is_user_loaded @@ -241,7 +244,7 @@ async def switch_to( async def update( self, - data: Dict, + data: dict, show_mode: Optional[ShowMode] = None, ) -> None: await self._load() diff --git a/src/aiogram_dialog/manager/manager.py b/src/aiogram_dialog/manager/manager.py index fc984f13..79cef84b 100644 --- a/src/aiogram_dialog/manager/manager.py +++ b/src/aiogram_dialog/manager/manager.py @@ -1,49 +1,68 @@ from copy import deepcopy from logging import getLogger -from typing import Any, cast, Dict, Optional, Union +from typing import Any, Optional, Union, cast from aiogram import Router from aiogram.enums import ChatType from aiogram.fsm.state import State from aiogram.types import ( - CallbackQuery, Chat, ErrorEvent, Message, ReplyKeyboardMarkup, User, + CallbackQuery, + Chat, + ErrorEvent, + Message, + ReplyKeyboardMarkup, + User, ) from aiogram_dialog.api.entities import ( + DEFAULT_STACK_ID, + EVENT_CONTEXT_KEY, AccessSettings, ChatEvent, Context, Data, - DEFAULT_STACK_ID, - EVENT_CONTEXT_KEY, EventContext, LaunchMode, MediaId, NewMessage, + OldMessage, ShowMode, Stack, StartMode, + UnknownText, ) -from aiogram_dialog.api.entities import OldMessage, UnknownText from aiogram_dialog.api.exceptions import ( - IncorrectBackgroundError, InvalidKeyboardType, NoContextError, + IncorrectBackgroundError, + InvalidKeyboardType, + NoContextError, ) from aiogram_dialog.api.internal import ( - CONTEXT_KEY, EVENT_SIMULATED, FakeChat, FakeUser, - STACK_KEY, STORAGE_KEY, + CONTEXT_KEY, + EVENT_SIMULATED, + STACK_KEY, + STORAGE_KEY, + FakeChat, + FakeUser, ) from aiogram_dialog.api.protocols import ( - BaseDialogManager, DialogManager, DialogProtocol, DialogRegistryProtocol, - MediaIdStorageProtocol, MessageManagerProtocol, MessageNotModified, + BaseDialogManager, + DialogManager, + DialogProtocol, + DialogRegistryProtocol, + MediaIdStorageProtocol, + MessageManagerProtocol, + MessageNotModified, UnsetId, ) from aiogram_dialog.context.storage import StorageProxy from aiogram_dialog.utils import get_media_id + from .bg_manager import ( BgManager, coalesce_business_connection_id, coalesce_thread_id, ) + logger = getLogger(__name__) @@ -55,7 +74,7 @@ def __init__( media_id_storage: MediaIdStorageProtocol, registry: DialogRegistryProtocol, router: Router, - data: Dict, + data: dict, ): self.disabled = False self.message_manager = message_manager @@ -81,12 +100,12 @@ def event(self) -> ChatEvent: return self._event @property - def middleware_data(self) -> Dict: + def middleware_data(self) -> dict: """Middleware data.""" return self._data @property - def dialog_data(self) -> Dict: + def dialog_data(self) -> dict: """Dialog data for current context.""" return self.current_context().dialog_data @@ -103,7 +122,7 @@ def check_disabled(self): "method to access methods from background tasks", ) - async def load_data(self) -> Dict: + async def load_data(self) -> dict: context = self.current_context() return { "dialog_data": context.dialog_data, @@ -188,9 +207,9 @@ async def done( async def answer_callback(self) -> None: if not isinstance(self.event, CallbackQuery): - return + return None if self.is_event_simulated(): - return + return None return await self.message_manager.answer_callback( bot=self._data["bot"], callback_query=self.event, @@ -289,7 +308,7 @@ async def _process_launch_mode( ): if new_dialog.launch_mode in (LaunchMode.EXCLUSIVE, LaunchMode.ROOT): await self.reset_stack(remove_keyboard=False) - if new_dialog.launch_mode is LaunchMode.SINGLE_TOP: + if new_dialog.launch_mode is LaunchMode.SINGLE_TOP: # noqa: SIM102 if new_dialog is old_dialog: await self.storage().remove_context(self.current_stack().pop()) self._data[CONTEXT_KEY] = None @@ -402,6 +421,7 @@ def _get_message_from_callback( chat=event_context.chat, message_id=current_message.message_id, business_connection_id=event_context.business_connection_id, + content_type=current_message.content_type, ) elif not stack or not stack.last_message_id: return None @@ -414,6 +434,7 @@ def _get_message_from_callback( chat=event_context.chat, message_id=stack.last_message_id, business_connection_id=event_context.business_connection_id, + content_type=stack.content_type, ) def _get_last_message(self) -> Optional[OldMessage]: @@ -438,16 +459,18 @@ def _get_last_message(self) -> Optional[OldMessage]: chat=event_context.chat, message_id=stack.last_message_id, business_connection_id=event_context.business_connection_id, + content_type=stack.content_type, ) - def _save_last_message(self, message: OldMessage): + def _save_last_message(self, message: OldMessage) -> None: stack = self.current_stack() stack.last_message_id = message.message_id stack.last_media_id = message.media_id stack.last_media_unique_id = message.media_uniq_id stack.last_reply_keyboard = message.has_reply_keyboard + stack.content_type = message.content_type - def _calc_show_mode(self) -> ShowMode: + def _calc_show_mode(self) -> ShowMode: # noqa: PLR0911 if self.show_mode is not ShowMode.AUTO: return self.show_mode if self.middleware_data["event_chat"].type != ChatType.PRIVATE: @@ -468,7 +491,7 @@ def _calc_show_mode(self) -> ShowMode: async def update( self, - data: Dict, + data: dict, show_mode: Optional[ShowMode] = None, ) -> None: self.current_context().dialog_data.update(data) diff --git a/src/aiogram_dialog/manager/manager_factory.py b/src/aiogram_dialog/manager/manager_factory.py index a42f157c..ae364e14 100644 --- a/src/aiogram_dialog/manager/manager_factory.py +++ b/src/aiogram_dialog/manager/manager_factory.py @@ -1,13 +1,14 @@ -from typing import Dict - from aiogram import Router from aiogram_dialog.api.entities import ChatEvent from aiogram_dialog.api.internal import DialogManagerFactory from aiogram_dialog.api.protocols import ( - DialogManager, DialogRegistryProtocol, - MediaIdStorageProtocol, MessageManagerProtocol, + DialogManager, + DialogRegistryProtocol, + MediaIdStorageProtocol, + MessageManagerProtocol, ) + from .manager import ManagerImpl @@ -21,7 +22,7 @@ def __init__( self.media_id_storage = media_id_storage def __call__( - self, event: ChatEvent, data: Dict, + self, event: ChatEvent, data: dict, registry: DialogRegistryProtocol, router: Router, ) -> DialogManager: diff --git a/src/aiogram_dialog/manager/manager_middleware.py b/src/aiogram_dialog/manager/manager_middleware.py index 4d9d4829..041a7d71 100644 --- a/src/aiogram_dialog/manager/manager_middleware.py +++ b/src/aiogram_dialog/manager/manager_middleware.py @@ -1,13 +1,16 @@ -from typing import Any, Awaitable, Callable, Dict, Union +from collections.abc import Awaitable, Callable +from typing import Any, Union from aiogram import Router from aiogram.dispatcher.middlewares.base import BaseMiddleware from aiogram.types import TelegramObject, Update from aiogram_dialog.api.entities import ChatEvent, DialogUpdateEvent -from aiogram_dialog.api.internal import DialogManagerFactory, STORAGE_KEY +from aiogram_dialog.api.internal import STORAGE_KEY, DialogManagerFactory from aiogram_dialog.api.protocols import ( - BgManagerFactory, DialogManager, DialogRegistryProtocol, + BgManagerFactory, + DialogManager, + DialogRegistryProtocol, ) MANAGER_KEY = "dialog_manager" @@ -27,18 +30,18 @@ def __init__( self.router = router def _is_event_supported( - self, event: TelegramObject, data: Dict[str, Any], + self, event: TelegramObject, data: dict[str, Any], ) -> bool: return STORAGE_KEY in data async def __call__( self, handler: Callable[ - [Union[Update, DialogUpdateEvent], Dict[str, Any]], + [Union[Update, DialogUpdateEvent], dict[str, Any]], Awaitable[Any], ], event: ChatEvent, - data: Dict[str, Any], + data: dict[str, Any], ) -> Any: if self._is_event_supported(event, data): data[MANAGER_KEY] = self.dialog_manager_factory( @@ -67,11 +70,11 @@ def __init__( async def __call__( self, handler: Callable[ - [Union[TelegramObject, DialogUpdateEvent], Dict[str, Any]], + [Union[TelegramObject, DialogUpdateEvent], dict[str, Any]], Awaitable[TelegramObject], ], event: TelegramObject, - data: Dict[str, Any], + data: dict[str, Any], ) -> Any: data[BG_FACTORY_KEY] = self.bg_manager_factory return await handler(event, data) diff --git a/src/aiogram_dialog/manager/message_manager.py b/src/aiogram_dialog/manager/message_manager.py index 773c9cb2..2ad38c09 100644 --- a/src/aiogram_dialog/manager/message_manager.py +++ b/src/aiogram_dialog/manager/message_manager.py @@ -20,10 +20,15 @@ ) from aiogram_dialog.api.entities import ( - MediaAttachment, MediaId, NewMessage, OldMessage, ShowMode, + MediaAttachment, + MediaId, + NewMessage, + OldMessage, + ShowMode, ) from aiogram_dialog.api.protocols import ( - MessageManagerProtocol, MessageNotModified, + MessageManagerProtocol, + MessageNotModified, ) from aiogram_dialog.utils import get_media_id @@ -66,6 +71,7 @@ def _combine(sent_message: NewMessage, message_result: Message) -> OldMessage: media_uniq_id=(media_id.file_unique_id if media_id else None), media_id=(media_id.file_id if media_id else None), business_connection_id=message_result.business_connection_id, + content_type=message_result.content_type, ) @@ -111,13 +117,25 @@ def need_reply_keyboard(self, new_message: Optional[NewMessage]) -> bool: return False return isinstance(new_message.reply_markup, ReplyKeyboardMarkup) + def had_voice(self, old_message: OldMessage) -> bool: + return old_message.content_type == ContentType.VOICE + + def need_voice(self, new_message: NewMessage) -> bool: + return ( + new_message.media is not None + and new_message.media.type == ContentType.VOICE + ) + def _message_changed( self, new_message: NewMessage, old_message: OldMessage, ) -> bool: - if new_message.text != old_message.text: - return True - # we cannot actually compare reply keyboards - if new_message.reply_markup or old_message.has_reply_keyboard: + if ( + (new_message.text != old_message.text) or + # we cannot actually compare reply keyboards + (new_message.reply_markup or old_message.has_reply_keyboard) or + # we do not know if link preview changed + new_message.link_preview_options + ): return True if self.had_media(old_message) != self.need_media(new_message): @@ -132,11 +150,15 @@ def _message_changed( def _can_edit(self, new_message: NewMessage, old_message: OldMessage) -> bool: - # we cannot edit message if media appeared or removed - return ( - self.had_media(old_message) == self.need_media(new_message) and - not self.had_reply_keyboard(old_message) and - not self.need_reply_keyboard(new_message) + # we cannot edit message if media removed + if self.had_media(old_message) and not self.need_media(new_message): + return False + # we cannot edit a message if there was voice + if self.had_voice(old_message) or self.need_voice(new_message): + return False + return not ( + self.had_reply_keyboard(old_message) + or self.need_reply_keyboard(new_message) ) async def show_message( @@ -195,7 +217,7 @@ async def remove_kbd( old_message: Optional[OldMessage], ) -> Optional[Message]: if show_mode is ShowMode.NO_UPDATE: - return + return None if show_mode is ShowMode.DELETE_AND_SEND and old_message: return await self.remove_message_safe(bot, old_message, None) return await self._remove_kbd(bot, old_message, None) @@ -209,6 +231,7 @@ async def _remove_kbd( if self.had_reply_keyboard(old_message): if not self.need_reply_keyboard(new_message): return await self.remove_reply_kbd(bot, old_message) + return None else: return await self.remove_inline_kbd(bot, old_message) @@ -216,7 +239,7 @@ async def remove_inline_kbd( self, bot: Bot, old_message: Optional[OldMessage], ) -> Optional[Message]: if not old_message: - return + return None logger.debug("remove_inline_kbd in %s", old_message.chat) try: return await bot.edit_message_reply_markup( @@ -231,6 +254,8 @@ async def remove_inline_kbd( pass elif "message to edit not found" in err.message: pass + elif "MESSAGE_ID_INVALID" in err.message: + pass else: raise err @@ -238,7 +263,7 @@ async def remove_reply_kbd( self, bot: Bot, old_message: Optional[OldMessage], ) -> Optional[Message]: if not old_message: - return + return None logger.debug("remove_reply_kbd in %s", old_message.chat) return await self.send_text( bot=bot, @@ -293,7 +318,10 @@ async def edit_message( self, bot: Bot, new_message: NewMessage, old_message: OldMessage, ) -> Message: if new_message.media: - if new_message.media.file_id == old_message.media_id: + if ( + old_message.media_id is not None and + new_message.media.file_id == old_message.media_id + ): return await self.edit_caption(bot, new_message, old_message) return await self.edit_media(bot, new_message, old_message) else: @@ -323,7 +351,7 @@ async def edit_text( text=new_message.text, reply_markup=new_message.reply_markup, parse_mode=new_message.parse_mode, - disable_web_page_preview=new_message.disable_web_page_preview, + link_preview_options=new_message.link_preview_options, ) async def edit_media( @@ -338,7 +366,6 @@ async def edit_media( caption=new_message.text, reply_markup=new_message.reply_markup, parse_mode=new_message.parse_mode, - disable_web_page_preview=new_message.disable_web_page_preview, media=await self.get_media_source(new_message.media, bot), **new_message.media.kwargs, ) @@ -367,9 +394,9 @@ async def send_text(self, bot: Bot, new_message: NewMessage) -> Message: text=new_message.text, message_thread_id=new_message.thread_id, business_connection_id=new_message.business_connection_id, - disable_web_page_preview=new_message.disable_web_page_preview, reply_markup=new_message.reply_markup, parse_mode=new_message.parse_mode, + link_preview_options=new_message.link_preview_options, ) async def send_media(self, bot: Bot, new_message: NewMessage) -> Message: diff --git a/src/aiogram_dialog/manager/sub_manager.py b/src/aiogram_dialog/manager/sub_manager.py index 86069a34..5977cf5a 100644 --- a/src/aiogram_dialog/manager/sub_manager.py +++ b/src/aiogram_dialog/manager/sub_manager.py @@ -1,17 +1,23 @@ import dataclasses -from typing import Any, Dict, Optional, Union +from typing import Any, Optional, Union from aiogram.fsm.state import State from aiogram.types import Message from aiogram_dialog.api.entities import ( AccessSettings, - ChatEvent, Data, ShowMode, StartMode, + ChatEvent, + Context, + Data, + ShowMode, + Stack, + StartMode, ) -from aiogram_dialog.api.entities import Context, Stack from aiogram_dialog.api.internal import Widget from aiogram_dialog.api.protocols import ( - BaseDialogManager, DialogManager, UnsetId, + BaseDialogManager, + DialogManager, + UnsetId, ) @@ -33,12 +39,12 @@ def event(self) -> ChatEvent: return self.manager.event @property - def middleware_data(self) -> Dict: + def middleware_data(self) -> dict: """Middleware data.""" return self.manager.middleware_data @property - def dialog_data(self) -> Dict: + def dialog_data(self) -> dict: """Dialog data for current context.""" return self.current_context().dialog_data @@ -74,7 +80,7 @@ async def answer_callback(self) -> None: async def reset_stack(self, remove_keyboard: bool = True) -> None: return await self.manager.reset_stack(remove_keyboard) - async def load_data(self) -> Dict: + async def load_data(self) -> dict: return await self.manager.load_data() def find(self, widget_id) -> Optional[Any]: @@ -133,7 +139,7 @@ async def switch_to( async def update( self, - data: Dict, + data: dict, show_mode: Optional[ShowMode] = None, ) -> None: self.current_context().dialog_data.update(data) diff --git a/src/aiogram_dialog/manager/updater.py b/src/aiogram_dialog/manager/updater.py index df8926f7..fa44ecad 100644 --- a/src/aiogram_dialog/manager/updater.py +++ b/src/aiogram_dialog/manager/updater.py @@ -16,7 +16,7 @@ def __init__(self, dp: Router): async def notify(self, bot: Bot, update: DialogUpdate) -> None: def callback(): - asyncio.create_task( + asyncio.create_task( # noqa: RUF006 self._process_update(bot, update), ) diff --git a/src/aiogram_dialog/setup.py b/src/aiogram_dialog/setup.py index c7825799..e26b12b5 100644 --- a/src/aiogram_dialog/setup.py +++ b/src/aiogram_dialog/setup.py @@ -1,8 +1,9 @@ -from typing import Callable, Dict, Iterable, Optional, Type, Union +from collections.abc import Callable, Iterable +from typing import Optional, Union from aiogram import Router from aiogram.dispatcher.event.telegram import TelegramEventObserver -from aiogram.fsm.state import any_state, State, StatesGroup +from aiogram.fsm.state import State, StatesGroup, any_state from aiogram.fsm.storage.base import BaseEventIsolation from aiogram.fsm.storage.memory import SimpleEventIsolation @@ -10,23 +11,29 @@ from aiogram_dialog.api.exceptions import UnregisteredDialogError from aiogram_dialog.api.internal import DialogManagerFactory from aiogram_dialog.api.protocols import ( - BgManagerFactory, DialogProtocol, DialogRegistryProtocol, - MediaIdStorageProtocol, MessageManagerProtocol, StackAccessValidator, + BgManagerFactory, + DialogProtocol, + DialogRegistryProtocol, + MediaIdStorageProtocol, + MessageManagerProtocol, + StackAccessValidator, ) from aiogram_dialog.context.intent_middleware import ( - context_saver_middleware, - context_unlocker_middleware, IntentErrorMiddleware, IntentMiddlewareFactory, + context_saver_middleware, + context_unlocker_middleware, ) from aiogram_dialog.context.media_storage import MediaIdStorage from aiogram_dialog.manager.bg_manager import BgManagerFactoryImpl from aiogram_dialog.manager.manager_factory import DefaultManagerFactory from aiogram_dialog.manager.manager_middleware import ( - BgFactoryMiddleware, ManagerMiddleware, + BgFactoryMiddleware, + ManagerMiddleware, ) from aiogram_dialog.manager.message_manager import MessageManager from aiogram_dialog.manager.update_handler import handle_update + from .about import about_dialog from .context.access_validator import DefaultAccessValidator @@ -63,7 +70,7 @@ def find_dialog(self, state: Union[State, str]) -> DialogProtocol: f" (looking by state `{state}`)", ) from e - def states_groups(self) -> Dict[str, Type[StatesGroup]]: + def states_groups(self) -> dict[str, type[StatesGroup]]: self._ensure_loaded() return self._states_groups diff --git a/src/aiogram_dialog/test_tools/bot_client.py b/src/aiogram_dialog/test_tools/bot_client.py index 961b5a92..e0426870 100644 --- a/src/aiogram_dialog/test_tools/bot_client.py +++ b/src/aiogram_dialog/test_tools/bot_client.py @@ -5,10 +5,20 @@ from aiogram import Bot, Dispatcher from aiogram.methods import AnswerCallbackQuery, TelegramMethod from aiogram.types import ( - CallbackQuery, Chat, ChatJoinRequest, - ChatMemberAdministrator, ChatMemberBanned, ChatMemberLeft, - ChatMemberMember, ChatMemberOwner, ChatMemberRestricted, ChatMemberUpdated, - InlineKeyboardButton, Message, Update, User, + CallbackQuery, + Chat, + ChatJoinRequest, + ChatMemberAdministrator, + ChatMemberBanned, + ChatMemberLeft, + ChatMemberMember, + ChatMemberOwner, + ChatMemberRestricted, + ChatMemberUpdated, + InlineKeyboardButton, + Message, + Update, + User, ) from .keyboard import InlineButtonLocator diff --git a/src/aiogram_dialog/test_tools/memory_storage.py b/src/aiogram_dialog/test_tools/memory_storage.py index f8112c34..d0ffba70 100644 --- a/src/aiogram_dialog/test_tools/memory_storage.py +++ b/src/aiogram_dialog/test_tools/memory_storage.py @@ -1,12 +1,11 @@ import json from collections import defaultdict from dataclasses import dataclass -from typing import Any, Dict, Optional, Union +from typing import Any, Optional, Union from aiogram.fsm.state import State from aiogram.fsm.storage.base import BaseStorage, StorageKey - StateType = Optional[Union[str, State]] @@ -17,7 +16,7 @@ class MemoryStorageRecord: class JsonMemoryStorage(BaseStorage): - storage: Dict[StorageKey, MemoryStorageRecord] + storage: dict[StorageKey, MemoryStorageRecord] def __init__(self) -> None: self.storage = defaultdict(MemoryStorageRecord) @@ -37,8 +36,8 @@ async def set_state( async def get_state(self, key: StorageKey) -> Optional[str]: return self.storage[key].state - async def set_data(self, key: StorageKey, data: Dict[str, Any]) -> None: + async def set_data(self, key: StorageKey, data: dict[str, Any]) -> None: self.storage[key].data = json.dumps(data) - async def get_data(self, key: StorageKey) -> Dict[str, Any]: + async def get_data(self, key: StorageKey) -> dict[str, Any]: return json.loads(self.storage[key].data) diff --git a/src/aiogram_dialog/test_tools/mock_message_manager.py b/src/aiogram_dialog/test_tools/mock_message_manager.py index 8a526da3..6c5fbf36 100644 --- a/src/aiogram_dialog/test_tools/mock_message_manager.py +++ b/src/aiogram_dialog/test_tools/mock_message_manager.py @@ -1,18 +1,24 @@ from copy import deepcopy from datetime import datetime -from typing import Optional, Set +from typing import Optional from uuid import uuid4 from aiogram import Bot from aiogram.types import ( - Audio, CallbackQuery, Document, Message, PhotoSize, ReplyKeyboardMarkup, + Audio, + CallbackQuery, + Document, + Message, + PhotoSize, + ReplyKeyboardMarkup, Video, ) from aiogram_dialog import ShowMode from aiogram_dialog.api.entities import MediaAttachment, NewMessage, OldMessage from aiogram_dialog.api.protocols import ( - MessageManagerProtocol, MessageNotModified, + MessageManagerProtocol, + MessageNotModified, ) @@ -51,7 +57,7 @@ def file_unique_id(media: MediaAttachment) -> str: class MockMessageManager(MessageManagerProtocol): def __init__(self): - self.answered_callbacks: Set[str] = set() + self.answered_callbacks: set[str] = set() self.sent_messages = [] self.last_message_id = 0 @@ -79,9 +85,9 @@ async def remove_kbd( old_message: Optional[OldMessage], ) -> Optional[Message]: if not old_message: - return + return None if show_mode in (ShowMode.DELETE_AND_SEND, ShowMode.NO_UPDATE): - return + return None assert isinstance(old_message, OldMessage) message = Message( diff --git a/src/aiogram_dialog/tools/preview.py b/src/aiogram_dialog/tools/preview.py index 63b24188..75d7b269 100644 --- a/src/aiogram_dialog/tools/preview.py +++ b/src/aiogram_dialog/tools/preview.py @@ -2,27 +2,35 @@ import logging from dataclasses import dataclass from datetime import datetime -from typing import Any, Dict, List, Optional, Type, Union +from typing import Any, Optional, Union from aiogram import Router from aiogram.fsm.state import State, StatesGroup from aiogram.types import ( - CallbackQuery, Chat, ContentType, InlineKeyboardMarkup, Message, - ReplyKeyboardMarkup, User, + CallbackQuery, + Chat, + ContentType, + InlineKeyboardMarkup, + Message, + ReplyKeyboardMarkup, + User, ) from jinja2 import Environment, PackageLoader, select_autoescape from aiogram_dialog import ( - BaseDialogManager, Dialog, DialogManager, DialogProtocol, + BaseDialogManager, + Dialog, + DialogManager, + DialogProtocol, ) from aiogram_dialog.api.entities import ( + EVENT_CONTEXT_KEY, AccessSettings, ChatEvent, Context, Data, DialogAction, DialogUpdateEvent, - EVENT_CONTEXT_KEY, EventContext, MediaAttachment, NewMessage, @@ -47,8 +55,8 @@ class RenderWindow: message: str state: str state_name: str - keyboard: List[List[RenderButton]] - reply_keyboard: List[List[RenderButton]] + keyboard: list[list[RenderButton]] + reply_keyboard: list[list[RenderButton]] photo: Optional[str] text_input: Optional[RenderButton] attachment_input: Optional[RenderButton] @@ -57,7 +65,7 @@ class RenderWindow: @dataclass class RenderDialog: state_group: str - windows: List[RenderWindow] + windows: list[RenderWindow] class FakeManager(DialogManager): @@ -104,14 +112,14 @@ async def back(self, show_mode: Optional[ShowMode] = None) -> None: await self.switch_to(new_state, show_mode) @property - def middleware_data(self) -> Dict: + def middleware_data(self) -> dict: return self._data @property def event(self) -> ChatEvent: return self._event - async def load_data(self) -> Dict: + async def load_data(self) -> dict: return {} async def close_manager(self) -> None: @@ -131,7 +139,7 @@ def is_preview(self) -> bool: return True @property - def dialog_data(self) -> Dict: + def dialog_data(self) -> dict: return self.current_context().dialog_data def reset_context(self) -> None: @@ -208,7 +216,7 @@ def find(self, widget_id) -> Optional[Any]: async def update( self, - data: Dict, + data: dict, show_mode: Optional[ShowMode] = None, ) -> None: pass @@ -261,7 +269,7 @@ async def create_button( manager.set_state(state) try: await dialog._callback_handler(callback_query, dialog_manager=manager) - except Exception: # noqa: B902 + except Exception: logging.debug("Click %s", callback) state = manager.current_context().state return RenderButton(title=title, state=state.state) @@ -289,7 +297,7 @@ async def render_input( manager.set_state(state) try: await dialog._message_handler(message, dialog_manager=manager) - except Exception: # noqa: B902 + except Exception: logging.debug("Input %s", content_type) if state == manager.current_context().state: @@ -311,22 +319,20 @@ async def render_inline_keyboard( dialog: Dialog, simulate_events: bool, ): - keyboard = [] - for row in reply_markup.inline_keyboard: - keyboard_row = [] - for button in row: - keyboard_row.append( - await create_button( - title=button.text, - callback=button.callback_data, - manager=manager, - dialog=dialog, - state=state, - simulate_events=simulate_events, - ), + return [ + [ + await create_button( + title=button.text, + callback=button.callback_data, + manager=manager, + dialog=dialog, + state=state, + simulate_events=simulate_events, ) - keyboard.append(keyboard_row) - return keyboard + for button in row + ] + for row in reply_markup.inline_keyboard + ] async def render_reply_keyboard( @@ -408,7 +414,7 @@ async def create_window( async def render_dialog( manager: FakeManager, - group: Type[StatesGroup], + group: type[StatesGroup], dialog: Dialog, simulate_events: bool, ) -> RenderDialog: diff --git a/src/aiogram_dialog/tools/transitions.py b/src/aiogram_dialog/tools/transitions.py index 161dd32d..3706658d 100644 --- a/src/aiogram_dialog/tools/transitions.py +++ b/src/aiogram_dialog/tools/transitions.py @@ -1,5 +1,5 @@ import os.path -from typing import Iterable, List, Sequence, Tuple +from collections.abc import Iterable, Sequence from aiogram import Router from aiogram.fsm.state import State @@ -46,7 +46,7 @@ def widget_edges(nodes, dialog, starts, current_state, kbd): def walk_keyboard( nodes, dialog, - starts: List[Tuple[State, State]], + starts: list[tuple[State, State]], current_state: State, keyboards: Sequence, ): @@ -59,7 +59,7 @@ def walk_keyboard( def find_starts( current_state, keyboards: Sequence, -) -> Iterable[Tuple[State, State]]: +) -> Iterable[tuple[State, State]]: for kbd in keyboards: if isinstance(kbd, Group): yield from find_starts(current_state, kbd.buttons) @@ -68,7 +68,7 @@ def find_starts( def render_window( - nodes: dict, dialog: Dialog, starts: List[Tuple[State, State]], + nodes: dict, dialog: Dialog, starts: list[tuple[State, State]], window: WindowProtocol, ): walk_keyboard( diff --git a/src/aiogram_dialog/utils.py b/src/aiogram_dialog/utils.py index 17682f63..57a9831a 100644 --- a/src/aiogram_dialog/utils.py +++ b/src/aiogram_dialog/utils.py @@ -1,5 +1,5 @@ from logging import getLogger -from typing import List, Optional, Tuple, Union +from typing import Optional, Union from aiogram.types import ( CallbackQuery, @@ -14,7 +14,9 @@ ) from aiogram_dialog.api.entities import ( - ChatEvent, DialogUpdateEvent, MediaId, + ChatEvent, + DialogUpdateEvent, + MediaId, ) from aiogram_dialog.api.internal import RawKeyboard @@ -70,7 +72,7 @@ def join_reply_callback(text: str, callback_data: str) -> str: def split_reply_callback( data: Optional[str], -) -> Tuple[Optional[str], Optional[str]]: +) -> tuple[Optional[str], Optional[str]]: if not data: return None, None text = data.rstrip(REPLY_CALLBACK_SYMBOLS) @@ -103,15 +105,12 @@ def _transform_to_reply_button( def transform_to_reply_keyboard( - keyboard: List[List[Union[InlineKeyboardButton, KeyboardButton]]], -) -> List[List[KeyboardButton]]: - new_kdb = [] - for row in keyboard: - new_row = [] - new_kdb.append(new_row) - for button in row: - new_row.append(_transform_to_reply_button(button)) - return new_kdb + keyboard: list[list[Union[InlineKeyboardButton, KeyboardButton]]], +) -> list[list[KeyboardButton]]: + return [ + [_transform_to_reply_button(button) for button in row] + for row in keyboard + ] def get_chat(event: ChatEvent) -> Chat: @@ -124,6 +123,8 @@ def get_chat(event: ChatEvent) -> Chat: if not event.message: return Chat(id=event.from_user.id, type="") return event.message.chat + else: + raise TypeError def is_chat_loaded(chat: Chat) -> bool: @@ -186,7 +187,7 @@ def add_intent_id(keyboard: RawKeyboard, intent_id: str): ) -def remove_intent_id(callback_data: str) -> Tuple[Optional[str], str]: +def remove_intent_id(callback_data: str) -> tuple[Optional[str], str]: if CB_SEP in callback_data: intent_id, new_data = callback_data.split(CB_SEP, maxsplit=1) return intent_id, new_data diff --git a/src/aiogram_dialog/widgets/common/__init__.py b/src/aiogram_dialog/widgets/common/__init__.py index 7f0dd208..1dcad9bc 100644 --- a/src/aiogram_dialog/widgets/common/__init__.py +++ b/src/aiogram_dialog/widgets/common/__init__.py @@ -2,16 +2,26 @@ "Actionable", "BaseWidget", "ManagedWidget", - "BaseScroll", "ManagedScroll", - "OnPageChanged", "OnPageChangedVariants", "Scroll", "sync_scroll", - "true_condition", "Whenable", "WhenCondition", + "BaseScroll", + "ManagedScroll", + "OnPageChanged", + "OnPageChangedVariants", + "Scroll", + "sync_scroll", + "true_condition", + "Whenable", + "WhenCondition", ] from .action import Actionable from .base import BaseWidget from .managed import ManagedWidget from .scroll import ( - BaseScroll, ManagedScroll, OnPageChanged, OnPageChangedVariants, Scroll, + BaseScroll, + ManagedScroll, + OnPageChanged, + OnPageChangedVariants, + Scroll, sync_scroll, ) -from .when import true_condition, Whenable, WhenCondition +from .when import Whenable, WhenCondition, true_condition diff --git a/src/aiogram_dialog/widgets/common/action.py b/src/aiogram_dialog/widgets/common/action.py index 1246e78f..4f460049 100644 --- a/src/aiogram_dialog/widgets/common/action.py +++ b/src/aiogram_dialog/widgets/common/action.py @@ -3,6 +3,7 @@ from aiogram_dialog.api.exceptions import InvalidWidgetIdError from aiogram_dialog.api.protocols import DialogManager + from .base import BaseWidget ID_PATTERN = re.compile("^[a-zA-Z0-9_.]+$") diff --git a/src/aiogram_dialog/widgets/common/items.py b/src/aiogram_dialog/widgets/common/items.py index 211a5b11..6419f3d4 100644 --- a/src/aiogram_dialog/widgets/common/items.py +++ b/src/aiogram_dialog/widgets/common/items.py @@ -1,9 +1,10 @@ +from collections.abc import Callable, Sequence from operator import itemgetter -from typing import Callable, Dict, Sequence, Union +from typing import Union from magic_filter import MagicFilter -ItemsGetter = Callable[[Dict], Sequence] +ItemsGetter = Callable[[dict], Sequence] ItemsGetterVariant = Union[str, ItemsGetter, MagicFilter, Sequence] @@ -15,7 +16,7 @@ def identity(data) -> Sequence: def _get_magic_getter(f: MagicFilter) -> ItemsGetter: - def items_magic(data: Dict) -> Sequence: + def items_magic(data: dict) -> Sequence: items = f.resolve(data) if isinstance(items, Sequence): return items diff --git a/src/aiogram_dialog/widgets/common/scroll.py b/src/aiogram_dialog/widgets/common/scroll.py index 5c1ceb86..42ba21ac 100644 --- a/src/aiogram_dialog/widgets/common/scroll.py +++ b/src/aiogram_dialog/widgets/common/scroll.py @@ -1,19 +1,22 @@ from abc import ABC, abstractmethod -from typing import Awaitable, Callable, Dict, Protocol, Sequence, Union +from collections.abc import Awaitable, Callable, Sequence +from typing import Protocol, Union from aiogram_dialog.api.entities import ChatEvent from aiogram_dialog.api.internal import Widget from aiogram_dialog.api.protocols import DialogManager from aiogram_dialog.widgets.widget_event import ( - ensure_event_processor, WidgetEventProcessor, + WidgetEventProcessor, + ensure_event_processor, ) + from .action import Actionable from .managed import ManagedWidget class Scroll(Widget, Protocol): @abstractmethod - async def get_page_count(self, data: Dict, manager: DialogManager) -> int: + async def get_page_count(self, data: dict, manager: DialogManager) -> int: raise NotImplementedError @abstractmethod @@ -32,7 +35,7 @@ def managed(self, manager: DialogManager) -> "ManagedScroll": class ManagedScroll(ManagedWidget[Scroll]): - async def get_page_count(self, data: Dict) -> int: + async def get_page_count(self, data: dict) -> int: return await self.widget.get_page_count(data, self.manager) async def get_page(self) -> int: diff --git a/src/aiogram_dialog/widgets/common/when.py b/src/aiogram_dialog/widgets/common/when.py index fec1eb81..5dd9a391 100644 --- a/src/aiogram_dialog/widgets/common/when.py +++ b/src/aiogram_dialog/widgets/common/when.py @@ -1,7 +1,7 @@ from __future__ import annotations from abc import abstractmethod -from typing import Dict, Protocol, Union +from typing import Protocol, Union from magic_filter import MagicFilter @@ -12,7 +12,7 @@ class Predicate(Protocol): @abstractmethod def __call__( self, - data: Dict, + data: dict, widget: Whenable, dialog_manager: DialogManager, ) -> bool: @@ -32,7 +32,7 @@ def __call__( def new_when_field(fieldname: str) -> Predicate: def when_field( - data: Dict, widget: "Whenable", manager: DialogManager, + data: dict, widget: Whenable, manager: DialogManager, ) -> bool: return bool(data.get(fieldname)) @@ -41,14 +41,14 @@ def when_field( def new_when_magic(f: MagicFilter) -> Predicate: def when_magic( - data: Dict, widget: "Whenable", manager: DialogManager, + data: dict, widget: Whenable, manager: DialogManager, ) -> bool: return f.resolve(data) return when_magic -def true_condition(data: Dict, widget: "Whenable", manager: DialogManager): +def true_condition(data: dict, widget: Whenable, manager: DialogManager): return True @@ -64,5 +64,5 @@ def __init__(self, when: WhenCondition = None): else: self.condition = when - def is_(self, data: Dict, manager: DialogManager): + def is_(self, data: dict, manager: DialogManager): return self.condition(data, self, manager) diff --git a/src/aiogram_dialog/widgets/data/data_context.py b/src/aiogram_dialog/widgets/data/data_context.py index 4798b7ca..c3cbd941 100644 --- a/src/aiogram_dialog/widgets/data/data_context.py +++ b/src/aiogram_dialog/widgets/data/data_context.py @@ -1,12 +1,10 @@ -from typing import Dict, List - from aiogram_dialog.api.internal.widgets import DataGetter from aiogram_dialog.api.protocols import DialogManager class CompositeGetter: def __init__(self, *getters: DataGetter): - self.getters: List[DataGetter] = list(getters) + self.getters: list[DataGetter] = list(getters) async def __call__(self, **kwargs): data = {} @@ -16,7 +14,7 @@ async def __call__(self, **kwargs): class StaticGetter: - def __init__(self, data: Dict): + def __init__(self, data: dict): self.data = data async def __call__(self, **kwargs): diff --git a/src/aiogram_dialog/widgets/input/base.py b/src/aiogram_dialog/widgets/input/base.py index 32d3d143..b9f3e1a3 100644 --- a/src/aiogram_dialog/widgets/input/base.py +++ b/src/aiogram_dialog/widgets/input/base.py @@ -1,5 +1,6 @@ from abc import abstractmethod -from typing import Any, Awaitable, Callable, Optional, Sequence, Union +from collections.abc import Awaitable, Callable, Sequence +from typing import Any, Optional, Union from aiogram import F from aiogram.dispatcher.event.handler import FilterObject @@ -7,12 +8,13 @@ from aiogram_dialog.api.internal import InputWidget from aiogram_dialog.api.protocols import ( - DialogManager, DialogProtocol, + DialogManager, + DialogProtocol, ) from aiogram_dialog.widgets.common import Actionable from aiogram_dialog.widgets.widget_event import ( - ensure_event_processor, WidgetEventProcessor, + ensure_event_processor, ) MessageHandlerFunc = Callable[ @@ -44,9 +46,8 @@ def __init__( if isinstance(content_types, str): if content_types != ContentType.ANY: filters.append(FilterObject(F.content_type == content_types)) - else: - if ContentType.ANY not in content_types: - filters.append(FilterObject(F.content_type.in_(content_types))) + elif ContentType.ANY not in content_types: + filters.append(FilterObject(F.content_type.in_(content_types))) if filter is not None: filters.append(FilterObject(filter)) self.filters = filters diff --git a/src/aiogram_dialog/widgets/input/combined.py b/src/aiogram_dialog/widgets/input/combined.py index 9b37f3a8..b0f8078c 100644 --- a/src/aiogram_dialog/widgets/input/combined.py +++ b/src/aiogram_dialog/widgets/input/combined.py @@ -1,11 +1,14 @@ -from typing import Any, Callable, Optional +from collections.abc import Callable +from typing import Any, Optional from aiogram.dispatcher.event.handler import FilterObject from aiogram.types import Message from aiogram_dialog.api.protocols import ( - DialogManager, DialogProtocol, + DialogManager, + DialogProtocol, ) + from .base import BaseInput diff --git a/src/aiogram_dialog/widgets/input/text.py b/src/aiogram_dialog/widgets/input/text.py index 0d1f0761..27029008 100644 --- a/src/aiogram_dialog/widgets/input/text.py +++ b/src/aiogram_dialog/widgets/input/text.py @@ -1,8 +1,14 @@ from __future__ import annotations from abc import abstractmethod +from collections.abc import Callable from typing import ( - Any, Callable, Generic, Optional, Protocol, TypeVar, Union, + Any, + Generic, + Optional, + Protocol, + TypeVar, + Union, ) from aiogram.dispatcher.event.handler import FilterObject @@ -11,9 +17,10 @@ from aiogram_dialog.api.protocols import DialogManager, DialogProtocol from aiogram_dialog.widgets.common import ManagedWidget from aiogram_dialog.widgets.widget_event import ( - ensure_event_processor, WidgetEventProcessor, + ensure_event_processor, ) + from .base import BaseInput T = TypeVar("T") diff --git a/src/aiogram_dialog/widgets/kbd/__init__.py b/src/aiogram_dialog/widgets/kbd/__init__.py index 07b67b98..c955d3b5 100644 --- a/src/aiogram_dialog/widgets/kbd/__init__.py +++ b/src/aiogram_dialog/widgets/kbd/__init__.py @@ -41,15 +41,20 @@ "ListGroup", "ManagedListGroup", "StubScroll", + "CopyText", ] from .base import Keyboard from .button import Button, SwitchInlineQuery, Url, WebApp from .calendar_kbd import ( - Calendar, CalendarConfig, CalendarScope, CalendarUserConfig, + Calendar, + CalendarConfig, + CalendarScope, + CalendarUserConfig, ManagedCalendar, ) from .checkbox import Checkbox, ManagedCheckbox +from .copy import CopyText from .counter import Counter, ManagedCounter from .group import Column, Group, Row from .list_group import ListGroup, ManagedListGroup diff --git a/src/aiogram_dialog/widgets/kbd/button.py b/src/aiogram_dialog/widgets/kbd/button.py index 8329e5d2..21f99dca 100644 --- a/src/aiogram_dialog/widgets/kbd/button.py +++ b/src/aiogram_dialog/widgets/kbd/button.py @@ -1,4 +1,5 @@ -from typing import Awaitable, Callable, Dict, List, Optional, Union +from collections.abc import Awaitable, Callable +from typing import Optional, Union from aiogram.types import CallbackQuery, InlineKeyboardButton, WebAppInfo @@ -7,9 +8,10 @@ from aiogram_dialog.widgets.common import WhenCondition from aiogram_dialog.widgets.text import Text from aiogram_dialog.widgets.widget_event import ( - ensure_event_processor, WidgetEventProcessor, + ensure_event_processor, ) + from .base import Keyboard OnClick = Callable[[CallbackQuery, "Button", DialogManager], Awaitable] @@ -38,7 +40,7 @@ async def _process_own_callback( async def _render_keyboard( self, - data: Dict, + data: dict, manager: DialogManager, ) -> RawKeyboard: return [ @@ -65,7 +67,7 @@ def __init__( async def _render_keyboard( self, - data: Dict, + data: dict, manager: DialogManager, ) -> RawKeyboard: return [ @@ -80,8 +82,8 @@ async def _render_keyboard( class WebApp(Url): async def _render_keyboard( - self, data: Dict, manager: DialogManager, - ) -> List[List[InlineKeyboardButton]]: + self, data: dict, manager: DialogManager, + ) -> list[list[InlineKeyboardButton]]: text = await self.text.render_text(data, manager) web_app_url = await self.url.render_text(data, manager) @@ -104,9 +106,9 @@ def __init__( async def _render_keyboard( self, - data: Dict, + data: dict, manager: DialogManager, - ) -> List[List[InlineKeyboardButton]]: + ) -> list[list[InlineKeyboardButton]]: return [ [ InlineKeyboardButton( diff --git a/src/aiogram_dialog/widgets/kbd/calendar_kbd.py b/src/aiogram_dialog/widgets/kbd/calendar_kbd.py index a886f347..89399e01 100644 --- a/src/aiogram_dialog/widgets/kbd/calendar_kbd.py +++ b/src/aiogram_dialog/widgets/kbd/calendar_kbd.py @@ -1,10 +1,16 @@ from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass from datetime import date, datetime, timedelta, timezone from enum import Enum from typing import ( - Any, Callable, Dict, List, Optional, Protocol, TypedDict, TypeVar, Union, + Any, + Optional, + Protocol, + TypedDict, + TypeVar, + Union, ) from aiogram.types import CallbackQuery, InlineKeyboardButton @@ -15,8 +21,10 @@ from aiogram_dialog.widgets.common import ManagedWidget, WhenCondition from aiogram_dialog.widgets.text import Format, Text from aiogram_dialog.widgets.widget_event import ( - ensure_event_processor, WidgetEventProcessor, + WidgetEventProcessor, + ensure_event_processor, ) + from .base import Keyboard EPOCH = date(1970, 1, 1) @@ -66,8 +74,7 @@ class CalendarScope(Enum): def raw_from_date(d: date) -> int: diff = d - EPOCH - raw_date = int(diff.total_seconds()) - return raw_date + return int(diff.total_seconds()) def date_from_raw(raw_date: int) -> date: @@ -161,9 +168,9 @@ async def render( self, config: CalendarConfig, offset: date, - data: Dict, + data: dict, manager: DialogManager, - ) -> List[List[InlineKeyboardButton]]: + ) -> list[list[InlineKeyboardButton]]: """ Render keyboard for current scope. @@ -203,7 +210,7 @@ async def _render_date_button( self, selected_date: date, today: date, - data: Dict, + data: dict, manager: DialogManager, ) -> InlineKeyboardButton: current_data = { @@ -228,9 +235,9 @@ async def _render_days( self, config: CalendarConfig, offset: date, - data: Dict, + data: dict, manager: DialogManager, - ) -> List[List[InlineKeyboardButton]]: + ) -> list[list[InlineKeyboardButton]]: keyboard = [] # align beginning start_date = offset.replace(day=1) # month beginning @@ -247,7 +254,7 @@ async def _render_days( end_date += timedelta(days=days_till_week_end) # add days today = get_today(config.timezone) - for offset in range(0, (end_date - start_date).days, 7): + for offset in range(0, (end_date - start_date).days, 7): # noqa: PLR1704 row = [] for row_offset in range(7): days_offset = timedelta(days=(offset + row_offset)) @@ -264,9 +271,9 @@ async def _render_days( async def _render_week_header( self, config: CalendarConfig, - data: Dict, + data: dict, manager: DialogManager, - ) -> List[InlineKeyboardButton]: + ) -> list[InlineKeyboardButton]: week_range = range(config.firstweekday, config.firstweekday + 7) header = [] for week_day in week_range: @@ -286,9 +293,9 @@ async def _render_pager( self, config: CalendarConfig, offset: date, - data: Dict, + data: dict, manager: DialogManager, - ) -> List[InlineKeyboardButton]: + ) -> list[InlineKeyboardButton]: curr_month = offset.month next_month = (curr_month % 12) + 1 prev_month = (curr_month - 2) % 12 + 1 @@ -347,9 +354,9 @@ async def _render_header( self, config: CalendarConfig, offset: date, - data: Dict, + data: dict, manager: DialogManager, - ) -> List[InlineKeyboardButton]: + ) -> list[InlineKeyboardButton]: data = { "date": offset, "data": data, @@ -363,9 +370,9 @@ async def render( self, config: CalendarConfig, offset: date, - data: Dict, + data: dict, manager: DialogManager, - ) -> List[List[InlineKeyboardButton]]: + ) -> list[list[InlineKeyboardButton]]: return [ await self._render_header(config, offset, data, manager), await self._render_week_header(config, data, manager), @@ -397,9 +404,9 @@ async def _render_pager( self, config: CalendarConfig, offset: date, - data: Dict, + data: dict, manager: DialogManager, - ) -> List[InlineKeyboardButton]: + ) -> list[InlineKeyboardButton]: curr_year = offset.year next_year = curr_year + 1 prev_year = curr_year - 1 @@ -467,7 +474,7 @@ async def _render_month_button( self, month: int, this_month: int, - data: Dict, + data: dict, offset: date, config: CalendarConfig, manager: DialogManager, @@ -498,9 +505,9 @@ async def _render_months( self, config: CalendarConfig, offset: date, - data: Dict, + data: dict, manager: DialogManager, - ) -> List[List[InlineKeyboardButton]]: + ) -> list[list[InlineKeyboardButton]]: keyboard = [] today = get_today(config.timezone) if offset.year == today.year: @@ -519,7 +526,7 @@ async def _render_months( async def _render_header( self, config, offset, data, manager, - ) -> List[InlineKeyboardButton]: + ) -> list[InlineKeyboardButton]: data = { "date": offset, "data": data, @@ -533,9 +540,9 @@ async def render( self, config: CalendarConfig, offset: date, - data: Dict, + data: dict, manager: DialogManager, - ) -> List[List[InlineKeyboardButton]]: + ) -> list[list[InlineKeyboardButton]]: return [ await self._render_header(config, offset, data, manager), *await self._render_months(config, offset, data, manager), @@ -562,9 +569,9 @@ async def _render_pager( self, config: CalendarConfig, offset: date, - data: Dict, + data: dict, manager: DialogManager, - ) -> List[InlineKeyboardButton]: + ) -> list[InlineKeyboardButton]: curr_year = offset.year next_year = curr_year + config.years_per_page prev_year = curr_year - config.years_per_page @@ -619,7 +626,7 @@ async def _render_year_button( self, year: int, this_year: int, - data: Dict, + data: dict, config: CalendarConfig, manager: DialogManager, ) -> InlineKeyboardButton: @@ -648,9 +655,9 @@ async def _render_years( self, config: CalendarConfig, offset: date, - data: Dict, + data: dict, manager: DialogManager, - ) -> List[List[InlineKeyboardButton]]: + ) -> list[list[InlineKeyboardButton]]: keyboard = [] this_year = get_today(config.timezone).year years_columns = config.years_columns @@ -670,9 +677,9 @@ async def render( self, config: CalendarConfig, offset: date, - data: Dict, + data: dict, manager: DialogManager, - ) -> List[List[InlineKeyboardButton]]: + ) -> list[list[InlineKeyboardButton]]: return [ *await self._render_years(config, offset, data, manager), await self._render_pager(config, offset, data, manager), @@ -718,7 +725,7 @@ def __init__( CALLBACK_SCOPE_YEARS: self._handle_scope_years, } - def _init_views(self) -> Dict[CalendarScope, CalendarScopeView]: + def _init_views(self) -> dict[CalendarScope, CalendarScopeView]: """ Calendar scopes view initializer. @@ -734,7 +741,7 @@ def _init_views(self) -> Dict[CalendarScope, CalendarScopeView]: async def _get_user_config( self, - data: Dict, + data: dict, manager: DialogManager, ) -> CalendarUserConfig: """ @@ -790,7 +797,7 @@ def set_scope(self, new_scope: CalendarScope, data = self.get_widget_data(manager, {}) data["current_scope"] = new_scope.value - def managed(self, manager: DialogManager) -> "ManagedCalendar": + def managed(self, manager: DialogManager) -> ManagedCalendar: return ManagedCalendar(self, manager) async def _handle_scope_months( diff --git a/src/aiogram_dialog/widgets/kbd/checkbox.py b/src/aiogram_dialog/widgets/kbd/checkbox.py index 634f8e15..973b040d 100644 --- a/src/aiogram_dialog/widgets/kbd/checkbox.py +++ b/src/aiogram_dialog/widgets/kbd/checkbox.py @@ -1,5 +1,6 @@ from abc import ABC, abstractmethod -from typing import Awaitable, Callable, Dict, Optional, Union +from collections.abc import Awaitable, Callable +from typing import Optional, Union from aiogram.types import CallbackQuery, InlineKeyboardButton @@ -9,9 +10,10 @@ from aiogram_dialog.widgets.common import ManagedWidget, WhenCondition from aiogram_dialog.widgets.text import Case, Text from aiogram_dialog.widgets.widget_event import ( - ensure_event_processor, WidgetEventProcessor, + ensure_event_processor, ) + from .base import Keyboard OnStateChanged = Callable[ @@ -41,7 +43,7 @@ def __init__( self.on_state_changed = ensure_event_processor(on_state_changed) async def _render_keyboard( - self, data: Dict, manager: DialogManager, + self, data: dict, manager: DialogManager, ) -> RawKeyboard: checked = int(self.is_checked(manager)) # store current checked status in callback data @@ -70,7 +72,7 @@ async def _process_item_callback( return True def _is_text_checked( - self, data: Dict, case: Case, manager: DialogManager, + self, data: dict, case: Case, manager: DialogManager, ) -> bool: del data # unused del case # unused diff --git a/src/aiogram_dialog/widgets/kbd/copy.py b/src/aiogram_dialog/widgets/kbd/copy.py new file mode 100644 index 00000000..c755180f --- /dev/null +++ b/src/aiogram_dialog/widgets/kbd/copy.py @@ -0,0 +1,37 @@ +from typing import Any + +from aiogram.types import CopyTextButton, InlineKeyboardButton + +from aiogram_dialog import DialogManager +from aiogram_dialog.api.internal import RawKeyboard +from aiogram_dialog.widgets.common import WhenCondition +from aiogram_dialog.widgets.kbd import Keyboard +from aiogram_dialog.widgets.text import Text + + +class CopyText(Keyboard): + def __init__( + self, + text: Text, + copy_text: Text, + when: WhenCondition = None, + ) -> None: + super().__init__(when=when) + self._text = text + self._copy_text = copy_text + + async def _render_keyboard( + self, + data: dict[str, Any], + manager: DialogManager, + ) -> RawKeyboard: + return [ + [ + InlineKeyboardButton( + text=await self._text.render_text(data, manager), + copy_text=CopyTextButton( + text=await self._copy_text.render_text(data, manager), + ), + ), + ], + ] diff --git a/src/aiogram_dialog/widgets/kbd/counter.py b/src/aiogram_dialog/widgets/kbd/counter.py index 35f914c7..abc243fd 100644 --- a/src/aiogram_dialog/widgets/kbd/counter.py +++ b/src/aiogram_dialog/widgets/kbd/counter.py @@ -1,5 +1,5 @@ from abc import abstractmethod -from typing import Dict, Optional, Protocol, Union +from typing import Optional, Protocol, Union from aiogram.types import CallbackQuery, InlineKeyboardButton @@ -10,8 +10,8 @@ from aiogram_dialog.widgets.kbd.base import Keyboard from aiogram_dialog.widgets.text import Const, Format, Text from aiogram_dialog.widgets.widget_event import ( - ensure_event_processor, WidgetEventProcessor, + ensure_event_processor, ) @@ -20,7 +20,7 @@ class OnCounterEvent(Protocol): async def __call__( self, event: ChatEvent, - counter: "ManagedCounter", # noqa: F841 + counter: "ManagedCounter", # noqa: F841, RUF100 dialog_manager: DialogManager, ): raise NotImplementedError @@ -104,7 +104,7 @@ async def set_value(self, manager: DialogManager, async def _render_keyboard( self, - data: Dict, + data: dict, manager: DialogManager, ) -> RawKeyboard: row = [] diff --git a/src/aiogram_dialog/widgets/kbd/group.py b/src/aiogram_dialog/widgets/kbd/group.py index c8d5ec23..a8757977 100644 --- a/src/aiogram_dialog/widgets/kbd/group.py +++ b/src/aiogram_dialog/widgets/kbd/group.py @@ -1,11 +1,13 @@ +from collections.abc import Iterable from itertools import chain -from typing import Dict, Iterable, List, Optional +from typing import Optional from aiogram.types import CallbackQuery, InlineKeyboardButton from aiogram_dialog.api.internal import ButtonVariant, RawKeyboard from aiogram_dialog.api.protocols import DialogManager, DialogProtocol from aiogram_dialog.widgets.common import WhenCondition + from .base import Keyboard @@ -14,7 +16,7 @@ def __init__( self, *buttons: Keyboard, id: Optional[str] = None, - width: int = None, + width: Optional[int] = None, when: WhenCondition = None, ): super().__init__(id=id, when=when) @@ -22,7 +24,7 @@ def __init__( self.width = width def find(self, widget_id): - widget = super(Group, self).find(widget_id) + widget = super().find(widget_id) if widget: return widget for btn in self.buttons: @@ -33,7 +35,7 @@ def find(self, widget_id): async def _render_keyboard( self, - data: Dict, + data: dict, manager: DialogManager, ) -> RawKeyboard: kbd: RawKeyboard = [] @@ -54,7 +56,7 @@ def _wrap_kbd( kbd: Iterable[InlineKeyboardButton], ) -> RawKeyboard: res: RawKeyboard = [] - row: List[ButtonVariant] = [] + row: list[ButtonVariant] = [] for b in kbd: row.append(b) if len(row) >= self.width: diff --git a/src/aiogram_dialog/widgets/kbd/list_group.py b/src/aiogram_dialog/widgets/kbd/list_group.py index d137d02d..05087112 100644 --- a/src/aiogram_dialog/widgets/kbd/list_group.py +++ b/src/aiogram_dialog/widgets/kbd/list_group.py @@ -1,15 +1,21 @@ -from typing import Any, Callable, Dict, Optional, Union +from collections.abc import Callable +from typing import Any, Optional, Union from aiogram.types import CallbackQuery from aiogram_dialog.api.internal import RawKeyboard, Widget from aiogram_dialog.api.protocols import ( - DialogManager, DialogProtocol, + DialogManager, + DialogProtocol, ) from aiogram_dialog.manager.sub_manager import SubManager from aiogram_dialog.widgets.common import ManagedWidget, WhenCondition +from aiogram_dialog.widgets.common.items import ( + ItemsGetterVariant, + get_items_getter, +) + from .base import Keyboard -from ..common.items import get_items_getter, ItemsGetterVariant ItemIdGetter = Callable[[Any], Union[str, int]] @@ -29,7 +35,7 @@ def __init__( self.items_getter = get_items_getter(items) async def _render_keyboard( - self, data: Dict, manager: DialogManager, + self, data: dict, manager: DialogManager, ) -> RawKeyboard: kbd: RawKeyboard = [] for pos, item in enumerate(self.items_getter(data)): @@ -40,7 +46,7 @@ async def _render_item( self, pos: int, item: Any, - data: Dict, + data: dict, manager: DialogManager, ) -> RawKeyboard: kbd: RawKeyboard = [] @@ -89,7 +95,7 @@ async def _process_item_callback( widget_id=self.widget_id, item_id=item_id, ) - for b in self.buttons: + for b in self.buttons: # noqa: RET503 if await b.process_callback(callback, dialog, sub_manager): return True diff --git a/src/aiogram_dialog/widgets/kbd/pager.py b/src/aiogram_dialog/widgets/kbd/pager.py index 7d0b7ffa..b5c016ec 100644 --- a/src/aiogram_dialog/widgets/kbd/pager.py +++ b/src/aiogram_dialog/widgets/kbd/pager.py @@ -1,6 +1,6 @@ from abc import ABC from enum import Enum -from typing import Dict, TypedDict, Union +from typing import TypedDict, Union from aiogram.types import CallbackQuery, InlineKeyboardButton @@ -8,6 +8,7 @@ from aiogram_dialog.api.protocols import DialogManager, DialogProtocol from aiogram_dialog.widgets.common import ManagedScroll, Scroll, WhenCondition from aiogram_dialog.widgets.text import Const, Format, Text + from .base import Keyboard @@ -20,7 +21,7 @@ class PageDirection(Enum): class PagerData(TypedDict): - data: Dict + data: dict current_page: int current_page1: int pages: int @@ -107,7 +108,7 @@ async def _get_target_page( return min(last_page, current_page) async def _prepare_data( - self, data: Dict, + self, data: dict, target_page: int, current_page: int, pages: int, ) -> PagerPageData: @@ -122,7 +123,7 @@ async def _prepare_data( } async def render_keyboard( - self, data: Dict, manager: DialogManager, + self, data: dict, manager: DialogManager, ) -> RawKeyboard: scroll = self._find_scroll(manager) pages = await scroll.get_page_count(data) @@ -229,7 +230,7 @@ def __init__( self.current_page_text = current_page_text async def _prepare_data( - self, data: Dict, + self, data: dict, current_page: int, pages: int, ) -> PagerData: return { @@ -240,7 +241,7 @@ async def _prepare_data( } async def _prepare_page_data( - self, data: Dict, target_page: int, + self, data: dict, target_page: int, ) -> PagerData: data = data.copy() data["target_page"] = target_page @@ -248,7 +249,7 @@ async def _prepare_page_data( return data async def render_keyboard( - self, data: Dict, manager: DialogManager, + self, data: dict, manager: DialogManager, ) -> RawKeyboard: scroll = self._find_scroll(manager) pages = await scroll.get_page_count(data) diff --git a/src/aiogram_dialog/widgets/kbd/request.py b/src/aiogram_dialog/widgets/kbd/request.py index 026086c6..5625652f 100644 --- a/src/aiogram_dialog/widgets/kbd/request.py +++ b/src/aiogram_dialog/widgets/kbd/request.py @@ -1,10 +1,12 @@ -from typing import Callable, Dict, Union +from collections.abc import Callable +from typing import Union from aiogram.types import KeyboardButton from aiogram_dialog.api.internal import RawKeyboard from aiogram_dialog.api.protocols import DialogManager from aiogram_dialog.widgets.text import Text + from .base import Keyboard @@ -19,7 +21,7 @@ def __init__( async def _render_keyboard( self, - data: Dict, + data: dict, manager: DialogManager, ) -> RawKeyboard: return [ @@ -43,7 +45,7 @@ def __init__( async def _render_keyboard( self, - data: Dict, + data: dict, manager: DialogManager, ) -> RawKeyboard: return [ diff --git a/src/aiogram_dialog/widgets/kbd/scrolling_group.py b/src/aiogram_dialog/widgets/kbd/scrolling_group.py index 4bc01ad7..acf6d168 100644 --- a/src/aiogram_dialog/widgets/kbd/scrolling_group.py +++ b/src/aiogram_dialog/widgets/kbd/scrolling_group.py @@ -1,12 +1,15 @@ -from typing import Dict, List, Optional +from typing import Optional from aiogram.types import CallbackQuery, InlineKeyboardButton from aiogram_dialog.api.internal import RawKeyboard from aiogram_dialog.api.protocols import DialogManager, DialogProtocol from aiogram_dialog.widgets.common import ( - BaseScroll, OnPageChangedVariants, WhenCondition, + BaseScroll, + OnPageChangedVariants, + WhenCondition, ) + from .base import Keyboard from .group import Group @@ -37,7 +40,7 @@ def _get_page_count( async def _render_contents( self, - data: Dict, + data: dict, manager: DialogManager, ) -> RawKeyboard: return await super()._render_keyboard(data, manager) @@ -84,8 +87,8 @@ async def _render_pager( async def _render_page( self, page: int, - keyboard: List[List[InlineKeyboardButton]], - ) -> List[List[InlineKeyboardButton]]: + keyboard: list[list[InlineKeyboardButton]], + ) -> list[list[InlineKeyboardButton]]: pages = self._get_page_count(keyboard) last_page = pages - 1 current_page = min(last_page, page) @@ -95,7 +98,7 @@ async def _render_page( async def _render_keyboard( self, - data: Dict, + data: dict, manager: DialogManager, ) -> RawKeyboard: keyboard = await self._render_contents(data, manager) @@ -119,6 +122,6 @@ async def _process_item_callback( await self.set_page(callback, int(data), manager) return True - async def get_page_count(self, data: Dict, manager: DialogManager) -> int: + async def get_page_count(self, data: dict, manager: DialogManager) -> int: keyboard = await self._render_contents(data, manager) return self._get_page_count(keyboard=keyboard) diff --git a/src/aiogram_dialog/widgets/kbd/select.py b/src/aiogram_dialog/widgets/kbd/select.py index b3959760..5d4571d9 100644 --- a/src/aiogram_dialog/widgets/kbd/select.py +++ b/src/aiogram_dialog/widgets/kbd/select.py @@ -1,10 +1,8 @@ from abc import ABC, abstractmethod +from collections.abc import Callable from typing import ( Any, - Callable, - Dict, Generic, - List, Optional, Protocol, TypeVar, @@ -17,13 +15,17 @@ from aiogram_dialog.api.internal import RawKeyboard from aiogram_dialog.api.protocols import DialogManager, DialogProtocol from aiogram_dialog.widgets.common import ManagedWidget, WhenCondition +from aiogram_dialog.widgets.common.items import ( + ItemsGetterVariant, + get_items_getter, +) from aiogram_dialog.widgets.text import Case, Text from aiogram_dialog.widgets.widget_event import ( - ensure_event_processor, WidgetEventProcessor, + ensure_event_processor, ) + from .base import Keyboard -from ..common.items import get_items_getter, ItemsGetterVariant T = TypeVar("T") ManagedT = TypeVar("ManagedT") @@ -36,7 +38,7 @@ class OnItemStateChanged(Protocol[ManagedT, T]): async def __call__( self, event: ChatEvent, - select: ManagedT, # noqa: F841 + select: ManagedT, dialog_manager: DialogManager, data: T, /, @@ -49,7 +51,7 @@ class OnItemClick(Protocol[ManagedT, T]): async def __call__( self, event: CallbackQuery, - select: ManagedT, # noqa: F841 + select: ManagedT, dialog_manager: DialogManager, data: T, /, @@ -79,7 +81,7 @@ def __init__( async def _render_keyboard( self, - data: Dict, + data: dict, manager: DialogManager, ) -> RawKeyboard: return [ @@ -90,7 +92,7 @@ async def _render_keyboard( ] async def _render_button( - self, pos: int, item: Any, target_item: Any, data: Dict, + self, pos: int, item: Any, target_item: Any, data: dict, manager: DialogManager, ) -> InlineKeyboardButton: """ @@ -141,7 +143,7 @@ def __init__( on_state_changed: Union[ OnItemStateChanged[ManagedT, T], WidgetEventProcessor, None, ] = None, - when: Union[str, Callable] = None, + when: Optional[Union[str, Callable]] = None, ): text = Case( {True: checked_text, False: unchecked_text}, @@ -167,7 +169,7 @@ async def _process_on_state_changed( @abstractmethod def _is_text_checked( - self, data: Dict, case: Case, manager: DialogManager, + self, data: dict, case: Case, manager: DialogManager, ) -> bool: raise NotImplementedError @@ -227,7 +229,7 @@ def __init__( OnItemStateChanged["ManagedRadio[T]", T], WidgetEventProcessor, None, ] = None, - when: Union[str, Callable] = None, + when: Optional[Union[str, Callable]] = None, ): super().__init__( @@ -272,7 +274,7 @@ def _preview_checked_id( return self.get_widget_data(manager, item_id) def _is_text_checked( - self, data: Dict, case: Case, manager: DialogManager, + self, data: dict, case: Case, manager: DialogManager, ) -> bool: item_id = self.item_id_getter(data["item"]) if manager.is_preview(): @@ -329,7 +331,7 @@ def __init__( OnItemStateChanged["ManagedMultiselect[T]", T], WidgetEventProcessor, None, ] = None, - when: Union[str, Callable] = None, + when: Optional[Union[str, Callable]] = None, ): super().__init__( checked_text=checked_text, @@ -344,13 +346,13 @@ def __init__( self.max_selected = max_selected def _is_text_checked( - self, data: Dict, case: Case, manager: DialogManager, + self, data: dict, case: Case, manager: DialogManager, ) -> bool: item_id = str(self.item_id_getter(data["item"])) if manager.is_preview(): return ( # just stupid way to make it differ in preview - ord(item_id[-1]) % 2 == 1 + ord(item_id[-1]) % 2 == 1 ) return self.is_checked(item_id, manager) @@ -360,10 +362,10 @@ def is_checked( data = self._get_checked(manager) return str(item_id) in data - def _get_checked(self, manager: DialogManager) -> List[str]: + def _get_checked(self, manager: DialogManager) -> list[str]: return self.get_widget_data(manager, []) - def get_checked(self, manager: DialogManager) -> List[T]: + def get_checked(self, manager: DialogManager) -> list[T]: return [self.type_factory(item) for item in self._get_checked(manager)] async def reset_checked( @@ -379,15 +381,14 @@ async def set_checked( manager: DialogManager, ) -> None: item_id_str = str(item_id) - data: List = self._get_checked(manager) + data: list = self._get_checked(manager) changed = False if item_id_str in data: - if not checked: - if len(data) > self.min_selected: - data.remove(item_id_str) - changed = True - else: - if checked: + if not checked and len(data) > self.min_selected: + data.remove(item_id_str) + changed = True + else: # noqa: PLR5501 + if checked: # noqa: SIM102 if self.max_selected == 0 or self.max_selected > len(data): data.append(item_id_str) changed = True @@ -415,7 +416,7 @@ def is_checked(self, item_id: T) -> bool: """Get if an item identified by ``item_id`` is checked.""" return self.widget.is_checked(item_id, self.manager) - def get_checked(self) -> List[T]: + def get_checked(self) -> list[T]: """Get a list of checked items ids.""" return self.widget.get_checked(self.manager) @@ -448,7 +449,7 @@ def __init__( OnItemStateChanged["ManagedToggle[T]", T], WidgetEventProcessor, None, ] = None, - when: Union[str, Callable] = None, + when: Optional[Union[str, Callable]] = None, ): super().__init__( checked_text=text, unchecked_text=text, @@ -459,7 +460,7 @@ def __init__( async def _render_keyboard( self, - data: Dict, + data: dict, manager: DialogManager, ) -> RawKeyboard: items_it = iter(self.items_getter(data)) diff --git a/src/aiogram_dialog/widgets/kbd/stub_scroll.py b/src/aiogram_dialog/widgets/kbd/stub_scroll.py index 3e71625e..7910f1ca 100644 --- a/src/aiogram_dialog/widgets/kbd/stub_scroll.py +++ b/src/aiogram_dialog/widgets/kbd/stub_scroll.py @@ -1,18 +1,23 @@ -from typing import Callable, Dict, Union +from collections.abc import Callable +from typing import Union from magic_filter import MagicFilter from aiogram_dialog.api.internal import RawKeyboard from aiogram_dialog.api.protocols import DialogManager +from aiogram_dialog.widgets.common.scroll import ( + BaseScroll, + OnPageChangedVariants, +) + from .base import Keyboard -from ..common.scroll import BaseScroll, OnPageChangedVariants -PagesGetter = Callable[[Dict, "StubScroll", DialogManager], int] +PagesGetter = Callable[[dict, "StubScroll", DialogManager], int] def new_pages_field(fieldname: str) -> PagesGetter: def pages_field( - data: Dict, widget: "StubScroll", manager: DialogManager, + data: dict, widget: "StubScroll", manager: DialogManager, ) -> int: return data.get(fieldname) @@ -21,7 +26,7 @@ def pages_field( def new_pages_magic(f: MagicFilter) -> PagesGetter: def pages_magic( - data: Dict, widget: "StubScroll", manager: DialogManager, + data: dict, widget: "StubScroll", manager: DialogManager, ) -> int: return f.resolve(data) @@ -30,7 +35,7 @@ def pages_magic( def new_pages_fixed(pages: int) -> PagesGetter: def pages_fixed( - data: Dict, widget: "StubScroll", manager: DialogManager, + data: dict, widget: "StubScroll", manager: DialogManager, ) -> int: return pages @@ -55,10 +60,10 @@ def __init__( async def _render_keyboard( self, - data: Dict, + data: dict, manager: DialogManager, ) -> RawKeyboard: return [[]] - async def get_page_count(self, data: Dict, manager: DialogManager) -> int: + async def get_page_count(self, data: dict, manager: DialogManager) -> int: return self._pages(data, self, manager) diff --git a/src/aiogram_dialog/widgets/link_preview/__init__.py b/src/aiogram_dialog/widgets/link_preview/__init__.py new file mode 100644 index 00000000..1d848cca --- /dev/null +++ b/src/aiogram_dialog/widgets/link_preview/__init__.py @@ -0,0 +1,3 @@ +__all__ = ["LinkPreviewBase", "LinkPreview"] + +from .base import LinkPreview, LinkPreviewBase diff --git a/src/aiogram_dialog/widgets/link_preview/base.py b/src/aiogram_dialog/widgets/link_preview/base.py new file mode 100644 index 00000000..299b711b --- /dev/null +++ b/src/aiogram_dialog/widgets/link_preview/base.py @@ -0,0 +1,64 @@ +from typing import Optional + +from aiogram.types import LinkPreviewOptions + +from aiogram_dialog import DialogManager +from aiogram_dialog.api.internal import LinkPreviewWidget, TextWidget +from aiogram_dialog.widgets.common import BaseWidget, Whenable, WhenCondition + + +class LinkPreviewBase(Whenable, BaseWidget, LinkPreviewWidget): + def __init__(self, when: WhenCondition = None): + super().__init__(when=when) + + async def render_link_preview( + self, data: dict, manager: DialogManager, + ) -> Optional[LinkPreviewOptions]: + if not self.is_(data, manager): + return None + return await self._render_link_preview(data, manager) + + async def _render_link_preview( + self, data: dict, manager: DialogManager, + ) -> Optional[LinkPreviewOptions]: + return None + + +class LinkPreview(LinkPreviewBase): + def __init__( + self, + url: Optional[TextWidget] = None, + is_disabled: bool = False, + prefer_small_media: bool = False, + prefer_large_media: bool = False, + show_above_text: bool = False, + when: WhenCondition = None, + ): + super().__init__(when=when) + self.url = url + self.is_disabled = is_disabled + self.prefer_small_media = prefer_small_media + self.prefer_large_media = prefer_large_media + self.show_above_text = show_above_text + + async def render_link_preview( + self, data: dict, manager: DialogManager, + ) -> Optional[LinkPreviewOptions]: + if not self.is_(data, manager): + return None + return await self._render_link_preview(data, manager) + + async def _render_link_preview( + self, data: dict, manager: DialogManager, + ) -> Optional[LinkPreviewOptions]: + return LinkPreviewOptions( + url=( + await self.url.render_text(data, manager) + if self.url + else None + ), + is_disabled=self.is_disabled, + prefer_small_media=self.prefer_small_media, + prefer_large_media=self.prefer_large_media, + show_above_text=self.show_above_text, + ) diff --git a/src/aiogram_dialog/widgets/markup/force_reply.py b/src/aiogram_dialog/widgets/markup/force_reply.py index 5673ca70..0c6be743 100644 --- a/src/aiogram_dialog/widgets/markup/force_reply.py +++ b/src/aiogram_dialog/widgets/markup/force_reply.py @@ -4,7 +4,9 @@ from aiogram_dialog import DialogManager from aiogram_dialog.api.internal.widgets import ( - MarkupFactory, MarkupVariant, RawKeyboard, + MarkupFactory, + MarkupVariant, + RawKeyboard, ) from aiogram_dialog.widgets.text import Text diff --git a/src/aiogram_dialog/widgets/markup/inline_keyboard.py b/src/aiogram_dialog/widgets/markup/inline_keyboard.py index 47352a8a..2e918ca6 100644 --- a/src/aiogram_dialog/widgets/markup/inline_keyboard.py +++ b/src/aiogram_dialog/widgets/markup/inline_keyboard.py @@ -2,7 +2,9 @@ from aiogram_dialog import DialogManager from aiogram_dialog.api.internal.widgets import ( - MarkupFactory, MarkupVariant, RawKeyboard, + MarkupFactory, + MarkupVariant, + RawKeyboard, ) from aiogram_dialog.utils import add_intent_id diff --git a/src/aiogram_dialog/widgets/markup/reply_keyboard.py b/src/aiogram_dialog/widgets/markup/reply_keyboard.py index a5dca48a..74a8891f 100644 --- a/src/aiogram_dialog/widgets/markup/reply_keyboard.py +++ b/src/aiogram_dialog/widgets/markup/reply_keyboard.py @@ -4,7 +4,9 @@ from aiogram_dialog import DialogManager from aiogram_dialog.api.internal.widgets import ( - MarkupFactory, MarkupVariant, RawKeyboard, + MarkupFactory, + MarkupVariant, + RawKeyboard, ) from aiogram_dialog.utils import add_intent_id, transform_to_reply_keyboard from aiogram_dialog.widgets.text import Text diff --git a/src/aiogram_dialog/widgets/media/dynamic.py b/src/aiogram_dialog/widgets/media/dynamic.py index c4920739..3a0523f2 100644 --- a/src/aiogram_dialog/widgets/media/dynamic.py +++ b/src/aiogram_dialog/widgets/media/dynamic.py @@ -5,8 +5,9 @@ https://github.com/SamWarden/aiogram_dialog_extras """ +from collections.abc import Callable from operator import itemgetter -from typing import Callable, Optional, Union +from typing import Optional, Union from aiogram_dialog import DialogManager from aiogram_dialog.api.entities import MediaAttachment diff --git a/src/aiogram_dialog/widgets/media/scroll.py b/src/aiogram_dialog/widgets/media/scroll.py index 9157d693..5c5a5b93 100644 --- a/src/aiogram_dialog/widgets/media/scroll.py +++ b/src/aiogram_dialog/widgets/media/scroll.py @@ -1,12 +1,18 @@ -from typing import Dict, Optional +from typing import Optional from aiogram_dialog.api.entities import MediaAttachment from aiogram_dialog.api.protocols import DialogManager from aiogram_dialog.widgets.common import ( - BaseScroll, OnPageChangedVariants, WhenCondition, + BaseScroll, + OnPageChangedVariants, + WhenCondition, ) +from aiogram_dialog.widgets.common.items import ( + ItemsGetterVariant, + get_items_getter, +) + from .base import Media -from ..common.items import get_items_getter, ItemsGetterVariant class MediaScroll(Media, BaseScroll): @@ -44,6 +50,6 @@ async def _render_media( manager, ) - async def get_page_count(self, data: Dict, manager: DialogManager) -> int: + async def get_page_count(self, data: dict, manager: DialogManager) -> int: items = self.items_getter(data) return len(items) diff --git a/src/aiogram_dialog/widgets/media/static.py b/src/aiogram_dialog/widgets/media/static.py index 43f1af7d..239fb48e 100644 --- a/src/aiogram_dialog/widgets/media/static.py +++ b/src/aiogram_dialog/widgets/media/static.py @@ -7,6 +7,7 @@ from aiogram_dialog.api.protocols import DialogManager from aiogram_dialog.widgets.common import WhenCondition from aiogram_dialog.widgets.text import Const, Text + from .base import Media @@ -18,7 +19,7 @@ def __init__( url: Union[Text, str, None] = None, type: ContentType = ContentType.PHOTO, use_pipe: bool = False, - media_params: dict = None, + media_params: Optional[dict] = None, when: WhenCondition = None, ): super().__init__(when=when) diff --git a/src/aiogram_dialog/widgets/text/base.py b/src/aiogram_dialog/widgets/text/base.py index 4309c406..f5e80877 100644 --- a/src/aiogram_dialog/widgets/text/base.py +++ b/src/aiogram_dialog/widgets/text/base.py @@ -1,10 +1,13 @@ from abc import abstractmethod -from typing import Dict, Optional, Union +from typing import Optional, Union from aiogram_dialog.api.internal import TextWidget from aiogram_dialog.api.protocols import DialogManager from aiogram_dialog.widgets.common import ( - BaseWidget, true_condition, Whenable, WhenCondition, + BaseWidget, + Whenable, + WhenCondition, + true_condition, ) @@ -13,7 +16,7 @@ def __init__(self, when: WhenCondition = None): super().__init__(when=when) async def render_text( - self, data: Dict, manager: DialogManager, + self, data: dict, manager: DialogManager, ) -> str: """ Create text. @@ -69,7 +72,7 @@ def __init__(self, text: str, when: WhenCondition = None): self.text = text async def _render_text( - self, data: Dict, manager: DialogManager, + self, data: dict, manager: DialogManager, ) -> str: return self.text @@ -81,7 +84,7 @@ def __init__(self, *texts: Text, sep="\n", when: WhenCondition = None): self.sep = sep async def _render_text( - self, data: Dict, manager: DialogManager, + self, data: dict, manager: DialogManager, ) -> str: texts = [await t.render_text(data, manager) for t in self.texts] return self.sep.join(filter(None, texts)) @@ -123,7 +126,7 @@ def __init__(self, *texts: Text): self.texts = texts async def _render_text( - self, data: Dict, manager: DialogManager, + self, data: dict, manager: DialogManager, ) -> str: for text in self.texts: res = await text.render_text(data, manager) diff --git a/src/aiogram_dialog/widgets/text/format.py b/src/aiogram_dialog/widgets/text/format.py index d6caedbe..035e1d17 100644 --- a/src/aiogram_dialog/widgets/text/format.py +++ b/src/aiogram_dialog/widgets/text/format.py @@ -1,7 +1,6 @@ -from typing import Dict - from aiogram_dialog.api.protocols import DialogManager from aiogram_dialog.widgets.common import WhenCondition + from .base import Text @@ -34,7 +33,7 @@ def __init__(self, text: str, when: WhenCondition = None): self.text = text async def _render_text( - self, data: Dict, manager: DialogManager, + self, data: dict, manager: DialogManager, ) -> str: if manager.is_preview(): return self.text.format_map(_FormatDataStub(data=data)) diff --git a/src/aiogram_dialog/widgets/text/jinja.py b/src/aiogram_dialog/widgets/text/jinja.py index 65a2348f..3a9edc47 100644 --- a/src/aiogram_dialog/widgets/text/jinja.py +++ b/src/aiogram_dialog/widgets/text/jinja.py @@ -1,12 +1,8 @@ import warnings +from collections.abc import Callable, Iterable, Mapping from typing import ( Any, - Callable, - Dict, - Iterable, - Mapping, Optional, - Tuple, Union, ) @@ -15,12 +11,13 @@ from aiogram_dialog.api.protocols import DialogManager from aiogram_dialog.widgets.common import WhenCondition + from .base import Text JINJA_ENV_FIELD = "DialogsJinjaEnvironment" Filter = Callable[..., str] -Filters = Union[Iterable[Tuple[str, Filter]], Mapping[str, Filter]] +Filters = Union[Iterable[tuple[str, Filter]], Mapping[str, Filter]] class Jinja(Text): @@ -29,7 +26,7 @@ def __init__(self, text: str, when: WhenCondition = None): self.template_text = text async def _render_text( - self, data: Dict, manager: DialogManager, + self, data: dict, manager: DialogManager, ) -> str: if JINJA_ENV_FIELD in manager.middleware_data: env = manager.middleware_data[JINJA_ENV_FIELD] @@ -58,7 +55,7 @@ def _create_env( kwargs.setdefault("trim_blocks", True) if "loader" not in kwargs: kwargs["loader"] = StubLoader() - env = Environment(*args, **kwargs) + env = Environment(*args, **kwargs) # noqa: S701 if filters is not None: env.filters.update(filters) return env diff --git a/src/aiogram_dialog/widgets/text/list.py b/src/aiogram_dialog/widgets/text/list.py index b64a52d9..f3930f54 100644 --- a/src/aiogram_dialog/widgets/text/list.py +++ b/src/aiogram_dialog/widgets/text/list.py @@ -1,11 +1,18 @@ -from typing import Any, Dict, Optional, Sequence +from collections.abc import Sequence +from typing import Any, Optional from aiogram_dialog.api.protocols import DialogManager from aiogram_dialog.widgets.common import ( - BaseScroll, OnPageChangedVariants, WhenCondition, + BaseScroll, + OnPageChangedVariants, + WhenCondition, ) +from aiogram_dialog.widgets.common.items import ( + ItemsGetterVariant, + get_items_getter, +) + from .base import Text -from ..common.items import get_items_getter, ItemsGetterVariant class List(Text, BaseScroll): @@ -27,7 +34,7 @@ def __init__( self.page_size = page_size async def _render_text( - self, data: Dict, manager: DialogManager, + self, data: dict, manager: DialogManager, ) -> str: items = self.items_getter(data) pages = self._get_page_count(items) @@ -57,7 +64,7 @@ async def _render_text( ] return self.sep.join(filter(None, texts)) - async def get_page_count(self, data: Dict, manager: DialogManager) -> int: + async def get_page_count(self, data: dict, manager: DialogManager) -> int: items = self.items_getter(data) return self._get_page_count(items) diff --git a/src/aiogram_dialog/widgets/text/multi.py b/src/aiogram_dialog/widgets/text/multi.py index 221e6be4..d7facd0a 100644 --- a/src/aiogram_dialog/widgets/text/multi.py +++ b/src/aiogram_dialog/widgets/text/multi.py @@ -1,17 +1,19 @@ -from typing import Any, Callable, Dict, Hashable, Optional, Union +from collections.abc import Callable, Hashable +from typing import Any, Optional, Union from magic_filter import MagicFilter from aiogram_dialog.api.protocols import DialogManager from aiogram_dialog.widgets.common import WhenCondition + from .base import Text -Selector = Callable[[Dict, "Case", DialogManager], Hashable] +Selector = Callable[[dict, "Case", DialogManager], Hashable] def new_case_field(fieldname: str) -> Selector: def case_field( - data: Dict, widget: "Case", manager: DialogManager, + data: dict, widget: "Case", manager: DialogManager, ) -> Hashable: return data.get(fieldname) @@ -20,7 +22,7 @@ def case_field( def new_magic_selector(f: MagicFilter) -> Selector: def when_magic( - data: Dict, widget: "Case", manager: DialogManager, + data: dict, widget: "Case", manager: DialogManager, ) -> bool: return f.resolve(data) @@ -30,7 +32,7 @@ def when_magic( class Case(Text): def __init__( self, - texts: Dict[Any, Text], + texts: dict[Any, Text], selector: Union[str, Selector, MagicFilter], when: WhenCondition = None, ): diff --git a/src/aiogram_dialog/widgets/text/progress.py b/src/aiogram_dialog/widgets/text/progress.py index 1b5245f7..6f492f26 100644 --- a/src/aiogram_dialog/widgets/text/progress.py +++ b/src/aiogram_dialog/widgets/text/progress.py @@ -1,7 +1,6 @@ -from typing import Dict - from aiogram_dialog.api.protocols import DialogManager from aiogram_dialog.widgets.common import WhenCondition + from .base import Text @@ -21,7 +20,7 @@ def __init__( self.empty = empty async def _render_text( - self, data: Dict, manager: DialogManager, + self, data: dict, manager: DialogManager, ) -> str: if manager.is_preview(): percent = 15 diff --git a/src/aiogram_dialog/widgets/text/scrolling_text.py b/src/aiogram_dialog/widgets/text/scrolling_text.py index df5376a9..488b9b29 100644 --- a/src/aiogram_dialog/widgets/text/scrolling_text.py +++ b/src/aiogram_dialog/widgets/text/scrolling_text.py @@ -1,9 +1,10 @@ -from typing import Dict - from aiogram_dialog.api.protocols import DialogManager from aiogram_dialog.widgets.common import ( - BaseScroll, OnPageChangedVariants, WhenCondition, + BaseScroll, + OnPageChangedVariants, + WhenCondition, ) + from .base import Text @@ -29,7 +30,7 @@ def _get_page_count( async def _render_contents( self, - data: Dict, + data: dict, manager: DialogManager, ) -> str: return await self.text.render_text(data, manager) @@ -44,6 +45,6 @@ async def _render_text(self, data, manager: DialogManager) -> str: return text[page_offset: page_offset + self.page_size] - async def get_page_count(self, data: Dict, manager: DialogManager) -> int: + async def get_page_count(self, data: dict, manager: DialogManager) -> int: text = await self._render_contents(data, manager) return self._get_page_count(text) diff --git a/src/aiogram_dialog/widgets/utils.py b/src/aiogram_dialog/widgets/utils.py index 3deb8807..977d4984 100644 --- a/src/aiogram_dialog/widgets/utils.py +++ b/src/aiogram_dialog/widgets/utils.py @@ -1,22 +1,27 @@ -from typing import Callable, Dict, List, Sequence, Tuple, Union +from collections.abc import Callable, Sequence +from typing import Optional, Union from aiogram_dialog.api.exceptions import InvalidWidgetType -from aiogram_dialog.api.internal import DataGetter +from aiogram_dialog.api.internal import DataGetter, LinkPreviewWidget + from .data.data_context import CompositeGetter, StaticGetter from .input import BaseInput, CombinedInput, MessageHandlerFunc, MessageInput from .kbd import Group, Keyboard +from .link_preview import LinkPreviewBase from .media import Media from .text import Format, Multi, Text from .widget_event import WidgetEventProcessor -WidgetSrc = Union[str, Text, Keyboard, MessageHandlerFunc, Media, BaseInput] +WidgetSrc = Union[ + str, Text, Keyboard, MessageHandlerFunc, Media, BaseInput, LinkPreviewBase, +] -SingleGetterBase = Union[DataGetter, Dict] +SingleGetterBase = Union[DataGetter, dict] GetterVariant = Union[ None, SingleGetterBase, - List[SingleGetterBase], - Tuple[SingleGetterBase, ...], + list[SingleGetterBase], + tuple[SingleGetterBase, ...], ] @@ -69,13 +74,32 @@ def ensure_media(widget: Union[Media, Sequence[Media]]) -> Media: return Media() +def ensure_link_preview( + widget: Union[LinkPreviewWidget, Sequence[LinkPreviewWidget]], +) -> Optional[LinkPreviewWidget]: + if isinstance(widget, LinkPreviewWidget): + return widget + if len(widget) > 1: + raise ValueError("Only one link preview widget is supported") + if len(widget) == 1: + return widget[0] + return None + + def ensure_widgets( widgets: Sequence[WidgetSrc], -) -> Tuple[Text, Keyboard, Union[BaseInput, None], Media]: +) -> tuple[ + Text, + Keyboard, + Optional[BaseInput], + Media, + Optional[LinkPreviewWidget], +]: texts = [] keyboards = [] inputs = [] media = [] + link_preview = [] for w in widgets: if isinstance(w, (str, Text)): @@ -86,6 +110,8 @@ def ensure_widgets( inputs.append(ensure_input(w)) elif isinstance(w, Media): media.append(ensure_media(w)) + elif isinstance(w, LinkPreviewBase): + link_preview.append(ensure_link_preview(w)) else: raise InvalidWidgetType( f"Cannot add widget of type {type(w)}. " @@ -97,6 +123,7 @@ def ensure_widgets( ensure_keyboard(keyboards), ensure_input(inputs), ensure_media(media), + ensure_link_preview(link_preview), ) diff --git a/src/aiogram_dialog/widgets/widget_event.py b/src/aiogram_dialog/widgets/widget_event.py index 42823b05..f3c31da8 100644 --- a/src/aiogram_dialog/widgets/widget_event.py +++ b/src/aiogram_dialog/widgets/widget_event.py @@ -1,5 +1,6 @@ from abc import abstractmethod -from typing import Any, Callable, Union +from collections.abc import Callable +from typing import Any, Union from aiogram_dialog.api.entities import ChatEvent from aiogram_dialog.api.protocols import DialogManager diff --git a/src/aiogram_dialog/window.py b/src/aiogram_dialog/window.py index af02a0d2..035835dd 100644 --- a/src/aiogram_dialog/window.py +++ b/src/aiogram_dialog/window.py @@ -1,13 +1,14 @@ +import warnings from logging import getLogger -from typing import Any, cast, Dict, List, Optional +from typing import Any, Optional, cast from aiogram.fsm.state import State from aiogram.types import ( + UNSET_PARSE_MODE, CallbackQuery, + LinkPreviewOptions, Message, - UNSET_PARSE_MODE, ) -from aiogram.types.base import UNSET_DISABLE_WEB_PAGE_PREVIEW from aiogram_dialog.api.entities import ( EVENT_CONTEXT_KEY, @@ -17,18 +18,20 @@ NewMessage, ) from aiogram_dialog.api.internal import Widget, WindowProtocol + from .api.entities import Data from .api.internal.widgets import MarkupFactory from .api.protocols import DialogManager, DialogProtocol from .dialog import OnResultEvent from .widgets.data import PreviewAwareGetter from .widgets.kbd import Keyboard +from .widgets.link_preview import LinkPreview from .widgets.markup.inline_keyboard import InlineKeyboardFactory from .widgets.utils import ( - ensure_data_getter, - ensure_widgets, GetterVariant, WidgetSrc, + ensure_data_getter, + ensure_widgets, ) logger = getLogger(__name__) @@ -45,8 +48,8 @@ def __init__( on_process_result: Optional[OnResultEvent] = None, markup_factory: MarkupFactory = _DEFAULT_MARKUP_FACTORY, parse_mode: Optional[str] = UNSET_PARSE_MODE, - disable_web_page_preview: Optional[bool] = UNSET_DISABLE_WEB_PAGE_PREVIEW, # noqa: E501 - preview_add_transitions: Optional[List[Keyboard]] = None, + disable_web_page_preview: Optional[bool] = None, + preview_add_transitions: Optional[list[Keyboard]] = None, preview_data: GetterVariant = None, ): ( @@ -54,6 +57,7 @@ def __init__( self.keyboard, self.on_message, self.media, + self.link_preview, ) = ensure_widgets(widgets) self.getter = PreviewAwareGetter( ensure_data_getter(getter), @@ -63,32 +67,52 @@ def __init__( self.on_process_result = on_process_result self.markup_factory = markup_factory self.parse_mode = parse_mode - self.disable_web_page_preview = disable_web_page_preview self.preview_add_transitions = preview_add_transitions + if disable_web_page_preview is not None: + if self.link_preview: + raise ValueError( + "Cannot use LinkPreview widget " + "together with disable_web_page_preview", + ) + warnings.warn( + "disable_web_page_preview is deprecated, " + "use `LinkPreview` widget instead", + category=DeprecationWarning, + stacklevel=2, + ) + self.link_preview = LinkPreview(is_disabled=True) async def render_text( - self, data: Dict, manager: DialogManager, + self, data: dict, manager: DialogManager, ) -> str: return await self.text.render_text(data, manager) async def render_media( - self, data: Dict, manager: DialogManager, + self, data: dict, manager: DialogManager, ) -> Optional[MediaAttachment]: if self.media: return await self.media.render_media(data, manager) + return None async def render_kbd( - self, data: Dict, manager: DialogManager, + self, data: dict, manager: DialogManager, ) -> MarkupVariant: keyboard = await self.keyboard.render_keyboard(data, manager) return await self.markup_factory.render_markup( data, manager, keyboard, ) + async def render_link_preview( + self, data: dict, manager: DialogManager, + ) -> Optional[LinkPreviewOptions]: + if self.link_preview: + return await self.link_preview.render_link_preview(data, manager) + return None + async def load_data( self, dialog: "DialogProtocol", manager: DialogManager, - ) -> Dict: + ) -> dict: data = await dialog.load_data(manager) data.update(await self.getter(**manager.middleware_data)) return data @@ -127,7 +151,7 @@ async def render( chat = manager.middleware_data["event_chat"] try: current_data = await self.load_data(dialog, manager) - except Exception: # noqa: B902 + except Exception: logger.error("Cannot get window data for state %s", self.state) raise try: @@ -141,10 +165,12 @@ async def render( text=await self.render_text(current_data, manager), reply_markup=await self.render_kbd(current_data, manager), parse_mode=self.parse_mode, - disable_web_page_preview=self.disable_web_page_preview, media=await self.render_media(current_data, manager), + link_preview_options=await self.render_link_preview( + current_data, manager, + ), ) - except Exception: # noqa: B902 + except Exception: logger.error("Cannot render window for state %s", self.state) raise @@ -153,9 +179,8 @@ def get_state(self) -> State: def find(self, widget_id) -> Optional[Widget]: for root in (self.text, self.keyboard, self.on_message, self.media): - if root: - if found := root.find(widget_id): - return found + if root and (found := root.find(widget_id)): + return found return None def __repr__(self) -> str: diff --git a/tests/test_click.py b/tests/test_click.py index 3233dba9..54ab6f29 100644 --- a/tests/test_click.py +++ b/tests/test_click.py @@ -1,4 +1,4 @@ -from typing import Any, Dict +from typing import Any from unittest.mock import Mock import pytest @@ -8,7 +8,11 @@ from aiogram.types import Message from aiogram_dialog import ( - Dialog, DialogManager, setup_dialogs, StartMode, Window, + Dialog, + DialogManager, + StartMode, + Window, + setup_dialogs, ) from aiogram_dialog.test_tools import BotClient, MockMessageManager from aiogram_dialog.test_tools.keyboard import InlineButtonTextLocator @@ -31,7 +35,7 @@ async def on_finish(event, button, manager: DialogManager) -> None: await manager.done() -async def second_getter(user_getter, **kwargs) -> Dict[str, Any]: +async def second_getter(user_getter, **kwargs) -> dict[str, Any]: return { "user": user_getter(), } diff --git a/tests/test_create.py b/tests/test_create.py index 7d6764f9..5a69a4e1 100644 --- a/tests/test_create.py +++ b/tests/test_create.py @@ -1,7 +1,7 @@ from aiogram import Dispatcher from aiogram.fsm.state import State, StatesGroup -from aiogram_dialog import Dialog, setup_dialogs, Window +from aiogram_dialog import Dialog, Window, setup_dialogs from aiogram_dialog.widgets.text import Format diff --git a/tests/test_events.py b/tests/test_events.py index 9a9c5017..b2d17b25 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -7,7 +7,11 @@ from aiogram.types import ChatMemberMember, ChatMemberOwner from aiogram_dialog import ( - Dialog, DialogManager, setup_dialogs, StartMode, Window, + Dialog, + DialogManager, + StartMode, + Window, + setup_dialogs, ) from aiogram_dialog.test_tools import BotClient, MockMessageManager from aiogram_dialog.test_tools.memory_storage import JsonMemoryStorage @@ -28,12 +32,12 @@ async def start(event: Any, dialog_manager: DialogManager): await dialog_manager.start(MainSG.start, mode=StartMode.RESET_STACK) -@pytest.fixture() +@pytest.fixture def message_manager(): return MockMessageManager() -@pytest.fixture() +@pytest.fixture def dp(message_manager): dp = Dispatcher(storage=JsonMemoryStorage()) dp.include_router(Dialog(window)) @@ -41,7 +45,7 @@ def dp(message_manager): return dp -@pytest.fixture() +@pytest.fixture def client(dp): return BotClient(dp) diff --git a/tests/test_group.py b/tests/test_group.py index daba4b59..5c8d0651 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -7,9 +7,13 @@ from aiogram.fsm.state import State, StatesGroup from aiogram_dialog import ( - Dialog, DialogManager, setup_dialogs, StartMode, Window, + Dialog, + DialogManager, + StartMode, + Window, + setup_dialogs, ) -from aiogram_dialog.api.entities import AccessSettings, GROUP_STACK_ID +from aiogram_dialog.api.entities import GROUP_STACK_ID, AccessSettings from aiogram_dialog.test_tools import BotClient, MockMessageManager from aiogram_dialog.test_tools.keyboard import InlineButtonTextLocator from aiogram_dialog.test_tools.memory_storage import JsonMemoryStorage @@ -43,12 +47,12 @@ async def add_shared(event: Any, dialog_manager: DialogManager): )) -@pytest.fixture() +@pytest.fixture def message_manager(): return MockMessageManager() -@pytest.fixture() +@pytest.fixture def dp(message_manager): dp = Dispatcher(storage=JsonMemoryStorage()) dp.include_router(Dialog(window)) @@ -56,12 +60,12 @@ def dp(message_manager): return dp -@pytest.fixture() +@pytest.fixture def client(dp): return BotClient(dp, chat_id=-1, user_id=1, chat_type="group") -@pytest.fixture() +@pytest.fixture def second_client(dp): return BotClient(dp, chat_id=-1, user_id=2, chat_type="group") @@ -159,7 +163,8 @@ async def test_same_user(dp, client, message_manager): async def test_shared_stack(dp, client, second_client, message_manager): dp.message.register(start_shared, CommandStart()) await client.send("/start") - await asyncio.sleep(0.01) # synchronization workaround, fixme + await asyncio.sleep(0.02) # synchronization workaround, fixme + first_message = message_manager.one_message() assert first_message.text == "stub" message_manager.reset_history() diff --git a/tests/test_nested_transitions.py b/tests/test_nested_transitions.py index 53c77c22..e8d96180 100644 --- a/tests/test_nested_transitions.py +++ b/tests/test_nested_transitions.py @@ -5,7 +5,11 @@ from aiogram.types import Message from aiogram_dialog import ( - Dialog, DialogManager, setup_dialogs, StartMode, Window, + Dialog, + DialogManager, + StartMode, + Window, + setup_dialogs, ) from aiogram_dialog.test_tools import BotClient, MockMessageManager from aiogram_dialog.test_tools.keyboard import InlineButtonTextLocator @@ -42,17 +46,17 @@ async def on_process_result_sub(_, __, dialog_manager: DialogManager): await dialog_manager.done() -@pytest.fixture() +@pytest.fixture def message_manager() -> MockMessageManager: return MockMessageManager() -@pytest.fixture() +@pytest.fixture def client(dp) -> BotClient: return BotClient(dp) -@pytest.fixture() +@pytest.fixture def dp(message_manager: MockMessageManager): dp = Dispatcher(storage=JsonMemoryStorage()) dp.message.register(start, CommandStart()) diff --git a/tests/test_transitions.py b/tests/test_transitions.py index 64ef53bc..9334dedd 100644 --- a/tests/test_transitions.py +++ b/tests/test_transitions.py @@ -5,7 +5,11 @@ from aiogram.types import Message from aiogram_dialog import ( - Dialog, DialogManager, setup_dialogs, StartMode, Window, + Dialog, + DialogManager, + StartMode, + Window, + setup_dialogs, ) from aiogram_dialog.test_tools import BotClient, MockMessageManager from aiogram_dialog.test_tools.keyboard import InlineButtonTextLocator @@ -27,17 +31,17 @@ async def start(message: Message, dialog_manager: DialogManager): await dialog_manager.start(MainSG.start, mode=StartMode.RESET_STACK) -@pytest.fixture() +@pytest.fixture def message_manager() -> MockMessageManager: return MockMessageManager() -@pytest.fixture() +@pytest.fixture def client(dp) -> BotClient: return BotClient(dp) -@pytest.fixture() +@pytest.fixture def dp(message_manager: MockMessageManager): dp = Dispatcher(storage=JsonMemoryStorage()) dp.message.register(start, CommandStart()) diff --git a/tests/widgets/conftest.py b/tests/widgets/conftest.py index 70b5cc02..16f2912f 100644 --- a/tests/widgets/conftest.py +++ b/tests/widgets/conftest.py @@ -7,7 +7,7 @@ from aiogram_dialog.api.entities import Context -@pytest.fixture() +@pytest.fixture def mock_manager() -> DialogManager: manager = MagicMock() context = Context( diff --git a/tests/widgets/kbd/test_group.py b/tests/widgets/kbd/test_group.py index 7d7b7c60..5dd094bf 100644 --- a/tests/widgets/kbd/test_group.py +++ b/tests/widgets/kbd/test_group.py @@ -7,7 +7,11 @@ from aiogram.types import Message from aiogram_dialog import ( - Dialog, DialogManager, setup_dialogs, StartMode, Window, + Dialog, + DialogManager, + StartMode, + Window, + setup_dialogs, ) from aiogram_dialog.test_tools import BotClient, MockMessageManager from aiogram_dialog.test_tools.keyboard import InlineButtonTextLocator diff --git a/tests/widgets/media/test_media_message.py b/tests/widgets/media/test_media_message.py index 6d9087be..174ade26 100644 --- a/tests/widgets/media/test_media_message.py +++ b/tests/widgets/media/test_media_message.py @@ -8,9 +8,9 @@ from aiogram_dialog import ( Dialog, DialogManager, - setup_dialogs, StartMode, Window, + setup_dialogs, ) from aiogram_dialog.test_tools import BotClient, MockMessageManager from aiogram_dialog.widgets.media.static import StaticMedia diff --git a/tests/widgets/text/test_jinja.py b/tests/widgets/text/test_jinja.py index 5a94f200..47c1cb7e 100644 --- a/tests/widgets/text/test_jinja.py +++ b/tests/widgets/text/test_jinja.py @@ -4,7 +4,7 @@ from aiogram_dialog.widgets.text import Jinja -@pytest.fixture() +@pytest.fixture def mock_manager(mock_manager) -> DialogManager: mock_manager.middleware_data = {} return mock_manager