Compare commits

...

4 Commits

Author SHA1 Message Date
chacha
616a53578c immplement Element behaviour and some tests 2025-09-21 01:00:01 +02:00
chacha
d20712a72f remove last references to DAB 2025-09-20 19:01:21 +02:00
chacha
2837b6439f cleaning 2025-09-20 18:57:32 +02:00
chacha
b4d6ed6130 add missing __init__ files 2025-09-20 18:48:17 +02:00
20 changed files with 1329 additions and 788 deletions

Binary file not shown.

View File

@@ -3,8 +3,8 @@ from typing import Generic, TypeVar
T_Field = TypeVar("T_Field")
class BaseConstraint(Generic[T_Field]):
"""BaseConstraint class
class Constraint(Generic[T_Field]):
"""Constraint class
Base class for Field's constraints
"""

View File

@@ -1,7 +1,7 @@
from typing import Generic, TypeVar, Any
from .LAMField import LAMField
from .Constraint import BaseConstraint
from .Constraint import Constraint
from ..tools import LAMdeepfreeze
T_Field = TypeVar("T_Field")
@@ -12,7 +12,7 @@ class FrozenLAMField(Generic[T_Field]):
a read-only proxy of a Field
"""
def __init__(self, inner_field: LAMField):
def __init__(self, inner_field: LAMField[T_Field]):
self._inner_field = inner_field
@property
@@ -21,7 +21,7 @@ class FrozenLAMField(Generic[T_Field]):
return LAMdeepfreeze(self._inner_field.doc)
@property
def constraints(self) -> tuple[BaseConstraint]:
def constraints(self) -> tuple[Constraint]:
"""Returns Field's constraint (frozen)"""
return LAMdeepfreeze(self._inner_field.constraints)

View File

@@ -0,0 +1,38 @@
from typing import Generic, TypeVar, Self, Hashable, Any
from abc import ABC, abstractmethod
from ..tools import JSONType
TV_LAMCompatbile = TypeVar("TV_LABCompatbile", bound="LABCompatible")
class LAMCompatible(Generic[TV_LAMCompatbile], ABC):
"""Any type that can safely live inside a LABField."""
@classmethod
def lam_validate_annotation(cls, annotation: Any) -> None:
"""
Validate the type annotation (e.g., SpecialList[int]).
Raise if it's not compatible.
"""
return # default: do nothing (simple types dont need it)
@abstractmethod
def lam_validate(self) -> None:
"""Raise if the value is invalid for this type."""
...
@abstractmethod
def lam_freeze(self) -> Hashable:
"""Return an immutable/hashable representation of this value."""
...
@abstractmethod
def lam_to_plain(self) -> JSONType:
"""Return a plain serializable form (str, dict, etc.)."""
...
@classmethod
@abstractmethod
def lam_from_plain(cls, plain: JSONType) -> Self:
"""Return an Object from a plain serializable form (str, dict, etc.)."""
...

View File

@@ -1,23 +1,23 @@
from typing import Generic, TypeVar, Optional, Any
from .LAMFieldInfo import LAMFieldInfo
from .Constraint import BaseConstraint
from .Constraint import Constraint
from ..tools import LAMdeepfreeze
T_Field = TypeVar("T_Field")
TV_LABField = TypeVar("TV_LABField")
class LAMField(Generic[T_Field]):
class LAMField(Generic[TV_LABField]):
"""This class describe a Field in Schema"""
def __init__(self, name: str, v: Optional[T_Field], a: Any, i: LAMFieldInfo):
def __init__(self, name: str, v: Optional[TV_LABField], a: Any, i: LAMFieldInfo):
self._name: str = name
self._source: Optional[type] = None
self._default_value: Optional[T_Field] = v
self._value: Optional[T_Field] = v
self._default_value: Optional[TV_LABField] = v
self._value: Optional[TV_LABField] = v
self._annotations: Any = a
self._info: LAMFieldInfo = i
self._constraints: list[BaseConstraint[Any]] = i.constraints
self._constraints: list[Constraint[Any]] = i.constraints
def add_source(self, s: type) -> None:
"""Adds source Appliance to the Field"""
@@ -28,12 +28,12 @@ class LAMField(Generic[T_Field]):
"""Returns Field's documentation"""
return self._info.doc
def add_constraint(self, c: BaseConstraint) -> None:
def add_constraint(self, c: Constraint) -> None:
"""Adds constraint to the Field"""
self._constraints.append(c)
@property
def constraints(self) -> list[BaseConstraint]:
def constraints(self) -> list[Constraint]:
"""Returns Field's constraint"""
return self._info.constraints
@@ -42,7 +42,7 @@ class LAMField(Generic[T_Field]):
"""Returns Field's default value (frozen)"""
return LAMdeepfreeze(self._default_value)
def update_value(self, v: Optional[T_Field] = None) -> None:
def update_value(self, v: Optional[TV_LABField] = None) -> None:
"""Updates Field's value"""
self._value = v
@@ -52,7 +52,7 @@ class LAMField(Generic[T_Field]):
return LAMdeepfreeze(self._value)
@property
def raw_value(self) -> Optional[T_Field]:
def raw_value(self) -> Optional[TV_LABField]:
"""Returns Field's value"""
return self._value

View File

@@ -1,15 +1,15 @@
from typing import Optional, Any
from .Constraint import BaseConstraint
from .Constraint import Constraint
class LAMFieldInfo:
"""This Class allows to describe a Field in Appliance class"""
def __init__(
self, *, doc: str = "", constraints: Optional[list[BaseConstraint]] = None
self, *, doc: str = "", constraints: Optional[list[Constraint]] = None
):
self._doc: str = doc
self._constraints: list[BaseConstraint]
self._constraints: list[Constraint]
if constraints is None:
self._constraints = []
else:
@@ -21,6 +21,6 @@ class LAMFieldInfo:
return self._doc
@property
def constraints(self) -> list[BaseConstraint[Any]]:
def constraints(self) -> list[Constraint[Any]]:
"""Returns Field's constraints"""
return self._constraints

View File

View File

