Compare commits

...

24 Commits

Author SHA1 Message Date
chacha
19a6c802bb yet another huge rework 2025-09-23 21:47:23 +02:00
chacha
0537d2d912 fix exceptions 2025-09-22 22:29:18 +02:00
chacha
09237ff8cd work 2025-09-22 21:43:03 +02:00
chacha
ff55ef18d1 all tests passes ! 2025-09-22 01:50:26 +02:00
chacha
827e5e3f55 huge work 2025-09-22 01:14:14 +02:00
chacha
b9f5b83690 work 2025-09-21 10:47:53 +02:00
chacha
616a53578c immplement Element behaviour and some tests 2025-09-21 01:00:01 +02:00
chacha
d20712a72f remove last references to DAB 2025-09-20 19:01:21 +02:00
chacha
2837b6439f cleaning 2025-09-20 18:57:32 +02:00
chacha
b4d6ed6130 add missing __init__ files 2025-09-20 18:48:17 +02:00
chacha
cd69fc22a8 continue renaming 2025-09-20 18:44:19 +02:00
chacha
9aec2d66cd reorganize and rename (partial) 2025-09-20 18:27:36 +02:00
chacha
af81ec5fd3 more test cases 2025-09-20 13:18:40 +02:00
chacha
26e32a004f increase coverage 2025-09-20 12:43:43 +02:00
chacha
b7cbc50f79 work 2025-09-20 11:38:05 +02:00
chacha
86eee2e378 continue features implementation + code lint + typing + etc 2025-09-18 23:21:42 +02:00
chacha
3e0defc574 work 2025-09-18 00:32:32 +02:00
chacha
f6e581381d cleaning 2025-09-17 00:16:30 +02:00
chacha
981c5201a9 partially fix features 2025-09-16 23:40:41 +02:00
chacha
ab11052c8f work 2025-09-09 00:13:06 +02:00
chacha
4f5dade949 first feature implementation 2025-09-08 01:23:46 +02:00
cclecle
cce260bc5e reordering 2025-09-07 18:42:38 +02:00
cclecle
915a4332ee tiny fix :) 2025-09-06 01:47:49 +02:00
cclecle
4dca3eb9d1 improve typing 2025-09-06 01:43:20 +02:00
22 changed files with 3825 additions and 954 deletions

View File

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

View File

@@ -0,0 +1,126 @@
from typing import Generic, TypeVar, Optional, Any, Self
from typeguard import check_type, CollectionCheckStrategy, TypeCheckError
from .LAMFieldInfo import LAMFieldInfo
from .Constraint import Constraint
from ..tools import LAMdeepfreeze
from ..exception import InvalidFieldValue, ReadOnlyField
TV_LABField = TypeVar("TV_LABField")
class LAMField(Generic[TV_LABField]):
"""This class describe a Field in Schema"""
def __init__(self, name: str, v: Optional[TV_LABField], a: Any, i: LAMFieldInfo):
self.__name: str = name
self.__source: Optional[type] = None
self.__info: LAMFieldInfo = i
self.__annotations: Any = LAMdeepfreeze(a)
self.validate(v)
self.__default_value: Optional[TV_LABField] = v
self.__value: Optional[TV_LABField] = v
self.__constraints: list[Constraint[Any]] = i.constraints
self.__frozen_constraints_set = False
self.__frozen_constraints = ()
self.__frozen = False
self.__frozen_value = None
self.__frozen_value_set = False
def is_frozen(self) -> bool:
return self.__frozen
def freeze(self):
self.__frozen = True
def clone_unfrozen(self) -> Self:
field = LAMField(self.__name, self.__default_value, self.__annotations, self.info)
field.update_value(self.__value)
return field
def add_source(self, s: type) -> None:
"""Adds source Appliance to the Field"""
if self.__frozen:
raise ReadOnlyField("Field is frozen")
self.__source = s
@property
def doc(self) -> str:
"""Returns Field's documentation"""
return self.__info.doc
def add_constraint(self, c: Constraint) -> None:
"""Adds constraint to the Field"""
if self.__frozen:
raise ReadOnlyField("Field is frozen")
self.__constraints.append(c)
self.__frozen_constraints_set = False
@property
def constraints(self) -> list[Constraint]:
"""Returns Field's constraint"""
if not self.__frozen_constraints_set:
self.__frozen_constraints = LAMdeepfreeze(self.__info.constraints)
self.__frozen_value_set = True
return self.__frozen_constraints
def validate_self(self):
self.validate(self.__value)
def validate(self, v: Optional[TV_LABField]):
try:
check_type(
v,
self.annotations,
collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS,
)
except TypeCheckError as exp:
raise InvalidFieldValue(
f"Value of Field <{self.__name}> is not of expected type {self.annotations}."
) from exp
@property
def default_value(self) -> Any:
"""Returns Field's default value (frozen)"""
return LAMdeepfreeze(self.__default_value)
def update_value(self, v: Optional[TV_LABField] = None) -> None:
"""Updates Field's value"""
if self.__frozen:
raise ReadOnlyField("Field is frozen")
self.validate(v)
self.__value = v
self.__frozen_value_set = False
@property
def value(self) -> Any:
"""Returns Field's value (frozen)"""
if self.__frozen:
return self.frozen_value
else:
return self.raw_value
@property
def raw_value(self) -> Optional[TV_LABField]:
"""Returns Field's value"""
if self.__frozen:
raise ReadOnlyField("Field is frozen")
return self.__value
@property
def frozen_value(self) -> Any:
if not self.__frozen_value_set:
self.__frozen_value = LAMdeepfreeze(self.__value)
self.__frozen_value_set = True
return self.__frozen_value
@property
def annotations(self) -> Any:
"""Returns Field's annotation"""
return self.__annotations
@property
def info(self) -> LAMFieldInfo:
"""Returns Field's info"""
return self.__info

View File

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

View File

View File

