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
This commit is contained in:
Alex Root Junior 2024-10-06 16:37:18 +03:00 committed by GitHub
parent 080878be86
commit 1dbdcf0516
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 125 additions and 9 deletions

5
CHANGES/1583.feature.rst Normal file
View file

@ -0,0 +1,5 @@
Enhanced the inheritance of handlers and actions in :ref:`Scenes <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.

View file

@ -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):

View file

@ -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