@@ -16,8 +16,10 @@ from .__metadata__ import __version__, __Summuary__, __Name__
from .LAMFields.LAMField import LAMField
from .LAMFields.LAMFieldInfo import LAMFieldInfo
from .LAMFields.FrozenLAMField import FrozenLAMField
from .LAMFields.LAMCompatible import LAMCompatible
from .appliance import Appliance
from .feature import Feature
from .element import Element
from .exception import (
@@ -35,4 +37,7 @@ from .exception import (
FunctionForbidden,
InvalidFeatureInheritance,
FeatureNotBound,
UnsupportedFieldType,
)
__all__ = [name for name in globals() if not name.startswith("_")]

View File

@@ -2,7 +2,7 @@ from .element import Element
from .meta.appliance import _MetaAppliance
class Appliance(Element, metaclass=_MetaAppliance):
class Appliance(metaclass=_MetaAppliance):
"""BaseFeature class
Base class for Appliance.
An appliance is a server configuration / image that is built using appliance's code and Fields.

View File

@@ -1,7 +1,7 @@
from .meta.base import _MetaElement
from .meta.element import _MetaElement, IElement
class Element(metaclass=_MetaElement):
class Element(IElement, metaclass=_MetaElement):
"""Element class
Base class to apply metaclass and set common Fields.
"""

View File

@@ -67,6 +67,12 @@ class IncompletelyAnnotatedField(InvalidFieldAnnotation):
"""
class UnsupportedFieldType(InvalidFieldAnnotation):
"""UnsupportedFieldType Exception class
The field type is unsupported
"""
class ReadOnlyFieldAnnotation(DABModelException):
"""ReadOnlyFieldAnnotation Exception class
Field annotation connot be modified

View File

@@ -2,7 +2,7 @@ from .element import Element
from .meta.feature import _MetaFeature
class Feature(Element, metaclass=_MetaFeature):
class Feature(metaclass=_MetaFeature):
"""Feature class
Base class for Appliance's Features.
Features are optional traits of an appliance.

View File

View File

@@ -6,7 +6,7 @@ from typeguard import check_type, CollectionCheckStrategy, TypeCheckError
from ..tools import LAMdeepfreeze
from ..LAMFields.LAMField import LAMField
from ..LAMFields.FrozenLAMField import FrozenLAMField
from .base import _MetaElement
from .element import _MetaElement
from ..feature import Feature
from ..exception import (
InvalidFieldValue,
@@ -34,11 +34,11 @@ class _MetaAppliance(_MetaElement):
Copies the parent's `features` mapping when inheriting to keep it per-class.
"""
super().check_class(name, bases, namespace, extensions) # type: ignore[misc]
if "features" not in namespace["__DABSchema__"]:
namespace["__DABSchema__"]["features"] = {}
if "features" not in namespace["__LAMSchema__"]:
namespace["__LAMSchema__"]["features"] = {}
else:
namespace["__DABSchema__"]["features"] = copy(
namespace["__DABSchema__"]["features"]
namespace["__LAMSchema__"]["features"] = copy(
namespace["__LAMSchema__"]["features"]
)
@classmethod
@@ -78,8 +78,8 @@ class _MetaAppliance(_MetaElement):
- Else, if `_fvalue` is a Feature *class*, stage into `new_features`.
- Otherwise, it is a regular field: delegate to meta.process_new_field.
"""
if _fname in namespace["__DABSchema__"]["features"].keys():
if not issubclass(_fvalue, namespace["__DABSchema__"]["features"][_fname]):
if _fname in namespace["__LAMSchema__"]["features"].keys():
if not issubclass(_fvalue, namespace["__LAMSchema__"]["features"][_fname]):
raise InvalidFeatureInheritance(
f"Feature {_fname} is not an instance of {bases[0]}.{_fname}"
)
@@ -103,16 +103,16 @@ class _MetaAppliance(_MetaElement):
For each new/modified feature:
- bind it to `cls` (sets the feature's `_BoundAppliance`),
- register it under `cls.__DABSchema__["features"]`.
- register it under `cls.__LAMSchema__["features"]`.
"""
super().commit_fields(cls, name, bases, namespace, extensions) # type: ignore[misc]
for _ftname, _ftvalue in extensions["modified_features"].items():
_ftvalue._BoundAppliance = cls # pylint: disable=protected-access
cls.__DABSchema__["features"][_ftname] = _ftvalue
cls.__LAMSchema__["features"][_ftname] = _ftvalue
for _ftname, _ftvalue in extensions["new_features"].items():
_ftvalue._BoundAppliance = cls # pylint: disable=protected-access
cls.__DABSchema__["features"][_ftname] = _ftvalue
cls.__LAMSchema__["features"][_ftname] = _ftvalue
def finalize_instance(cls: Type, obj, extensions: dict[str, Any]):
"""
@@ -122,7 +122,7 @@ class _MetaAppliance(_MetaElement):
- Subclass replacements
- Dict overrides (class + patch dict)
"""
for fname, fdef in obj.__DABSchema__.get("features", {}).items():
for fname, fdef in obj.__LAMSchema__.get("features", {}).items():
# Case 1: plain class or subclass
if isinstance(fdef, type) and issubclass(fdef, Feature):
inst = fdef()
@@ -133,11 +133,11 @@ class _MetaAppliance(_MetaElement):
feat_cls, overrides = fdef
inst = feat_cls()
for field_name, new_val in overrides.items():
if field_name not in feat_cls.__DABSchema__:
if field_name not in feat_cls.__LAMSchema__:
raise InvalidFieldValue(
f"Feature '{fname}' has no field '{field_name}'"
)
field = feat_cls.__DABSchema__[field_name]
field = feat_cls.__LAMSchema__[field_name]
try:
check_type(
new_val,
@@ -150,7 +150,7 @@ class _MetaAppliance(_MetaElement):
f"expected {field.annotations}, got {new_val!r}"
) from exp
object.__setattr__(inst, field_name, LAMdeepfreeze(new_val))
inst.__DABSchema__[field_name] = FrozenLAMField(
inst.__LAMSchema__[field_name] = FrozenLAMField(
LAMField(field_name, new_val, field.annotations, field._info)
)
object.__setattr__(obj, fname, inst)
@@ -171,31 +171,11 @@ class _MetaAppliance(_MetaElement):
MyApp(F1=MyF1) # inheritance / replacement
MyApp(F1={"val": 42, ...}) # dict override of existing feature
"""
# --- field overrides (unchanged) ---
for k, v in list(kwargs.items()):
if k in cls.__DABSchema__: # regular field
field = cls.__DABSchema__[k]
try:
check_type(
v,
field.annotations,
collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS,
)
except TypeCheckError as exp:
raise InvalidFieldValue(
f"Invalid value for field '{k}': expected {field.annotations}, got {v!r}"
) from exp
object.__setattr__(obj, k, LAMdeepfreeze(v))
obj.__DABSchema__[k] = FrozenLAMField(
LAMField(k, v, field.annotations, field._info)
)
kwargs.pop(k)
# --- feature overrides ---
for k, v in list(kwargs.items()):
if k in cls.__DABSchema__.get("features", {}):
base_feat_cls = cls.__DABSchema__["features"][k]
if k in cls.__LAMSchema__.get("features", {}):
base_feat_cls = cls.__LAMSchema__["features"][k]
# Case 1: subclass replacement (inheritance)
if isinstance(v, type) and issubclass(v, base_feat_cls):
@@ -205,13 +185,13 @@ class _MetaAppliance(_MetaElement):
f"Feature {v.__name__} is not bound to {cls.__name__}"
)
# record subclass into instance schema
obj.__DABSchema__["features"][k] = v
obj.__LAMSchema__["features"][k] = v
kwargs.pop(k)
# Case 2: dict override
elif isinstance(v, dict):
# store (class, override_dict) for finalize_instance
obj.__DABSchema__["features"][k] = (base_feat_cls, v)
obj.__LAMSchema__["features"][k] = (base_feat_cls, v)
kwargs.pop(k)
else:
@@ -227,9 +207,7 @@ class _MetaAppliance(_MetaElement):
raise FeatureNotBound(
f"Feature {v.__name__} is not bound to {cls.__name__}"
)
obj.__DABSchema__["features"][k] = v
obj.__LAMSchema__["features"][k] = v
kwargs.pop(k)
if kwargs:
unknown = ", ".join(sorted(kwargs.keys()))
raise InvalidFieldValue(f"Unknown parameters: {unknown}")
super().apply_overrides(obj, extensions, *args, **kwargs)

View File

@@ -23,10 +23,12 @@ import inspect, ast, textwrap
from typeguard import check_type, TypeCheckError, CollectionCheckStrategy
from ..tools import LAMdeepfreeze
from ..LAMFields.LAMField import LAMField
from ..LAMFields.LAMFieldInfo import LAMFieldInfo
from ..LAMFields.FrozenLAMField import FrozenLAMField
from ..exception import (
MultipleInheritanceForbidden,
BrokenInheritance,
@@ -41,6 +43,7 @@ from ..exception import (
FunctionForbidden,
NonExistingField,
InvalidInitializerType,
UnsupportedFieldType,
)
ALLOWED_ANNOTATIONS: dict[str, Any] = {
@@ -124,6 +127,10 @@ ALLOWED_HELPERS_DEFAULT: dict[str, object] = {
}
class IElement:
pass
def _check_initializer_safety(func) -> None:
"""
Preliminary structural check for __initializer__.
@@ -145,38 +152,40 @@ def _check_initializer_safety(func) -> None:
mod = ast.parse(src)
# Find the FunctionDef node that corresponds to this initializer
init_node = None
for n in mod.body:
if (
isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
init_node = next(
(
n
for n in mod.body
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
and n.name == func.__name__
):
init_node = n
break
),
None,
)
if init_node is None:
# Fallback: if not found, analyze nothing further to avoid false positives
return
# Walk ONLY the body of the initializer (don't flag the def itself)
body_tree = ast.Module(body=init_node.body, type_ignores=[])
for node in ast.walk(body_tree):
# Forbid imports
for node in ast.walk(ast.Module(body=init_node.body, type_ignores=[])):
if isinstance(node, (ast.Import, ast.ImportFrom)):
raise ImportForbidden("imports disabled in __initializer")
# Forbid nested defs (but allow lambdas)
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
raise FunctionForbidden(
"Nested function definitions are forbidden in __initializer"
)
if isinstance(node, ast.Lambda): # Forbid lambda
raise FunctionForbidden("Lambdas are forbidden in __initializer")
raise FunctionForbidden("Nested defs are forbidden in __initializer")
# if isinstance(node, ast.Lambda):
# raise FunctionForbidden("Lambdas are forbidden in __initializer")
# Optional: forbid closures (keeps determinism; allows lambdas that don't capture)
if func.__code__.co_freevars:
raise FunctionForbidden("Closures are forbidden in __initializer__")
# Inspect captured free vars
closure_vars = inspect.getclosurevars(func)
captured = {**closure_vars.globals, **closure_vars.nonlocals}
for name, val in captured.items():
if isinstance(val, type) and issubclass(val, IElement):
continue
if isinstance(val, (int, str, float, bool, type(None))):
continue
raise FunctionForbidden(
f"Closures are forbidden in __initializer__ (captured: {name}={val!r})"
)
def _blocked_import(*args, **kwargs):
@@ -212,12 +221,15 @@ def _peel_annotated(t: Any) -> Any:
def _check_annotation_definition( # pylint: disable=too-complex,too-many-return-statements
_type,
) -> bool:
):
print(f"_type={_type}")
_type = _peel_annotated(_type)
_origin = get_origin(_type) or _type
_args = get_args(_type)
# handle Optional[] and Union[None,...]
if (get_origin(_type) is Union or get_origin(_type) is UnionType) and type(
None
) in get_args(_type):
if (_origin is Union or _origin is UnionType) and type(None) in _args:
return all(
_check_annotation_definition(_)
for _ in get_args(_type)
@@ -225,43 +237,46 @@ def _check_annotation_definition( # pylint: disable=too-complex,too-many-return
)
# handle other Union[...]
if get_origin(_type) is Union or get_origin(_type) is UnionType:
return all(_check_annotation_definition(_) for _ in get_args(_type))
if _origin is Union or _origin is UnionType:
return all(_check_annotation_definition(_) for _ in _args)
# handle Dict[...]
if get_origin(_type) is dict:
inner = get_args(_type)
if len(inner) != 2:
if _origin is dict:
print("a")
if len(_args) != 2:
raise IncompletelyAnnotatedField(
f"Dict Annotation requires 2 inner definitions: {_type}"
)
return _peel_annotated(
inner[0]
) in ALLOWED_MODEL_FIELDS_TYPES and _check_annotation_definition(inner[1])
if not _peel_annotated(_args[0]) in ALLOWED_MODEL_FIELDS_TYPES:
raise IncompletelyAnnotatedField(
f"Dict Key must be simple builtin: {_type}"
)
return _check_annotation_definition(_args[1])
# handle Tuple[]
if get_origin(_type) in [tuple]:
inner_types = get_args(_type)
if len(inner_types) == 0:
if _origin is tuple:
if len(_args) == 0:
raise IncompletelyAnnotatedField(
f"Annotation requires inner definition: {_type}"
)
if len(inner_types) == 2 and inner_types[1] is Ellipsis:
return _check_annotation_definition(inner_types[0])
return all(_check_annotation_definition(_) for _ in inner_types)
if len(_args) == 2 and _args[1] is Ellipsis:
return _check_annotation_definition(_args[0])
return all(_check_annotation_definition(_) for _ in _args)
# handle Set[],Tuple[],FrozenSet[],List[]
if get_origin(_type) in [set, frozenset, tuple, list]:
inner_types = get_args(_type)
if len(inner_types) == 0:
if _origin in [set, frozenset, tuple, list]:
if len(_args) == 0:
raise IncompletelyAnnotatedField(
f"Annotation requires inner definition: {_type}"
)
return all(_check_annotation_definition(_) for _ in inner_types)
return all(_check_annotation_definition(_) for _ in _args)
if isinstance(_origin, type) and issubclass(_origin, IElement):
return
if _type in ALLOWED_MODEL_FIELDS_TYPES:
return True
return False
return
raise UnsupportedFieldType(_type)
class ModelSpecView:
@@ -337,7 +352,7 @@ class _MetaElement(type):
modified_fields: Dict[str, Any] = {}
new_fields: Dict[str, LAMField[Any]] = {}
initializer: Optional[Callable[..., Any]] = None
__DABSchema__: dict[str, Any] = {}
__LAMSchema__: dict[str, Any] = {}
@classmethod
def check_class(
@@ -362,16 +377,20 @@ class _MetaElement(type):
raise MultipleInheritanceForbidden(
"Multiple inheritance is not supported by dabmodel"
)
if len(bases) == 0: # base class (BaseElement)
namespace["__DABSchema__"] = {}
if len(bases) == 0: # base class (Appliance,Feature)
namespace["__LAMSchema__"] = {}
elif (
len(bases) == 1 and bases[0] is IElement
): # special case for Element (hidden layer IElement)
namespace["__LAMSchema__"] = {}
else: # standard inheritance
# check class tree origin
if "__DABSchema__" not in dir(bases[0]):
if "__LAMSchema__" not in dir(bases[0]):
raise BrokenInheritance(
"__DABSchema__ not found in base class, broken inheritance chain."
"__LAMSchema__ not found in base class, broken inheritance chain."
)
# copy inherited schema
namespace["__DABSchema__"] = copy(bases[0].__DABSchema__)
namespace["__LAMSchema__"] = copy(bases[0].__LAMSchema__)
# force field without default value to be instantiated (with None)
if "__annotations__" in namespace:
@@ -411,7 +430,9 @@ class _MetaElement(type):
name.startswith("_") and _fname == "__initializer"
):
if not isinstance(_fvalue, classmethod):
raise InvalidInitializerType()
raise InvalidInitializerType(
"__initializer should be a classmethod"
)
mcs.initializer = _fvalue.__func__
if name.startswith("_"):
initializer_name = "__initializer"
@@ -424,7 +445,7 @@ class _MetaElement(type):
else:
print(f"Parsing Field: {_fname} / {_fvalue}")
if (
len(bases) == 1 and _fname in namespace["__DABSchema__"].keys()
len(bases) == 1 and _fname in namespace["__LAMSchema__"].keys()
): # Modified fields
mcs.process_modified_field(
name, bases, namespace, _fname, _fvalue, extensions
@@ -464,7 +485,7 @@ class _MetaElement(type):
try:
check_type(
_fvalue,
namespace["__DABSchema__"][_fname].annotations,
namespace["__LAMSchema__"][_fname].annotations,
collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS,
)
except TypeCheckError as exp:
@@ -490,7 +511,7 @@ class _MetaElement(type):
payloads (allowing only LAMFieldInfo), checks the default value type,
and stages the field as a `LAMField` in `mcs.new_fields`.
"""
# print(f"New field: {_fname}")
print(f"New field: {_fname}")
# check if field is annotated
if (
@@ -507,9 +528,13 @@ class _MetaElement(type):
namespace["__annotations__"][_fname]
)
if not _check_annotation_definition(namespace["__annotations__"][_fname]):
try:
_check_annotation_definition(namespace["__annotations__"][_fname])
except InvalidFieldAnnotation:
raise
except Exception as ex:
raise InvalidFieldAnnotation(
f"Field <{_fname}> has not an allowed or valid annotation."
f"Field <{_fname}> has not an allowed or valid annotation.", ex
)
_finfo: LAMFieldInfo = LAMFieldInfo()
@@ -544,6 +569,7 @@ class _MetaElement(type):
raise InvalidFieldValue(
f"Value of Field <{_fname}> is not of expected type {namespace['__annotations__'][_fname]}."
) from exp
print(f"!!VAL: {_fvalue}")
mcs.new_fields[_fname] = LAMField(
_fname, _fvalue, namespace["__annotations__"][_fname], _finfo
)
@@ -569,7 +595,7 @@ class _MetaElement(type):
_check_initializer_safety(mcs.initializer)
init_fieldvalues = {}
init_fieldtypes = {}
for _fname, _fvalue in cls.__DABSchema__.items():
for _fname, _fvalue in cls.__LAMSchema__.items():
if isinstance(_fvalue, LAMField):
init_fieldvalues[_fname] = deepcopy(_fvalue.raw_value)
init_fieldtypes[_fname] = _fvalue.annotations
@@ -580,28 +606,29 @@ class _MetaElement(type):
"__builtins__": {"__import__": _blocked_import},
**ALLOWED_HELPERS_DEFAULT,
}
if mcs.initializer.__code__.co_freevars:
raise FunctionForbidden("__initializer must not use closures")
# if mcs.initializer.__code__.co_freevars:
# raise FunctionForbidden("__initializer must not use closures")
safe_initializer = FunctionType(
mcs.initializer.__code__,
safe_globals,
name=mcs.initializer.__name__,
argdefs=mcs.initializer.__defaults__,
closure=None,
closure=mcs.initializer.__closure__,
)
safe_initializer(fakecls) # pylint: disable=not-callable
for _fname, _fvalue in fakecls.export().items():
try:
check_type(
_fvalue,
cls.__DABSchema__[_fname].annotations,
cls.__LAMSchema__[_fname].annotations,
collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS,
)
except TypeCheckError as exp:
raise InvalidFieldValue(
f"Value of Field <{_fname}> is not of expected type {namespace['__annotations__'][_fname]}."
) from exp
cls.__DABSchema__[_fname].update_value(_fvalue)
cls.__LAMSchema__[_fname] = deepcopy(cls.__LAMSchema__[_fname])
cls.__LAMSchema__[_fname].update_value(_fvalue)
def __new__(
mcs: type["_MetaElement"],
@@ -632,18 +659,18 @@ class _MetaElement(type):
extensions: dict[str, Any],
):
"""
Commit staged fields into the class schema (`__DABSchema__`).
Commit staged fields into the class schema (`__LAMSchema__`).
- For modified fields: copy the parent's LAMField, update its value.
- For new fields: set the freshly built LAMField and record its source.
"""
for _fname, _fvalue in mcs.modified_fields.items():
cls.__DABSchema__[_fname] = deepcopy(bases[0].__DABSchema__[_fname])
cls.__DABSchema__[_fname].update_value(_fvalue)
cls.__LAMSchema__[_fname] = deepcopy(bases[0].__LAMSchema__[_fname])
cls.__LAMSchema__[_fname].update_value(_fvalue)
for _fname, _fvalue in mcs.new_fields.items():
_fvalue.add_source(cls)
cls.__DABSchema__[_fname] = _fvalue
cls.__LAMSchema__[_fname] = _fvalue
def __call__(cls: Type, *args: Any, **kw: Any): # intentionally untyped
"""BaseElement new instance"""
@@ -672,7 +699,7 @@ class _MetaElement(type):
Copies each LAMField.value to an instance attribute (deep-frozen view).
"""
for _fname, _fvalue in cls.__DABSchema__.items():
for _fname, _fvalue in cls.__LAMSchema__.items():
if isinstance(_fvalue, LAMField):
object.__setattr__(obj, _fname, _fvalue.value)
@@ -682,17 +709,17 @@ class _MetaElement(type):
"""
Freeze the instance's schema by wrapping DABFields into FrozenLAMField.
Creates a per-instance `__DABSchema__` dict where each field is read-only.
Creates a per-instance `__LAMSchema__` dict where each field is read-only.
"""
inst_schema = copy(obj.__DABSchema__)
for _fname, _fvalue in cls.__DABSchema__.items():
inst_schema = copy(obj.__LAMSchema__)
for _fname, _fvalue in cls.__LAMSchema__.items():
if isinstance(_fvalue, LAMField):
inst_schema[_fname] = FrozenLAMField(_fvalue)
if "features" in inst_schema:
inst_schema["features"] = dict(inst_schema["features"])
object.__setattr__(obj, "__DABSchema__", inst_schema)
object.__setattr__(obj, "__LAMSchema__", inst_schema)
def apply_overrides(cls, obj, extensions, *args, **kwargs):
"""
@@ -702,12 +729,33 @@ class _MetaElement(type):
Subclasses of _MetaElement can override this to support things like:
- Field overrides: MyApp(field=value)
- Feature overrides: MyApp(FeatureName=CustomFeature)
- Feature attachments: MyApp(NewFeature=FeatureClass)
By default this does nothing.
"""
# --- field overrides (unchanged) ---
for k, v in list(kwargs.items()):
if k in cls.__LAMSchema__: # regular field
field = cls.__LAMSchema__[k]
try:
check_type(
v,
field.annotations,
collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS,
)
except TypeCheckError as exp:
raise InvalidFieldValue(
f"Invalid value for field '{k}': expected {field.annotations}, got {v!r}"
) from exp
object.__setattr__(obj, k, LAMdeepfreeze(v))
obj.__LAMSchema__[k] = FrozenLAMField(
LAMField(k, v, field.annotations, field._info)
)
kwargs.pop(k)
if kwargs:
unknown = ", ".join(sorted(kwargs.keys()))
raise InvalidFieldValue(f"Unknown parameters: {unknown}")
def finalize_instance(cls: Type, obj: Any, extensions: dict[str, Any]):
"""
Finalization hook invoked at the end of instance construction.
@@ -735,10 +783,10 @@ class _MetaElement(type):
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 key in _self.__LAMSchema__.keys():
if key in _self.__dict__:
raise ReadOnlyField(f"{key} is read-only")
# elif key in _self.__DABSchema__["features"].keys():
# elif key in _self.__LAMSchema__["features"].keys():
# if key in _self.__dict__:
# raise ReadOnlyField(f"{key} is read-only")
else:

View File

@@ -1,5 +1,5 @@
from typing import Type, Any
from .base import _MetaElement
from .element import _MetaElement
from ..exception import FeatureNotBound

View File

@@ -1,11 +1,15 @@
"""library's internal tools"""
from typing import Union, List, Any, Dict
from uuid import UUID
from datetime import datetime
import json
from frozendict import deepfreeze
JSONPrimitive = Union[str, int, float, bool, None]
JSONType = Union[JSONPrimitive, List[Any], Dict[str, Any]] # recursive in practice
class LAMJSONEncoder(json.JSONEncoder):
"""allows to JSON encode non supported data type"""

View File

@@ -41,7 +41,7 @@ def test_initializer_safe_testfc():
eval("print('hi')")
class MainTests(unittest.TestCase):
class ApplianceTest(unittest.TestCase):
def setUp(self):
print("\n->", unittest.TestCase.id(self))
@@ -53,7 +53,7 @@ class MainTests(unittest.TestCase):
# field is in the object
self.assertIn(name, dir(obj))
# field is in the schema
self.assertIn(name, obj.__DABSchema__.keys())
self.assertIn(name, obj.__LAMSchema__.keys())
# field is readable
self.assertEqual(getattr(obj, name), default_value)
# field is read only
@@ -370,7 +370,6 @@ class MainTests(unittest.TestCase):
with self.assertRaises(AttributeError):
app1.testVar.add(3)
# /!\ THIS SHOULD RAISE AN EXCEPTION BUT IT DOESNT :-/
with self.assertRaises(dm.InvalidFieldValue):
print("youhou")
@@ -596,19 +595,19 @@ class MainTests(unittest.TestCase):
expected_default_value: str,
expected_type: type,
):
self.assertIn(field_name, appliance.__DABSchema__)
self.assertIn("doc", dir(appliance.__DABSchema__[field_name]))
self.assertEqual(appliance.__DABSchema__[field_name].doc, "")
self.assertIn("annotations", dir(appliance.__DABSchema__[field_name]))
self.assertEqual(appliance.__DABSchema__[field_name].annotations, expected_type)
self.assertIn("value", dir(appliance.__DABSchema__[field_name]))
self.assertEqual(appliance.__DABSchema__[field_name].value, expected_value)
self.assertIn("default_value", dir(appliance.__DABSchema__[field_name]))
self.assertIn(field_name, appliance.__LAMSchema__)
self.assertIn("doc", dir(appliance.__LAMSchema__[field_name]))
self.assertEqual(appliance.__LAMSchema__[field_name].doc, "")
self.assertIn("annotations", dir(appliance.__LAMSchema__[field_name]))
self.assertEqual(appliance.__LAMSchema__[field_name].annotations, expected_type)
self.assertIn("value", dir(appliance.__LAMSchema__[field_name]))
self.assertEqual(appliance.__LAMSchema__[field_name].value, expected_value)
self.assertIn("default_value", dir(appliance.__LAMSchema__[field_name]))
self.assertEqual(
appliance.__DABSchema__[field_name].default_value, expected_default_value
appliance.__LAMSchema__[field_name].default_value, expected_default_value
)
self.assertIn("constraints", dir(appliance.__DABSchema__[field_name]))
self.assertEqual(appliance.__DABSchema__[field_name].constraints, ())
self.assertIn("constraints", dir(appliance.__LAMSchema__[field_name]))
self.assertEqual(appliance.__LAMSchema__[field_name].constraints, ())
def test_immutable_fields_schema(self):
"""Testing first appliance level, and Field types (annotated)"""
@@ -630,8 +629,8 @@ class MainTests(unittest.TestCase):
app1 = Appliance1()
self.assertIn("__DABSchema__", dir(app1))
self.assertIn("__DABSchema__", app1.__dict__)
self.assertIn("__LAMSchema__", dir(app1))
self.assertIn("__LAMSchema__", app1.__dict__)
self.check_immutable_fields_schema(
app1, "StrVar", "default value", "default value", str
@@ -684,8 +683,8 @@ class MainTests(unittest.TestCase):
app1 = Appliance1()
self.assertIn("__DABSchema__", dir(app1))
self.assertIn("__DABSchema__", app1.__dict__)
self.assertIn("__LAMSchema__", dir(app1))
self.assertIn("__LAMSchema__", app1.__dict__)
self.check_immutable_fields_schema(
app1, "ListStr", ("val1", "val2"), ("val1", "val2"), list[str]
@@ -742,8 +741,8 @@ class MainTests(unittest.TestCase):
app1 = Appliance1()
self.assertIn("__DABSchema__", dir(app1))
self.assertIn("__DABSchema__", app1.__dict__)
self.assertIn("__LAMSchema__", dir(app1))
self.assertIn("__LAMSchema__", app1.__dict__)
self.check_immutable_fields_schema(
app1, "ListStr", ("val1", "val2"), ("val1", "val2"), List[str]
@@ -809,40 +808,40 @@ class MainTests(unittest.TestCase):
app1 = Appliance1()
self.immutable_vars__test_field(app1, "StrVar", "default value", "test")
self.assertEqual(app1.__DABSchema__["StrVar"].doc, "foo1")
self.assertEqual(app1.__LAMSchema__["StrVar"].doc, "foo1")
self.immutable_vars__test_field(app1, "StrVar2", "default value2", "test2")
self.assertEqual(app1.__DABSchema__["StrVar2"].doc, "foo2")
self.assertEqual(app1.__LAMSchema__["StrVar2"].doc, "foo2")
self.immutable_vars__test_field(app1, "VarInt", 12, 13)
self.assertEqual(app1.__DABSchema__["VarInt"].doc, "foo3")
self.assertEqual(app1.__LAMSchema__["VarInt"].doc, "foo3")
self.immutable_vars__test_field(app1, "VarInt2", 21, 22)
self.assertEqual(app1.__DABSchema__["VarInt2"].doc, "foo4")
self.assertEqual(app1.__LAMSchema__["VarInt2"].doc, "foo4")
self.immutable_vars__test_field(app1, "VarFloat", 12.1, 32)
self.assertEqual(app1.__DABSchema__["VarFloat"].doc, "foo5")
self.assertEqual(app1.__LAMSchema__["VarFloat"].doc, "foo5")
self.immutable_vars__test_field(app1, "VarFloat2", 21.2, 42)
self.assertEqual(app1.__DABSchema__["VarFloat2"].doc, "foo6")
self.assertEqual(app1.__LAMSchema__["VarFloat2"].doc, "foo6")
self.immutable_vars__test_field(
app1, "VarComplex", complex(3, 5), complex(1, 2)
)
self.assertEqual(app1.__DABSchema__["VarComplex"].doc, "foo7")
self.assertEqual(app1.__LAMSchema__["VarComplex"].doc, "foo7")
self.immutable_vars__test_field(
app1, "VarComplex2", complex(8, 6), complex(3, 2)
)
self.assertEqual(app1.__DABSchema__["VarComplex2"].doc, "foo8")
self.assertEqual(app1.__LAMSchema__["VarComplex2"].doc, "foo8")
self.immutable_vars__test_field(app1, "VarBool", True, False)
self.assertEqual(app1.__DABSchema__["VarBool"].doc, "foo9")
self.assertEqual(app1.__LAMSchema__["VarBool"].doc, "foo9")
self.immutable_vars__test_field(app1, "VarBool2", False, True)
self.assertEqual(app1.__DABSchema__["VarBool2"].doc, "foo10")
self.assertEqual(app1.__LAMSchema__["VarBool2"].doc, "foo10")
self.immutable_vars__test_field(
app1, "VarBytes", bytes.fromhex("2Ef0 F1f2 "), bytes.fromhex("11f0 F1f2 ")
)
self.assertEqual(app1.__DABSchema__["VarBytes"].doc, "foo11")
self.assertEqual(app1.__LAMSchema__["VarBytes"].doc, "foo11")
self.immutable_vars__test_field(
app1,
"VarBytes2",
bytes.fromhex("2ff0 F7f2 "),
bytes.fromhex("11f0 F1e2 "),
)
self.assertEqual(app1.__DABSchema__["VarBytes2"].doc, "foo12")
self.assertEqual(app1.__LAMSchema__["VarBytes2"].doc, "foo12")
with self.assertRaises(dm.InvalidFieldAnnotation):
@@ -1008,10 +1007,10 @@ class MainTests(unittest.TestCase):
class Appliance3(Appliance2):
NewValue: str = "newval"
self.assertNotIn("NewValue", Appliance2.__DABSchema__)
self.assertNotIn("NewValue", app2.__DABSchema__)
self.assertNotIn("NewValue", Appliance1.__DABSchema__)
self.assertNotIn("NewValue", app1.__DABSchema__)
self.assertNotIn("NewValue", Appliance2.__LAMSchema__)
self.assertNotIn("NewValue", app2.__LAMSchema__)
self.assertNotIn("NewValue", Appliance1.__LAMSchema__)
self.assertNotIn("NewValue", app1.__LAMSchema__)
app3 = Appliance3()
self.immutable_vars__test_field(app3, "StrVar", "moded value", "test")
@@ -1510,6 +1509,23 @@ class MainTests(unittest.TestCase):
app2 = _()
self.assertEqual(app2.VarInt, 42)
def test_initializer_dont_leak(self):
class A(dm.Appliance):
VarInt: int = 12
class B(A):
@classmethod
def __initializer(cls):
cls.VarInt = cls.VarInt + 1
a = A()
b = B()
self.assertEqual(a.VarInt, 12)
self.assertEqual(b.VarInt, 13)
def test_initializer_safe(self):
with self.assertRaises(dm.ImportForbidden):
@@ -1566,10 +1582,10 @@ class MainTests(unittest.TestCase):
def __initializer(cls):
compile("print(55)", "test", "eval")
with self.assertRaises(dm.FunctionForbidden):
def testfc():
eval("print('test')")
def testfc():
eval("print('test')")
with self.assertRaises(dm.FunctionForbidden):
class _(dm.Appliance):
_: int = 0
@@ -1578,6 +1594,16 @@ class MainTests(unittest.TestCase):
def __initializer(cls):
testfc()
with self.assertRaises(dm.FunctionForbidden):
class _(dm.Appliance):
_: int = 0
@classmethod
def __initializer(cls):
if True:
testfc()
# class can be created
class Appliance2(dm.Appliance):
VarInt: int = -56
@@ -1626,144 +1652,6 @@ class MainTests(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.Appliance):
VarStrOuter: str = "testvalue APPLIANCE"
class Feature1(dm.Feature):
VarStrInner: str = "testvalue FEATURE"
app1 = Appliance1()
self.assertIsInstance(Appliance1.__DABSchema__["VarStrOuter"], dm.LAMField)
self.assertIsInstance(app1.__DABSchema__["VarStrOuter"], dm.FrozenLAMField)
self.assertIn("Feature1", app1.__DABSchema__["features"])
self.assertIn(
"VarStrInner", app1.__DABSchema__["features"]["Feature1"].__DABSchema__
)
self.assertIsInstance(
app1.__DABSchema__["features"]["Feature1"].__DABSchema__["VarStrInner"],
dm.LAMField,
)
self.assertTrue(hasattr(app1, "Feature1"))
self.assertIsInstance(
app1.Feature1.__DABSchema__["VarStrInner"], dm.FrozenLAMField
)
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.Appliance):
VarStrOuter: str = "testvalue APPLIANCE1"
class Feature1(dm.Feature):
VarStrInner: str = "testvalue FEATURE1"
VarInt: int = 42
print(dir(Appliance1))
class Appliance2(Appliance1):
VarStrOuter = "testvalue APPLIANCE2"
class Feature2(dm.Feature):
VarStrInner: str = "testvalue FEATURE2"
print(dir(Appliance2))
class Appliance3(Appliance2):
VarStrOuter = "testvalue APPLIANCE3"
class Feature1(Appliance1.Feature1):
VarStrInner = "testvalue FEATURE1 modded"
class Feature3(dm.Feature):
VarStrInner: str = "testvalue FEATURE3"
print(dir(Appliance3))
app1 = Appliance1()
app2 = Appliance2()
app3 = Appliance3()
self.assertIsInstance(Appliance1.__DABSchema__["VarStrOuter"], dm.LAMField)
self.assertIsInstance(app1.__DABSchema__["VarStrOuter"], dm.FrozenLAMField)
self.assertIn("Feature1", app1.__DABSchema__["features"])
self.assertIn(
"VarStrInner", app1.__DABSchema__["features"]["Feature1"].__DABSchema__
)
self.assertIsInstance(
app1.__DABSchema__["features"]["Feature1"].__DABSchema__["VarStrInner"],
dm.LAMField,
)
self.assertTrue(hasattr(app1, "Feature1"))
self.assertIsInstance(
app1.Feature1.__DABSchema__["VarStrInner"], dm.FrozenLAMField
)
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.Appliance):
class Feature1(dm.Feature):
VarStrInner: str = "testvalue FEATURE1"
# check cannot REdefine a feature from Feature
with self.assertRaises(dm.InvalidFeatureInheritance):
class Appliance2(Appliance1):
class Feature1(dm.Feature): ...
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
@@ -1802,49 +1690,6 @@ class MainTests(unittest.TestCase):
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.Appliance):
pass
class Feature1(dm.Feature):
_BoundAppliance = Appliance1
VarStrInner: str = "testvalue FEATURE1"
app = Appliance1(feat1=Feature1)
self.assertEqual(app.feat1.VarStrInner, "testvalue FEATURE1")
# check it does not leak accross instances
app = Appliance1(feat2=Feature1)
self.assertEqual(app.feat2.VarStrInner, "testvalue FEATURE1")
with self.assertRaises(AttributeError):
app.feat1
def test_feature_register_notbound(self):
"""Testing first appliance feature, and Field types (simple)"""
# class can be created
class Appliance1(dm.Appliance):
pass
class Feature1(dm.Feature):
VarStrInner: str = "testvalue FEATURE1"
with self.assertRaises(dm.FeatureNotBound):
app = Appliance1(feat1=Feature1)
def test_feature_register_defect(self):
class Feature1(dm.Feature):
pass
with self.assertRaises(dm.FeatureNotBound):
feat1 = Feature1()
def test_override(self):
"""Testing first appliance level, and Field types (List)"""
@@ -1891,72 +1736,6 @@ class MainTests(unittest.TestCase):
app._internal = 42 # should be allowed
self.assertEqual(app._internal, 42)
def test_inherit_declared_feature(self):
class App(dm.Appliance):
class F1(dm.Feature):
val: int = 1
class MyF1(App.F1):
val = 2
val2: str = "toto"
app = App(F1=MyF1)
self.assertIsInstance(app.F1, MyF1)
self.assertEqual(app.F1.val, 2)
self.assertEqual(app.F1.val2, "toto")
def test_override_declared_feature(self):
class App(dm.Appliance):
class F1(dm.Feature):
val: int = 1
val2: str = "toto"
app = App(F1={"val": 42, "val2": "tata"})
self.assertEqual(app.F1.val, 42)
self.assertEqual(app.F1.val2, "tata")
def test_feature_dict_override_type_error(self):
class App(dm.Appliance):
class F1(dm.Feature):
val: int = 1
# wrong type for val → must raise InvalidFieldValue
with self.assertRaises(dm.InvalidFieldValue):
App(F1={"val": "not-an-int"})
def test_feature_dict_override_nonexisting_field(self):
class App(dm.Appliance):
class F1(dm.Feature):
val: int = 1
# field does not exist → must raise
with self.assertRaises(dm.InvalidFieldValue):
App(F1={"doesnotexist": 123})
def test_feature_inheritance_with_extra_fields(self):
class App(dm.Appliance):
class F1(dm.Feature):
val: int = 1
class MyF1(App.F1):
val = 2
extra: str = "hello"
app = App(F1=MyF1)
self.assertEqual(app.F1.val, 2)
self.assertEqual(app.F1.extra, "hello")
def test_feature_not_bound_runtime_attach_fails(self):
class App(dm.Appliance):
pass
class UnboundFeature(dm.Feature):
x: int = 1
# attaching an unbound feature should raise
with self.assertRaises(dm.FeatureNotBound):
App(Unbound=UnboundFeature)
def test_field_cannot_be_reassigned(self):
class App(dm.Appliance):
x: int = 1
@@ -1965,16 +1744,6 @@ class MainTests(unittest.TestCase):
with self.assertRaises(dm.ReadOnlyField):
app.x = 99
def test_feature_override_does_not_leak_between_instances(self):
class App(dm.Appliance):
class F1(dm.Feature):
val: int = 1
app1 = App(F1={"val": 99})
app2 = App()
self.assertEqual(app1.F1.val, 99)
self.assertEqual(app2.F1.val, 1)
def test_schema_fields_are_frozen(self):
class App(dm.Appliance):
x: list[int] = [1, 2]
@@ -2023,16 +1792,29 @@ class MainTests(unittest.TestCase):
cls.x = inner()
def test_initializer_lambda_forbidden(self):
with self.assertRaises(dm.FunctionForbidden):
class App(dm.Appliance):
class _(dm.Appliance):
x: int = 1
@classmethod
def __initializer(cls):
f = lambda: 42 # forbidden
cls.x = f()
y = 2
def inner():
return y + 1
cls.x = lambda: inner()
def test_initializer_lambda_forbidden(self):
class App(dm.Appliance):
x: int = 1
@classmethod
def __initializer(cls):
f = lambda: 42
cls.x = f()
def test_multiple_inheritance_forbidden(self):
with self.assertRaises(dm.MultipleInheritanceForbidden):
@@ -2197,61 +1979,6 @@ class MainTests(unittest.TestCase):
with self.assertRaises(dm.InvalidFieldValue):
App(a=["oops"]) # wrong inner type
def test_runtime_attach_bound_feature_success(self):
class App(dm.Appliance):
class F1(dm.Feature):
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.Appliance):
class F1(dm.Feature):
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.Appliance):
class F(dm.Feature):
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.Appliance):
class F(dm.Feature):
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.Appliance):
x: int = 1
@@ -2259,227 +1986,6 @@ class MainTests(unittest.TestCase):
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.Appliance):
class Feat1(dm.Feature):
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* Feature, not subclass of B.Feat1
with self.assertRaises(dm.InvalidFeatureInheritance):
class D(B):
class Feat1(dm.Feature):
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.Feature):
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.Appliance):
class Feat1(dm.Feature):
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.Appliance):
class F1(dm.Feature):
a: int = 1
class F2(dm.Feature):
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.Feature):
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.Appliance):
class F1(dm.Feature):
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.Appliance):
class F1(dm.Feature):
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"))
def test_dict_field_override_with_nested_containers(self):
class App(dm.Appliance):
data: dict[str, list[int]] = {"numbers": [1, 2]}
@@ -2494,18 +2000,6 @@ class MainTests(unittest.TestCase):
with self.assertRaises(dm.InvalidFieldValue):
App(data={"numbers": [1, "oops"]})
def test_feature_dict_override_with_nested_containers(self):
class App(dm.Appliance):
class F1(dm.Feature):
values: list[int] = [1, 2]
app = App(F1={"values": [5, 6]})
self.assertEqual(app.F1.values, (5, 6)) # deepfreeze → tuple
# Invalid type in list should fail
with self.assertRaises(dm.InvalidFieldValue):
App(F1={"values": [1, "oops"]})
def test_initializer_modifies_nested_containers(self):
class App(dm.Appliance):
data: dict[str, list[int]] = {"nums": [1]}
@@ -2518,61 +2012,6 @@ class MainTests(unittest.TestCase):
app = App()
self.assertEqual(app.data["nums"], (1, 2)) # frozen tuple after init
def test_feature_dict_override_with_unknown_key(self):
class App(dm.Appliance):
class F1(dm.Feature):
a: int = 1
# Dict override with unknown field 'zzz'
with self.assertRaises(dm.InvalidFieldValue):
App(F1={"zzz": 42})
def test_schema_isolation_across_multiple_feature_overrides(self):
class App(dm.Appliance):
class F1(dm.Feature):
a: int = 1
class F1a(App.F1):
a = 10
class F1b(App.F1):
a = 20
app1 = App(F1=F1a)
self.assertIsInstance(app1.F1, F1a)
self.assertEqual(app1.F1.a, 10)
app2 = App(F1=F1b)
self.assertIsInstance(app2.F1, F1b)
self.assertEqual(app2.F1.a, 20)
# Original appliance schema must not be polluted
app3 = App()
self.assertIsInstance(app3.F1, App.F1)
self.assertEqual(app3.F1.a, 1)
def test_feature_inheritance_with_annotated_fields(self):
from typing_extensions import Annotated
class App(dm.Appliance):
class F1(dm.Feature):
a: Annotated[int, dm.LAMFieldInfo(doc="field a")] = 1
# ✅ Subclass override must inherit from parent F1
class F1Ex(App.F1):
b: str = "ok"
app = App(F1=F1Ex)
self.assertIsInstance(app.F1, F1Ex)
self.assertEqual((app.F1.a, app.F1.b), (1, "ok"))
# ❌ Wrong: fresh Feature under same name
with self.assertRaises(dm.InvalidFeatureInheritance):
class Bad(App):
class F1(dm.Feature):
fail: str = "oops"
# ---------- main ----------

