first feature implementation
This commit is contained in:
@@ -27,4 +27,5 @@ from .model import (
|
||||
IncompletelyAnnotatedField,
|
||||
ImportForbidden,
|
||||
FunctionForbidden,
|
||||
FrozenDABField
|
||||
)
|
||||
|
||||
@@ -55,7 +55,6 @@ ALLOWED_ANNOTATIONS = {
|
||||
}
|
||||
|
||||
ALLOWED_MODEL_FIELDS_TYPES = (str, int, float, complex, bool, bytes)
|
||||
ALLOWED_MODEL_FIELDS_CONTAINERS = (dict, list, set, frozenset, tuple)
|
||||
|
||||
|
||||
# TV_ALLOWED_MODEL_FIELDS_TYPES = TypeVar("TV_ALLOWED_MODEL_FIELDS_TYPES", *ALLOWED_MODEL_FIELDS_TYPES, *ALLOWED_MODEL_FIELDS_CONTAINERS)
|
||||
@@ -66,7 +65,11 @@ class DABModelException(Exception):
|
||||
Base Exception for DABModelException class
|
||||
"""
|
||||
|
||||
|
||||
class ReservedFieldName(Exception):
|
||||
"""DABModelException Exception class
|
||||
Base Exception for DABModelException class
|
||||
"""
|
||||
|
||||
class MultipleInheritanceForbidden(DABModelException):
|
||||
"""MultipleInheritanceForbidden Exception class
|
||||
Multiple inheritance is forbidden when using dabmodel
|
||||
@@ -616,42 +619,31 @@ class BaseMeta(type):
|
||||
f"Value of Field <{_fname}> is not of expected type {namespace['__annotations__'][_fname]}."
|
||||
) from exp
|
||||
cls.__DABSchema__[_fname].update_value(_fvalue)
|
||||
|
||||
|
||||
def __new__(mcs: type["BaseMeta"], name: str, bases: tuple[type[Any], ...], namespace: dict[str, Any]) -> Type:
|
||||
"""BaseElement new class"""
|
||||
mcs.pre_check(name, bases, namespace)
|
||||
mcs.pre_processing(name, bases, namespace)
|
||||
|
||||
orig_setattr = namespace.get("__setattr__", object.__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):
|
||||
raise ReadOnlyField(f"{key} is read-only")
|
||||
else:
|
||||
raise NewFieldForbidden("creating new fields is not allowed")
|
||||
|
||||
return orig_setattr(self, key, value)
|
||||
|
||||
namespace["__setattr__"] = guarded_setattr
|
||||
|
||||
|
||||
_cls = super().__new__(mcs, name, bases, namespace)
|
||||
|
||||
for _fname, _fvalue in mcs.modified_field.items():
|
||||
_cls.__DABSchema__[_fname] = deepcopy(bases[0].__DABSchema__[_fname])
|
||||
_cls.__DABSchema__[_fname].update_value(_fvalue)
|
||||
|
||||
for _fname, _fvalue in mcs.new_fields.items():
|
||||
_fvalue.add_source(mcs)
|
||||
_cls.__DABSchema__[_fname] = _fvalue
|
||||
|
||||
mcs.save_values(_cls, name, bases, namespace)
|
||||
mcs.call_initializer(_cls, name, bases, namespace)
|
||||
|
||||
return _cls
|
||||
|
||||
@classmethod
|
||||
def save_values(
|
||||
mcs: type["BaseMeta"], cls, name: str, bases: tuple[type[Any], ...], namespace: dict[str, Any] # pylint: disable=unused-argument
|
||||
):
|
||||
for _fname, _fvalue in mcs.modified_field.items():
|
||||
cls.__DABSchema__[_fname] = deepcopy(bases[0].__DABSchema__[_fname])
|
||||
cls.__DABSchema__[_fname].update_value(_fvalue)
|
||||
|
||||
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
|
||||
"""BaseElement new instance"""
|
||||
obj = super().__call__(*args, **kw)
|
||||
@@ -661,24 +653,85 @@ class BaseMeta(type):
|
||||
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)
|
||||
return obj
|
||||
|
||||
|
||||
cls.modify_object(obj)
|
||||
cls.install_guard(obj)
|
||||
return obj
|
||||
|
||||
def modify_object(self,obj):
|
||||
pass
|
||||
|
||||
def install_guard(self, obj):
|
||||
orig_setattr = getattr(self,"__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):
|
||||
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)
|
||||
|
||||
class BaseElement(metaclass=BaseMeta):
|
||||
"""BaseElement class
|
||||
Base class to apply metaclass and set common Fields.
|
||||
"""
|
||||
|
||||
class BaseMetaFeature(BaseMeta):
|
||||
pass
|
||||
|
||||
class BaseFeature(BaseElement):
|
||||
class BaseFeature(BaseElement,metaclass=BaseMetaFeature):
|
||||
"""BaseFeature class
|
||||
Base class for Appliance's Features.
|
||||
Features are optional traits of an appliance.
|
||||
"""
|
||||
|
||||
|
||||
class BaseAppliance(BaseElement):
|
||||
class BaseMetaAppliance(BaseMeta):
|
||||
|
||||
@classmethod
|
||||
def pre_processing(mcs: type["BaseMeta"], name: str, bases: tuple[type[Any], ...], namespace: dict[str, Any]):
|
||||
mcs.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
|
||||
elif isinstance(_fvalue,BaseMetaFeature):
|
||||
mcs.features[_fname]=_fvalue
|
||||
else:
|
||||
super().pre_processing_new_fields(name,bases,namespace,_fname,_fvalue)
|
||||
|
||||
@classmethod
|
||||
def save_values(
|
||||
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
|
||||
|
||||
def modify_object(self, obj): # intentionally untyped
|
||||
for _ftname,_ftvalue in self.features.items():
|
||||
instft = _ftvalue()
|
||||
setattr(self, _ftname,instft )
|
||||
self.__DABFeatures__[_ftname] = instft
|
||||
|
||||
class BaseAppliance(BaseElement,metaclass=BaseMetaAppliance):
|
||||
"""BaseFeature class
|
||||
Base class for Appliance.
|
||||
An appliance is a server configuration / image that is built using appliance's code and Fields.
|
||||
|
||||
@@ -1175,7 +1175,24 @@ class TestConfigWithoutEnabledFlag(unittest.TestCase):
|
||||
def __initializer(cls):
|
||||
test_initializer_safe_testfc()
|
||||
|
||||
def test_feature(self):
|
||||
"""Testing first appliance level, and Field types (simple)"""
|
||||
|
||||
|
||||
|
||||
# class can be created
|
||||
class Appliance1(dm.BaseAppliance):
|
||||
VarStrOuter: str = "testvalue APPLIANCE"
|
||||
class Feature1(dm.BaseFeature):
|
||||
VarStrInner: str = "testvalue FEATURE"
|
||||
app1 = Appliance1()
|
||||
|
||||
self.assertIsInstance(app1.__DABSchema__["VarStrOuter"],dm.FrozenDABField)
|
||||
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.__DABFeatures__["Feature1"].__DABSchema__["VarStrInner"],dm.FrozenDABField)
|
||||
# ---------- main ----------
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user