From 5201858b7978797022898762905a0a057c8a2749 Mon Sep 17 00:00:00 2001 From: Kodjo Sossouvi Date: Sat, 10 Jan 2026 19:17:17 +0100 Subject: [PATCH] working on improving component loading --- src/myfasthtml/controls/DataGrid.py | 6 ++-- src/myfasthtml/controls/TabsManager.py | 9 +++++- src/myfasthtml/core/instances.py | 29 ++++++++++++++--- src/myfasthtml/core/utils.py | 24 ++++++++++++++ tests/core/test_utils.py | 45 +++++++++++++++++++++++++- 5 files changed, 104 insertions(+), 9 deletions(-) diff --git a/src/myfasthtml/controls/DataGrid.py b/src/myfasthtml/controls/DataGrid.py index 0a25d79..34afd20 100644 --- a/src/myfasthtml/controls/DataGrid.py +++ b/src/myfasthtml/controls/DataGrid.py @@ -12,6 +12,8 @@ from myfasthtml.core.constants import ColumnType, ROW_INDEX_ID, FooterAggregatio from myfasthtml.core.dbmanager import DbObject from myfasthtml.core.instances import MultipleInstance from myfasthtml.core.utils import make_safe_id +from myfasthtml.icons.fluent import checkbox_unchecked16_regular +from myfasthtml.icons.fluent_p2 import checkbox_checked16_regular class DatagridState(DbObject): @@ -149,7 +151,7 @@ class DataGrid(MultipleInstance): def mk_body_cell_content(self, col_pos, row_index, col_def: DataGridColumnState): def mk_bool(_value): - return Div(mk.icon(icon_checked if _value else icon_unchecked, can_select=False), + return Div(mk.icon(checkbox_checked16_regular if _value else checkbox_unchecked16_regular, can_select=False), cls="dt2-cell-content-checkbox") def mk_text(_value): @@ -301,7 +303,7 @@ class DataGrid(MultipleInstance): else: value = None - return Div(mk_ellipsis(value, cls="dt2-cell-content-number"), + return Div(mk.label(value, cls="dt2-cell-content-number"), data_col=col_def.col_id, style=f"width:{col_def.width}px;", cls="dt2-cell dt2-footer-cell", diff --git a/src/myfasthtml/controls/TabsManager.py b/src/myfasthtml/controls/TabsManager.py index 2eb82a9..2f7058a 100644 --- a/src/myfasthtml/controls/TabsManager.py +++ b/src/myfasthtml/controls/TabsManager.py @@ -115,11 +115,18 @@ class TabsManager(MultipleInstance): def _dynamic_get_content(self, tab_id): if tab_id not in self._state.tabs: return Div("Tab not found.") + tab_config = self._state.tabs[tab_id] if tab_config["component"] is None: return Div("Tab content does not support serialization.") + + res = InstancesManager.get(self._session, tab_config["component"][1], None) + if res is not None: + return res + try: - return InstancesManager.get(self._session, tab_config["component"][1]) + logger.debug(f"Component not created yet. Need to manually create it.") + return InstancesManager.dynamic_get(self._session, tab_config["component_parent"], tab_config["component"]) except Exception as e: logger.error(f"Error while retrieving tab content: {e}") return Div("Failed to retrieve tab content.") diff --git a/src/myfasthtml/core/instances.py b/src/myfasthtml/core/instances.py index 0bc550c..a3f4e95 100644 --- a/src/myfasthtml/core/instances.py +++ b/src/myfasthtml/core/instances.py @@ -3,7 +3,7 @@ import uuid from typing import Optional from myfasthtml.controls.helpers import Ids -from myfasthtml.core.utils import pascal_to_snake +from myfasthtml.core.utils import pascal_to_snake, get_class, snake_to_pascal logger = logging.getLogger("InstancesManager") @@ -183,16 +183,22 @@ class InstancesManager: return instance @staticmethod - def get(session: dict, instance_id: str): + def get(session: dict, instance_id: str, default="**__no_default__**"): """ Get or create an instance of the given type (from its id) :param session: :param instance_id: + :param default: :return: """ - session_id = InstancesManager.get_session_id(session) - key = (session_id, instance_id) - return InstancesManager.instances[key] + try: + session_id = InstancesManager.get_session_id(session) + key = (session_id, instance_id) + return InstancesManager.instances[key] + except KeyError: + if default != "**__non__**": + return default + raise @staticmethod def get_by_type(session: dict, cls: type): @@ -202,6 +208,19 @@ class InstancesManager: assert len(res) > 0, f"No instance of type {cls.__name__} found" return res[0] + @staticmethod + def dynamic_get(session, component_parent: tuple, component: tuple): + parent_type, parent_id = component_parent + component_type, component_id = component + + # parent should always exist + parent = InstancesManager.get(session, parent_id) + + real_component_type = snake_to_pascal(component_type.removeprefix("mf-")) + component_full_type = f"myfasthtml.controls.{real_component_type}.{real_component_type}" + cls = get_class(component_full_type) + return cls(parent, _id=component_id) + @staticmethod def get_session_id(session): if isinstance(session, str): diff --git a/src/myfasthtml/core/utils.py b/src/myfasthtml/core/utils.py index cc52526..0efa89f 100644 --- a/src/myfasthtml/core/utils.py +++ b/src/myfasthtml/core/utils.py @@ -1,3 +1,4 @@ +import importlib import logging import re @@ -317,6 +318,29 @@ def make_safe_id(s: str | None): res = re.sub('-', '_', make_html_id(s)) # replace '-' by '_' return res.lower() # no uppercase + +def get_class(qualified_class_name: str): + """ + Dynamically loads and returns a class type from its fully qualified name. + Note that the class is not instantiated. + + :param qualified_class_name: Fully qualified name of the class (e.g., 'some.module.ClassName'). + :return: The class object. + :raises ImportError: If the module cannot be imported. + :raises AttributeError: If the class cannot be resolved in the module. + """ + module_name, class_name = qualified_class_name.rsplit(".", 1) + + try: + module = importlib.import_module(module_name) + except ModuleNotFoundError as e: + raise ImportError(f"Could not import module '{module_name}' for '{qualified_class_name}': {e}") + + if not hasattr(module, class_name): + raise AttributeError(f"Component '{class_name}' not found in '{module.__name__}'.") + + return getattr(module, class_name) + @utils_rt(Routes.Commands) def post(session, c_id: str, client_response: dict = None): """ diff --git a/tests/core/test_utils.py b/tests/core/test_utils.py index ac5db08..bce1699 100644 --- a/tests/core/test_utils.py +++ b/tests/core/test_utils.py @@ -1,6 +1,6 @@ import pytest -from myfasthtml.core.utils import flatten, make_html_id +from myfasthtml.core.utils import flatten, make_html_id, pascal_to_snake, snake_to_pascal @pytest.mark.parametrize("input_args,expected,test_description", [ @@ -64,3 +64,46 @@ def test_i_can_flatten(input_args, expected, test_description): ]) def test_i_can_have_valid_html_id(string, expected): assert make_html_id(string) == expected + + +@pytest.mark.parametrize("input_str, expected, test_description", [ + ("MyClass", "my_class", "simple PascalCase"), + ("myVariable", "my_variable", "camelCase"), + ("HTTPServer", "http_server", "short uppercase sequence"), + ("XMLHttpRequest", "xml_http_request", "long uppercase sequence"), + ("A", "a", "single letter"), + ("already_snake", "already_snake", "already snake_case"), + ("MyClass123", "my_class123", "with numbers"), + ("MyLongClassName", "my_long_class_name", "long class name"), + (" MyClass ", "my_class", "with spaces to trim"), + ("iPhone", "i_phone", "starts lowercase then uppercase"), + (None, None, "None input"), + ("", "", "empty string"), + (" ", "", "only spaces"), +]) +def test_i_can_convert_pascal_to_snake(input_str, expected, test_description): + """Test that pascal_to_snake correctly converts PascalCase/camelCase to snake_case.""" + result = pascal_to_snake(input_str) + assert result == expected, f"Failed for test case: {test_description}" + + +@pytest.mark.parametrize("input_str, expected, test_description", [ + ("my_class", "MyClass", "simple snake_case"), + ("my_long_class_name", "MyLongClassName", "long class name"), + ("a", "A", "single letter"), + ("myclass", "Myclass", "no underscore"), + (" my_class ", "MyClass", "with spaces to trim"), + ("my__class", "MyClass", "multiple consecutive underscores"), + ("_my_class", "MyClass", "starts with underscore"), + ("my_class_", "MyClass", "ends with underscore"), + ("_my_class_", "MyClass", "starts and ends with underscore"), + ("my_class_123", "MyClass123", "with numbers"), + (None, None, "None input"), + ("", "", "empty string"), + (" ", "", "only spaces"), + ("___", "", "only underscores"), +]) +def test_i_can_convert_snake_to_pascal(input_str, expected, test_description): + """Test that snake_to_pascal correctly converts snake_case to PascalCase.""" + result = snake_to_pascal(input_str) + assert result == expected, f"Failed for test case: {test_description}"