293
test/test_element.py Normal file
View File

@@ -0,0 +1,293 @@
# dabmodel (c) by chacha
#
# dabmodel is licensed under a
# Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Unported License.
#
# You should have received a copy of the license along with this
# work. If not, see <https://creativecommons.org/licenses/by-nc-sa/4.0/>.
import unittest
import sys
import subprocess
from os import chdir, environ
from pathlib import Path
print(__name__)
print(__package__)
from src import dabmodel as dm
testdir_path = Path(__file__).parent.resolve()
chdir(testdir_path.parent.resolve())
class ElementTest(unittest.TestCase):
def setUp(self):
print("\n->", unittest.TestCase.id(self))
def test_element_simple(self):
class E(dm.Element):
ivalue: int = 43
strvalue: str = "test"
fvalue: float = 1.4322
ar_int: list[int] = [1, 54, 65]
ar_int2: list[int] = [1, 54, 65]
class A(dm.Appliance):
elem: E = E(ivalue=45, strvalue="coucou", ar_int=[5, 7])
a = A()
self.assertIsInstance(a.elem, E)
self.assertIsInstance(a.elem.ivalue, int)
self.assertEqual(a.elem.ivalue, 45)
self.assertIsInstance(a.elem.strvalue, str)
self.assertEqual(a.elem.strvalue, "coucou")
self.assertIsInstance(a.elem.fvalue, float)
self.assertEqual(a.elem.fvalue, 1.4322)
self.assertIsInstance(a.elem.ar_int, tuple)
self.assertEqual(a.elem.ar_int, (5, 7))
self.assertIsInstance(a.elem.ar_int2, tuple)
self.assertEqual(a.elem.ar_int2, (1, 54, 65))
def test_element_in_container(self):
class E(dm.Element):
ivalue: int = 43
strvalue: str = "test"
fvalue: float = 1.4322
ar_int: list[int] = [1, 54, 65]
ar_int2: list[int] = [1, 54, 65]
class A(dm.Appliance):
elems: list[E] = [
E(ivalue=45, strvalue="coucou", ar_int=[5, 7]),
E(ivalue=46, strvalue="coucou2", ar_int=[50, 7]),
]
a = A()
self.assertIsInstance(a.elems, tuple)
self.assertEqual(len(a.elems), 2)
self.assertIsInstance(a.elems[0], E)
self.assertIsInstance(a.elems[0].ivalue, int)
self.assertEqual(a.elems[0].ivalue, 45)
self.assertIsInstance(a.elems[0].strvalue, str)
self.assertEqual(a.elems[0].strvalue, "coucou")
self.assertIsInstance(a.elems[0].fvalue, float)
self.assertEqual(a.elems[0].fvalue, 1.4322)
self.assertIsInstance(a.elems[0].ar_int, tuple)
self.assertEqual(a.elems[0].ar_int, (5, 7))
self.assertIsInstance(a.elems[0].ar_int2, tuple)
self.assertEqual(a.elems[0].ar_int2, (1, 54, 65))
self.assertIsInstance(a.elems[1], E)
self.assertIsInstance(a.elems[1].ivalue, int)
self.assertEqual(a.elems[1].ivalue, 46)
self.assertIsInstance(a.elems[1].strvalue, str)
self.assertEqual(a.elems[1].strvalue, "coucou2")
self.assertIsInstance(a.elems[1].fvalue, float)
self.assertEqual(a.elems[1].fvalue, 1.4322)
self.assertIsInstance(a.elems[1].ar_int, tuple)
self.assertEqual(a.elems[1].ar_int, (50, 7))
self.assertIsInstance(a.elems[1].ar_int2, tuple)
self.assertEqual(a.elems[1].ar_int2, (1, 54, 65))
def test_element_frozen(self):
class E(dm.Element):
ivalue: int = 43
strvalue: str = "test"
fvalue: float = 1.4322
ar_int: list[int] = [1, 54, 65]
ar_int2: list[int] = [1, 54, 65]
class A(dm.Appliance):
elems: list[E] = [
E(ivalue=45, strvalue="coucou", ar_int=[5, 7]),
E(ivalue=46, strvalue="coucou2", ar_int=[50, 7]),
]
elem: E = E()
a = A()
with self.assertRaises(AttributeError):
a.elems.add(E())
with self.assertRaises(dm.ReadOnlyField):
a.elem.ivalue = 1
with self.assertRaises(dm.ReadOnlyField):
a.elems[0].ivalue = 1
def test_element_inheritance(self):
class E(dm.Element):
ivalue: int = 43
strvalue: str = "test"
fvalue: float = 1.4322
ar_int: list[int] = [1, 54, 65]
ar_int2: list[int] = [1, 54, 65]
class E2(E):
ivalue2: int = 43
class A(dm.Appliance):
elems: list[E] = [
E(ivalue=45, strvalue="coucou", ar_int=[5, 7]),
E2(ivalue=46, strvalue="coucou2", ar_int=[50, 7], ivalue2=32),
]
elem: E = E()
elem2: E2 = E2(ivalue=7, ivalue2=33)
a = A()
self.assertIsInstance(a.elems, tuple)
self.assertEqual(len(a.elems), 2)
self.assertIsInstance(a.elems[0], E)
self.assertIsInstance(a.elems[0].ivalue, int)
self.assertEqual(a.elems[0].ivalue, 45)
self.assertIsInstance(a.elems[0].strvalue, str)
self.assertEqual(a.elems[0].strvalue, "coucou")
self.assertIsInstance(a.elems[0].fvalue, float)
self.assertEqual(a.elems[0].fvalue, 1.4322)
self.assertIsInstance(a.elems[0].ar_int, tuple)
self.assertEqual(a.elems[0].ar_int, (5, 7))
self.assertIsInstance(a.elems[0].ar_int2, tuple)
self.assertEqual(a.elems[0].ar_int2, (1, 54, 65))
self.assertIsInstance(a.elems[1], E2)
self.assertIsInstance(a.elems[1].ivalue, int)
self.assertEqual(a.elems[1].ivalue, 46)
self.assertIsInstance(a.elems[1].ivalue2, int)
self.assertEqual(a.elems[1].ivalue2, 32)
self.assertIsInstance(a.elems[1].strvalue, str)
self.assertEqual(a.elems[1].strvalue, "coucou2")
self.assertIsInstance(a.elems[1].fvalue, float)
self.assertEqual(a.elems[1].fvalue, 1.4322)
self.assertIsInstance(a.elems[1].ar_int, tuple)
self.assertEqual(a.elems[1].ar_int, (50, 7))
self.assertIsInstance(a.elems[1].ar_int2, tuple)
self.assertEqual(a.elems[1].ar_int2, (1, 54, 65))
self.assertIsInstance(a.elem, E)
self.assertIsInstance(a.elem.ivalue, int)
self.assertEqual(a.elem.ivalue, 43)
self.assertIsInstance(a.elem.strvalue, str)
self.assertEqual(a.elem.strvalue, "test")
self.assertIsInstance(a.elem.fvalue, float)
self.assertEqual(a.elem.fvalue, 1.4322)
self.assertIsInstance(a.elem.ar_int, tuple)
self.assertEqual(a.elem.ar_int, (1, 54, 65))
self.assertIsInstance(a.elem.ar_int2, tuple)
self.assertEqual(a.elem.ar_int2, (1, 54, 65))
self.assertIsInstance(a.elem2, E2)
self.assertIsInstance(a.elem2.ivalue, int)
self.assertEqual(a.elem2.ivalue, 7)
self.assertIsInstance(a.elem2.ivalue2, int)
self.assertEqual(a.elem2.ivalue2, 33)
self.assertIsInstance(a.elem2.strvalue, str)
self.assertEqual(a.elem2.strvalue, "test")
self.assertIsInstance(a.elem2.fvalue, float)
self.assertEqual(a.elem2.fvalue, 1.4322)
self.assertIsInstance(a.elem2.ar_int, tuple)
self.assertEqual(a.elem2.ar_int, (1, 54, 65))
self.assertIsInstance(a.elem2.ar_int2, tuple)
self.assertEqual(a.elem2.ar_int2, (1, 54, 65))
def test_element_initializer(self):
class E(dm.Element):
ivalue: int = 43
strvalue: str = "test"
fvalue: float = 1.4322
ar_int: list[int] = [1, 54, 65]
ar_int2: list[int] = [1, 54, 65]
class A(dm.Appliance):
elem: E = E(ivalue=45, strvalue="coucou", ar_int=[5, 7])
@classmethod
def __initializer(self):
self.elem = E(ivalue=12, strvalue="coucou", ar_int=[5, 7])
a = A()
self.assertIsInstance(a.elem, E)
self.assertIsInstance(a.elem.ivalue, int)
self.assertEqual(a.elem.ivalue, 12)
self.assertIsInstance(a.elem.strvalue, str)
self.assertEqual(a.elem.strvalue, "coucou")
self.assertIsInstance(a.elem.fvalue, float)
self.assertEqual(a.elem.fvalue, 1.4322)
self.assertIsInstance(a.elem.ar_int, tuple)
self.assertEqual(a.elem.ar_int, (5, 7))
self.assertIsInstance(a.elem.ar_int2, tuple)
self.assertEqual(a.elem.ar_int2, (1, 54, 65))
def test_element_in_container_initializer(self):
class E(dm.Element):
ivalue: int = 43
strvalue: str = "test"
fvalue: float = 1.4322
ar_int: list[int] = [1, 54, 65]
ar_int2: list[int] = [1, 54, 65]
class A(dm.Appliance):
elems: list[E] = [E(ivalue=45, strvalue="coucou", ar_int=[5, 7])]
class B(A):
@classmethod
def __initializer(cls):
cls.elems.append(E(ivalue=46, strvalue="coucou2", ar_int=[50, 7]))
a = A()
b = B()
self.assertIsInstance(a.elems, tuple)
self.assertEqual(len(a.elems), 1)
self.assertIsInstance(a.elems[0], E)
self.assertIsInstance(a.elems[0].ivalue, int)
self.assertEqual(a.elems[0].ivalue, 45)
self.assertIsInstance(a.elems[0].strvalue, str)
self.assertEqual(a.elems[0].strvalue, "coucou")
self.assertIsInstance(a.elems[0].fvalue, float)
self.assertEqual(a.elems[0].fvalue, 1.4322)
self.assertIsInstance(a.elems[0].ar_int, tuple)
self.assertEqual(a.elems[0].ar_int, (5, 7))
self.assertIsInstance(a.elems[0].ar_int2, tuple)
self.assertEqual(a.elems[0].ar_int2, (1, 54, 65))
self.assertIsInstance(b.elems, tuple)
self.assertEqual(len(b.elems), 2)
self.assertIsInstance(b.elems[0], E)
self.assertIsInstance(b.elems[0].ivalue, int)
self.assertEqual(b.elems[0].ivalue, 45)
self.assertIsInstance(b.elems[0].strvalue, str)
self.assertEqual(b.elems[0].strvalue, "coucou")
self.assertIsInstance(b.elems[0].fvalue, float)
self.assertEqual(b.elems[0].fvalue, 1.4322)
self.assertIsInstance(b.elems[0].ar_int, tuple)
self.assertEqual(b.elems[0].ar_int, (5, 7))
self.assertIsInstance(b.elems[0].ar_int2, tuple)
self.assertEqual(b.elems[0].ar_int2, (1, 54, 65))
self.assertIsInstance(b.elems[1], E)
self.assertIsInstance(b.elems[1].ivalue, int)
self.assertEqual(b.elems[1].ivalue, 46)
self.assertIsInstance(b.elems[1].strvalue, str)
self.assertEqual(b.elems[1].strvalue, "coucou2")
self.assertIsInstance(b.elems[1].fvalue, float)
self.assertEqual(b.elems[1].fvalue, 1.4322)
self.assertIsInstance(b.elems[1].ar_int, tuple)
self.assertEqual(b.elems[1].ar_int, (50, 7))
self.assertIsInstance(b.elems[1].ar_int2, tuple)
self.assertEqual(b.elems[1].ar_int2, (1, 54, 65))
# ---------- main ----------
if __name__ == "__main__":
unittest.main()

