diff --git a/src/dabmodel/LAMFields/Constraint.py b/src/dabmodel/LAMFields/Constraint.py
index 1c1d64c..e9d1e3a 100644
--- a/src/dabmodel/LAMFields/Constraint.py
+++ b/src/dabmodel/LAMFields/Constraint.py
@@ -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
"""
diff --git a/src/dabmodel/LAMFields/FrozenLAMField.py b/src/dabmodel/LAMFields/FrozenLAMField.py
index 5b45d55..8df202d 100644
--- a/src/dabmodel/LAMFields/FrozenLAMField.py
+++ b/src/dabmodel/LAMFields/FrozenLAMField.py
@@ -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)
diff --git a/src/dabmodel/LAMFields/LAMCompatible.py b/src/dabmodel/LAMFields/LAMCompatible.py
new file mode 100644
index 0000000..e3af4bf
--- /dev/null
+++ b/src/dabmodel/LAMFields/LAMCompatible.py
@@ -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 don’t 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.)."""
+ ...
diff --git a/src/dabmodel/LAMFields/LAMField.py b/src/dabmodel/LAMFields/LAMField.py
index 11575f7..2437520 100644
--- a/src/dabmodel/LAMFields/LAMField.py
+++ b/src/dabmodel/LAMFields/LAMField.py
@@ -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
diff --git a/src/dabmodel/LAMFields/LAMFieldInfo.py b/src/dabmodel/LAMFields/LAMFieldInfo.py
index 3b0487c..880b6a7 100644
--- a/src/dabmodel/LAMFields/LAMFieldInfo.py
+++ b/src/dabmodel/LAMFields/LAMFieldInfo.py
@@ -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
diff --git a/src/dabmodel/__init__.py b/src/dabmodel/__init__.py
index 2bb9fe5..3af5f89 100644
--- a/src/dabmodel/__init__.py
+++ b/src/dabmodel/__init__.py
@@ -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,6 +37,7 @@ from .exception import (
FunctionForbidden,
InvalidFeatureInheritance,
FeatureNotBound,
+ UnsupportedFieldType,
)
__all__ = [name for name in globals() if not name.startswith("_")]
diff --git a/src/dabmodel/appliance.py b/src/dabmodel/appliance.py
index c65ce55..af2b76e 100644
--- a/src/dabmodel/appliance.py
+++ b/src/dabmodel/appliance.py
@@ -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.
diff --git a/src/dabmodel/element.py b/src/dabmodel/element.py
index 3567248..bf30164 100644
--- a/src/dabmodel/element.py
+++ b/src/dabmodel/element.py
@@ -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.
"""
diff --git a/src/dabmodel/exception.py b/src/dabmodel/exception.py
index b4ebbf1..948c291 100644
--- a/src/dabmodel/exception.py
+++ b/src/dabmodel/exception.py
@@ -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
diff --git a/src/dabmodel/feature.py b/src/dabmodel/feature.py
index f5fe80b..634f9c7 100644
--- a/src/dabmodel/feature.py
+++ b/src/dabmodel/feature.py
@@ -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.
diff --git a/src/dabmodel/meta/appliance.py b/src/dabmodel/meta/appliance.py
index 66fc325..26a17dd 100644
--- a/src/dabmodel/meta/appliance.py
+++ b/src/dabmodel/meta/appliance.py
@@ -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,
@@ -171,26 +171,6 @@ 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.__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)
# --- feature overrides ---
for k, v in list(kwargs.items()):
@@ -230,6 +210,4 @@ class _MetaAppliance(_MetaElement):
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)
diff --git a/src/dabmodel/meta/base.py b/src/dabmodel/meta/element.py
similarity index 82%
rename from src/dabmodel/meta/base.py
rename to src/dabmodel/meta/element.py
index 63bd554..0262767 100644
--- a/src/dabmodel/meta/base.py
+++ b/src/dabmodel/meta/element.py
@@ -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:
@@ -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].__LAMSchema__)
+ 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
)
@@ -580,14 +606,14 @@ 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():
@@ -601,6 +627,7 @@ class _MetaElement(type):
raise InvalidFieldValue(
f"Value of Field <{_fname}> is not of expected type {namespace['__annotations__'][_fname]}."
) from exp
+ cls.__LAMSchema__[_fname] = deepcopy(cls.__LAMSchema__[_fname])
cls.__LAMSchema__[_fname].update_value(_fvalue)
def __new__(
@@ -632,7 +659,7 @@ 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.
@@ -682,7 +709,7 @@ 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.__LAMSchema__)
for _fname, _fvalue in cls.__LAMSchema__.items():
@@ -692,7 +719,7 @@ class _MetaElement(type):
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.
@@ -738,7 +786,7 @@ class _MetaElement(type):
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:
diff --git a/src/dabmodel/meta/feature.py b/src/dabmodel/meta/feature.py
index 1c25012..c4c6a93 100644
--- a/src/dabmodel/meta/feature.py
+++ b/src/dabmodel/meta/feature.py
@@ -1,5 +1,5 @@
from typing import Type, Any
-from .base import _MetaElement
+from .element import _MetaElement
from ..exception import FeatureNotBound
diff --git a/src/dabmodel/tools.py b/src/dabmodel/tools.py
index 85697e2..03e54ba 100644
--- a/src/dabmodel/tools.py
+++ b/src/dabmodel/tools.py
@@ -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"""
diff --git a/test/test_model.py b/test/test_appliance.py
similarity index 78%
rename from test/test_model.py
rename to test/test_appliance.py
index 83dd715..ac2d3f1 100644
--- a/test/test_model.py
+++ b/test/test_appliance.py
@@ -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))
@@ -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")
@@ -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]
@@ -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.__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_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.__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_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 ----------
diff --git a/test/test_element.py b/test/test_element.py
new file mode 100644
index 0000000..952e3cf
--- /dev/null
+++ b/test/test_element.py
@@ -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 .
+
+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()
diff --git a/test/test_feature.py b/test/test_feature.py
new file mode 100644
index 0000000..1b95d6c
--- /dev/null
+++ b/test/test_feature.py
@@ -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 .
+
+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()