@@ -11,10 +11,19 @@ Main module __init__ file.
"""
from .__metadata__ import __version__, __Summuary__, __Name__
from .model import (
DABFieldInfo,
BaseAppliance,
BaseFeature,
from .meta.element import ClassMutable, ObjectMutable
from .element import Element
from .LAMFields.LAMField import LAMField
from .LAMFields.LAMFieldInfo import LAMFieldInfo
# from .LAMFields.FrozenLAMField import FrozenLAMField
from .appliance import Appliance
from .feature import Feature
from .exception import (
DABModelException,
MultipleInheritanceForbidden,
BrokenInheritance,
@@ -27,4 +36,10 @@ from .model import (
IncompletelyAnnotatedField,
ImportForbidden,
FunctionForbidden,
InvalidFeatureInheritance,
FeatureNotBound,
UnsupportedFieldType,
NonExistingField,
)
__all__ = [name for name in globals() if not name.startswith("_")]

View File

@@ -15,20 +15,26 @@ import warnings
try: # pragma: no cover
__version__ = version("dabmodel")
except PackageNotFoundError: # pragma: no cover
warnings.warn("can not read __version__, assuming local test context, setting it to ?.?.?")
except PackageNotFoundError: # pragma: no cover
warnings.warn(
"can not read __version__, assuming local test context, setting it to ?.?.?"
)
__version__ = "?.?.?"
try: # pragma: no cover
dist = distribution("dabmodel")
__Summuary__ = dist.metadata["Summary"]
except PackageNotFoundError: # pragma: no cover
warnings.warn('can not read dist.metadata["Summary"], assuming local test context, setting it to <dabmodel description>')
warnings.warn(
'can not read dist.metadata["Summary"], assuming local test context, setting it to <dabmodel description>'
)
__Summuary__ = "dabmodel description"
try: # pragma: no cover
dist = distribution("dabmodel")
__Name__ = dist.metadata["Name"]
except PackageNotFoundError: # pragma: no cover
warnings.warn('can not read dist.metadata["Name"], assuming local test context, setting it to <dabmodel>')
warnings.warn(
'can not read dist.metadata["Name"], assuming local test context, setting it to <dabmodel>'
)
__Name__ = "dabmodel"

51
src/dabmodel/appliance.py Normal file
View File

@@ -0,0 +1,51 @@
from .meta.element import IAppliance
from .meta.appliance import _MetaAppliance
from .feature import Feature
class Appliance(IAppliance, metaclass=_MetaAppliance):
"""BaseFeature class
Base class for Appliance.
An appliance is a server configuration / image that is built using appliance's code and Fields.
"""
def _freeze_unknown_attr(self, name: str):
if isinstance(self.__dict__[name], Feature):
self.__dict__[name].freeze()
return
super()._freeze_unknown_attr(name)
def _freeze_missing_attr(self, name: str):
if name == "features":
return
super()._freeze_missing_attr(name)
def _validate_schema_unknown_attr(self, name: str):
if isinstance(self.__dict__[name], Feature):
return
super()._validate_schema_unknown_attr(name)
def _validate_schema_missing_attr(self, name: str):
if name == "features":
return
super()._validate_schema_missing_attr(name)
@classmethod
def _freeze_unknown_field_schema(cls, name: str):
if name == "features":
for feature in cls.__lam_schema__["features"].values():
feature.freeze_class()
return
# print(name)
# print("!!!!!!!!!!")
# print(cls.__lam_schema__)
# print(cls.__lam_schema__["feature"])
# return
# cls.__lam_schema__["features"] = dict(cls.__lam_schema__["features"])
super()._freeze_unknown_field_schema(name)
@classmethod
def _validate_unknown_field_schema(cls, name: str):
if name == "features":
return
super()._validate_unknown_field_schema(name)

View File

@@ -0,0 +1,121 @@
from typing import Any
# from .LAMFields.FrozenLAMField import FrozenLAMField
from .LAMFields.LAMField import LAMField
from .exception import ReadOnlyField, NewFieldForbidden, SchemaViolation
from .tools import LAMdeepfreeze, is_data_attribute
class BaseElement:
__lam_schema__ = {}
__lam_initialized__ = False
__lam_class_mutable__ = False
__lam_object_mutable__ = False
def __setattr__(self, key: str, value: Any):
print(f"!guarded_setattr {self} {key} {value}")
if key.startswith("_"):
return super().__setattr__(key, value)
if key not in self.__lam_schema__:
raise NewFieldForbidden(f"Can't create new object attributes: {key}")
if not self.__lam_object_mutable__:
raise ReadOnlyField(f"{key} is read-only")
self.__lam_schema__[key].validate(value)
return super().__setattr__(key, value)
def freeze(self, force: bool = False):
if self.__lam_object_mutable__ or force:
if self.__lam_class_mutable__:
self.validate_schema()
setSchemaKeys = set(self.__lam_schema__.keys())
setInstanceKeys = {_[0] for _ in self.__dict__.items() if is_data_attribute(_[0], _[1])}
for unknown_attr in setInstanceKeys - setSchemaKeys:
self._freeze_unknown_attr(unknown_attr)
for unknown_attr in setSchemaKeys - setInstanceKeys:
self._freeze_missing_attr(unknown_attr)
for attrName in setSchemaKeys & setInstanceKeys:
object.__setattr__(self, attrName, LAMdeepfreeze(self.__dict__[attrName]))
self.__lam_object_mutable__ = False
def _freeze_unknown_attr(self, name: str):
raise SchemaViolation(f"Attribute <{name}> is not in the schema")
def _freeze_missing_attr(self, name: str):
raise SchemaViolation(f"Attribute <{name}> is missing from instance")
def validate_schema(self):
setSchemaKeys = set(self.__lam_schema__.keys())
setInstanceKeys = {_[0] for _ in self.__dict__.items() if is_data_attribute(_[0], _[1])}
for unknown_attr in setInstanceKeys - setSchemaKeys:
self._validate_schema_unknown_attr(unknown_attr)
for missing_attr in setSchemaKeys - setInstanceKeys:
self._validate_schema_missing_attr(missing_attr)
for attrName in setSchemaKeys & setInstanceKeys:
self.__lam_schema__[attrName].validate(self.__dict__[attrName])
def _validate_schema_unknown_attr(self, name: str):
raise SchemaViolation(f"Attribute <{name}> is not in the schema")
def _validate_schema_missing_attr(self, name: str):
raise SchemaViolation(f"Attribute <{name}> is missing from instance")
@classmethod
def freeze_class(cls, force: bool = False):
if cls.__lam_class_mutable__ or force:
cls.validate_schema_class()
# class should not have any elements so they are all unknown
for unknown_attr in {
_[0] for _ in cls.__dict__.items() if is_data_attribute(_[0], _[1])
}:
cls._freeze_unknown_attr_class(unknown_attr)
for k, v in cls.__lam_schema__.items():
if isinstance(v, LAMField):
cls.__lam_schema__[k].freeze()
else:
cls._freeze_unknown_field_schema(k)
cls.__lam_class_mutable__ = False
@classmethod
def _freeze_unknown_attr_class(cls, name: str):
raise SchemaViolation(f"Class attribute <{name}> is not in the schema")
@classmethod
def _freeze_unknown_field_schema(cls, name: str):
raise SchemaViolation(f"Unknown field <{name} in the schema> ")
@classmethod
def validate_schema_class(cls):
# class should not have any elements so they are all unknown
for unknown_attr in {_[0] for _ in cls.__dict__.items() if is_data_attribute(_[0], _[1])}:
cls._validate_unknown_attr_class(unknown_attr)
for k, v in cls.__lam_schema__.items():
if isinstance(v, LAMField):
v.validate_self()
else:
cls._validate_unknown_field_schema(k)
@classmethod
def _validate_unknown_attr_class(cls, name: str):
raise SchemaViolation(f"Class attribute <{name}> is not in the schema")
@classmethod
def _validate_unknown_field_schema(cls, name: str):
raise SchemaViolation(f"Unknown field <{name} in the schema> ")

87
src/dabmodel/defines.py Normal file
View File

@@ -0,0 +1,87 @@
from typing import Any, Union, Optional, List, Dict, Tuple, Set, FrozenSet, Annotated
from types import SimpleNamespace
import math
ALLOWED_MODEL_FIELDS_TYPES: tuple[type[Any], ...] = (
str,
int,
float,
complex,
bool,
bytes,
)
ALLOWED_ANNOTATIONS: dict[str, Any] = {
"Union": Union,
"Optional": Optional,
"List": List,
"Dict": Dict,
"Tuple": Tuple,
"Set": Set,
"FrozenSet": FrozenSet,
"Annotated": Annotated,
# builtins:
"int": int,
"str": str,
"float": float,
"bool": bool,
"complex": complex,
"bytes": bytes,
"None": type(None),
"list": list,
"dict": dict,
"set": set,
"frozenset": frozenset,
"tuple": tuple,
}
ALLOWED_HELPERS_MATH = SimpleNamespace(
sqrt=math.sqrt,
floor=math.floor,
ceil=math.ceil,
trunc=math.trunc,
fabs=math.fabs,
copysign=math.copysign,
hypot=math.hypot,
exp=math.exp,
log=math.log,
log10=math.log10,
sin=math.sin,
cos=math.cos,
tan=math.tan,
atan2=math.atan2,
radians=math.radians,
degrees=math.degrees,
)
ALLOWED_HELPERS_DEFAULT: dict[str, object] = {
"math": ALLOWED_HELPERS_MATH,
"print": print,
# Numbers & reducers (pure, deterministic)
"abs": abs,
"round": round,
"min": min,
"max": max,
"sum": sum,
# Introspection-free basics
"len": len,
"sorted": sorted,
# Basic constructors (for copy-on-write patterns)
"tuple": tuple,
"list": list,
"dict": dict,
"set": set,
# Simple casts if they need to normalize types
"int": int,
"float": float,
"str": str,
"bool": bool,
"bytes": bytes,
"complex": complex,
# Easy iteration helpers (optional but handy)
"range": range,
}
JSONPrimitive = Union[str, int, float, bool, None]
JSONType = Union[JSONPrimitive, List[Any], Dict[str, Any]] # recursive in practice

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

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

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

@@ -0,0 +1,148 @@
class DABModelException(Exception):
"""DABModelException Exception class
Base Exception for DABModelException class
"""
class FunctionForbidden(DABModelException):
"""FunctionForbidden Exception class"""
class ExternalCodeForbidden(FunctionForbidden):
"""ExternalCodeForbidden Exception class"""
class ClosureForbidden(FunctionForbidden):
"""ClosureForbidden Exception class"""
class ReservedFieldName(AttributeError, DABModelException):
"""ReservedFieldName Exception class
Base Exception for DABModelException class
"""
class MultipleInheritanceForbidden(DABModelException):
"""MultipleInheritanceForbidden Exception class
Multiple inheritance is forbidden when using dabmodel
"""
class BrokenInheritance(DABModelException):
"""BrokenInheritance Exception class
inheritance chain is broken
"""
class ReadOnlyField(AttributeError, DABModelException):
"""ReadOnlyField Exception class
The used Field is ReadOnly
"""
class NewFieldForbidden(AttributeError, DABModelException):
"""NewFieldForbidden Exception class
Field creation is forbidden
"""
class InvalidFieldAnnotation(AttributeError, DABModelException):
"""InvalidFieldAnnotation Exception class
The field annotation is invalid
"""
class InvalidInitializerType(DABModelException):
"""InvalidInitializerType Exception class
The initializer is not a valid type
"""
class NotAnnotatedField(InvalidFieldAnnotation):
"""NotAnnotatedField Exception class
The Field is not Annotated
"""
class IncompletelyAnnotatedField(InvalidFieldAnnotation):
"""IncompletelyAnnotatedField Exception class
The field annotation is incomplete
"""
class UnsupportedFieldType(InvalidFieldAnnotation):
"""UnsupportedFieldType Exception class
The field type is unsupported
"""
class ReadOnlyFieldAnnotation(AttributeError, DABModelException):
"""ReadOnlyFieldAnnotation Exception class
Field annotation connot be modified
"""
class InvalidFieldValue(AttributeError, DABModelException):
"""InvalidFieldValue Exception class
The Field value is invalid
"""
class InvalidFieldName(AttributeError, DABModelException):
"""InvalidFieldName Exception class
The Field name is invalid
"""
class NonExistingField(AttributeError, DABModelException):
"""NonExistingField Exception class
The given Field is non existing
"""
class SchemaViolation(AttributeError, DABModelException):
"""SchemaViolation Exception class
The Element Schema is not respected
"""
class ImportForbidden(DABModelException):
"""ImportForbidden Exception class
Imports are forbidden
"""
class InvalidFeatureInheritance(DABModelException):
"""InvalidFeatureInheritance Exception class
Features of same name in child appliance need to be from same type
"""
class FeatureNotBound(DABModelException):
"""FeatureNotBound Exception class
a Feature must be bound to the appliance (or parent)
"""
class FeatureAlreadyBound(DABModelException):
"""FeatureAlreadyBound Exception class
Feature can only be bind once
"""
class FeatureBoundToNonAppliance(DABModelException):
"""FeatureBoundToNonAppliance Exception class
Feature can only be bind to Appliance class
"""
class FeatureBoundToIncompatiblegAppliance(DABModelException):
"""FeatureBoundToWrongAppliance Exception class
Feature have to be bound to correct appliance
"""
class FeatureWrongBound(DABModelException):
"""FeatureWrongBound Exception class
Feature can be bind to one and only one Appliance
"""

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

@@ -0,0 +1,50 @@
from .meta.element import IFeature, IAppliance
from .meta.feature import _MetaFeature
from .exception import (
FeatureAlreadyBound,
FeatureNotBound,
FeatureBoundToNonAppliance,
FeatureBoundToIncompatiblegAppliance,
)
class Feature(IFeature, metaclass=_MetaFeature):
"""Feature class
Base class for Appliance's Features.
Features are optional traits of an appliance.
"""
__lam_bound_appliance__ = None
@classmethod
def check_appliance_bound(cls):
if cls.__lam_bound_appliance__ is None:
raise FeatureNotBound(f"Feature {cls} is not bound to any Appliance")
@classmethod
def check_appliance_compatibility(cls, appliance_cls):
cls.check_appliance_bound()
if not issubclass(appliance_cls, cls.__lam_bound_appliance__):
raise FeatureBoundToIncompatiblegAppliance(
f"Feature {cls} is bound to an incompatible Appliance {appliance_cls}"
)
@classmethod
def bind_appliance(cls, appliance_cls):
if cls.__lam_bound_appliance__ is not None:
raise FeatureAlreadyBound(
f"Feature {cls} already bound to an Appliance {cls.__lam_bound_appliance__}"
)
if (
appliance_cls is None
or not isinstance(appliance_cls, type)
or not issubclass(appliance_cls, IAppliance)
):
raise FeatureBoundToNonAppliance(
f"Trying to bind Feature {cls} to an invalid Appliance Reference: {appliance_cls}"
)
cls.__lam_bound_appliance__ = appliance_cls
Enabled: bool = False

View File

View File

@@ -0,0 +1,219 @@
from typing import Any, Type
from copy import copy
from frozendict import frozendict
from ..LAMFields.LAMField import LAMField
# from ..LAMFields.FrozenLAMField import FrozenLAMField
from .element import _MetaElement
from ..feature import Feature
from ..exception import InvalidFieldValue, InvalidFeatureInheritance, InvalidFieldName
from ..tools import LAMdeepfreeze
class _MetaAppliance(_MetaElement):
"""_MetaAppliance class
Appliance specific metaclass code
"""
@classmethod
def check_class(
mcs: type["meta"],
name: str,
bases: tuple[type[Any], ...],
namespace: dict[str, Any], # pylint: disable=unused-argument
extensions: dict[str, Any],
) -> None:
"""
Appliance-specific pre-check: ensure the `features` slot exists in schema.
Copies the parent's `features` mapping when inheriting to keep it per-class.
"""
super().check_class(name, bases, namespace, extensions) # type: ignore[misc]
if "features" not in namespace["__lam_schema__"]:
namespace["__lam_schema__"]["features"] = {}
@classmethod
def inherit_schema( # pylint: disable=too-complex,too-many-branches
mcs: type["_MetaElement"],
name: str,
base: type[Any],
namespace: dict[str, Any],
extensions: dict[str, Any],
):
super().inherit_schema(name, base, namespace, extensions)
if "features" in base.__lam_schema__:
print("COPY feature")
namespace["__lam_schema__"]["features"] = {}
namespace["__lam_schema__"]["features"].update(base.__lam_schema__["features"])
@classmethod
def process_class_fields(
mcs: type["meta"],
name: str,
bases: tuple[type[Any], ...],
namespace: dict[str, Any],
extensions: dict[str, Any],
):
"""
Like meta.process_class_fields but also stages Feature declarations.
Initializes:
extensions["new_features"], extensions["modified_features"]
then defers to the base scanner for regular fields.
"""
extensions["new_features"] = {}
extensions["modified_features"] = {}
super().process_class_fields(name, bases, namespace, extensions) # type: ignore[misc]
@classmethod
def process_new_field(
mcs: type["meta"],
name: str,
bases: tuple[type[Any], ...],
namespace: dict[str, Any],
_fname: str,
_fvalue: Any,
extensions: dict[str, Any],
): # pylint: disable=unused-argument
"""
Intercept Feature declarations.
- If `_fname` already exists in parent's `features`, enforce same type;
stage into `modified_features`.
- Else, if `_fvalue` is a Feature *class*, stage into `new_features`.
- Otherwise, it is a regular field: delegate to meta.process_new_field.
"""
if _fname == "feature":
raise InvalidFieldName("'feature' is a reserved Field name")
if _fname in namespace["__lam_schema__"]["features"].keys():
if not issubclass(_fvalue, namespace["__lam_schema__"]["features"][_fname]):
raise InvalidFeatureInheritance(
f"Feature {_fname} is not an instance of {bases[0]}.{_fname}"
)
extensions["modified_features"][_fname] = _fvalue
elif isinstance(_fvalue, type) and issubclass(_fvalue, Feature):
extensions["new_features"][_fname] = _fvalue
else:
super().process_new_field(name, bases, namespace, _fname, _fvalue, extensions) # type: ignore[misc]
@classmethod
def commit_fields(
mcs: type["meta"],
cls,
name: str,
bases: tuple[type[Any], ...],
namespace: dict[str, Any], # pylint: disable=unused-argument
extensions: dict[str, Any],
):
"""
Commit regular fields (via meta) and then bind staged Feature classes.
For each new/modified feature:
- bind it to `cls` (sets the feature's `_BoundAppliance`),
- register it under `cls.__LAMSchema__["features"]`.
"""
super().commit_fields(cls, name, bases, namespace, extensions) # type: ignore[misc]
cls.__lam_schema__["features"].update(extensions["modified_features"])
for v in extensions["new_features"].values():
v.bind_appliance(cls)
cls.__lam_schema__["features"].update(extensions["new_features"])
def populate_instance(cls: Type, obj: Any, extensions: dict[str, Any], *args: Any, **kw: Any):
super().populate_instance(obj, extensions, *args, **kw)
obj.__lam_schema__["features"] = {}
obj.__lam_schema__["features"].update(cls.__lam_schema__["features"])
def finalize_instance(cls: Type, obj, extensions: dict[str, Any]):
"""
Instantiate and attach all features declared (or overridden) in the instance schema.
Handles:
- Declared features (plain class)
- Subclass replacements
- Dict overrides (class + patch dict)
"""
for fname, fdef in obj.__lam_schema__["features"].items():
# Case 1: plain class or subclass
if isinstance(fdef, type) and issubclass(fdef, Feature):
inst = fdef()
object.__setattr__(obj, fname, inst)
# Case 2: (class, dict) → dict overrides
elif isinstance(fdef, tuple) and len(fdef) == 2:
feat_cls, overrides = fdef
print(overrides)
print(feat_cls)
inst = feat_cls(**overrides)
object.__setattr__(obj, fname, inst)
obj.__lam_schema__["features"][fname] = feat_cls
else:
raise InvalidFieldValue(
f"Invalid feature definition stored for '{fname}': {fdef!r}"
)
def apply_overrides(cls, obj, extensions, *args, **kwargs):
"""
Support for runtime field and feature overrides.
Fields:
MyApp(name="foo")
Features:
MyApp(F1=MyF1) # inheritance / replacement
MyApp(F1={"val": 42, ...}) # dict override of existing feature
"""
# --- feature overrides ---
for k, v in list(kwargs.items()):
if k in cls.__lam_schema__["features"]:
base_feat_cls = cls.__lam_schema__["features"][k]
# print(f"!!!!! {v}")
# print(f"!!!!! {base_feat_cls}")
# print(isinstance(v, type))
# print(issubclass(v, base_feat_cls))
# Case 1: subclass replacement (inheritance)
if isinstance(v, type) and issubclass(v, base_feat_cls):
print("hhhh")
v.check_appliance_compatibility(cls)
# record subclass into instance schema
obj.__lam_schema__["features"][k] = v
kwargs.pop(k)
# Case 2: dict override
elif isinstance(v, dict):
# store (class, override_dict) for finalize_instance
obj.__lam_schema__["features"][k] = (base_feat_cls, v)
kwargs.pop(k)
else:
raise InvalidFieldValue(
f"Feature override for '{k}' must be a Feature subclass or dict, got {type(v)}"
)
# --- new features not declared at class level ---
for k, v in list(kwargs.items()):
if isinstance(v, type) and issubclass(v, Feature):
v.check_appliance_compatibility(cls)
obj.__lam_schema__["features"][k] = v
kwargs.pop(k)
super().apply_overrides(obj, extensions, *args, **kwargs)
@classmethod
def finalize_class(
mcs: type["_MetaElement"],
cls,
name: str,
bases: tuple[type[Any], ...],
namespace: dict[str, Any], # pylint: disable=unused-argument
extensions: dict[str, Any],
):
cls.__lam_schema__["features"] = frozendict(cls.__lam_schema__["features"])
super().finalize_class(cls, name, bases, namespace, extensions)

View File

@@ -0,0 +1,639 @@
from typing import Optional, TypeVar, get_origin, get_args, Dict, Any, Callable, Type, Union
from types import FunctionType, UnionType
from copy import deepcopy, copy
import inspect, ast, textwrap
from typeguard import check_type, TypeCheckError, CollectionCheckStrategy
from frozendict import frozendict
from ..tools import _resolve_annotation, _peel_annotated
from ..LAMFields.LAMField import LAMField
from ..LAMFields.LAMFieldInfo import LAMFieldInfo
# from ..LAMFields.FrozenLAMField import FrozenLAMField
from ..defines import ALLOWED_HELPERS_MATH, ALLOWED_HELPERS_DEFAULT, ALLOWED_MODEL_FIELDS_TYPES
from ..base_element import BaseElement
from ..exception import (
MultipleInheritanceForbidden,
BrokenInheritance,
ReadOnlyField,
NotAnnotatedField,
ReadOnlyFieldAnnotation,
InvalidFieldValue,
InvalidFieldAnnotation,
ImportForbidden,
FunctionForbidden,
NonExistingField,
InvalidInitializerType,
IncompletelyAnnotatedField,
UnsupportedFieldType,
)
class IElement(BaseElement): ...
class IFeature(BaseElement): ...
class IAppliance(BaseElement): ...
def _check_annotation_definition( # pylint: disable=too-complex,too-many-return-statements
_type,
):
# print(f"_type={_type}")
_type = _peel_annotated(_type)
_origin = get_origin(_type) or _type
_args = get_args(_type)
# handle Optional[] and Union[None,...]
if (_origin is Union or _origin is UnionType) and type(None) in _args:
return all(_check_annotation_definition(_) for _ in get_args(_type) if _ is not type(None))
# handle other Union[...]
if _origin is Union or _origin is UnionType:
return all(_check_annotation_definition(_) for _ in _args)
# handle Dict[...]
if _origin is dict:
if len(_args) != 2:
raise IncompletelyAnnotatedField(
f"Dict Annotation requires 2 inner definitions: {_type}"
)
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 _origin is tuple:
if len(_args) == 0:
raise IncompletelyAnnotatedField(f"Annotation requires inner definition: {_type}")
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 _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 _args)
if isinstance(_origin, type) and issubclass(_origin, IElement):
return
if _type in ALLOWED_MODEL_FIELDS_TYPES:
return
raise UnsupportedFieldType(_type)
def _check_initializer_safety(func) -> None:
"""
Preliminary structural check for __initializer__.
Policy (minimal):
- Forbid 'import' / 'from ... import ...' inside the initializer body.
- Forbid nested function definitions (closures/helpers) in the body.
- Allow lambdas.
- No restrictions on calls here (keep it simple).
- Optionally forbid closures (free vars) for determinism.
"""
try:
src = inspect.getsource(func)
except OSError as exc:
# If source isn't available (rare), fail closed (or skip if you prefer)
raise FunctionForbidden("Cannot inspect __initializer__ source") from exc
src = textwrap.dedent(src)
mod = ast.parse(src)
# Find the FunctionDef node that corresponds to this initializer
init_node = next(
(
n
for n in mod.body
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef)) and n.name == func.__name__
),
None,
)
if init_node is None:
# Fallback: if not found, analyze nothing further to avoid false positives
return
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")
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
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:
# 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):
raise ImportForbidden("imports disabled in __initializer")
class ModelSpecView:
"""ModelSpecView class
A class that will act as fake BaseElement proxy to allow setting values"""
__slots__ = ("_vals", "_types", "_touched", "_name", "_module")
def __init__(self, values: dict[str, Any], types_map: dict[str, type], name: str, module: str):
self._name: str
self._vals: dict[str, Any]
self._types: dict[str, type]
self._touched: set
self._module: str
object.__setattr__(self, "_vals", dict(values))
object.__setattr__(self, "_types", types_map)
object.__setattr__(self, "_name", name)
object.__setattr__(self, "_module", module)
@property
def __name__(self) -> str:
"""returns proxified class' name"""
return self._name
@property
def __module__(self) -> str:
"""returns proxified module's name"""
return self._module
@__module__.setter
def __module__(self, value: str):
pass
def __getattr__(self, name: str) -> Any:
"""internal proxy getattr"""
if name not in self._types:
raise AttributeError(f"Unknown field {name}")
return self._vals[name]
def __setattr__(self, name: str, value: Any):
"""internal proxy setattr"""
if name not in self._types:
raise NonExistingField(f"Cannot set unknown field {name}")
T = self._types[name]
try:
check_type(
value,
T,
collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS,
)
except TypeCheckError as exp:
raise InvalidFieldValue(f"Field <{name}> value is not of expected type {T}.") from exp
self._vals[name] = value
def export(self) -> dict:
"""exports all proxified values"""
return dict(self._vals)
T_Meta = TypeVar("T_Meta", bound="_MetaElement")
T_BE = TypeVar("T_BE", bound="BaseElement")
class ElementOption:
pass
class ClassMutable:
pass
class ObjectMutable:
pass
class _MetaElement(type):
"""metaclass to use to build BaseElement"""
modified_fields: Dict[str, Any] = {}
new_fields: Dict[str, LAMField[Any]] = {}
initializer: Optional[Callable[..., Any]] = None
__lam_schema__: dict[str, Any] = {}
@classmethod
def check_class(
mcs: type["_MetaElement"],
name: str,
bases: tuple[type[Any], ...],
namespace: dict[str, Any], # pylint: disable=unused-argument
extensions: dict[str, Any],
) -> None:
"""
Early class-build hook.
Validates the inheritance shape, initializes an empty schema for root classes,
copies the parent schema for subclasses, and ensures all annotated fields
have a default (inserting `None` when missing).
This runs before the class object is created.
"""
print(f"__NEW__ Defining {name}, bases {bases}, with keys { list(namespace)}")
if len(bases) > 1:
raise MultipleInheritanceForbidden("Multiple inheritance is not supported by dabmodel")
if len(bases) == 0:
raise BrokenInheritance("wrong base class (missing base class")
if not issubclass(bases[0], BaseElement):
raise BrokenInheritance("wrong base class")
mcs.inherit_schema(name, bases[0], namespace, extensions)
# force field without default value to be instantiated (with None)
if "__annotations__" in namespace:
for _funknown in [_ for _ in namespace["__annotations__"] if _ not in namespace.keys()]:
namespace[_funknown] = None
namespace["__lam_initialized__"] = False
namespace["__lam_class_mutable__"] = ClassMutable in extensions["kwargs"]["options"]
namespace["__lam_object_mutable__"] = ObjectMutable in extensions["kwargs"]["options"]
@classmethod
def inherit_schema( # pylint: disable=too-complex,too-many-branches
mcs: type["_MetaElement"],
name: str,
base: type[Any],
namespace: dict[str, Any],
extensions: dict[str, Any],
):
namespace["__lam_schema__"] = {}
namespace["__lam_schema__"].update(base.__lam_schema__)
for k, v in namespace["__lam_schema__"].items():
print(f"TEST FIELD {k} : {v}")
if isinstance(v, LAMField):
print(f"COPY FIELD {k}")
namespace["__lam_schema__"][k] = namespace["__lam_schema__"][k].clone_unfrozen()
@classmethod
def process_class_fields( # pylint: disable=too-complex,too-many-branches
mcs: type["_MetaElement"],
name: str,
bases: tuple[type[Any], ...],
namespace: dict[str, Any],
extensions: dict[str, Any],
):
"""
Scan the class namespace and partition fields.
Detects:
- modified fields (shadowing parent values),
- new fields (present in annotations),
- the optional `__initializer` classmethod (in mangled or unmangled form).
Validates annotations and types and removes processed items from `namespace`
so they won't become normal attributes. Results are staged into:
mcs.new_fields, mcs.modified_fields, mcs.initializer
to be committed later.
"""
# iterating new and modified fields
mcs.modified_fields = {}
mcs.new_fields = {}
mcs.initializer = None
initializer_name: Optional[str] = None
for _fname, _fvalue in namespace.items():
if _fname == f"_{name}__initializer" or (
name.startswith("_") and _fname == "__initializer"
):
if not isinstance(_fvalue, classmethod):
raise InvalidInitializerType("__initializer should be a classmethod")
mcs.initializer = _fvalue.__func__
if name.startswith("_"):
initializer_name = "__initializer"
else:
initializer_name = f"_{name}__initializer"
elif _fname.startswith("_"):
pass
elif isinstance(_fvalue, classmethod):
pass
elif isinstance(_fvalue, FunctionType):
pass
else:
print(f"Parsing Field: {_fname} / {_fvalue}")
if _fname in namespace["__lam_schema__"]: # Modified fields
mcs.process_modified_field(name, bases, namespace, _fname, _fvalue, extensions)
else: # New fieds
mcs.process_new_field(name, bases, namespace, _fname, _fvalue, extensions)
# removing modified fields from class (will add them back later)
for _fname in mcs.new_fields:
del namespace[_fname]
for _fname in mcs.modified_fields:
del namespace[_fname]
if mcs.initializer is not None and initializer_name is not None:
del namespace[initializer_name]
@classmethod
def process_modified_field(
mcs: type["_MetaElement"],
name: str,
bases: tuple[type[Any], ...],
namespace: dict[str, Any],
_fname: str,
_fvalue: Any,
extensions: dict[str, Any],
): # pylint: disable=unused-argument
"""
Handle a *modified* field declared by a subclass.
Forbids annotation changes, validates the new default value against
the inherited annotation, and stages the new default into `mcs.modified_fields`.
"""
if "__annotations__" in namespace and _fname in namespace["__annotations__"]:
raise ReadOnlyFieldAnnotation(
f"annotations cannot be modified on derived classes {_fname}"
)
namespace["__lam_schema__"][_fname].validate(_fvalue)
mcs.modified_fields[_fname] = _fvalue
@classmethod
def process_new_field(
mcs: type["_MetaElement"],
name: str,
bases: tuple[type[Any], ...],
namespace: dict[str, Any],
_fname: str,
_fvalue: Any,
extensions: dict[str, Any],
): # pylint: disable=unused-argument
"""
Handle a *new* field declared on the class.
Resolves string annotations against a whitelist, validates `Annotated[...]`
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}")
# check if field is annotated
if "__annotations__" not in namespace or _fname not in namespace["__annotations__"]:
raise NotAnnotatedField(f"Every dabmodel Fields must be annotated ({_fname})")
# check if annotation is allowed
if isinstance(namespace["__annotations__"][_fname], str):
namespace["__annotations__"][_fname] = _resolve_annotation(
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."
) from ex
_finfo: LAMFieldInfo = LAMFieldInfo()
origin = get_origin(namespace["__annotations__"][_fname])
tname = (
getattr(origin, "__name__", "") or getattr(origin, "__qualname__", "") or str(origin)
)
if "Annotated" in tname:
args = get_args(namespace["__annotations__"][_fname])
if args:
if len(args) > 2:
raise InvalidFieldAnnotation(f"Field <{_fname}> had invalid Annotated value.")
if len(args) == 2 and not isinstance(args[1], LAMFieldInfo):
raise InvalidFieldAnnotation(
"Only LAMFieldInfo object is allowed as Annotated data."
)
_finfo = args[1]
mcs.new_fields[_fname] = LAMField(
_fname, _fvalue, namespace["__annotations__"][_fname], _finfo
)
@classmethod
def apply_initializer(
mcs: type["_MetaElement"],
cls,
name: str,
bases: tuple[type[Any], ...],
namespace: dict[str, Any], # pylint: disable=unused-argument
extensions: dict[str, Any],
):
"""
Apply the optional `__initializer` classmethod to compute derived defaults.
The initializer runs in a restricted, import-blocked environment using a
`ModelSpecView` proxy that enforces type checking on assignments.
On success, the computed values are validated and written back into the
class schema's DABFields.
"""
if mcs.initializer is not None:
_check_initializer_safety(mcs.initializer)
init_fieldvalues = {}
init_fieldtypes = {}
for _fname, _fvalue in cls.__lam_schema__.items():
if isinstance(_fvalue, LAMField):
init_fieldvalues[_fname] = deepcopy(_fvalue.value)
init_fieldtypes[_fname] = _fvalue.annotations
fakecls = ModelSpecView(init_fieldvalues, init_fieldtypes, cls.__name__, cls.__module__)
# fakecls = cls
safe_globals = {
"__builtins__": {"__import__": _blocked_import},
**ALLOWED_HELPERS_DEFAULT,
}
# 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=mcs.initializer.__closure__,
)
safe_initializer(fakecls) # pylint: disable=not-callable
for _fname, _fvalue in fakecls.export().items():
field = cls.__lam_schema__[_fname]
# field.validate(_fvalue)
field.update_value(_fvalue)
# cls.__lam_schema__[_fname] = LAMField(_fname, _fvalue, field.annotations, field.info )
def __new__(
mcs: type["_MetaElement"],
name: str,
bases: tuple[type[Any], ...],
namespace: dict[str, Any],
**kwargs,
) -> Type:
"""BaseElement new class"""
extensions: dict[str, Any] = {}
extensions["kwargs"] = kwargs
if "options" not in extensions["kwargs"]:
extensions["kwargs"]["options"] = ()
elif extensions["kwargs"]["options"] is not tuple:
extensions["kwargs"]["options"] = (extensions["kwargs"]["options"],)
mcs.check_class(name, bases, namespace, extensions)
mcs.process_class_fields(name, bases, namespace, extensions)
_cls = super().__new__(mcs, name, bases, namespace)
mcs.commit_fields(_cls, name, bases, namespace, extensions)
mcs.apply_initializer(_cls, name, bases, namespace, extensions)
mcs.finalize_class(_cls, name, bases, namespace, extensions)
if not _cls.__lam_class_mutable__:
_cls.freeze_class(True)
_cls.__lam_initialized__ = True
return _cls
@classmethod
def commit_fields(
mcs: type["_MetaElement"],
cls,
name: str,
bases: tuple[type[Any], ...],
namespace: dict[str, Any], # pylint: disable=unused-argument
extensions: dict[str, Any],
):
"""
Commit staged fields into the class schema (`__lam_schema__`).
- 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.__lam_schema__[_fname] = deepcopy(bases[0].__lam_schema__[_fname])
cls.__lam_schema__[_fname].update_value(_fvalue)
for _fname, _fvalue in mcs.new_fields.items():
_fvalue.add_source(cls)
cls.__lam_schema__[_fname] = _fvalue
def __call__(cls: Type, *args: Any, **kw: Any): # intentionally untyped
"""BaseElement new instance"""
obj = super().__call__(*args)
extensions: dict[str, Any] = {}
cls.populate_instance(
obj, extensions, *args, **kw
) # pylint: disable=no-value-for-parameter
cls.apply_overrides(obj, extensions, *args, **kw) # pylint: disable=no-value-for-parameter
cls.finalize_instance(obj, extensions) # pylint: disable=no-value-for-parameter
if not cls.__lam_object_mutable__:
obj.freeze(True)
return obj
def populate_instance(cls: Type, obj: Any, extensions: dict[str, Any], *args: Any, **kw: Any):
"""
Populate the new instance with field values from the class schema.
Copies each LAMField.value to an instance attribute (deep-frozen view).
"""
obj.__lam_schema__ = {}
obj.__lam_schema__.update(cls.__lam_schema__)
for _fname, _fvalue in cls.__lam_schema__.items():
if isinstance(_fvalue, LAMField):
object.__setattr__(obj, _fname, _fvalue.value)
def apply_overrides(cls, obj, extensions, *args, **kwargs):
"""
Hook for runtime overrides at instance creation.
Invoked after the schema has been frozen but before finalize_instance.
Subclasses of _MetaElement can override this to support things like:
- Field overrides: MyApp(field=value)
"""
# --- field overrides (unchanged) ---
for k, v in list(kwargs.items()):
if k in cls.__lam_schema__: # regular field
field = cls.__lam_schema__[k].clone_unfrozen()
# field.validate(v)
field.update_value(v)
obj.__lam_schema__[k] = field
object.__setattr__(obj, k, v)
# lam_field = LAMField(k, v, field.annotations, field.info)
# if cls.__lam_class_mutable__:
# obj.__lam_schema__[k] = lam_field
# else:
# obj.__lam_schema__[k] = FrozenLAMField(lam_field)
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.
Subclasses of the metaclass override this to attach runtime components
to the instance. (Example: BaseMetaAppliance instantiates bound Features
and sets them as attributes on the appliance instance.)
"""
obj.__lam_schema__ = frozendict(obj.__lam_schema__)
def __setattr__(cls, name: str, value: Any):
if not hasattr(cls, "__lam_initialized__") or not getattr(cls, "__lam_initialized__"):
return super().__setattr__(name, value)
if name.startswith("_"):
return super().__setattr__(name, value)
if name not in cls.__lam_schema__:
raise NonExistingField(f"Can't create new class attributes: {name}")
if not cls.__lam_class_mutable__:
raise ReadOnlyField(f"Class is immutable.")
field = cls.__lam_schema__[name]
field.update_value(value)
# field.validate(value)
# cls.__lam_schema__[name] = LAMField(name, value, field.annotations, field.info)
return
def __getattr__(cls, name) -> Any:
if (
hasattr(cls, "__lam_initialized__")
and getattr(cls, "__lam_initialized__")
and name in cls.__lam_schema__
):
# if cls.__lam_class_mutable__:
# return cls.__lam_schema__[name].raw_value
return cls.__lam_schema__[name].value
raise NonExistingField(f"Non existing class attribute: {name}")
@classmethod
def finalize_class(
mcs: type["_MetaElement"],
cls,
name: str,
bases: tuple[type[Any], ...],
namespace: dict[str, Any], # pylint: disable=unused-argument
extensions: dict[str, Any],
):
cls.__lam_schema__ = frozendict(cls.__lam_schema__)

View File

@@ -0,0 +1,25 @@
from typing import Type, Any
from .element import _MetaElement
class _MetaFeature(_MetaElement):
"""_MetaFeature class
Feature specific metaclass code
"""
@classmethod
def finalize_class(
mcs: type["_MetaElement"],
cls,
name: str,
bases: tuple[type[Any], ...],
namespace: dict[str, Any], # pylint: disable=unused-argument
extensions: dict[str, Any],
):
if "appliance" in extensions["kwargs"]:
cls.bind_appliance(extensions["kwargs"]["appliance"])
def finalize_instance(cls: Type, obj: Any, extensions: dict[str, Any]):
print(f"2GOT {cls}: {cls.__lam_bound_appliance__}")
cls.check_appliance_bound()
super().finalize_instance(obj, extensions)

View File

@@ -1,682 +0,0 @@
""" dabmodel model module
This module implements DAB model classes.
This module contains metaclass and bases classes used to create models.
BaseAppliance can be used to create a new Appliance Data.
BaseFeature can be used to create new Appliance's Features."""
from typing import (
Optional,
TypeVar,
Generic,
Union,
get_origin,
get_args,
List,
Dict,
Any,
Tuple,
Set,
Annotated,
FrozenSet,
Callable,
Type,
)
from types import UnionType, FunctionType, SimpleNamespace
from copy import deepcopy, copy
# from pprint import pprint
import math
from frozendict import deepfreeze
from typeguard import check_type, TypeCheckError, CollectionCheckStrategy
ALLOWED_ANNOTATIONS = {
"Union": Union,
"Optional": Optional,
"List": List,
"Dict": Dict,
"Tuple": Tuple,
"Set": Set,
"FrozenSet": FrozenSet,
"Annotated": Annotated,
# builtins:
"int": int,
"str": str,
"float": float,
"bool": bool,
"complex": complex,
"bytes": bytes,
"None": type(None),
"list": list,
"dict": dict,
"set": set,
"frozenset": frozenset,
"tuple": tuple,
}
ALLOWED_MODEL_FIELDS_TYPES = (str, int, float, complex, bool, bytes)
ALLOWED_MODEL_FIELDS_CONTAINERS = (dict, list, set, frozenset, tuple)
# TV_ALLOWED_MODEL_FIELDS_TYPES = TypeVar("TV_ALLOWED_MODEL_FIELDS_TYPES", *ALLOWED_MODEL_FIELDS_TYPES, *ALLOWED_MODEL_FIELDS_CONTAINERS)
class DABModelException(Exception):
"""DABModelException Exception class
Base Exception for DABModelException class
"""
class MultipleInheritanceForbidden(DABModelException):
"""MultipleInheritanceForbidden Exception class
Multiple inheritance is forbidden when using dabmodel
"""
class BrokenInheritance(DABModelException):
"""BrokenInheritance Exception class
inheritance chain is broken
"""
class ReadOnlyField(DABModelException):
"""ReadOnlyField Exception class
The used Field is ReadOnly
"""
class NewFieldForbidden(DABModelException):
"""NewFieldForbidden Exception class
Field creation is forbidden
"""
class InvalidFieldAnnotation(DABModelException):
"""InvalidFieldAnnotation Exception class
The field annotation is invalid
"""
class InvalidInitializerType(DABModelException):
"""InvalidInitializerType Exception class
The initializer is not a valid type
"""
class NotAnnotatedField(InvalidFieldAnnotation):
"""NotAnnotatedField Exception class
The Field is not Annotated
"""
class IncompletelyAnnotatedField(InvalidFieldAnnotation):
"""IncompletelyAnnotatedField Exception class
The field annotation is incomplete
"""
class ReadOnlyFieldAnnotation(DABModelException):
"""ReadOnlyFieldAnnotation Exception class
Field annotation connot be modified
"""
class InvalidFieldValue(DABModelException):
"""InvalidFieldValue Exception class
The Field value is invalid
"""
class NonExistingField(DABModelException):
"""NonExistingField Exception class
The given Field is non existing
"""
class ImportForbidden(DABModelException):
"""ImportForbidden Exception class
Imports are forbidden
"""
class FunctionForbidden(DABModelException):
"""FunctionForbidden Exception class
function call are forbidden
"""
ALLOWED_HELPERS_MATH = SimpleNamespace(
sqrt=math.sqrt,
floor=math.floor,
ceil=math.ceil,
trunc=math.trunc,
fabs=math.fabs,
copysign=math.copysign,
hypot=math.hypot,
exp=math.exp,
log=math.log,
log10=math.log10,
sin=math.sin,
cos=math.cos,
tan=math.tan,
atan2=math.atan2,
radians=math.radians,
degrees=math.degrees,
)
ALLOWED_HELPERS_DEFAULT = {
"math": ALLOWED_HELPERS_MATH,
"print": print,
# Numbers & reducers (pure, deterministic)
"abs": abs,
"round": round,
"min": min,
"max": max,
"sum": sum,
# Introspection-free basics
"len": len,
"sorted": sorted,
# Basic constructors (for copy-on-write patterns)
"tuple": tuple,
"list": list,
"dict": dict,
"set": set,
# Simple casts if they need to normalize types
"int": int,
"float": float,
"str": str,
"bool": bool,
"bytes": bytes,
"complex": complex,
# Easy iteration helpers (optional but handy)
"range": range,
}
def _blocked_import(*args, **kwargs):
raise ImportForbidden("imports disabled in __initializer")
def _resolve_annotation(ann):
if isinstance(ann, str):
# Safe eval against a **whitelist** only
return eval(ann, {"__builtins__": {}}, ALLOWED_ANNOTATIONS) # pylint: disable=eval-used
return ann
def _peel_annotated(t: Any) -> Any:
# If you ever allow Annotated[T, ...], peel to T
while True:
origin = get_origin(t)
if origin is None:
return t
name = getattr(origin, "__name__", "") or getattr(origin, "__qualname__", "") or str(origin)
if "Annotated" in name:
args = get_args(t)
t = args[0] if args else t
else:
return t
def _check_annotation_definition(_type) -> bool: # pylint: disable=too-complex,too-many-return-statements
_type = _peel_annotated(_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):
return all(_check_annotation_definition(_) for _ in get_args(_type) if _ is not type(None))
# 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))
# handle Dict[...]
if get_origin(_type) is dict:
inner = get_args(_type)
if len(inner) != 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])
# handle Tuple[]
if get_origin(_type) in [tuple]:
inner_types = get_args(_type)
if len(inner_types) == 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)
# handle Set[],Tuple[],FrozenSet[],List[]
if get_origin(_type) in [set, frozenset, tuple, list]:
inner_types = get_args(_type)
if len(inner_types) == 0:
raise IncompletelyAnnotatedField(f"Annotation requires inner definition: {_type}")
return all(_check_annotation_definition(_) for _ in inner_types)
if _type in ALLOWED_MODEL_FIELDS_TYPES:
return True
return False
T_Field = TypeVar("T_Field")
class BaseConstraint(Generic[T_Field]):
"""BaseConstraint class
Base class for Field's constraints
"""
_bound_type: type
def __init__(self): ...
def check(self, value: T_Field) -> bool:
"""Check if a Constraint is completed"""
return True
def _deepfreeze(value):
"""recursive freeze helper function"""
if isinstance(value, dict):
return deepfreeze(value)
if isinstance(value, set):
return frozenset(_deepfreeze(v) for v in value)
if isinstance(value, list):
return tuple(_deepfreeze(v) for v in value)
if isinstance(value, tuple):
return tuple(_deepfreeze(v) for v in value)
return value
class DABFieldInfo:
"""This Class allows to describe a Field in Appliance class"""
def __init__(self, *, doc: str = "", constraints: Optional[list[BaseConstraint]] = None):
self._doc: str = doc
self._constraints: list[BaseConstraint]
if constraints is None:
self._constraints = []
else:
self._constraints = constraints
@property
def doc(self):
"""Returns Field's documentation"""
return self._doc
@property
def constraints(self) -> list[BaseConstraint[Any]]:
"""Returns Field's constraints"""
return self._constraints
class DABField(Generic[T_Field]):
"""This class describe a Field in Schema"""
def __init__(self, name: str, v: Optional[T_Field], a: Any, i: DABFieldInfo):
self._name: str = name
self._source: Optional[type] = None
self._default_value: Optional[T_Field] = v
self._value: Optional[T_Field] = v
self._annotations: Any = a
self._info: DABFieldInfo = i
self._constraints: List[BaseConstraint[Any]] = i.constraints
def add_source(self, s: type) -> None:
"""Adds source Appliance to the Field"""
self._source = s
@property
def doc(self):
"""Returns Field's documentation"""
return self._info.doc
def add_constraint(self, c: BaseConstraint) -> None:
"""Adds constraint to the Field"""
self._constraints.append(c)
@property
def constraints(self) -> list[BaseConstraint]:
"""Returns Field's constraint"""
return self._info.constraints
@property
def default_value(self) -> Any:
"""Returns Field's default value (frozen)"""
return _deepfreeze(self._default_value)
def update_value(self, v: Optional[T_Field] = None) -> None:
"""Updates Field's value"""
self._value = v
@property
def value(self) -> Any:
"""Returns Field's value (frosen)"""
return _deepfreeze(self._value)
@property
def raw_value(self) -> Optional[T_Field]:
"""Returns Field's value"""
return self._value
@property
def annotations(self) -> Any:
"""Returns Field's annotation"""
return self._annotations
class FrozenDABField(Generic[T_Field]):
"""FrozenDABField class
a read-only proxy of a Field
"""
def __init__(self, inner_field: DABField):
self._inner_field = inner_field
@property
def doc(self) -> str:
"""Returns Field's documentation (frozen)"""
return _deepfreeze(self._inner_field.doc)
@property
def constraints(self) -> tuple[BaseConstraint]:
"""Returns Field's constraint (frozen)"""
return _deepfreeze(self._inner_field.constraints)
@property
def default_value(self) -> Any:
"""Returns Field's default value (frozen)"""
return self._inner_field.default_value
@property
def value(self) -> Any:
"""Returns Field's value (frosen)"""
return self._inner_field.value
@property
def annotations(self) -> Any:
"""Returns Field's annotation (frozen)"""
return _deepfreeze(self._inner_field.annotations)
class ModelSpecView:
"""ModelSpecView class
A class that will act as fake BaseElement proxy to allow setting values"""
__slots__ = ("_vals", "_types", "_touched", "_name", "_module")
def __init__(self, values: dict, types_map: dict[str, type], name, module):
object.__setattr__(self, "_vals", dict(values))
object.__setattr__(self, "_types", types_map)
object.__setattr__(self, "_touched", set())
object.__setattr__(self, "_name", name)
object.__setattr__(self, "_module", module)
@property
def __name__(self) -> str:
"""returns proxified class' name"""
return self._name
@property
def __module__(self) -> str:
"""returns proxified module's name"""
return self._module
@__module__.setter
def __module__(self, value):
pass
def __getattr__(self, name):
"""internal proxy getattr"""
if name not in self._types:
raise AttributeError(f"Unknown field {name}")
return self._vals[name]
def __setattr__(self, name, value):
"""internal proxy setattr"""
if name not in self._types:
raise NonExistingField(f"Cannot set unknown field {name}")
T = self._types[name]
try:
check_type(
value,
T,
collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS,
)
except TypeCheckError as exp:
raise InvalidFieldValue(f"Field <{name}> value is not of expected type {T}.") from exp
self._vals[name] = value
self._touched.add(name)
def export(self) -> dict:
"""exports all proxified values"""
return dict(self._vals)
T_Meta = TypeVar("T_Meta", bound="BaseMeta")
T_BE = TypeVar("T_BE", bound="BaseElement")
class BaseMeta(type):
"""metaclass to use to build BaseElement"""
modified_field: Dict[str, Any] = {}
new_fields: Dict[str, DABField[Any]] = {}
initializer: Optional[Callable[..., Any]] = None
__DABSchema__: dict[str, Any] = {}
@classmethod
def pre_check(
mcs: type["BaseMeta"], name: str, bases: tuple[type[Any], ...], namespace: dict[str, Any] # pylint: disable=unused-argument
) -> None:
"""early BaseElement checks"""
# print("__NEW__ Defining:", name, "with keys:", list(namespace))
if len(bases) > 1:
raise MultipleInheritanceForbidden("Multiple inheritance is not supported by dabmodel")
if len(bases) == 0: # base class (BaseElement)
namespace["__DABSchema__"] = {}
else: # standard inheritance
# check class tree origin
if "__DABSchema__" not in dir(bases[0]):
raise BrokenInheritance("__DABSchema__ not found in base class, broken inheritance chain.")
# copy inherited schema
namespace["__DABSchema__"] = copy(bases[0].__DABSchema__)
# force field without default value to be instantiated (with None)
if "__annotations__" in namespace:
for _funknown in [_ for _ in namespace["__annotations__"] if _ not in namespace.keys()]:
namespace[_funknown] = None
@classmethod
def pre_processing_modified(
mcs: type["BaseMeta"], name: str, bases: tuple[type[Any], ...], namespace: dict[str, Any], _fname: str, _fvalue: Any
): # pylint: disable=unused-argument
"""preprocessing BaseElement modified Fields"""
# print(f"Modified field: {_fname}")
if "__annotations__" in namespace and _fname in namespace["__annotations__"]:
raise ReadOnlyFieldAnnotation("annotations cannot be modified on derived classes")
try:
check_type(
_fvalue,
namespace["__DABSchema__"][_fname].annotations,
collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS,
)
except TypeCheckError as exp:
raise InvalidFieldValue(
f"Field <{_fname}> New Field value is not of expected type {bases[0].__annotations__[_fname]}."
) from exp
mcs.modified_field[_fname] = _fvalue
@classmethod
def pre_processing_new(
mcs: type["BaseMeta"], name: str, bases: tuple[type[Any], ...], namespace: dict[str, Any], _fname: str, _fvalue: Any
): # pylint: disable=unused-argument
"""preprocessing BaseElement new Fields"""
# print(f"New field: {_fname}")
# print(f"type is: {type(_fvalue)}")
# print(f"value is: {_fvalue}")
# check if field is annotated
if "__annotations__" not in namespace or _fname not in namespace["__annotations__"]:
raise NotAnnotatedField(f"Every dabmodel Fields must be annotated ({_fname})")
# check if annotation is allowed
if isinstance(namespace["__annotations__"][_fname], str):
namespace["__annotations__"][_fname] = _resolve_annotation(namespace["__annotations__"][_fname])
if not _check_annotation_definition(namespace["__annotations__"][_fname]):
raise InvalidFieldAnnotation(f"Field <{_fname}> has not an allowed or valid annotation.")
_finfo: DABFieldInfo = DABFieldInfo()
origin = get_origin(namespace["__annotations__"][_fname])
tname = getattr(origin, "__name__", "") or getattr(origin, "__qualname__", "") or str(origin)
if "Annotated" in tname:
args = get_args(namespace["__annotations__"][_fname])
if args:
if len(args) > 2:
raise InvalidFieldAnnotation(f"Field <{_fname}> had invalid Annotated value.")
if len(args) == 2 and not isinstance(args[1], DABFieldInfo):
raise InvalidFieldAnnotation("Only DABFieldInfo object is allowed as Annotated data.")
_finfo = args[1]
# print(f"annotation is: {namespace['__annotations__'][_fname]}")
# check if value is valid
try:
check_type(_fvalue, namespace["__annotations__"][_fname], collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS)
except TypeCheckError as exp:
raise InvalidFieldValue(f"Value of Field <{_fname}> is not of expected type {namespace['__annotations__'][_fname]}.") from exp
mcs.new_fields[_fname] = DABField(_fname, _fvalue, namespace["__annotations__"][_fname], _finfo)
@classmethod
def pre_processing(mcs: type["BaseMeta"], name: str, bases: tuple[type[Any], ...], namespace: dict[str, Any]):
"""preprocessing BaseElement"""
# iterating new and modified fields
mcs.modified_field = {}
mcs.new_fields = {}
mcs.initializer = None
initializer_name: Optional[str] = None
for _fname, _fvalue in namespace.items():
if _fname == f"_{name}__initializer" or (name.startswith("_") and _fname == "__initializer"):
if not isinstance(_fvalue, classmethod):
raise InvalidInitializerType()
mcs.initializer = _fvalue.__func__
if name.startswith("_"):
initializer_name = "__initializer"
else:
initializer_name = f"_{name}__initializer"
elif _fname.startswith("__"):
pass
else:
# print(f"Parsing Field: {_fname} / {_fvalue}")
if len(bases) == 1 and _fname in namespace["__DABSchema__"].keys(): # Modified fields
mcs.pre_processing_modified(name, bases, namespace, _fname, _fvalue)
else: # New fieds
mcs.pre_processing_new(name, bases, namespace, _fname, _fvalue)
# removing modified fields from class (will add them back later)
for _fname in mcs.new_fields:
del namespace[_fname]
for _fname in mcs.modified_field:
del namespace[_fname]
if mcs.initializer is not None and initializer_name is not None:
del namespace[initializer_name]
@classmethod
def call_initializer(
mcs: type["BaseMeta"], cls, name: str, bases: tuple[type[Any], ...], namespace: dict[str, Any]
): # pylint: disable=unused-argument
"""BaseElement initializer processing"""
if mcs.initializer is not None:
init_fieldvalues = {}
init_fieldtypes = {}
for _fname, _fvalue in cls.__DABSchema__.items():
init_fieldvalues[_fname] = deepcopy(_fvalue.raw_value)
init_fieldtypes[_fname] = _fvalue.annotations
fakecls = ModelSpecView(init_fieldvalues, init_fieldtypes, cls.__name__, cls.__module__)
safe_globals = {"__builtins__": {"__import__": _blocked_import}, **ALLOWED_HELPERS_DEFAULT}
if mcs.initializer.__code__.co_freevars:
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,
)
safe_initializer(fakecls) # pylint: disable=not-callable
for _fname, _fvalue in fakecls.export().items():
try:
check_type(_fvalue, cls.__DABSchema__[_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)
def __new__(mcs: type["BaseMeta"], name: str, bases: tuple[type[Any], ...], namespace: dict[str, Any]) -> Type:
"""BaseElement new class"""
mcs.pre_check(name, bases, namespace)
mcs.pre_processing(name, bases, namespace)
orig_setattr = namespace.get("__setattr__", object.__setattr__)
def guarded_setattr(self, key, value):
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 hasattr(self, key):
raise ReadOnlyField(f"{key} is read-only")
else:
raise NewFieldForbidden("creating new fields is not allowed")
return orig_setattr(self, key, value)
namespace["__setattr__"] = guarded_setattr
_cls = super().__new__(mcs, name, bases, namespace)
for _fname, _fvalue in mcs.modified_field.items():
_cls.__DABSchema__[_fname] = deepcopy(bases[0].__DABSchema__[_fname])
_cls.__DABSchema__[_fname].update_value(_fvalue)
for _fname, _fvalue in mcs.new_fields.items():
_fvalue.add_source(mcs)
_cls.__DABSchema__[_fname] = _fvalue
mcs.call_initializer(_cls, name, bases, namespace)
return _cls
def __call__(cls, *args: Any, **kw: Any): # intentionally untyped
"""BaseElement new instance"""
obj = super().__call__(*args, **kw)
for _fname, _fvalue in cls.__DABSchema__.items():
setattr(obj, _fname, _fvalue)
inst_schema: dict[str, Any] = {}
for _fname, _fvalue in cls.__DABSchema__.items():
inst_schema[_fname] = FrozenDABField(_fvalue)
setattr(obj, "__DABSchema__", inst_schema)
return obj
class BaseElement(metaclass=BaseMeta):
"""BaseElement class
Base class to apply metaclass and set common Fields.
"""
class BaseFeature(BaseElement):
"""BaseFeature class
Base class for Appliance's Features.
Features are optional traits of an appliance.
"""
class BaseAppliance(BaseElement):
"""BaseFeature class
Base class for Appliance.
An appliance is a server configuration / image that is built using appliance's code and Fields.
"""

View File

@@ -1,11 +1,19 @@
"""library's internal tools"""
from typing import Any, get_origin, get_args
from uuid import UUID
from datetime import datetime
import json
import inspect
from frozendict import deepfreeze
from .defines import (
ALLOWED_ANNOTATIONS,
)
class DABJSONEncoder(json.JSONEncoder):
class LAMJSONEncoder(json.JSONEncoder):
"""allows to JSON encode non supported data type"""
def default(self, o):
@@ -15,3 +23,47 @@ class DABJSONEncoder(json.JSONEncoder):
if isinstance(o, datetime):
return str(o)
return json.JSONEncoder.default(self, o)
def LAMdeepfreeze(value):
"""recursive freeze helper function"""
if isinstance(value, dict):
return deepfreeze(value)
if isinstance(value, set):
return frozenset(LAMdeepfreeze(v) for v in value)
if isinstance(value, list):
return tuple(LAMdeepfreeze(v) for v in value)
if isinstance(value, tuple):
return tuple(LAMdeepfreeze(v) for v in value)
return value
def is_data_attribute(name: str, value: any) -> bool:
if name.startswith("__") and name.endswith("__"):
return False
if isinstance(value, (staticmethod, classmethod, property)):
return False
if inspect.isfunction(value) or inspect.isclass(value) or inspect.ismethoddescriptor(value):
return False
return True
def _peel_annotated(t: Any) -> Any:
# If you ever allow Annotated[T, ...], peel to T
while True:
origin = get_origin(t)
if origin is None:
return t
name = getattr(origin, "__name__", "") or getattr(origin, "__qualname__", "") or str(origin)
if "Annotated" in name:
args = get_args(t)
t = args[0] if args else t
else:
return t
def _resolve_annotation(ann):
if isinstance(ann, str):
# Safe eval against a **whitelist** only
return eval(ann, {"__builtins__": {}}, ALLOWED_ANNOTATIONS) # pylint: disable=eval-used
return ann

File diff suppressed because it is too large Load Diff

414
test/test_element.py Normal file
View File

@@ -0,0 +1,414 @@
# 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_class_frozen(self):
class E(dm.Element):
ivalue: int = 43
strvalue: str = "test"
fvalue: float = 1.4322
ar_int: list[int] = [1, 54, 65]
with self.assertRaises(dm.ReadOnlyField):
E.ivalue = 3
with self.assertRaises(dm.ReadOnlyField):
E.strvalue = "toto"
with self.assertRaises(dm.ReadOnlyField):
E.fvalue = 3.14
with self.assertRaises(AttributeError):
E.ar_int.append(5)
def test_instance_frozen(self):
class E(dm.Element):
ivalue: int = 43
strvalue: str = "test"
fvalue: float = 1.4322
ar_int: list[int] = [1, 54, 65]
e = E()
with self.assertRaises(dm.ReadOnlyField):
e.ivalue = 3
with self.assertRaises(dm.ReadOnlyField):
e.strvalue = "toto"
with self.assertRaises(dm.ReadOnlyField):
e.fvalue = 3.14
with self.assertRaises(AttributeError):
e.ar_int.append(5)
def test_composition_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))
def test_method(self):
class E(dm.Element):
ivalue: int = 43
def get_increment(self) -> int:
return self.ivalue + 1
def increment(self) -> int:
return type(self)(ivalue=self.ivalue + 1)
class A(dm.Appliance):
elem: E = E(ivalue=45)
a = A()
self.assertIsInstance(a.elem, E)
self.assertEqual(a.elem.ivalue, 45)
self.assertEqual(a.elem.get_increment(), 46)
class B(A):
@classmethod
def __initializer(cls):
cls.elem = cls.elem.increment()
b = B()
self.assertEqual(b.elem.ivalue, 46)
self.assertEqual(b.elem.get_increment(), 47)
def test_initializer_appliance_function_forbidden(self):
def test_fun() -> int:
return 12
class E(dm.Element):
ivalue: int = 43
with self.assertRaises(dm.FunctionForbidden):
class B(dm.Element):
elem: E = E()
@classmethod
def __initializer(cls):
cls.elem.ivalue = test_fun()
with self.assertRaises(dm.FunctionForbidden):
class B(dm.Element):
elem: E = E()
@classmethod
def __initializer(cls):
print("COUCOU")
cls.elem = E(ivalue=test_fun())
def test_mutable_class_freeze(self):
class E(dm.Element, options=(dm.ClassMutable)):
ivalue: int = 43
E.ivalue = 12
self.assertEqual(E.ivalue, 12)
E.freeze_class()
self.assertEqual(E.ivalue, 12)
with self.assertRaises(dm.ReadOnlyField):
E.ivalue = 13
e = E()
self.assertEqual(e.ivalue, 12)
def test_mutable_class2_newelement_fails(self):
class E(dm.Element, options=(dm.ClassMutable)):
ivalue: int = 43
with self.assertRaises(dm.NonExistingField):
E.test = 123
# ---------- main ----------
if __name__ == "__main__":
unittest.main()

