From 1dbdcf05164807f6f674891f057efa3ad64a413f Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 6 Oct 2024 16:37:18 +0300 Subject: [PATCH] Improved Scene handler and action inheritance mechanism (#1585) * #1583 Improved Scene handler and action inheritance mechanism Enhanced the inheritance of handlers and actions in scenes. Refactored to eliminate the copying of previously connected handlers and actions from parent scenes. Now, handlers are dynamically rebuilt based on the current class, properly utilizing class inheritance and enabling handler overrides. * Added more tests * Added more tests for non-function handlers --- CHANGES/1583.feature.rst | 5 ++ aiogram/fsm/scene.py | 8 +-- tests/test_fsm/test_scene.py | 121 ++++++++++++++++++++++++++++++++++- 3 files changed, 125 insertions(+), 9 deletions(-) create mode 100644 CHANGES/1583.feature.rst diff --git a/CHANGES/1583.feature.rst b/CHANGES/1583.feature.rst new file mode 100644 index 00000000..e060762e --- /dev/null +++ b/CHANGES/1583.feature.rst @@ -0,0 +1,5 @@ +Enhanced the inheritance of handlers and actions in :ref:`Scenes `. +Refactored to eliminate the copying of previously connected handlers and actions from parent scenes. +Now, handlers are dynamically rebuilt based on the current class, properly utilizing class inheritance and enabling handler overrides. + +That's mean that you can now override handlers and actions in the child scene, instead of copying and duplicating them. diff --git a/aiogram/fsm/scene.py b/aiogram/fsm/scene.py index fe4de5c4..991b3472 100644 --- a/aiogram/fsm/scene.py +++ b/aiogram/fsm/scene.py @@ -316,10 +316,6 @@ class Scene: if not parent_scene_config: continue - handlers.extend(parent_scene_config.handlers) - for action, action_handlers in parent_scene_config.actions.items(): - actions[action].update(action_handlers) - if reset_data_on_enter is None: reset_data_on_enter = parent_scene_config.reset_data_on_enter if reset_history_on_enter is None: @@ -327,9 +323,7 @@ class Scene: if callback_query_without_state is None: callback_query_without_state = parent_scene_config.callback_query_without_state - for name in vars(cls): - value = getattr(cls, name) - + for name, value in inspect.getmembers(cls): if scene_handlers := getattr(value, "__aiogram_handler__", None): handlers.extend(scene_handlers) if isinstance(value, ObserverDecorator): diff --git a/tests/test_fsm/test_scene.py b/tests/test_fsm/test_scene.py index 2bb2cf0a..9c388a11 100644 --- a/tests/test_fsm/test_scene.py +++ b/tests/test_fsm/test_scene.py @@ -1,5 +1,4 @@ import inspect -import platform from datetime import datetime from unittest.mock import ANY, AsyncMock, patch @@ -7,8 +6,9 @@ import pytest from aiogram import Dispatcher, F, Router from aiogram.dispatcher.event.bases import NextMiddlewareType +from aiogram.enums import UpdateType from aiogram.exceptions import SceneException -from aiogram.filters import StateFilter +from aiogram.filters import Command, StateFilter from aiogram.fsm.context import FSMContext from aiogram.fsm.scene import ( ActionContainer, @@ -1545,3 +1545,120 @@ class TestSceneRegistry: assert isinstance(data["scenes"], ScenesManager) handler.assert_called_once_with(event, data) assert result == handler.return_value + + +class TestSceneInheritance: + def test_inherit_handlers(self): + class ParentScene(Scene): + @on.message(Command("exit")) + async def command_exit(self, message: Message) -> None: + pass + + class ChildScene(ParentScene): + pass + + assert len(ParentScene.__scene_config__.handlers) == 1 + assert len(ChildScene.__scene_config__.handlers) == 1 + + parent_command_handler = ParentScene.__scene_config__.handlers[0] + child_command_handler = ChildScene.__scene_config__.handlers[0] + + assert parent_command_handler.handler is ParentScene.command_exit + assert child_command_handler.handler is ParentScene.command_exit + + def test_override_handlers(self): + class ParentScene(Scene): + @on.message(Command("exit")) + async def command_exit(self, message: Message) -> int: + return 1 + + class ChildScene(ParentScene): + @on.message(Command("exit")) + async def command_exit(self, message: Message) -> int: + return 2 + + assert len(ParentScene.__scene_config__.handlers) == 1 + assert len(ChildScene.__scene_config__.handlers) == 1 + + parent_command_handler = ParentScene.__scene_config__.handlers[0] + child_command_handler = ChildScene.__scene_config__.handlers[0] + + assert parent_command_handler.handler is ParentScene.command_exit + assert child_command_handler.handler is not ParentScene.command_exit + assert child_command_handler.handler is ChildScene.command_exit + + def test_inherit_actions(self): + class ParentScene(Scene): + @on.message.enter() + async def on_enter(self, message: Message) -> None: + pass + + class ChildScene(ParentScene): + pass + + parent_enter_action = ParentScene.__scene_config__.actions[SceneAction.enter][ + UpdateType.MESSAGE + ] + child_enter_action = ChildScene.__scene_config__.actions[SceneAction.enter][ + UpdateType.MESSAGE + ] + + assert parent_enter_action.callback is ParentScene.on_enter + assert child_enter_action.callback is ParentScene.on_enter + assert child_enter_action.callback is ChildScene.on_enter + + def test_override_actions(self): + class ParentScene(Scene): + @on.message.enter() + async def on_enter(self, message: Message) -> int: + return 1 + + class ChildScene(ParentScene): + @on.message.enter() + async def on_enter(self, message: Message) -> int: + return 2 + + parent_enter_action = ParentScene.__scene_config__.actions[SceneAction.enter][ + UpdateType.MESSAGE + ] + child_enter_action = ChildScene.__scene_config__.actions[SceneAction.enter][ + UpdateType.MESSAGE + ] + + assert parent_enter_action.callback is ParentScene.on_enter + assert child_enter_action.callback is not ParentScene.on_enter + assert child_enter_action.callback is ChildScene.on_enter + + def test_override_non_function_handler_by_function(self): + class ParentScene(Scene): + do_exit = on.message(Command("exit"), after=After.exit) + + class ChildScene1(ParentScene): + pass + + class ChildScene2(ParentScene): + do_exit = on.message(Command("exit"), after=After.back) + + class ChildScene3(ParentScene): + @on.message(Command("exit"), after=After.back) + async def do_exit(self, message: Message) -> None: + pass + + assert len(ParentScene.__scene_config__.handlers) == 1 + assert len(ChildScene1.__scene_config__.handlers) == 1 + assert len(ChildScene2.__scene_config__.handlers) == 1 + assert len(ChildScene3.__scene_config__.handlers) == 1 + + parent_handler = ParentScene.__scene_config__.handlers[0] + child_1_handler = ChildScene1.__scene_config__.handlers[0] + child_2_handler = ChildScene2.__scene_config__.handlers[0] + child_3_handler = ChildScene3.__scene_config__.handlers[0] + + assert child_1_handler.handler is parent_handler.handler + assert child_1_handler.after == parent_handler.after + assert child_1_handler.handler is _empty_handler + + assert child_2_handler.after != parent_handler.after + assert child_2_handler.handler is _empty_handler + + assert child_3_handler.handler is not _empty_handler