|
|
|
|
@@ -1876,14 +1876,6 @@ class MainTests(unittest.TestCase):
|
|
|
|
|
self.immutable_vars__test_field(app2, "testVar5", ("a", "b"), ("h", "c"))
|
|
|
|
|
self.immutable_vars__test_field(app2, "testVar6", ("a", "b"), ("h", "c"))
|
|
|
|
|
|
|
|
|
|
def test_field_is_readonly(self):
|
|
|
|
|
class App(dm.BaseAppliance):
|
|
|
|
|
x: int = 1
|
|
|
|
|
|
|
|
|
|
app = App()
|
|
|
|
|
with self.assertRaises(dm.ReadOnlyField):
|
|
|
|
|
app.x = 2
|
|
|
|
|
|
|
|
|
|
def test_new_field_forbidden(self):
|
|
|
|
|
class App(dm.BaseAppliance):
|
|
|
|
|
x: int = 1
|
|
|
|
|
@@ -1974,22 +1966,6 @@ class MainTests(unittest.TestCase):
|
|
|
|
|
with self.assertRaises(dm.ReadOnlyField):
|
|
|
|
|
app.x = 99
|
|
|
|
|
|
|
|
|
|
def test_new_public_field_forbidden(self):
|
|
|
|
|
class App(dm.BaseAppliance):
|
|
|
|
|
x: int = 1
|
|
|
|
|
|
|
|
|
|
app = App()
|
|
|
|
|
with self.assertRaises(dm.NewFieldForbidden):
|
|
|
|
|
app.newfield = "oops"
|
|
|
|
|
|
|
|
|
|
def test_private_field_allowed_assignment(self):
|
|
|
|
|
class App(dm.BaseAppliance):
|
|
|
|
|
x: int = 1
|
|
|
|
|
|
|
|
|
|
app = App()
|
|
|
|
|
app._debug = "ok"
|
|
|
|
|
self.assertEqual(app._debug, "ok")
|
|
|
|
|
|
|
|
|
|
def test_feature_override_does_not_leak_between_instances(self):
|
|
|
|
|
class App(dm.BaseAppliance):
|
|
|
|
|
class F1(dm.BaseFeature):
|
|
|
|
|
@@ -2071,31 +2047,6 @@ class MainTests(unittest.TestCase):
|
|
|
|
|
class C(A, B):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
def test_wrong_feature_override(self):
|
|
|
|
|
class App(dm.BaseAppliance):
|
|
|
|
|
class F1(dm.BaseFeature):
|
|
|
|
|
val: int = 1
|
|
|
|
|
|
|
|
|
|
with self.assertRaises(dm.InvalidFieldValue):
|
|
|
|
|
App(F1={"doesnotexist": 123})
|
|
|
|
|
|
|
|
|
|
def test_nested_list_frozen(self):
|
|
|
|
|
class A(dm.BaseAppliance):
|
|
|
|
|
x: list[list[int]] = [[1, 2]]
|
|
|
|
|
|
|
|
|
|
a = A()
|
|
|
|
|
with self.assertRaises(AttributeError):
|
|
|
|
|
a.x[0].append(3) # inner list should also be frozen
|
|
|
|
|
|
|
|
|
|
def test_feature_override_no_leakage(self):
|
|
|
|
|
class App(dm.BaseAppliance):
|
|
|
|
|
class F1(dm.BaseFeature):
|
|
|
|
|
val: int = 1
|
|
|
|
|
|
|
|
|
|
a1 = App(F1={"val": 99})
|
|
|
|
|
a2 = App()
|
|
|
|
|
self.assertEqual(a2.F1.val, 1) # no leakage
|
|
|
|
|
|
|
|
|
|
def test_initializer_can_use_math(self):
|
|
|
|
|
|
|
|
|
|
class App(dm.BaseAppliance):
|
|
|
|
|
@@ -2110,6 +2061,426 @@ class MainTests(unittest.TestCase):
|
|
|
|
|
# math.ceil(1.2)=2, math.floor(2.8)=2 → 4
|
|
|
|
|
self.assertEqual(app.val, 4)
|
|
|
|
|
|
|
|
|
|
def test_deepfreeze_nested_mixed_tuple_list(self):
|
|
|
|
|
class App(dm.BaseAppliance):
|
|
|
|
|
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.BaseAppliance):
|
|
|
|
|
SomeVar: list = []
|
|
|
|
|
|
|
|
|
|
with self.assertRaises(dm.InvalidFieldAnnotation):
|
|
|
|
|
|
|
|
|
|
class Appliance2(dm.BaseAppliance):
|
|
|
|
|
SomeVar: list[Any] = []
|
|
|
|
|
|
|
|
|
|
with self.assertRaises(dm.InvalidFieldAnnotation):
|
|
|
|
|
|
|
|
|
|
class Appliance3(dm.BaseAppliance):
|
|
|
|
|
SomeVar: list[object] = []
|
|
|
|
|
|
|
|
|
|
with self.assertRaises(dm.InvalidFieldAnnotation):
|
|
|
|
|
|
|
|
|
|
class Appliance4(dm.BaseAppliance):
|
|
|
|
|
SomeVar: dict = {}
|
|
|
|
|
|
|
|
|
|
with self.assertRaises(dm.InvalidFieldAnnotation):
|
|
|
|
|
|
|
|
|
|
class Appliance5(dm.BaseAppliance):
|
|
|
|
|
SomeVar: dict[str, Any] = {}
|
|
|
|
|
|
|
|
|
|
with self.assertRaises(dm.InvalidFieldAnnotation):
|
|
|
|
|
|
|
|
|
|
class Appliance6(dm.BaseAppliance):
|
|
|
|
|
SomeVar: dict[Any, Any] = {}
|
|
|
|
|
|
|
|
|
|
with self.assertRaises(dm.InvalidFieldAnnotation):
|
|
|
|
|
|
|
|
|
|
class Appliance7(dm.BaseAppliance):
|
|
|
|
|
SomeVar: dict[Any, str] = {}
|
|
|
|
|
|
|
|
|
|
with self.assertRaises(dm.InvalidFieldAnnotation):
|
|
|
|
|
|
|
|
|
|
class Appliance8(dm.BaseAppliance):
|
|
|
|
|
SomeVar: dict[str, object] = {}
|
|
|
|
|
|
|
|
|
|
def test_deepfreeze_nested_dict_list_set(self):
|
|
|
|
|
class App(dm.BaseAppliance):
|
|
|
|
|
data: dict[str, list[int] | set[str] | dict[str, list[int] | set[str]]] = {
|
|
|
|
|
"numbers": [1, 2, 3],
|
|
|
|
|
"letters": {"a", "b", "c"},
|
|
|
|
|
"mixed": {"x": [4, 5], "y": {"z"}},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
app = App()
|
|
|
|
|
|
|
|
|
|
# Top-level: should be frozendict
|
|
|
|
|
self.assertEqual(type(app.data).__name__, "frozendict")
|
|
|
|
|
|
|
|
|
|
# Lists must be frozen to tuple
|
|
|
|
|
self.assertIsInstance(app.data["numbers"], tuple)
|
|
|
|
|
self.assertIsInstance(app.data["mixed"]["x"], tuple)
|
|
|
|
|
|
|
|
|
|
# Sets must be frozen to frozenset
|
|
|
|
|
self.assertIsInstance(app.data["letters"], frozenset)
|
|
|
|
|
self.assertIsInstance(app.data["mixed"]["y"], frozenset)
|
|
|
|
|
|
|
|
|
|
# Check immutability
|
|
|
|
|
with self.assertRaises(TypeError):
|
|
|
|
|
app.data["numbers"] += (99,)
|
|
|
|
|
|
|
|
|
|
with self.assertRaises(AttributeError):
|
|
|
|
|
app.data["letters"].add("d")
|
|
|
|
|
|
|
|
|
|
with self.assertRaises(TypeError):
|
|
|
|
|
app.data["mixed"]["x"] += (6,)
|
|
|
|
|
|
|
|
|
|
def test_unknown_parameters_raise(self):
|
|
|
|
|
class App(dm.BaseAppliance):
|
|
|
|
|
a: int = 1
|
|
|
|
|
|
|
|
|
|
with self.assertRaises(dm.InvalidFieldValue) as cm:
|
|
|
|
|
App(foo=123)
|
|
|
|
|
|
|
|
|
|
self.assertIn("Unknown parameters:", str(cm.exception))
|
|
|
|
|
self.assertIn("foo", str(cm.exception))
|
|
|
|
|
|
|
|
|
|
def test_variadic_tuple_annotation_ok_and_errors(self):
|
|
|
|
|
class A(dm.BaseAppliance):
|
|
|
|
|
xs: tuple[int, ...] = (1, 2, 3)
|
|
|
|
|
|
|
|
|
|
self.assertEqual(A().xs, (1, 2, 3))
|
|
|
|
|
|
|
|
|
|
class B(dm.BaseAppliance):
|
|
|
|
|
xs: "Tuple[int, ...]" = (4, 5)
|
|
|
|
|
|
|
|
|
|
self.assertEqual(B().xs, (4, 5))
|
|
|
|
|
|
|
|
|
|
# wrong inner type should fail
|
|
|
|
|
with self.assertRaises(dm.InvalidFieldValue):
|
|
|
|
|
|
|
|
|
|
class _Bad(dm.BaseAppliance):
|
|
|
|
|
xs: tuple[int, ...] = (1, "x")
|
|
|
|
|
|
|
|
|
|
def test_field_override_typechecked_and_instance_local(self):
|
|
|
|
|
class App(dm.BaseAppliance):
|
|
|
|
|
a: list[int] = [1]
|
|
|
|
|
|
|
|
|
|
app1 = App(a=[9])
|
|
|
|
|
app2 = App()
|
|
|
|
|
# deepfreeze: lists become tuples
|
|
|
|
|
self.assertEqual(app1.a, (9,))
|
|
|
|
|
self.assertEqual(app2.a, (1,)) # no leakage
|
|
|
|
|
|
|
|
|
|
with self.assertRaises(dm.InvalidFieldValue):
|
|
|
|
|
App(a=["oops"]) # wrong inner type
|
|
|
|
|
|
|
|
|
|
def test_runtime_attach_bound_feature_success(self):
|
|
|
|
|
class App(dm.BaseAppliance):
|
|
|
|
|
class F1(dm.BaseFeature):
|
|
|
|
|
val: int = 1
|
|
|
|
|
|
|
|
|
|
class Extra(App.F1): # stays bound to App
|
|
|
|
|
val = 7
|
|
|
|
|
|
|
|
|
|
app = App(Extra=Extra)
|
|
|
|
|
self.assertTrue(hasattr(app, "Extra"))
|
|
|
|
|
self.assertIsInstance(app.Extra, Extra)
|
|
|
|
|
self.assertEqual(app.Extra.val, 7)
|
|
|
|
|
|
|
|
|
|
def test_cant_override_inherited_feature_annotation(self):
|
|
|
|
|
class App(dm.BaseAppliance):
|
|
|
|
|
class F1(dm.BaseFeature):
|
|
|
|
|
val: int = 1
|
|
|
|
|
|
|
|
|
|
with self.assertRaises(dm.ReadOnlyFieldAnnotation):
|
|
|
|
|
|
|
|
|
|
class Extra(App.F1):
|
|
|
|
|
val: str = "test"
|
|
|
|
|
|
|
|
|
|
def test_feature_fields_are_frozen_after_override(self):
|
|
|
|
|
class App(dm.BaseAppliance):
|
|
|
|
|
class F(dm.BaseFeature):
|
|
|
|
|
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_feature_dict_partial_override_keeps_other_defaults(self):
|
|
|
|
|
class App(dm.BaseAppliance):
|
|
|
|
|
class F(dm.BaseFeature):
|
|
|
|
|
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_root_field_override_nonexisting_rejected(self):
|
|
|
|
|
class App(dm.BaseAppliance):
|
|
|
|
|
x: int = 1
|
|
|
|
|
|
|
|
|
|
with self.assertRaises(dm.InvalidFieldValue):
|
|
|
|
|
App(y=2) # not in schema -> unknown parameter
|
|
|
|
|
|
|
|
|
|
def test_feature_override_linear_chain(self):
|
|
|
|
|
# Base appliance defines Feat1
|
|
|
|
|
class A(dm.BaseAppliance):
|
|
|
|
|
class Feat1(dm.BaseFeature):
|
|
|
|
|
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* BaseFeature, not subclass of B.Feat1
|
|
|
|
|
with self.assertRaises(dm.InvalidFeatureInheritance):
|
|
|
|
|
|
|
|
|
|
class D(B):
|
|
|
|
|
class Feat1(dm.BaseFeature):
|
|
|
|
|
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.BaseFeature):
|
|
|
|
|
other: str = "ok"
|
|
|
|
|
|
|
|
|
|
self.assertTrue(hasattr(F, "Feat2"))
|
|
|
|
|
|
|
|
|
|
def test_feature_override_chain_runtime_replacement(self):
|
|
|
|
|
# Build a linear chain: A -> B -> C for feature 'Feat1'
|
|
|
|
|
class A(dm.BaseAppliance):
|
|
|
|
|
class Feat1(dm.BaseFeature):
|
|
|
|
|
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_feature_inheritance_tree_and_no_leakage(self):
|
|
|
|
|
class A(dm.BaseAppliance):
|
|
|
|
|
class F1(dm.BaseFeature):
|
|
|
|
|
a: int = 1
|
|
|
|
|
|
|
|
|
|
class F2(dm.BaseFeature):
|
|
|
|
|
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.BaseFeature):
|
|
|
|
|
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_feature_isolation(self):
|
|
|
|
|
class A(dm.BaseAppliance):
|
|
|
|
|
class F1(dm.BaseFeature):
|
|
|
|
|
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)
|
|
|
|
|
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.BaseAppliance):
|
|
|
|
|
class F1(dm.BaseFeature):
|
|
|
|
|
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"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------- main ----------
|
|
|
|
|
|
|
|
|
|
|