Compare commits

...

7 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
chacha
cd69fc22a8 continue renaming 2025-09-20 18:44:19 +02:00
chacha
9aec2d66cd reorganize and rename (partial) 2025-09-20 18:27:36 +02:00
chacha
af81ec5fd3 more test cases 2025-09-20 13:18:40 +02:00
19 changed files with 2003 additions and 1316 deletions

View File

@@ -0,0 +1,17 @@
from typing import Generic, TypeVar
T_Field = TypeVar("T_Field")
class Constraint(Generic[T_Field]):
"""Constraint class
Base class for Field's constraints
"""
_bound_type: type
def __init__(self): ...
def check(self, value: T_Field) -> bool:
"""Check if a Constraint is completed"""
return True

View File

@@ -0,0 +1,41 @@
from typing import Generic, TypeVar, Any
from .LAMField import LAMField
from .Constraint import Constraint
from ..tools import LAMdeepfreeze
T_Field = TypeVar("T_Field")
class FrozenLAMField(Generic[T_Field]):
"""FrozenLAMField class
a read-only proxy of a Field
"""
def __init__(self, inner_field: LAMField[T_Field]):
self._inner_field = inner_field
@property
def doc(self) -> str:
"""Returns Field's documentation (frozen)"""
return LAMdeepfreeze(self._inner_field.doc)
@property
def constraints(self) -> tuple[Constraint]:
"""Returns Field's constraint (frozen)"""
return LAMdeepfreeze(self._inner_field.constraints)
@property
def default_value(self) -> Any:
"""Returns Field's default value (frozen)"""
return self._inner_field.default_value
@property
def value(self) -> Any:
"""Returns Field's value (frosen)"""
return self._inner_field.value
@property
def annotations(self) -> Any:
"""Returns Field's annotation (frozen)"""
return LAMdeepfreeze(self._inner_field.annotations)

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

@@ -0,0 +1,62 @@
from typing import Generic, TypeVar, Optional, Any
from .LAMFieldInfo import LAMFieldInfo
from .Constraint import Constraint
from ..tools import LAMdeepfreeze
TV_LABField = TypeVar("TV_LABField")
class LAMField(Generic[TV_LABField]):
"""This class describe a Field in Schema"""
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[TV_LABField] = v
self._value: Optional[TV_LABField] = v
self._annotations: Any = a
self._info: LAMFieldInfo = i
self._constraints: list[Constraint[Any]] = i.constraints
def add_source(self, s: type) -> None:
"""Adds source Appliance to the Field"""
self._source = s
@property
def doc(self) -> str:
"""Returns Field's documentation"""
return self._info.doc
def add_constraint(self, c: Constraint) -> None:
"""Adds constraint to the Field"""
self._constraints.append(c)
@property
def constraints(self) -> list[Constraint]:
"""Returns Field's constraint"""
return self._info.constraints
@property
def default_value(self) -> Any:
"""Returns Field's default value (frozen)"""
return LAMdeepfreeze(self._default_value)
def update_value(self, v: Optional[TV_LABField] = None) -> None:
"""Updates Field's value"""
self._value = v
@property
def value(self) -> Any:
"""Returns Field's value (frozen)"""
return LAMdeepfreeze(self._value)
@property
def raw_value(self) -> Optional[TV_LABField]:
"""Returns Field's value"""
return self._value
@property
def annotations(self) -> Any:
"""Returns Field's annotation"""
return self._annotations

View File

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

View File

View File

@@ -11,11 +11,18 @@ Main module __init__ file.
"""
from .__metadata__ import __version__, __Summuary__, __Name__
from .model import (
DABFieldInfo,
DABField,
BaseAppliance,
BaseFeature,
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 (
DABModelException,
MultipleInheritanceForbidden,
BrokenInheritance,
@@ -28,7 +35,9 @@ from .model import (
IncompletelyAnnotatedField,
ImportForbidden,
FunctionForbidden,
FrozenDABField,
InvalidFeatureInheritance,
FeatureNotBound,
UnsupportedFieldType,
)
__all__ = [name for name in globals() if not name.startswith("_")]

View File

@@ -0,0 +1,9 @@
from .element import Element
from .meta.appliance import _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.
"""

7
src/dabmodel/element.py Normal file
View File

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

109
src/dabmodel/exception.py Normal file
View File

