Skip to content

feat(dialogs): Incorporate DialogDependencies #2219

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions libraries/botbuilder-dialogs/botbuilder/dialogs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .component_dialog import ComponentDialog
from .dialog_container import DialogContainer
from .dialog_context import DialogContext
from .dialog_dependencies import DialogDependencies
from .dialog_event import DialogEvent
from .dialog_events import DialogEvents
from .dialog_instance import DialogInstance
Expand All @@ -35,6 +36,7 @@
"ComponentDialog",
"DialogContainer",
"DialogContext",
"DialogDependencies",
"DialogEvent",
"DialogEvents",
"DialogInstance",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from typing import Iterable, Protocol, runtime_checkable

from .dialog import Dialog


@runtime_checkable
class DialogDependencies(Protocol):
"""Protocol for dialogs that have dependencies on other dialogs.

If implemented, when the dialog is added to a DialogSet all of its dependencies will be added as well.
"""

def get_dependencies(self) -> Iterable[Dialog]:
"""Returns an iterable of the dialogs that this dialog depends on.

:return: The dialog dependencies."""
12 changes: 11 additions & 1 deletion libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Licensed under the MIT License.
import inspect
from hashlib import sha256
from typing import Dict
from typing import Dict, TYPE_CHECKING

from botbuilder.core import (
NullTelemetryClient,
Expand All @@ -14,6 +14,9 @@
from .dialog import Dialog
from .dialog_state import DialogState

if TYPE_CHECKING:
from .dialog_context import DialogContext
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is necessary for the -> "DialogContext" used below, otherwise intellisense has a hard time understanding that the object returned is a DialogContext. It has no runtime impact because of TYPE_CHECKING being False at runtime



class DialogSet:
def __init__(self, dialog_state: StatePropertyAccessor = None):
Expand Down Expand Up @@ -92,6 +95,8 @@ def add(self, dialog: Dialog):
)

if dialog.id in self._dialogs:
if self._dialogs[dialog.id] == dialog:
return self
raise TypeError(
"DialogSet.add(): A dialog with an id of '%s' already added."
% dialog.id
Expand All @@ -100,6 +105,11 @@ def add(self, dialog: Dialog):
# dialog.telemetry_client = this._telemetry_client;
self._dialogs[dialog.id] = dialog

# Automatically add any child dependencies the dialog might have, see DialogDependencies.
if hasattr(dialog, "get_dependencies") and callable(dialog.get_dependencies):
for child in dialog.get_dependencies():
self.add(child)

return self

async def create_context(self, turn_context: TurnContext) -> "DialogContext":
Expand Down
74 changes: 73 additions & 1 deletion libraries/botbuilder-dialogs/tests/test_dialog_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
# Licensed under the MIT License.

import aiounittest
from botbuilder.dialogs import DialogSet, ComponentDialog, WaterfallDialog
from botbuilder.dialogs import (
DialogDependencies,
DialogSet,
ComponentDialog,
WaterfallDialog,
)
from botbuilder.core import ConversationState, MemoryStorage, NullTelemetryClient


Expand Down Expand Up @@ -90,6 +95,73 @@ def test_dialogset_nulltelemetryset(self):
)
)

def test_dialogset_raises_on_repeated_id(self):
convo_state = ConversationState(MemoryStorage())
dialog_state_property = convo_state.create_property("dialogstate")
dialog_set = DialogSet(dialog_state_property)

dialog_set.add(WaterfallDialog("A"))
with self.assertRaises(TypeError):
dialog_set.add(WaterfallDialog("A"))

self.assertTrue(dialog_set.find_dialog("A") is not None)

def test_dialogset_idempotenticy_add(self):
convo_state = ConversationState(MemoryStorage())
dialog_state_property = convo_state.create_property("dialogstate")
dialog_set = DialogSet(dialog_state_property)
dialog_a = WaterfallDialog("A")
dialog_set.add(dialog_a)
dialog_set.add(dialog_a)

async def test_dialogset_dependency_tree_add(self):
class MyDialog(WaterfallDialog, DialogDependencies):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._dependencies = []

def add_dependency(self, dialog):
self._dependencies.append(dialog)

def get_dependencies(self):
return self._dependencies

convo_state = ConversationState(MemoryStorage())
dialog_state_property = convo_state.create_property("dialogstate")
dialog_set = DialogSet(dialog_state_property)

dialog_a = MyDialog("A")
dialog_b = MyDialog("B")
dialog_c = MyDialog("C")
dialog_d = MyDialog("D")
dialog_e = MyDialog("E")
dialog_i = MyDialog("I")

dialog_a.add_dependency(dialog_b)

# Multi-hierarchy should be OK
dialog_b.add_dependency(dialog_d)
dialog_b.add_dependency(dialog_e)

# circular dependencies should be OK
dialog_c.add_dependency(dialog_d)
dialog_d.add_dependency(dialog_c)

assert dialog_set.find_dialog(dialog_a.id) is None
dialog_set.add(dialog_a)

for dialog in [
dialog_a,
dialog_b,
dialog_c,
dialog_d,
dialog_e,
]:
self.assertTrue(dialog_set.find_dialog(dialog.id) is dialog)
self.assertTrue(await dialog_set.find(dialog.id) is dialog)

assert dialog_set.find_dialog(dialog_i.id) is None

# pylint: disable=pointless-string-statement
"""
This test will be enabled when telematry tests are fixed for DialogSet telemetry
Expand Down