diff --git a/libraries/botbuilder-core/botbuilder/core/__init__.py b/libraries/botbuilder-core/botbuilder/core/__init__.py index d596d2d07..f34eec933 100644 --- a/libraries/botbuilder-core/botbuilder/core/__init__.py +++ b/libraries/botbuilder-core/botbuilder/core/__init__.py @@ -8,10 +8,12 @@ from .about import __version__ from .activity_handler import ActivityHandler +from .auto_save_state_middleware import AutoSaveStateMiddleware from .bot_assert import BotAssert from .bot_adapter import BotAdapter from .bot_framework_adapter import BotFrameworkAdapter, BotFrameworkAdapterSettings from .bot_state import BotState +from .bot_state_set import BotStateSet from .bot_telemetry_client import BotTelemetryClient from .card_factory import CardFactory from .conversation_state import ConversationState @@ -33,11 +35,13 @@ __all__ = [ "ActivityHandler", "AnonymousReceiveMiddleware", + "AutoSaveStateMiddleware", "BotAdapter", "BotAssert", "BotFrameworkAdapter", "BotFrameworkAdapterSettings", "BotState", + "BotStateSet", "BotTelemetryClient", "calculate_change_hash", "CardFactory", diff --git a/libraries/botbuilder-core/botbuilder/core/auto_save_state_middleware.py b/libraries/botbuilder-core/botbuilder/core/auto_save_state_middleware.py new file mode 100644 index 000000000..3e42664fc --- /dev/null +++ b/libraries/botbuilder-core/botbuilder/core/auto_save_state_middleware.py @@ -0,0 +1,29 @@ +from typing import Awaitable, Callable, List, Union + +from .bot_state import BotState +from .bot_state_set import BotStateSet +from .middleware_set import Middleware +from .turn_context import TurnContext + + +class AutoSaveStateMiddleware(Middleware): + def __init__(self, bot_states: Union[List[BotState], BotStateSet] = None): + if bot_states is None: + bot_states = [] + if isinstance(bot_states, BotStateSet): + self.bot_state_set: BotStateSet = bot_states + else: + self.bot_state_set: BotStateSet = BotStateSet(bot_states) + + def add(self, bot_state: BotState) -> "AutoSaveStateMiddleware": + if bot_state is None: + raise TypeError("Expected BotState") + + self.bot_state_set.add(bot_state) + return self + + async def on_process_request( + self, context: TurnContext, logic: Callable[[TurnContext], Awaitable] + ): + await logic() + await self.bot_state_set.save_all_changes(context, False) diff --git a/libraries/botbuilder-core/botbuilder/core/bot_state_set.py b/libraries/botbuilder-core/botbuilder/core/bot_state_set.py new file mode 100644 index 000000000..8a86aaba0 --- /dev/null +++ b/libraries/botbuilder-core/botbuilder/core/bot_state_set.py @@ -0,0 +1,29 @@ +from asyncio import wait +from typing import List +from .bot_state import BotState +from .turn_context import TurnContext + + +class BotStateSet: + def __init__(self, bot_states: List[BotState]): + self.bot_states = list(bot_states) + + def add(self, bot_state: BotState) -> "BotStateSet": + if bot_state is None: + raise TypeError("Expected BotState") + + self.bot_states.append(bot_state) + return self + + async def load_all(self, turn_context: TurnContext, force: bool = False): + await wait( + [bot_state.load(turn_context, force) for bot_state in self.bot_states] + ) + + async def save_all_changes(self, turn_context: TurnContext, force: bool = False): + await wait( + [ + bot_state.save_changes(turn_context, force) + for bot_state in self.bot_states + ] + ) diff --git a/libraries/botbuilder-core/tests/test_auto_save_middleware.py b/libraries/botbuilder-core/tests/test_auto_save_middleware.py new file mode 100644 index 000000000..d63d84764 --- /dev/null +++ b/libraries/botbuilder-core/tests/test_auto_save_middleware.py @@ -0,0 +1,115 @@ +import aiounittest +from botbuilder.core import AutoSaveStateMiddleware, BotState, TurnContext +from botbuilder.core.adapters import TestAdapter +from botbuilder.schema import Activity + + +async def aux_func(): + return + + +class BotStateMock(BotState): + def __init__(self, state): # pylint: disable=super-init-not-called + self.state = state + self.assert_force = False + self.read_called = False + self.write_called = False + + async def load(self, turn_context: TurnContext, force: bool = False) -> None: + assert turn_context is not None, "BotStateMock.load() not passed context." + if self.assert_force: + assert force, "BotStateMock.load(): force not set." + self.read_called = True + + async def save_changes( + self, turn_context: TurnContext, force: bool = False + ) -> None: + assert ( + turn_context is not None + ), "BotStateMock.save_changes() not passed context." + if self.assert_force: + assert force, "BotStateMock.save_changes(): force not set." + self.write_called = True + + def get_storage_key( + self, turn_context: TurnContext # pylint: disable=unused-argument + ) -> str: + return "" + + +class TestAutoSaveMiddleware(aiounittest.AsyncTestCase): + async def test_should_add_and_call_load_all_on_single_plugin(self): + adapter = TestAdapter() + context = TurnContext(adapter, Activity()) + foo_state = BotStateMock({"foo": "bar"}) + bot_state_set = AutoSaveStateMiddleware().add(foo_state) + await bot_state_set.bot_state_set.load_all(context) + + async def test_should_add_and_call_load_all_on_multiple_plugins(self): + adapter = TestAdapter() + context = TurnContext(adapter, Activity()) + foo_state = BotStateMock({"foo": "bar"}) + bar_state = BotStateMock({"bar": "foo"}) + bot_state_set = AutoSaveStateMiddleware([foo_state, bar_state]) + await bot_state_set.bot_state_set.load_all(context) + + async def test_should_add_and_call_save_all_changes_on_a_single_plugin(self): + adapter = TestAdapter() + context = TurnContext(adapter, Activity()) + foo_state = BotStateMock({"foo": "bar"}) + bot_state_set = AutoSaveStateMiddleware().add(foo_state) + await bot_state_set.bot_state_set.save_all_changes(context) + assert foo_state.write_called, "write not called for plugin." + + async def test_should_add_and_call_save_all_changes_on_multiple_plugins(self): + adapter = TestAdapter() + context = TurnContext(adapter, Activity()) + foo_state = BotStateMock({"foo": "bar"}) + bar_state = BotStateMock({"bar": "foo"}) + autosave_middleware = AutoSaveStateMiddleware([foo_state, bar_state]) + await autosave_middleware.bot_state_set.save_all_changes(context) + assert ( + foo_state.write_called or bar_state.write_called + ), "write not called for either plugin." + assert foo_state.write_called, "write not called for 'foo_state' plugin." + assert bar_state.write_called, "write not called for 'bar_state' plugin." + + async def test_should_pass_force_flag_through_in_load_all_call(self): + adapter = TestAdapter() + context = TurnContext(adapter, Activity()) + foo_state = BotStateMock({"foo": "bar"}) + foo_state.assert_force = True + autosave_middleware = AutoSaveStateMiddleware().add(foo_state) + await autosave_middleware.bot_state_set.load_all(context, True) + + async def test_should_pass_force_flag_through_in_save_all_changes_call(self): + adapter = TestAdapter() + context = TurnContext(adapter, Activity()) + foo_state = BotStateMock({"foo": "bar"}) + foo_state.assert_force = True + autosave_middleware = AutoSaveStateMiddleware().add(foo_state) + await autosave_middleware.bot_state_set.save_all_changes(context, True) + + async def test_should_work_as_a_middleware_plugin(self): + adapter = TestAdapter() + context = TurnContext(adapter, Activity()) + foo_state = BotStateMock({"foo": "bar"}) + autosave_middleware = AutoSaveStateMiddleware().add(foo_state) + await autosave_middleware.on_process_request(context, aux_func) + assert foo_state.write_called, "save_all_changes() not called." + + async def test_should_support_plugins_passed_to_constructor(self): + adapter = TestAdapter() + context = TurnContext(adapter, Activity()) + foo_state = BotStateMock({"foo": "bar"}) + autosave_middleware = AutoSaveStateMiddleware().add(foo_state) + await autosave_middleware.on_process_request(context, aux_func) + assert foo_state.write_called, "save_all_changes() not called." + + async def test_should_not_add_any_bot_state_on_construction_if_none_are_passed_in( + self + ): + middleware = AutoSaveStateMiddleware() + assert ( + not middleware.bot_state_set.bot_states + ), "should not have added any BotState." diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/number_prompt.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/number_prompt.py index 54d6f15dd..cddb174c9 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/number_prompt.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/number_prompt.py @@ -3,9 +3,9 @@ from typing import Callable, Dict -from babel.numbers import parse_decimal from recognizers_number import recognize_number from recognizers_text import Culture, ModelResult +from babel.numbers import parse_decimal from botbuilder.core.turn_context import TurnContext from botbuilder.schema import ActivityTypes