2 Commits

Author SHA1 Message Date
a6ab4b2a68 Added LambdaCommand 2025-11-24 21:31:23 +01:00
84c63f0c5a Fixed memory leak for instances 2025-11-24 21:00:04 +01:00
10 changed files with 71 additions and 28 deletions

View File

@@ -36,7 +36,7 @@ def index(session):
layout = Layout(session_instance, "Testing Layout") layout = Layout(session_instance, "Testing Layout")
layout.set_footer("Goodbye World") layout.set_footer("Goodbye World")
tabs_manager = TabsManager(layout, _id=f"{TabsManager.compute_prefix()}-main") tabs_manager = TabsManager(layout, _id=f"-tabs_manager")
btn_show_right_drawer = mk.button("show", btn_show_right_drawer = mk.button("show",
command=layout.commands.toggle_drawer("right"), command=layout.commands.toggle_drawer("right"),
id="btn_show_right_drawer_id") id="btn_show_right_drawer_id")
@@ -55,7 +55,7 @@ def index(session):
btn_file_upload = mk.label("Upload", btn_file_upload = mk.label("Upload",
icon=folder_open20_regular, icon=folder_open20_regular,
command=tabs_manager.commands.add_tab("File Open", FileUpload(layout)), command=tabs_manager.commands.add_tab("File Open", FileUpload(layout, _id="-file_upload")),
id="file_upload_id") id="file_upload_id")
layout.header_left.add(tabs_manager.add_tab_btn()) layout.header_left.add(tabs_manager.add_tab_btn())
@@ -64,8 +64,11 @@ def index(session):
layout.left_drawer.add(btn_show_commands_debugger, "Debugger") layout.left_drawer.add(btn_show_commands_debugger, "Debugger")
layout.left_drawer.add(btn_file_upload, "Test") layout.left_drawer.add(btn_file_upload, "Test")
layout.set_main(tabs_manager) layout.set_main(tabs_manager)
keyboard = Keyboard(layout).add("ctrl+o", tabs_manager.commands.add_tab("File Open", FileUpload(layout))) keyboard = Keyboard(layout, _id="-keyboard").add("ctrl+o",
keyboard.add("ctrl+n", tabs_manager.commands.add_tab("File Open", FileUpload(layout))) tabs_manager.commands.add_tab("File Open",
FileUpload(layout,
_id="-file_upload")))
keyboard.add("ctrl+n", tabs_manager.commands.add_tab("File Open", FileUpload(layout, _id="-file_upload")))
return layout, keyboard return layout, keyboard

View File

@@ -13,7 +13,7 @@ class InstancesDebugger(SingleInstance):
nodes, edges = from_parent_child_list( nodes, edges = from_parent_child_list(
instances, instances,
id_getter=lambda x: x.get_full_id(), id_getter=lambda x: x.get_full_id(),
label_getter=lambda x: f"{s_name(x.get_session())}-{x.get_prefix()}", label_getter=lambda x: f"{x.get_id()}",
parent_getter=lambda x: x.get_full_parent_id() parent_getter=lambda x: x.get_full_parent_id()
) )
for edge in edges: for edge in edges:
@@ -23,7 +23,7 @@ class InstancesDebugger(SingleInstance):
for node in nodes: for node in nodes:
node["shape"] = "box" node["shape"] = "box"
vis_network = VisNetwork(self, nodes=nodes, edges=edges) vis_network = VisNetwork(self, nodes=nodes, edges=edges, _id="-vis")
# vis_network.add_to_options(physics={"wind": {"x": 0, "y": 1}}) # vis_network.add_to_options(physics={"wind": {"x": 0, "y": 1}})
return vis_network return vis_network

View File

@@ -4,7 +4,7 @@ from typing import Callable, Any
from fasthtml.components import * 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 mk
from myfasthtml.core.commands import Command from myfasthtml.core.commands import Command
from myfasthtml.core.instances import MultipleInstance, BaseInstance 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

View File

@@ -84,7 +84,8 @@ class TabsManager(MultipleInstance):
self._search = Search(self, 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,
_id="-search")
logger.debug(f"TabsManager created with id: {self._id}") logger.debug(f"TabsManager created with id: {self._id}")
logger.debug(f" tabs : {self._get_ordered_tabs()}") logger.debug(f" tabs : {self._get_ordered_tabs()}")
logger.debug(f" active tab : {self._state.active_tab}") logger.debug(f" active tab : {self._state.active_tab}")

