Parent is now mandatory when creating a new BaseInstance class

This commit is contained in:
2025-11-16 17:46:44 +01:00
parent edcd3ae1a8
commit e286b60348
14 changed files with 55 additions and 38 deletions

View File

@@ -7,7 +7,7 @@ from myfasthtml.controls.Layout import Layout
from myfasthtml.controls.TabsManager import TabsManager from myfasthtml.controls.TabsManager import TabsManager
from myfasthtml.controls.helpers import Ids, mk from myfasthtml.controls.helpers import Ids, mk
from myfasthtml.core.commands import Command 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 from myfasthtml.myfastapp import create_app
with open('logging.yaml', 'r') as f: with open('logging.yaml', 'r') as f:
@@ -27,10 +27,10 @@ app, rt = create_app(protect_routes=True,
@rt("/") @rt("/")
def index(session): 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") 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", btn_show_right_drawer = mk.button("show",
command=Command("ShowRightDrawer", command=Command("ShowRightDrawer",

View File

@@ -27,7 +27,7 @@ class Boundaries(SingleInstance):
""" """
def __init__(self, session, owner, container_id: str = None, on_resize=None): 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._owner = owner
self._container_id = container_id or owner.get_id() self._container_id = container_id or owner.get_id()
self._on_resize = on_resize self._on_resize = on_resize

View File

@@ -84,7 +84,7 @@ class Layout(SingleInstance):
def get_content(self): def get_content(self):
return self._content return self._content
def __init__(self, session, app_name): def __init__(self, session, app_name, parent=None):
""" """
Initialize the Layout component. Initialize the Layout component.
@@ -93,7 +93,7 @@ class Layout(SingleInstance):
left_drawer (bool): Enable left drawer. Default is True. left_drawer (bool): Enable left drawer. Default is True.
right_drawer (bool): Enable right 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 self.app_name = app_name
# Content storage # Content storage

View File

@@ -6,7 +6,7 @@ from fasthtml.components import *
from myfasthtml.controls.BaseCommands import BaseCommands from myfasthtml.controls.BaseCommands import BaseCommands
from myfasthtml.controls.helpers import Ids, mk from myfasthtml.controls.helpers import Ids, mk
from myfasthtml.core.commands import Command 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 from myfasthtml.core.matching_utils import subsequence_matching, fuzzy_matching
logger = logging.getLogger("Search") logger = logging.getLogger("Search")
@@ -22,7 +22,7 @@ class Commands(BaseCommands):
class Search(MultipleInstance): class Search(MultipleInstance):
def __init__(self, def __init__(self,
session, parent: BaseInstance,
_id=None, _id=None,
items_names=None, # what is the name of the items to filter items_names=None, # what is the name of the items to filter
items=None, # first set of 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. function that returns the item as is.
:param template: Callable function to render the filtered items. Defaults to a Div rendering function. :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_names = items_names or ''
self.items = items or [] self.items = items or []
self.filtered = self.items.copy() self.filtered = self.items.copy()

View File

@@ -77,12 +77,12 @@ class Commands(BaseCommands):
class TabsManager(MultipleInstance): class TabsManager(MultipleInstance):
_tab_count = 0 _tab_count = 0
def __init__(self, session, _id=None): def __init__(self, parent, _id=None):
super().__init__(session, Ids.TabsManager, _id=_id) super().__init__(Ids.TabsManager, parent, _id=_id)
self._state = TabsManagerState(self) self._state = TabsManagerState(self)
self.commands = Commands(self) self.commands = Commands(self)
self._boundaries = Boundaries() self._boundaries = Boundaries()
self._search = Search(self._session, self._search = Search(self,
items=self._get_tab_list(), items=self._get_tab_list(),
get_attr=lambda x: x["label"], get_attr=lambda x: x["label"],
template=self._mk_tab_button) template=self._mk_tab_button)
@@ -102,7 +102,7 @@ class TabsManager(MultipleInstance):
tab_config = self._state.tabs[tab_id] tab_config = self._state.tabs[tab_id]
if tab_config["component_type"] is None: if tab_config["component_type"] is None:
return 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 @staticmethod
def _get_tab_count(): def _get_tab_count():
@@ -114,7 +114,7 @@ class TabsManager(MultipleInstance):
logger.debug(f"on_new_tab {label=}, {component=}, {auto_increment=}") logger.debug(f"on_new_tab {label=}, {component=}, {auto_increment=}")
if auto_increment: if auto_increment:
label = f"{label}_{self._get_tab_count()}" 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) tab_id = self.add_tab(label, component)
return ( return (
self._mk_tabs_controller(), self._mk_tabs_controller(),
@@ -328,7 +328,7 @@ class TabsManager(MultipleInstance):
tab_content = self._mk_tab_content(active_tab, content) tab_content = self._mk_tab_content(active_tab, content)
self._state._tabs_content[active_tab] = tab_content self._state._tabs_content[active_tab] = tab_content
else: else:
tab_content = self._mk_tab_content("", None) tab_content = self._mk_tab_content(None, None)
return Div( return Div(
tab_content, tab_content,

View File

@@ -35,8 +35,8 @@ class Commands(BaseCommands):
class UserProfile(SingleInstance): class UserProfile(SingleInstance):
def __init__(self, session): def __init__(self, session, parent=None):
super().__init__(session, Ids.UserProfile) super().__init__(session, Ids.UserProfile, parent)
self._state = UserProfileState(self) self._state = UserProfileState(self)
self._commands = Commands(self) self._commands = Commands(self)

View File

@@ -28,8 +28,8 @@ class VisNetworkState(DbObject):
class VisNetwork(MultipleInstance): class VisNetwork(MultipleInstance):
def __init__(self, session, _id=None, nodes=None, edges=None, options=None): def __init__(self, parent, _id=None, nodes=None, edges=None, options=None):
super().__init__(session, Ids.VisNetwork, _id=_id) super().__init__(Ids.VisNetwork, parent, _id=_id)
logger.debug(f"VisNetwork created with id: {self._id}") logger.debug(f"VisNetwork created with id: {self._id}")
self._state = VisNetworkState(self) self._state = VisNetworkState(self)

View File

@@ -11,6 +11,7 @@ class Ids:
Boundaries = "mf-boundaries" Boundaries = "mf-boundaries"
DbManager = "mf-dbmanager" DbManager = "mf-dbmanager"
Layout = "mf-layout" Layout = "mf-layout"
Root = "mf-root"
Search = "mf-search" Search = "mf-search"
TabsManager = "mf-tabs-manager" TabsManager = "mf-tabs-manager"
UserProfile = "mf-user-profile" UserProfile = "mf-user-profile"

View File

@@ -1,11 +1,11 @@
from myfasthtml.auth.utils import login_user, save_user_info, register_user from myfasthtml.auth.utils import login_user, save_user_info, register_user
from myfasthtml.controls.helpers import Ids from myfasthtml.controls.helpers import Ids
from myfasthtml.core.instances import special_session, UniqueInstance from myfasthtml.core.instances import UniqueInstance, RootInstance
class AuthProxy(UniqueInstance): class AuthProxy(UniqueInstance):
def __init__(self, base_url: str = None): def __init__(self, base_url: str = None):
super().__init__(special_session, Ids.AuthProxy) super().__init__(Ids.AuthProxy, RootInstance)
self._base_url = base_url self._base_url = base_url
def login_user(self, email: str, password: str): def login_user(self, email: str, password: str):

View File

@@ -9,8 +9,8 @@ from myfasthtml.core.utils import retrieve_user_info
class DbManager(SingleInstance): class DbManager(SingleInstance):
def __init__(self, session, root=".myFastHtmlDb", auto_register: bool = True): def __init__(self, session, parent=None, root=".myFastHtmlDb", auto_register: bool = True):
super().__init__(session, Ids.DbManager, auto_register=auto_register) super().__init__(session, Ids.DbManager, parent, auto_register=auto_register)
self.db = DbEngine(root=root) self.db = DbEngine(root=root)
def save(self, entry, obj): def save(self, entry, obj):

View File

@@ -1,4 +1,5 @@
import uuid import uuid
from typing import Self
from myfasthtml.controls.helpers import Ids from myfasthtml.controls.helpers import Ids
@@ -17,10 +18,11 @@ class BaseInstance:
Base class for all instances (manageable by InstancesManager) 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._session = session
self._id = _id self._id = _id
self._prefix = prefix self._prefix = prefix
self._parent = parent
if auto_register: if auto_register:
InstancesManager.register(session, self) InstancesManager.register(session, self)
@@ -39,8 +41,8 @@ class SingleInstance(BaseInstance):
Base class for instances that can only have one instance at a time. Base class for instances that can only have one instance at a time.
""" """
def __init__(self, session: dict, prefix: str, auto_register: bool = True): def __init__(self, session: dict, prefix: str, parent, auto_register: bool = True):
super().__init__(session, prefix, prefix, auto_register) super().__init__(session, prefix, prefix, parent, auto_register)
class UniqueInstance(BaseInstance): class UniqueInstance(BaseInstance):
@@ -49,8 +51,8 @@ class UniqueInstance(BaseInstance):
Does not throw exception if the instance already exists, it simply overwrites it. Does not throw exception if the instance already exists, it simply overwrites it.
""" """
def __init__(self, session: dict, prefix: str, auto_register: bool = True): def __init__(self, prefix: str, parent: BaseInstance, auto_register: bool = True):
super().__init__(session, prefix, prefix, auto_register) super().__init__(parent.get_session(), prefix, prefix, parent, auto_register)
self._prefix = prefix self._prefix = prefix
@@ -59,8 +61,8 @@ class MultipleInstance(BaseInstance):
Base class for instances that can have multiple instances at a time. 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): def __init__(self, prefix: str, parent: BaseInstance, auto_register: bool = True, _id=None):
super().__init__(session, prefix, _id or f"{prefix}-{str(uuid.uuid4())}", auto_register) super().__init__(parent.get_session(), prefix, _id or f"{prefix}-{str(uuid.uuid4())}", parent, auto_register)
self._prefix = prefix self._prefix = prefix
@@ -84,12 +86,13 @@ class InstancesManager:
return instance return instance
@staticmethod @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) Get or create an instance of the given type (from its id)
:param session: :param session:
:param instance_id: :param instance_id:
:param instance_type: :param instance_type:
:param parent:
:param args: :param args:
:param kwargs: :param kwargs:
:return: :return:
@@ -100,7 +103,9 @@ class InstancesManager:
return InstancesManager.instances[key] return InstancesManager.instances[key]
except KeyError: except KeyError:
if instance_type: 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: else:
raise raise
@@ -119,3 +124,6 @@ class InstancesManager:
@staticmethod @staticmethod
def reset(): def reset():
return InstancesManager.instances.clear() return InstancesManager.instances.clear()
RootInstance = SingleInstance(special_session, Ids.Root, None)

View File

@@ -1,11 +1,12 @@
from myfasthtml.controls.VisNetwork import VisNetwork from myfasthtml.controls.VisNetwork import VisNetwork
from myfasthtml.controls.helpers import Ids from myfasthtml.controls.helpers import Ids
from myfasthtml.core.instances import BaseInstance
class InstancesHelper: class InstancesHelper:
@staticmethod @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: if component_type == Ids.VisNetwork:
return VisNetwork(session, _id=instance_id) return VisNetwork(parent, _id=instance_id)
return None return None

View File

@@ -1,5 +1,7 @@
import pytest import pytest
from myfasthtml.core.instances import SingleInstance
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def session(): def session():
@@ -14,3 +16,8 @@ def session():
'updated_at': '2025-11-10T15:52:59.006213' 'updated_at': '2025-11-10T15:52:59.006213'
} }
} }
@pytest.fixture(scope="session")
def root_instance(session):
return SingleInstance(session, "TestRoot", None)

View File

@@ -4,13 +4,13 @@ from fasthtml.xtend import Script
from myfasthtml.controls.TabsManager import TabsManager from myfasthtml.controls.TabsManager import TabsManager
from myfasthtml.core.instances import InstancesManager 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 from .conftest import session
@pytest.fixture() @pytest.fixture()
def tabs_manager(session): def tabs_manager(root_instance):
yield TabsManager(session) yield TabsManager(root_instance)
InstancesManager.reset() InstancesManager.reset()
@@ -113,7 +113,7 @@ class TestTabsManagerRender:
id=f"{tabs_manager.get_id()}-header-wrapper" id=f"{tabs_manager.get_id()}-header-wrapper"
), ),
Div( Div(
Div(id=StartsWith(tabs_manager.get_id())), Div("Content 3"), # active tab content
# Lasy loading for the other contents # Lasy loading for the other contents
id=f"{tabs_manager.get_id()}-content-wrapper" id=f"{tabs_manager.get_id()}-content-wrapper"
), ),