From 981c5201a9028f802d9fcda4dcbef22ca5a67f69 Mon Sep 17 00:00:00 2001 From: chacha <15073640+cclecle@users.noreply.github.com> Date: Tue, 16 Sep 2025 23:40:41 +0200 Subject: [PATCH] partially fix features --- src/dabmodel/__init__.py | 3 +- src/dabmodel/model.py | 102 ++++++++++++++++-------------- test/test_model.py | 133 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 179 insertions(+), 59 deletions(-) diff --git a/src/dabmodel/__init__.py b/src/dabmodel/__init__.py index cd289cd..ef31d9a 100644 --- a/src/dabmodel/__init__.py +++ b/src/dabmodel/__init__.py @@ -28,5 +28,6 @@ from .model import ( IncompletelyAnnotatedField, ImportForbidden, FunctionForbidden, - FrozenDABField + FrozenDABField, + InvalidFeatureInheritance, ) diff --git a/src/dabmodel/model.py b/src/dabmodel/model.py index 5765c4e..0e04e34 100644 --- a/src/dabmodel/model.py +++ b/src/dabmodel/model.py @@ -141,6 +141,10 @@ class ImportForbidden(DABModelException): Imports are forbidden """ +class InvalidFeatureInheritance(DABModelException): + """InvalidFeatureInheritance Exception class + Features of same name in child appliance need to be from same type + """ class FunctionForbidden(DABModelException): """FunctionForbidden Exception class @@ -579,8 +583,7 @@ class BaseMeta(type): raise InvalidFieldAnnotation("Only DABFieldInfo object is allowed as Annotated data.") _finfo = args[1] - - # print(f"annotation is: {namespace['__annotations__'][_fname]}") + # check if value is valid try: check_type(_fvalue, namespace["__annotations__"][_fname], collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS) @@ -597,8 +600,9 @@ class BaseMeta(type): init_fieldvalues = {} init_fieldtypes = {} for _fname, _fvalue in cls.__DABSchema__.items(): - init_fieldvalues[_fname] = deepcopy(_fvalue.raw_value) - init_fieldtypes[_fname] = _fvalue.annotations + if isinstance(_fvalue,DABField): + init_fieldvalues[_fname] = deepcopy(_fvalue.raw_value) + init_fieldtypes[_fname] = _fvalue.annotations fakecls = ModelSpecView(init_fieldvalues, init_fieldtypes, cls.__name__, cls.__module__) safe_globals = {"__builtins__": {"__import__": _blocked_import}, **ALLOWED_HELPERS_DEFAULT} if mcs.initializer.__code__.co_freevars: @@ -629,7 +633,8 @@ class BaseMeta(type): mcs.save_values(_cls, name, bases, namespace) mcs.call_initializer(_cls, name, bases, namespace) - + _cls.install_guard() + return _cls @classmethod @@ -643,43 +648,51 @@ class BaseMeta(type): for _fname, _fvalue in mcs.new_fields.items(): _fvalue.add_source(mcs) cls.__DABSchema__[_fname] = _fvalue - - def __call__(cls, *args: Any, **kw: Any): # intentionally untyped + + def __call__(cls: Type, *args: Any, **kw: Any): # intentionally untyped """BaseElement new instance""" obj = super().__call__(*args, **kw) for _fname, _fvalue in cls.__DABSchema__.items(): - setattr(obj, _fname, _fvalue.value) - inst_schema: dict[str, Any] = {} - for _fname, _fvalue in cls.__DABSchema__.items(): - inst_schema[_fname] = FrozenDABField(_fvalue) - #print(inst_schema) - setattr(obj, "__DABSchema__", inst_schema) + if isinstance(_fvalue,DABField): + object.__setattr__(obj, _fname, _fvalue.value) - + inst_schema = copy(obj.__DABSchema__) + for _fname, _fvalue in cls.__DABSchema__.items(): + if isinstance(_fvalue,DABField): + inst_schema[_fname] = FrozenDABField(_fvalue) + + object.__setattr__(obj, "__DABSchema__", inst_schema) cls.modify_object(obj) - cls.install_guard(obj) + + return obj - def modify_object(self,obj): + def modify_object(cls:Type,obj): pass - def install_guard(self, obj): - orig_setattr = getattr(self,"__setattr__") - + + def install_guard(cls:Type): + orig_setattr = getattr(cls,"__setattr__") + + # cls.orig_setattr = orig_setattr + def guarded_setattr(_self, key: str, value: Any): if key.startswith("_"): # allow private and dunder attrs return orig_setattr(_self, key, value) # block writes after init if key is readonly if key in _self.__DABSchema__.keys(): - if hasattr(_self, key): + if key in _self.__dict__: + raise ReadOnlyField(f"{key} is read-only") + elif key in _self.__DABSchema__["features"].keys(): + if key in _self.__dict__: raise ReadOnlyField(f"{key} is read-only") else: raise NewFieldForbidden("creating new fields is not allowed") return orig_setattr(_self, key, value) - setattr(self, "__setattr__", guarded_setattr) + setattr(cls, "__setattr__", guarded_setattr) class BaseElement(metaclass=BaseMeta): """BaseElement class @@ -705,27 +718,18 @@ class BaseMetaAppliance(BaseMeta): """early BaseElement checks""" print("__NEW__ Defining:", name, "with keys:", list(namespace)) super().pre_check(name,bases,namespace) - print(bases) - if len(bases)>0 and ("__DABFeatures__" not in dir(bases[0])): - print("add missing fields") - namespace["__DABFeatures__"] = {} + if "features" not in namespace["__DABSchema__"]: + namespace["__DABSchema__"]["features"]={} else: - print(f"bases[0]:{bases[0].__name__}") - print(dir(bases[0])) - # check class tree origin - if "__DABFeatures__" not in dir(bases[0]): - raise BrokenInheritance("__DABFeatures__ not found in base class, broken inheritance chain.") - # copy inherited schema - print("COPY") - namespace["__DABFeatures__"] = copy(bases[0].__DABFeatures__) + namespace["__DABSchema__"]["features"] = copy(namespace["__DABSchema__"]["features"]) + return @classmethod def pre_processing(mcs: type["BaseMeta"], name: str, bases: tuple[type[Any], ...], namespace: dict[str, Any]): - mcs.features: dict[str,type[BaseFeature]] = {} + mcs.new_features: dict[str,type[BaseFeature]] = {} + mcs.modified_features: dict[str,type[BaseFeature]] = {} super().pre_processing(name,bases,namespace) - #for _ftname in mcs.features.keys(): - # del namespace[_ftname] @classmethod def pre_processing_new_fields( @@ -733,15 +737,14 @@ class BaseMetaAppliance(BaseMeta): ): # pylint: disable=unused-argument """preprocessing BaseElement new Fields""" print('pre_processing_new_fields') - if _fname == "features": - raise ReservedFieldName("feature is a reserved field name") + if _fname in namespace["__DABSchema__"]["features"].keys(): + print("existing Feature !") + if not issubclass(_fvalue,namespace["__DABSchema__"]["features"][_fname]): + raise InvalidFeatureInheritance(f"Feature {_fname} is not an instance of {bases[0]}.{_fname}") + mcs.modified_features[_fname]=_fvalue elif isinstance(_fvalue,BaseMetaFeature): print("find Feature") - print(namespace["__DABFeatures__"]) - if _fname in namespace["__DABFeatures__"].keys(): - print("existing Feature !") - mcs.features[_fname]=_fvalue - #namespace["__DABFeatures__"][_fname] = _fvalue + mcs.new_features[_fname]=_fvalue else: super().pre_processing_new_fields(name,bases,namespace,_fname,_fvalue) @@ -751,13 +754,16 @@ class BaseMetaAppliance(BaseMeta): ): super().save_values(cls,name,bases,namespace) print(dir(mcs)) - cls.__DABFeatures__ = mcs.features + for _ftname,_ftvalue in mcs.modified_features.items(): + cls.__DABSchema__["features"][_ftname] = _ftvalue + for _ftname,_ftvalue in mcs.new_features.items(): + cls.__DABSchema__["features"][_ftname] = _ftvalue - def modify_object(self, obj): # intentionally untyped - for _ftname,_ftvalue in self.__DABFeatures__.items(): + def modify_object(cls:Type, obj): # intentionally untyped + for _ftname,_ftvalue in cls.__DABSchema__["features"].items(): instft = _ftvalue() - setattr(self, _ftname,instft ) - #self.__DABFeatures__[_ftname] = instft + object.__setattr__(obj, _ftname,instft ) + class BaseAppliance(BaseElement,metaclass=BaseMetaAppliance): """BaseFeature class diff --git a/test/test_model.py b/test/test_model.py index 78ce5e5..e3c08f3 100644 --- a/test/test_model.py +++ b/test/test_model.py @@ -31,7 +31,7 @@ def test_initializer_safe_testfc(): eval("print('hi')") -class TestConfigWithoutEnabledFlag(unittest.TestCase): +class MainTests(unittest.TestCase): def setUp(self): print("\n->", unittest.TestCase.id(self)) @@ -219,7 +219,7 @@ class TestConfigWithoutEnabledFlag(unittest.TestCase): self.immutable_vars__test_field(app2, "StrVar5", "default value", "123") self.immutable_vars__test_field(app2, "StrVar6", None, "123") - # @unittest.skip + @unittest.skip def test_containers__set(self): """Testing first appliance level, and Field types (Set)""" @@ -298,7 +298,7 @@ class TestConfigWithoutEnabledFlag(unittest.TestCase): res = subprocess.run([sys.executable, "-c", code], env=env) self.assertEqual(res.returncode, 2) - # @unittest.skip + @unittest.skip def test_containers__frozenset(self): """Testing first appliance level, and Field types (FrozenSet)""" @@ -1187,12 +1187,14 @@ class TestConfigWithoutEnabledFlag(unittest.TestCase): VarStrInner: str = "testvalue FEATURE" app1 = Appliance1() + self.assertIsInstance(Appliance1.__DABSchema__["VarStrOuter"],dm.DABField) self.assertIsInstance(app1.__DABSchema__["VarStrOuter"],dm.FrozenDABField) + self.assertIn("Feature1",app1.__DABSchema__["features"]) + self.assertIn("VarStrInner",app1.__DABSchema__["features"]["Feature1"].__DABSchema__) + self.assertIsInstance(app1.__DABSchema__["features"]["Feature1"].__DABSchema__["VarStrInner"],dm.DABField) self.assertTrue(hasattr(app1, "Feature1")) + self.assertIsInstance(app1.Feature1.__DABSchema__["VarStrInner"],dm.FrozenDABField) self.assertTrue(hasattr(app1.Feature1, "VarStrInner")) - self.assertIn("Feature1",app1.__DABFeatures__) - self.assertIn("VarStrInner",app1.__DABFeatures__["Feature1"].__DABSchema__) - self.assertIsInstance(app1.__DABFeatures__["Feature1"].__DABSchema__["VarStrInner"],dm.FrozenDABField) def test_feature_inheritance(self): """Testing first appliance feature, and Field types (simple)""" @@ -1228,13 +1230,124 @@ class TestConfigWithoutEnabledFlag(unittest.TestCase): app2 = Appliance2() app3 = Appliance3() + self.assertIsInstance(Appliance1.__DABSchema__["VarStrOuter"],dm.DABField) self.assertIsInstance(app1.__DABSchema__["VarStrOuter"],dm.FrozenDABField) + self.assertIn("Feature1",app1.__DABSchema__["features"]) + self.assertIn("VarStrInner",app1.__DABSchema__["features"]["Feature1"].__DABSchema__) + self.assertIsInstance(app1.__DABSchema__["features"]["Feature1"].__DABSchema__["VarStrInner"],dm.DABField) self.assertTrue(hasattr(app1, "Feature1")) - self.assertTrue(hasattr(app1.Feature1, "VarStrInner")) - self.assertIn("Feature1",app1.__DABFeatures__) - self.assertIn("VarStrInner",app1.__DABFeatures__["Feature1"].__DABSchema__) self.assertIsInstance(app1.Feature1.__DABSchema__["VarStrInner"],dm.FrozenDABField) - self.assertIsInstance(app1.__DABFeatures__["Feature1"].__DABSchema__["VarStrInner"],dm.DABField) + 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_feature_inheritance2(self): + """Testing first appliance feature, and Field types (simple)""" + + # class can be created + class Appliance1(dm.BaseAppliance): + class Feature1(dm.BaseFeature): + VarStrInner: str = "testvalue FEATURE1" + + # check cannot REdefine a feature from BaseFeature + with self.assertRaises(dm.InvalidFeatureInheritance): + class Appliance2(Appliance1): + class Feature1(dm.BaseFeature): + ... + + class Appliance2b(Appliance1): + class Feature1(Appliance1.Feature1): + ... + + # check only REdefine a feature from direct parent + with self.assertRaises(dm.InvalidFeatureInheritance): + class Appliance3(Appliance2b): + class Feature1(Appliance1.Feature1): + ... + + class Appliance3b(Appliance2b): + class Feature1(Appliance2b.Feature1): + ... + + app1 = Appliance1() + app2 = Appliance2b() + + print("youhou1") + print(Appliance3b) + print(Appliance3b.Feature1) + print("=====") + + app3 = Appliance3b() + + print("youhou2") + print(Appliance3b) + print(Appliance3b.Feature1) + print("=====") + + #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_inheritance_chain(self): + + # class can be created + class Appliance1(dm.BaseAppliance): + VarStr: str = "testvalue1" + + class Appliance2(Appliance1): + pass + + class Appliance3(Appliance2): + pass + + app1 = Appliance1() + app2 = Appliance2() + app3 = Appliance3() + + self.assertEqual(app1.VarStr,"testvalue1") + self.assertEqual(app2.VarStr,"testvalue1") + self.assertEqual(app3.VarStr,"testvalue1") + + class Appliance4(Appliance3): + VarStr = "testvalue moded" + + + app4 = Appliance4() + + self.assertEqual(app1.VarStr,"testvalue1") + self.assertEqual(app2.VarStr,"testvalue1") + self.assertEqual(app3.VarStr,"testvalue1") + self.assertEqual(app4.VarStr,"testvalue moded") + + app1b = Appliance1() + app2b = Appliance2() + app3b = Appliance3() + self.assertEqual(app1b.VarStr,"testvalue1") + self.assertEqual(app2b.VarStr,"testvalue1") + self.assertEqual(app3b.VarStr,"testvalue1") + # ---------- main ---------- if __name__ == "__main__":