Compare commits
4 Commits
0.0.1.post
...
0.0.1.post
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
616a53578c | ||
|
|
d20712a72f | ||
|
|
2837b6439f | ||
|
|
b4d6ed6130 |
BIN
dabmodel.zip
BIN
dabmodel.zip
Binary file not shown.
@@ -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
|
||||
"""
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
38
src/dabmodel/LAMFields/LAMCompatible.py
Normal file
38
src/dabmodel/LAMFields/LAMCompatible.py
Normal 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 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.)."""
|
||||
...
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
0
src/dabmodel/LAMFields/__init__.py
Normal file
0
src/dabmodel/LAMFields/__init__.py
Normal file
@@ -16,8 +16,10 @@ from .__metadata__ import __version__, __Summuary__, __Name__
|
||||
from .LAMFields.LAMField import LAMField
|
||||
from .LAMFields.LAMFieldInfo import LAMFieldInfo
|
||||
from .LAMFields.FrozenLAMField import FrozenLAMField
|
||||
from .LAMFields.LAMCompatible import LAMCompatible
|
||||
from .appliance import Appliance
|
||||
from .feature import Feature
|
||||
from .element import Element
|
||||
|
||||
|
||||
from .exception import (
|
||||
@@ -35,4 +37,7 @@ from .exception import (
|
||||
FunctionForbidden,
|
||||
InvalidFeatureInheritance,
|
||||
FeatureNotBound,
|
||||
UnsupportedFieldType,
|
||||
)
|
||||
|
||||
__all__ = [name for name in globals() if not name.startswith("_")]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
0
src/dabmodel/meta/__init__.py
Normal file
0
src/dabmodel/meta/__init__.py
Normal file
@@ -6,7 +6,7 @@ from typeguard import check_type, CollectionCheckStrategy, TypeCheckError
|
||||
from ..tools import LAMdeepfreeze
|
||||
from ..LAMFields.LAMField import LAMField
|
||||
from ..LAMFields.FrozenLAMField import FrozenLAMField
|
||||
from .base import _MetaElement
|
||||
from .element import _MetaElement
|
||||
from ..feature import Feature
|
||||
from ..exception import (
|
||||
InvalidFieldValue,
|
||||
@@ -34,11 +34,11 @@ class _MetaAppliance(_MetaElement):
|
||||
Copies the parent's `features` mapping when inheriting to keep it per-class.
|
||||
"""
|
||||
super().check_class(name, bases, namespace, extensions) # type: ignore[misc]
|
||||
if "features" not in namespace["__DABSchema__"]:
|
||||
namespace["__DABSchema__"]["features"] = {}
|
||||
if "features" not in namespace["__LAMSchema__"]:
|
||||
namespace["__LAMSchema__"]["features"] = {}
|
||||
else:
|
||||
namespace["__DABSchema__"]["features"] = copy(
|
||||
namespace["__DABSchema__"]["features"]
|
||||
namespace["__LAMSchema__"]["features"] = copy(
|
||||
namespace["__LAMSchema__"]["features"]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -78,8 +78,8 @@ class _MetaAppliance(_MetaElement):
|
||||
- Else, if `_fvalue` is a Feature *class*, stage into `new_features`.
|
||||
- Otherwise, it is a regular field: delegate to meta.process_new_field.
|
||||
"""
|
||||
if _fname in namespace["__DABSchema__"]["features"].keys():
|
||||
if not issubclass(_fvalue, namespace["__DABSchema__"]["features"][_fname]):
|
||||
if _fname in namespace["__LAMSchema__"]["features"].keys():
|
||||
if not issubclass(_fvalue, namespace["__LAMSchema__"]["features"][_fname]):
|
||||
raise InvalidFeatureInheritance(
|
||||
f"Feature {_fname} is not an instance of {bases[0]}.{_fname}"
|
||||
)
|
||||
@@ -103,16 +103,16 @@ class _MetaAppliance(_MetaElement):
|
||||
|
||||
For each new/modified feature:
|
||||
- bind it to `cls` (sets the feature's `_BoundAppliance`),
|
||||
- register it under `cls.__DABSchema__["features"]`.
|
||||
- register it under `cls.__LAMSchema__["features"]`.
|
||||
"""
|
||||
super().commit_fields(cls, name, bases, namespace, extensions) # type: ignore[misc]
|
||||
|
||||
for _ftname, _ftvalue in extensions["modified_features"].items():
|
||||
_ftvalue._BoundAppliance = cls # pylint: disable=protected-access
|
||||
cls.__DABSchema__["features"][_ftname] = _ftvalue
|
||||
cls.__LAMSchema__["features"][_ftname] = _ftvalue
|
||||
for _ftname, _ftvalue in extensions["new_features"].items():
|
||||
_ftvalue._BoundAppliance = cls # pylint: disable=protected-access
|
||||
cls.__DABSchema__["features"][_ftname] = _ftvalue
|
||||
cls.__LAMSchema__["features"][_ftname] = _ftvalue
|
||||
|
||||
def finalize_instance(cls: Type, obj, extensions: dict[str, Any]):
|
||||
"""
|
||||
@@ -122,7 +122,7 @@ class _MetaAppliance(_MetaElement):
|
||||
- Subclass replacements
|
||||
- Dict overrides (class + patch dict)
|
||||
"""
|
||||
for fname, fdef in obj.__DABSchema__.get("features", {}).items():
|
||||
for fname, fdef in obj.__LAMSchema__.get("features", {}).items():
|
||||
# Case 1: plain class or subclass
|
||||
if isinstance(fdef, type) and issubclass(fdef, Feature):
|
||||
inst = fdef()
|
||||
@@ -133,11 +133,11 @@ class _MetaAppliance(_MetaElement):
|
||||
feat_cls, overrides = fdef
|
||||
inst = feat_cls()
|
||||
for field_name, new_val in overrides.items():
|
||||
if field_name not in feat_cls.__DABSchema__:
|
||||
if field_name not in feat_cls.__LAMSchema__:
|
||||
raise InvalidFieldValue(
|
||||
f"Feature '{fname}' has no field '{field_name}'"
|
||||
)
|
||||
field = feat_cls.__DABSchema__[field_name]
|
||||
field = feat_cls.__LAMSchema__[field_name]
|
||||
try:
|
||||
check_type(
|
||||
new_val,
|
||||
@@ -150,7 +150,7 @@ class _MetaAppliance(_MetaElement):
|
||||
f"expected {field.annotations}, got {new_val!r}"
|
||||
) from exp
|
||||
object.__setattr__(inst, field_name, LAMdeepfreeze(new_val))
|
||||
inst.__DABSchema__[field_name] = FrozenLAMField(
|
||||
inst.__LAMSchema__[field_name] = FrozenLAMField(
|
||||
LAMField(field_name, new_val, field.annotations, field._info)
|
||||
)
|
||||
object.__setattr__(obj, fname, inst)
|
||||
@@ -171,31 +171,11 @@ class _MetaAppliance(_MetaElement):
|
||||
MyApp(F1=MyF1) # inheritance / replacement
|
||||
MyApp(F1={"val": 42, ...}) # dict override of existing feature
|
||||
"""
|
||||
# --- field overrides (unchanged) ---
|
||||
for k, v in list(kwargs.items()):
|
||||
if k in cls.__DABSchema__: # regular field
|
||||
field = cls.__DABSchema__[k]
|
||||
try:
|
||||
check_type(
|
||||
v,
|
||||
field.annotations,
|
||||
collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS,
|
||||
)
|
||||
except TypeCheckError as exp:
|
||||
raise InvalidFieldValue(
|
||||
f"Invalid value for field '{k}': expected {field.annotations}, got {v!r}"
|
||||
) from exp
|
||||
|
||||
object.__setattr__(obj, k, LAMdeepfreeze(v))
|
||||
obj.__DABSchema__[k] = FrozenLAMField(
|
||||
LAMField(k, v, field.annotations, field._info)
|
||||
)
|
||||
kwargs.pop(k)
|
||||
|
||||
# --- feature overrides ---
|
||||
for k, v in list(kwargs.items()):
|
||||
if k in cls.__DABSchema__.get("features", {}):
|
||||
base_feat_cls = cls.__DABSchema__["features"][k]
|
||||
if k in cls.__LAMSchema__.get("features", {}):
|
||||
base_feat_cls = cls.__LAMSchema__["features"][k]
|
||||
|
||||
# Case 1: subclass replacement (inheritance)
|
||||
if isinstance(v, type) and issubclass(v, base_feat_cls):
|
||||
@@ -205,13 +185,13 @@ class _MetaAppliance(_MetaElement):
|
||||
f"Feature {v.__name__} is not bound to {cls.__name__}"
|
||||
)
|
||||
# record subclass into instance schema
|
||||
obj.__DABSchema__["features"][k] = v
|
||||
obj.__LAMSchema__["features"][k] = v
|
||||
kwargs.pop(k)
|
||||
|
||||
# Case 2: dict override
|
||||
elif isinstance(v, dict):
|
||||
# store (class, override_dict) for finalize_instance
|
||||
obj.__DABSchema__["features"][k] = (base_feat_cls, v)
|
||||
obj.__LAMSchema__["features"][k] = (base_feat_cls, v)
|
||||
kwargs.pop(k)
|
||||
|
||||
else:
|
||||
@@ -227,9 +207,7 @@ class _MetaAppliance(_MetaElement):
|
||||
raise FeatureNotBound(
|
||||
f"Feature {v.__name__} is not bound to {cls.__name__}"
|
||||
)
|
||||
obj.__DABSchema__["features"][k] = v
|
||||
obj.__LAMSchema__["features"][k] = v
|
||||
kwargs.pop(k)
|
||||
|
||||
if kwargs:
|
||||
unknown = ", ".join(sorted(kwargs.keys()))
|
||||
raise InvalidFieldValue(f"Unknown parameters: {unknown}")
|
||||
super().apply_overrides(obj, extensions, *args, **kwargs)
|
||||
|
||||
@@ -23,10 +23,12 @@ import inspect, ast, textwrap
|
||||
|
||||
from typeguard import check_type, TypeCheckError, CollectionCheckStrategy
|
||||
|
||||
from ..tools import LAMdeepfreeze
|
||||
from ..LAMFields.LAMField import LAMField
|
||||
from ..LAMFields.LAMFieldInfo import LAMFieldInfo
|
||||
from ..LAMFields.FrozenLAMField import FrozenLAMField
|
||||
|
||||
|
||||
from ..exception import (
|
||||
MultipleInheritanceForbidden,
|
||||
BrokenInheritance,
|
||||
@@ -41,6 +43,7 @@ from ..exception import (
|
||||
FunctionForbidden,
|
||||
NonExistingField,
|
||||
InvalidInitializerType,
|
||||
UnsupportedFieldType,
|
||||
)
|
||||
|
||||
ALLOWED_ANNOTATIONS: dict[str, Any] = {
|
||||
@@ -124,6 +127,10 @@ ALLOWED_HELPERS_DEFAULT: dict[str, object] = {
|
||||
}
|
||||
|
||||
|
||||
class IElement:
|
||||
pass
|
||||
|
||||
|
||||
def _check_initializer_safety(func) -> None:
|
||||
"""
|
||||
Preliminary structural check for __initializer__.
|
||||
@@ -145,38 +152,40 @@ def _check_initializer_safety(func) -> None:
|
||||
mod = ast.parse(src)
|
||||
|
||||
# Find the FunctionDef node that corresponds to this initializer
|
||||
init_node = None
|
||||
for n in mod.body:
|
||||
if (
|
||||
isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
|
||||
init_node = next(
|
||||
(
|
||||
n
|
||||
for n in mod.body
|
||||
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
|
||||
and n.name == func.__name__
|
||||
):
|
||||
init_node = n
|
||||
break
|
||||
),
|
||||
None,
|
||||
)
|
||||
if init_node is None:
|
||||
# Fallback: if not found, analyze nothing further to avoid false positives
|
||||
return
|
||||
|
||||
# Walk ONLY the body of the initializer (don't flag the def itself)
|
||||
body_tree = ast.Module(body=init_node.body, type_ignores=[])
|
||||
|
||||
for node in ast.walk(body_tree):
|
||||
# Forbid imports
|
||||
for node in ast.walk(ast.Module(body=init_node.body, type_ignores=[])):
|
||||
if isinstance(node, (ast.Import, ast.ImportFrom)):
|
||||
raise ImportForbidden("imports disabled in __initializer")
|
||||
|
||||
# Forbid nested defs (but allow lambdas)
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
raise FunctionForbidden(
|
||||
"Nested function definitions are forbidden in __initializer"
|
||||
)
|
||||
|
||||
if isinstance(node, ast.Lambda): # Forbid lambda
|
||||
raise FunctionForbidden("Lambdas are forbidden in __initializer")
|
||||
raise FunctionForbidden("Nested defs are forbidden in __initializer")
|
||||
# if isinstance(node, ast.Lambda):
|
||||
# raise FunctionForbidden("Lambdas are forbidden in __initializer")
|
||||
|
||||
# Optional: forbid closures (keeps determinism; allows lambdas that don't capture)
|
||||
if func.__code__.co_freevars:
|
||||
raise FunctionForbidden("Closures are forbidden in __initializer__")
|
||||
# Inspect captured free vars
|
||||
closure_vars = inspect.getclosurevars(func)
|
||||
captured = {**closure_vars.globals, **closure_vars.nonlocals}
|
||||
for name, val in captured.items():
|
||||
if isinstance(val, type) and issubclass(val, IElement):
|
||||
continue
|
||||
if isinstance(val, (int, str, float, bool, type(None))):
|
||||
continue
|
||||
raise FunctionForbidden(
|
||||
f"Closures are forbidden in __initializer__ (captured: {name}={val!r})"
|
||||
)
|
||||
|
||||
|
||||
def _blocked_import(*args, **kwargs):
|
||||
@@ -212,12 +221,15 @@ def _peel_annotated(t: Any) -> Any:
|
||||
|
||||
def _check_annotation_definition( # pylint: disable=too-complex,too-many-return-statements
|
||||
_type,
|
||||
) -> bool:
|
||||
):
|
||||
|
||||
print(f"_type={_type}")
|
||||
_type = _peel_annotated(_type)
|
||||
_origin = get_origin(_type) or _type
|
||||
_args = get_args(_type)
|
||||
|
||||
# handle Optional[] and Union[None,...]
|
||||
if (get_origin(_type) is Union or get_origin(_type) is UnionType) and type(
|
||||
None
|
||||
) in get_args(_type):
|
||||
if (_origin is Union or _origin is UnionType) and type(None) in _args:
|
||||
return all(
|
||||
_check_annotation_definition(_)
|
||||
for _ in get_args(_type)
|
||||
@@ -225,43 +237,46 @@ def _check_annotation_definition( # pylint: disable=too-complex,too-many-return
|
||||
)
|
||||
|
||||
# handle other Union[...]
|
||||
if get_origin(_type) is Union or get_origin(_type) is UnionType:
|
||||
return all(_check_annotation_definition(_) for _ in get_args(_type))
|
||||
if _origin is Union or _origin is UnionType:
|
||||
return all(_check_annotation_definition(_) for _ in _args)
|
||||
|
||||
# handle Dict[...]
|
||||
if get_origin(_type) is dict:
|
||||
inner = get_args(_type)
|
||||
if len(inner) != 2:
|
||||
if _origin is dict:
|
||||
print("a")
|
||||
if len(_args) != 2:
|
||||
raise IncompletelyAnnotatedField(
|
||||
f"Dict Annotation requires 2 inner definitions: {_type}"
|
||||
)
|
||||
return _peel_annotated(
|
||||
inner[0]
|
||||
) in ALLOWED_MODEL_FIELDS_TYPES and _check_annotation_definition(inner[1])
|
||||
if not _peel_annotated(_args[0]) in ALLOWED_MODEL_FIELDS_TYPES:
|
||||
raise IncompletelyAnnotatedField(
|
||||
f"Dict Key must be simple builtin: {_type}"
|
||||
)
|
||||
return _check_annotation_definition(_args[1])
|
||||
|
||||
# handle Tuple[]
|
||||
if get_origin(_type) in [tuple]:
|
||||
inner_types = get_args(_type)
|
||||
if len(inner_types) == 0:
|
||||
if _origin is tuple:
|
||||
if len(_args) == 0:
|
||||
raise IncompletelyAnnotatedField(
|
||||
f"Annotation requires inner definition: {_type}"
|
||||
)
|
||||
if len(inner_types) == 2 and inner_types[1] is Ellipsis:
|
||||
return _check_annotation_definition(inner_types[0])
|
||||
return all(_check_annotation_definition(_) for _ in inner_types)
|
||||
if len(_args) == 2 and _args[1] is Ellipsis:
|
||||
return _check_annotation_definition(_args[0])
|
||||
return all(_check_annotation_definition(_) for _ in _args)
|
||||
|
||||
# handle Set[],Tuple[],FrozenSet[],List[]
|
||||
if get_origin(_type) in [set, frozenset, tuple, list]:
|
||||
inner_types = get_args(_type)
|
||||
if len(inner_types) == 0:
|
||||
if _origin in [set, frozenset, tuple, list]:
|
||||
if len(_args) == 0:
|
||||
raise IncompletelyAnnotatedField(
|
||||
f"Annotation requires inner definition: {_type}"
|
||||
)
|
||||
return all(_check_annotation_definition(_) for _ in inner_types)
|
||||
return all(_check_annotation_definition(_) for _ in _args)
|
||||
|
||||
if isinstance(_origin, type) and issubclass(_origin, IElement):
|
||||
return
|
||||
|
||||
if _type in ALLOWED_MODEL_FIELDS_TYPES:
|
||||
return True
|
||||
return False
|
||||
return
|
||||
raise UnsupportedFieldType(_type)
|
||||
|
||||
|
||||
class ModelSpecView:
|
||||
@@ -337,7 +352,7 @@ class _MetaElement(type):
|
||||
modified_fields: Dict[str, Any] = {}
|
||||
new_fields: Dict[str, LAMField[Any]] = {}
|
||||
initializer: Optional[Callable[..., Any]] = None
|
||||
__DABSchema__: dict[str, Any] = {}
|
||||
__LAMSchema__: dict[str, Any] = {}
|
||||
|
||||
@classmethod
|
||||
def check_class(
|
||||
@@ -362,16 +377,20 @@ class _MetaElement(type):
|
||||
raise MultipleInheritanceForbidden(
|
||||
"Multiple inheritance is not supported by dabmodel"
|
||||
)
|
||||
if len(bases) == 0: # base class (BaseElement)
|
||||
namespace["__DABSchema__"] = {}
|
||||
if len(bases) == 0: # base class (Appliance,Feature)
|
||||
namespace["__LAMSchema__"] = {}
|
||||
elif (
|
||||
len(bases) == 1 and bases[0] is IElement
|
||||
): # special case for Element (hidden layer IElement)
|
||||
namespace["__LAMSchema__"] = {}
|
||||
else: # standard inheritance
|
||||
# check class tree origin
|
||||
if "__DABSchema__" not in dir(bases[0]):
|
||||
if "__LAMSchema__" not in dir(bases[0]):
|
||||
raise BrokenInheritance(
|
||||
"__DABSchema__ not found in base class, broken inheritance chain."
|
||||
"__LAMSchema__ not found in base class, broken inheritance chain."
|
||||
)
|
||||
# copy inherited schema
|
||||
namespace["__DABSchema__"] = copy(bases[0].__DABSchema__)
|
||||
namespace["__LAMSchema__"] = copy(bases[0].__LAMSchema__)
|
||||
|
||||
# force field without default value to be instantiated (with None)
|
||||
if "__annotations__" in namespace:
|
||||
@@ -411,7 +430,9 @@ class _MetaElement(type):
|
||||
name.startswith("_") and _fname == "__initializer"
|
||||
):
|
||||
if not isinstance(_fvalue, classmethod):
|
||||
raise InvalidInitializerType()
|
||||
raise InvalidInitializerType(
|
||||
"__initializer should be a classmethod"
|
||||
)
|
||||
mcs.initializer = _fvalue.__func__
|
||||
if name.startswith("_"):
|
||||
initializer_name = "__initializer"
|
||||
@@ -424,7 +445,7 @@ class _MetaElement(type):
|
||||
else:
|
||||
print(f"Parsing Field: {_fname} / {_fvalue}")
|
||||
if (
|
||||
len(bases) == 1 and _fname in namespace["__DABSchema__"].keys()
|
||||
len(bases) == 1 and _fname in namespace["__LAMSchema__"].keys()
|
||||
): # Modified fields
|
||||
mcs.process_modified_field(
|
||||
name, bases, namespace, _fname, _fvalue, extensions
|
||||
@@ -464,7 +485,7 @@ class _MetaElement(type):
|
||||
try:
|
||||
check_type(
|
||||
_fvalue,
|
||||
namespace["__DABSchema__"][_fname].annotations,
|
||||
namespace["__LAMSchema__"][_fname].annotations,
|
||||
collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS,
|
||||
)
|
||||
except TypeCheckError as exp:
|
||||
@@ -490,7 +511,7 @@ class _MetaElement(type):
|
||||
payloads (allowing only LAMFieldInfo), checks the default value type,
|
||||
and stages the field as a `LAMField` in `mcs.new_fields`.
|
||||
"""
|
||||
# print(f"New field: {_fname}")
|
||||
print(f"New field: {_fname}")
|
||||
|
||||
# check if field is annotated
|
||||
if (
|
||||
@@ -507,9 +528,13 @@ class _MetaElement(type):
|
||||
namespace["__annotations__"][_fname]
|
||||
)
|
||||
|
||||
if not _check_annotation_definition(namespace["__annotations__"][_fname]):
|
||||
try:
|
||||
_check_annotation_definition(namespace["__annotations__"][_fname])
|
||||
except InvalidFieldAnnotation:
|
||||
raise
|
||||
except Exception as ex:
|
||||
raise InvalidFieldAnnotation(
|
||||
f"Field <{_fname}> has not an allowed or valid annotation."
|
||||
f"Field <{_fname}> has not an allowed or valid annotation.", ex
|
||||
)
|
||||
|
||||
_finfo: LAMFieldInfo = LAMFieldInfo()
|
||||
@@ -544,6 +569,7 @@ class _MetaElement(type):
|
||||
raise InvalidFieldValue(
|
||||
f"Value of Field <{_fname}> is not of expected type {namespace['__annotations__'][_fname]}."
|
||||
) from exp
|
||||
print(f"!!VAL: {_fvalue}")
|
||||
mcs.new_fields[_fname] = LAMField(
|
||||
_fname, _fvalue, namespace["__annotations__"][_fname], _finfo
|
||||
)
|
||||
@@ -569,7 +595,7 @@ class _MetaElement(type):
|
||||
_check_initializer_safety(mcs.initializer)
|
||||
init_fieldvalues = {}
|
||||
init_fieldtypes = {}
|
||||
for _fname, _fvalue in cls.__DABSchema__.items():
|
||||
for _fname, _fvalue in cls.__LAMSchema__.items():
|
||||
if isinstance(_fvalue, LAMField):
|
||||
init_fieldvalues[_fname] = deepcopy(_fvalue.raw_value)
|
||||
init_fieldtypes[_fname] = _fvalue.annotations
|
||||
@@ -580,28 +606,29 @@ class _MetaElement(type):
|
||||
"__builtins__": {"__import__": _blocked_import},
|
||||
**ALLOWED_HELPERS_DEFAULT,
|
||||
}
|
||||
if mcs.initializer.__code__.co_freevars:
|
||||
raise FunctionForbidden("__initializer must not use closures")
|
||||
# if mcs.initializer.__code__.co_freevars:
|
||||
# raise FunctionForbidden("__initializer must not use closures")
|
||||
safe_initializer = FunctionType(
|
||||
mcs.initializer.__code__,
|
||||
safe_globals,
|
||||
name=mcs.initializer.__name__,
|
||||
argdefs=mcs.initializer.__defaults__,
|
||||
closure=None,
|
||||
closure=mcs.initializer.__closure__,
|
||||
)
|
||||
safe_initializer(fakecls) # pylint: disable=not-callable
|
||||
for _fname, _fvalue in fakecls.export().items():
|
||||
try:
|
||||
check_type(
|
||||
_fvalue,
|
||||
cls.__DABSchema__[_fname].annotations,
|
||||
cls.__LAMSchema__[_fname].annotations,
|
||||
collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS,
|
||||
)
|
||||
except TypeCheckError as exp:
|
||||
raise InvalidFieldValue(
|
||||
f"Value of Field <{_fname}> is not of expected type {namespace['__annotations__'][_fname]}."
|
||||
) from exp
|
||||
cls.__DABSchema__[_fname].update_value(_fvalue)
|
||||
cls.__LAMSchema__[_fname] = deepcopy(cls.__LAMSchema__[_fname])
|
||||
cls.__LAMSchema__[_fname].update_value(_fvalue)
|
||||
|
||||
def __new__(
|
||||
mcs: type["_MetaElement"],
|
||||
@@ -632,18 +659,18 @@ class _MetaElement(type):
|
||||
extensions: dict[str, Any],
|
||||
):
|
||||
"""
|
||||
Commit staged fields into the class schema (`__DABSchema__`).
|
||||
Commit staged fields into the class schema (`__LAMSchema__`).
|
||||
|
||||
- For modified fields: copy the parent's LAMField, update its value.
|
||||
- For new fields: set the freshly built LAMField and record its source.
|
||||
"""
|
||||
for _fname, _fvalue in mcs.modified_fields.items():
|
||||
cls.__DABSchema__[_fname] = deepcopy(bases[0].__DABSchema__[_fname])
|
||||
cls.__DABSchema__[_fname].update_value(_fvalue)
|
||||
cls.__LAMSchema__[_fname] = deepcopy(bases[0].__LAMSchema__[_fname])
|
||||
cls.__LAMSchema__[_fname].update_value(_fvalue)
|
||||
|
||||
for _fname, _fvalue in mcs.new_fields.items():
|
||||
_fvalue.add_source(cls)
|
||||
cls.__DABSchema__[_fname] = _fvalue
|
||||
cls.__LAMSchema__[_fname] = _fvalue
|
||||
|
||||
def __call__(cls: Type, *args: Any, **kw: Any): # intentionally untyped
|
||||
"""BaseElement new instance"""
|
||||
@@ -672,7 +699,7 @@ class _MetaElement(type):
|
||||
|
||||
Copies each LAMField.value to an instance attribute (deep-frozen view).
|
||||
"""
|
||||
for _fname, _fvalue in cls.__DABSchema__.items():
|
||||
for _fname, _fvalue in cls.__LAMSchema__.items():
|
||||
if isinstance(_fvalue, LAMField):
|
||||
object.__setattr__(obj, _fname, _fvalue.value)
|
||||
|
||||
@@ -682,17 +709,17 @@ class _MetaElement(type):
|
||||
"""
|
||||
Freeze the instance's schema by wrapping DABFields into FrozenLAMField.
|
||||
|
||||
Creates a per-instance `__DABSchema__` dict where each field is read-only.
|
||||
Creates a per-instance `__LAMSchema__` dict where each field is read-only.
|
||||
"""
|
||||
inst_schema = copy(obj.__DABSchema__)
|
||||
for _fname, _fvalue in cls.__DABSchema__.items():
|
||||
inst_schema = copy(obj.__LAMSchema__)
|
||||
for _fname, _fvalue in cls.__LAMSchema__.items():
|
||||
if isinstance(_fvalue, LAMField):
|
||||
inst_schema[_fname] = FrozenLAMField(_fvalue)
|
||||
|
||||
if "features" in inst_schema:
|
||||
inst_schema["features"] = dict(inst_schema["features"])
|
||||
|
||||
object.__setattr__(obj, "__DABSchema__", inst_schema)
|
||||
object.__setattr__(obj, "__LAMSchema__", inst_schema)
|
||||
|
||||
def apply_overrides(cls, obj, extensions, *args, **kwargs):
|
||||
"""
|
||||
@@ -702,12 +729,33 @@ class _MetaElement(type):
|
||||
Subclasses of _MetaElement can override this to support things like:
|
||||
|
||||
- Field overrides: MyApp(field=value)
|
||||
- Feature overrides: MyApp(FeatureName=CustomFeature)
|
||||
- Feature attachments: MyApp(NewFeature=FeatureClass)
|
||||
|
||||
By default this does nothing.
|
||||
"""
|
||||
|
||||
# --- field overrides (unchanged) ---
|
||||
for k, v in list(kwargs.items()):
|
||||
if k in cls.__LAMSchema__: # regular field
|
||||
field = cls.__LAMSchema__[k]
|
||||
try:
|
||||
check_type(
|
||||
v,
|
||||
field.annotations,
|
||||
collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS,
|
||||
)
|
||||
except TypeCheckError as exp:
|
||||
raise InvalidFieldValue(
|
||||
f"Invalid value for field '{k}': expected {field.annotations}, got {v!r}"
|
||||
) from exp
|
||||
|
||||
object.__setattr__(obj, k, LAMdeepfreeze(v))
|
||||
obj.__LAMSchema__[k] = FrozenLAMField(
|
||||
LAMField(k, v, field.annotations, field._info)
|
||||
)
|
||||
kwargs.pop(k)
|
||||
|
||||
if kwargs:
|
||||
unknown = ", ".join(sorted(kwargs.keys()))
|
||||
raise InvalidFieldValue(f"Unknown parameters: {unknown}")
|
||||
|
||||
def finalize_instance(cls: Type, obj: Any, extensions: dict[str, Any]):
|
||||
"""
|
||||
Finalization hook invoked at the end of instance construction.
|
||||
@@ -735,10 +783,10 @@ class _MetaElement(type):
|
||||
if key.startswith("_"): # allow private and dunder attrs
|
||||
return orig_setattr(_self, key, value)
|
||||
# block writes after init if key is readonly
|
||||
if key in _self.__DABSchema__.keys():
|
||||
if key in _self.__LAMSchema__.keys():
|
||||
if key in _self.__dict__:
|
||||
raise ReadOnlyField(f"{key} is read-only")
|
||||
# elif key in _self.__DABSchema__["features"].keys():
|
||||
# elif key in _self.__LAMSchema__["features"].keys():
|
||||
# if key in _self.__dict__:
|
||||
# raise ReadOnlyField(f"{key} is read-only")
|
||||
else:
|
||||
@@ -1,5 +1,5 @@
|
||||
from typing import Type, Any
|
||||
from .base import _MetaElement
|
||||
from .element import _MetaElement
|
||||
from ..exception import FeatureNotBound
|
||||
|
||||
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -41,7 +41,7 @@ def test_initializer_safe_testfc():
|
||||
eval("print('hi')")
|
||||
|
||||
|
||||
class MainTests(unittest.TestCase):
|
||||
class ApplianceTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
print("\n->", unittest.TestCase.id(self))
|
||||
|
||||
@@ -53,7 +53,7 @@ class MainTests(unittest.TestCase):
|
||||
# field is in the object
|
||||
self.assertIn(name, dir(obj))
|
||||
# field is in the schema
|
||||
self.assertIn(name, obj.__DABSchema__.keys())
|
||||
self.assertIn(name, obj.__LAMSchema__.keys())
|
||||
# field is readable
|
||||
self.assertEqual(getattr(obj, name), default_value)
|
||||
# field is read only
|
||||
@@ -370,7 +370,6 @@ class MainTests(unittest.TestCase):
|
||||
with self.assertRaises(AttributeError):
|
||||
app1.testVar.add(3)
|
||||
|
||||
# /!\ THIS SHOULD RAISE AN EXCEPTION BUT IT DOESNT :-/
|
||||
with self.assertRaises(dm.InvalidFieldValue):
|
||||
print("youhou")
|
||||
|
||||
@@ -596,19 +595,19 @@ class MainTests(unittest.TestCase):
|
||||
expected_default_value: str,
|
||||
expected_type: type,
|
||||
):
|
||||
self.assertIn(field_name, appliance.__DABSchema__)
|
||||
self.assertIn("doc", dir(appliance.__DABSchema__[field_name]))
|
||||
self.assertEqual(appliance.__DABSchema__[field_name].doc, "")
|
||||
self.assertIn("annotations", dir(appliance.__DABSchema__[field_name]))
|
||||
self.assertEqual(appliance.__DABSchema__[field_name].annotations, expected_type)
|
||||
self.assertIn("value", dir(appliance.__DABSchema__[field_name]))
|
||||
self.assertEqual(appliance.__DABSchema__[field_name].value, expected_value)
|
||||
self.assertIn("default_value", dir(appliance.__DABSchema__[field_name]))
|
||||
self.assertIn(field_name, appliance.__LAMSchema__)
|
||||
self.assertIn("doc", dir(appliance.__LAMSchema__[field_name]))
|
||||
self.assertEqual(appliance.__LAMSchema__[field_name].doc, "")
|
||||
self.assertIn("annotations", dir(appliance.__LAMSchema__[field_name]))
|
||||
self.assertEqual(appliance.__LAMSchema__[field_name].annotations, expected_type)
|
||||
self.assertIn("value", dir(appliance.__LAMSchema__[field_name]))
|
||||
self.assertEqual(appliance.__LAMSchema__[field_name].value, expected_value)
|
||||
self.assertIn("default_value", dir(appliance.__LAMSchema__[field_name]))
|
||||
self.assertEqual(
|
||||
appliance.__DABSchema__[field_name].default_value, expected_default_value
|
||||
appliance.__LAMSchema__[field_name].default_value, expected_default_value
|
||||
)
|
||||
self.assertIn("constraints", dir(appliance.__DABSchema__[field_name]))
|
||||
self.assertEqual(appliance.__DABSchema__[field_name].constraints, ())
|
||||
self.assertIn("constraints", dir(appliance.__LAMSchema__[field_name]))
|
||||
self.assertEqual(appliance.__LAMSchema__[field_name].constraints, ())
|
||||
|
||||
def test_immutable_fields_schema(self):
|
||||
"""Testing first appliance level, and Field types (annotated)"""
|
||||
@@ -630,8 +629,8 @@ class MainTests(unittest.TestCase):
|
||||
|
||||
app1 = Appliance1()
|
||||
|
||||
self.assertIn("__DABSchema__", dir(app1))
|
||||
self.assertIn("__DABSchema__", app1.__dict__)
|
||||
self.assertIn("__LAMSchema__", dir(app1))
|
||||
self.assertIn("__LAMSchema__", app1.__dict__)
|
||||
|
||||
self.check_immutable_fields_schema(
|
||||
app1, "StrVar", "default value", "default value", str
|
||||
@@ -684,8 +683,8 @@ class MainTests(unittest.TestCase):
|
||||
|
||||
app1 = Appliance1()
|
||||
|
||||
self.assertIn("__DABSchema__", dir(app1))
|
||||
self.assertIn("__DABSchema__", app1.__dict__)
|
||||
self.assertIn("__LAMSchema__", dir(app1))
|
||||
self.assertIn("__LAMSchema__", app1.__dict__)
|
||||
|
||||
self.check_immutable_fields_schema(
|
||||
app1, "ListStr", ("val1", "val2"), ("val1", "val2"), list[str]
|
||||
@@ -742,8 +741,8 @@ class MainTests(unittest.TestCase):
|
||||
|
||||
app1 = Appliance1()
|
||||
|
||||
self.assertIn("__DABSchema__", dir(app1))
|
||||
self.assertIn("__DABSchema__", app1.__dict__)
|
||||
self.assertIn("__LAMSchema__", dir(app1))
|
||||
self.assertIn("__LAMSchema__", app1.__dict__)
|
||||
|
||||
self.check_immutable_fields_schema(
|
||||
app1, "ListStr", ("val1", "val2"), ("val1", "val2"), List[str]
|
||||
@@ -809,40 +808,40 @@ class MainTests(unittest.TestCase):
|
||||
app1 = Appliance1()
|
||||
|
||||
self.immutable_vars__test_field(app1, "StrVar", "default value", "test")
|
||||
self.assertEqual(app1.__DABSchema__["StrVar"].doc, "foo1")
|
||||
self.assertEqual(app1.__LAMSchema__["StrVar"].doc, "foo1")
|
||||
self.immutable_vars__test_field(app1, "StrVar2", "default value2", "test2")
|
||||
self.assertEqual(app1.__DABSchema__["StrVar2"].doc, "foo2")
|
||||
self.assertEqual(app1.__LAMSchema__["StrVar2"].doc, "foo2")
|
||||
self.immutable_vars__test_field(app1, "VarInt", 12, 13)
|
||||
self.assertEqual(app1.__DABSchema__["VarInt"].doc, "foo3")
|
||||
self.assertEqual(app1.__LAMSchema__["VarInt"].doc, "foo3")
|
||||
self.immutable_vars__test_field(app1, "VarInt2", 21, 22)
|
||||
self.assertEqual(app1.__DABSchema__["VarInt2"].doc, "foo4")
|
||||
self.assertEqual(app1.__LAMSchema__["VarInt2"].doc, "foo4")
|
||||
self.immutable_vars__test_field(app1, "VarFloat", 12.1, 32)
|
||||
self.assertEqual(app1.__DABSchema__["VarFloat"].doc, "foo5")
|
||||
self.assertEqual(app1.__LAMSchema__["VarFloat"].doc, "foo5")
|
||||
self.immutable_vars__test_field(app1, "VarFloat2", 21.2, 42)
|
||||
self.assertEqual(app1.__DABSchema__["VarFloat2"].doc, "foo6")
|
||||
self.assertEqual(app1.__LAMSchema__["VarFloat2"].doc, "foo6")
|
||||
self.immutable_vars__test_field(
|
||||
app1, "VarComplex", complex(3, 5), complex(1, 2)
|
||||
)
|
||||
self.assertEqual(app1.__DABSchema__["VarComplex"].doc, "foo7")
|
||||
self.assertEqual(app1.__LAMSchema__["VarComplex"].doc, "foo7")
|
||||
self.immutable_vars__test_field(
|
||||
app1, "VarComplex2", complex(8, 6), complex(3, 2)
|
||||
)
|
||||
self.assertEqual(app1.__DABSchema__["VarComplex2"].doc, "foo8")
|
||||
self.assertEqual(app1.__LAMSchema__["VarComplex2"].doc, "foo8")
|
||||
self.immutable_vars__test_field(app1, "VarBool", True, False)
|
||||
self.assertEqual(app1.__DABSchema__["VarBool"].doc, "foo9")
|
||||
self.assertEqual(app1.__LAMSchema__["VarBool"].doc, "foo9")
|
||||
self.immutable_vars__test_field(app1, "VarBool2", False, True)
|
||||
self.assertEqual(app1.__DABSchema__["VarBool2"].doc, "foo10")
|
||||
self.assertEqual(app1.__LAMSchema__["VarBool2"].doc, "foo10")
|
||||
self.immutable_vars__test_field(
|
||||
app1, "VarBytes", bytes.fromhex("2Ef0 F1f2 "), bytes.fromhex("11f0 F1f2 ")
|
||||
)
|
||||
self.assertEqual(app1.__DABSchema__["VarBytes"].doc, "foo11")
|
||||
self.assertEqual(app1.__LAMSchema__["VarBytes"].doc, "foo11")
|
||||
self.immutable_vars__test_field(
|
||||
app1,
|
||||
"VarBytes2",
|
||||
bytes.fromhex("2ff0 F7f2 "),
|
||||
bytes.fromhex("11f0 F1e2 "),
|
||||
)
|
||||
self.assertEqual(app1.__DABSchema__["VarBytes2"].doc, "foo12")
|
||||
self.assertEqual(app1.__LAMSchema__["VarBytes2"].doc, "foo12")
|
||||
|
||||
with self.assertRaises(dm.InvalidFieldAnnotation):
|
||||
|
||||
@@ -1008,10 +1007,10 @@ class MainTests(unittest.TestCase):
|
||||
class Appliance3(Appliance2):
|
||||
NewValue: str = "newval"
|
||||
|
||||
self.assertNotIn("NewValue", Appliance2.__DABSchema__)
|
||||
self.assertNotIn("NewValue", app2.__DABSchema__)
|
||||
self.assertNotIn("NewValue", Appliance1.__DABSchema__)
|
||||
self.assertNotIn("NewValue", app1.__DABSchema__)
|
||||
self.assertNotIn("NewValue", Appliance2.__LAMSchema__)
|
||||
self.assertNotIn("NewValue", app2.__LAMSchema__)
|
||||
self.assertNotIn("NewValue", Appliance1.__LAMSchema__)
|
||||
self.assertNotIn("NewValue", app1.__LAMSchema__)
|
||||
|
||||
app3 = Appliance3()
|
||||
self.immutable_vars__test_field(app3, "StrVar", "moded value", "test")
|
||||
@@ -1510,6 +1509,23 @@ class MainTests(unittest.TestCase):
|
||||
app2 = _()
|
||||
self.assertEqual(app2.VarInt, 42)
|
||||
|
||||
def test_initializer_dont_leak(self):
|
||||
|
||||
class A(dm.Appliance):
|
||||
VarInt: int = 12
|
||||
|
||||
class B(A):
|
||||
|
||||
@classmethod
|
||||
def __initializer(cls):
|
||||
cls.VarInt = cls.VarInt + 1
|
||||
|
||||
a = A()
|
||||
b = B()
|
||||
|
||||
self.assertEqual(a.VarInt, 12)
|
||||
self.assertEqual(b.VarInt, 13)
|
||||
|
||||
def test_initializer_safe(self):
|
||||
|
||||
with self.assertRaises(dm.ImportForbidden):
|
||||
@@ -1566,10 +1582,10 @@ class MainTests(unittest.TestCase):
|
||||
def __initializer(cls):
|
||||
compile("print(55)", "test", "eval")
|
||||
|
||||
with self.assertRaises(dm.FunctionForbidden):
|
||||
def testfc():
|
||||
eval("print('test')")
|
||||
|
||||
def testfc():
|
||||
eval("print('test')")
|
||||
with self.assertRaises(dm.FunctionForbidden):
|
||||
|
||||
class _(dm.Appliance):
|
||||
_: int = 0
|
||||
@@ -1578,6 +1594,16 @@ class MainTests(unittest.TestCase):
|
||||
def __initializer(cls):
|
||||
testfc()
|
||||
|
||||
with self.assertRaises(dm.FunctionForbidden):
|
||||
|
||||
class _(dm.Appliance):
|
||||
_: int = 0
|
||||
|
||||
@classmethod
|
||||
def __initializer(cls):
|
||||
if True:
|
||||
testfc()
|
||||
|
||||
# class can be created
|
||||
class Appliance2(dm.Appliance):
|
||||
VarInt: int = -56
|
||||
@@ -1626,144 +1652,6 @@ class MainTests(unittest.TestCase):
|
||||
def __initializer(cls):
|
||||
test_initializer_safe_testfc()
|
||||
|
||||
def test_feature(self):
|
||||
"""Testing first appliance feature, and Field types (simple)"""
|
||||
|
||||
# class can be created
|
||||
class Appliance1(dm.Appliance):
|
||||
VarStrOuter: str = "testvalue APPLIANCE"
|
||||
|
||||
class Feature1(dm.Feature):
|
||||
VarStrInner: str = "testvalue FEATURE"
|
||||
|
||||
app1 = Appliance1()
|
||||
|
||||
self.assertIsInstance(Appliance1.__DABSchema__["VarStrOuter"], dm.LAMField)
|
||||
self.assertIsInstance(app1.__DABSchema__["VarStrOuter"], dm.FrozenLAMField)
|
||||
self.assertIn("Feature1", app1.__DABSchema__["features"])
|
||||
self.assertIn(
|
||||
"VarStrInner", app1.__DABSchema__["features"]["Feature1"].__DABSchema__
|
||||
)
|
||||
self.assertIsInstance(
|
||||
app1.__DABSchema__["features"]["Feature1"].__DABSchema__["VarStrInner"],
|
||||
dm.LAMField,
|
||||
)
|
||||
self.assertTrue(hasattr(app1, "Feature1"))
|
||||
self.assertIsInstance(
|
||||
app1.Feature1.__DABSchema__["VarStrInner"], dm.FrozenLAMField
|
||||
)
|
||||
self.assertTrue(hasattr(app1.Feature1, "VarStrInner"))
|
||||
|
||||
def test_feature_inheritance(self):
|
||||
"""Testing first appliance feature, and Field types (simple)"""
|
||||
|
||||
# class can be created
|
||||
class Appliance1(dm.Appliance):
|
||||
VarStrOuter: str = "testvalue APPLIANCE1"
|
||||
|
||||
class Feature1(dm.Feature):
|
||||
VarStrInner: str = "testvalue FEATURE1"
|
||||
VarInt: int = 42
|
||||
|
||||
print(dir(Appliance1))
|
||||
|
||||
class Appliance2(Appliance1):
|
||||
VarStrOuter = "testvalue APPLIANCE2"
|
||||
|
||||
class Feature2(dm.Feature):
|
||||
VarStrInner: str = "testvalue FEATURE2"
|
||||
|
||||
print(dir(Appliance2))
|
||||
|
||||
class Appliance3(Appliance2):
|
||||
VarStrOuter = "testvalue APPLIANCE3"
|
||||
|
||||
class Feature1(Appliance1.Feature1):
|
||||
VarStrInner = "testvalue FEATURE1 modded"
|
||||
|
||||
class Feature3(dm.Feature):
|
||||
VarStrInner: str = "testvalue FEATURE3"
|
||||
|
||||
print(dir(Appliance3))
|
||||
|
||||
app1 = Appliance1()
|
||||
app2 = Appliance2()
|
||||
app3 = Appliance3()
|
||||
|
||||
self.assertIsInstance(Appliance1.__DABSchema__["VarStrOuter"], dm.LAMField)
|
||||
self.assertIsInstance(app1.__DABSchema__["VarStrOuter"], dm.FrozenLAMField)
|
||||
self.assertIn("Feature1", app1.__DABSchema__["features"])
|
||||
self.assertIn(
|
||||
"VarStrInner", app1.__DABSchema__["features"]["Feature1"].__DABSchema__
|
||||
)
|
||||
self.assertIsInstance(
|
||||
app1.__DABSchema__["features"]["Feature1"].__DABSchema__["VarStrInner"],
|
||||
dm.LAMField,
|
||||
)
|
||||
self.assertTrue(hasattr(app1, "Feature1"))
|
||||
self.assertIsInstance(
|
||||
app1.Feature1.__DABSchema__["VarStrInner"], dm.FrozenLAMField
|
||||
)
|
||||
self.assertTrue(hasattr(app1.Feature1, "VarStrInner"))
|
||||
self.assertEqual(app1.VarStrOuter, "testvalue APPLIANCE1")
|
||||
self.assertEqual(app1.Feature1.VarStrInner, "testvalue FEATURE1")
|
||||
self.assertEqual(app1.Feature1.VarInt, 42)
|
||||
self.assertEqual(app2.VarStrOuter, "testvalue APPLIANCE2")
|
||||
self.assertEqual(app2.Feature2.VarStrInner, "testvalue FEATURE2")
|
||||
self.assertEqual(app3.VarStrOuter, "testvalue APPLIANCE3")
|
||||
self.assertEqual(app3.Feature1.VarStrInner, "testvalue FEATURE1 modded")
|
||||
self.assertEqual(app3.Feature1.VarInt, 42)
|
||||
self.assertEqual(app3.Feature3.VarStrInner, "testvalue FEATURE3")
|
||||
|
||||
def test_feature_inheritance2(self):
|
||||
"""Testing first appliance feature, and Field types (simple)"""
|
||||
|
||||
# class can be created
|
||||
class Appliance1(dm.Appliance):
|
||||
class Feature1(dm.Feature):
|
||||
VarStrInner: str = "testvalue FEATURE1"
|
||||
|
||||
# check cannot REdefine a feature from Feature
|
||||
with self.assertRaises(dm.InvalidFeatureInheritance):
|
||||
|
||||
class Appliance2(Appliance1):
|
||||
class Feature1(dm.Feature): ...
|
||||
|
||||
class Appliance2b(Appliance1):
|
||||
class Feature1(Appliance1.Feature1): ...
|
||||
|
||||
# check only REdefine a feature from highest parent
|
||||
with self.assertRaises(dm.InvalidFeatureInheritance):
|
||||
|
||||
class Appliance3(Appliance2b):
|
||||
class Feature1(Appliance1.Feature1): ...
|
||||
|
||||
class Appliance3b(Appliance2b):
|
||||
class Feature1(Appliance2b.Feature1): ...
|
||||
|
||||
app1 = Appliance1()
|
||||
app2 = Appliance2b()
|
||||
app3 = Appliance3b()
|
||||
|
||||
self.assertEqual(app1.Feature1.VarStrInner, "testvalue FEATURE1")
|
||||
self.assertEqual(app2.Feature1.VarStrInner, "testvalue FEATURE1")
|
||||
self.assertEqual(app3.Feature1.VarStrInner, "testvalue FEATURE1")
|
||||
|
||||
class Appliance4(Appliance3b):
|
||||
class Feature1(Appliance3b.Feature1):
|
||||
VarStrInner = "testvalue FEATURE4"
|
||||
|
||||
self.assertEqual(app1.Feature1.VarStrInner, "testvalue FEATURE1")
|
||||
self.assertEqual(app2.Feature1.VarStrInner, "testvalue FEATURE1")
|
||||
self.assertEqual(app3.Feature1.VarStrInner, "testvalue FEATURE1")
|
||||
|
||||
app4 = Appliance4()
|
||||
|
||||
self.assertEqual(app1.Feature1.VarStrInner, "testvalue FEATURE1")
|
||||
self.assertEqual(app2.Feature1.VarStrInner, "testvalue FEATURE1")
|
||||
self.assertEqual(app3.Feature1.VarStrInner, "testvalue FEATURE1")
|
||||
self.assertEqual(app4.Feature1.VarStrInner, "testvalue FEATURE4")
|
||||
|
||||
def test_inheritance_chain(self):
|
||||
|
||||
# class can be created
|
||||
@@ -1802,49 +1690,6 @@ class MainTests(unittest.TestCase):
|
||||
self.assertEqual(app2b.VarStr, "testvalue1")
|
||||
self.assertEqual(app3b.VarStr, "testvalue1")
|
||||
|
||||
def test_feature_register(self):
|
||||
"""Testing first appliance feature, and Field types (simple)"""
|
||||
|
||||
# class can be created
|
||||
class Appliance1(dm.Appliance):
|
||||
pass
|
||||
|
||||
class Feature1(dm.Feature):
|
||||
_BoundAppliance = Appliance1
|
||||
VarStrInner: str = "testvalue FEATURE1"
|
||||
|
||||
app = Appliance1(feat1=Feature1)
|
||||
self.assertEqual(app.feat1.VarStrInner, "testvalue FEATURE1")
|
||||
|
||||
# check it does not leak accross instances
|
||||
|
||||
app = Appliance1(feat2=Feature1)
|
||||
self.assertEqual(app.feat2.VarStrInner, "testvalue FEATURE1")
|
||||
|
||||
with self.assertRaises(AttributeError):
|
||||
app.feat1
|
||||
|
||||
def test_feature_register_notbound(self):
|
||||
"""Testing first appliance feature, and Field types (simple)"""
|
||||
|
||||
# class can be created
|
||||
class Appliance1(dm.Appliance):
|
||||
pass
|
||||
|
||||
class Feature1(dm.Feature):
|
||||
VarStrInner: str = "testvalue FEATURE1"
|
||||
|
||||
with self.assertRaises(dm.FeatureNotBound):
|
||||
app = Appliance1(feat1=Feature1)
|
||||
|
||||
def test_feature_register_defect(self):
|
||||
|
||||
class Feature1(dm.Feature):
|
||||
pass
|
||||
|
||||
with self.assertRaises(dm.FeatureNotBound):
|
||||
feat1 = Feature1()
|
||||
|
||||
def test_override(self):
|
||||
"""Testing first appliance level, and Field types (List)"""
|
||||
|
||||
@@ -1891,72 +1736,6 @@ class MainTests(unittest.TestCase):
|
||||
app._internal = 42 # should be allowed
|
||||
self.assertEqual(app._internal, 42)
|
||||
|
||||
def test_inherit_declared_feature(self):
|
||||
class App(dm.Appliance):
|
||||
class F1(dm.Feature):
|
||||
val: int = 1
|
||||
|
||||
class MyF1(App.F1):
|
||||
val = 2
|
||||
val2: str = "toto"
|
||||
|
||||
app = App(F1=MyF1)
|
||||
self.assertIsInstance(app.F1, MyF1)
|
||||
self.assertEqual(app.F1.val, 2)
|
||||
self.assertEqual(app.F1.val2, "toto")
|
||||
|
||||
def test_override_declared_feature(self):
|
||||
class App(dm.Appliance):
|
||||
class F1(dm.Feature):
|
||||
val: int = 1
|
||||
val2: str = "toto"
|
||||
|
||||
app = App(F1={"val": 42, "val2": "tata"})
|
||||
self.assertEqual(app.F1.val, 42)
|
||||
self.assertEqual(app.F1.val2, "tata")
|
||||
|
||||
def test_feature_dict_override_type_error(self):
|
||||
class App(dm.Appliance):
|
||||
class F1(dm.Feature):
|
||||
val: int = 1
|
||||
|
||||
# wrong type for val → must raise InvalidFieldValue
|
||||
with self.assertRaises(dm.InvalidFieldValue):
|
||||
App(F1={"val": "not-an-int"})
|
||||
|
||||
def test_feature_dict_override_nonexisting_field(self):
|
||||
class App(dm.Appliance):
|
||||
class F1(dm.Feature):
|
||||
val: int = 1
|
||||
|
||||
# field does not exist → must raise
|
||||
with self.assertRaises(dm.InvalidFieldValue):
|
||||
App(F1={"doesnotexist": 123})
|
||||
|
||||
def test_feature_inheritance_with_extra_fields(self):
|
||||
class App(dm.Appliance):
|
||||
class F1(dm.Feature):
|
||||
val: int = 1
|
||||
|
||||
class MyF1(App.F1):
|
||||
val = 2
|
||||
extra: str = "hello"
|
||||
|
||||
app = App(F1=MyF1)
|
||||
self.assertEqual(app.F1.val, 2)
|
||||
self.assertEqual(app.F1.extra, "hello")
|
||||
|
||||
def test_feature_not_bound_runtime_attach_fails(self):
|
||||
class App(dm.Appliance):
|
||||
pass
|
||||
|
||||
class UnboundFeature(dm.Feature):
|
||||
x: int = 1
|
||||
|
||||
# attaching an unbound feature should raise
|
||||
with self.assertRaises(dm.FeatureNotBound):
|
||||
App(Unbound=UnboundFeature)
|
||||
|
||||
def test_field_cannot_be_reassigned(self):
|
||||
class App(dm.Appliance):
|
||||
x: int = 1
|
||||
@@ -1965,16 +1744,6 @@ class MainTests(unittest.TestCase):
|
||||
with self.assertRaises(dm.ReadOnlyField):
|
||||
app.x = 99
|
||||
|
||||
def test_feature_override_does_not_leak_between_instances(self):
|
||||
class App(dm.Appliance):
|
||||
class F1(dm.Feature):
|
||||
val: int = 1
|
||||
|
||||
app1 = App(F1={"val": 99})
|
||||
app2 = App()
|
||||
self.assertEqual(app1.F1.val, 99)
|
||||
self.assertEqual(app2.F1.val, 1)
|
||||
|
||||
def test_schema_fields_are_frozen(self):
|
||||
class App(dm.Appliance):
|
||||
x: list[int] = [1, 2]
|
||||
@@ -2023,16 +1792,29 @@ class MainTests(unittest.TestCase):
|
||||
|
||||
cls.x = inner()
|
||||
|
||||
def test_initializer_lambda_forbidden(self):
|
||||
with self.assertRaises(dm.FunctionForbidden):
|
||||
|
||||
class App(dm.Appliance):
|
||||
class _(dm.Appliance):
|
||||
x: int = 1
|
||||
|
||||
@classmethod
|
||||
def __initializer(cls):
|
||||
f = lambda: 42 # forbidden
|
||||
cls.x = f()
|
||||
y = 2
|
||||
|
||||
def inner():
|
||||
return y + 1
|
||||
|
||||
cls.x = lambda: inner()
|
||||
|
||||
def test_initializer_lambda_forbidden(self):
|
||||
|
||||
class App(dm.Appliance):
|
||||
x: int = 1
|
||||
|
||||
@classmethod
|
||||
def __initializer(cls):
|
||||
f = lambda: 42
|
||||
cls.x = f()
|
||||
|
||||
def test_multiple_inheritance_forbidden(self):
|
||||
with self.assertRaises(dm.MultipleInheritanceForbidden):
|
||||
@@ -2197,61 +1979,6 @@ class MainTests(unittest.TestCase):
|
||||
with self.assertRaises(dm.InvalidFieldValue):
|
||||
App(a=["oops"]) # wrong inner type
|
||||
|
||||
def test_runtime_attach_bound_feature_success(self):
|
||||
class App(dm.Appliance):
|
||||
class F1(dm.Feature):
|
||||
val: int = 1
|
||||
|
||||
class Extra(App.F1): # stays bound to App
|
||||
val = 7
|
||||
|
||||
app = App(Extra=Extra)
|
||||
self.assertTrue(hasattr(app, "Extra"))
|
||||
self.assertIsInstance(app.Extra, Extra)
|
||||
self.assertEqual(app.Extra.val, 7)
|
||||
|
||||
def test_cant_override_inherited_feature_annotation(self):
|
||||
class App(dm.Appliance):
|
||||
class F1(dm.Feature):
|
||||
val: int = 1
|
||||
|
||||
with self.assertRaises(dm.ReadOnlyFieldAnnotation):
|
||||
|
||||
class Extra(App.F1):
|
||||
val: str = "test"
|
||||
|
||||
def test_feature_fields_are_frozen_after_override(self):
|
||||
class App(dm.Appliance):
|
||||
class F(dm.Feature):
|
||||
nums: list[int] = [1, 2]
|
||||
tag: str = "x"
|
||||
|
||||
# dict override
|
||||
app1 = App(F={"nums": [9], "tag": "y"})
|
||||
self.assertEqual(app1.F.nums, (9,))
|
||||
self.assertEqual(app1.F.tag, "y")
|
||||
with self.assertRaises(AttributeError):
|
||||
app1.F.nums.append(3) # tuple
|
||||
|
||||
# subclass override
|
||||
class F2(App.F):
|
||||
nums = [4, 5]
|
||||
|
||||
app2 = App(F=F2)
|
||||
self.assertEqual(app2.F.nums, (4, 5))
|
||||
with self.assertRaises(dm.ReadOnlyField):
|
||||
app2.F.nums += (6,) # still immutable
|
||||
|
||||
def test_feature_dict_partial_override_keeps_other_defaults(self):
|
||||
class App(dm.Appliance):
|
||||
class F(dm.Feature):
|
||||
a: int = 1
|
||||
b: str = "k"
|
||||
|
||||
app = App(F={"b": "z"})
|
||||
self.assertEqual(app.F.a, 1) # default remains
|
||||
self.assertEqual(app.F.b, "z") # overridden
|
||||
|
||||
def test_root_field_override_nonexisting_rejected(self):
|
||||
class App(dm.Appliance):
|
||||
x: int = 1
|
||||
@@ -2259,227 +1986,6 @@ class MainTests(unittest.TestCase):
|
||||
with self.assertRaises(dm.InvalidFieldValue):
|
||||
App(y=2) # not in schema -> unknown parameter
|
||||
|
||||
def test_feature_override_linear_chain(self):
|
||||
# Base appliance defines Feat1
|
||||
class A(dm.Appliance):
|
||||
class Feat1(dm.Feature):
|
||||
x: int = 1
|
||||
|
||||
# ✅ Appliance B overrides Feat1 by subclassing A.Feat1
|
||||
class B(A):
|
||||
class Feat1(A.Feat1):
|
||||
y: int = 2
|
||||
|
||||
self.assertTrue(issubclass(B.Feat1, A.Feat1))
|
||||
|
||||
# ✅ Appliance C overrides Feat1 again by subclassing B.Feat1 (not A.Feat1)
|
||||
class C(B):
|
||||
class Feat1(B.Feat1):
|
||||
z: int = 3
|
||||
|
||||
self.assertTrue(issubclass(C.Feat1, B.Feat1))
|
||||
self.assertTrue(issubclass(C.Feat1, A.Feat1))
|
||||
|
||||
# ❌ Bad: D tries to override with a *fresh* Feature, not subclass of B.Feat1
|
||||
with self.assertRaises(dm.InvalidFeatureInheritance):
|
||||
|
||||
class D(B):
|
||||
class Feat1(dm.Feature):
|
||||
fail: str = "oops"
|
||||
|
||||
# ❌ Bad: E tries to override with ancestor (A.Feat1) instead of B.Feat1
|
||||
with self.assertRaises(dm.InvalidFeatureInheritance):
|
||||
|
||||
class E(B):
|
||||
class Feat1(A.Feat1):
|
||||
fail: str = "oops"
|
||||
|
||||
# ✅ New feature name in child is always fine
|
||||
class F(B):
|
||||
class Feat2(dm.Feature):
|
||||
other: str = "ok"
|
||||
|
||||
self.assertTrue(hasattr(F, "Feat2"))
|
||||
|
||||
def test_feature_override_chain_runtime_replacement(self):
|
||||
# Build a linear chain: A -> B -> C for feature 'Feat1'
|
||||
class A(dm.Appliance):
|
||||
class Feat1(dm.Feature):
|
||||
x: int = 1
|
||||
|
||||
class B(A):
|
||||
class Feat1(A.Feat1):
|
||||
y: int = 2
|
||||
|
||||
class C(B):
|
||||
class Feat1(B.Feat1):
|
||||
z: int = 3
|
||||
|
||||
# ✅ OK: at instantiation of C, replacing Feat1 with a subclass of the LATEST (C.Feat1)
|
||||
class CFeat1Plus(C.Feat1):
|
||||
w: int = 4
|
||||
|
||||
c_ok = C(Feat1=CFeat1Plus)
|
||||
self.assertIsInstance(c_ok.Feat1, CFeat1Plus)
|
||||
self.assertEqual(
|
||||
(c_ok.Feat1.x, c_ok.Feat1.y, c_ok.Feat1.z, c_ok.Feat1.w), (1, 2, 3, 4)
|
||||
)
|
||||
|
||||
# ❌ Not OK: replacing with a subclass of the ancestor (A.Feat1) — must target latest (C.Feat1)
|
||||
class AFeat1Alt(A.Feat1):
|
||||
pass
|
||||
|
||||
with self.assertRaises(dm.InvalidFieldValue):
|
||||
C(Feat1=AFeat1Alt)
|
||||
|
||||
# ❌ Not OK: replacing with a subclass of the mid ancestor (B.Feat1) — still must target latest (C.Feat1)
|
||||
class BFeat1Alt(B.Feat1):
|
||||
pass
|
||||
|
||||
with self.assertRaises(dm.InvalidFieldValue):
|
||||
C(Feat1=BFeat1Alt)
|
||||
|
||||
def test_feature_inheritance_tree_and_no_leakage(self):
|
||||
class A(dm.Appliance):
|
||||
class F1(dm.Feature):
|
||||
a: int = 1
|
||||
|
||||
class F2(dm.Feature):
|
||||
b: int = 2
|
||||
|
||||
# ✅ Child inherits both features automatically
|
||||
class B(A):
|
||||
c: str = "extra"
|
||||
|
||||
b1 = B()
|
||||
self.assertIsInstance(b1.F1, A.F1)
|
||||
self.assertIsInstance(b1.F2, A.F2)
|
||||
self.assertEqual((b1.F1.a, b1.F2.b, b1.c), (1, 2, "extra"))
|
||||
|
||||
# ✅ Override only F2, F1 should still come from A
|
||||
class C(B):
|
||||
class F2(B.F2):
|
||||
bb: int = 22
|
||||
|
||||
c1 = C()
|
||||
self.assertIsInstance(c1.F1, A.F1) # unchanged
|
||||
self.assertIsInstance(c1.F2, C.F2) # overridden
|
||||
self.assertEqual((c1.F1.a, c1.F2.b, c1.F2.bb), (1, 2, 22))
|
||||
|
||||
# ✅ No leakage: instances of B are not affected by C's override
|
||||
b2 = B()
|
||||
self.assertIsInstance(b2.F2, A.F2)
|
||||
self.assertFalse(hasattr(b2.F2, "bb"))
|
||||
|
||||
# ✅ Adding a new feature in D is independent of previous appliances
|
||||
class D(C):
|
||||
class F3(dm.Feature):
|
||||
d: int = 3
|
||||
|
||||
d1 = D()
|
||||
self.assertIsInstance(d1.F1, A.F1)
|
||||
self.assertIsInstance(d1.F2, C.F2)
|
||||
self.assertIsInstance(d1.F3, D.F3)
|
||||
|
||||
# ✅ No leakage: instances of A and B should not see F3
|
||||
a1 = A()
|
||||
self.assertFalse(hasattr(a1, "F3"))
|
||||
b3 = B()
|
||||
self.assertFalse(hasattr(b3, "F3"))
|
||||
|
||||
def test_appliance_inheritance_tree_feature_isolation(self):
|
||||
class A(dm.Appliance):
|
||||
class F1(dm.Feature):
|
||||
a: int = 1
|
||||
|
||||
# Branch 1 overrides F1
|
||||
class B(A):
|
||||
class F1(A.F1):
|
||||
b: int = 2
|
||||
|
||||
# Branch 2 also overrides F1 differently
|
||||
class C(A):
|
||||
class F1(A.F1):
|
||||
c: int = 3
|
||||
|
||||
# ✅ Instances of B use B.F1
|
||||
b = B()
|
||||
self.assertIsInstance(b.F1, B.F1)
|
||||
self.assertEqual((b.F1.a, b.F1.b), (1, 2))
|
||||
self.assertFalse(hasattr(b.F1, "c"))
|
||||
|
||||
# ✅ Instances of C use C.F1
|
||||
c = C()
|
||||
self.assertIsInstance(c.F1, C.F1)
|
||||
self.assertEqual((c.F1.a, c.F1.c), (1, 3))
|
||||
self.assertFalse(hasattr(c.F1, "b"))
|
||||
|
||||
# ✅ Base appliance A still uses its original feature
|
||||
a = A()
|
||||
self.assertIsInstance(a.F1, A.F1)
|
||||
self.assertEqual(a.F1.a, 1)
|
||||
self.assertFalse(hasattr(a.F1, "b"))
|
||||
self.assertFalse(hasattr(a.F1, "c"))
|
||||
|
||||
# ✅ No leakage: B's override doesn't affect C and vice versa
|
||||
b2 = B()
|
||||
c2 = C()
|
||||
self.assertTrue(hasattr(b2.F1, "b"))
|
||||
self.assertFalse(hasattr(b2.F1, "c"))
|
||||
self.assertTrue(hasattr(c2.F1, "c"))
|
||||
self.assertFalse(hasattr(c2.F1, "b"))
|
||||
|
||||
def test_appliance_inheritance_tree_runtime_attach_isolation(self):
|
||||
class A(dm.Appliance):
|
||||
class F1(dm.Feature):
|
||||
a: int = 1
|
||||
|
||||
class B(A):
|
||||
class F1(A.F1):
|
||||
b: int = 2
|
||||
|
||||
class C(A):
|
||||
class F1(A.F1):
|
||||
c: int = 3
|
||||
|
||||
# Define new runtime-attachable features
|
||||
class FextraB(B.F1):
|
||||
xb: int = 99
|
||||
|
||||
class FextraC(C.F1):
|
||||
xc: int = -99
|
||||
|
||||
# ✅ Attach to B at instantiation
|
||||
b = B(F1=FextraB)
|
||||
self.assertIsInstance(b.F1, FextraB)
|
||||
self.assertEqual((b.F1.a, b.F1.b, b.F1.xb), (1, 2, 99))
|
||||
self.assertFalse(hasattr(b.F1, "c"))
|
||||
self.assertFalse(hasattr(b.F1, "xc"))
|
||||
|
||||
# ✅ Attach to C at instantiation
|
||||
c = C(F1=FextraC)
|
||||
self.assertIsInstance(c.F1, FextraC)
|
||||
self.assertEqual((c.F1.a, c.F1.c, c.F1.xc), (1, 3, -99))
|
||||
self.assertFalse(hasattr(c.F1, "b"))
|
||||
self.assertFalse(hasattr(c.F1, "xb"))
|
||||
|
||||
# ✅ Base appliance still untouched
|
||||
a = A()
|
||||
self.assertIsInstance(a.F1, A.F1)
|
||||
self.assertEqual(a.F1.a, 1)
|
||||
self.assertFalse(hasattr(a.F1, "b"))
|
||||
self.assertFalse(hasattr(a.F1, "c"))
|
||||
self.assertFalse(hasattr(a.F1, "xb"))
|
||||
self.assertFalse(hasattr(a.F1, "xc"))
|
||||
|
||||
# ✅ Repeated instantiations stay isolated
|
||||
b2 = B()
|
||||
c2 = C()
|
||||
self.assertIsInstance(b2.F1, B.F1)
|
||||
self.assertIsInstance(c2.F1, C.F1)
|
||||
self.assertFalse(hasattr(b2.F1, "xb"))
|
||||
self.assertFalse(hasattr(c2.F1, "xc"))
|
||||
|
||||
def test_dict_field_override_with_nested_containers(self):
|
||||
class App(dm.Appliance):
|
||||
data: dict[str, list[int]] = {"numbers": [1, 2]}
|
||||
@@ -2494,18 +2000,6 @@ class MainTests(unittest.TestCase):
|
||||
with self.assertRaises(dm.InvalidFieldValue):
|
||||
App(data={"numbers": [1, "oops"]})
|
||||
|
||||
def test_feature_dict_override_with_nested_containers(self):
|
||||
class App(dm.Appliance):
|
||||
class F1(dm.Feature):
|
||||
values: list[int] = [1, 2]
|
||||
|
||||
app = App(F1={"values": [5, 6]})
|
||||
self.assertEqual(app.F1.values, (5, 6)) # deepfreeze → tuple
|
||||
|
||||
# Invalid type in list should fail
|
||||
with self.assertRaises(dm.InvalidFieldValue):
|
||||
App(F1={"values": [1, "oops"]})
|
||||
|
||||
def test_initializer_modifies_nested_containers(self):
|
||||
class App(dm.Appliance):
|
||||
data: dict[str, list[int]] = {"nums": [1]}
|
||||
@@ -2518,61 +2012,6 @@ class MainTests(unittest.TestCase):
|
||||
app = App()
|
||||
self.assertEqual(app.data["nums"], (1, 2)) # frozen tuple after init
|
||||
|
||||
def test_feature_dict_override_with_unknown_key(self):
|
||||
class App(dm.Appliance):
|
||||
class F1(dm.Feature):
|
||||
a: int = 1
|
||||
|
||||
# Dict override with unknown field 'zzz'
|
||||
with self.assertRaises(dm.InvalidFieldValue):
|
||||
App(F1={"zzz": 42})
|
||||
|
||||
def test_schema_isolation_across_multiple_feature_overrides(self):
|
||||
class App(dm.Appliance):
|
||||
class F1(dm.Feature):
|
||||
a: int = 1
|
||||
|
||||
class F1a(App.F1):
|
||||
a = 10
|
||||
|
||||
class F1b(App.F1):
|
||||
a = 20
|
||||
|
||||
app1 = App(F1=F1a)
|
||||
self.assertIsInstance(app1.F1, F1a)
|
||||
self.assertEqual(app1.F1.a, 10)
|
||||
|
||||
app2 = App(F1=F1b)
|
||||
self.assertIsInstance(app2.F1, F1b)
|
||||
self.assertEqual(app2.F1.a, 20)
|
||||
|
||||
# Original appliance schema must not be polluted
|
||||
app3 = App()
|
||||
self.assertIsInstance(app3.F1, App.F1)
|
||||
self.assertEqual(app3.F1.a, 1)
|
||||
|
||||
def test_feature_inheritance_with_annotated_fields(self):
|
||||
from typing_extensions import Annotated
|
||||
|
||||
class App(dm.Appliance):
|
||||
class F1(dm.Feature):
|
||||
a: Annotated[int, dm.LAMFieldInfo(doc="field a")] = 1
|
||||
|
||||
# ✅ Subclass override must inherit from parent F1
|
||||
class F1Ex(App.F1):
|
||||
b: str = "ok"
|
||||
|
||||
app = App(F1=F1Ex)
|
||||
self.assertIsInstance(app.F1, F1Ex)
|
||||
self.assertEqual((app.F1.a, app.F1.b), (1, "ok"))
|
||||
|
||||
# ❌ Wrong: fresh Feature under same name
|
||||
with self.assertRaises(dm.InvalidFeatureInheritance):
|
||||
|
||||
class Bad(App):
|
||||
class F1(dm.Feature):
|
||||
fail: str = "oops"
|
||||
|
||||
|
||||
# ---------- main ----------
|
||||
|
||||
293
test/test_element.py
Normal file
293
test/test_element.py
Normal 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
730
test/test_feature.py
Normal 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()
|
||||
Reference in New Issue
Block a user