730
test/test_feature.py Normal file
View File

@@ -0,0 +1,730 @@
# dabmodel (c) by chacha
#
# dabmodel is licensed under a
# Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Unported License.
#
# You should have received a copy of the license along with this
# work. If not, see <https://creativecommons.org/licenses/by-nc-sa/4.0/>.
import unittest
from os import chdir
from pathlib import Path
from typing import (
Any,
Annotated,
)
print(__name__)
print(__package__)
from src import dabmodel as dm
testdir_path = Path(__file__).parent.resolve()
chdir(testdir_path.parent.resolve())
def test_initializer_safe_testfc():
eval("print('hi')")
class FeatureTest(unittest.TestCase):
def setUp(self):
print("\n->", unittest.TestCase.id(self))
def immutable_vars__test_field(
self, obj: Any, name: str, default_value: Any, test_value: Any
):
# field is not in the class
self.assertNotIn(name, dir(obj.__class__))
# field is in the object
self.assertIn(name, dir(obj))
# field is in the schema
self.assertIn(name, obj.__LAMSchema__.keys())
# field is readable
self.assertEqual(getattr(obj, name), default_value)
# field is read only
with self.assertRaises(dm.ReadOnlyField):
setattr(obj, name, test_value)
def test_simple(self):
"""Testing first appliance feature, and Field types (simple)"""
# class can be created
class Appliance1(dm.Appliance):
VarStrOuter: str = "testvalue APPLIANCE"
class Feature1(dm.Feature):
VarStrInner: str = "testvalue FEATURE"
app1 = Appliance1()
self.assertIsInstance(Appliance1.__LAMSchema__["VarStrOuter"], dm.LAMField)
self.assertIsInstance(app1.__LAMSchema__["VarStrOuter"], dm.FrozenLAMField)
self.assertIn("Feature1", app1.__LAMSchema__["features"])
self.assertIn(
"VarStrInner", app1.__LAMSchema__["features"]["Feature1"].__LAMSchema__
)
self.assertIsInstance(
app1.__LAMSchema__["features"]["Feature1"].__LAMSchema__["VarStrInner"],
dm.LAMField,
)
self.assertTrue(hasattr(app1, "Feature1"))
self.assertIsInstance(
app1.Feature1.__LAMSchema__["VarStrInner"], dm.FrozenLAMField
)
self.assertTrue(hasattr(app1.Feature1, "VarStrInner"))
def test_inheritance(self):
"""Testing first appliance feature, and Field types (simple)"""
# class can be created
class Appliance1(dm.Appliance):
VarStrOuter: str = "testvalue APPLIANCE1"
class Feature1(dm.Feature):
VarStrInner: str = "testvalue FEATURE1"
VarInt: int = 42
print(dir(Appliance1))
class Appliance2(Appliance1):
VarStrOuter = "testvalue APPLIANCE2"
class Feature2(dm.Feature):
VarStrInner: str = "testvalue FEATURE2"
print(dir(Appliance2))
class Appliance3(Appliance2):
VarStrOuter = "testvalue APPLIANCE3"
class Feature1(Appliance1.Feature1):
VarStrInner = "testvalue FEATURE1 modded"
class Feature3(dm.Feature):
VarStrInner: str = "testvalue FEATURE3"
print(dir(Appliance3))
app1 = Appliance1()
app2 = Appliance2()
app3 = Appliance3()
self.assertIsInstance(Appliance1.__LAMSchema__["VarStrOuter"], dm.LAMField)
self.assertIsInstance(app1.__LAMSchema__["VarStrOuter"], dm.FrozenLAMField)
self.assertIn("Feature1", app1.__LAMSchema__["features"])
self.assertIn(
"VarStrInner", app1.__LAMSchema__["features"]["Feature1"].__LAMSchema__
)
self.assertIsInstance(
app1.__LAMSchema__["features"]["Feature1"].__LAMSchema__["VarStrInner"],
dm.LAMField,
)
self.assertTrue(hasattr(app1, "Feature1"))
self.assertIsInstance(
app1.Feature1.__LAMSchema__["VarStrInner"], dm.FrozenLAMField
)
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_inheritance2(self):
"""Testing first appliance feature, and Field types (simple)"""
# class can be created
class Appliance1(dm.Appliance):
class Feature1(dm.Feature):
VarStrInner: str = "testvalue FEATURE1"
# check cannot REdefine a feature from Feature
with self.assertRaises(dm.InvalidFeatureInheritance):
class Appliance2(Appliance1):
class Feature1(dm.Feature): ...
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_register(self):
"""Testing first appliance feature, and Field types (simple)"""
# class can be created
class Appliance1(dm.Appliance):
pass
class Feature1(dm.Feature):
_BoundAppliance = Appliance1
VarStrInner: str = "testvalue FEATURE1"
app = Appliance1(feat1=Feature1)
self.assertEqual(app.feat1.VarStrInner, "testvalue FEATURE1")
# check it does not leak accross instances
app = Appliance1(feat2=Feature1)
self.assertEqual(app.feat2.VarStrInner, "testvalue FEATURE1")
with self.assertRaises(AttributeError):
app.feat1
def test_register_notbound(self):
"""Testing first appliance feature, and Field types (simple)"""
# class can be created
class Appliance1(dm.Appliance):
pass
class Feature1(dm.Feature):
VarStrInner: str = "testvalue FEATURE1"
with self.assertRaises(dm.FeatureNotBound):
Appliance1(feat1=Feature1)
def test_register_defect(self):
class Feature1(dm.Feature):
pass
with self.assertRaises(dm.FeatureNotBound):
Feature1()
def test_new_field_forbidden(self):
class App(dm.Appliance):
x: int = 1
app = App()
with self.assertRaises(dm.NewFieldForbidden):
app.y = 2
def test_inherit_declared(self):
class App(dm.Appliance):
class F1(dm.Feature):
val: int = 1
class MyF1(App.F1):
val = 2
val2: str = "toto"
app = App(F1=MyF1)
self.assertIsInstance(app.F1, MyF1)
self.assertEqual(app.F1.val, 2)
self.assertEqual(app.F1.val2, "toto")
def test_override_declared(self):
class App(dm.Appliance):
class F1(dm.Feature):
val: int = 1
val2: str = "toto"
app = App(F1={"val": 42, "val2": "tata"})
self.assertEqual(app.F1.val, 42)
self.assertEqual(app.F1.val2, "tata")
def test_dict_override_type_error(self):
class App(dm.Appliance):
class F1(dm.Feature):
val: int = 1
# wrong type for val → must raise InvalidFieldValue
with self.assertRaises(dm.InvalidFieldValue):
App(F1={"val": "not-an-int"})
def test_dict_override_nonexisting_field(self):
class App(dm.Appliance):
class F1(dm.Feature):
val: int = 1
# field does not exist → must raise
with self.assertRaises(dm.InvalidFieldValue):
App(F1={"doesnotexist": 123})
def test_inheritance_with_extra_fields(self):
class App(dm.Appliance):
class F1(dm.Feature):
val: int = 1
class MyF1(App.F1):
val = 2
extra: str = "hello"
app = App(F1=MyF1)
self.assertEqual(app.F1.val, 2)
self.assertEqual(app.F1.extra, "hello")
def test_not_bound_runtime_attach_fails(self):
class App(dm.Appliance):
pass
class UnboundFeature(dm.Feature):
x: int = 1
# attaching an unbound feature should raise
with self.assertRaises(dm.FeatureNotBound):
App(Unbound=UnboundFeature)
def test_override_does_not_leak_between_instances(self):
class App(dm.Appliance):
class F1(dm.Feature):
val: int = 1
app1 = App(F1={"val": 99})
app2 = App()
self.assertEqual(app1.F1.val, 99)
self.assertEqual(app2.F1.val, 1)
def test_deepfreeze_nested_mixed_tuple_list(self):
class App(dm.Appliance):
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.Appliance):
SomeVar: list = []
with self.assertRaises(dm.InvalidFieldAnnotation):
class Appliance2(dm.Appliance):
SomeVar: list[Any] = []
with self.assertRaises(dm.InvalidFieldAnnotation):
class Appliance3(dm.Appliance):
SomeVar: list[object] = []
with self.assertRaises(dm.InvalidFieldAnnotation):
class Appliance4(dm.Appliance):
SomeVar: dict = {}
with self.assertRaises(dm.InvalidFieldAnnotation):
class Appliance5(dm.Appliance):
SomeVar: dict[str, Any] = {}
with self.assertRaises(dm.InvalidFieldAnnotation):
class Appliance6(dm.Appliance):
SomeVar: dict[Any, Any] = {}
with self.assertRaises(dm.InvalidFieldAnnotation):
class Appliance7(dm.Appliance):
SomeVar: dict[Any, str] = {}
with self.assertRaises(dm.InvalidFieldAnnotation):
class Appliance8(dm.Appliance):
SomeVar: dict[str, object] = {}
def test_runtime_attach_bound_success(self):
class App(dm.Appliance):
class F1(dm.Feature):
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_annotation(self):
class App(dm.Appliance):
class F1(dm.Feature):
val: int = 1
with self.assertRaises(dm.ReadOnlyFieldAnnotation):
class Extra(App.F1):
val: str = "test"
def test_fields_are_frozen_after_override(self):
class App(dm.Appliance):
class F(dm.Feature):
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_dict_partial_override_keeps_other_defaults(self):
class App(dm.Appliance):
class F(dm.Feature):
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_override_linear_chain(self):
# Base appliance defines Feat1
class A(dm.Appliance):
class Feat1(dm.Feature):
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* Feature, not subclass of B.Feat1
with self.assertRaises(dm.InvalidFeatureInheritance):
class D(B):
class Feat1(dm.Feature):
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.Feature):
other: str = "ok"
self.assertTrue(hasattr(F, "Feat2"))
def test_override_chain_runtime_replacement(self):
# Build a linear chain: A -> B -> C for feature 'Feat1'
class A(dm.Appliance):
class Feat1(dm.Feature):
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_inheritance_tree_and_no_leakage(self):
class A(dm.Appliance):
class F1(dm.Feature):
a: int = 1
class F2(dm.Feature):
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.Feature):
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_isolation(self):
class A(dm.Appliance):
class F1(dm.Feature):
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.Appliance):
class F1(dm.Feature):
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"))
def test_feature_dict_override_with_nested_containers(self):
class App(dm.Appliance):
class F1(dm.Feature):
values: list[int] = [1, 2]
app = App(F1={"values": [5, 6]})
self.assertEqual(app.F1.values, (5, 6)) # deepfreeze → tuple
# Invalid type in list should fail
with self.assertRaises(dm.InvalidFieldValue):
App(F1={"values": [1, "oops"]})
def test_dict_override_with_unknown_key(self):
class App(dm.Appliance):
class F1(dm.Feature):
a: int = 1
# Dict override with unknown field 'zzz'
with self.assertRaises(dm.InvalidFieldValue):
App(F1={"zzz": 42})
def test_schema_isolation_across_multiple_overrides(self):
class App(dm.Appliance):
class F1(dm.Feature):
a: int = 1
class F1a(App.F1):
a = 10
class F1b(App.F1):
a = 20
app1 = App(F1=F1a)
self.assertIsInstance(app1.F1, F1a)
self.assertEqual(app1.F1.a, 10)
app2 = App(F1=F1b)
self.assertIsInstance(app2.F1, F1b)
self.assertEqual(app2.F1.a, 20)
# Original appliance schema must not be polluted
app3 = App()
self.assertIsInstance(app3.F1, App.F1)
self.assertEqual(app3.F1.a, 1)
def test_inheritance_with_annotated_fields(self):
class App(dm.Appliance):
class F1(dm.Feature):
a: Annotated[int, dm.LAMFieldInfo(doc="field a")] = 1
# ✅ Subclass override must inherit from parent F1
class F1Ex(App.F1):
b: str = "ok"
app = App(F1=F1Ex)
self.assertIsInstance(app.F1, F1Ex)
self.assertEqual((app.F1.a, app.F1.b), (1, "ok"))
# ❌ Wrong: fresh Feature under same name
with self.assertRaises(dm.InvalidFeatureInheritance):
class Bad(App):
class F1(dm.Feature):
fail: str = "oops"
# ---------- main ----------
if __name__ == "__main__":
unittest.main()