653
test/test_feature.py Normal file
View File

@@ -0,0 +1,653 @@
# 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.__lam_schema__.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.__lam_schema__["VarStrOuter"], dm.LAMField)
self.assertTrue(app1.__lam_schema__["VarStrOuter"].is_frozen())
self.assertIn("Feature1", app1.__lam_schema__["features"])
self.assertIn("VarStrInner", app1.__lam_schema__["features"]["Feature1"].__lam_schema__)
self.assertIsInstance(
app1.__lam_schema__["features"]["Feature1"].__lam_schema__["VarStrInner"],
dm.LAMField,
)
self.assertTrue(hasattr(app1, "Feature1"))
self.assertTrue(app1.Feature1.__lam_schema__["VarStrInner"].is_frozen())
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.__lam_schema__["VarStrOuter"], dm.LAMField)
self.assertTrue(app1.__lam_schema__["VarStrOuter"].is_frozen())
self.assertIn("Feature1", app1.__lam_schema__["features"])
self.assertIn("VarStrInner", app1.__lam_schema__["features"]["Feature1"].__lam_schema__)
self.assertIsInstance(
app1.__lam_schema__["features"]["Feature1"].__lam_schema__["VarStrInner"],
dm.LAMField,
)
self.assertTrue(hasattr(app1, "Feature1"))
self.assertTrue(app1.Feature1.__lam_schema__["VarStrInner"].is_frozen())
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_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_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_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)
print(b.F1)
print(dir(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()

166
test/test_feature_bind.py Normal file
View File

@@ -0,0 +1,166 @@
# 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,
)
from dabmodel.appliance import Appliance
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 FeatureBindTest(unittest.TestCase):
def setUp(self):
print("\n->", unittest.TestCase.id(self))
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.assertIn("Feature1", app1.__lam_schema__["features"])
self.assertTrue(hasattr(app1, "Feature1"))
def test_outside_bind(self):
"""Testing first appliance feature, and Field types (simple)"""
# class can be created
class Appliance1(dm.Appliance):
pass
class Feature1(dm.Feature, appliance=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_outside_bind2(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"
Feature1.bind_appliance(Appliance1)
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_bind_inheritance_no_leak(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"
class Feature2(Feature1, appliance=Appliance1):
VarStrInner = "testvalue FEATURE2"
app = Appliance1(feat=Feature2)
self.assertEqual(app.feat.VarStrInner, "testvalue FEATURE2")
with self.assertRaises(dm.FeatureNotBound):
app = Appliance1(feat=Feature1)
def test_bind_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_bind_defect(self):
class Feature1(dm.Feature):
pass
with self.assertRaises(dm.FeatureNotBound):
Feature1()
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_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)
# ---------- main ----------
if __name__ == "__main__":
unittest.main()