diff --git a/Makefile b/Makefile index e1375ec3..3b2f3dca 100644 --- a/Makefile +++ b/Makefile @@ -104,7 +104,7 @@ test: .PHONY: test-coverage test-coverage: mkdir -p reports/tests/ - $(py) pytest --cov=aiogram --cov-config .coveragerc --html=reports/tests/index.html tests/ + $(py) pytest --cov=aiogram --cov-config .coveragerc --html=reports/tests/index.html -p no:warnings tests/ $(py) coverage html -d reports/coverage # ================================================================================================= diff --git a/aiogram/dispatcher/router.py b/aiogram/dispatcher/router.py index d8c73f4c..716bfd98 100644 --- a/aiogram/dispatcher/router.py +++ b/aiogram/dispatcher/router.py @@ -1,8 +1,9 @@ from __future__ import annotations -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Union from ..api.types import Chat, Update, User +from ..utils.imports import import_module from .event.observer import EventObserver, SkipHandler, TelegramEventObserver from .filters import BUILTIN_FILTERS @@ -76,7 +77,13 @@ class Router: self._parent_router = router - def include_router(self, router: Router) -> Router: + def include_router(self, router: Union[Router, str]) -> Router: + if isinstance(router, str): + router = import_module(router) + if not isinstance(router, Router): + raise ValueError( + f"router should be instance of Router not {type(router).__class__.__name__}" + ) router.parent_router = self self.sub_routers.append(router) return router diff --git a/aiogram/utils/imports.py b/aiogram/utils/imports.py new file mode 100644 index 00000000..edc0a6a0 --- /dev/null +++ b/aiogram/utils/imports.py @@ -0,0 +1,23 @@ +import importlib +from typing import Any + + +def import_module(target: str) -> Any: + if not isinstance(target, str): + raise ValueError(f"Target should be string not {type(target).__class__.__name__}") + + module_name, _, attr_name = target.partition(":") + if not module_name or not attr_name: + raise ValueError(f'Import string "{target}" must be in format ":"') + + try: + module = importlib.import_module(module_name) + except ImportError: + raise ValueError(f'Could not import module "{module_name}".') + + try: + attribute = getattr(module, attr_name) + except AttributeError: + raise ValueError(f'Module "{module_name}" has no attribute "{attr_name}"') + + return attribute diff --git a/handlers.py b/handlers.py new file mode 100644 index 00000000..ce3c7c34 --- /dev/null +++ b/handlers.py @@ -0,0 +1,13 @@ +from typing import Any + +from aiogram import Router +from aiogram.api.methods import SendMessage +from aiogram.dispatcher.handler.message import MessageHandler + +router = Router() + + +@router.message_handler(commands=["test"]) +class MyHandler(MessageHandler): + async def handle(self) -> Any: + return SendMessage(chat_id=self.chat.id, text="PASS") diff --git a/tests/test_dispatcher/test_router.py b/tests/test_dispatcher/test_router.py index b61fc28e..bf185eb8 100644 --- a/tests/test_dispatcher/test_router.py +++ b/tests/test_dispatcher/test_router.py @@ -20,6 +20,8 @@ from aiogram.api.types import ( from aiogram.dispatcher.event.observer import SkipHandler from aiogram.dispatcher.router import Router +importable_router = Router() + class TestRouter: def test_including_routers(self): @@ -50,6 +52,15 @@ class TestRouter: assert router3.parent_router is router2 assert router3.sub_routers == [] + def test_include_router_by_string(self): + router = Router() + router.include_router("tests.test_dispatcher.test_router:importable_router") + + def test_include_router_by_string_bad_type(self): + router = Router() + with pytest.raises(ValueError, match=r"router should be instance of Router"): + router.include_router("tests.test_dispatcher.test_router:TestRouter") + def test_observers_config(self): router = Router() assert router.update_handler.handlers diff --git a/tests/test_utils/test_imports.py b/tests/test_utils/test_imports.py new file mode 100644 index 00000000..e877201c --- /dev/null +++ b/tests/test_utils/test_imports.py @@ -0,0 +1,29 @@ +import pytest + +import aiogram +from aiogram.utils.imports import import_module + + +class TestImports: + def test_bad_type(self): + with pytest.raises(ValueError, match=r"Target should be string not"): + import_module(42) + + @pytest.mark.parametrize("value", ["module", "module:", ":attribute"]) + def test_bad_format(self, value): + with pytest.raises(ValueError, match='must be in format ":"'): + import_module(value) + + @pytest.mark.parametrize("value", ["module", "aiogram.KABOOM", "aiogram.KABOOM.TEST"]) + def test_bad_value(self, value): + with pytest.raises(ValueError, match="Could not import module"): + import_module(f"{value}:attribute") + + def test_has_no_attribute(self): + with pytest.raises(ValueError, match="has no attribute"): + import_module("aiogram:KABOOM") + + def test_imported(self): + value = import_module("aiogram:__version__") + isinstance(value, str) + assert value == aiogram.__version__