654 lines
21 KiB
Python
654 lines
21 KiB
Python
# dabmodel (c) by chacha
|
|
#
|
|
# dabmodel is licensed under a
|
|
# Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Unported License.
|
|
#
|
|
# You should have received a copy of the license along with this
|
|
# work. If not, see <https://creativecommons.org/licenses/by-nc-sa/4.0/>.
|
|
|
|
import unittest
|
|
|
|
from os import chdir
|
|
from pathlib import Path
|
|
|
|
from typing import (
|
|
Any,
|
|
Annotated,
|
|
)
|
|
|
|
|
|
print(__name__)
|
|
print(__package__)
|
|
|
|
from src import dabmodel as dm
|
|
|
|
|
|
testdir_path = Path(__file__).parent.resolve()
|
|
chdir(testdir_path.parent.resolve())
|
|
|
|
|
|
def test_initializer_safe_testfc():
|
|
eval("print('hi')")
|
|
|
|
|
|
class FeatureTest(unittest.TestCase):
|
|
def setUp(self):
|
|
print("\n->", unittest.TestCase.id(self))
|
|
|
|
def immutable_vars__test_field(self, obj: Any, name: str, default_value: Any, test_value: Any):
|
|
# field is not in the class
|
|
self.assertNotIn(name, dir(obj.__class__))
|
|
# field is in the object
|
|
self.assertIn(name, dir(obj))
|
|
# field is in the schema
|
|
self.assertIn(name, obj.__lam_schema__.keys())
|
|
# field is readable
|
|
self.assertEqual(getattr(obj, name), default_value)
|
|
# field is read only
|
|
with self.assertRaises(dm.ReadOnlyField):
|
|
setattr(obj, name, test_value)
|
|
|
|
def test_simple(self):
|
|
"""Testing first appliance feature, and Field types (simple)"""
|
|
|
|
# class can be created
|
|
class Appliance1(dm.Appliance):
|
|
VarStrOuter: str = "testvalue APPLIANCE"
|
|
|
|
class Feature1(dm.Feature):
|
|
VarStrInner: str = "testvalue FEATURE"
|
|
|
|
app1 = Appliance1()
|
|
|
|
self.assertIsInstance(Appliance1.__lam_schema__["VarStrOuter"], dm.LAMField)
|
|
self.assertIsInstance(app1.__lam_schema__["VarStrOuter"], dm.FrozenLAMField)
|
|
self.assertIn("Feature1", app1.__lam_schema__["features"])
|
|
self.assertIn("VarStrInner", app1.__lam_schema__["features"]["Feature1"].__lam_schema__)
|
|
self.assertIsInstance(
|
|
app1.__lam_schema__["features"]["Feature1"].__lam_schema__["VarStrInner"],
|
|
dm.LAMField,
|
|
)
|
|
self.assertTrue(hasattr(app1, "Feature1"))
|
|
self.assertIsInstance(app1.Feature1.__lam_schema__["VarStrInner"], dm.FrozenLAMField)
|
|
self.assertTrue(hasattr(app1.Feature1, "VarStrInner"))
|
|
|
|
def test_inheritance(self):
|
|
"""Testing first appliance feature, and Field types (simple)"""
|
|
|
|
# class can be created
|
|
class Appliance1(dm.Appliance):
|
|
VarStrOuter: str = "testvalue APPLIANCE1"
|
|
|
|
class Feature1(dm.Feature):
|
|
VarStrInner: str = "testvalue FEATURE1"
|
|
VarInt: int = 42
|
|
|
|
print(dir(Appliance1))
|
|
|
|
class Appliance2(Appliance1):
|
|
VarStrOuter = "testvalue APPLIANCE2"
|
|
|
|
class Feature2(dm.Feature):
|
|
VarStrInner: str = "testvalue FEATURE2"
|
|
|
|
print(dir(Appliance2))
|
|
|
|
class Appliance3(Appliance2):
|
|
VarStrOuter = "testvalue APPLIANCE3"
|
|
|
|
class Feature1(Appliance1.Feature1):
|
|
VarStrInner = "testvalue FEATURE1 modded"
|
|
|
|
class Feature3(dm.Feature):
|
|
VarStrInner: str = "testvalue FEATURE3"
|
|
|
|
print(dir(Appliance3))
|
|
|
|
app1 = Appliance1()
|
|
app2 = Appliance2()
|
|
app3 = Appliance3()
|
|
|
|
self.assertIsInstance(Appliance1.__lam_schema__["VarStrOuter"], dm.LAMField)
|
|
self.assertIsInstance(app1.__lam_schema__["VarStrOuter"], dm.FrozenLAMField)
|
|
self.assertIn("Feature1", app1.__lam_schema__["features"])
|
|
self.assertIn("VarStrInner", app1.__lam_schema__["features"]["Feature1"].__lam_schema__)
|
|
self.assertIsInstance(
|
|
app1.__lam_schema__["features"]["Feature1"].__lam_schema__["VarStrInner"],
|
|
dm.LAMField,
|
|
)
|
|
self.assertTrue(hasattr(app1, "Feature1"))
|
|
self.assertIsInstance(app1.Feature1.__lam_schema__["VarStrInner"], dm.FrozenLAMField)
|
|
self.assertTrue(hasattr(app1.Feature1, "VarStrInner"))
|
|
self.assertEqual(app1.VarStrOuter, "testvalue APPLIANCE1")
|
|
self.assertEqual(app1.Feature1.VarStrInner, "testvalue FEATURE1")
|
|
self.assertEqual(app1.Feature1.VarInt, 42)
|
|
self.assertEqual(app2.VarStrOuter, "testvalue APPLIANCE2")
|
|
self.assertEqual(app2.Feature2.VarStrInner, "testvalue FEATURE2")
|
|
self.assertEqual(app3.VarStrOuter, "testvalue APPLIANCE3")
|
|
self.assertEqual(app3.Feature1.VarStrInner, "testvalue FEATURE1 modded")
|
|
self.assertEqual(app3.Feature1.VarInt, 42)
|
|
self.assertEqual(app3.Feature3.VarStrInner, "testvalue FEATURE3")
|
|
|
|
def test_inheritance2(self):
|
|
"""Testing first appliance feature, and Field types (simple)"""
|
|
|
|
# class can be created
|
|
class Appliance1(dm.Appliance):
|
|
class Feature1(dm.Feature):
|
|
VarStrInner: str = "testvalue FEATURE1"
|
|
|
|
# check cannot REdefine a feature from Feature
|
|
with self.assertRaises(dm.InvalidFeatureInheritance):
|
|
|
|
class Appliance2(Appliance1):
|
|
class Feature1(dm.Feature): ...
|
|
|
|
class Appliance2b(Appliance1):
|
|
class Feature1(Appliance1.Feature1): ...
|
|
|
|
# check only REdefine a feature from highest parent
|
|
with self.assertRaises(dm.InvalidFeatureInheritance):
|
|
|
|
class Appliance3(Appliance2b):
|
|
class Feature1(Appliance1.Feature1): ...
|
|
|
|
class Appliance3b(Appliance2b):
|
|
class Feature1(Appliance2b.Feature1): ...
|
|
|
|
app1 = Appliance1()
|
|
app2 = Appliance2b()
|
|
app3 = Appliance3b()
|
|
|
|
self.assertEqual(app1.Feature1.VarStrInner, "testvalue FEATURE1")
|
|
self.assertEqual(app2.Feature1.VarStrInner, "testvalue FEATURE1")
|
|
self.assertEqual(app3.Feature1.VarStrInner, "testvalue FEATURE1")
|
|
|
|
class Appliance4(Appliance3b):
|
|
class Feature1(Appliance3b.Feature1):
|
|
VarStrInner = "testvalue FEATURE4"
|
|
|
|
self.assertEqual(app1.Feature1.VarStrInner, "testvalue FEATURE1")
|
|
self.assertEqual(app2.Feature1.VarStrInner, "testvalue FEATURE1")
|
|
self.assertEqual(app3.Feature1.VarStrInner, "testvalue FEATURE1")
|
|
|
|
app4 = Appliance4()
|
|
|
|
self.assertEqual(app1.Feature1.VarStrInner, "testvalue FEATURE1")
|
|
self.assertEqual(app2.Feature1.VarStrInner, "testvalue FEATURE1")
|
|
self.assertEqual(app3.Feature1.VarStrInner, "testvalue FEATURE1")
|
|
self.assertEqual(app4.Feature1.VarStrInner, "testvalue FEATURE4")
|
|
|
|
def test_new_field_forbidden(self):
|
|
class App(dm.Appliance):
|
|
x: int = 1
|
|
|
|
app = App()
|
|
with self.assertRaises(dm.NewFieldForbidden):
|
|
app.y = 2
|
|
|
|
def test_inherit_declared(self):
|
|
class App(dm.Appliance):
|
|
class F1(dm.Feature):
|
|
val: int = 1
|
|
|
|
class MyF1(App.F1):
|
|
val = 2
|
|
val2: str = "toto"
|
|
|
|
app = App(F1=MyF1)
|
|
self.assertIsInstance(app.F1, MyF1)
|
|
self.assertEqual(app.F1.val, 2)
|
|
self.assertEqual(app.F1.val2, "toto")
|
|
|
|
def test_override_declared(self):
|
|
class App(dm.Appliance):
|
|
class F1(dm.Feature):
|
|
val: int = 1
|
|
val2: str = "toto"
|
|
|
|
app = App(F1={"val": 42, "val2": "tata"})
|
|
self.assertEqual(app.F1.val, 42)
|
|
self.assertEqual(app.F1.val2, "tata")
|
|
|
|
def test_dict_override_type_error(self):
|
|
class App(dm.Appliance):
|
|
class F1(dm.Feature):
|
|
val: int = 1
|
|
|
|
# wrong type for val → must raise InvalidFieldValue
|
|
with self.assertRaises(dm.InvalidFieldValue):
|
|
App(F1={"val": "not-an-int"})
|
|
|
|
def test_dict_override_nonexisting_field(self):
|
|
class App(dm.Appliance):
|
|
class F1(dm.Feature):
|
|
val: int = 1
|
|
|
|
# field does not exist → must raise
|
|
with self.assertRaises(dm.InvalidFieldValue):
|
|
App(F1={"doesnotexist": 123})
|
|
|
|
def test_inheritance_with_extra_fields(self):
|
|
class App(dm.Appliance):
|
|
class F1(dm.Feature):
|
|
val: int = 1
|
|
|
|
class MyF1(App.F1):
|
|
val = 2
|
|
extra: str = "hello"
|
|
|
|
app = App(F1=MyF1)
|
|
self.assertEqual(app.F1.val, 2)
|
|
self.assertEqual(app.F1.extra, "hello")
|
|
|
|
def test_override_does_not_leak_between_instances(self):
|
|
class App(dm.Appliance):
|
|
class F1(dm.Feature):
|
|
val: int = 1
|
|
|
|
app1 = App(F1={"val": 99})
|
|
app2 = App()
|
|
self.assertEqual(app1.F1.val, 99)
|
|
self.assertEqual(app2.F1.val, 1)
|
|
|
|
def test_deepfreeze_nested_mixed_tuple_list(self):
|
|
class App(dm.Appliance):
|
|
data: tuple[list[int], tuple[int, list[int]]] = ([1, 2], (3, [4, 5]))
|
|
|
|
app = App()
|
|
|
|
# Top-level: must be tuple
|
|
self.assertIsInstance(app.data, tuple)
|
|
|
|
# First element of tuple: should have been frozen to tuple, not list
|
|
self.assertIsInstance(app.data[0], tuple)
|
|
|
|
# Nested second element: itself a tuple
|
|
self.assertIsInstance(app.data[1], tuple)
|
|
|
|
# Deepest element: inner list should also be frozen to tuple
|
|
self.assertIsInstance(app.data[1][1], tuple)
|
|
|
|
# Check immutability
|
|
with self.assertRaises(TypeError):
|
|
app.data[0] += (99,) # tuples are immutable
|
|
|
|
with self.assertRaises(TypeError):
|
|
app.data[1][1] += (42,) # inner tuple also immutable
|
|
|
|
def test_inacurate_type(self):
|
|
with self.assertRaises(dm.InvalidFieldAnnotation):
|
|
|
|
class Appliance1(dm.Appliance):
|
|
SomeVar: list = []
|
|
|
|
with self.assertRaises(dm.InvalidFieldAnnotation):
|
|
|
|
class Appliance2(dm.Appliance):
|
|
SomeVar: list[Any] = []
|
|
|
|
with self.assertRaises(dm.InvalidFieldAnnotation):
|
|
|
|
class Appliance3(dm.Appliance):
|
|
SomeVar: list[object] = []
|
|
|
|
with self.assertRaises(dm.InvalidFieldAnnotation):
|
|
|
|
class Appliance4(dm.Appliance):
|
|
SomeVar: dict = {}
|
|
|
|
with self.assertRaises(dm.InvalidFieldAnnotation):
|
|
|
|
class Appliance5(dm.Appliance):
|
|
SomeVar: dict[str, Any] = {}
|
|
|
|
with self.assertRaises(dm.InvalidFieldAnnotation):
|
|
|
|
class Appliance6(dm.Appliance):
|
|
SomeVar: dict[Any, Any] = {}
|
|
|
|
with self.assertRaises(dm.InvalidFieldAnnotation):
|
|
|
|
class Appliance7(dm.Appliance):
|
|
SomeVar: dict[Any, str] = {}
|
|
|
|
with self.assertRaises(dm.InvalidFieldAnnotation):
|
|
|
|
class Appliance8(dm.Appliance):
|
|
SomeVar: dict[str, object] = {}
|
|
|
|
def test_cant_override_inherited_annotation(self):
|
|
class App(dm.Appliance):
|
|
class F1(dm.Feature):
|
|
val: int = 1
|
|
|
|
with self.assertRaises(dm.ReadOnlyFieldAnnotation):
|
|
|
|
class Extra(App.F1):
|
|
val: str = "test"
|
|
|
|
def test_fields_are_frozen_after_override(self):
|
|
class App(dm.Appliance):
|
|
class F(dm.Feature):
|
|
nums: list[int] = [1, 2]
|
|
tag: str = "x"
|
|
|
|
# dict override
|
|
app1 = App(F={"nums": [9], "tag": "y"})
|
|
self.assertEqual(app1.F.nums, (9,))
|
|
self.assertEqual(app1.F.tag, "y")
|
|
with self.assertRaises(AttributeError):
|
|
app1.F.nums.append(3) # tuple
|
|
|
|
# subclass override
|
|
class F2(App.F):
|
|
nums = [4, 5]
|
|
|
|
app2 = App(F=F2)
|
|
self.assertEqual(app2.F.nums, (4, 5))
|
|
with self.assertRaises(dm.ReadOnlyField):
|
|
app2.F.nums += (6,) # still immutable
|
|
|
|
def test_dict_partial_override_keeps_other_defaults(self):
|
|
class App(dm.Appliance):
|
|
class F(dm.Feature):
|
|
a: int = 1
|
|
b: str = "k"
|
|
|
|
app = App(F={"b": "z"})
|
|
self.assertEqual(app.F.a, 1) # default remains
|
|
self.assertEqual(app.F.b, "z") # overridden
|
|
|
|
def test_override_linear_chain(self):
|
|
# Base appliance defines Feat1
|
|
class A(dm.Appliance):
|
|
class Feat1(dm.Feature):
|
|
x: int = 1
|
|
|
|
# ✅ Appliance B overrides Feat1 by subclassing A.Feat1
|
|
class B(A):
|
|
class Feat1(A.Feat1):
|
|
y: int = 2
|
|
|
|
self.assertTrue(issubclass(B.Feat1, A.Feat1))
|
|
|
|
# ✅ Appliance C overrides Feat1 again by subclassing B.Feat1 (not A.Feat1)
|
|
class C(B):
|
|
class Feat1(B.Feat1):
|
|
z: int = 3
|
|
|
|
self.assertTrue(issubclass(C.Feat1, B.Feat1))
|
|
self.assertTrue(issubclass(C.Feat1, A.Feat1))
|
|
|
|
# ❌ Bad: D tries to override with a *fresh* Feature, not subclass of B.Feat1
|
|
with self.assertRaises(dm.InvalidFeatureInheritance):
|
|
|
|
class D(B):
|
|
class Feat1(dm.Feature):
|
|
fail: str = "oops"
|
|
|
|
# ❌ Bad: E tries to override with ancestor (A.Feat1) instead of B.Feat1
|
|
with self.assertRaises(dm.InvalidFeatureInheritance):
|
|
|
|
class E(B):
|
|
class Feat1(A.Feat1):
|
|
fail: str = "oops"
|
|
|
|
# ✅ New feature name in child is always fine
|
|
class F(B):
|
|
class Feat2(dm.Feature):
|
|
other: str = "ok"
|
|
|
|
self.assertTrue(hasattr(F, "Feat2"))
|
|
|
|
def test_override_chain_runtime_replacement(self):
|
|
# Build a linear chain: A -> B -> C for feature 'Feat1'
|
|
class A(dm.Appliance):
|
|
class Feat1(dm.Feature):
|
|
x: int = 1
|
|
|
|
class B(A):
|
|
class Feat1(A.Feat1):
|
|
y: int = 2
|
|
|
|
class C(B):
|
|
class Feat1(B.Feat1):
|
|
z: int = 3
|
|
|
|
# ✅ OK: at instantiation of C, replacing Feat1 with a subclass of the LATEST (C.Feat1)
|
|
class CFeat1Plus(C.Feat1):
|
|
w: int = 4
|
|
|
|
c_ok = C(Feat1=CFeat1Plus)
|
|
self.assertIsInstance(c_ok.Feat1, CFeat1Plus)
|
|
self.assertEqual((c_ok.Feat1.x, c_ok.Feat1.y, c_ok.Feat1.z, c_ok.Feat1.w), (1, 2, 3, 4))
|
|
|
|
# ❌ Not OK: replacing with a subclass of the ancestor (A.Feat1) — must target latest (C.Feat1)
|
|
class AFeat1Alt(A.Feat1):
|
|
pass
|
|
|
|
with self.assertRaises(dm.InvalidFieldValue):
|
|
C(Feat1=AFeat1Alt)
|
|
|
|
# ❌ Not OK: replacing with a subclass of the mid ancestor (B.Feat1) — still must target latest (C.Feat1)
|
|
class BFeat1Alt(B.Feat1):
|
|
pass
|
|
|
|
with self.assertRaises(dm.InvalidFieldValue):
|
|
C(Feat1=BFeat1Alt)
|
|
|
|
def test_inheritance_tree_and_no_leakage(self):
|
|
class A(dm.Appliance):
|
|
class F1(dm.Feature):
|
|
a: int = 1
|
|
|
|
class F2(dm.Feature):
|
|
b: int = 2
|
|
|
|
# ✅ Child inherits both features automatically
|
|
class B(A):
|
|
c: str = "extra"
|
|
|
|
b1 = B()
|
|
self.assertIsInstance(b1.F1, A.F1)
|
|
self.assertIsInstance(b1.F2, A.F2)
|
|
self.assertEqual((b1.F1.a, b1.F2.b, b1.c), (1, 2, "extra"))
|
|
|
|
# ✅ Override only F2, F1 should still come from A
|
|
class C(B):
|
|
class F2(B.F2):
|
|
bb: int = 22
|
|
|
|
c1 = C()
|
|
self.assertIsInstance(c1.F1, A.F1) # unchanged
|
|
self.assertIsInstance(c1.F2, C.F2) # overridden
|
|
self.assertEqual((c1.F1.a, c1.F2.b, c1.F2.bb), (1, 2, 22))
|
|
|
|
# ✅ No leakage: instances of B are not affected by C's override
|
|
b2 = B()
|
|
self.assertIsInstance(b2.F2, A.F2)
|
|
self.assertFalse(hasattr(b2.F2, "bb"))
|
|
|
|
# ✅ Adding a new feature in D is independent of previous appliances
|
|
class D(C):
|
|
class F3(dm.Feature):
|
|
d: int = 3
|
|
|
|
d1 = D()
|
|
self.assertIsInstance(d1.F1, A.F1)
|
|
self.assertIsInstance(d1.F2, C.F2)
|
|
self.assertIsInstance(d1.F3, D.F3)
|
|
|
|
# ✅ No leakage: instances of A and B should not see F3
|
|
a1 = A()
|
|
self.assertFalse(hasattr(a1, "F3"))
|
|
b3 = B()
|
|
self.assertFalse(hasattr(b3, "F3"))
|
|
|
|
def test_appliance_inheritance_tree_isolation(self):
|
|
class A(dm.Appliance):
|
|
class F1(dm.Feature):
|
|
a: int = 1
|
|
|
|
# Branch 1 overrides F1
|
|
class B(A):
|
|
class F1(A.F1):
|
|
b: int = 2
|
|
|
|
# Branch 2 also overrides F1 differently
|
|
class C(A):
|
|
class F1(A.F1):
|
|
c: int = 3
|
|
|
|
# ✅ Instances of B use B.F1
|
|
b = B()
|
|
self.assertIsInstance(b.F1, B.F1)
|
|
print(b.F1)
|
|
print(dir(b.F1))
|
|
self.assertEqual((b.F1.a, b.F1.b), (1, 2))
|
|
self.assertFalse(hasattr(b.F1, "c"))
|
|
|
|
# ✅ Instances of C use C.F1
|
|
c = C()
|
|
self.assertIsInstance(c.F1, C.F1)
|
|
self.assertEqual((c.F1.a, c.F1.c), (1, 3))
|
|
self.assertFalse(hasattr(c.F1, "b"))
|
|
|
|
# ✅ Base appliance A still uses its original feature
|
|
a = A()
|
|
self.assertIsInstance(a.F1, A.F1)
|
|
self.assertEqual(a.F1.a, 1)
|
|
self.assertFalse(hasattr(a.F1, "b"))
|
|
self.assertFalse(hasattr(a.F1, "c"))
|
|
|
|
# ✅ No leakage: B's override doesn't affect C and vice versa
|
|
b2 = B()
|
|
c2 = C()
|
|
self.assertTrue(hasattr(b2.F1, "b"))
|
|
self.assertFalse(hasattr(b2.F1, "c"))
|
|
self.assertTrue(hasattr(c2.F1, "c"))
|
|
self.assertFalse(hasattr(c2.F1, "b"))
|
|
|
|
def test_appliance_inheritance_tree_runtime_attach_isolation(self):
|
|
class A(dm.Appliance):
|
|
class F1(dm.Feature):
|
|
a: int = 1
|
|
|
|
class B(A):
|
|
class F1(A.F1):
|
|
b: int = 2
|
|
|
|
class C(A):
|
|
class F1(A.F1):
|
|
c: int = 3
|
|
|
|
# Define new runtime-attachable features
|
|
class FextraB(B.F1):
|
|
xb: int = 99
|
|
|
|
class FextraC(C.F1):
|
|
xc: int = -99
|
|
|
|
# ✅ Attach to B at instantiation
|
|
b = B(F1=FextraB)
|
|
self.assertIsInstance(b.F1, FextraB)
|
|
self.assertEqual((b.F1.a, b.F1.b, b.F1.xb), (1, 2, 99))
|
|
self.assertFalse(hasattr(b.F1, "c"))
|
|
self.assertFalse(hasattr(b.F1, "xc"))
|
|
|
|
# ✅ Attach to C at instantiation
|
|
c = C(F1=FextraC)
|
|
self.assertIsInstance(c.F1, FextraC)
|
|
self.assertEqual((c.F1.a, c.F1.c, c.F1.xc), (1, 3, -99))
|
|
self.assertFalse(hasattr(c.F1, "b"))
|
|
self.assertFalse(hasattr(c.F1, "xb"))
|
|
|
|
# ✅ Base appliance still untouched
|
|
a = A()
|
|
self.assertIsInstance(a.F1, A.F1)
|
|
self.assertEqual(a.F1.a, 1)
|
|
self.assertFalse(hasattr(a.F1, "b"))
|
|
self.assertFalse(hasattr(a.F1, "c"))
|
|
self.assertFalse(hasattr(a.F1, "xb"))
|
|
self.assertFalse(hasattr(a.F1, "xc"))
|
|
|
|
# ✅ Repeated instantiations stay isolated
|
|
b2 = B()
|
|
c2 = C()
|
|
self.assertIsInstance(b2.F1, B.F1)
|
|
self.assertIsInstance(c2.F1, C.F1)
|
|
self.assertFalse(hasattr(b2.F1, "xb"))
|
|
self.assertFalse(hasattr(c2.F1, "xc"))
|
|
|
|
def test_feature_dict_override_with_nested_containers(self):
|
|
class App(dm.Appliance):
|
|
class F1(dm.Feature):
|
|
values: list[int] = [1, 2]
|
|
|
|
app = App(F1={"values": [5, 6]})
|
|
self.assertEqual(app.F1.values, (5, 6)) # deepfreeze → tuple
|
|
|
|
# Invalid type in list should fail
|
|
with self.assertRaises(dm.InvalidFieldValue):
|
|
App(F1={"values": [1, "oops"]})
|
|
|
|
def test_dict_override_with_unknown_key(self):
|
|
class App(dm.Appliance):
|
|
class F1(dm.Feature):
|
|
a: int = 1
|
|
|
|
# Dict override with unknown field 'zzz'
|
|
with self.assertRaises(dm.InvalidFieldValue):
|
|
App(F1={"zzz": 42})
|
|
|
|
def test_schema_isolation_across_multiple_overrides(self):
|
|
class App(dm.Appliance):
|
|
class F1(dm.Feature):
|
|
a: int = 1
|
|
|
|
class F1a(App.F1):
|
|
a = 10
|
|
|
|
class F1b(App.F1):
|
|
a = 20
|
|
|
|
app1 = App(F1=F1a)
|
|
self.assertIsInstance(app1.F1, F1a)
|
|
self.assertEqual(app1.F1.a, 10)
|
|
|
|
app2 = App(F1=F1b)
|
|
self.assertIsInstance(app2.F1, F1b)
|
|
self.assertEqual(app2.F1.a, 20)
|
|
|
|
# Original appliance schema must not be polluted
|
|
app3 = App()
|
|
self.assertIsInstance(app3.F1, App.F1)
|
|
self.assertEqual(app3.F1.a, 1)
|
|
|
|
def test_inheritance_with_annotated_fields(self):
|
|
|
|
class App(dm.Appliance):
|
|
class F1(dm.Feature):
|
|
a: Annotated[int, dm.LAMFieldInfo(doc="field a")] = 1
|
|
|
|
# ✅ Subclass override must inherit from parent F1
|
|
class F1Ex(App.F1):
|
|
b: str = "ok"
|
|
|
|
app = App(F1=F1Ex)
|
|
self.assertIsInstance(app.F1, F1Ex)
|
|
self.assertEqual((app.F1.a, app.F1.b), (1, "ok"))
|
|
|
|
# ❌ Wrong: fresh Feature under same name
|
|
with self.assertRaises(dm.InvalidFeatureInheritance):
|
|
|
|
class Bad(App):
|
|
class F1(dm.Feature):
|
|
fail: str = "oops"
|
|
|
|
|
|
# ---------- main ----------
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|