Skip to content

Commit 6cfc497

Browse files
authored
feat: вложенные роутеры с наследованием middleware и фильтров (#63)
* feat(dispatcher): поддержка вложенных роутеров с наследованием middleware и фильтров Добавлен рекурсивный обход дерева роутеров через _iter_routers: дочерние роутеры автоматически наследуют middleware, MagicFilter и BaseFilter от всех родителей по цепочке вложенности. Покрыто тестами: unit на _iter_routers, интеграционные на dispatch, наследование middleware (3+ уровня), BaseFilter и MagicFilter. * fix(dispatcher): дедупликация повторно включённых роутеров Вынесен обход уникальных экземпляров роутеров в _iter_unique_routers и применён в _prepare_handlers, _process_event и handle_raw_response, чтобы один и тот же роутер не обрабатывал событие дважды. При подготовке обработчиков добавлено предупреждение в лог о повторных включениях. Добавлены тесты на дедупликацию и логирование. * fix(dispatcher): уточнить тип результата _check_router_filters Метод возвращает только dict (включая {}) или False, поэтому убрано | None из аннотации и обновлён Returns в докстринге. * fix(dispatcher): защита _iter_routers от циклических включений роутеров Добавлен обход с path по текущей ветви DFS: повторный заход в роутер на цикле пропускается, после обхода поддерева ключ снимается из path. Добавлен тест, что при a↔b полный обход конечен и даёт оба роутера. * test(nested-routers): MagicFilter на r1/r2 по is_bot и user_id для трёх уровней Добавлены сценарии с накопленными фильтрами: сначала не-бот, затем «админский» user_id; события задаются через мок sender с полями как у User. Покрыты случаи обоих совпадений и блокировки на r1 или r2. * style: правки ruff (E501, E712) в dispatcher и test_nested_routers Разбиты длинные строки в докстрингах _iter_routers и _iter_unique_routers; в тестах MagicFilter заменено сравнение is_bot с False на ~F; укорочены имена длинных тестов и модульный docstring. * fix(dispatcher): убраны ложные дубли роутеров через self в _prepare_handlers В _iter_routers отключён спуск в поддерево Dispatcher-self, чтобы корневые роутеры не обходились повторно через dp.routers и не давали ложные warning о повторных включениях. Добавлен регрессионный тест на отсутствие такого warning при уникальных корневых роутерах.
1 parent a93544b commit 6cfc497

2 files changed

Lines changed: 1143 additions & 16 deletions

File tree

maxapi/dispatcher.py

Lines changed: 177 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from .webhook.aiohttp import AiohttpMaxWebhook
2626

2727
if TYPE_CHECKING:
28-
from collections.abc import Awaitable, Callable
28+
from collections.abc import Awaitable, Callable, Iterator
2929

3030
from magic_filter import MagicFilter
3131

@@ -236,7 +236,9 @@ def _prepare_handlers(self, bot: Bot) -> None:
236236

237237
handlers_count = 0
238238

239-
for router in self.routers:
239+
for router, *_ in self._iter_unique_routers(
240+
self.routers, warn_duplicates=True
241+
):
240242
router.bot = bot
241243

242244
for handler in router.event_handlers:
@@ -339,26 +341,177 @@ async def process_base_filters(
339341

340342
return data
341343

344+
def _iter_routers(
345+
self,
346+
routers: list[Router | Dispatcher],
347+
parent_middlewares: list[BaseMiddleware] | None = None,
348+
parent_filters: list[MagicFilter] | None = None,
349+
parent_base_filters: list[BaseFilter] | None = None,
350+
path: set[int] | None = None,
351+
) -> Iterator[
352+
tuple[
353+
Router | Dispatcher,
354+
list[BaseMiddleware],
355+
list[MagicFilter],
356+
list[BaseFilter],
357+
]
358+
]:
359+
"""
360+
Рекурсивно обходит роутеры, накапливая middleware и фильтры родителей.
361+
362+
Args:
363+
routers: Список роутеров для обхода.
364+
parent_middlewares: Накопленные middleware от родительских
365+
роутеров.
366+
parent_filters: Накопленные MagicFilter от родительских
367+
роутеров.
368+
parent_base_filters: Накопленные BaseFilter от родительских
369+
роутеров.
370+
path: Идентификаторы роутеров в текущей ветви обхода; используется,
371+
чтобы не уходить в бесконечную рекурсию при циклических
372+
включениях между роутерами.
373+
374+
Yields:
375+
Кортеж (роутер, middleware, MagicFilter, BaseFilter) с накопленными
376+
значениями от всех родителей.
377+
"""
378+
parent_middlewares = parent_middlewares or []
379+
parent_filters = parent_filters or []
380+
parent_base_filters = parent_base_filters or []
381+
path = path if path is not None else set()
382+
383+
for router in routers:
384+
router_key = id(router)
385+
if router_key in path:
386+
continue
387+
388+
if router is self:
389+
accumulated_middlewares = parent_middlewares
390+
else:
391+
accumulated_middlewares = (
392+
parent_middlewares + router.middlewares
393+
)
394+
395+
accumulated_filters = parent_filters + router.filters
396+
accumulated_base_filters = (
397+
parent_base_filters + router.base_filters
398+
)
399+
400+
yield (
401+
router,
402+
accumulated_middlewares,
403+
accumulated_filters,
404+
accumulated_base_filters,
405+
)
406+
407+
sub_routers = (
408+
[]
409+
if router is self
410+
else [r for r in router.routers if r is not self]
411+
)
412+
if sub_routers:
413+
path.add(router_key)
414+
try:
415+
yield from self._iter_routers(
416+
routers=sub_routers,
417+
parent_middlewares=accumulated_middlewares,
418+
parent_filters=accumulated_filters,
419+
parent_base_filters=accumulated_base_filters,
420+
path=path,
421+
)
422+
finally:
423+
path.discard(router_key)
424+
425+
def _iter_unique_routers(
426+
self,
427+
routers: list[Router | Dispatcher],
428+
parent_middlewares: list[BaseMiddleware] | None = None,
429+
parent_filters: list[MagicFilter] | None = None,
430+
parent_base_filters: list[BaseFilter] | None = None,
431+
*,
432+
warn_duplicates: bool = False,
433+
) -> Iterator[
434+
tuple[
435+
Router | Dispatcher,
436+
list[BaseMiddleware],
437+
list[MagicFilter],
438+
list[BaseFilter],
439+
]
440+
]:
441+
"""
442+
Обходит дерево роутеров и исключает повторные экземпляры роутеров.
443+
444+
При повторном включении одного и того же объекта роутера используется
445+
контекст первого вхождения (накопленные middleware и фильтры).
446+
447+
Args:
448+
routers: Список роутеров для обхода.
449+
parent_middlewares: Накопленные middleware от родительских
450+
роутеров.
451+
parent_filters: Накопленные MagicFilter от родительских
452+
роутеров.
453+
parent_base_filters: Накопленные BaseFilter от родительских
454+
роутеров.
455+
warn_duplicates: Если True, выводит предупреждение при обнаружении
456+
повторных включений одного и того же экземпляра роутера.
457+
"""
458+
seen: set[int] = set()
459+
duplicate_keys: set[int] = set()
460+
duplicate_titles: list[str] = []
461+
try:
462+
for item in self._iter_routers(
463+
routers=routers,
464+
parent_middlewares=parent_middlewares,
465+
parent_filters=parent_filters,
466+
parent_base_filters=parent_base_filters,
467+
):
468+
router = item[0]
469+
router_key = id(router)
470+
if router_key in seen:
471+
if warn_duplicates and router_key not in duplicate_keys:
472+
duplicate_keys.add(router_key)
473+
rid = getattr(router, "router_id", None)
474+
router_title = (
475+
str(rid)
476+
if rid is not None
477+
else router.__class__.__name__
478+
)
479+
duplicate_titles.append(router_title)
480+
continue
481+
seen.add(router_key)
482+
yield item
483+
finally:
484+
if warn_duplicates and duplicate_titles:
485+
logger_dp.warning(
486+
"Обнаружены повторные включения роутеров: %s. "
487+
"Повторные вхождения будут дедуплицированы.",
488+
", ".join(duplicate_titles),
489+
)
490+
342491
async def _check_router_filters(
343-
self, event: UpdateUnion, router: Router | Dispatcher
344-
) -> dict[str, Any] | None | Literal[False]:
492+
self,
493+
event: UpdateUnion,
494+
filters: list[MagicFilter],
495+
base_filters: list[BaseFilter],
496+
) -> dict[str, Any] | Literal[False]:
345497
"""
346-
Проверяет фильтры роутера для события.
498+
Проверяет накопленные фильтры роутера для события.
347499
348500
Args:
349501
event (UpdateUnion): Событие.
350-
router (Router | Dispatcher): Роутер для проверки.
502+
filters: Накопленные MagicFilter.
503+
base_filters: Накопленные BaseFilter.
351504
352505
Returns:
353-
Optional[Dict[str, Any]] | Literal[False]: Словарь с данными
354-
или False, если фильтры не прошли.
506+
Dict[str, Any] | Literal[False]: Словарь с данными или False,
507+
если фильтры не прошли.
355508
"""
356-
if router.filters and not filter_attrs(event, *router.filters):
509+
if filters and not filter_attrs(event, *filters):
357510
return False
358511

359-
if router.base_filters:
512+
if base_filters:
360513
result = await self.process_base_filters(
361-
event=event, filters=router.base_filters
514+
event=event, filters=base_filters
362515
)
363516
if isinstance(result, dict):
364517
return result
@@ -482,7 +635,7 @@ async def handle_raw_response(
482635
"""
483636
Специальный метод для обработки сырых ответов API.
484637
"""
485-
for router in self.routers:
638+
for router, *_ in self._iter_unique_routers(self.routers):
486639
matching_handlers = self._find_matching_handlers(
487640
router, event_type
488641
)
@@ -525,14 +678,19 @@ async def _process_event(
525678

526679
data["context"] = memory_context
527680

528-
for index, router in enumerate(self.routers):
681+
for index, (
682+
router,
683+
router_middlewares,
684+
router_filters,
685+
router_base_filters,
686+
) in enumerate(self._iter_unique_routers(self.routers)):
529687
if is_handled:
530688
break
531689

532690
router_id = router.router_id or index
533691

534692
router_filter_result = await self._check_router_filters(
535-
event_object, router
693+
event_object, router_filters, router_base_filters
536694
)
537695

538696
if router_filter_result is False:
@@ -545,6 +703,9 @@ async def _process_event(
545703
router, event_object.update_type
546704
)
547705

706+
if not matching_handlers:
707+
continue
708+
548709
async def _process_handlers(
549710
event: UpdateUnion, handler_data: dict[str, Any]
550711
) -> None:
@@ -582,9 +743,9 @@ async def _process_handlers(
582743
is_handled = True
583744
break
584745

585-
if isinstance(router, Router) and router.middlewares:
746+
if router_middlewares:
586747
router_chain = self.build_middleware_chain(
587-
router.middlewares, _process_handlers
748+
router_middlewares, _process_handlers
588749
)
589750
await router_chain(event_object, data)
590751
else:

0 commit comments

Comments
 (0)