@@ -0,0 +1,109 @@
class DABModelException(Exception):
"""DABModelException Exception class
Base Exception for DABModelException class
"""
class FunctionForbidden(DABModelException): ...
class ExternalCodeForbidden(FunctionForbidden): ...
class ClosureForbidden(FunctionForbidden): ...
class ReservedFieldName(Exception):
"""DABModelException Exception class
Base Exception for DABModelException class
"""
class MultipleInheritanceForbidden(DABModelException):
"""MultipleInheritanceForbidden Exception class
Multiple inheritance is forbidden when using dabmodel
"""
class BrokenInheritance(DABModelException):
"""BrokenInheritance Exception class
inheritance chain is broken
"""
class ReadOnlyField(DABModelException):
"""ReadOnlyField Exception class
The used Field is ReadOnly
"""
class NewFieldForbidden(DABModelException):
"""NewFieldForbidden Exception class
Field creation is forbidden
"""
class InvalidFieldAnnotation(DABModelException):
"""InvalidFieldAnnotation Exception class
The field annotation is invalid
"""
class InvalidInitializerType(DABModelException):
"""InvalidInitializerType Exception class
The initializer is not a valid type
"""
class NotAnnotatedField(InvalidFieldAnnotation):
"""NotAnnotatedField Exception class
The Field is not Annotated
"""
class IncompletelyAnnotatedField(InvalidFieldAnnotation):
"""IncompletelyAnnotatedField Exception class
The field annotation is incomplete
"""
class UnsupportedFieldType(InvalidFieldAnnotation):
"""UnsupportedFieldType Exception class
The field type is unsupported
"""
class ReadOnlyFieldAnnotation(DABModelException):
"""ReadOnlyFieldAnnotation Exception class
Field annotation connot be modified
"""
class InvalidFieldValue(DABModelException):
"""InvalidFieldValue Exception class
The Field value is invalid
"""
class NonExistingField(DABModelException):
"""NonExistingField Exception class
The given Field is non existing
"""
class ImportForbidden(DABModelException):
"""ImportForbidden Exception class
Imports are forbidden
"""
class InvalidFeatureInheritance(DABModelException):
"""InvalidFeatureInheritance Exception class
Features of same name in child appliance need to be from same type
"""
class FeatureNotBound(DABModelException):
"""FeatureNotBound Exception class
a Feature must be bound to the appliance (or parent)
"""

12
src/dabmodel/feature.py Normal file
View File

@@ -0,0 +1,12 @@
from .element import Element
from .meta.feature import _MetaFeature
class Feature(metaclass=_MetaFeature):
"""Feature class
Base class for Appliance's Features.
Features are optional traits of an appliance.
"""
_BoundAppliance: "Optional[Type[BaseAppliance]]" = None
Enabled: bool = False

View File

View File

