diff --git a/mesop/component_helpers/BUILD b/mesop/component_helpers/BUILD index 7bd4e31f..71a9598a 100644 --- a/mesop/component_helpers/BUILD +++ b/mesop/component_helpers/BUILD @@ -29,6 +29,7 @@ py_test( srcs = ["helper_test.py"], deps = [ ":component_helpers", + "//mesop/runtime", "//mesop/server", ] + THIRD_PARTY_PY_PYTEST, ) diff --git a/mesop/component_helpers/helper.py b/mesop/component_helpers/helper.py index c1ff0458..7bed0150 100644 --- a/mesop/component_helpers/helper.py +++ b/mesop/component_helpers/helper.py @@ -75,10 +75,10 @@ def slot(name: str = ""): @dataclass(kw_only=True) class SlotMetadata: - """Metadata for slot + """Metadata for slot. Attributes: - node_slot: Position of the slot + node_slot: Position of the slot. node_tree_state: Detached node tree state for this slot. """ @@ -107,6 +107,8 @@ def __exit__(self, exc_type, exc_val, exc_tb): class DetachedNodeTreeStateContextFactory: + """Creates `DetachedNodeTreeStateContext` instances with the given named slots.""" + def __init__(self, named_slots: dict[str, SlotMetadata]): self.named_slots_accessed = set() self.named_slots = named_slots @@ -157,6 +159,13 @@ def __init__(self, fn: Callable[[], T]): "Must configure at least one child slot when defining a composite component." ) + if len(self.unnamed_slots) == 0 and len(self.named_slots) != len( + node_tree_state.node_slots() + ): + raise MesopDeveloperException( + "Multiple slots of the same name encountered. The names must be unique within the composite component." + ) + if ( len(self.named_slots) > 0 and len(self.named_slots) != len(node_tree_state.node_slots()) diff --git a/mesop/component_helpers/helper_test.py b/mesop/component_helpers/helper_test.py index a53384ac..071cd44a 100644 --- a/mesop/component_helpers/helper_test.py +++ b/mesop/component_helpers/helper_test.py @@ -1,7 +1,28 @@ +from unittest.mock import patch + import pytest +from flask import Flask -from mesop.component_helpers.helper import check_property_keys_is_safe +import mesop.protos.ui_pb2 as pb +from mesop.component_helpers.helper import ( + DetachedNodeTreeStateContext, + check_property_keys_is_safe, +) from mesop.exceptions import MesopDeveloperException +from mesop.runtime.context import NodeTreeState +from mesop.runtime.runtime import Runtime + + +def create_default_single_component(): + return pb.Component( + key=pb.Key(key="key"), + style=pb.Style(color="red", columns="1"), + style_debug_json="debug json string", + type=pb.Type( + name=pb.ComponentName(core_module=True, fn_name="test"), value=b"value" + ), + source_code_location=pb.SourceCodeLocation(module="x", line=1, col=2), + ) def test_check_property_keys_is_safe_raises_exception(): @@ -29,5 +50,28 @@ def test_check_property_keys_is_safe_passes(): check_property_keys_is_safe({"click-on": None}.keys()) +@pytest.fixture +def app(): + app = Flask(__name__) + return app + + +@patch("mesop.component_helpers.helper.runtime") +def test_detached_node_tree_state_context(mock_runtime, app): + with app.app_context(): + runtime = Runtime() + mock_runtime.return_value = runtime + node_tree_state = NodeTreeState() + runtime.context().set_node_tree_state(node_tree_state) + detached_node_tree_state = NodeTreeState() + c1 = create_default_single_component() + detached_node_tree_state.set_current_node(c1) + + with DetachedNodeTreeStateContext(detached_node_tree_state): + assert runtime.context().get_node_tree_state() == detached_node_tree_state + + assert runtime.context().get_node_tree_state() == node_tree_state + + if __name__ == "__main__": raise SystemExit(pytest.main([__file__])) diff --git a/mesop/runtime/BUILD b/mesop/runtime/BUILD index cccaf7ef..b293e02b 100644 --- a/mesop/runtime/BUILD +++ b/mesop/runtime/BUILD @@ -1,4 +1,4 @@ -load("//build_defs:defaults.bzl", "THIRD_PARTY_PY_FLASK", "py_library") +load("//build_defs:defaults.bzl", "THIRD_PARTY_PY_FLASK", "THIRD_PARTY_PY_PYTEST", "py_library", "py_test") package( default_visibility = ["//build_defs:mesop_internal"], @@ -19,3 +19,9 @@ py_library( "//mesop/warn", ] + THIRD_PARTY_PY_FLASK, ) + +py_test( + name = "node_tree_state_test", + srcs = ["node_tree_state_test.py"], + deps = [":runtime"] + THIRD_PARTY_PY_PYTEST, +) diff --git a/mesop/runtime/node_tree_state_test.py b/mesop/runtime/node_tree_state_test.py new file mode 100644 index 00000000..fb802d6f --- /dev/null +++ b/mesop/runtime/node_tree_state_test.py @@ -0,0 +1,117 @@ +import pytest + +import mesop.protos.ui_pb2 as pb +from mesop.runtime.context import NodeTreeState + + +def create_default_single_component(): + return pb.Component( + key=pb.Key(key="key"), + style=pb.Style(color="red", columns="1"), + style_debug_json="debug json string", + type=pb.Type( + name=pb.ComponentName(core_module=True, fn_name="test"), value=b"value" + ), + source_code_location=pb.SourceCodeLocation(module="x", line=1, col=2), + ) + + +def test_set_current_node(): + node_tree_state = NodeTreeState() + current_component = create_default_single_component() + node_tree_state.set_current_node(current_component) + + assert node_tree_state.current_node() == current_component + + +def test_set_previous_node_from_current_node(): + node_tree_state = NodeTreeState() + current_component = create_default_single_component() + node_tree_state.set_current_node(current_component) + assert node_tree_state.previous_node() is None + + node_tree_state.set_previous_node_from_current_node() + + assert node_tree_state.previous_node() == current_component + + +def test_reset_current_node(): + node_tree_state = NodeTreeState() + current_component = create_default_single_component() + node_tree_state.set_current_node(current_component) + assert node_tree_state.current_node() == current_component + + node_tree_state.reset_current_node() + + assert node_tree_state.current_node() == pb.Component() + + +def test_reset_previous_node(): + node_tree_state = NodeTreeState() + node_tree_state.set_previous_node_from_current_node() + assert node_tree_state.previous_node() == node_tree_state.current_node() + + node_tree_state.reset_previous_node() + + assert node_tree_state.previous_node() is None + + +def test_save_current_node_as_slot(): + node_tree_state = NodeTreeState() + c1 = create_default_single_component() + c1_c1 = create_default_single_component() + c1_c2 = create_default_single_component() + c1.children.append(c1_c1) + c1.children.append(c1_c2) + node_tree_state.set_current_node(c1) + + node_tree_state.save_current_node_as_slot() + + node_slots = node_tree_state.node_slots() + assert len(node_slots) == 1 + assert node_slots[0].name == "" + assert node_slots[0].parent_node == c1 + assert node_slots[0].insertion_index == 2 + + +def test_save_current_node_as_slot_with_name(): + node_tree_state = NodeTreeState() + current_node = create_default_single_component() + node_tree_state.set_current_node(current_node) + + node_tree_state.save_current_node_as_slot("test") + + node_slots = node_tree_state.node_slots() + assert len(node_slots) == 1 + assert node_slots[0].name == "test" + assert node_slots[0].parent_node == current_node + assert node_slots[0].insertion_index == 0 + + +def test_multiple_save_current_node_as_slot(): + node_tree_state = NodeTreeState() + c1 = create_default_single_component() + c2 = create_default_single_component() + node_tree_state.set_current_node(c1) + node_tree_state.save_current_node_as_slot() + node_tree_state.set_current_node(c2) + node_tree_state.save_current_node_as_slot() + + node_slots = node_tree_state.node_slots() + assert len(node_slots) == 2 + assert node_slots[0].parent_node == c1 + assert node_slots[1].parent_node == c2 + + +def test_clear_node_slots(): + node_tree_state = NodeTreeState() + current_node = create_default_single_component() + node_tree_state.set_current_node(current_node) + + node_tree_state.save_current_node_as_slot() + node_tree_state.clear_node_slots() + assert len(node_tree_state.node_slots()) == 0 + + +if __name__ == "__main__": + raise SystemExit(pytest.main([__file__]))