From e286b60348fc0a56aa6807384651a944293f3e8e Mon Sep 17 00:00:00 2001 From: Kodjo Sossouvi Date: Sun, 16 Nov 2025 17:46:44 +0100 Subject: [PATCH] Parent is now mandatory when creating a new BaseInstance class --- src/app.py | 6 +++--- src/myfasthtml/controls/Boundaries.py | 2 +- src/myfasthtml/controls/Layout.py | 4 ++-- src/myfasthtml/controls/Search.py | 6 +++--- src/myfasthtml/controls/TabsManager.py | 12 ++++++------ src/myfasthtml/controls/UserProfile.py | 4 ++-- src/myfasthtml/controls/VisNetwork.py | 4 ++-- src/myfasthtml/controls/helpers.py | 1 + src/myfasthtml/core/AuthProxy.py | 4 ++-- src/myfasthtml/core/dbmanager.py | 4 ++-- src/myfasthtml/core/instances.py | 26 ++++++++++++++++--------- src/myfasthtml/core/instances_helper.py | 5 +++-- tests/controls/conftest.py | 7 +++++++ tests/controls/test_tabsmanager.py | 8 ++++---- 14 files changed, 55 insertions(+), 38 deletions(-) diff --git a/src/app.py b/src/app.py index 8573dd9..45637ac 100644 --- a/src/app.py +++ b/src/app.py @@ -7,7 +7,7 @@ from myfasthtml.controls.Layout import Layout from myfasthtml.controls.TabsManager import TabsManager from myfasthtml.controls.helpers import Ids, mk from myfasthtml.core.commands import Command -from myfasthtml.core.instances import InstancesManager +from myfasthtml.core.instances import InstancesManager, RootInstance from myfasthtml.myfastapp import create_app with open('logging.yaml', 'r') as f: @@ -27,10 +27,10 @@ app, rt = create_app(protect_routes=True, @rt("/") def index(session): - layout = InstancesManager.get(session, Ids.Layout, Layout, "Testing Layout") + layout = InstancesManager.get(session, Ids.Layout, Layout, RootInstance, "Testing Layout") layout.set_footer("Goodbye World") - tabs_manager = TabsManager(session, _id="main") + tabs_manager = TabsManager(layout, _id=f"{Ids.TabsManager}-main") btn_show_right_drawer = mk.button("show", command=Command("ShowRightDrawer", diff --git a/src/myfasthtml/controls/Boundaries.py b/src/myfasthtml/controls/Boundaries.py index 1aead26..c7624c6 100644 --- a/src/myfasthtml/controls/Boundaries.py +++ b/src/myfasthtml/controls/Boundaries.py @@ -27,7 +27,7 @@ class Boundaries(SingleInstance): """ def __init__(self, session, owner, container_id: str = None, on_resize=None): - super().__init__(session, Ids.Boundaries) + super().__init__(session, Ids.Boundaries, owner) self._owner = owner self._container_id = container_id or owner.get_id() self._on_resize = on_resize diff --git a/src/myfasthtml/controls/Layout.py b/src/myfasthtml/controls/Layout.py index 2b456af..4488711 100644 --- a/src/myfasthtml/controls/Layout.py +++ b/src/myfasthtml/controls/Layout.py @@ -84,7 +84,7 @@ class Layout(SingleInstance): def get_content(self): return self._content - def __init__(self, session, app_name): + def __init__(self, session, app_name, parent=None): """ Initialize the Layout component. @@ -93,7 +93,7 @@ class Layout(SingleInstance): left_drawer (bool): Enable left drawer. Default is True. right_drawer (bool): Enable right drawer. Default is True. """ - super().__init__(session, Ids.Layout) + super().__init__(session, Ids.Layout, parent) self.app_name = app_name # Content storage diff --git a/src/myfasthtml/controls/Search.py b/src/myfasthtml/controls/Search.py index 8fba746..6f54b4c 100644 --- a/src/myfasthtml/controls/Search.py +++ b/src/myfasthtml/controls/Search.py @@ -6,7 +6,7 @@ from fasthtml.components import * from myfasthtml.controls.BaseCommands import BaseCommands from myfasthtml.controls.helpers import Ids, mk from myfasthtml.core.commands import Command -from myfasthtml.core.instances import MultipleInstance +from myfasthtml.core.instances import MultipleInstance, BaseInstance from myfasthtml.core.matching_utils import subsequence_matching, fuzzy_matching logger = logging.getLogger("Search") @@ -22,7 +22,7 @@ class Commands(BaseCommands): class Search(MultipleInstance): def __init__(self, - session, + parent: BaseInstance, _id=None, items_names=None, # what is the name of the items to filter items=None, # first set of items to filter @@ -42,7 +42,7 @@ class Search(MultipleInstance): function that returns the item as is. :param template: Callable function to render the filtered items. Defaults to a Div rendering function. """ - super().__init__(session, Ids.Search, _id=_id) + super().__init__(Ids.Search, parent, _id=_id) self.items_names = items_names or '' self.items = items or [] self.filtered = self.items.copy() diff --git a/src/myfasthtml/controls/TabsManager.py b/src/myfasthtml/controls/TabsManager.py index 8915575..696c70c 100644 --- a/src/myfasthtml/controls/TabsManager.py +++ b/src/myfasthtml/controls/TabsManager.py @@ -77,12 +77,12 @@ class Commands(BaseCommands): class TabsManager(MultipleInstance): _tab_count = 0 - def __init__(self, session, _id=None): - super().__init__(session, Ids.TabsManager, _id=_id) + def __init__(self, parent, _id=None): + super().__init__(Ids.TabsManager, parent, _id=_id) self._state = TabsManagerState(self) self.commands = Commands(self) self._boundaries = Boundaries() - self._search = Search(self._session, + self._search = Search(self, items=self._get_tab_list(), get_attr=lambda x: x["label"], template=self._mk_tab_button) @@ -102,7 +102,7 @@ class TabsManager(MultipleInstance): tab_config = self._state.tabs[tab_id] if tab_config["component_type"] is None: return None - return InstancesHelper.dynamic_get(self._session, tab_config["component_type"], tab_config["component_id"]) + return InstancesHelper.dynamic_get(self, tab_config["component_type"], tab_config["component_id"]) @staticmethod def _get_tab_count(): @@ -114,7 +114,7 @@ class TabsManager(MultipleInstance): logger.debug(f"on_new_tab {label=}, {component=}, {auto_increment=}") if auto_increment: label = f"{label}_{self._get_tab_count()}" - component = component or VisNetwork(self._session, nodes=vis_nodes, edges=vis_edges) + component = component or VisNetwork(self, nodes=vis_nodes, edges=vis_edges) tab_id = self.add_tab(label, component) return ( self._mk_tabs_controller(), @@ -328,7 +328,7 @@ class TabsManager(MultipleInstance): tab_content = self._mk_tab_content(active_tab, content) self._state._tabs_content[active_tab] = tab_content else: - tab_content = self._mk_tab_content("", None) + tab_content = self._mk_tab_content(None, None) return Div( tab_content, diff --git a/src/myfasthtml/controls/UserProfile.py b/src/myfasthtml/controls/UserProfile.py index e2182ef..763f19b 100644 --- a/src/myfasthtml/controls/UserProfile.py +++ b/src/myfasthtml/controls/UserProfile.py @@ -35,8 +35,8 @@ class Commands(BaseCommands): class UserProfile(SingleInstance): - def __init__(self, session): - super().__init__(session, Ids.UserProfile) + def __init__(self, session, parent=None): + super().__init__(session, Ids.UserProfile, parent) self._state = UserProfileState(self) self._commands = Commands(self) diff --git a/src/myfasthtml/controls/VisNetwork.py b/src/myfasthtml/controls/VisNetwork.py index 9da5a91..51a8418 100644 --- a/src/myfasthtml/controls/VisNetwork.py +++ b/src/myfasthtml/controls/VisNetwork.py @@ -28,8 +28,8 @@ class VisNetworkState(DbObject): class VisNetwork(MultipleInstance): - def __init__(self, session, _id=None, nodes=None, edges=None, options=None): - super().__init__(session, Ids.VisNetwork, _id=_id) + def __init__(self, parent, _id=None, nodes=None, edges=None, options=None): + super().__init__(Ids.VisNetwork, parent, _id=_id) logger.debug(f"VisNetwork created with id: {self._id}") self._state = VisNetworkState(self) diff --git a/src/myfasthtml/controls/helpers.py b/src/myfasthtml/controls/helpers.py index cebab03..f0919d7 100644 --- a/src/myfasthtml/controls/helpers.py +++ b/src/myfasthtml/controls/helpers.py @@ -11,6 +11,7 @@ class Ids: Boundaries = "mf-boundaries" DbManager = "mf-dbmanager" Layout = "mf-layout" + Root = "mf-root" Search = "mf-search" TabsManager = "mf-tabs-manager" UserProfile = "mf-user-profile" diff --git a/src/myfasthtml/core/AuthProxy.py b/src/myfasthtml/core/AuthProxy.py index fa7c4dd..e593690 100644 --- a/src/myfasthtml/core/AuthProxy.py +++ b/src/myfasthtml/core/AuthProxy.py @@ -1,11 +1,11 @@ from myfasthtml.auth.utils import login_user, save_user_info, register_user from myfasthtml.controls.helpers import Ids -from myfasthtml.core.instances import special_session, UniqueInstance +from myfasthtml.core.instances import UniqueInstance, RootInstance class AuthProxy(UniqueInstance): def __init__(self, base_url: str = None): - super().__init__(special_session, Ids.AuthProxy) + super().__init__(Ids.AuthProxy, RootInstance) self._base_url = base_url def login_user(self, email: str, password: str): diff --git a/src/myfasthtml/core/dbmanager.py b/src/myfasthtml/core/dbmanager.py index e44c863..1a922d0 100644 --- a/src/myfasthtml/core/dbmanager.py +++ b/src/myfasthtml/core/dbmanager.py @@ -9,8 +9,8 @@ from myfasthtml.core.utils import retrieve_user_info class DbManager(SingleInstance): - def __init__(self, session, root=".myFastHtmlDb", auto_register: bool = True): - super().__init__(session, Ids.DbManager, auto_register=auto_register) + def __init__(self, session, parent=None, root=".myFastHtmlDb", auto_register: bool = True): + super().__init__(session, Ids.DbManager, parent, auto_register=auto_register) self.db = DbEngine(root=root) def save(self, entry, obj): diff --git a/src/myfasthtml/core/instances.py b/src/myfasthtml/core/instances.py index 9fa3e3a..f022c7c 100644 --- a/src/myfasthtml/core/instances.py +++ b/src/myfasthtml/core/instances.py @@ -1,4 +1,5 @@ import uuid +from typing import Self from myfasthtml.controls.helpers import Ids @@ -17,10 +18,11 @@ class BaseInstance: Base class for all instances (manageable by InstancesManager) """ - def __init__(self, session: dict, prefix: str, _id: str, auto_register: bool = True): + def __init__(self, session: dict, prefix: str, _id: str, parent: Self, auto_register: bool = True): self._session = session self._id = _id self._prefix = prefix + self._parent = parent if auto_register: InstancesManager.register(session, self) @@ -39,8 +41,8 @@ class SingleInstance(BaseInstance): Base class for instances that can only have one instance at a time. """ - def __init__(self, session: dict, prefix: str, auto_register: bool = True): - super().__init__(session, prefix, prefix, auto_register) + def __init__(self, session: dict, prefix: str, parent, auto_register: bool = True): + super().__init__(session, prefix, prefix, parent, auto_register) class UniqueInstance(BaseInstance): @@ -49,8 +51,8 @@ class UniqueInstance(BaseInstance): Does not throw exception if the instance already exists, it simply overwrites it. """ - def __init__(self, session: dict, prefix: str, auto_register: bool = True): - super().__init__(session, prefix, prefix, auto_register) + def __init__(self, prefix: str, parent: BaseInstance, auto_register: bool = True): + super().__init__(parent.get_session(), prefix, prefix, parent, auto_register) self._prefix = prefix @@ -59,8 +61,8 @@ class MultipleInstance(BaseInstance): Base class for instances that can have multiple instances at a time. """ - def __init__(self, session: dict, prefix: str, auto_register: bool = True, _id=None): - super().__init__(session, prefix, _id or f"{prefix}-{str(uuid.uuid4())}", auto_register) + def __init__(self, prefix: str, parent: BaseInstance, auto_register: bool = True, _id=None): + super().__init__(parent.get_session(), prefix, _id or f"{prefix}-{str(uuid.uuid4())}", parent, auto_register) self._prefix = prefix @@ -84,12 +86,13 @@ class InstancesManager: return instance @staticmethod - def get(session: dict, instance_id: str, instance_type: type = None, *args, **kwargs): + def get(session: dict, instance_id: str, instance_type: type = None, parent: BaseInstance = None, *args, **kwargs): """ Get or create an instance of the given type (from its id) :param session: :param instance_id: :param instance_type: + :param parent: :param args: :param kwargs: :return: @@ -100,7 +103,9 @@ class InstancesManager: return InstancesManager.instances[key] except KeyError: if instance_type: - return instance_type(session, *args, **kwargs) # it will be automatically registered + if not issubclass(instance_type, SingleInstance): + assert parent is not None, "Parent instance must be provided if not SingleInstance" + return instance_type(session, parent=parent, *args, **kwargs) # it will be automatically registered else: raise @@ -119,3 +124,6 @@ class InstancesManager: @staticmethod def reset(): return InstancesManager.instances.clear() + + +RootInstance = SingleInstance(special_session, Ids.Root, None) diff --git a/src/myfasthtml/core/instances_helper.py b/src/myfasthtml/core/instances_helper.py index e14d87f..533e793 100644 --- a/src/myfasthtml/core/instances_helper.py +++ b/src/myfasthtml/core/instances_helper.py @@ -1,11 +1,12 @@ from myfasthtml.controls.VisNetwork import VisNetwork from myfasthtml.controls.helpers import Ids +from myfasthtml.core.instances import BaseInstance class InstancesHelper: @staticmethod - def dynamic_get(session, component_type: str, instance_id: str): + def dynamic_get(parent: BaseInstance, component_type: str, instance_id: str): if component_type == Ids.VisNetwork: - return VisNetwork(session, _id=instance_id) + return VisNetwork(parent, _id=instance_id) return None diff --git a/tests/controls/conftest.py b/tests/controls/conftest.py index fdc9e33..0af3e1e 100644 --- a/tests/controls/conftest.py +++ b/tests/controls/conftest.py @@ -1,5 +1,7 @@ import pytest +from myfasthtml.core.instances import SingleInstance + @pytest.fixture(scope="session") def session(): @@ -14,3 +16,8 @@ def session(): 'updated_at': '2025-11-10T15:52:59.006213' } } + + +@pytest.fixture(scope="session") +def root_instance(session): + return SingleInstance(session, "TestRoot", None) diff --git a/tests/controls/test_tabsmanager.py b/tests/controls/test_tabsmanager.py index aae41ed..871df8b 100644 --- a/tests/controls/test_tabsmanager.py +++ b/tests/controls/test_tabsmanager.py @@ -4,13 +4,13 @@ from fasthtml.xtend import Script from myfasthtml.controls.TabsManager import TabsManager from myfasthtml.core.instances import InstancesManager -from myfasthtml.test.matcher import matches, NoChildren, StartsWith +from myfasthtml.test.matcher import matches, NoChildren from .conftest import session @pytest.fixture() -def tabs_manager(session): - yield TabsManager(session) +def tabs_manager(root_instance): + yield TabsManager(root_instance) InstancesManager.reset() @@ -113,7 +113,7 @@ class TestTabsManagerRender: id=f"{tabs_manager.get_id()}-header-wrapper" ), Div( - Div(id=StartsWith(tabs_manager.get_id())), + Div("Content 3"), # active tab content # Lasy loading for the other contents id=f"{tabs_manager.get_id()}-content-wrapper" ),