Compare commits

...

2 Commits

Author SHA1 Message Date
chacha
981c5201a9 partially fix features 2025-09-16 23:40:41 +02:00
chacha
ab11052c8f work 2025-09-09 00:13:06 +02:00
3 changed files with 238 additions and 47 deletions

View File

@@ -13,6 +13,7 @@ Main module __init__ file.
from .__metadata__ import __version__, __Summuary__, __Name__
from .model import (
DABFieldInfo,
DABField,
BaseAppliance,
BaseFeature,
DABModelException,
@@ -27,5 +28,6 @@ from .model import (
IncompletelyAnnotatedField,
ImportForbidden,
FunctionForbidden,
FrozenDABField
FrozenDABField,
InvalidFeatureInheritance,
)

View File

@@ -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
@@ -476,7 +480,7 @@ class BaseMeta(type):
mcs: type["BaseMeta"], name: str, bases: tuple[type[Any], ...], namespace: dict[str, Any] # pylint: disable=unused-argument
) -> None:
"""early BaseElement checks"""
# print("__NEW__ Defining:", name, "with keys:", list(namespace))
print("__NEW__ Defining:", name, "with keys:", list(namespace))
if len(bases) > 1:
raise MultipleInheritanceForbidden("Multiple inheritance is not supported by dabmodel")
@@ -532,9 +536,9 @@ class BaseMeta(type):
mcs: type["BaseMeta"], name: str, bases: tuple[type[Any], ...], namespace: dict[str, Any], _fname: str, _fvalue: Any
): # pylint: disable=unused-argument
"""preprocessing BaseElement modified Fields"""
# print(f"Modified field: {_fname}")
print(f"Modified field: {_fname}")
if "__annotations__" in namespace and _fname in namespace["__annotations__"]:
raise ReadOnlyFieldAnnotation("annotations cannot be modified on derived classes")
raise ReadOnlyFieldAnnotation(f"annotations cannot be modified on derived classes {_fname}")
try:
check_type(
_fvalue,
@@ -552,7 +556,7 @@ class BaseMeta(type):
mcs: type["BaseMeta"], name: str, bases: tuple[type[Any], ...], namespace: dict[str, Any], _fname: str, _fvalue: Any
): # pylint: disable=unused-argument
"""preprocessing BaseElement new Fields"""
# print(f"New field: {_fname}")
print(f"New field: {_fname}")
# print(f"type is: {type(_fvalue)}")
# print(f"value is: {_fvalue}")
@@ -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
@@ -694,27 +707,44 @@ class BaseFeature(BaseElement,metaclass=BaseMetaFeature):
Base class for Appliance's Features.
Features are optional traits of an appliance.
"""
Enabled:bool=False
class BaseMetaAppliance(BaseMeta):
@classmethod
def pre_check(
mcs: type["BaseMeta"], name: str, bases: tuple[type[Any], ...], namespace: dict[str, Any] # pylint: disable=unused-argument
) -> None:
"""early BaseElement checks"""
print("__NEW__ Defining:", name, "with keys:", list(namespace))
super().pre_check(name,bases,namespace)
if "features" not in namespace["__DABSchema__"]:
namespace["__DABSchema__"]["features"]={}
else:
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(
mcs: type["BaseMeta"], name: str, bases: tuple[type[Any], ...], namespace: dict[str, Any], _fname: str, _fvalue: Any
): # pylint: disable=unused-argument
"""preprocessing BaseElement new Fields"""
if _fname == "features":
raise ReservedFieldName("feature is a reserved field name")
elif _fname =="get_features":
pass
print('pre_processing_new_fields')
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):
mcs.features[_fname]=_fvalue
print("find Feature")
mcs.new_features[_fname]=_fvalue
else:
super().pre_processing_new_fields(name,bases,namespace,_fname,_fvalue)
@@ -723,13 +753,17 @@ class BaseMetaAppliance(BaseMeta):
mcs: type["BaseMeta"], cls, name: str, bases: tuple[type[Any], ...], namespace: dict[str, Any] # pylint: disable=unused-argument
):
super().save_values(cls,name,bases,namespace)
cls.__DABFeatures__ = mcs.features
print(dir(mcs))
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.features.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

View File

@@ -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)"""
@@ -1176,7 +1176,7 @@ class TestConfigWithoutEnabledFlag(unittest.TestCase):
test_initializer_safe_testfc()
def test_feature(self):
"""Testing first appliance level, and Field types (simple)"""
"""Testing first appliance feature, and Field types (simple)"""
@@ -1187,12 +1187,167 @@ 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)"""
# class can be created
class Appliance1(dm.BaseAppliance):
VarStrOuter: str = "testvalue APPLIANCE1"
class Feature1(dm.BaseFeature):
VarStrInner: str = "testvalue FEATURE1"
VarInt:int=42
print(dir(Appliance1))
class Appliance2(Appliance1):
VarStrOuter = "testvalue APPLIANCE2"
class Feature2(dm.BaseFeature):
VarStrInner: str = "testvalue FEATURE2"
print(dir(Appliance2))
class Appliance3(Appliance2):
VarStrOuter = "testvalue APPLIANCE3"
class Feature1(Appliance1.Feature1):
VarStrInner = "testvalue FEATURE1 modded"
class Feature3(dm.BaseFeature):
VarStrInner: str = "testvalue FEATURE3"
print(dir(Appliance3))
app1 = Appliance1()
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.assertIsInstance(app1.Feature1.__DABSchema__["VarStrInner"],dm.FrozenDABField)
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__":