diff --git a/examples/test/print.py b/examples/test/print.py index 86c1281..41ea9d8 100644 --- a/examples/test/print.py +++ b/examples/test/print.py @@ -2,7 +2,7 @@ from trame.ui.html import DivLayout from trame.widgets import html -server = get_server() +server = get_server(client_type="vue3") # Test to dynamically add a fake directive html.Div.register_directive("v_seb_directive") diff --git a/examples/test/reactivity.py b/examples/test/reactivity.py index 6fcddab..160d696 100644 --- a/examples/test/reactivity.py +++ b/examples/test/reactivity.py @@ -1,7 +1,7 @@ from trame.app import get_server from trame_client.utils.testing import enable_testing -server = get_server() +server = get_server(client_type="vue3") server.state.count = 1 server.state.trame__template_main = """
diff --git a/examples/vue2/client_components.py b/examples/vue2/client_components.py index 33cc22f..140279c 100644 --- a/examples/vue2/client_components.py +++ b/examples/vue2/client_components.py @@ -2,8 +2,7 @@ from trame.widgets import html, client from trame.ui.html import DivLayout -server = get_server() -server.client_type = "vue2" +server = get_server(client_type="vue2") state, ctrl = server.state, server.controller dyna_var_count = 1 diff --git a/examples/vue2/core_reactive_state.py b/examples/vue2/core_reactive_state.py index 0979dff..a23c260 100644 --- a/examples/vue2/core_reactive_state.py +++ b/examples/vue2/core_reactive_state.py @@ -1,7 +1,6 @@ from trame.app import get_server -server = get_server() -server.client_type = "vue2" # default until trame>=3.x +server = get_server(client_type="vue2") state, ctrl = server.state, server.controller state.count = 2 diff --git a/examples/vue2/dynamic_template.py b/examples/vue2/dynamic_template.py index acd8a65..1a43ff6 100644 --- a/examples/vue2/dynamic_template.py +++ b/examples/vue2/dynamic_template.py @@ -3,8 +3,7 @@ from trame.ui.html import DivLayout from trame_client.utils.testing import enable_testing -server = enable_testing(get_server(), "count") -server.client_type = "vue2" +server = enable_testing(get_server(client_type="vue2"), "count") state, ctrl = server.state, server.controller state.count = 1 diff --git a/examples/vue2/getter.py b/examples/vue2/getter.py index 5b1d2d3..f8abe4d 100644 --- a/examples/vue2/getter.py +++ b/examples/vue2/getter.py @@ -6,8 +6,7 @@ # Trame setup # ----------------------------------------------------------------------------- -server = get_server() -server.client_type = "vue2" +server = get_server(client_type="vue2") state, ctrl = server.state, server.controller state.message_1 = "Hello 1" diff --git a/examples/vue2/js_call.py b/examples/vue2/js_call.py index a515e12..f4bcecd 100644 --- a/examples/vue2/js_call.py +++ b/examples/vue2/js_call.py @@ -3,8 +3,7 @@ from trame.ui.html import DivLayout from trame_client.utils.testing import enable_testing -server = enable_testing(get_server(), "message") -server.client_type = "vue2" +server = enable_testing(get_server(client_type="vue2"), "message") state, ctrl = server.state, server.controller diff --git a/examples/vue2/life_cycle.py b/examples/vue2/life_cycle.py index 118a7cf..b85a85e 100644 --- a/examples/vue2/life_cycle.py +++ b/examples/vue2/life_cycle.py @@ -1,7 +1,6 @@ from trame.app import get_server -server = get_server() -server.client_type = "vue2" +server = get_server(client_type="vue2") ctrl = server.controller server.state.trame__template_main = "Life Cycle App" diff --git a/examples/vue2/namespace_nested.py b/examples/vue2/namespace_nested.py new file mode 100644 index 0000000..202f420 --- /dev/null +++ b/examples/vue2/namespace_nested.py @@ -0,0 +1,31 @@ +from trame.app import get_server + +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../../tests")) +from test_translator import NestedApp # noqa: E402 + +CLIENT_TYPE = "vue2" +ISOLATED = False + + +def main(): + root_server = get_server(client_type=CLIENT_TYPE) + + root_app = NestedApp(root_server, client_type=CLIENT_TYPE) + + if not ISOLATED: + root_app.child_a_app.server.translator.add_translation("f", "common_f") + root_app.child_b_app.server.translator.add_translation("f", "common_f") + + root_app.ui + + root_server.controller.on_server_ready.add( + lambda **kwargs: print("Root server ready") + ) + root_app.server.start() + + +if __name__ == "__main__": + main() diff --git a/examples/vue2/namespace_prefix.py b/examples/vue2/namespace_prefix.py new file mode 100644 index 0000000..0c3dfa0 --- /dev/null +++ b/examples/vue2/namespace_prefix.py @@ -0,0 +1,34 @@ +from trame.app import get_server + +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../../tests")) +from test_translator import BasicApp # noqa: E402 + +CLIENT_TYPE = "vue2" + + +def main(): + root_server = get_server(client_type=CLIENT_TYPE) + one_of_the_app = None + for app_name in ["main", "a", "b", "c"]: + child_server = root_server.create_child_server(prefix=f"{app_name}_") + app = BasicApp(child_server, app_name, CLIENT_TYPE) + app.ui + app.add() + app.ctrl.plus() + one_of_the_app = app + + # root_server.controller.on_server_ready.add(lambda **kwargs: print(json.dumps(root_server.state.to_dict(), indent=2))) + root_server.controller.on_server_ready.add( + lambda **kwargs: print("Root server ready") + ) + one_of_the_app.server.controller.on_server_ready.add( + lambda **kwargs: print("Sub server ready") + ) + one_of_the_app.server.start() + + +if __name__ == "__main__": + main() diff --git a/examples/vue3/client_components.py b/examples/vue3/client_components.py index 39aea40..1f9f9c2 100644 --- a/examples/vue3/client_components.py +++ b/examples/vue3/client_components.py @@ -2,8 +2,7 @@ from trame.widgets import html, client from trame.ui.html import DivLayout -server = get_server() -server.client_type = "vue3" +server = get_server(client_type="vue3") state, ctrl = server.state, server.controller dyna_var_count = 1 diff --git a/examples/vue3/core_reactive_state.py b/examples/vue3/core_reactive_state.py index cbceb4f..8c99fbe 100644 --- a/examples/vue3/core_reactive_state.py +++ b/examples/vue3/core_reactive_state.py @@ -1,7 +1,6 @@ from trame.app import get_server -server = get_server() -server.client_type = "vue3" +server = get_server(client_type="vue3") state, ctrl = server.state, server.controller state.count = 2 diff --git a/examples/vue3/dynamic_template.py b/examples/vue3/dynamic_template.py index 123c00b..f970a22 100644 --- a/examples/vue3/dynamic_template.py +++ b/examples/vue3/dynamic_template.py @@ -3,8 +3,7 @@ from trame.ui.html import DivLayout from trame_client.utils.testing import enable_testing -server = get_server() -server.client_type = "vue3" +server = get_server(client_type="vue3") state, ctrl = server.state, server.controller state.count = 1 diff --git a/examples/vue3/getter.py b/examples/vue3/getter.py index b572d76..d46944f 100644 --- a/examples/vue3/getter.py +++ b/examples/vue3/getter.py @@ -6,8 +6,7 @@ # Trame setup # ----------------------------------------------------------------------------- -server = get_server() -server.client_type = "vue3" +server = get_server(client_type="vue3") state, ctrl = server.state, server.controller state.message_1 = "Hello 1" diff --git a/examples/vue3/js_call.py b/examples/vue3/js_call.py index c2a476c..ae08116 100644 --- a/examples/vue3/js_call.py +++ b/examples/vue3/js_call.py @@ -3,8 +3,7 @@ from trame.ui.html import DivLayout from trame_client.utils.testing import enable_testing -server = enable_testing(get_server(), "message") -server.client_type = "vue3" +server = enable_testing(get_server(client_type="vue3"), "message") state, ctrl = server.state, server.controller diff --git a/examples/vue3/life_cycle.py b/examples/vue3/life_cycle.py index 5d3ec3f..051a778 100644 --- a/examples/vue3/life_cycle.py +++ b/examples/vue3/life_cycle.py @@ -1,7 +1,6 @@ from trame.app import get_server -server = get_server() -server.client_type = "vue3" +server = get_server(client_type="vue3") ctrl = server.controller server.state.trame__template_main = "Life Cycle App" diff --git a/examples/vue3/namespace_nested.py b/examples/vue3/namespace_nested.py new file mode 100644 index 0000000..02ecab2 --- /dev/null +++ b/examples/vue3/namespace_nested.py @@ -0,0 +1,31 @@ +from trame.app import get_server + +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../../tests")) +from test_translator import NestedApp # noqa: E402 + +CLIENT_TYPE = "vue3" +ISOLATED = False + + +def main(): + root_server = get_server(client_type=CLIENT_TYPE) + + root_app = NestedApp(root_server, client_type=CLIENT_TYPE) + + if not ISOLATED: + root_app.child_a_app.server.translator.add_translation("f", "common_f") + root_app.child_b_app.server.translator.add_translation("f", "common_f") + + root_app.ui + + root_server.controller.on_server_ready.add( + lambda **kwargs: print("Root server ready") + ) + root_app.server.start() + + +if __name__ == "__main__": + main() diff --git a/examples/vue3/namespace_prefix.py b/examples/vue3/namespace_prefix.py new file mode 100644 index 0000000..a53997f --- /dev/null +++ b/examples/vue3/namespace_prefix.py @@ -0,0 +1,34 @@ +from trame.app import get_server + +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../../tests")) +from test_translator import BasicApp # noqa: E402 + +CLIENT_TYPE = "vue3" + + +def main(): + root_server = get_server(client_type=CLIENT_TYPE) + one_of_the_app = None + for app_name in ["main", "a", "b", "c"]: + child_server = root_server.create_child_server(prefix=f"{app_name}_") + app = BasicApp(child_server, app_name, CLIENT_TYPE) + app.ui + app.add() + app.ctrl.plus() + one_of_the_app = app + + # root_server.controller.on_server_ready.add(lambda **kwargs: print(json.dumps(root_server.state.to_dict(), indent=2))) + root_server.controller.on_server_ready.add( + lambda **kwargs: print("Root server ready") + ) + one_of_the_app.server.controller.on_server_ready.add( + lambda **kwargs: print("Sub server ready") + ) + one_of_the_app.server.start() + + +if __name__ == "__main__": + main() diff --git a/tests/requirements.txt b/tests/requirements.txt index 23c3773..23b18e5 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -3,4 +3,5 @@ seleniumbase pixelmatch Pillow pytest-xprocess -trame \ No newline at end of file +trame +trame-server>=2.13.1 \ No newline at end of file diff --git a/tests/test_translator.py b/tests/test_translator.py new file mode 100644 index 0000000..d9438e4 --- /dev/null +++ b/tests/test_translator.py @@ -0,0 +1,358 @@ +from trame.app import get_server +from trame.ui.html import DivLayout +from trame.widgets import html + +from trame_server.utils.namespace import Translator + +CLIENT_TYPE = "vue3" + + +class BasicApp: + def __init__(self, server=None, template_name=None, client_type=CLIENT_TYPE): + self.server = get_server(server, client_type=client_type) + self._ui = None + self.template_name = template_name + self.state.a = 1 + self.state["b"] = 2 + self.state.update( + { + "c": 3, + "d": 4, + "longer_name": "hello", + } + ) + self.state.setdefault("e", 5) + + self.ctrl.plus = self.add + self.ctrl.minus = self.remove + + def add(self): + self.state.f += 1 + + def remove(self): + self.state.f -= 1 + + def ui_content(self): + html.Input(type="range", v_model_number=("f", 10), min=-10, max=10, step=1) + html.Div( + "Bigger than 5 {{ longer_name }}", + v_show="Math.abs(f)>5 && longer_name.length", + ) + html.Div( + "Current value {{ Math.abs(f) + f }} <=> {{ f }} | {{ longer_name }}", + ) + + @property + def ui(self): + if self._ui is None: + with DivLayout(self.server, template_name=self.template_name) as layout: + self._ui = layout + self.ui_content() + + return self._ui + + @property + def nestable_ui(self): + if self._ui is None: + with html.Div(trame_server=self.server) as div: + self._ui = div + self.ui_content() + + return self._ui + + @property + def state(self): + return self.server.state + + @property + def ctrl(self): + return self.server.controller + + +class NestedApp: + def __init__(self, server=None, client_type=CLIENT_TYPE): + self.server = get_server(server, client_type=client_type) + self._ui = None + self.child_a_app = BasicApp( + self.server.create_child_server(prefix="child_a_"), client_type=client_type + ) + self.child_b_app = BasicApp( + self.server.create_child_server(prefix="child_b_"), client_type=client_type + ) + + @property + def ui(self): + if self._ui is None: + with DivLayout(self.server) as layout: + self._ui = layout + self.child_a_app.nestable_ui + self.child_b_app.nestable_ui + + return self._ui + + @property + def state(self): + return self.server.state + + @property + def ctrl(self): + return self.server.controller + + +def test_prefix(): + root_server = get_server("test_prefix", client_type=CLIENT_TYPE) + main_app = BasicApp(root_server) + child_a_app = BasicApp(root_server.create_child_server(prefix="child_a_")) + child_b_app = BasicApp(root_server.create_child_server(prefix="child_b_")) + + main_app.state.ready() + child_a_app.state.ready() + child_b_app.state.ready() + + main_app.ui + child_a_app.ui + child_b_app.ui + + global_state = { + k: v for k, v in main_app.state.to_dict().items() if not k.startswith("trame_") + } + expected = dict( + a=1, + b=2, + c=3, + d=4, + e=5, + f=10, + longer_name="hello", + child_a_a=1, + child_a_b=2, + child_a_c=3, + child_a_d=4, + child_a_e=5, + child_a_f=10, + child_a_longer_name="hello", + child_b_a=1, + child_b_b=2, + child_b_c=3, + child_b_d=4, + child_b_e=5, + child_b_f=10, + child_b_longer_name="hello", + ) + assert expected == global_state + + main_app.add() + assert main_app.state.f == 11 + main_app.ctrl.plus() + assert main_app.state.f == 12 + + # with child_a_app.state: + child_a_app.remove() + assert child_a_app.state.f == 9 + child_a_app.remove() + assert child_a_app.state.f == 8 + assert main_app.state.child_a_f == 8 + + # with child_b_app.state: + child_b_app.ctrl.minus() + assert child_b_app.state.f == 9 + child_b_app.remove() + assert child_b_app.state.f == 8 + assert main_app.state.child_b_f == 8 + + # Test main app output + expected_main = """
+ +
+Bigger than 5 {{ longer_name }} +
+
+Current value {{ Math.abs(f) + f }} <=> {{ f }} | {{ longer_name }} +
+
""" + assert main_app.ui.html == expected_main + + # Test child_a app output + expected_child_a = """
+ +
+Bigger than 5 {{ child_a_longer_name }} +
+
+Current value {{ Math.abs(child_a_f) + child_a_f }} <=> {{ child_a_f }} | {{ child_a_longer_name }} +
+
""" + assert child_a_app.ui.html == expected_child_a + + # Test child_b app output + expected_child_b = """
+ +
+Bigger than 5 {{ child_b_longer_name }} +
+
+Current value {{ Math.abs(child_b_f) + child_b_f }} <=> {{ child_b_f }} | {{ child_b_longer_name }} +
+
""" + assert child_b_app.ui.html == expected_child_b + + +def test_translation(): + root_server = get_server("test_translation", client_type=CLIENT_TYPE) + main_app = BasicApp(root_server) + + a_translator = Translator() + a_translator.add_translation("longer_name", "child_a_longer_name") + child_a_app = BasicApp(root_server.create_child_server(translator=a_translator)) + + b_translator = Translator() + b_translator.add_translation("longer_name", "child_b_longer_name") + child_b_app = BasicApp(root_server.create_child_server(translator=b_translator)) + + main_app.state.ready() + child_a_app.state.ready() + child_b_app.state.ready() + + main_app.ui + child_a_app.ui + child_b_app.ui + + global_state = { + k: v for k, v in main_app.state.to_dict().items() if not k.startswith("trame_") + } + expected = dict( + a=1, + b=2, + c=3, + d=4, + e=5, + f=10, + longer_name="hello", + child_a_longer_name="hello", + child_b_longer_name="hello", + ) + assert expected == global_state + + # f points to the same value in all 3 apps + assert main_app.state.f == 10 + assert child_a_app.state.f == 10 + assert child_a_app.state.f == 10 + main_app.add() + assert main_app.state.f == 11 + assert child_a_app.state.f == 11 + assert child_a_app.state.f == 11 + main_app.ctrl.plus() + assert main_app.state.f == 12 + assert child_a_app.state.f == 12 + assert child_a_app.state.f == 12 + + # with child_a_app.state: + child_a_app.remove() + assert main_app.state.f == 11 + assert child_a_app.state.f == 11 + assert child_a_app.state.f == 11 + child_a_app.ctrl.minus() + assert main_app.state.f == 10 + assert child_a_app.state.f == 10 + assert child_a_app.state.f == 10 + + # with child_b_app.state: + child_b_app.remove() + assert main_app.state.f == 9 + assert child_a_app.state.f == 9 + assert child_a_app.state.f == 9 + child_b_app.ctrl.minus() + assert main_app.state.f == 8 + assert child_a_app.state.f == 8 + assert child_a_app.state.f == 8 + + # Test main app output + expected_main = """
+ +
+Bigger than 5 {{ longer_name }} +
+
+Current value {{ Math.abs(f) + f }} <=> {{ f }} | {{ longer_name }} +
+
""" + assert main_app.ui.html == expected_main + + # Test child_a app output + expected_child_a = """
+ +
+Bigger than 5 {{ child_a_longer_name }} +
+
+Current value {{ Math.abs(f) + f }} <=> {{ f }} | {{ child_a_longer_name }} +
+
""" + assert child_a_app.ui.html == expected_child_a + + # Test child_b app output + expected_child_b = """
+ +
+Bigger than 5 {{ child_b_longer_name }} +
+
+Current value {{ Math.abs(f) + f }} <=> {{ f }} | {{ child_b_longer_name }} +
+
""" + assert child_b_app.ui.html == expected_child_b + + +def test_nested(): + root_server = get_server("test_nested", client_type=CLIENT_TYPE) + main_app = NestedApp(root_server) + + main_app.state.ready() + main_app.child_a_app.state.ready() + main_app.child_b_app.state.ready() + + main_app.ui + + global_state = { + k: v for k, v in main_app.state.to_dict().items() if not k.startswith("trame_") + } + expected = dict( + child_a_a=1, + child_a_b=2, + child_a_c=3, + child_a_d=4, + child_a_e=5, + child_a_f=10, + child_a_longer_name="hello", + child_b_a=1, + child_b_b=2, + child_b_c=3, + child_b_d=4, + child_b_e=5, + child_b_f=10, + child_b_longer_name="hello", + ) + assert expected == global_state + + # Test main app output + expected_main = """
+
+ +
+Bigger than 5 {{ child_a_longer_name }} +
+
+Current value {{ Math.abs(child_a_f) + child_a_f }} <=> {{ child_a_f }} | {{ child_a_longer_name }} +
+
+
+ +
+Bigger than 5 {{ child_b_longer_name }} +
+
+Current value {{ Math.abs(child_b_f) + child_b_f }} <=> {{ child_b_f }} | {{ child_b_longer_name }} +
+
+
""" + assert main_app.ui.html == expected_main diff --git a/trame_client/utils/web_module.py b/trame_client/utils/web_module.py index e59cc96..3f78bbc 100644 --- a/trame_client/utils/web_module.py +++ b/trame_client/utils/web_module.py @@ -28,6 +28,17 @@ def file_with_digest(file_path, digest=None, digest_size=40): return output_file +def is_relative_to(path, *other_paths): + # Convert the input path and the base path into absolute paths + abs_path = path.resolve() + abs_other_paths = [Path(other).resolve() for other in other_paths] + + # Check if the parts of the base paths are the beginning of the input path's parts + return all( + abs_path.parts[: len(base.parts)] == base.parts for base in abs_other_paths + ) + + class WebModule: def __init__(self, vue_use=None, digest_size=6): self._digest_size = digest_size @@ -47,7 +58,7 @@ def _add_file(self, file_path): for entry in self._serving_entries: fs_path, www_path = entry - if file_path.is_relative_to(fs_path): + if is_relative_to(file_path, fs_path): if self._digest_size > 0: file_path = file_with_digest( file_path, digest_size=self._digest_size diff --git a/trame_client/widgets/core.py b/trame_client/widgets/core.py index 2a5c3bf..325b43b 100644 --- a/trame_client/widgets/core.py +++ b/trame_client/widgets/core.py @@ -1,3 +1,4 @@ +import logging from ..utils.defaults import TrameDefault from ..utils.formatter import to_pretty_html @@ -62,6 +63,8 @@ "contextmenu", ] +logger = logging.getLogger(__name__) + def py2js_key(key): return key.replace("_", "-") @@ -407,6 +410,8 @@ def attrs(self, *names): if value is None: continue + logger.info("js_key = %s", js_key) + if ( AbstractElement._debug and js_key.startswith("v-") @@ -418,29 +423,41 @@ def attrs(self, *names): if isinstance(value, (tuple, list)): if len(value) > 1: - # handle vue3 syntax {name}.value / state.{name} - if value[0].startswith("state."): - self.server.state.setdefault(value[0][6:], value[1]) - elif value[0].endswith(".value"): - self.server.state.setdefault(value[0][:-6], value[1]) - elif isinstance(value[1], TrameDefault): + if isinstance(value[1], TrameDefault): value[1].set_defaults(self.server) else: self.server.state.setdefault(value[0], value[1]) + logger.info("before: %s = %s", js_key, value[0]) + translated_value = ( + self.server.state.translator.translate_js_expression( + self.server.state, value[0] + ) + ) + logger.info("after: %s = %s", js_key, translated_value) if js_key.startswith("v-"): - self._attributes[name] = f'{js_key}="{value[0]}"' + self._attributes[name] = f'{js_key}="{translated_value}"' elif js_key.startswith(":"): - self._attributes[name] = f'{js_key}="{value[0]}"' + self._attributes[name] = f'{js_key}="{translated_value}"' else: - self._attributes[name] = f':{js_key}="{value[0]}"' + self._attributes[name] = f':{js_key}="{translated_value}"' elif isinstance(value, bool): if value: self._attributes[name] = js_key else: self._attributes[name] = f':{js_key}="false"' elif isinstance(value, (str, int, float)): - self._attributes[name] = f'{js_key}="{value}"' + if js_key.startswith("v-") or js_key.startswith(":"): + logger.info("before: %s = %s", js_key, value) + translated_value = ( + self.server.state.translator.translate_js_expression( + self.server.state, value + ) + ) + logger.info("after: %s = %s", js_key, translated_value) + self._attributes[name] = f'{js_key}="{translated_value}"' + else: + self._attributes[name] = f'{js_key}="{value}"' else: print( f"Error: Don't know how to handle attribute name '{name}' with value '{value}' in {self.__class__}::{self._elem_name}" @@ -575,7 +592,12 @@ def html(self): out_buffer.append(f"<{self._elem_name} {self._attr_str()}>") for child in self._children: if isinstance(child, str): - out_buffer.append(child) + translated_value = ( + self.server.state.translator.translate_vue_templating( + self.server.state, child + ) + ) + out_buffer.append(translated_value) else: out_buffer.append(child.html) out_buffer.append(f"") @@ -583,7 +605,7 @@ def html(self): else: return f"<{self._elem_name} {self._attr_str()} />" except Exception as e: - print(e) + logger.error(e) return f"<{self._elem_name} html-error />" def __repr__(self):