@@ -0,0 +1,213 @@
from typing import Any, Type
from copy import copy
from typeguard import check_type, CollectionCheckStrategy, TypeCheckError
from ..tools import LAMdeepfreeze
from ..LAMFields.LAMField import LAMField
from ..LAMFields.FrozenLAMField import FrozenLAMField
from .element import _MetaElement
from ..feature import Feature
from ..exception import (
InvalidFieldValue,
InvalidFeatureInheritance,
FeatureNotBound,
)
class _MetaAppliance(_MetaElement):
"""_MetaAppliance class
Appliance specific metaclass code
"""
@classmethod
def check_class(
mcs: type["meta"],
name: str,
bases: tuple[type[Any], ...],
namespace: dict[str, Any], # pylint: disable=unused-argument
extensions: dict[str, Any],
) -> None:
"""
Appliance-specific pre-check: ensure the `features` slot exists in schema.
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["__LAMSchema__"]:
namespace["__LAMSchema__"]["features"] = {}
else:
namespace["__LAMSchema__"]["features"] = copy(
namespace["__LAMSchema__"]["features"]
)
@classmethod
def process_class_fields(
mcs: type["meta"],
name: str,
bases: tuple[type[Any], ...],
namespace: dict[str, Any],
extensions: dict[str, Any],
):
"""
Like meta.process_class_fields but also stages Feature declarations.
Initializes:
extensions["new_features"], extensions["modified_features"]
then defers to the base scanner for regular fields.
"""
extensions["new_features"] = {}
extensions["modified_features"] = {}
super().process_class_fields(name, bases, namespace, extensions) # type: ignore[misc]
@classmethod
def process_new_field(
mcs: type["meta"],
name: str,
bases: tuple[type[Any], ...],
namespace: dict[str, Any],
_fname: str,
_fvalue: Any,
extensions: dict[str, Any],
): # pylint: disable=unused-argument
"""
Intercept Feature declarations.
- If `_fname` already exists in parent's `features`, enforce same type;
stage into `modified_features`.
- 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["__LAMSchema__"]["features"].keys():
if not issubclass(_fvalue, namespace["__LAMSchema__"]["features"][_fname]):
raise InvalidFeatureInheritance(
f"Feature {_fname} is not an instance of {bases[0]}.{_fname}"
)
extensions["modified_features"][_fname] = _fvalue
elif isinstance(_fvalue, type) and issubclass(_fvalue, Feature):
extensions["new_features"][_fname] = _fvalue
else:
super().process_new_field(name, bases, namespace, _fname, _fvalue, extensions) # type: ignore[misc]
@classmethod
def commit_fields(
mcs: type["meta"],
cls,
name: str,
bases: tuple[type[Any], ...],
namespace: dict[str, Any], # pylint: disable=unused-argument
extensions: dict[str, Any],
):
"""
Commit regular fields (via meta) and then bind staged Feature classes.
For each new/modified feature:
- bind it to `cls` (sets the feature's `_BoundAppliance`),
- 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.__LAMSchema__["features"][_ftname] = _ftvalue
for _ftname, _ftvalue in extensions["new_features"].items():
_ftvalue._BoundAppliance = cls # pylint: disable=protected-access
cls.__LAMSchema__["features"][_ftname] = _ftvalue
def finalize_instance(cls: Type, obj, extensions: dict[str, Any]):
"""
Instantiate and attach all features declared (or overridden) in the instance schema.
Handles:
- Declared features (plain class)
- Subclass replacements
- Dict overrides (class + patch dict)
"""
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()
object.__setattr__(obj, fname, inst)
# Case 2: (class, dict) → dict overrides
elif isinstance(fdef, tuple) and len(fdef) == 2:
feat_cls, overrides = fdef
inst = feat_cls()
for field_name, new_val in overrides.items():
if field_name not in feat_cls.__LAMSchema__:
raise InvalidFieldValue(
f"Feature '{fname}' has no field '{field_name}'"
)
field = feat_cls.__LAMSchema__[field_name]
try:
check_type(
new_val,
field.annotations,
collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS,
)
except TypeCheckError as exp:
raise InvalidFieldValue(
f"Invalid value for {fname}.{field_name}: "
f"expected {field.annotations}, got {new_val!r}"
) from exp
object.__setattr__(inst, field_name, LAMdeepfreeze(new_val))
inst.__LAMSchema__[field_name] = FrozenLAMField(
LAMField(field_name, new_val, field.annotations, field._info)
)
object.__setattr__(obj, fname, inst)
else:
raise InvalidFieldValue(
f"Invalid feature definition stored for '{fname}': {fdef!r}"
)
def apply_overrides(cls, obj, extensions, *args, **kwargs):
"""
Support for runtime field and feature overrides.
Fields:
MyApp(name="foo")
Features:
MyApp(F1=MyF1) # inheritance / replacement
MyApp(F1={"val": 42, ...}) # dict override of existing feature
"""
# --- feature overrides ---
for k, v in list(kwargs.items()):
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):
bound = getattr(v, "_BoundAppliance", None)
if bound is None or not issubclass(cls, bound):
raise FeatureNotBound(
f"Feature {v.__name__} is not bound to {cls.__name__}"
)
# record subclass into instance schema
obj.__LAMSchema__["features"][k] = v
kwargs.pop(k)
# Case 2: dict override
elif isinstance(v, dict):
# store (class, override_dict) for finalize_instance
obj.__LAMSchema__["features"][k] = (base_feat_cls, v)
kwargs.pop(k)
else:
raise InvalidFieldValue(
f"Feature override for '{k}' must be a Feature subclass or dict, got {type(v)}"
)
# --- new features not declared at class level ---
for k, v in list(kwargs.items()):
if isinstance(v, type) and issubclass(v, Feature):
bound = getattr(v, "_BoundAppliance", None)
if bound is None or not issubclass(cls, bound):
raise FeatureNotBound(
f"Feature {v.__name__} is not bound to {cls.__name__}"
)
obj.__LAMSchema__["features"][k] = v
kwargs.pop(k)
super().apply_overrides(obj, extensions, *args, **kwargs)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,19 @@
from typing import Type, Any
from .element import _MetaElement
from ..exception import FeatureNotBound
class _MetaFeature(_MetaElement):
"""_MetaFeature class
Feature specific metaclass code
"""
def __call__(cls: Type, *args: Any, **kw: Any): # intentionally untyped
"""BaseFeature new instance"""
if cls._BoundAppliance is None:
raise FeatureNotBound()
obj = super().__call__(*args, **kw)
return obj

View File

@@ -1,11 +1,17 @@
"""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
class DABJSONEncoder(json.JSONEncoder):
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"""
def default(self, o):
@@ -15,3 +21,16 @@ class DABJSONEncoder(json.JSONEncoder):
if isinstance(o, datetime):
return str(o)
return json.JSONEncoder.default(self, o)
def LAMdeepfreeze(value):
"""recursive freeze helper function"""
if isinstance(value, dict):
return deepfreeze(value)
if isinstance(value, set):
return frozenset(LAMdeepfreeze(v) for v in value)
if isinstance(value, list):
return tuple(LAMdeepfreeze(v) for v in value)
if isinstance(value, tuple):
return tuple(LAMdeepfreeze(v) for v in value)
return value

File diff suppressed because it is too large Load Diff

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()