Compare commits

...

8 Commits

Author SHA1 Message Date
chacha
3e0defc574 work 2025-09-18 00:32:32 +02:00
chacha
f6e581381d cleaning 2025-09-17 00:16:30 +02:00
chacha
981c5201a9 partially fix features 2025-09-16 23:40:41 +02:00
chacha
ab11052c8f work 2025-09-09 00:13:06 +02:00
chacha
4f5dade949 first feature implementation 2025-09-08 01:23:46 +02:00
cclecle
cce260bc5e reordering 2025-09-07 18:42:38 +02:00
cclecle
915a4332ee tiny fix :) 2025-09-06 01:47:49 +02:00
cclecle
4dca3eb9d1 improve typing 2025-09-06 01:43:20 +02:00
3 changed files with 429 additions and 132 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,4 +28,7 @@ from .model import (
IncompletelyAnnotatedField,
ImportForbidden,
FunctionForbidden,
FrozenDABField,
InvalidFeatureInheritance,
FeatureNotBound,
)

View File

@@ -21,7 +21,7 @@ from typing import (
Callable,
Type,
)
from types import UnionType, FunctionType, SimpleNamespace
from types import UnionType, FunctionType, SimpleNamespace, MethodType
from copy import deepcopy, copy
# from pprint import pprint
@@ -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
@@ -138,12 +141,20 @@ 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
function call are forbidden
"""
class FeatureNotBound(DABModelException):
"""FeatureNotBound Exception class
a Feature must be bound to an Appliance
"""
ALLOWED_HELPERS_MATH = SimpleNamespace(
sqrt=math.sqrt,
@@ -298,7 +309,7 @@ class DABFieldInfo:
self._constraints = constraints
@property
def doc(self):
def doc(self) -> str:
"""Returns Field's documentation"""
return self._doc
@@ -325,7 +336,7 @@ class DABField(Generic[T_Field]):
self._source = s
@property
def doc(self):
def doc(self) -> str:
"""Returns Field's documentation"""
return self._info.doc
@@ -403,10 +414,14 @@ class ModelSpecView:
__slots__ = ("_vals", "_types", "_touched", "_name", "_module")
def __init__(self, values: dict, types_map: dict[str, type], name, module):
def __init__(self, values: dict[str, Any], types_map: dict[str, type], name: str, module: str):
self._name: str
self._vals: dict[str, Any]
self._types: dict[str, type]
self._touched: set
self._module: str
object.__setattr__(self, "_vals", dict(values))
object.__setattr__(self, "_types", types_map)
object.__setattr__(self, "_touched", set())
object.__setattr__(self, "_name", name)
object.__setattr__(self, "_module", module)
@@ -421,16 +436,16 @@ class ModelSpecView:
return self._module
@__module__.setter
def __module__(self, value):
def __module__(self, value: str):
pass
def __getattr__(self, name):
def __getattr__(self, name: str) -> Any:
"""internal proxy getattr"""
if name not in self._types:
raise AttributeError(f"Unknown field {name}")
return self._vals[name]
def __setattr__(self, name, value):
def __setattr__(self, name: str, value: Any):
"""internal proxy setattr"""
if name not in self._types:
raise NonExistingField(f"Cannot set unknown field {name}")
@@ -446,7 +461,6 @@ class ModelSpecView:
raise InvalidFieldValue(f"Field <{name}> value is not of expected type {T}.") from exp
self._vals[name] = value
self._touched.add(name)
def export(self) -> dict:
"""exports all proxified values"""
@@ -467,7 +481,8 @@ class BaseMeta(type):
@classmethod
def pre_check(
mcs: type["BaseMeta"], name: str, bases: tuple[type[Any], ...], namespace: dict[str, Any] # pylint: disable=unused-argument
mcs: type["BaseMeta"], name: str, bases: tuple[type[Any], ...], namespace: dict[str, Any], # pylint: disable=unused-argument
extensions : dict[str,Any]
) -> None:
"""early BaseElement checks"""
# print("__NEW__ Defining:", name, "with keys:", list(namespace))
@@ -489,13 +504,48 @@ class BaseMeta(type):
namespace[_funknown] = None
@classmethod
def pre_processing_modified(
mcs: type["BaseMeta"], name: str, bases: tuple[type[Any], ...], namespace: dict[str, Any], _fname: str, _fvalue: Any
def pre_processing(mcs: type["BaseMeta"], name: str, bases: tuple[type[Any], ...], namespace: dict[str, Any],extensions : dict[str,Any]):
"""preprocessing BaseElement"""
# iterating new and modified fields
mcs.modified_field = {}
mcs.new_fields = {}
mcs.initializer = None
initializer_name: Optional[str] = None
for _fname, _fvalue in namespace.items():
if _fname == f"_{name}__initializer" or (name.startswith("_") and _fname == "__initializer"):
if not isinstance(_fvalue, classmethod):
raise InvalidInitializerType()
mcs.initializer = _fvalue.__func__
if name.startswith("_"):
initializer_name = "__initializer"
else:
initializer_name = f"_{name}__initializer"
elif _fname.startswith("_"):
pass
elif isinstance(_fvalue, classmethod):
pass
else:
print(f"Parsing Field: {_fname} / {_fvalue}")
if len(bases) == 1 and _fname in namespace["__DABSchema__"].keys(): # Modified fields
mcs.pre_processing_modified_fields(name, bases, namespace, _fname, _fvalue, extensions)
else: # New fieds
mcs.pre_processing_new_fields(name, bases, namespace, _fname, _fvalue, extensions)
# removing modified fields from class (will add them back later)
for _fname in mcs.new_fields:
del namespace[_fname]
for _fname in mcs.modified_field:
del namespace[_fname]
if mcs.initializer is not None and initializer_name is not None:
del namespace[initializer_name]
@classmethod
def pre_processing_modified_fields(
mcs: type["BaseMeta"], name: str, bases: tuple[type[Any], ...], namespace: dict[str, Any], _fname: str, _fvalue: Any,
extensions : dict[str,Any]
): # pylint: disable=unused-argument
"""preprocessing BaseElement modified Fields"""
# 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,
@@ -509,13 +559,12 @@ class BaseMeta(type):
mcs.modified_field[_fname] = _fvalue
@classmethod
def pre_processing_new(
mcs: type["BaseMeta"], name: str, bases: tuple[type[Any], ...], namespace: dict[str, Any], _fname: str, _fvalue: Any
def pre_processing_new_fields(
mcs: type["BaseMeta"], name: str, bases: tuple[type[Any], ...], namespace: dict[str, Any], _fname: str, _fvalue: Any,
extensions : dict[str,Any]
): # pylint: disable=unused-argument
"""preprocessing BaseElement new Fields"""
# print(f"New field: {_fname}")
# print(f"type is: {type(_fvalue)}")
# print(f"value is: {_fvalue}")
#print(f"New field: {_fname}")
# check if field is annotated
if "__annotations__" not in namespace or _fname not in namespace["__annotations__"]:
@@ -540,8 +589,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)
@@ -549,50 +597,19 @@ class BaseMeta(type):
raise InvalidFieldValue(f"Value of Field <{_fname}> is not of expected type {namespace['__annotations__'][_fname]}.") from exp
mcs.new_fields[_fname] = DABField(_fname, _fvalue, namespace["__annotations__"][_fname], _finfo)
@classmethod
def pre_processing(mcs: type["BaseMeta"], name: str, bases: tuple[type[Any], ...], namespace: dict[str, Any]):
"""preprocessing BaseElement"""
# iterating new and modified fields
mcs.modified_field = {}
mcs.new_fields = {}
mcs.initializer = None
initializer_name: Optional[str] = None
for _fname, _fvalue in namespace.items():
if _fname == f"_{name}__initializer" or (name.startswith("_") and _fname == "__initializer"):
if not isinstance(_fvalue, classmethod):
raise InvalidInitializerType()
mcs.initializer = _fvalue.__func__
if name.startswith("_"):
initializer_name = "__initializer"
else:
initializer_name = f"_{name}__initializer"
elif _fname.startswith("__"):
pass
else:
# print(f"Parsing Field: {_fname} / {_fvalue}")
if len(bases) == 1 and _fname in namespace["__DABSchema__"].keys(): # Modified fields
mcs.pre_processing_modified(name, bases, namespace, _fname, _fvalue)
else: # New fieds
mcs.pre_processing_new(name, bases, namespace, _fname, _fvalue)
# removing modified fields from class (will add them back later)
for _fname in mcs.new_fields:
del namespace[_fname]
for _fname in mcs.modified_field:
del namespace[_fname]
if mcs.initializer is not None and initializer_name is not None:
del namespace[initializer_name]
@classmethod
def call_initializer(
mcs: type["BaseMeta"], cls, name: str, bases: tuple[type[Any], ...], namespace: dict[str, Any]
): # pylint: disable=unused-argument
mcs: type["BaseMeta"], cls, name: str, bases: tuple[type[Any], ...], namespace: dict[str, Any], # pylint: disable=unused-argument
extensions : dict[str,Any]
):
"""BaseElement initializer processing"""
if mcs.initializer is not None:
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:
@@ -613,69 +630,167 @@ 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)
extensions : dict[str,Any] = dict()
mcs.pre_check(name, bases, namespace, extensions)
mcs.pre_processing(name, bases, namespace, extensions)
_cls = super().__new__(mcs, name, bases, namespace)
orig_setattr = namespace.get("__setattr__", object.__setattr__)
mcs.save_values(_cls, name, bases, namespace, extensions)
mcs.call_initializer(_cls, name, bases, namespace, extensions)
_cls.install_guard(extensions)
return _cls
@classmethod
def save_values(
mcs: type["BaseMeta"], cls, name: str, bases: tuple[type[Any], ...], namespace: dict[str, Any], # pylint: disable=unused-argument
extensions : dict[str,Any]
):
for _fname, _fvalue in mcs.modified_field.items():
cls.__DABSchema__[_fname] = deepcopy(bases[0].__DABSchema__[_fname])
cls.__DABSchema__[_fname].update_value(_fvalue)
def guarded_setattr(self, key, value):
for _fname, _fvalue in mcs.new_fields.items():
_fvalue.add_source(mcs)
cls.__DABSchema__[_fname] = _fvalue
def __call__(cls: Type, *args: Any, **kw: Any): # intentionally untyped
"""BaseElement new instance"""
obj = super().__call__(*args, **kw)
extensions = dict()
for _fname, _fvalue in cls.__DABSchema__.items():
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,extensions)
return obj
def modify_object(cls:Type,obj,extensions : dict[str,Any]):
pass
def install_guard(cls:Type,extensions : dict[str,Any]):
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)
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.__DABSchema__.keys():
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)
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.call_initializer(_cls, name, bases, namespace)
return _cls
def __call__(cls, *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)
inst_schema: dict[str, Any] = {}
for _fname, _fvalue in cls.__DABSchema__.items():
inst_schema[_fname] = FrozenDABField(_fvalue)
setattr(obj, "__DABSchema__", inst_schema)
return obj
return orig_setattr(_self, key, value)
setattr(cls, "__setattr__", guarded_setattr)
class BaseElement(metaclass=BaseMeta):
"""BaseElement class
Base class to apply metaclass and set common Fields.
"""
class BaseMetaFeature(BaseMeta):
def __call__(cls: Type, *args: Any, **kw: Any): # intentionally untyped
"""BaseFeature new instance"""
if cls._BoundAppliance is None:
raise FeatureNotBound()
obj = super().__call__(*args, **kw)
return obj
class BaseFeature(BaseElement):
class BaseFeature(BaseElement,metaclass=BaseMetaFeature):
"""BaseFeature class
Base class for Appliance's Features.
Features are optional traits of an appliance.
"""
_BoundAppliance:"Optional[Type[BaseAppliance]]" = None
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
extensions : dict[str,Any]
) -> None:
"""early BaseElement checks"""
super().pre_check(name,bases,namespace,extensions)
if "features" not in namespace["__DABSchema__"]:
namespace["__DABSchema__"]["features"]={}
else:
namespace["__DABSchema__"]["features"] = copy(namespace["__DABSchema__"]["features"])
return
class BaseAppliance(BaseElement):
@classmethod
def pre_processing(mcs: type["BaseMeta"], name: str, bases: tuple[type[Any], ...], namespace: dict[str, Any],extensions : dict[str,Any]):
extensions["new_features"]: dict[str,type[BaseFeature]] = {}
extensions["modified_features"]: dict[str,type[BaseFeature]] = {}
super().pre_processing(name,bases,namespace,extensions)
@classmethod
def pre_processing_new_fields(
mcs: type["BaseMeta"], name: str, bases: tuple[type[Any], ...], namespace: dict[str, Any], _fname: str, _fvalue: Any,
extensions : dict[str,Any]
): # pylint: disable=unused-argument
"""preprocessing BaseElement new Fields"""
#print('pre_processing_new_fields')
if _fname in namespace["__DABSchema__"]["features"].keys():
if not issubclass(_fvalue,namespace["__DABSchema__"]["features"][_fname]):
raise InvalidFeatureInheritance(f"Feature {_fname} is not an instance of {bases[0]}.{_fname}")
extensions["modified_features"][_fname]=_fvalue
elif isinstance(_fvalue,BaseMetaFeature):
extensions["new_features"][_fname]=_fvalue
else:
super().pre_processing_new_fields(name,bases,namespace,_fname,_fvalue,extensions)
@classmethod
def save_values(
mcs: type["BaseMeta"], cls, name: str, bases: tuple[type[Any], ...], namespace: dict[str, Any], # pylint: disable=unused-argument
extensions : dict[str,Any]
):
super().save_values(cls,name,bases,namespace,extensions)
for _ftname,_ftvalue in extensions["modified_features"].items():
_ftvalue._BoundAppliance = cls
cls.__DABSchema__["features"][_ftname] = _ftvalue
for _ftname,_ftvalue in extensions["new_features"].items():
_ftvalue._BoundAppliance = cls
cls.__DABSchema__["features"][_ftname] = _ftvalue
def modify_object(cls:Type, obj, extensions : dict[str,Any]): # intentionally untyped
for _ftname,_ftvalue in cls.__DABSchema__["features"].items():
instft = _ftvalue()
object.__setattr__(obj, _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.

View File

@@ -16,6 +16,7 @@ import textwrap
from typing import List, Optional, Dict, Union, Tuple, Set, FrozenSet, TypeVar, Generic, Any, Annotated
from pprint import pprint
from frozendict import frozendict
import math
print(__name__)
print(__package__)
@@ -31,7 +32,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))
@@ -84,32 +85,32 @@ class TestConfigWithoutEnabledFlag(unittest.TestCase):
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: str = 12
test: str = 12
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: int = "value"
test: int = "value"
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: float = "value"
test: float = "value"
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: complex = "value"
test: complex = "value"
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: bool = "value"
test: bool = "value"
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: bytes = "value"
test: bytes = "value"
def test_annotation(self):
"""Testing first appliance level, and Field types (simple annotations)"""
@@ -147,38 +148,38 @@ class TestConfigWithoutEnabledFlag(unittest.TestCase):
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: "str" = 12
test: "str" = 12
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: "int" = "value"
test: "int" = "value"
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: "float" = "value"
test: "float" = "value"
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: "complex" = "value"
test: "complex" = "value"
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: "bool" = "value"
test: "bool" = "value"
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: "bytes" = "value"
test: "bytes" = "value"
# class cannot be created if not annotated field
with self.assertRaises(dm.NotAnnotatedField):
class _(dm.BaseAppliance):
_ = "default value"
test = "default value"
def test_optionnal(self):
"""Testing first appliance level, and Field types (Optionnal annotations)"""
@@ -219,7 +220,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 +299,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)"""
@@ -415,22 +416,22 @@ class TestConfigWithoutEnabledFlag(unittest.TestCase):
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: List[int] = ["a"]
test: List[int] = ["a"]
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: "List[int]" = ["a"]
test: "List[int]" = ["a"]
with self.assertRaises(dm.IncompletelyAnnotatedField):
class _(dm.BaseAppliance):
_: List = [1, 2]
test: List = [1, 2]
with self.assertRaises(dm.IncompletelyAnnotatedField):
class _(dm.BaseAppliance):
_: "List" = [1, 2]
test: "List" = [1, 2]
def test_containers__dict(self):
"""Testing first appliance level, and Field types (Dict)"""
@@ -455,29 +456,29 @@ class TestConfigWithoutEnabledFlag(unittest.TestCase):
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: Dict[int, str] = {1: 64, 2: "b"}
test: Dict[int, str] = {1: 64, 2: "b"}
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: "Dict[int, str]" = {1: 64, 2: "b"}
test: "Dict[int, str]" = {1: 64, 2: "b"}
with self.assertRaises(dm.IncompletelyAnnotatedField):
class _(dm.BaseAppliance):
_: Dict = {1: 64, 2: "b"}
test: Dict = {1: 64, 2: "b"}
# annotation is parsed before the library can do anything, so the exception can only be TypeError
with self.assertRaises(TypeError):
class _(dm.BaseAppliance):
_: Dict[int] = {1: 64, 2: "b"}
test: Dict[int] = {1: 64, 2: "b"}
# annotation is parsed before the library can do anything, so the exception can only be TypeError
with self.assertRaises(TypeError):
class _(dm.BaseAppliance):
_: "Dict[int]" = {1: 64, 2: "b"}
test: "Dict[int]" = {1: 64, 2: "b"}
def test_containers__tuple(self):
"""Testing first appliance level, and Field types (Tuple)"""
@@ -509,32 +510,32 @@ class TestConfigWithoutEnabledFlag(unittest.TestCase):
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: Tuple[int] = "a"
test: Tuple[int] = "a"
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: "Tuple[int]" = "a"
test: "Tuple[int]" = "a"
with self.assertRaises(dm.IncompletelyAnnotatedField):
class _(dm.BaseAppliance):
_: Tuple = (1, 2)
test: Tuple = (1, 2)
with self.assertRaises(dm.IncompletelyAnnotatedField):
class _(dm.BaseAppliance):
_: "Tuple" = (1, 2)
test: "Tuple" = (1, 2)
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: "Tuple[int,...]" = (1, "a")
test: "Tuple[int,...]" = (1, "a")
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: "tuple[int,...]" = (1, "a")
test: "tuple[int,...]" = (1, "a")
def check_immutable_fields_schema(
self, appliance: dm.BaseAppliance, field_name: str, expected_value: str, expected_default_value: str, expected_type: type
@@ -701,13 +702,13 @@ class TestConfigWithoutEnabledFlag(unittest.TestCase):
with self.assertRaises(dm.InvalidFieldAnnotation):
class _(dm.BaseAppliance):
_: Annotated[str, "foo2"] = "default value2"
test: Annotated[str, "foo2"] = "default value2"
# annotation is parsed before the library can do anything, so the exception can only be TypeError
with self.assertRaises(TypeError):
class _(dm.BaseAppliance):
_: Annotated[str] = "default value2"
test: Annotated[str] = "default value2"
def test_immutable_fields_inheritance(self):
"""Testing first appliance level, and Field types (simple)"""
@@ -1175,7 +1176,184 @@ class TestConfigWithoutEnabledFlag(unittest.TestCase):
def __initializer(cls):
test_initializer_safe_testfc()
def test_feature(self):
"""Testing first appliance feature, 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(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"))
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 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_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")
def test_feature_register(self):
"""Testing first appliance feature, and Field types (simple)"""
# class can be created
class Appliance1(dm.BaseAppliance):
pass
class Feature1(dm.BaseFeature):
_BoundAppliance = Appliance1
VarStrInner: str = "testvalue FEATURE1"
print(dir(Feature1))
def test_feature_register_defect(self):
class Feature1(dm.BaseFeature):
pass
with self.assertRaises(dm.FeatureNotBound):
feat1 = Feature1()
# ---------- main ----------
if __name__ == "__main__":