View File

@@ -45,7 +45,7 @@ class BaseCommand:
def execute(self, client_response: dict = None): def execute(self, client_response: dict = None):
raise NotImplementedError raise NotImplementedError
def htmx(self, target="this", swap="outerHTML", trigger=None): def htmx(self, target: Optional[str] = "this", swap="outerHTML", trigger=None):
# Note that the default value is the same than in get_htmx_params() # Note that the default value is the same than in get_htmx_params()
if target is None: if target is None:
self._htmx_extra["hx-swap"] = "none" self._htmx_extra["hx-swap"] = "none"
@@ -180,6 +180,15 @@ class Command(BaseCommand):
return [ret] + ret_from_bindings return [ret] + ret_from_bindings
class LambdaCommand(Command):
def __init__(self, delegate, name="LambdaCommand", description="Lambda Command"):
super().__init__(name, description, delegate)
self.htmx(target=None)
def execute(self, client_response: dict = None):
return self.callback(client_response)
class CommandsManager: class CommandsManager:
commands = {} commands = {}

View File

@@ -31,9 +31,8 @@ class BaseInstance:
session = args[1] if len(args) > 1 and isinstance(args[1], dict) else kwargs.get("session", None) session = args[1] if len(args) > 1 and isinstance(args[1], dict) else kwargs.get("session", None)
_id = args[2] if len(args) > 2 and isinstance(args[2], str) else kwargs.get("_id", None) _id = args[2] if len(args) > 2 and isinstance(args[2], str) else kwargs.get("_id", None)
# Compute _id if not provided # Compute _id
if _id is None: _id = cls.compute_id(_id, parent)
_id = cls.compute_id()
if session is None: if session is None:
if parent is not None: if parent is not None:
@@ -68,7 +67,7 @@ class BaseInstance:
self._parent = parent self._parent = parent
self._session = session or (parent.get_session() if parent else None) self._session = session or (parent.get_session() if parent else None)
self._id = _id or self.compute_id() self._id = self.compute_id(_id, parent)
self._prefix = self._id if isinstance(self, (UniqueInstance, SingleInstance)) else self.compute_prefix() self._prefix = self._id if isinstance(self, (UniqueInstance, SingleInstance)) else self.compute_prefix()
if auto_register: if auto_register:
@@ -98,12 +97,18 @@ class BaseInstance:
return f"mf-{pascal_to_snake(cls.__name__)}" return f"mf-{pascal_to_snake(cls.__name__)}"
@classmethod @classmethod
def compute_id(cls): def compute_id(cls, _id: Optional[str], parent: Optional['BaseInstance']):
prefix = cls.compute_prefix() if _id is None:
if issubclass(cls, SingleInstance): prefix = cls.compute_prefix()
_id = prefix if issubclass(cls, SingleInstance):
else: _id = prefix
_id = f"{prefix}-{str(uuid.uuid4())}" else:
_id = f"{prefix}-{str(uuid.uuid4())}"
return _id
if _id.startswith("-") and parent is not None:
return f"{parent.get_prefix()}{_id}"
return _id return _id

View File

@@ -1,5 +1,7 @@
from collections.abc import Callable from collections.abc import Callable
ROOT_COLOR = "#ff9999"
GHOST_COLOR = "#cccccc"
def from_nested_dict(trees: list[dict]) -> tuple[list, list]: def from_nested_dict(trees: list[dict]) -> tuple[list, list]:
""" """
@@ -147,8 +149,8 @@ def from_parent_child_list(
id_getter: Callable = None, id_getter: Callable = None,
label_getter: Callable = None, label_getter: Callable = None,
parent_getter: Callable = None, parent_getter: Callable = None,
ghost_color: str = "#cccccc", ghost_color: str = GHOST_COLOR,
root_color: str | None = "#ff9999" root_color: str | None = ROOT_COLOR
) -> tuple[list, list]: ) -> tuple[list, list]:
""" """
Convert a list of items with parent references to vis.js nodes and edges format. Convert a list of items with parent references to vis.js nodes and edges format.

View File

