Refactored instances management
This commit is contained in:
@@ -20,4 +20,4 @@ def session():
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def root_instance(session):
|
||||
return SingleInstance(session, "TestRoot", None)
|
||||
return SingleInstance(None, session, "TestRoot")
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import shutil
|
||||
|
||||
import pytest
|
||||
from fasthtml.components import *
|
||||
from fasthtml.xtend import Script
|
||||
@@ -10,9 +12,11 @@ from .conftest import session
|
||||
|
||||
@pytest.fixture()
|
||||
def tabs_manager(root_instance):
|
||||
shutil.rmtree(".myFastHtmlDb", ignore_errors=True)
|
||||
yield TabsManager(root_instance)
|
||||
|
||||
InstancesManager.reset()
|
||||
shutil.rmtree(".myFastHtmlDb", ignore_errors=True)
|
||||
|
||||
|
||||
class TestTabsManagerBehaviour:
|
||||
|
||||
@@ -4,6 +4,7 @@ from dataclasses import dataclass
|
||||
import pytest
|
||||
|
||||
from myfasthtml.core.dbmanager import DbManager, DbObject
|
||||
from myfasthtml.core.instances import SingleInstance, BaseInstance
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
@@ -19,9 +20,14 @@ def session():
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db_manager(session):
|
||||
def parent(session):
|
||||
return SingleInstance(session=session, _id="test_parent_id")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db_manager(parent):
|
||||
shutil.rmtree("TestDb", ignore_errors=True)
|
||||
db_manager_instance = DbManager(session, root="TestDb", auto_register=False)
|
||||
db_manager_instance = DbManager(parent, root="TestDb", auto_register=False)
|
||||
|
||||
yield db_manager_instance
|
||||
|
||||
@@ -32,17 +38,17 @@ def simplify(res: dict) -> dict:
|
||||
return {k: v for k, v in res.items() if not k.startswith("_")}
|
||||
|
||||
|
||||
def test_i_can_init(session, db_manager):
|
||||
def test_i_can_init(parent, db_manager):
|
||||
class DummyObject(DbObject):
|
||||
def __init__(self, sess: dict):
|
||||
super().__init__(sess, "DummyObject", db_manager)
|
||||
def __init__(self, owner: BaseInstance):
|
||||
super().__init__(owner, "DummyObject", db_manager)
|
||||
|
||||
with self.initializing():
|
||||
self.value: str = "hello"
|
||||
self.number: int = 42
|
||||
self.none_value: None = None
|
||||
|
||||
dummy = DummyObject(session)
|
||||
dummy = DummyObject(parent)
|
||||
|
||||
props = dummy._get_properties()
|
||||
|
||||
@@ -52,17 +58,17 @@ def test_i_can_init(session, db_manager):
|
||||
assert len(history) == 1
|
||||
|
||||
|
||||
def test_i_can_init_from_dataclass(session, db_manager):
|
||||
def test_i_can_init_from_dataclass(parent, db_manager):
|
||||
@dataclass
|
||||
class DummyObject(DbObject):
|
||||
def __init__(self, sess: dict):
|
||||
super().__init__(sess, "DummyObject", db_manager)
|
||||
def __init__(self, owner: BaseInstance):
|
||||
super().__init__(owner, "DummyObject", db_manager)
|
||||
|
||||
value: str = "hello"
|
||||
number: int = 42
|
||||
none_value: None = None
|
||||
|
||||
DummyObject(session)
|
||||
DummyObject(parent)
|
||||
|
||||
in_db = db_manager.load("DummyObject")
|
||||
history = db_manager.db.history(db_manager.get_tenant(), "DummyObject")
|
||||
@@ -70,10 +76,10 @@ def test_i_can_init_from_dataclass(session, db_manager):
|
||||
assert len(history) == 1
|
||||
|
||||
|
||||
def test_i_can_init_from_db_with(session, db_manager):
|
||||
def test_i_can_init_from_db_with(parent, db_manager):
|
||||
class DummyObject(DbObject):
|
||||
def __init__(self, sess: dict):
|
||||
super().__init__(sess, "DummyObject", db_manager)
|
||||
def __init__(self, owner: BaseInstance):
|
||||
super().__init__(owner, "DummyObject", db_manager)
|
||||
|
||||
with self.initializing():
|
||||
self.value: str = "hello"
|
||||
@@ -82,17 +88,17 @@ def test_i_can_init_from_db_with(session, db_manager):
|
||||
# insert other values in db
|
||||
db_manager.save("DummyObject", {"value": "other_value", "number": 34})
|
||||
|
||||
dummy = DummyObject(session)
|
||||
dummy = DummyObject(parent)
|
||||
|
||||
assert dummy.value == "other_value"
|
||||
assert dummy.number == 34
|
||||
|
||||
|
||||
def test_i_can_init_from_db_with_dataclass(session, db_manager):
|
||||
def test_i_can_init_from_db_with_dataclass(parent, db_manager):
|
||||
@dataclass
|
||||
class DummyObject(DbObject):
|
||||
def __init__(self, sess: dict):
|
||||
super().__init__(sess, "DummyObject", db_manager)
|
||||
def __init__(self, owner: BaseInstance):
|
||||
super().__init__(owner, "DummyObject", db_manager)
|
||||
|
||||
value: str = "hello"
|
||||
number: int = 42
|
||||
@@ -100,16 +106,16 @@ def test_i_can_init_from_db_with_dataclass(session, db_manager):
|
||||
# insert other values in db
|
||||
db_manager.save("DummyObject", {"value": "other_value", "number": 34})
|
||||
|
||||
dummy = DummyObject(session)
|
||||
dummy = DummyObject(parent)
|
||||
|
||||
assert dummy.value == "other_value"
|
||||
assert dummy.number == 34
|
||||
|
||||
|
||||
def test_i_do_not_save_when_prefixed_by_underscore_or_ns(session, db_manager):
|
||||
def test_i_do_not_save_when_prefixed_by_underscore_or_ns(parent, db_manager):
|
||||
class DummyObject(DbObject):
|
||||
def __init__(self, sess: dict):
|
||||
super().__init__(sess, "DummyObject", db_manager)
|
||||
def __init__(self, owner: BaseInstance):
|
||||
super().__init__(owner, "DummyObject", db_manager)
|
||||
|
||||
with self.initializing():
|
||||
self.to_save: str = "value"
|
||||
@@ -120,7 +126,7 @@ def test_i_do_not_save_when_prefixed_by_underscore_or_ns(session, db_manager):
|
||||
_not_to_save: str = "value"
|
||||
ns_not_to_save: str = "value"
|
||||
|
||||
dummy = DummyObject(session)
|
||||
dummy = DummyObject(parent)
|
||||
dummy.to_save = "other_value"
|
||||
dummy.ns_not_to_save = "other_value"
|
||||
dummy._not_to_save = "other_value"
|
||||
@@ -131,17 +137,17 @@ def test_i_do_not_save_when_prefixed_by_underscore_or_ns(session, db_manager):
|
||||
assert "ns_not_to_save" not in in_db
|
||||
|
||||
|
||||
def test_i_do_not_save_when_prefixed_by_underscore_or_ns_with_dataclass(session, db_manager):
|
||||
def test_i_do_not_save_when_prefixed_by_underscore_or_ns_with_dataclass(parent, db_manager):
|
||||
@dataclass
|
||||
class DummyObject(DbObject):
|
||||
def __init__(self, sess: dict):
|
||||
super().__init__(sess, "DummyObject", db_manager)
|
||||
def __init__(self, owner: BaseInstance):
|
||||
super().__init__(owner, "DummyObject", db_manager)
|
||||
|
||||
to_save: str = "value"
|
||||
_not_to_save: str = "value"
|
||||
ns_not_to_save: str = "value"
|
||||
|
||||
dummy = DummyObject(session)
|
||||
dummy = DummyObject(parent)
|
||||
dummy.to_save = "other_value"
|
||||
dummy.ns_not_to_save = "other_value"
|
||||
dummy._not_to_save = "other_value"
|
||||
@@ -152,31 +158,31 @@ def test_i_do_not_save_when_prefixed_by_underscore_or_ns_with_dataclass(session,
|
||||
assert "ns_not_to_save" not in in_db
|
||||
|
||||
|
||||
def test_db_is_updated_when_attribute_is_modified(session, db_manager):
|
||||
def test_db_is_updated_when_attribute_is_modified(parent, db_manager):
|
||||
@dataclass
|
||||
class DummyObject(DbObject):
|
||||
def __init__(self, sess: dict):
|
||||
super().__init__(sess, "DummyObject", db_manager)
|
||||
def __init__(self, owner: BaseInstance):
|
||||
super().__init__(owner, "DummyObject", db_manager)
|
||||
|
||||
value: str = "hello"
|
||||
number: int = 42
|
||||
|
||||
dummy = DummyObject(session)
|
||||
dummy = DummyObject(parent)
|
||||
dummy.value = "other_value"
|
||||
|
||||
assert simplify(db_manager.load("DummyObject")) == {"value": "other_value", "number": 42}
|
||||
|
||||
|
||||
def test_i_do_not_save_in_db_when_value_is_the_same(session, db_manager):
|
||||
def test_i_do_not_save_in_db_when_value_is_the_same(parent, db_manager):
|
||||
@dataclass
|
||||
class DummyObject(DbObject):
|
||||
def __init__(self, sess: dict):
|
||||
super().__init__(sess, "DummyObject", db_manager)
|
||||
def __init__(self, owner: BaseInstance):
|
||||
super().__init__(owner, "DummyObject", db_manager)
|
||||
|
||||
value: str = "hello"
|
||||
number: int = 42
|
||||
|
||||
dummy = DummyObject(session)
|
||||
dummy = DummyObject(parent)
|
||||
dummy.value = "other_value"
|
||||
in_db_1 = db_manager.load("DummyObject")
|
||||
|
||||
@@ -186,16 +192,16 @@ def test_i_do_not_save_in_db_when_value_is_the_same(session, db_manager):
|
||||
assert in_db_1["__parent__"] == in_db_2["__parent__"]
|
||||
|
||||
|
||||
def test_i_can_update(session, db_manager):
|
||||
def test_i_can_update(parent, db_manager):
|
||||
@dataclass
|
||||
class DummyObject(DbObject):
|
||||
def __init__(self, sess: dict):
|
||||
super().__init__(sess, "DummyObject", db_manager)
|
||||
def __init__(self, owner: BaseInstance):
|
||||
super().__init__(owner, "DummyObject", db_manager)
|
||||
|
||||
value: str = "hello"
|
||||
number: int = 42
|
||||
|
||||
dummy = DummyObject(session)
|
||||
dummy = DummyObject(parent)
|
||||
clone = dummy.copy()
|
||||
|
||||
clone.number = 34
|
||||
@@ -207,54 +213,52 @@ def test_i_can_update(session, db_manager):
|
||||
assert simplify(db_manager.load("DummyObject")) == {"value": "other_value", "number": 34}
|
||||
|
||||
|
||||
def test_forbidden_attributes_are_not_the_copy(session, db_manager):
|
||||
def test_forbidden_attributes_are_not_the_copy(parent, db_manager):
|
||||
class DummyObject(DbObject):
|
||||
def __init__(self, sess: dict):
|
||||
super().__init__(sess, "DummyObject", db_manager)
|
||||
def __init__(self, owner: BaseInstance):
|
||||
super().__init__(owner, "DummyObject", db_manager)
|
||||
|
||||
with self.initializing():
|
||||
self.value: str = "hello"
|
||||
self.number: int = 42
|
||||
self.none_value: None = None
|
||||
|
||||
dummy = DummyObject(session)
|
||||
|
||||
dummy = DummyObject(parent)
|
||||
clone = dummy.copy()
|
||||
|
||||
for k in DbObject._forbidden_attrs:
|
||||
assert not hasattr(clone, k), f"Clone should not have forbidden attribute '{k}'"
|
||||
|
||||
|
||||
def test_forbidden_attributes_are_not_the_copy_for_dataclass(session, db_manager):
|
||||
def test_forbidden_attributes_are_not_the_copy_for_dataclass(parent, db_manager):
|
||||
@dataclass
|
||||
class DummyObject(DbObject):
|
||||
def __init__(self, sess: dict):
|
||||
super().__init__(sess, "DummyObject", db_manager)
|
||||
def __init__(self, owner: BaseInstance):
|
||||
super().__init__(owner, "DummyObject", db_manager)
|
||||
|
||||
value: str = "hello"
|
||||
number: int = 42
|
||||
none_value: None = None
|
||||
|
||||
dummy = DummyObject(session)
|
||||
|
||||
dummy = DummyObject(parent)
|
||||
clone = dummy.copy()
|
||||
|
||||
for k in DbObject._forbidden_attrs:
|
||||
assert not hasattr(clone, k), f"Clone should not have forbidden attribute '{k}'"
|
||||
|
||||
|
||||
def test_i_cannot_update_a_forbidden_attribute(session, db_manager):
|
||||
def test_i_cannot_update_a_forbidden_attribute(parent, db_manager):
|
||||
@dataclass
|
||||
class DummyObject(DbObject):
|
||||
def __init__(self, sess: dict):
|
||||
super().__init__(sess, "DummyObject", db_manager)
|
||||
def __init__(self, owner: BaseInstance):
|
||||
super().__init__(owner, "DummyObject", db_manager)
|
||||
|
||||
value: str = "hello"
|
||||
number: int = 42
|
||||
none_value: None = None
|
||||
|
||||
dummy = DummyObject(session)
|
||||
dummy = DummyObject(parent)
|
||||
|
||||
dummy.update(_session="other_value")
|
||||
dummy.update(_owner="other_value")
|
||||
|
||||
assert dummy._session == session
|
||||
assert dummy._owner is parent
|
||||
|
||||
387
tests/core/test_instances.py
Normal file
387
tests/core/test_instances.py
Normal file
@@ -0,0 +1,387 @@
|
||||
import pytest
|
||||
|
||||
from myfasthtml.core.instances import (
|
||||
BaseInstance,
|
||||
SingleInstance,
|
||||
MultipleInstance,
|
||||
InstancesManager,
|
||||
DuplicateInstanceError,
|
||||
special_session,
|
||||
Ids,
|
||||
RootInstance
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_instances():
|
||||
"""Reset instances before each test to ensure isolation."""
|
||||
InstancesManager.instances.clear()
|
||||
yield
|
||||
InstancesManager.instances.clear()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def session():
|
||||
"""Create a test session."""
|
||||
return {"user_info": {"id": "test-user-123"}}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def another_session():
|
||||
"""Create another test session."""
|
||||
return {"user_info": {"id": "test-user-456"}}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def root_instance(session):
|
||||
"""Create a root instance for testing."""
|
||||
return SingleInstance(parent=None, session=session, _id="test-root")
|
||||
|
||||
|
||||
# Example subclasses for testing
|
||||
class SubSingleInstance(SingleInstance):
|
||||
"""Example subclass of SingleInstance with simplified signature."""
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent=parent)
|
||||
|
||||
|
||||
class SubMultipleInstance(MultipleInstance):
|
||||
"""Example subclass of MultipleInstance with custom parameter."""
|
||||
|
||||
def __init__(self, parent, _id=None, custom_param=None):
|
||||
super().__init__(parent=parent, _id=_id)
|
||||
self.custom_param = custom_param
|
||||
|
||||
|
||||
class TestBaseInstance:
|
||||
|
||||
def test_i_can_create_a_base_instance_with_positional_args(self, session, root_instance):
|
||||
"""Test that a BaseInstance can be created with positional arguments."""
|
||||
instance = BaseInstance(root_instance, session, "test_id")
|
||||
|
||||
assert instance is not None
|
||||
assert instance.get_id() == "test_id"
|
||||
assert instance.get_session() == session
|
||||
assert instance.get_parent() == root_instance
|
||||
|
||||
def test_i_can_create_a_base_instance_with_kwargs(self, session, root_instance):
|
||||
"""Test that a BaseInstance can be created with keyword arguments."""
|
||||
instance = BaseInstance(parent=root_instance, session=session, _id="test_id")
|
||||
|
||||
assert instance is not None
|
||||
assert instance.get_id() == "test_id"
|
||||
assert instance.get_session() == session
|
||||
assert instance.get_parent() == root_instance
|
||||
|
||||
def test_i_can_create_a_base_instance_with_mixed_args(self, session, root_instance):
|
||||
"""Test that a BaseInstance can be created with mixed positional and keyword arguments."""
|
||||
instance = BaseInstance(root_instance, session=session, _id="test_id")
|
||||
|
||||
assert instance is not None
|
||||
assert instance.get_id() == "test_id"
|
||||
assert instance.get_session() == session
|
||||
assert instance.get_parent() == root_instance
|
||||
|
||||
def test_i_can_retrieve_the_same_instance_when_using_same_session_and_id(self, session, root_instance):
|
||||
"""Test that creating an instance with same session and id returns the existing instance."""
|
||||
instance1 = BaseInstance(root_instance, session, "same_id")
|
||||
instance2 = BaseInstance(root_instance, session, "same_id")
|
||||
|
||||
assert instance1 is instance2
|
||||
|
||||
def test_i_can_control_instances_registration(self, session, root_instance):
|
||||
"""Test that auto_register=False prevents automatic registration."""
|
||||
BaseInstance(parent=root_instance, session=session, _id="test_id", auto_register=False)
|
||||
|
||||
session_id = InstancesManager.get_session_id(session)
|
||||
key = (session_id, "test_id")
|
||||
|
||||
assert key not in InstancesManager.instances
|
||||
|
||||
def test_i_can_have_different_instances_for_different_sessions(self, session, another_session, root_instance):
|
||||
"""Test that different sessions can have instances with the same id."""
|
||||
root_instance2 = SingleInstance(parent=None, session=another_session, _id="test-root")
|
||||
|
||||
instance1 = BaseInstance(root_instance, session, "same_id")
|
||||
instance2 = BaseInstance(root_instance2, another_session, "same_id")
|
||||
|
||||
assert instance1 is not instance2
|
||||
assert instance1.get_session() == session
|
||||
assert instance2.get_session() == another_session
|
||||
|
||||
def test_i_can_create_instance_with_parent_only(self, session, root_instance):
|
||||
"""Test that session can be extracted from parent when not provided."""
|
||||
instance = BaseInstance(parent=root_instance, _id="test_id")
|
||||
|
||||
assert instance.get_session() == root_instance.get_session()
|
||||
assert instance.get_parent() == root_instance
|
||||
|
||||
def test_i_cannot_create_instance_without_parent_or_session(self):
|
||||
"""Test that creating an instance without parent or session raises TypeError."""
|
||||
with pytest.raises(TypeError, match="Either session or parent must be provided"):
|
||||
BaseInstance(None, _id="test_id")
|
||||
|
||||
def test_i_can_get_auto_generated_id(self, session, root_instance):
|
||||
"""Test that if _id is not provided, an ID is auto-generated via compute_id()."""
|
||||
instance = BaseInstance(parent=root_instance, session=session)
|
||||
|
||||
assert instance.get_id() is not None
|
||||
assert instance.get_id().startswith("mf-base_instance-")
|
||||
|
||||
def test_i_can_get_prefix_from_class_name(self):
|
||||
"""Test that get_prefix() returns the correct snake_case prefix."""
|
||||
prefix = BaseInstance.get_prefix()
|
||||
|
||||
assert prefix == "mf-base_instance"
|
||||
|
||||
|
||||
class TestSingleInstance:
|
||||
|
||||
def test_i_can_create_a_single_instance(self, session, root_instance):
|
||||
"""Test that a SingleInstance can be created."""
|
||||
instance = SingleInstance(parent=root_instance, session=session)
|
||||
|
||||
assert instance is not None
|
||||
assert instance.get_id() == "mf-single_instance"
|
||||
assert instance.get_session() == session
|
||||
assert instance.get_parent() == root_instance
|
||||
|
||||
def test_i_can_create_single_instance_with_positional_args(self, session, root_instance):
|
||||
"""Test that a SingleInstance can be created with positional arguments."""
|
||||
instance = SingleInstance(root_instance, session, "custom_id")
|
||||
|
||||
assert instance is not None
|
||||
assert instance.get_id() == "custom_id"
|
||||
assert instance.get_session() == session
|
||||
assert instance.get_parent() == root_instance
|
||||
|
||||
def test_the_same_instance_is_returned(self, session):
|
||||
"""Test that single instance is cached and returned on subsequent calls."""
|
||||
instance1 = SingleInstance(parent=None, session=session, _id="unique_id")
|
||||
instance2 = SingleInstance(parent=None, session=session, _id="unique_id")
|
||||
|
||||
assert instance1 is instance2
|
||||
|
||||
def test_i_cannot_create_duplicate_single_instance(self, session):
|
||||
"""Test that creating a duplicate SingleInstance raises DuplicateInstanceError."""
|
||||
instance = SingleInstance(parent=None, session=session, _id="unique_id")
|
||||
|
||||
with pytest.raises(DuplicateInstanceError):
|
||||
InstancesManager.register(session, instance)
|
||||
|
||||
def test_i_can_retrieve_existing_single_instance(self, session):
|
||||
"""Test that attempting to create an existing SingleInstance returns the same instance."""
|
||||
instance1 = SingleInstance(parent=None, session=session, _id="same_id")
|
||||
instance2 = SingleInstance(parent=None, session=session, _id="same_id", auto_register=False)
|
||||
|
||||
assert instance1 is instance2
|
||||
|
||||
def test_i_can_get_auto_computed_id_for_single_instance(self, session):
|
||||
"""Test that the default ID equals prefix for SingleInstance."""
|
||||
instance = SingleInstance(parent=None, session=session)
|
||||
|
||||
assert instance.get_id() == "mf-single_instance"
|
||||
assert instance.get_id() == SingleInstance.get_prefix()
|
||||
|
||||
|
||||
class TestSingleInstanceSubclass:
|
||||
|
||||
def test_i_can_create_subclass_of_single_instance(self, root_instance):
|
||||
"""Test that a subclass of SingleInstance works correctly."""
|
||||
instance = SubSingleInstance(root_instance)
|
||||
|
||||
assert instance is not None
|
||||
assert isinstance(instance, SingleInstance)
|
||||
assert isinstance(instance, SubSingleInstance)
|
||||
|
||||
def test_i_can_create_subclass_with_custom_signature(self, root_instance):
|
||||
"""Test that subclass with simplified signature works correctly."""
|
||||
instance = SubSingleInstance(root_instance)
|
||||
|
||||
assert instance.get_parent() == root_instance
|
||||
assert instance.get_session() == root_instance.get_session()
|
||||
assert instance.get_id() == "mf-sub_single_instance"
|
||||
assert instance.get_prefix() == "mf-sub_single_instance"
|
||||
|
||||
def test_i_can_retrieve_subclass_instance_from_cache(self, root_instance):
|
||||
"""Test that cache works for subclasses."""
|
||||
instance1 = SubSingleInstance(root_instance)
|
||||
instance2 = SubSingleInstance(root_instance)
|
||||
|
||||
assert instance1 is instance2
|
||||
|
||||
|
||||
class TestMultipleInstance:
|
||||
|
||||
def test_i_can_create_multiple_instances_with_same_prefix(self, session, root_instance):
|
||||
"""Test that multiple MultipleInstance objects can be created with the same prefix."""
|
||||
instance1 = MultipleInstance(parent=root_instance, session=session)
|
||||
instance2 = MultipleInstance(parent=root_instance, session=session)
|
||||
|
||||
assert instance1 is not instance2
|
||||
assert instance1.get_id() != instance2.get_id()
|
||||
assert instance1.get_id().startswith("mf-multiple_instance-")
|
||||
assert instance2.get_id().startswith("mf-multiple_instance-")
|
||||
|
||||
def test_i_can_have_auto_generated_unique_ids(self, session, root_instance):
|
||||
"""Test that each MultipleInstance receives a unique auto-generated ID."""
|
||||
instances = [MultipleInstance(parent=root_instance, session=session) for _ in range(5)]
|
||||
ids = [inst.get_id() for inst in instances]
|
||||
|
||||
# All IDs should be unique
|
||||
assert len(ids) == len(set(ids))
|
||||
|
||||
# All IDs should start with the prefix
|
||||
assert all(id.startswith("mf-multiple_instance-") for id in ids)
|
||||
|
||||
def test_i_can_provide_custom_id_to_multiple_instance(self, session, root_instance):
|
||||
"""Test that a custom _id can be provided to MultipleInstance."""
|
||||
custom_id = "custom-instance-id"
|
||||
instance = MultipleInstance(parent=root_instance, session=session, _id=custom_id)
|
||||
|
||||
assert instance.get_id() == custom_id
|
||||
|
||||
def test_i_can_retrieve_multiple_instance_by_custom_id(self, session, root_instance):
|
||||
"""Test that a MultipleInstance with custom _id can be retrieved from cache."""
|
||||
custom_id = "custom-instance-id"
|
||||
instance1 = MultipleInstance(parent=root_instance, session=session, _id=custom_id)
|
||||
instance2 = MultipleInstance(parent=root_instance, session=session, _id=custom_id)
|
||||
|
||||
assert instance1 is instance2
|
||||
|
||||
|
||||
class TestMultipleInstanceSubclass:
|
||||
|
||||
def test_i_can_create_subclass_of_multiple_instance(self, root_instance):
|
||||
"""Test that a subclass of MultipleInstance works correctly."""
|
||||
instance = SubMultipleInstance(root_instance, custom_param="test")
|
||||
|
||||
assert instance is not None
|
||||
assert isinstance(instance, MultipleInstance)
|
||||
assert isinstance(instance, SubMultipleInstance)
|
||||
assert instance.custom_param == "test"
|
||||
|
||||
def test_i_can_create_multiple_subclass_instances_with_auto_generated_ids(self, root_instance):
|
||||
"""Test that multiple instances of subclass can be created with unique IDs."""
|
||||
instance1 = SubMultipleInstance(root_instance, custom_param="first")
|
||||
instance2 = SubMultipleInstance(root_instance, custom_param="second")
|
||||
|
||||
assert instance1 is not instance2
|
||||
assert instance1.get_id() != instance2.get_id()
|
||||
assert instance1.get_id().startswith("mf-sub_multiple_instance-")
|
||||
assert instance2.get_id().startswith("mf-sub_multiple_instance-")
|
||||
|
||||
def test_i_can_create_subclass_with_custom_signature(self, root_instance):
|
||||
"""Test that subclass with custom parameters works correctly."""
|
||||
instance = SubMultipleInstance(root_instance, custom_param="value")
|
||||
|
||||
assert instance.get_parent() == root_instance
|
||||
assert instance.get_session() == root_instance.get_session()
|
||||
assert instance.custom_param == "value"
|
||||
|
||||
def test_i_can_retrieve_subclass_instance_from_cache(self, root_instance):
|
||||
"""Test that cache works for subclasses."""
|
||||
instance1 = SubMultipleInstance(root_instance, custom_param="first")
|
||||
instance2 = SubMultipleInstance(root_instance, custom_param="second", _id=instance1.get_id())
|
||||
|
||||
assert instance1 is instance2
|
||||
|
||||
def test_i_cannot_retrieve_subclass_instance_when_type_differs(self, root_instance):
|
||||
"""Test that cache works for subclasses with custom _id."""
|
||||
# Need to pass _id explicitly to enable caching
|
||||
instance1 = SubMultipleInstance(root_instance)
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
MultipleInstance(parent=root_instance, _id=instance1.get_id())
|
||||
|
||||
def test_i_can_get_correct_prefix_for_multiple_subclass(self):
|
||||
"""Test that subclass has correct auto-generated prefix."""
|
||||
prefix = SubMultipleInstance.get_prefix()
|
||||
|
||||
assert prefix == "mf-sub_multiple_instance"
|
||||
|
||||
|
||||
class TestInstancesManager:
|
||||
|
||||
def test_i_can_register_an_instance_manually(self, session, root_instance):
|
||||
"""Test that an instance can be manually registered."""
|
||||
instance = BaseInstance(parent=root_instance, session=session, _id="manual_id", auto_register=False)
|
||||
|
||||
InstancesManager.register(session, instance)
|
||||
|
||||
session_id = InstancesManager.get_session_id(session)
|
||||
key = (session_id, "manual_id")
|
||||
|
||||
assert key in InstancesManager.instances
|
||||
assert InstancesManager.instances[key] is instance
|
||||
|
||||
def test_i_can_get_existing_instance_by_id(self, session, root_instance):
|
||||
"""Test that an existing instance can be retrieved by ID."""
|
||||
instance = BaseInstance(parent=root_instance, session=session, _id="get_id")
|
||||
|
||||
retrieved = InstancesManager.get(session, "get_id")
|
||||
|
||||
assert retrieved is instance
|
||||
|
||||
def test_i_cannot_get_nonexistent_instance_without_type(self, session):
|
||||
"""Test that getting a non-existent instance without type raises KeyError."""
|
||||
with pytest.raises(KeyError):
|
||||
InstancesManager.get(session, "nonexistent_id")
|
||||
|
||||
def test_i_can_get_session_id_from_valid_session(self, session):
|
||||
"""Test that session ID is correctly extracted from a valid session."""
|
||||
session_id = InstancesManager.get_session_id(session)
|
||||
|
||||
assert session_id == "test-user-123"
|
||||
|
||||
def test_i_can_handle_none_session(self):
|
||||
"""Test that None session returns a special identifier."""
|
||||
session_id = InstancesManager.get_session_id(None)
|
||||
|
||||
assert session_id == "** NOT LOGGED IN **"
|
||||
|
||||
def test_i_can_handle_invalid_session(self):
|
||||
"""Test that invalid sessions return appropriate identifiers."""
|
||||
# Session is None
|
||||
session_id = InstancesManager.get_session_id(None)
|
||||
assert session_id == "** NOT LOGGED IN **"
|
||||
|
||||
# Session without user_info
|
||||
session_no_user = {}
|
||||
session_id = InstancesManager.get_session_id(session_no_user)
|
||||
assert session_id == "** UNKNOWN USER **"
|
||||
|
||||
# Session with user_info but no id
|
||||
session_no_id = {"user_info": {}}
|
||||
session_id = InstancesManager.get_session_id(session_no_id)
|
||||
assert session_id == "** INVALID SESSION **"
|
||||
|
||||
def test_i_can_reset_all_instances(self, session, root_instance):
|
||||
"""Test that reset() clears all instances."""
|
||||
BaseInstance(parent=root_instance, session=session, _id="id1")
|
||||
BaseInstance(parent=root_instance, session=session, _id="id2")
|
||||
|
||||
assert len(InstancesManager.instances) > 0
|
||||
|
||||
InstancesManager.reset()
|
||||
|
||||
assert len(InstancesManager.instances) == 0
|
||||
|
||||
|
||||
class TestRootInstance:
|
||||
|
||||
def test_i_can_create_root_instance_with_positional_args(self):
|
||||
"""Test that RootInstance can be created with positional arguments."""
|
||||
root = SingleInstance(None, special_session, Ids.Root)
|
||||
|
||||
assert root is not None
|
||||
assert root.get_id() == Ids.Root
|
||||
assert root.get_session() == special_session
|
||||
assert root.get_parent() is None
|
||||
|
||||
def test_i_can_access_root_instance(self):
|
||||
"""Test that RootInstance is created and accessible."""
|
||||
assert RootInstance is not None
|
||||
assert RootInstance.get_id() == Ids.Root
|
||||
assert RootInstance.get_session() == special_session
|
||||
@@ -311,7 +311,7 @@ class TestFromParentChildList:
|
||||
nodes, edges = from_parent_child_list(items)
|
||||
|
||||
assert len(nodes) == 1
|
||||
assert nodes[0] == {"id": "root", "label": "Root"}
|
||||
assert nodes[0] == {'color': '#ff9999', 'id': 'root', 'label': 'Root'}
|
||||
assert len(edges) == 0
|
||||
|
||||
def test_i_can_convert_simple_parent_child_relationship(self):
|
||||
@@ -323,7 +323,7 @@ class TestFromParentChildList:
|
||||
nodes, edges = from_parent_child_list(items)
|
||||
|
||||
assert len(nodes) == 2
|
||||
assert {"id": "root", "label": "Root"} in nodes
|
||||
assert {'color': '#ff9999', 'id': 'root', 'label': 'Root'} in nodes
|
||||
assert {"id": "child", "label": "Child"} in nodes
|
||||
|
||||
assert len(edges) == 1
|
||||
@@ -513,3 +513,136 @@ class TestFromParentChildList:
|
||||
|
||||
ghost_node = [n for n in nodes if n["id"] == "ghost_parent"][0]
|
||||
assert ghost_node["label"] == "ghost_parent"
|
||||
|
||||
def test_i_can_apply_root_color_to_single_root(self):
|
||||
"""Test that a single root node receives the root_color."""
|
||||
items = [{"id": "root", "label": "Root"}]
|
||||
nodes, edges = from_parent_child_list(items, root_color="#ff0000")
|
||||
|
||||
assert len(nodes) == 1
|
||||
assert nodes[0]["color"] == "#ff0000"
|
||||
|
||||
def test_i_can_apply_root_color_to_multiple_roots(self):
|
||||
"""Test root_color is assigned to all nodes without parent."""
|
||||
items = [
|
||||
{"id": "root1", "label": "Root 1"},
|
||||
{"id": "root2", "label": "Root 2"},
|
||||
{"id": "child", "parent": "root1", "label": "Child"}
|
||||
]
|
||||
nodes, edges = from_parent_child_list(items, root_color="#aa0000")
|
||||
|
||||
root_nodes = [n for n in nodes if n["id"] in ("root1", "root2")]
|
||||
assert all(n.get("color") == "#aa0000" for n in root_nodes)
|
||||
|
||||
# child must NOT have root_color
|
||||
child_node = next(n for n in nodes if n["id"] == "child")
|
||||
assert "color" not in child_node
|
||||
|
||||
def test_i_can_handle_root_with_parent_none(self):
|
||||
"""Test that root_color is applied when parent=None."""
|
||||
items = [
|
||||
{"id": "r1", "parent": None, "label": "R1"}
|
||||
]
|
||||
nodes, edges = from_parent_child_list(items, root_color="#112233")
|
||||
|
||||
assert nodes[0]["color"] == "#112233"
|
||||
|
||||
def test_i_can_handle_root_with_parent_empty_string(self):
|
||||
"""Test that root_color is applied when parent=''."""
|
||||
items = [
|
||||
{"id": "r1", "parent": "", "label": "R1"}
|
||||
]
|
||||
nodes, edges = from_parent_child_list(items, root_color="#334455")
|
||||
|
||||
assert nodes[0]["color"] == "#334455"
|
||||
|
||||
def test_i_do_not_apply_root_color_to_non_roots(self):
|
||||
"""Test that only real roots receive root_color."""
|
||||
items = [
|
||||
{"id": "root", "label": "Root"},
|
||||
{"id": "child", "parent": "root", "label": "Child"}
|
||||
]
|
||||
nodes, edges = from_parent_child_list(items, root_color="#ff0000")
|
||||
|
||||
# Only one root → only this one has the color
|
||||
root_node = next(n for n in nodes if n["id"] == "root")
|
||||
assert root_node["color"] == "#ff0000"
|
||||
|
||||
child_node = next(n for n in nodes if n["id"] == "child")
|
||||
assert "color" not in child_node
|
||||
|
||||
def test_i_do_not_override_ghost_color_with_root_color(self):
|
||||
"""Ghost nodes must keep ghost_color, not root_color."""
|
||||
items = [
|
||||
{"id": "child", "parent": "ghost_parent", "label": "Child"}
|
||||
]
|
||||
nodes, edges = from_parent_child_list(
|
||||
items,
|
||||
root_color="#ff0000",
|
||||
ghost_color="#00ff00"
|
||||
)
|
||||
|
||||
ghost_node = next(n for n in nodes if n["id"] == "ghost_parent")
|
||||
assert ghost_node["color"] == "#00ff00"
|
||||
|
||||
# child is not root → no color
|
||||
child_node = next(n for n in nodes if n["id"] == "child")
|
||||
assert "color" not in child_node
|
||||
|
||||
def test_i_can_use_custom_root_color(self):
|
||||
"""Test that a custom root_color is applied instead of default."""
|
||||
items = [{"id": "root", "label": "Root"}]
|
||||
nodes, edges = from_parent_child_list(items, root_color="#123456")
|
||||
|
||||
assert nodes[0]["color"] == "#123456"
|
||||
|
||||
def test_i_can_mix_root_nodes_and_ghost_nodes(self):
|
||||
"""Ensure root_color applies only to roots and ghost nodes keep ghost_color."""
|
||||
items = [
|
||||
{"id": "root", "label": "Root"},
|
||||
{"id": "child", "parent": "ghost_parent", "label": "Child"}
|
||||
]
|
||||
nodes, edges = from_parent_child_list(
|
||||
items,
|
||||
root_color="#ff0000",
|
||||
ghost_color="#00ff00"
|
||||
)
|
||||
|
||||
root_node = next(n for n in nodes if n["id"] == "root")
|
||||
ghost_node = next(n for n in nodes if n["id"] == "ghost_parent")
|
||||
|
||||
assert root_node["color"] == "#ff0000"
|
||||
assert ghost_node["color"] == "#00ff00"
|
||||
|
||||
def test_i_do_not_mark_node_as_root_if_parent_field_exists(self):
|
||||
"""Node with parent key but non-empty value should NOT get root_color."""
|
||||
items = [
|
||||
{"id": "root", "label": "Root"},
|
||||
{"id": "child", "parent": "root", "label": "Child"},
|
||||
{"id": "other", "parent": "unknown_parent", "label": "Other"}
|
||||
]
|
||||
nodes, edges = from_parent_child_list(
|
||||
items,
|
||||
root_color="#ff0000",
|
||||
ghost_color="#00ff00"
|
||||
)
|
||||
|
||||
# "root" is the only real root
|
||||
root_node = next(n for n in nodes if n["id"] == "root")
|
||||
assert root_node["color"] == "#ff0000"
|
||||
|
||||
# "other" is NOT root, even though its parent is missing
|
||||
other_node = next(n for n in nodes if n["id"] == "other")
|
||||
assert "color" not in other_node
|
||||
|
||||
# ghost parent must have ghost_color
|
||||
ghost_node = next(n for n in nodes if n["id"] == "unknown_parent")
|
||||
assert ghost_node["color"] == "#00ff00"
|
||||
|
||||
def test_i_do_no_add_root_color_when_its_none(self):
|
||||
"""Test that a single root node receives the root_color."""
|
||||
items = [{"id": "root", "label": "Root"}]
|
||||
nodes, edges = from_parent_child_list(items, root_color=None)
|
||||
|
||||
assert len(nodes) == 1
|
||||
assert "color" not in nodes[0]
|
||||
|
||||
Reference in New Issue
Block a user