@@ -5,7 +5,7 @@ import pytest
from fasthtml.components import Button, Div from fasthtml.components import Button, Div
from myutils.observable import make_observable, bind from myutils.observable import make_observable, bind
from myfasthtml.core.commands import Command, CommandsManager from myfasthtml.core.commands import Command, CommandsManager, LambdaCommand
from myfasthtml.core.constants import ROUTE_ROOT, Routes from myfasthtml.core.constants import ROUTE_ROOT, Routes
from myfasthtml.test.matcher import matches from myfasthtml.test.matcher import matches
@@ -183,3 +183,14 @@ class TestCommandExecute:
assert "hx-swap-oob" not in res[0].attrs assert "hx-swap-oob" not in res[0].attrs
assert "hx-swap-oob" not in res[1].attrs assert "hx-swap-oob" not in res[1].attrs
assert "hx-swap-oob" not in res[3].attrs assert "hx-swap-oob" not in res[3].attrs
class TestLambaCommand:
def test_i_can_create_a_command_from_lambda(self):
command = LambdaCommand(lambda resp: "Hello World")
assert command.execute() == "Hello World"
def test_by_default_target_is_none(self):
command = LambdaCommand(lambda resp: "Hello World")
assert command.get_htmx_params()["hx-swap"] == "none"

View File

@@ -129,9 +129,9 @@ class TestBaseInstance:
assert instance.get_id() is not None assert instance.get_id() is not None
assert instance.get_id().startswith("mf-base_instance-") assert instance.get_id().startswith("mf-base_instance-")
def test_i_can_get_prefix_from_class_name(self): def test_i_can_get_prefix_from_class_name(self, session):
"""Test that get_prefix() returns the correct snake_case prefix.""" """Test that get_prefix() returns the correct snake_case prefix."""
prefix = BaseInstance.get_prefix() prefix = BaseInstance(None, session).get_prefix()
assert prefix == "mf-base_instance" assert prefix == "mf-base_instance"
@@ -182,7 +182,7 @@ class TestSingleInstance:
instance = SingleInstance(parent=None, session=session) instance = SingleInstance(parent=None, session=session)
assert instance.get_id() == "mf-single_instance" assert instance.get_id() == "mf-single_instance"
assert instance.get_id() == SingleInstance.get_prefix() assert instance.get_prefix() == "mf-single_instance"
class TestSingleInstanceSubclass: class TestSingleInstanceSubclass:
@@ -250,6 +250,18 @@ class TestMultipleInstance:
assert instance1 is instance2 assert instance1 is instance2
def test_key_prefixed_by_underscore_uses_the_parent_id_as_prefix(self, root_instance):
"""Test that key prefixed by underscore uses the parent id as prefix."""
instance = MultipleInstance(parent=root_instance, _id="-test_id")
assert instance.get_id() == f"{root_instance.get_id()}-test_id"
def test_no_parent_id_as_prefix_if_parent_is_none(self, session, root_instance):
"""Test that key prefixed by underscore does not use the parent id as prefix if parent is None."""
instance = MultipleInstance(parent=None, session=session, _id="-test_id")
assert instance.get_id() == "-test_id"
class TestMultipleInstanceSubclass: class TestMultipleInstanceSubclass:
@@ -295,9 +307,9 @@ class TestMultipleInstanceSubclass:
with pytest.raises(TypeError): with pytest.raises(TypeError):
MultipleInstance(parent=root_instance, _id=instance1.get_id()) MultipleInstance(parent=root_instance, _id=instance1.get_id())
def test_i_can_get_correct_prefix_for_multiple_subclass(self): def test_i_can_get_correct_prefix_for_multiple_subclass(self, root_instance):
"""Test that subclass has correct auto-generated prefix.""" """Test that subclass has correct auto-generated prefix."""
prefix = SubMultipleInstance.get_prefix() prefix = SubMultipleInstance(root_instance).get_prefix()
assert prefix == "mf-sub_multiple_instance" assert prefix == "mf-sub_multiple_instance"

View File

@@ -405,7 +405,7 @@ class TestFromParentChildList:
ghost_node = [n for n in nodes if n["id"] == "ghost"][0] ghost_node = [n for n in nodes if n["id"] == "ghost"][0]
assert "color" in ghost_node assert "color" in ghost_node
assert ghost_node["color"] == "#ff9999" assert ghost_node["color"] == "#cccccc"
def test_i_can_use_custom_ghost_color(self): def test_i_can_use_custom_ghost_color(self):
"""Test that custom ghost_color parameter is applied.""" """Test that custom ghost_color parameter is applied."""