Compare commits

...

51 Commits

Author SHA1 Message Date
cclecle
25d5339946 work 2025-09-28 21:18:35 +02:00
cclecle
6efd914de1 non finished work 2025-09-28 17:33:34 +02:00
cclecle
2e81b3f0e6 work 2025-09-27 23:58:47 +02:00
cclecle
5d206ef266 fix Feature + cleaning 2025-09-27 22:00:47 +02:00
chacha
ba993b14b9 work 2025-09-27 16:41:36 +02:00
chacha
5232ad66fc some progress 2025-09-26 00:50:52 +02:00
chacha
44554006db work 2025-09-25 01:40:05 +02:00
chacha
1e84fd691e clean 2025-09-24 19:10:03 +02:00
chacha
2ac931edeb more work 2025-09-24 02:16:10 +02:00
chacha
7ffe05f514 clean 2025-09-23 22:43:39 +02:00
chacha
100d2cadcb renaming 2025-09-23 22:27:00 +02:00
chacha
71faefcf68 code cleaning 2025-09-23 22:10:03 +02:00
chacha
23f14a042d make metaclass thread safer 2025-09-23 22:06:02 +02:00
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
cclecle
e11c541139 small opt 2025-09-06 01:35:28 +02:00
cclecle
637b50b325 quality & typing fixes 2025-09-06 01:31:55 +02:00
cclecle
f45c9cc8f3 fix unittest 2025-09-05 23:04:16 +02:00
cclecle
95b0c298ce update deps 2025-09-05 23:00:36 +02:00
cclecle
04a4cf7b36 add deps 2025-09-05 22:56:17 +02:00
cclecle
f42a839cff work 2025-09-05 22:53:47 +02:00
cclecle
7f3a4ef545 work 2025-09-03 23:15:04 +02:00
cclecle
608c8a1010 work 2025-09-03 01:14:09 +02:00
cclecle
210781f086 new again 2025-09-02 19:18:31 +02:00
cclecle
df966ccac4 Merge branch 'dev' of https://chacha.ddns.net/gitea/chacha/dabmodel.git
into dev
2025-09-01 00:07:22 +02:00
chacha
29827b51bc add chatgpt code :) 2025-08-31 22:19:58 +02:00
chacha
7440731135 dev 2025-08-31 21:23:03 +02:00
cclecle
87682c2c9c cleaning a little bit... 2025-01-08 18:00:33 +01:00
chacha
0eef35e36f continue work 2024-12-08 01:04:27 +01:00
28 changed files with 6759 additions and 545 deletions

View File

@@ -1,13 +1,20 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?eclipse-pydev version="1.0"?><pydev_project>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python interpreter</pydev_property>
<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
<path>/${PROJECT_DIR_NAME}/src</path>
<path>/${PROJECT_DIR_NAME}</path>
</pydev_pathproperty>
</pydev_project>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?eclipse-pydev version="1.0"?><pydev_project>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Python311</pydev_property>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python interpreter</pydev_property>
<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
<path>/${PROJECT_DIR_NAME}/src</path>
<path>/${PROJECT_DIR_NAME}</path>
</pydev_pathproperty>
</pydev_project>

View File

@@ -34,7 +34,9 @@ classifiers = [
]
dependencies = [
'importlib-metadata; python_version<"3.9"',
'packaging'
'packaging',
'frozendict',
'typeguard'
]
dynamic = ["version"]
@@ -78,7 +80,7 @@ test = ["chacha_cicd_helper"]
coverage-check = ["chacha_cicd_helper"]
complexity-check = ["chacha_cicd_helper"]
quality-check = ["chacha_cicd_helper"]
type-check = ["chacha_cicd_helper"]
type-check = ["chacha_cicd_helper","types-pytz"]
doc-gen = ["chacha_cicd_helper"]
# [project.scripts]

View File

@@ -11,4 +11,35 @@ Main module __init__ file.
"""
from .__metadata__ import __version__, __Summuary__, __Name__
from .model import DABField, BaseFeature, BaseAppliance, default_values_override
from .meta.element import ClassMutable, ObjectMutable
from .element import Element
from .lam_field.lam_field import LAMField
from .lam_field.lam_field_info import LAMFieldInfo
# from .LAMFields.FrozenLAMField import FrozenLAMField
from .appliance import Appliance
from .feature import Feature
from .exception import (
DABModelException,
MultipleInheritanceForbidden,
BrokenInheritance,
ReadOnlyField,
NotAnnotatedField,
ReadOnlyFieldAnnotation,
InvalidFieldValue,
InvalidFieldAnnotation,
IncompletelyAnnotatedField,
ImportForbidden,
FunctionForbidden,
InvalidFeatureInheritance,
FeatureNotBound,
UnsupportedFieldType,
NonExistingField,
InvalidFieldName,
)
__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"

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

@@ -0,0 +1,71 @@
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 validate_schema(self):
super().validate_schema()
for k in self.__lam_schema__["features"]:
self.__dict__[k].validate_schema()
@classmethod
def validate_schema_class(cls):
super().validate_schema_class()
print(cls.__lam_schema__["features"])
for v in cls.__lam_schema__["features"].values():
v.validate_schema_class()
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 _validate_unknown_field_schema(cls, name: str):
if name == "features":
return
super()._validate_unknown_field_schema(name)
def _freeze_unknown_attr(self, name: str, force: bool = False):
if isinstance(self.__dict__[name], Feature):
self.__dict__[name].freeze(force)
return
super()._freeze_unknown_attr(name, force)
def _freeze_missing_attr(self, name: str, force: bool = False):
if name == "features":
for k, v in self.__lam_schema__["features"].items():
v.freeze_class(force)
return
super()._freeze_missing_attr(name, force)
@classmethod
def _freeze_unknown_field_schema(cls, name: str, force: bool = False):
if name == "features":
for v in cls.__lam_schema__["features"].values():
v.freeze_class(force)
return
super()._freeze_unknown_field_schema(name, force)
@classmethod
def _validate_unknown_attr_class(cls, name: str) -> None:
if issubclass(cls.__dict__[name], Feature):
return
super()._validate_unknown_attr_class(name)
@classmethod
def _freeze_unknown_attr_class(cls, name: str, force: bool = False) -> None:
if issubclass(cls.__dict__[name], Feature):
return
super()._freeze_unknown_attr_class(name, force)

View File

@@ -0,0 +1,253 @@
from typing import Any, Self, Dict, Optional
from typeguard import check_type, CollectionCheckStrategy, TypeCheckError
from copy import deepcopy
from .lam_field.lam_field import LAMField, LAMField_Element
from .exception import ReadOnlyField, SchemaViolation, NonExistingField, InvalidFieldValue
from .tools import LAMdeepfreeze, is_data_attribute
'''
class ElementView:
__slots__ = ("_vals", "_types", "_touched")
def __init__(
self,
values: dict[str, Any],
types_map: dict[str, type],
):
self._vals: dict[str, Any]
self._types: dict[str, type]
self._touched: set
object.__setattr__(self, "_vals", dict(values))
object.__setattr__(self, "_types", types_map)
def __getattr__(self, name: str) -> Any:
"""internal proxy getattr"""
if name not in self._types:
raise NonExistingField(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)
class ElementViewCls:
__slots__ = ("_vals", "_types", "_touched", "_name", "_module")
def __init__(
self,
values: dict[str, Any],
types_map: dict[str, type],
name: Optional[str] = None,
module: Optional[str] = None,
):
super().__init__(values, types_map)
self._name: str
self._module: str
if name is not None:
object.__setattr__(self, "_name", name)
if module is not None:
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
'''
class BaseElement:
__lam_schema__ = {}
__lam_initialized__ = False
__lam_class_mutable__ = False
__lam_object_mutable__ = False
__lam_options__ = {}
"""
def get_model_spec(self, name: str, module: str, memo: dict[str, Any]) -> ElementView:
# memo[self.__name__] = {}
init_fieldvalues = {}
init_fieldtypes = {}
for k, v in self.__lam_schema__.items():
if isinstance(v, LAMField_Element):
memo[k] = {}
init_fieldvalues[k] = v.value.get_model_spec(memo[k])
# clone = v.clone_unfrozen().value
elif isinstance(v, LAMField):
clone = deepcopy(v.value)
init_fieldvalues[k] = clone
else:
pass
init_fieldtypes[k] = v.annotations
return ElementView(init_fieldvalues, init_fieldtypes)
@classmethod
def get_model_spec_cls(cls, memo: dict[str, Any]) -> ElementView:
# memo[self.__name__] = {}
init_fieldvalues = {}
init_fieldtypes = {}
for k, v in cls.__lam_schema__.items():
if isinstance(v, LAMField_Element):
memo[k] = {}
init_fieldvalues[k] = v.value.get_model_spec(k, memo[k])
# clone = v.clone_unfrozen().value
elif isinstance(v, LAMField):
clone = deepcopy(v.value)
init_fieldvalues[k] = clone
else:
pass
init_fieldtypes[k] = v.annotations
return ElementViewCls(init_fieldvalues, init_fieldtypes, cls.__name__, cls.__modules__)
"""
def clone_as_mutable_variant(self, *, deep: bool = True, _memo: Dict[int, Self] | None = None) -> Self:
raise NotImplemented()
@classmethod
@property
def frozen_cls(cls) -> bool:
return not cls.__lam_class_mutable__
@classmethod
@property
def mutable_obj(cls) -> bool:
return cls.__lam_object_mutable__
@property
def frozen(self) -> bool:
return not self.__lam_object_mutable__
def __setattr__(self, key: str, value: Any) -> None:
if key.startswith("_"):
return super().__setattr__(key, value)
if key not in self.__lam_schema__:
raise NonExistingField(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) -> None:
if self.__lam_object_mutable__ or force:
if self.__lam_object_mutable__:
self.validate_schema()
setSchemaKeys = set(self.__lam_schema__)
setInstanceKeys = {_[0] for _ in self.__dict__.items() if is_data_attribute(_[0], _[1])}
for k_unknown in setInstanceKeys - setSchemaKeys:
self._freeze_unknown_attr(k_unknown, force)
for k_missing in setSchemaKeys - setInstanceKeys:
self._freeze_missing_attr(k_missing, force)
for k in list(setSchemaKeys & setInstanceKeys):
self.__lam_schema__[k].freeze()
if isinstance(self.__dict__[k], BaseElement):
self.__dict__[k].freeze(force)
else:
self.__dict__[k] = LAMdeepfreeze(self.__dict__[k])
self.__lam_object_mutable__ = False
def _freeze_unknown_attr(self, name: str, force: bool = False) -> None:
raise SchemaViolation(f"Attribute <{name}> is not in the schema")
def _freeze_missing_attr(self, name: str, force: bool = False) -> None:
raise SchemaViolation(f"Attribute <{name}> is missing from instance")
def validate_schema(self) -> None:
setSchemaKeys = set(self.__lam_schema__)
setInstanceKeys = {_[0] for _ in self.__dict__.items() if is_data_attribute(_[0], _[1])}
for k_unknown in setInstanceKeys - setSchemaKeys:
self._validate_schema_unknown_attr(k_unknown)
for k_missing in setSchemaKeys - setInstanceKeys:
self._validate_schema_missing_attr(k_missing)
for k in list(setSchemaKeys & setInstanceKeys):
self.__lam_schema__[k].validate_self()
def _validate_schema_unknown_attr(self, name: str) -> None:
raise SchemaViolation(f"Attribute <{name}> is not in the schema")
def _validate_schema_missing_attr(self, name: str) -> None:
raise SchemaViolation(f"Attribute <{name}> is missing from instance")
@classmethod
def freeze_class(cls, force: bool = False) -> None:
if cls.__lam_class_mutable__ or force:
cls.validate_schema_class()
# class should not have any elements so they are all unknown
for k_unknown in {_[0] for _ in cls.__dict__.items() if is_data_attribute(_[0], _[1])}:
cls._freeze_unknown_attr_class(k_unknown, force)
for k, v in cls.__lam_schema__.items():
if isinstance(v, LAMField):
cls.__lam_schema__[k].freeze()
else:
cls._freeze_unknown_field_schema(k, force)
cls.__lam_class_mutable__ = False
@classmethod
def _freeze_unknown_attr_class(cls, name: str, force: bool = False) -> None:
raise SchemaViolation(f"Class attribute <{name}> is not in the schema")
@classmethod
def _freeze_unknown_field_schema(cls, name: str, force: bool = False) -> None:
raise SchemaViolation(f"Unknown field <{name} in the schema> ")
@classmethod
def validate_schema_class(cls) -> None:
# class should not have any elements so they are all unknown
for k_unknown in {_[0] for _ in cls.__dict__.items() if is_data_attribute(_[0], _[1])}:
cls._validate_unknown_attr_class(k_unknown)
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) -> None:
raise SchemaViolation(f"Class attribute <{name}> is not in the schema")
@classmethod
def _validate_unknown_field_schema(cls, name: str) -> None:
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: set[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.
"""

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

@@ -0,0 +1,146 @@
class DABModelException(Exception):
"""DABModelException Exception class
Base Exception for DABModelException class
"""
class WrongUsage(DABModelException, RuntimeError):
pass
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 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 SchemaViolation(AttributeError, DABModelException):
"""SchemaViolation Exception class
The Element Schema is not respected
"""
class InvalidFieldValue(SchemaViolation):
"""InvalidFieldValue Exception class
The Field value is invalid
"""
class NonExistingField(SchemaViolation):
"""NonExistingField Exception class
The given Field is non existing
"""
class InvalidFieldName(AttributeError, DABModelException):
"""InvalidFieldName Exception class
The Field name is invalid
"""
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 FeatureBoundToIncompatibleAppliance(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,
FeatureBoundToIncompatibleAppliance,
)
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 FeatureBoundToIncompatibleAppliance(
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

@@ -0,0 +1,39 @@
from typing import Generic, TypeVar, Dict, Protocol, runtime_checkable, Self
TV_Freezable = TypeVar("TV_Freezable")
@runtime_checkable
class FreezableElement(Protocol, Generic[TV_Freezable]):
def clone_as_mutable_variant(self, *, deep: bool = True, _memo: Dict[int, Self] | None = None) -> Self:
pass
def freeze(self, force: bool = False) -> None:
pass
@classmethod
def freeze_class(cls, force: bool = False) -> None:
pass
def validate_schema(self) -> None:
pass
@classmethod
def validate_schema_class(cls) -> None:
pass
@classmethod
@property
def frozen_cls(cls) -> bool:
pass
@classmethod
@property
def mutable_obj(cls) -> bool:
pass
@property
def frozen(self) -> bool:
pass

View File

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,176 @@
from typing import Generic, TypeVar, Optional, Any, Self, Annotated, get_origin, get_args
from typeguard import check_type, CollectionCheckStrategy, TypeCheckError
from copy import deepcopy
from .lam_field_info import LAMFieldInfo
from .constraint import Constraint
from ..tools import LAMdeepfreeze
from ..exception import InvalidFieldValue, ReadOnlyField
from ..interfaces import FreezableElement
TV_LABField = TypeVar("TV_LABField")
class LAMField(Generic[TV_LABField]):
"""This class describe a Field in Schema"""
def __init__(self, name: str, val: Optional[TV_LABField], ann: Any, i: LAMFieldInfo):
self._default_value: Optional[TV_LABField]
self._value: Optional[TV_LABField]
self.__annotations: Any
self.__name: str = name
self.__source: Optional[type] = None
self.__info: LAMFieldInfo = deepcopy(i)
self._set_annotations(ann)
self.__frozen: bool = False
self._frozen_value: Any = None
self.__frozen_value_set: True = False
self.validate(val)
self._init_value(val)
def _set_annotations(self, ann: Any) -> None:
_origin = get_origin(ann) or ann
_args = get_args(ann)
if _origin is Annotated:
self.__annotations: Any = LAMdeepfreeze(_args[0])
else:
self.__annotations: Any = LAMdeepfreeze(ann)
def _init_value(self, val: Optional[TV_LABField | FreezableElement]):
self._default_value: Optional[TV_LABField] = deepcopy(val)
self._value: Optional[TV_LABField] = val
@property
def name(self) -> str:
return self.__name
def is_frozen(self) -> bool:
return self.__frozen
def freeze(self):
self.__frozen = True
def clone_unfrozen(self) -> Self:
field = LAMFieldFactory.create_field(self.__name, self._default_value, self.__annotations, self.__info)
field.update_value(self._value)
return field
def add_source(self, src: type) -> None:
"""Adds source Appliance to the Field"""
if self.__frozen:
raise ReadOnlyField("Field is frozen, cannot add source now")
self.__source = src
@property
def doc(self) -> str:
"""Returns Field's documentation"""
return self.__info.doc
def add_constraint(self, cons: Constraint) -> None:
"""Adds constraint to the Field"""
if self.__frozen:
raise ReadOnlyField("Field is frozen")
self.__info.add_constraint(cons)
@property
def constraints(self) -> list[Constraint]:
"""Returns Field's constraint"""
return LAMdeepfreeze(self.__info.constraints)
def validate_self(self):
self.validate(self._value)
def validate(self, val: Optional[TV_LABField]):
try:
check_type(
val,
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, val: Optional[TV_LABField] = None) -> None:
"""Updates Field's value"""
if self.__frozen:
raise ReadOnlyField("Field is frozen")
self.validate(val)
self._value = val
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
def _generate_frozen_value(self):
self._frozen_value = LAMdeepfreeze(self._value)
@property
def frozen_value(self) -> Any:
if not self.__frozen_value_set:
self._generate_frozen_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
class LAMField_Element(LAMField[FreezableElement]):
def _init_value(self, val: Optional[FreezableElement]):
self._default_value = deepcopy(val)
self._default_value.freeze()
self._value = val.clone_as_mutable_variant()
def validate(self, val: Optional[FreezableElement]):
super().validate(val)
if val is not None:
print(val)
val.validate_schema()
@property
def default_value(self) -> Any:
return self._default_value
def update_value(self, val: Optional[FreezableElement] = None) -> None:
super().update_value(val.clone_as_mutable_variant())
def _generate_frozen_value(self):
self._frozen_value = deepcopy(self._value)
self._frozen_value.freeze()
class LAMFieldFactory:
@staticmethod
def create_field(name: str, val: Optional[TV_LABField], anno: Any, info: LAMFieldInfo) -> LAMField:
if isinstance(val, FreezableElement):
print(f"Spawn LAMField_Element {name} !!!")
return LAMField_Element(name, val, anno, info)
else:
print(f"Spawn LAMField {name} !!!")
return LAMField(name, val, anno, info)

View File

@@ -0,0 +1,27 @@
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
def add_constraint(self, constraint: Constraint):
self.__constraints.append(constraint)
@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

@@ -0,0 +1,242 @@
from typing import Any, Type
from frozendict import frozendict
from copy import copy
from .element import _MetaElement, get_mutable_variant
from ..feature import Feature
from ..exception import InvalidFieldValue, InvalidFeatureInheritance, InvalidFieldName
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
stack_exts: 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, stack_exts) # 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],
stack_exts: dict[str, Any],
):
super().inherit_schema(name, base, namespace, stack_exts)
if "features" in base.__lam_schema__:
namespace["__lam_schema__"]["features"] = dict(base.__lam_schema__["features"])
@classmethod
def process_class_fields(
mcs: type["meta"],
name: str,
bases: tuple[type[Any], ...],
namespace: dict[str, Any],
stack_exts: dict[str, Any],
):
"""
Like meta.process_class_fields but also stages Feature declarations.
Initializes:
stack_exts["new_features"], stack_exts["modified_features"]
then defers to the base scanner for regular fields.
"""
stack_exts["new_features"] = {}
stack_exts["modified_features"] = {}
super().process_class_fields(name, bases, namespace, stack_exts) # 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,
stack_exts: 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 == "features":
raise InvalidFieldName("'feature' is a reserved Field name")
if _fname in namespace["__lam_schema__"]["features"]:
if not issubclass(_fvalue, namespace["__lam_schema__"]["features"][_fname]):
raise InvalidFeatureInheritance(f"Feature {_fname} is not a subclass of {bases[0]}.{_fname}")
stack_exts["modified_features"][_fname] = get_mutable_variant(_fvalue)
namespace[_fname] = stack_exts["modified_features"][_fname]
elif isinstance(_fvalue, type) and issubclass(_fvalue, Feature):
stack_exts["new_features"][_fname] = get_mutable_variant(_fvalue)
namespace[_fname] = stack_exts["new_features"][_fname]
else:
super().process_new_field(name, bases, namespace, _fname, _fvalue, stack_exts) # 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
stack_exts: 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, stack_exts) # type: ignore[misc]
cls.__lam_schema__["features"].update(stack_exts["modified_features"])
for v in stack_exts["new_features"].values():
v.bind_appliance(cls)
cls.__lam_schema__["features"].update(stack_exts["new_features"])
@classmethod
def prepare_initializer_fields(
mcs: type["_MetaElement"],
cls,
name: str,
bases: tuple[type[Any], ...],
namespace: dict[str, Any], # pylint: disable=unused-argument
init_fieldvalues: dict[str, Any],
init_fieldtypes: dict[str, Any],
stack_exts: dict[str, Any],
):
for k, v in cls.__lam_schema__["features"].items():
init_fieldvalues[k] = v
init_fieldtypes[k] = v
@classmethod
def commit_initializer_fields(
mcs: type["_MetaElement"],
cls,
name: str,
bases: tuple[type[Any], ...],
namespace: dict[str, Any], # pylint: disable=unused-argument
fakecls_exports: dict[str, Any],
stack_exts: dict[str, Any],
):
for fk, fv in cls.__lam_schema__["features"].items():
for k, v in fv.__lam_schema__.items():
v.update_value(fakecls_exports[fk].__getattr__(k))
@classmethod
def finalize_class(
mcs: type["_MetaElement"],
cls,
name: str,
bases: tuple[type[Any], ...],
namespace: dict[str, Any], # pylint: disable=unused-argument
stack_exts: dict[str, Any],
):
cls.__lam_schema__["features"] = frozendict(cls.__lam_schema__["features"])
super().finalize_class(cls, name, bases, namespace, stack_exts)
if not cls.__lam_class_mutable__:
for feat in cls.__lam_schema__["features"].values():
feat.freeze_class(True)
def populate_instance(cls: Type, obj: Any, stack_exts: dict[str, Any], *args: Any, **kw: Any):
super().populate_instance(obj, stack_exts, *args, **kw)
obj.__lam_schema__["features"] = dict(cls.__lam_schema__["features"])
def apply_overrides(cls, obj, stack_exts, *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 obj.__lam_schema__["features"]:
base_feat_cls = obj.__lam_schema__["features"][k]
# Case 1: subclass replacement (inheritance)
if isinstance(v, type) and issubclass(v, base_feat_cls):
v.check_appliance_compatibility(cls)
# record subclass into instance schema
obj.__lam_schema__["features"][k] = get_mutable_variant(v)
if not obj.__lam_class_mutable__:
obj.__lam_schema__["features"][k].freeze_class(True)
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] = get_mutable_variant(v)
if not obj.__lam_class_mutable__:
obj.__lam_schema__["features"][k].freeze_class(True)
kwargs.pop(k)
super().apply_overrides(obj, stack_exts, *args, **kwargs)
def finalize_instance(cls: Type, obj, stack_exts: 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 k, v in obj.__lam_schema__["features"].items():
# Case 1: plain class or subclass
if isinstance(v, type) and issubclass(v, Feature):
inst = v()
object.__setattr__(obj, k, inst)
# Case 2: (class, dict) → dict overrides
elif isinstance(v, tuple) and len(v) == 2:
feat_cls, overrides = v
inst = feat_cls(**overrides)
object.__setattr__(obj, k, inst)
obj.__lam_schema__["features"][k] = feat_cls
else:
raise InvalidFieldValue(f"Invalid feature definition stored for '{k}': {fdef!r}")
obj.__lam_schema__["features"] = frozendict(obj.__lam_schema__["features"])
super().finalize_instance(obj, stack_exts)

View File

@@ -0,0 +1,844 @@
from typing import Optional, TypeVar, get_origin, get_args, Any, Type, Union, Dict, Annotated
from types import FunctionType, UnionType, new_class
from copy import deepcopy, copy
import sys
import weakref
import inspect, ast, textwrap
from typeguard import check_type, TypeCheckError, CollectionCheckStrategy
from frozendict import frozendict
from ..tools import _resolve_annotation
from ..lam_field.lam_field import LAMFieldFactory, LAMField, LAMField_Element
from ..lam_field.lam_field_info import LAMFieldInfo
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,
WrongUsage,
)
# Cache per base class -> mutable variant
_MUTABLE_VARIANTS = weakref.WeakKeyDictionary()
def _merge_options(base_opts: tuple, *adds) -> tuple:
merged = list(base_opts)
for a in adds:
if a not in merged:
merged.append(a)
return tuple(merged)
class IMutableVariant:
pass
def get_mutable_variant(base: Type[BaseElement]) -> Type[BaseElement]:
"""
Return a subclass of `base` that behaves the same except that instances
are created object-mutable (because the class was defined with options=(ObjectMutable,)).
"""
print(f"____ Walking through: {base}")
print(f"bases: {base.__bases__}")
if base in (IElement, IFeature, IAppliance):
return base
if base.mutable_obj or issubclass(base, IMutableVariant):
print("already mutable")
return base # already mutable
cached = _MUTABLE_VARIANTS.get(base)
if cached:
print("cached")
return cached
meta = type(base) # keep the same metaclass
base_opts = getattr(base, "__lam_options__", ())
new_opts = _merge_options(base_opts, ObjectMutable, ClassMutable, _MutableClone)
# Recursively lift each direct base
lifted_bases = []
root_base: Optional[type[BaseElement]] = None
for b in base.__bases__:
if b in (IElement, IFeature, IAppliance):
if root_base:
raise BrokenInheritance(f"Multiple exclusive root bases (previous {root_base}, now {b}")
else:
root_base = b
elif b is BaseElement:
raise BrokenInheritance("BaseElement must not be used")
elif issubclass(b, IMutableVariant):
lifted_bases.append(b)
elif issubclass(b, BaseElement) and b is not BaseElement:
lifted_bases.append(get_mutable_variant(b))
print(f"lifted_bases: {lifted_bases}")
# Keep original behavior (inherit from C), AND attach lifted base variants
if len(lifted_bases) == 0:
if root_base is None:
raise BrokenInheritance("inheritance root not found")
bases = (root_base,)
else:
bases = tuple(lifted_bases) + (base,) # ensures B' is subclass of B and A'
if IMutableVariant not in bases:
bases = bases + (IMutableVariant,)
name = f"{base.__name__}__Mutable"
def body(ns: dict[str, Any]) -> None:
ns["__module__"] = base.__module__
ns["__annotations__"] = base.__annotations__
# ns["__lam_class_mutable__"] = True
# ns["__lam_object_mutable__"] = True
ns["__qualname__"] = f"{base.__qualname__}__Mutable"
ns["__doc__"] = f"Mutable runtime variant of {base.__qualname__}"
for fname, fval in base.__lam_schema__.items():
if isinstance(fval, LAMField):
# v = getattr(base, fname)
ns[fname] = fval.clone_unfrozen().value
# IMPORTANT: pass options via kwds so your meta receives them
print(f"CREATING {name}")
variant = new_class(
name,
bases,
{"metaclass": meta, "options": new_opts},
body,
)
# Optional: register for pickling/import
sys.modules[base.__module__].__dict__[name] = variant
_MUTABLE_VARIANTS[base] = variant
return variant
class IBaseElement(BaseElement):
def clone_as_mutable_variant(self, *, deep: bool = True, _memo: Dict[int, BaseElement] | None = None) -> BaseElement:
if isinstance(self, type):
raise WrongUsage("clone_as_mutable_variant can only be applied to an instance")
if self.mutable_obj:
return self # already mutable
if _memo is None:
_memo = {}
sid = id(self)
if sid in _memo:
return _memo[sid]
dst_cls = get_mutable_variant(type(self))
# Create a fresh instance using the normal constructor path
dst = dst_cls() # your meta will populate defaults; we'll overwrite below
_memo[sid] = dst
for fname, fval in self.__lam_schema__.items():
if isinstance(fval, LAMField):
v = getattr(self, fname)
if isinstance(v, BaseElement):
if deep:
v = v.clone_as_mutable_variant(deep=True, _memo=_memo)
setattr(dst, fname, v)
else:
setattr(dst, fname, fval.clone_unfrozen().value)
# dst.__dict__[fname] = v
return dst
class IElement(IBaseElement): ...
class IFeature(IBaseElement): ...
class IAppliance(IBaseElement): ...
def _check_annotation_definition(_type, first_layer: bool = True): # pylint: disable=too-complex,too-many-return-statements
print(f"_type={_type}, {first_layer}")
_origin = get_origin(_type) or _type
_args = get_args(_type)
# handle Annotated[,]
if _origin is Annotated:
if not first_layer:
raise UnsupportedFieldType("Annotated[] is only supported as parent annotation")
return _check_annotation_definition(_args[0], False)
# handle Optional[] and Union[None,...]
if _origin is Union or _origin is UnionType:
if (len(_args) != 2) or (type(None) not in list(_args)) or (not first_layer):
raise UnsupportedFieldType(
"Union[] is only supported to implement Optional (takes 2 parameters) and is only supported as parent annotation"
)
return any(_check_annotation_definition(_, False) for _ in get_args(_type) if _ is not type(None))
# handle Dict[...]
if _origin is dict:
if len(_args) != 2:
raise IncompletelyAnnotatedField(f"Dict Annotation requires 2 inner definitions: {_type}")
if not _args[0] in ALLOWED_MODEL_FIELDS_TYPES:
raise IncompletelyAnnotatedField(f"Dict Key must be simple builtin: {_type}")
# return _check_annotation_definition(_args[1], False)
return any(_check_annotation_definition(_, False) for _ in _args)
# 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], False)
return any(_check_annotation_definition(_, False) 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 any(_check_annotation_definition(_, False) for _ in _args)
if isinstance(_origin, type):
if issubclass(_origin, IElement):
return
elif issubclass(_origin, IAppliance):
raise UnsupportedFieldType(f"Nested Appliance are not supported: {_type}")
if _origin in ALLOWED_MODEL_FIELDS_TYPES:
return
raise UnsupportedFieldType(_origin)
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 NonExistingField(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 ElementOptions:
pass
class ClassMutable(ElementOptions):
pass
class ObjectMutable(ElementOptions):
pass
class _MutableClone(ElementOptions):
pass
class _MetaElement(type):
"""metaclass to use to build BaseElement"""
def __new__(
mcs: type["_MetaElement"],
name: str,
bases: tuple[type[BaseElement], ...],
namespace: dict[str, Any],
**kwargs,
) -> Type:
"""BaseElement new class"""
# create a dict to pass contextual arg onto the stack (multithread safe class init)
stack_exts: dict[str, Any] = {}
stack_exts["kwargs"] = kwargs
# retrieve options and normalize it (always a tuple)
if "options" not in stack_exts["kwargs"]:
stack_exts["kwargs"]["options"] = ()
elif not isinstance(stack_exts["kwargs"]["options"], tuple):
stack_exts["kwargs"]["options"] = (stack_exts["kwargs"]["options"],)
else:
stack_exts["kwargs"]["options"] = stack_exts["kwargs"]["options"]
# main class creation pipeline
mcs.check_class(name, bases, namespace, stack_exts)
mcs.process_class_fields(name, bases, namespace, stack_exts)
_cls = super().__new__(mcs, name, bases, namespace)
mcs.commit_fields(_cls, name, bases, namespace, stack_exts)
mcs.apply_initializer(_cls, name, bases, namespace, stack_exts)
mcs.finalize_class(_cls, name, bases, namespace, stack_exts)
if not _cls.__lam_class_mutable__:
_cls.freeze_class(True)
_cls.__lam_initialized__ = True
return _cls
@classmethod
def check_class(
mcs: type["_MetaElement"],
name: str,
bases: tuple[type[Any], ...],
namespace: dict[str, Any], # pylint: disable=unused-argument
stack_exts: 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}")
if len(bases) > 1:
if _MutableClone not in stack_exts["kwargs"]["options"]:
raise MultipleInheritanceForbidden(f"Multiple inheritance is not supported by dabmodel: {bases}")
if len(bases) == 0:
raise BrokenInheritance(f"missing base class")
if not any([issubclass(base, BaseElement) for base in bases]):
raise BrokenInheritance(f"wrong base class: {bases}")
# handle schema/fields inheritance
mcs.inherit_schema(name, bases[0], namespace, stack_exts)
# force annotated fields without value to be still instantiated (with None)
if "__annotations__" in namespace:
for k_unknown in [_ for _ in namespace["__annotations__"] if _ not in namespace]:
namespace[k_unknown] = None
namespace["__lam_initialized__"] = False
# process options
namespace["__lam_class_mutable__"] = ClassMutable in stack_exts["kwargs"]["options"]
namespace["__lam_object_mutable__"] = ObjectMutable in stack_exts["kwargs"]["options"]
namespace["__lam_options__"] = stack_exts["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],
stack_exts: dict[str, Any],
):
# create a new schema instance
namespace["__lam_schema__"] = {}
# copy elements from the parent class
namespace["__lam_schema__"].update(base.__lam_schema__)
# clone element (unfrozen)
for k, v in namespace["__lam_schema__"].items():
if isinstance(v, LAMField):
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],
stack_exts: 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:
stack_exts["new_fields"], stack_exts["modified_fields"], stack_exts["initializer"]
to be committed later.
"""
# Fields Factory
stack_exts["modified_fields"] = {}
stack_exts["new_fields"] = {}
stack_exts["initializer"] = None
initializer_name: Optional[str] = None
for k, v in namespace.items():
print(f" {name} Processing Field: {k} / {v}")
# handling initializer method
if k == f"_{name}__initializer" or (name.startswith("_") and k == "__initializer"):
if not isinstance(v, classmethod):
raise InvalidInitializerType("__initializer must be a classmethod")
stack_exts["initializer"] = v.__func__
if name.startswith("_"):
initializer_name = "__initializer"
else:
initializer_name = f"_{name}__initializer"
# skipping protected/private/dunder methods and attributes
elif k.startswith("_"):
pass
# skipping classmethods
elif isinstance(v, classmethod):
pass
# skipping methods
elif isinstance(v, FunctionType):
pass
# disallowing nested appliances
elif isinstance(v, IAppliance) or (isinstance(v, type) and issubclass(v, IAppliance)):
raise UnsupportedFieldType(f"Nested Appliance are not supported: {name}:{v}")
# supported Fields
else:
print(f"Staging Field: {k} / {v}")
# Modified fields (already in parent's schema as LAMField)
if k in namespace["__lam_schema__"] and isinstance(namespace["__lam_schema__"][k], LAMField):
mcs.process_modified_field(name, bases, namespace, k, v, stack_exts)
# New fields (others)
else:
mcs.process_new_field(name, bases, namespace, k, v, stack_exts)
# removing processed Fields and initializer from namespace (will add them back in the class later)
for k in stack_exts["new_fields"]:
del namespace[k]
for k in stack_exts["modified_fields"]:
del namespace[k]
if stack_exts["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,
stack_exts: 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 `stack_exts["modified_fields"]`.
"""
# forbid already existing Field's schema from being modified
if "__annotations__" in namespace and _fname in namespace["__annotations__"]:
raise ReadOnlyFieldAnnotation(f"annotations cannot be modified on derived classes {_fname}")
# validate the new value
namespace["__lam_schema__"][_fname].validate(_fvalue)
# stage the new value
stack_exts["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,
stack_exts: 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 `stack_exts["new_fields"]`.
"""
print(f"New field: {_fname} / {_fvalue}")
# forbid non annotated field
if "__annotations__" not in namespace or _fname not in namespace["__annotations__"]:
raise NotAnnotatedField(f"Every dabmodel Fields must be annotated ({_fname})")
# check if annotations' format is correct and save them
if isinstance(namespace["__annotations__"][_fname], str):
namespace["__annotations__"][_fname] = _resolve_annotation(namespace["__annotations__"][_fname])
# effectively checking the value is conform to annotations
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
# extracting LAMFieldInfo from Annotated[] annotations
_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 issubclass(type(args[1]), LAMFieldInfo):
raise InvalidFieldAnnotation("Only LAMFieldInfo object is allowed as Annotated data.")
_finfo = args[1]
# stage the new field
stack_exts["new_fields"][_fname] = LAMFieldFactory.create_field(_fname, _fvalue, namespace["__annotations__"][_fname], _finfo)
@classmethod
def commit_fields(
mcs: type["_MetaElement"],
cls,
name: str,
bases: tuple[type[Any], ...],
namespace: dict[str, Any], # pylint: disable=unused-argument
stack_exts: 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.
"""
# updating values of modified fields
for k, v in stack_exts["modified_fields"].items():
cls.__lam_schema__[k].update_value(v)
# registering new fields
for k, v in stack_exts["new_fields"].items():
v.add_source(cls)
cls.__lam_schema__[k] = v
@classmethod
def prepare_initializer_fields(
mcs: type["_MetaElement"],
cls,
name: str,
bases: tuple[type[Any], ...],
namespace: dict[str, Any], # pylint: disable=unused-argument
init_fieldvalues: dict[str, Any],
init_fieldtypes: dict[str, Any],
stack_exts: dict[str, Any],
):
pass
@classmethod
def commit_initializer_fields(
mcs: type["_MetaElement"],
cls,
name: str,
bases: tuple[type[Any], ...],
namespace: dict[str, Any], # pylint: disable=unused-argument
fakecls_exports: dict[str, Any],
stack_exts: dict[str, Any],
):
pass
@classmethod
def apply_initializer(
mcs: type["_MetaElement"],
cls,
name: str,
bases: tuple[type[Any], ...],
namespace: dict[str, Any], # pylint: disable=unused-argument
stack_exts: 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 stack_exts["initializer"] is not None:
# checking initializer function sanity
_check_initializer_safety(stack_exts["initializer"])
# preparing initializer context values
init_fieldvalues = {}
init_fieldtypes = {}
for k, v in cls.__lam_schema__.items():
if isinstance(v, LAMField):
# clone = v.clone_unfrozen().value
clone = deepcopy(v.value)
init_fieldvalues[k] = clone
init_fieldtypes[k] = v.annotations
mcs.prepare_initializer_fields(cls, name, bases, namespace, init_fieldvalues, init_fieldtypes, stack_exts)
# creating a fake class container to hold the context
fakecls = ModelSpecView(init_fieldvalues, init_fieldtypes, cls.__name__, cls.__module__)
# fakecls = cls
# preparing a fake and safe environment
safe_globals = {
"__builtins__": {"__import__": _blocked_import},
**ALLOWED_HELPERS_DEFAULT,
}
# if stack_exts["initializer"].__code__.co_freevars:
# raise FunctionForbidden("__initializer must not use closures")
# creating the fake call
safe_initializer = FunctionType(
stack_exts["initializer"].__code__,
safe_globals,
name=stack_exts["initializer"].__name__,
argdefs=stack_exts["initializer"].__defaults__,
closure=stack_exts["initializer"].__closure__,
)
# calling initializer
safe_initializer(fakecls) # pylint: disable=not-callable
# copying values back to the class
fakecls_exports = fakecls.export()
for k, v in cls.__lam_schema__.items():
if isinstance(v, LAMField):
cls.__lam_schema__[k].update_value(fakecls_exports[k])
mcs.commit_initializer_fields(cls, name, bases, namespace, fakecls_exports, stack_exts)
@classmethod
def finalize_class(
mcs: type["_MetaElement"],
cls,
name: str,
bases: tuple[type[Any], ...],
namespace: dict[str, Any], # pylint: disable=unused-argument
stack_exts: dict[str, Any],
):
# freezing the field schema
cls.__lam_schema__ = frozendict(cls.__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.")
cls.__lam_schema__[name].update_value(value)
return
def __getattr__(cls, name) -> Any:
if hasattr(cls, "__lam_initialized__") and getattr(cls, "__lam_initialized__") and name in cls.__lam_schema__:
return cls.__lam_schema__[name].value
raise NonExistingField(f"Non existing class attribute: {name}")
def __call__(cls: Type, *args: Any, **kw: Any): # intentionally untyped
"""BaseElement new instance"""
cls.validate_schema_class()
obj = super().__call__(*args)
# create a dict to pass contextual arg onto the stack (multithread safe class init)
stack_exts: dict[str, Any] = {}
# create instance attributes from schema
cls.populate_instance(obj, stack_exts, *args, **kw)
cls.apply_overrides(obj, stack_exts, *args, **kw)
cls.finalize_instance(obj, stack_exts)
return obj
def populate_instance(cls: Type, obj: Any, stack_exts: 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__ = dict(cls.__lam_schema__)
for k, v in obj.__lam_schema__.items():
if isinstance(v, LAMField):
unfrozen_clone = v.clone_unfrozen()
obj.__lam_schema__[k] = unfrozen_clone
if obj.__lam_object_mutable__:
object.__setattr__(obj, k, unfrozen_clone.value)
else:
object.__setattr__(obj, k, unfrozen_clone.frozen_value)
def apply_overrides(cls, obj, stack_exts, *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) ---
print("!!!???????")
for k, v in list(kwargs.items()):
if k in obj.__lam_schema__: # regular field
print(f"??????? {k} {v}")
print(obj.__lam_schema__[k])
if isinstance(obj.__lam_schema__[k], LAMField_Element):
valid_val = True
try:
obj.__lam_schema__[k].validate(v)
except InvalidFieldValue:
valid_val = False
if valid_val:
obj.__lam_schema__[k].update_value(v)
if not obj.__lam_object_mutable__:
object.__setattr__(obj, k, obj.__lam_schema__[k].frozen_value)
else:
object.__setattr__(obj, k, obj.__lam_schema__[k].value)
elif isinstance(v, dict):
raise RuntimeError("initializing Elem with dict is not supported yet")
else:
raise InvalidFieldValue(f"Element override for '{k}' must be a Feature subclass or dict, got {type(v)}")
else:
obj.__lam_schema__[k].update_value(v)
if not obj.__lam_object_mutable__:
object.__setattr__(obj, k, obj.__lam_schema__[k].frozen_value)
else:
object.__setattr__(obj, k, obj.__lam_schema__[k].value)
kwargs.pop(k)
if kwargs:
unknown = ", ".join(sorted(kwargs))
raise InvalidFieldValue(f"Unknown parameters: {unknown}")
def finalize_instance(cls: Type, obj: Any, stack_exts: 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__)
if not obj.__lam_object_mutable__:
for v in obj.__lam_schema__.values():
if isinstance(v, LAMField):
v.freeze()
obj.freeze(True)

View File

@@ -0,0 +1,24 @@
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
stack_exts: dict[str, Any],
):
if "appliance" in stack_exts["kwargs"]:
cls.bind_appliance(stack_exts["kwargs"]["appliance"])
def finalize_instance(cls: Type, obj: Any, stack_exts: dict[str, Any]):
cls.check_appliance_bound()
super().finalize_instance(obj, stack_exts)

View File

@@ -1,267 +0,0 @@
from __future__ import annotations
from abc import ABC, ABCMeta, abstractmethod
from uuid import uuid4
from typing import Annotated, ClassVar, Any, Self, TypeVar, TypeAlias, Generic, Union
from datetime import datetime
from copy import deepcopy, copy
from typing_extensions import dataclass_transform, get_origin
from pydantic import (
ConfigDict,
BaseModel,
StrictInt,
StrictStr,
constr,
ByteSize,
AwareDatetime,
UUID4,
model_validator,
field_validator,
field_serializer,
SerializeAsAny,
)
from pydantic.fields import Field, _Unset, PydanticUndefined
from pydantic._internal._model_construction import ModelMetaclass, PydanticModelField
from pydantic._internal._generics import PydanticGenericMetadata
from pydantic._internal._decorators import ensure_classmethod_based_on_signature
import pytz
import inspect
from runtype import issubclass as runtype_issubclass
class NoInstanceMethod:
"""Descriptor to forbid that other descriptors can be looked up on an instance"""
def __init__(self, descr, name=None):
self.descr = descr
self.name = name
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
# enforce the instance cannot look up the attribute at all
if instance is not None:
raise AttributeError(f"{type(instance).__name__!r} has no attribute {self.name!r}")
# invoke any descriptor we are wrapping
return self.descr.__get__(instance, owner)
def DABField(default: Any = PydanticUndefined, *, final: bool | None = _Unset, finalize_only: bool | None = _Unset, **kwargs):
return Field(default, **kwargs)
T_BaseElement = TypeVar("T_BaseElement", bound="BaseElement")
T_BaseElement_ConfigMethod_Arg: TypeAlias = dict[str, Any]
T_BaseElement_ConfigMethod: TypeAlias = "classmethod[T_BaseElement, [T_BaseElement_ConfigMethod_Arg], T_BaseElement_ConfigMethod_Arg]"
class ConfigElement:
def __init__(self) -> None:
self.default_values_override_methods: dict[T_BaseElement_ConfigMethod, None] = {}
self.main_build_method: dict[T_BaseElement_ConfigMethod, None] = {}
def __copy__(self) -> Self:
# we cannot deepcopy because of classmethods, so we do a manual enhanced copy
cls = self.__class__
result = cls.__new__(cls)
for k, v in self.__dict__.items():
setattr(result, k, copy(v))
return result
class IBaseElement(BaseModel, ABC):
_config_element: ClassVar[ConfigElement] = ConfigElement()
@dataclass_transform(kw_only_default=True, field_specifiers=(PydanticModelField,))
class BaseElementMeta(ModelMetaclass, ABCMeta):
def __new__(
mcs,
cls_name: str,
bases: tuple[type[Any], ...],
namespace: dict[str, Any],
__pydantic_generic_metadata__: PydanticGenericMetadata | None = None,
__pydantic_reset_parent_namespace__: bool = True,
_create_model_module: str | None = None,
**kwargs: Any,
) -> type:
result = super().__new__(
mcs,
cls_name,
bases,
namespace,
__pydantic_generic_metadata__,
__pydantic_reset_parent_namespace__,
_create_model_module,
**kwargs,
)
print(cls_name)
assert issubclass(result, IBaseElement), "Only IBaseElement subclasses are supported"
# forcing all Fields to be frozen
for _, field_val in result.model_fields.items():
field_val.frozen = True
# copying/forwarding base classes default-configs
if "_config_element" not in result.__dict__:
assert result.__base__ is not None, "Only IBaseElement subclasses are supported"
if issubclass(result.__base__, IBaseElement):
result._config_element = copy(result.__base__._config_element)
else:
result._config_element = ConfigElement()
# searching and storing current class default-configs
for _, method in result.__dict__.items():
if isinstance(method, classmethod):
if hasattr(method, "default_values_override"):
result._config_element.default_values_override_methods[method] = None
# todo: find a way to 'lock' and add restriction to a field after inheritance
return result
class BaseElement(
IBaseElement,
ABC,
validate_assignment=True,
# revalidate_instances="subclass-instances", # pydantic issue #10681
validate_default=True,
extra="forbid",
metaclass=BaseElementMeta,
):
class Config:
ignored_types = (NoInstanceMethod,)
template_id: Annotated[UUID4, DABField(..., repr=True)]
template_short_name: Annotated[
StrictStr, constr(strip_whitespace=True, to_lower=True, strict=True, max_length=16), DABField(..., repr=True)
]
template_long_name: Annotated[StrictStr | None, DABField()]
template_description: Annotated[StrictStr | None, DABField()]
_saved_default_value: ClassVar[dict[str, Any]]
@model_validator(mode="before")
@classmethod
def __default_values_override_hook__(cls, values: T_BaseElement_ConfigMethod_Arg) -> T_BaseElement_ConfigMethod_Arg:
# extracting default values that were set in model fields
cls._saved_default_value = dict()
for field_key, field_val in cls.model_fields.items():
assert field_val.annotation is not None, "all fields must have annotation"
assert not runtype_issubclass(
field_val.annotation, BaseFeature
), "Features can only be in Appliance's features[] dict attribute"
"""
if field_key == "features":
cls._saved_default_value[field_key] = dict()
for feat_key, feat_value in field_val.items():
cls._saved_default_value[field_key][feat_key] = feat_value.dict()
"""
if field_val.default != PydanticUndefined:
cls._saved_default_value[field_key] = deepcopy(field_val.default)
for method, _ in cls._config_element.default_values_override_methods.items():
method.__func__(cls, cls._saved_default_value)
cls._default_values_override_hook__input_apply__(values)
return cls._saved_default_value
@classmethod
@abstractmethod
def _default_values_override_hook__input_apply__(
cls,
values: T_BaseElement_ConfigMethod_Arg,
): ...
class BaseFeature(BaseElement, ABC):
@NoInstanceMethod
@classmethod
def _default_values_override_hook__input_apply__(
cls,
values: T_BaseElement_ConfigMethod_Arg,
):
print(f"BaseFeature._default_values_override_hook__input_apply__ {cls}")
# applying user-defined values
for attr_key, attr_val in values.items():
assert attr_key in cls.model_fields, f"given feature attribute does not exist ({attr_key})"
cls._saved_default_value[attr_key] = attr_val
print(f"BaseFeature._default_values_override_hook__input_apply__ {cls} DONE")
def default_values_override(func: T_BaseElement_ConfigMethod) -> T_BaseElement_ConfigMethod:
func = ensure_classmethod_based_on_signature(func)
setattr(func, "default_values_override", lambda: True)
return func
T_Feature = TypeVar("T_Feature", bound=BaseFeature)
def get_discriminator_value(v: Any) -> str:
if isinstance(v, dict):
return v.get("fruit", v.get("filling"))
return getattr(v, "fruit", getattr(v, "filling", None))
class BaseAppliance(Generic[T_Feature], BaseElement, ABC):
cpu_cnt: Annotated[StrictInt, DABField(1, gt=0)]
ram_size: Annotated[ByteSize, DABField(256, gt=128)]
swap_size: Annotated[ByteSize, DABField(200, ge=0)]
rootfs_size: Annotated[ByteSize, DABField(2048, ge=2048)]
dabinst_id: Annotated[UUID4, DABField(uuid4(), repr=True)]
dabinst_short_name: Annotated[
StrictStr, constr(strip_whitespace=True, to_lower=True, strict=True, max_length=16), DABField(..., repr=True)
]
dabinst_long_name: Annotated[StrictStr | None, DABField("")]
dabinst_description: Annotated[StrictStr | None, DABField("")]
dabinst_creationdate: Annotated[AwareDatetime | None, DABField(datetime.now(tz=pytz.utc))]
features: SerializeAsAny[dict[str, T_Feature]] = DABField({})
@NoInstanceMethod
@classmethod
def add_feature(cls, feat: T_Feature):
cls._saved_default_value["features"][type(feat).__name__] = feat.dict()
@NoInstanceMethod
@classmethod
def del_feature(cls, type_feat: type[T_Feature]):
del cls._saved_default_value["features"][type_feat.__name__]
@NoInstanceMethod
@classmethod
def get_feature(cls, type_feat: type[T_Feature]) -> T_Feature:
return cls._saved_default_value["features"][type_feat.__name__]
@NoInstanceMethod
@classmethod
def _default_values_override_hook__input_apply__(
cls,
values: T_BaseElement_ConfigMethod_Arg,
):
print(f"BaseAppliance._default_values_override_hook__input_apply__ {cls}")
# applying user-defined values
for attr_key, attr_val in values.items():
if attr_key == "features":
if cls._saved_default_value["features"] is None:
cls._saved_default_value["features"] = {}
for feature_key, feature_val in attr_val.items():
print(f"searching feature: {feature_key}")
assert hasattr(cls, feature_key), f"feature not found ({feature_key})"
cls_feature = getattr(cls, feature_key)
assert (
cls_feature is not None and inspect.isclass(cls_feature) and issubclass(cls_feature, BaseFeature),
"The requested feature does not exist in the current Appliance class tree",
)
cls._saved_default_value["features"][feature_key] = cls_feature(**feature_val)
else:
assert attr_key in cls.model_fields, f"given attribute does not exist ({attr_key})"
cls._saved_default_value[attr_key] = attr_val
print(f"BaseAppliance._default_values_override_hook__input_apply__ {cls} DONE")

974
src/dabmodel/tools.py Normal file
View File

@@ -0,0 +1,974 @@
"""library's internal tools"""
from collections import ChainMap
from typing import Any, Annotated, get_origin, get_args, Union, Self, Optional, List, Dict, Tuple, Set, FrozenSet, Mapping, Callable
import typing
from dataclasses import dataclass
from types import UnionType, NoneType
import types
from uuid import UUID
from datetime import datetime
import json
import inspect
from frozendict import deepfreeze, frozendict
from .defines import ALLOWED_ANNOTATIONS, ALLOWED_MODEL_FIELDS_TYPES
from .exception import IncompletelyAnnotatedField, UnsupportedFieldType
class LAMJSONEncoder(json.JSONEncoder):
"""allows to JSON encode non supported data type"""
def default(self, o):
if isinstance(o, UUID):
# if the o is uuid, we simply return the value of uuid
return o.hex
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("_"):
return False
if isinstance(value, (staticmethod, classmethod, property)):
return False
if inspect.isfunction(value) or inspect.ismethoddescriptor(value):
return False
return True
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
class AnnotationWalkerCtx:
def __init__(
self,
origin: Any,
args: Any,
layer: int,
parent: Optional[Self] = None,
allowed_types: set[type, ...] = frozenset(),
allowed_annotations: dict[str, Any] = {},
):
self.__origin = origin
self.args = args
self.__layer = layer
self.__parent = parent
self.__allowed_types: set[type, ...] = allowed_types
self.__allowed_annotations: dict[str, Any] = allowed_annotations
self.__ext: dict[Any, ChainMap] = {} # per-trigger namespaces (lazy)
# NEW: edge routing metadata (walker sets; triggers read)
self.__edge_role: str | None = None # 'elem' | 'key' | 'val' | 'branch' | 'annotated' | 'arg'
self.__edge_token: Any | None = None # index/key/branch-id/etc.
self.__arg_index: int | None = None # which schema arg of the parent this child is
@property
def origin(self) -> Any:
return self.__origin
@property
def layer(self) -> int:
return self.__layer
@property
def parent(self) -> Self:
return self.__parent
@property
def allowed_types(self) -> FrozenSet[type]:
return self.__allowed_types
@property
def allowed_annotations(self) -> Mapping[str, Any]:
return self.__allowed_annotations
# NEW: read-only edge metadata for routing inside triggers
@property
def edge_role(self) -> str | None:
return self.__edge_role
@property
def edge_token(self) -> Any | None:
return self.__edge_token
@property
def arg_index(self) -> int | None:
return self.__arg_index
def ns(self, owner: Any) -> ChainMap:
"""
A per-trigger overlay namespace that inherits from parent ctx.
Use as: bag = ctx.ns(self); bag['whatever'] = ...
Lookups fall back to parent's bag automatically.
"""
if owner in self.__ext:
return self.__ext[owner]
# Determine the parent chain for this owner
parent_chain = ()
if self.__parent is not None and hasattr(self.__parent, "_AnnotationWalkerCtx__ext"):
parent_map = self.__parent._AnnotationWalkerCtx__ext.get(owner) # access private attr intentionally
if isinstance(parent_map, ChainMap):
# IMPORTANT: preserve the whole chain (not the ChainMap object as a single mapping)
parent_chain = tuple(parent_map.maps)
elif isinstance(parent_map, dict):
parent_chain = (parent_map,)
else:
parent_chain = ()
# Build a new ChainMap whose first map is this node's local overlay
cm = ChainMap({}, *parent_chain)
self.__ext[owner] = cm
return cm
# INTERNAL (walker only): set edge metadata on a child ctx
def _set_edge(self, *, role: str | None, token: Any | None, arg_index: int | None) -> None:
self.__edge_role = role
self.__edge_token = token
self.__arg_index = arg_index
@dataclass(frozen=True)
class TriggerResult:
# If provided, children won't be walked and this value is returned.
replace_with: Any | None = None
# If true, skip walking children but don't replace current node value.
skip_children: bool = False
# If provided, walker will restart processing with the given value
restart_with: Any | None = None # NEW
@staticmethod
def passthrough() -> Self:
return TriggerResult()
@staticmethod
def replace(value: Any) -> Self:
return TriggerResult(replace_with=value, skip_children=True)
@staticmethod
def skip() -> Self:
return TriggerResult(skip_children=True)
@staticmethod
def restart(value: Any) -> Self:
print("Doo!")
return TriggerResult(restart_with=value)
class AnnotationTrigger:
def init_trigger(self) -> None:
pass
def process_annotated(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
return None
def process_union(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
return None
def process_dict(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
return None
def process_tuple(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
return None
def process_list(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
return None
def process_set(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
return None
def process_unknown(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
return None
def process_allowed(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
return None
def process_exit(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
return None
def end_trigger(self, ctx: Optional[AnnotationWalkerCtx]) -> None:
pass
class LAMSchemaValidation(AnnotationTrigger):
def init_trigger(self) -> None:
print(f"Initializing {self.__class__.__name__}")
def process_annotated(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
print("process_annotated")
print(ctx.origin)
print(ctx.args)
if len(ctx.args) != 2:
raise UnsupportedFieldType("Annotated[T,x] requires 2 parameters")
if ctx.parent is not None:
raise UnsupportedFieldType("Annotated[T,x] is only supported as parent annotation")
return None
def process_union(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
print("process_union")
print(ctx.args)
if (len(ctx.args) != 2) or (type(None) not in list(ctx.args)):
raise UnsupportedFieldType("Union[] is only supported to implement Optional[] (takes 2 parameters, including None)")
return None
def process_dict(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
print("process_dict")
if len(ctx.args) != 2:
raise IncompletelyAnnotatedField(f"Dict Annotation requires 2 inner definitions: {ctx.origin}")
if not ctx.args[0] in ctx.allowed_types:
raise IncompletelyAnnotatedField(f"Dict Key must be simple builtin: {ctx.origin}")
return None
def process_tuple(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
print("process_tuple")
if len(ctx.args) == 0:
raise IncompletelyAnnotatedField(f"Annotation requires inner definition: {ctx.origin}")
return None
def process_list(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
print("process_list")
if len(ctx.args) == 0:
raise IncompletelyAnnotatedField(f"Annotation requires inner definition: {ctx.origin}")
return None
def process_set(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
print("process_set")
if len(ctx.args) == 0:
raise IncompletelyAnnotatedField(f"Annotation requires inner definition: {ctx.origin}")
return None
def process_allowed(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
print("process_allowed")
if ctx.origin is type(None) or ctx.origin is None:
if ctx.parent is None or not (ctx.parent.origin is Union or ctx.parent.origin is UnionType):
raise IncompletelyAnnotatedField(f"None is only accepted with Union, to implement Optional[]")
return None
def process_exit(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
print(f"process_exit: {ctx.origin}")
return None
def end_trigger(self, ctx: AnnotationWalkerCtx):
pass
class DataValidation(AnnotationTrigger):
def __init__(self, value: Any) -> None:
self._root = value
def init_trigger(self) -> None:
self._seeded = False
def _bag(self, ctx: AnnotationWalkerCtx):
bag = ctx.ns(self)
if not self._seeded:
bag["value"] = self._root
bag["path"] = ()
self._seeded = True
return bag
def process_annotated(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
b = self._bag(ctx)
def process_union(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
b = self._bag(ctx)
def process_dict(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
b = self._bag(ctx)
def process_tuple(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
b = self._bag(ctx)
def process_list(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
b = self._bag(ctx)
def process_set(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
b = self._bag(ctx)
def process_unknown(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
b = self._bag(ctx)
def process_allowed(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
b = self._bag(ctx)
def process_exit(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
b = self._bag(ctx)
def end_trigger(self, ctx: AnnotationWalkerCtx):
pass
class SchemaValidationError(TypeError):
def __init__(self, path: tuple[Any, ...], msg: str):
dotted = "".join(f"[{p}]" if isinstance(p, int) else (f".{p}" if path and i else str(p)) for i, p in enumerate(path)) or "<root>"
super().__init__(f"{dotted}: {msg}")
self.path = path
class HorizontalValidationTrigger(AnnotationTrigger):
def __init__(
self,
value: Any,
*,
strict_bool: bool = False,
collect_all: bool = True,
union_summary_max_per_branch: int = 3, # NEW
show_branch_types: bool = True, # NEW
):
self._root_value = value
self._strict_bool = strict_bool
self._collect_all = collect_all
self._union_summary_max = union_summary_max_per_branch
self._show_branch_types = show_branch_types
self._seeded = False
self._id_counter = 0
# ---------- utilities ----------
# ---------- utilities ----------
def _mk_cand(self, value: Any, path: tuple[Any, ...]):
cid = self._id_counter
self._id_counter += 1
return {"id": cid, "value": value, "path": path}
def _spawn_from(self, bag, parent_cand: dict, value: Any, path: tuple[Any, ...]):
"""
Create a child candidate inheriting union root id when inside a union branch.
"""
nc = self._mk_cand(value, path)
if "__union_branch_id" in bag:
# Ensure every descendant carries union-root id
nc["union_cid"] = parent_cand.get("union_cid", parent_cand["id"])
elif "union_cid" in parent_cand:
# Preserve if already present (e.g., nested under a branch)
nc["union_cid"] = parent_cand["union_cid"]
return nc
def _bag(self, ctx: AnnotationWalkerCtx):
bag = ctx.ns(self)
local = bag.maps[0]
parent_bag = ctx.parent.ns(self) if ctx.parent is not None else None
if not self._seeded and ctx.parent is None:
local["candidates"] = [self._mk_cand(self._root_value, ())]
local["errors"] = []
self._seeded = True
if "errors" not in local and parent_bag is not None:
local["errors"] = parent_bag.get("errors", [])
if ctx.parent is not None:
role = ctx.edge_role
if role == "elem":
local["candidates"] = list(parent_bag.get("pending_elem", []))
elif role == "key":
local["candidates"] = list(parent_bag.get("pending_key", []))
elif role == "val":
local["candidates"] = list(parent_bag.get("pending_val", []))
elif role == "arg":
local["candidates"] = list(parent_bag.get(f"pending_arg_{ctx.arg_index}", []))
elif role in ("branch", "annotated"):
local["candidates"] = list(parent_bag.get("candidates", []))
else:
local["candidates"] = list(parent_bag.get("candidates", []))
# If immediate child of a Union (branch), switch to branch-local errors
if ctx.parent is not None and ctx.parent.origin is UnionType and ctx.edge_role == "branch":
ubag = ctx.parent.ns(self)
branches = ubag.setdefault("union_branches_state", {})
bid = ctx.edge_token
bstate = branches.setdefault(bid, {"failed_ids": set(), "errors": []})
local["errors"] = bstate["errors"]
local["__union_branch_id"] = bid
local["__union_state_ref"] = ubag
# NEW: propagate the union scope id to this branch
local["__union_scope_id"] = ubag.get("__union_scope_id")
# Stamp union_cid for all candidates at branch entry
stamped = []
for c in local.get("candidates", []):
c2 = dict(c)
c2.setdefault("union_cid", c2["id"])
stamped.append(c2)
local["candidates"] = stamped
return bag
def _mark_branch_fail(self, bag, cand: dict):
if "__union_branch_id" in bag and "__union_state_ref" in bag:
bid = bag["__union_branch_id"]
uref = bag["__union_state_ref"]
branches = uref.setdefault("union_branches_state", {})
bstate = branches.setdefault(bid, {"failed_ids": set(), "errors": []})
root_id = cand.get("union_cid", cand["id"])
bstate["failed_ids"].add(root_id)
def _err(self, bag, cand, msg: str):
"""
Record an error. If we are inside a union branch, attach (cid, scope) so the
outer union can attribute it correctly to its branch/candidate.
"""
e = SchemaValidationError(cand["path"], msg)
# Find nearest union scope (walk ChainMap parents if needed)
def nearest_union_scope(b):
if "__union_scope_id" in b:
return b["__union_scope_id"]
# ChainMap.get() already searches parents; use get() with a sentinel
sentinel = object()
v = b.get("__union_scope_id", sentinel)
return None if v is sentinel else v
in_union_branch = ("__union_branch_id" in bag) and ("__union_state_ref" in bag)
if in_union_branch:
# union-root candidate id for attribution (propagated by spawn_from / branch entry)
root_id = cand.get("union_cid", cand["id"])
scope = nearest_union_scope(bag)
# Branch-local error bucket lives under union state
uref = bag["__union_state_ref"]
bid = bag["__union_branch_id"]
branches = uref.setdefault("union_branches_state", {})
bstate = branches.setdefault(bid, {"failed_ids": set(), "errors": []})
bstate["failed_ids"].add(root_id)
bstate["errors"].append({"cid": root_id, "scope": scope, "err": e})
return
# Outside any union branch: add to global errors (and optional fail-fast)
bag["errors"].append(e)
if not self._collect_all:
raise e
def _check_leaf(self, bag, cand: dict, T: type):
v = cand["value"]
if T is type(None):
if v is not None:
self._err(bag, cand, "expected None")
return
if self._strict_bool and T is bool and type(v) is not bool:
self._err(bag, cand, f"expected bool, got {type(v).__name__}")
return
if v is None or not isinstance(v, T):
got = "None" if v is None else type(v).__name__
self._err(bag, cand, f"expected {T.__name__}, got {got}")
# ---------- entry hooks (seed pending; validate leaves; no aggregation here) ----------
def process_allowed(self, ctx: AnnotationWalkerCtx):
bag = self._bag(ctx)
T = ctx.origin
for cand in bag.get("candidates", []):
self._check_leaf(bag, cand, T)
def process_list(self, ctx: AnnotationWalkerCtx):
bag = self._bag(ctx)
cands = bag.get("candidates", [])
if len(ctx.args) != 1:
for cand in cands:
self._err(bag, cand, "List[T] requires 1 argument")
return
pending = []
for cand in cands:
v = cand["value"]
if not isinstance(v, list):
self._err(bag, cand, f"expected list, got {type(v).__name__}")
continue
base = cand["path"]
for i in range(len(v)):
pending.append(self._spawn_from(bag, cand, v[i], base + (i,)))
bag["pending_elem"] = pending
def process_tuple(self, ctx: AnnotationWalkerCtx):
bag = self._bag(ctx)
cands = bag.get("candidates", [])
vararg = len(ctx.args) == 2 and ctx.args[1] is Ellipsis
if vararg:
pending = []
for cand in cands:
v = cand["value"]
if not isinstance(v, tuple):
self._err(bag, cand, f"expected tuple, got {type(v).__name__}")
continue
base = cand["path"]
for i in range(len(v)):
pending.append(self._spawn_from(bag, cand, v[i], base + (i,)))
bag["pending_elem"] = pending
else:
arity = len(ctx.args)
slots: dict[int, list] = {}
for cand in cands:
v = cand["value"]
if not isinstance(v, tuple):
self._err(bag, cand, f"expected tuple, got {type(v).__name__}")
continue
if len(v) != arity:
self._err(bag, cand, f"expected tuple len {arity}, got {len(v)}")
continue
base = cand["path"]
for i, elem in enumerate(v):
slots.setdefault(i, []).append(self._spawn_from(bag, cand, elem, base + (i,)))
for i, group in slots.items():
bag[f"pending_arg_{i}"] = group
def process_set(self, ctx: AnnotationWalkerCtx):
bag = self._bag(ctx)
cands = bag.get("candidates", [])
if len(ctx.args) != 1:
for cand in cands:
self._err(bag, cand, "Set[T]/FrozenSet[T] requires 1 argument")
return
pending = []
for cand in cands:
v = cand["value"]
if not isinstance(v, (set, frozenset)):
self._err(bag, cand, f"expected set/frozenset, got {type(v).__name__}")
continue
base = cand["path"]
for e in v:
pending.append(self._spawn_from(bag, cand, e, base + ("<elem>",)))
bag["pending_elem"] = pending
def process_dict(self, ctx: AnnotationWalkerCtx):
bag = self._bag(ctx)
cands = bag.get("candidates", [])
if len(ctx.args) != 2:
for cand in cands:
self._err(bag, cand, "Dict[K,V] requires 2 arguments")
return
pkeys, pvals = [], []
for cand in cands:
v = cand["value"]
if not isinstance(v, dict):
self._err(bag, cand, f"expected dict, got {type(v).__name__}")
continue
base = cand["path"]
for k, val in v.items():
pkeys.append(self._spawn_from(bag, cand, k, base + ("<key>",)))
pvals.append(self._spawn_from(bag, cand, val, base + (k,)))
bag["pending_key"] = pkeys
bag["pending_val"] = pvals
def process_annotated(self, ctx: AnnotationWalkerCtx):
# No checks here; inner T will validate routed candidates.
self._bag(ctx)
def process_union(self, ctx: AnnotationWalkerCtx):
bag = self._bag(ctx)
# Candidates arriving at THIS union node (e.g., per list element when union is element type)
cands = bag.get("candidates", [])
# Track these exact candidate ids for this union scope
bag["union_candidate_ids"] = [c["id"] for c in cands]
# One entry per branch; each branch accumulates scoped errors keyed by union-root cid
bag.setdefault("union_branches_state", {}) # bid -> {"failed_ids": set(), "errors": []}
bag["union_branch_labels"] = [self._pretty_type(a) for a in ctx.args]
# Scope id distinguishes nested unions; only errors tagged with this scope count here
bag["__union_scope_id"] = id(ctx)
# ---------- exit (aggregation only) ----------
def process_exit(self, ctx: AnnotationWalkerCtx):
if ctx.origin is not UnionType:
return None
bag = ctx.ns(self)
root_errors = bag.get("errors", [])
branches = bag.get("union_branches_state", {})
cand_ids = bag.get("union_candidate_ids", [])
labels = bag.get("union_branch_labels", [])
scope_id = bag.get("__union_scope_id") # THIS union's scope
# Build: per_branch_errors[bid][cid] -> [SchemaValidationError, ...] filtered to THIS union scope
per_branch_errors: dict[int, dict[int, list[SchemaValidationError]]] = {}
for bid, state in branches.items():
berrs = state.get("errors", [])
m: dict[int, list[SchemaValidationError]] = {}
for item in berrs:
if isinstance(item, dict):
# Only errors tagged for THIS union scope
if item.get("scope") != scope_id:
continue
cid = item.get("cid")
err = item.get("err")
if cid is not None and isinstance(err, SchemaValidationError):
m.setdefault(cid, []).append(err)
elif isinstance(item, SchemaValidationError):
# Legacy/plain error: attribute to all failed ids of this branch
for cid in state.get("failed_ids", set()):
m.setdefault(cid, []).append(item)
# Dedupe identical (path, text)
for cid, lst in m.items():
seen = set()
uniq = []
for e in lst:
key = (e.path, str(e))
if key not in seen:
seen.add(key)
uniq.append(e)
m[cid] = uniq
per_branch_errors[bid] = m
# Decide per candidate of THIS union
for cid in cand_ids:
branch_ok = any(len(per_branch_errors.get(bid, {}).get(cid, [])) == 0 for bid in branches.keys())
if branch_ok:
continue # some branch matched, good
# Pretty summary for this failing candidate
# Get the path for this union's candidate; fall back to <root> if absent
cands_here = bag.get("candidates", [])
path = next((c["path"] for c in cands_here if c.get("id") == cid), ())
lines = ["no union branch matched; tried:"]
for bid, state in branches.items():
label = labels[bid] if bid < len(labels) else f"branch {bid}"
errs = per_branch_errors.get(bid, {}).get(cid, [])
if not errs:
lines.append(f" - {label}: mismatch")
continue
N = self._union_summary_max
shown = errs[:N]
lines.append(f" - {label}: {len(errs)} issue(s)")
for e in shown:
lines.append(f" - {e}")
if len(errs) > N:
lines.append(f" - (+{len(errs) - N} more)")
root_errors.append(SchemaValidationError(path, "\n".join(lines)))
# Also, if THIS union is nested inside an enclosing union branch, and ALL branches
# of THIS union failed for a candidate, bubble a single 'mismatch' up to that parent
# so the parent union can mark its branch as failed for that union-root cid.
if ("__union_state_ref" in bag) and ("__union_branch_id" in bag):
uref = bag["__union_state_ref"] # enclosing union's bag
up_bid = bag["__union_branch_id"] # which branch we're in
up_branches = uref.setdefault("union_branches_state", {})
up_state = up_branches.setdefault(up_bid, {"failed_ids": set(), "errors": []})
up_scope = uref.get("__union_scope_id")
# Map THIS union's local cid to the enclosing union's root cid via union_cid
local_cands = bag.get("candidates", [])
# Fallback map: if local_cands missing, assume 1:1
fallback_map = {cid: cid for cid in cand_ids}
map_local_to_outer = {}
for lc in local_cands:
local_id = lc.get("id")
outer_root = lc.get("union_cid", local_id)
map_local_to_outer[local_id] = outer_root
for cid in cand_ids:
branch_ok = any(len(per_branch_errors.get(bid, {}).get(cid, [])) == 0 for bid in branches.keys())
if branch_ok:
continue # do not bubble success
outer_cid = map_local_to_outer.get(cid, fallback_map[cid])
up_state["failed_ids"].add(outer_cid)
# lightweight marker so parent has at least one error on record for this cid
up_state["errors"].append({"cid": outer_cid, "scope": up_scope, "err": SchemaValidationError((), "mismatch")})
return None
def end_trigger(self, ctx: Optional[AnnotationWalkerCtx]):
if ctx is None:
return
errors = ctx.ns(self).get("errors", [])
if errors:
# Raise them together; swap for your preferred error carrier if needed
raise ExceptionGroup("schema validation failed", errors)
# --- pretty type for messages (best-effort) ---
def _pretty_type(self, t: Any) -> str:
origin = get_origin(t) or t
args = get_args(t)
try:
if origin is UnionType or origin is Union:
return " | ".join(self._pretty_type(a) for a in args)
if origin in (list, tuple, set, frozenset, dict, Annotated):
if origin is dict and len(args) == 2:
return f"dict[{self._pretty_type(args[0])}, {self._pretty_type(args[1])}]"
if origin in (list, set, frozenset) and len(args) == 1:
name = "list" if origin is list else ("set" if origin is set else "frozenset")
return f"{name}[{self._pretty_type(args[0])}]"
if origin is tuple:
if len(args) == 2 and args[1] is Ellipsis:
return f"tuple[{self._pretty_type(args[0])}, ...]"
return f"tuple[{', '.join(self._pretty_type(a) for a in args)}]"
if origin is Annotated and args:
return f"Annotated[{self._pretty_type(args[0])}, ...]"
if isinstance(origin, type):
return origin.__name__
return str(t)
except Exception:
return repr(t)
class AnnotationWalker:
DEFAULT_ALLOWED_TYPES = frozenset({str, int, float, complex, bool, bytes, NoneType})
DEFAULT_ALLOWED_ANNOTATIONS: dict[str, Any] = frozendict(
{
"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,
}
)
def __init__(self, ann: Any, triggers: tuple[AnnotationTrigger, ...], **kwargs):
if not triggers:
raise RuntimeError("AnnotationWalker requires trigger(s)")
# Normalize triggers into instances
insts: list[AnnotationTrigger] = []
for t in triggers if isinstance(triggers, tuple) else (triggers,):
if isinstance(t, AnnotationTrigger):
insts.append(t)
elif isinstance(t, type) and issubclass(t, AnnotationTrigger):
insts.append(t())
else:
raise RuntimeError(f"Unsupported trigger: {t}")
self._triggers = tuple(insts)
# Allowed types / annotations
atypes = set(type(self).DEFAULT_ALLOWED_TYPES)
if "ex_allowed_types" in kwargs:
atypes.update(kwargs["ex_allowed_types"])
self._allowed_types = frozenset(atypes)
annots = dict(type(self).DEFAULT_ALLOWED_ANNOTATIONS)
if "ex_allowed_annotations" in kwargs:
annots.update(kwargs["ex_allowed_annotations"])
self._allowed_annotations = frozendict(annots)
# Annotation can be string
self.__ann = ann
if isinstance(ann, str):
self.__ann = eval(ann, {"__builtins__": {}}, self._allowed_annotations)
def run(self) -> TriggerResult:
for t in self._triggers:
t.init_trigger()
self.root_ctx = None
self._walk(self.__ann, None)
for t in self._triggers:
t.end_trigger(self.root_ctx)
# --- Helpers ---
def _new_ctx(self, origin, args, layer, parent):
return AnnotationWalkerCtx(origin, args, layer, parent, self._allowed_types, self._allowed_annotations)
def _apply_triggers(self, method: str, ctx: AnnotationWalkerCtx) -> TriggerResult:
final = TriggerResult.passthrough()
for t in self._triggers:
res = getattr(t, method)(ctx)
if not res:
continue
if res.restart_with is not None:
return res # short-circuit on restart
if res.replace_with is not None:
final = TriggerResult.replace(res.replace_with)
if res.skip_children:
final = TriggerResult(
replace_with=final.replace_with,
skip_children=True,
)
return final
def _apply_exits(self, ctx: AnnotationWalkerCtx) -> Any | None:
"""
Run exits in order: type-specific (if implemented) then generic `process_exit`.
Only `replace_with` is honored at exit; last `replace_with` wins.
"""
final = None
for t in self._triggers:
res = t.process_exit(ctx)
if res and (res.replace_with is not None):
final = res.replace_with
return final
def _handle_with_triggers(
self,
trigger_name: str,
ctx: AnnotationWalkerCtx,
args_handler: Callable[[AnnotationWalkerCtx], Any] | None = None,
) -> Any:
# ENTER
res = self._apply_triggers(trigger_name, ctx)
if res.restart_with is not None:
return self._walk(res.restart_with, ctx.parent)
if res.replace_with is not None:
exit_val = self._apply_exits(ctx)
return exit_val if exit_val is not None else res.replace_with
node_value = None
if not res.skip_children:
if args_handler:
node_value = args_handler(ctx)
else:
# DEFAULT: descend once per schema arg; mark as positional arg
node_value = tuple(self._walk_child(a, ctx, arg_index=i, role="arg", token=i) for i, a in enumerate(ctx.args))
# EXIT
exit_val = self._apply_exits(ctx)
return exit_val if exit_val is not None else node_value
def _walk_args_tuple(self, ctx: AnnotationWalkerCtx):
# Tuple[T, ...] (variadic): one schema child, mark role='elem'
if len(ctx.args) == 2 and ctx.args[1] is Ellipsis:
return (self._walk_child(ctx.args[0], ctx, arg_index=0, role="elem", token=None), Ellipsis)
# Fixed tuple: each positional arg gets role='arg'
return tuple(self._walk_child(a, ctx, arg_index=i, role="arg", token=i) for i, a in enumerate(ctx.args))
# --- Dispatcher ---
def _walk(self, type_: Any, parent_ctx: Optional[AnnotationWalkerCtx]) -> Any:
# For logs only: show the calling layer
print(f"[{parent_ctx.layer if parent_ctx else 0}] walking through: {type_}")
origin = get_origin(type_) or type_
if origin is None:
origin = NoneType
if origin is Union:
origin = UnionType
if not isinstance(origin, type):
raise RuntimeError("Annotation must be using type(s), not instances")
args = get_args(type_)
# IMPORTANT:
# If caller (_walk_child) already constructed a child ctx with edge metadata,
# reuse that object as *the* ctx for this node instead of allocating a new one.
if isinstance(parent_ctx, AnnotationWalkerCtx) and parent_ctx.origin is origin and parent_ctx.args == args:
ctx = parent_ctx
else:
# Root or internal calls that didn't prebuild the ctx
layer = 0 if parent_ctx is None else parent_ctx.layer + 1
ctx = self._new_ctx(origin, args, layer, parent_ctx)
# Remember root ctx for end_trigger()
if ctx.parent is None:
self.root_ctx = ctx
print(origin)
match origin:
case typing.Annotated:
# inner type gets role='annotated'
return self._handle_with_triggers(
"process_annotated",
ctx,
args_handler=lambda c: self._walk_child(c.args[0], c, arg_index=0, role="annotated", token=None) if c.args else None,
)
case types.UnionType:
# branches get role='branch' and token=branch index
return self._handle_with_triggers(
"process_union",
ctx,
args_handler=lambda c: tuple(self._walk_child(a, c, arg_index=i, role="branch", token=i) for i, a in enumerate(c.args)),
)
case _ if issubclass(origin, dict):
# arg0=key (role='key'), arg1=value (role='val')
return self._handle_with_triggers(
"process_dict",
ctx,
args_handler=lambda c: (
self._walk_child(c.args[0], c, arg_index=0, role="key", token=None),
self._walk_child(c.args[1], c, arg_index=1, role="val", token=None),
),
)
case _ if issubclass(origin, tuple):
return self._handle_with_triggers("process_tuple", ctx, self._walk_args_tuple)
case _ if issubclass(origin, list):
# single child T with role='elem'
return self._handle_with_triggers(
"process_list",
ctx,
args_handler=lambda c: self._walk_child(c.args[0], c, arg_index=0, role="elem", token=None) if c.args else None,
)
case _ if issubclass(origin, set):
# single child T with role='elem'
return self._handle_with_triggers(
"process_set",
ctx,
args_handler=lambda c: self._walk_child(c.args[0], c, arg_index=0, role="elem", token=None) if c.args else None,
)
case _ if origin in self._allowed_types:
return self._handle_with_triggers("process_allowed", ctx)
case _:
res = self._apply_triggers("process_unknown", ctx)
if res.restart_with is not None:
return self._walk(res.restart_with, ctx.parent)
if res.replace_with is not None:
return res.replace_with
raise UnsupportedFieldType(f"Not supported Field: {ctx.origin}, Supported list: {self._allowed_types}")
def _walk_child(
self,
type_expr: Any,
parent_ctx: AnnotationWalkerCtx,
*,
arg_index: int,
role: str | None,
token: Any | None,
) -> Any:
origin = get_origin(type_expr) or type_expr
if origin is None:
origin = NoneType
if origin is Union:
origin = UnionType
if not isinstance(origin, type):
raise RuntimeError("Annotation must be using type(s), not instances")
args = get_args(type_expr)
child = self._new_ctx(origin, args, parent_ctx.layer + 1, parent_ctx)
# stamp routing metadata for triggers
child._set_edge(role=role, token=token, arg_index=arg_index)
# IMPORTANT: recurse with the CHILD as the parent_ctx for the next step,
# so the walker uses this child ctx (with edge metadata & incremented layer).
return self._walk(type_expr, child)

View File

@@ -0,0 +1,53 @@
# 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 typing import Annotated, Union, Optional
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 AnnotationsWalkerTest(unittest.TestCase):
def setUp(self):
print("\n->", unittest.TestCase.id(self))
def test_validate(self):
ann = dict[int, list[int]] | dict[int, list[int | str]]
val = {1: [2], 2: ["a", [1]]}
res = dm.tools.AnnotationWalker(ann, (dm.tools.HorizontalValidationTrigger(val),))
res.run()
def test_simple(self):
print(isinstance(None, type(None)))
print("\n== From OBJs ==")
res = dm.tools.AnnotationWalker(Annotated[Optional[dict[int, list[str]]], "comment"], (dm.tools.LAMSchemaValidation(),))
print(f"res={res.run()}")
print("\n== From STRING ==")
res = dm.tools.AnnotationWalker('Annotated[Optional[dict[int, list[str]]], "comment"]', (dm.tools.LAMSchemaValidation(),))
print(f"res={res.run()}")
res = dm.tools.AnnotationWalker(Annotated[Optional[dict[int, list[None]]], "comment"], (dm.tools.LAMSchemaValidation(),))
print(f"res={res.run()}")
# ---------- main ----------
if __name__ == "__main__":
unittest.main()

1825
test/test_appliance.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,37 +0,0 @@
from pydantic import BaseModel, SerializeAsAny
class commonbase(
BaseModel,
revalidate_instances="subclass-instances", # toogle to generate error
): ...
class basechild(commonbase):
test_val: int = 1
class derivedchild(basechild):
test_val2: int = 2
class container(commonbase):
ct_child_1: dict[str, basechild] = {}
ct_child_2: SerializeAsAny[dict[str, basechild]] = {}
ct_child_3: dict[str, SerializeAsAny[basechild]] = {}
if __name__ == "__main__":
test_val = container(
ct_child_1={"test1": derivedchild()},
ct_child_2={"test2": derivedchild()},
ct_child_3={"test3": derivedchild()},
)
print(test_val.model_dump_json(indent=1))
print(test_val.model_dump())
assert "test_val2" not in test_val.model_dump()["ct_child_1"]["test1"]
assert "test_val2" in test_val.model_dump()["ct_child_2"]["test2"]
assert "test_val2" in test_val.model_dump()["ct_child_3"]["test3"]

862
test/test_element.py Normal file
View File

@@ -0,0 +1,862 @@
# 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 typing import Annotated, Union
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 C(dm.Element):
elem: E = E()
@classmethod
def __initializer(cls):
cls.elem = E(ivalue=test_fun())
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
e = E()
with self.assertRaises(dm.NonExistingField):
e.test = 123
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
self.assertEqual(E.ivalue, 12)
e = E()
self.assertEqual(e.ivalue, 12)
with self.assertRaises(dm.ReadOnlyField):
e.ivalue = 14
self.assertEqual(e.ivalue, 12)
def test_mutable_object_freeze(self):
class E(dm.Element, options=(dm.ObjectMutable)):
ivalue: int = 43
self.assertEqual(E.ivalue, 43)
with self.assertRaises(dm.ReadOnlyField):
E.ivalue = 13
self.assertEqual(E.ivalue, 43)
e = E()
self.assertEqual(e.ivalue, 43)
e.ivalue = 14
self.assertEqual(e.ivalue, 14)
e.freeze()
self.assertEqual(e.ivalue, 14)
with self.assertRaises(dm.ReadOnlyField):
e.ivalue = 15
self.assertEqual(e.ivalue, 14)
def test_mutable_object_and_class_freeze(self):
class E(dm.Element, options=(dm.ObjectMutable, dm.ClassMutable)):
ivalue: int = 43
self.assertEqual(E.ivalue, 43)
E.ivalue = 13
self.assertEqual(E.ivalue, 13)
e = E()
self.assertEqual(e.ivalue, 13)
e.ivalue = 14
self.assertEqual(e.ivalue, 14)
e.freeze()
self.assertEqual(e.ivalue, 14)
with self.assertRaises(dm.ReadOnlyField):
e.ivalue = 15
self.assertEqual(e.ivalue, 14)
def test_mutable_object_and_class_freeze_2(self):
class E(dm.Element, options=(dm.ObjectMutable, dm.ClassMutable)):
ivalue: int = 43
self.assertFalse(E.__lam_schema__["ivalue"].is_frozen())
E.freeze_class()
self.assertTrue(E.__lam_schema__["ivalue"].is_frozen())
e = E()
self.assertFalse(e.__lam_schema__["ivalue"].is_frozen())
e.freeze()
self.assertTrue(e.__lam_schema__["ivalue"].is_frozen())
def test_mutable_class_freeze_container(self):
class E(dm.Element, options=(dm.ClassMutable)):
ar_ivalue: list[int] = [43]
E.ar_ivalue.append(12)
self.assertEqual(E.ar_ivalue, [43, 12])
E.freeze_class()
self.assertEqual(E.ar_ivalue, (43, 12))
with self.assertRaises(AttributeError):
E.ar_ivalue.append(52)
self.assertEqual(E.ar_ivalue, (43, 12))
e = E()
self.assertEqual(e.ar_ivalue, (43, 12))
with self.assertRaises(AttributeError):
e.ar_ivalue.append(52)
self.assertEqual(e.ar_ivalue, (43, 12))
def test_mutable_object_freeze_container(self):
class E(dm.Element, options=(dm.ObjectMutable)):
ar_ivalue: list[int] = [43, 54]
self.assertEqual(E.ar_ivalue, (43, 54))
with self.assertRaises(AttributeError):
E.ar_ivalue.append(13)
self.assertEqual(E.ar_ivalue, (43, 54))
e = E()
self.assertEqual(e.ar_ivalue, [43, 54])
e.ar_ivalue.append(32)
self.assertEqual(e.ar_ivalue, [43, 54, 32])
e.freeze()
self.assertEqual(e.ar_ivalue, (43, 54, 32))
with self.assertRaises(AttributeError):
e.ar_ivalue.append(12)
self.assertEqual(e.ar_ivalue, (43, 54, 32))
def test_mutable_object_and_class_freeze_container(self):
class E(dm.Element, options=(dm.ObjectMutable, dm.ClassMutable)):
ar_ivalue: list[int] = [43, 54]
self.assertEqual(E.ar_ivalue, [43, 54])
E.ar_ivalue.append(13)
self.assertEqual(E.ar_ivalue, [43, 54, 13])
e = E()
self.assertEqual(e.ar_ivalue, [43, 54, 13])
e.ar_ivalue.append(14)
self.assertEqual(e.ar_ivalue, [43, 54, 13, 14])
e.freeze()
self.assertEqual(e.ar_ivalue, (43, 54, 13, 14))
with self.assertRaises(AttributeError):
e.ar_ivalue.append(15)
self.assertEqual(e.ar_ivalue, (43, 54, 13, 14))
def test_mutable_class_freeze_inheritance(self):
class E(dm.Element, options=(dm.ClassMutable)):
ivalue: int = 12
class E2(E):
pass
self.assertEqual(E2.ivalue, 12)
with self.assertRaises(dm.ReadOnlyField):
E2.ivalue = 13
self.assertEqual(E2.ivalue, 12)
e = E2()
self.assertEqual(e.ivalue, 12)
with self.assertRaises(dm.ReadOnlyField):
e.ivalue = 14
self.assertEqual(e.ivalue, 12)
def test_mutable_object_freeze_inheritance(self):
class E(dm.Element, options=(dm.ObjectMutable)):
ivalue: int = 12
class E2(E):
pass
self.assertEqual(E2.ivalue, 12)
with self.assertRaises(dm.ReadOnlyField):
E2.ivalue = 13
self.assertEqual(E2.ivalue, 12)
e = E2()
self.assertEqual(e.ivalue, 12)
with self.assertRaises(dm.ReadOnlyField):
e.ivalue = 14
self.assertEqual(e.ivalue, 12)
def test_mutable_object_and_class_freeze_inheritance(self):
class E(dm.Element, options=(dm.ObjectMutable, dm.ClassMutable)):
ivalue: int = 12
class E2(E):
pass
self.assertEqual(E2.ivalue, 12)
with self.assertRaises(dm.ReadOnlyField):
E2.ivalue = 13
self.assertEqual(E2.ivalue, 12)
e = E2()
self.assertEqual(e.ivalue, 12)
with self.assertRaises(dm.ReadOnlyField):
e.ivalue = 14
self.assertEqual(e.ivalue, 12)
def test_mutable_object_and_class_freeze_inheritance_noleak(self):
class E(dm.Element, options=(dm.ObjectMutable, dm.ClassMutable)):
ivalue: int = 12
class E2(E):
pass
self.assertEqual(E2.ivalue, 12)
with self.assertRaises(dm.ReadOnlyField):
E2.ivalue = 13
self.assertEqual(E2.ivalue, 12)
e2 = E2()
self.assertEqual(e2.ivalue, 12)
with self.assertRaises(dm.ReadOnlyField):
e2.ivalue = 14
self.assertEqual(e2.ivalue, 12)
# no Leak
self.assertEqual(E.ivalue, 12)
E.ivalue = 13
self.assertEqual(E.ivalue, 13)
e = E()
self.assertEqual(e.ivalue, 13)
e.ivalue = 14
self.assertEqual(e.ivalue, 14)
e.freeze()
self.assertEqual(e.ivalue, 14)
with self.assertRaises(dm.ReadOnlyField):
e.ivalue = 15
self.assertEqual(e.ivalue, 14)
# no Leak 2
self.assertEqual(E2.ivalue, 12)
with self.assertRaises(dm.ReadOnlyField):
E2.ivalue = 13
self.assertEqual(E2.ivalue, 12)
e2 = E2()
self.assertEqual(e2.ivalue, 12)
with self.assertRaises(dm.ReadOnlyField):
e2.ivalue = 14
self.assertEqual(e2.ivalue, 12)
def test_mutable_class_freeze_nested_element(self):
class NElem(dm.Element):
ivalue: int = 43
class E(dm.Element, options=(dm.ClassMutable)):
e: NElem = NElem()
E.e.ivalue = 12
self.assertEqual(E.e.ivalue, 12)
E.freeze_class()
self.assertEqual(E.e.ivalue, 12)
with self.assertRaises(dm.ReadOnlyField):
E.e.ivalue = 13
self.assertEqual(E.e.ivalue, 12)
e = E()
self.assertEqual(e.e.ivalue, 12)
with self.assertRaises(dm.ReadOnlyField):
e.e.ivalue = 14
self.assertEqual(e.e.ivalue, 12)
def test_mutable_object_freeze_nested_element(self):
class NElem(dm.Element):
ivalue: int = 43
class E(dm.Element, options=(dm.ObjectMutable)):
e: NElem = NElem()
self.assertEqual(E.e.ivalue, 43)
with self.assertRaises(dm.ReadOnlyField):
E.e.ivalue = 13
self.assertEqual(E.e.ivalue, 43)
e = E()
self.assertEqual(e.e.ivalue, 43)
e.e.ivalue = 14
self.assertEqual(e.e.ivalue, 14)
e.freeze()
self.assertEqual(e.e.ivalue, 14)
with self.assertRaises(dm.ReadOnlyField):
e.e.ivalue = 15
self.assertEqual(e.e.ivalue, 14)
def test_mutable_object_and_class_freeze_nested_element(self):
class NElem(dm.Element):
ivalue: int = 43
class E(dm.Element, options=(dm.ObjectMutable, dm.ClassMutable)):
e: NElem = NElem()
self.assertEqual(E.e.ivalue, 43)
E.e.ivalue = 13
self.assertEqual(E.e.ivalue, 13)
e = E()
self.assertEqual(e.e.ivalue, 13)
e.e.ivalue = 14
self.assertEqual(e.e.ivalue, 14)
e.e.freeze()
self.assertEqual(e.e.ivalue, 14)
with self.assertRaises(dm.ReadOnlyField):
e.e.ivalue = 15
self.assertEqual(e.e.ivalue, 14)
def test_element_clone(self):
class Elem(dm.Element):
ivalue: int = 43
self.assertTrue(Elem.frozen_cls)
self.assertFalse(Elem.mutable_obj)
e = Elem()
print(e)
new_obj1 = e.clone_as_mutable_variant()
self.assertFalse(new_obj1.frozen_cls)
self.assertTrue(new_obj1.mutable_obj)
self.assertIsInstance(new_obj1, Elem)
self.assertEqual(new_obj1.ivalue, 43)
new_obj1.ivalue = 55
self.assertEqual(new_obj1.ivalue, 55)
new_obj2 = e.clone_as_mutable_variant()
self.assertFalse(new_obj2.frozen_cls)
self.assertTrue(new_obj2.mutable_obj)
self.assertNotEqual(new_obj1, new_obj2)
self.assertEqual(type(new_obj1), type(new_obj2))
self.assertEqual(new_obj2.ivalue, 43)
new_obj2.ivalue = 56
self.assertEqual(new_obj2.ivalue, 56)
new_obj3 = e.clone_as_mutable_variant()
self.assertFalse(new_obj3.frozen_cls)
self.assertTrue(new_obj3.mutable_obj)
self.assertNotEqual(new_obj1, new_obj3)
self.assertNotEqual(new_obj2, new_obj3)
self.assertEqual(type(new_obj1), type(new_obj3))
self.assertEqual(type(new_obj2), type(new_obj3))
self.assertEqual(new_obj3.ivalue, 43)
new_obj3.ivalue = 57
self.assertEqual(new_obj3.ivalue, 57)
self.assertEqual(e.ivalue, 43)
with self.assertRaises(dm.ReadOnlyField):
e.ivalue = 15
self.assertEqual(e.ivalue, 43)
self.assertEqual(new_obj1.ivalue, 55)
new_obj1.freeze()
self.assertEqual(new_obj1.ivalue, 55)
with self.assertRaises(dm.ReadOnlyField):
new_obj1.ivalue = 15
self.assertEqual(new_obj1.ivalue, 55)
self.assertEqual(new_obj2.ivalue, 56)
self.assertEqual(new_obj3.ivalue, 57)
new_obj2.ivalue = 76
self.assertEqual(new_obj2.ivalue, 76)
self.assertEqual(new_obj3.ivalue, 57)
self.assertEqual(new_obj1.ivalue, 55)
new_obj2.freeze()
self.assertEqual(new_obj2.ivalue, 76)
with self.assertRaises(dm.ReadOnlyField):
new_obj2.ivalue = 15
self.assertEqual(new_obj2.ivalue, 76)
self.assertTrue(e.frozen)
self.assertTrue(new_obj1.frozen)
self.assertTrue(new_obj2.frozen)
self.assertFalse(new_obj3.frozen)
def test_element_clone_inheritance(self):
class ElemA(dm.Element):
ivalue: int = 43
class ElemB(ElemA):
ivalue = 18
elemA = ElemA()
elemB = ElemB()
_elemA = elemA.clone_as_mutable_variant()
_elemB = elemB.clone_as_mutable_variant()
self.assertIsInstance(elemB, type(elemA))
self.assertIsInstance(_elemA, type(elemA))
self.assertIsInstance(_elemB, type(elemB))
self.assertIsInstance(_elemB, type(_elemA))
def test_element_clone_inheritance_nested(self):
class ElemAInner(dm.Element):
fvalue: float = 0.123
class ElemA(dm.Element):
ivalue: int = 43
el1: ElemAInner = ElemAInner()
class ElemBInner(ElemAInner):
fvalue = 0.9
class ElemB(ElemA):
ivalue = 18
el1 = ElemAInner(fvalue=6.54)
el2: ElemBInner = ElemBInner()
self.assertIsInstance(ElemA.el1, ElemAInner)
self.assertIsInstance(ElemB.el1, ElemAInner)
self.assertIsInstance(ElemB.el2, ElemAInner)
self.assertIsInstance(ElemB.el2, ElemBInner)
self.assertNotIsInstance(ElemB.el1, ElemBInner)
elemA = ElemA()
elemB = ElemB()
_elemA = elemA.clone_as_mutable_variant()
_elemB = elemB.clone_as_mutable_variant()
self.assertIsInstance(elemB, type(elemA))
self.assertIsInstance(_elemA, type(elemA))
self.assertIsInstance(_elemB, type(elemB))
self.assertIsInstance(_elemB, type(_elemA))
self.assertIsInstance(_elemB.el1, type(_elemA.el1))
self.assertIsInstance(_elemA.el1, type(elemA.el1))
self.assertIsInstance(_elemB.el1, type(elemB.el1))
self.assertIsInstance(_elemB.el1, type(_elemA.el1))
self.assertIsInstance(_elemB.el2, type(_elemA.el1))
self.assertIsInstance(_elemB.el2, type(elemB.el1))
self.assertIsInstance(_elemB.el2, type(_elemA.el1))
def test_element_mutable_invalid_instance_override_container(self):
class Elem(dm.Element, options=(dm.ClassMutable, dm.ObjectMutable)):
val: list[int] = []
with self.assertRaises(dm.InvalidFieldValue):
Elem(val=["test"])
def test_element_mutable_invalid_instance_override_container_nested(self):
class ElemInner(dm.Element, options=(dm.ClassMutable, dm.ObjectMutable)):
val: list[int] = []
class ElemOuter(dm.Element, options=(dm.ClassMutable, dm.ObjectMutable)):
inner: ElemInner = ElemInner()
# with self.assertRaises(dm.InvalidFieldValue):
elem = ElemOuter(inner={"val": [1, 2, 3]})
self.assertEqual(elem.inner.val, [1, 2, 3])
def test_class_protocol(self):
self.assertIsInstance(dm.base_element.BaseElement, dm.interfaces.FreezableElement)
def test_wrong_annotated(self):
with self.assertRaises(dm.UnsupportedFieldType):
class Elem(dm.Appliance):
StrVar: list[Annotated[str, dm.LAMFieldInfo(doc="foo1")]] = ["default value"]
def test_wrong_annotation_union(self):
with self.assertRaises(dm.UnsupportedFieldType):
class Elem(dm.Appliance):
StrVar: str | int = "test"
with self.assertRaises(dm.UnsupportedFieldType):
class Elem(dm.Appliance):
StrVar: list[str | int] = "test"
with self.assertRaises(dm.UnsupportedFieldType):
class Elem(dm.Appliance):
StrVar: list[None | int] = "test"
def test_annotation_union(self):
class Elem(dm.Appliance):
strvar1: str | None = "test"
strvar1: str | None = None
ivar1: Union[int, None] = 12
ivar2: Union[int, None] = None
# ---------- main ----------
if __name__ == "__main__":
unittest.main()

830
test/test_feature.py Normal file
View File

@@ -0,0 +1,830 @@
# 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.frozen)
print(app1)
print(app1.Feature1)
print(app1.Feature1.__lam_schema__["VarStrInner"])
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")
class Appliance4(Appliance3):
VarStrOuter = "testvalue APPLIANCE4"
class Feature1(Appliance3.Feature1):
VarStrInner = "testvalue FEATURE1 modded 3"
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_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"
def test_initializer(self):
class App(dm.Appliance):
integ: int = 18
class F(dm.Feature):
nums: list[int] = [1, 2]
tag: str = "x"
@classmethod
def __initializer(cls):
cls.F.tag = "test"
cls.F.nums.append(3)
self.assertEqual(App.F.tag, "test")
self.assertEqual(App.F.nums, (1, 2, 3))
def test_initializer_nested(self):
class App(dm.Appliance):
integ: int = 18
class F(dm.Feature):
nums: list[int] = [1, 2]
tag: str = "x"
@classmethod
def __initializer(cls):
cls.tag = "test"
cls.nums.append(3)
self.assertEqual(App.F.tag, "test")
self.assertEqual(App.F.nums, (1, 2, 3))
def test_initializer_nested_dual(self):
class App(dm.Appliance):
integ: int = 18
class F(dm.Feature):
nums: list[int] = [1, 2]
tag: str = "x"
@classmethod
def __initializer(cls):
cls.tag = "test1"
cls.nums.append(3)
@classmethod
def __initializer(cls):
cls.F.tag = "test2"
cls.F.nums.append(4)
self.assertEqual(App.F.tag, "test2")
self.assertEqual(App.F.nums, (1, 2, 3, 4))
def test_container_frozen(self):
class App(dm.Appliance):
integ: int = 18
class F(dm.Feature):
nums: list[int] = [1, 2]
tag: str = "x"
with self.assertRaises(AttributeError):
App.F.nums.append(3)
def test_container_class_mutable_validation(self):
class App(dm.Appliance, options=(dm.ClassMutable)):
integ: int = 18
class F(dm.Feature):
nums: list[int] = [1, 2]
tag: str = "x"
App.F.nums.append("test")
with self.assertRaises(dm.InvalidFieldValue):
App.freeze_class()
with self.assertRaises(dm.InvalidFieldValue):
a = App()
def test_container_object_mutable_validation(self):
class App(dm.Appliance, options=(dm.ObjectMutable)):
integ: int = 18
class F(dm.Feature):
nums: list[int] = [1, 2]
tag: str = "x"
a = App()
a.F.nums.append("test")
with self.assertRaises(dm.InvalidFieldValue):
a.freeze()
def test_container_object_and_class_mutable_validation(self):
class App(dm.Appliance, options=(dm.ClassMutable, dm.ObjectMutable)):
integ: int = 18
class F(dm.Feature):
nums: list[int] = [1, 2]
tag: str = "x"
App.F.nums.append("test")
with self.assertRaises(dm.InvalidFieldValue):
a = App()
with self.assertRaises(dm.InvalidFieldValue):
App.freeze_class()
def test_nested_element_container_object_and_class_mutable(self):
class E(dm.Element):
val: str = "testelem"
i: list[int] = [1]
class App(dm.Appliance, options=(dm.ClassMutable, dm.ObjectMutable)):
integ: int = 18
class F(dm.Feature):
nums: list[int] = [1, 2]
tag: str = "x"
e: E = E(val="modified")
App.F.e.i.append(21)
self.assertEquals(App.F.e.i, [1, 21])
App.freeze_class()
with self.assertRaises(AttributeError):
App.F.e.i.append(22)
a = App()
self.assertEquals(a.F.e.i, [1, 21])
a.F.e.i.append(28)
self.assertEquals(a.F.e.i, [1, 21, 28])
a.freeze()
self.assertEquals(a.F.e.i, (1, 21, 28))
with self.assertRaises(AttributeError):
a.F.e.i.append(23)
self.assertEquals(a.F.e.i, (1, 21, 28))
def test_nested_element_container_object_and_class_mutable_validation(self):
class E(dm.Element):
val: str = "testelem"
i: list[int] = [1]
class App(dm.Appliance, options=(dm.ClassMutable, dm.ObjectMutable)):
integ: int = 18
class F(dm.Feature):
nums: list[int] = [1, 2]
tag: str = "x"
e: E = E(val="modified")
App.F.e.i.append("test")
with self.assertRaises(dm.InvalidFieldValue):
a = App()
def test_nested_element_container_object_and_class_mutable_validation2(self):
class E(dm.Element):
val: str = "testelem"
i: list[int] = [1]
class App(dm.Appliance, options=(dm.ClassMutable, dm.ObjectMutable)):
integ: int = 18
class F(dm.Feature):
nums: list[int] = [1, 2]
tag: str = "x"
e: E = E(val="modified")
a = App()
a.F.e.i.append("test")
with self.assertRaises(dm.InvalidFieldValue):
a.freeze()
# ---------- 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()

View File

@@ -1,221 +0,0 @@
# 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 pydantic import StrictInt, model_validator
from pydantic.fields import Field
print(__name__)
print(__package__)
from src import dabmodel
from typing import Annotated, Any
from uuid import uuid4
import json
from uuid import UUID
from datetime import datetime
testdir_path = Path(__file__).parent.resolve()
chdir(testdir_path.parent.resolve())
class UUIDEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, UUID):
# if the obj is uuid, we simply return the value of uuid
return obj.hex
elif isinstance(obj, datetime):
return str(obj)
return json.JSONEncoder.default(self, obj)
class MyAppliance(dabmodel.BaseAppliance):
app_specifi_integer_arg: Annotated[StrictInt | None, dabmodel.DABField(42)]
class MyFeature(dabmodel.BaseFeature):
@dabmodel.default_values_override
@classmethod
def __override_config__(cls, values):
print("!!! CONFIG Feature 1")
values["template_id"] = "421d61cb-e364-46d8-9b77-ec439f1fa666"
values["template_short_name"] = "my-feature-1"
values["template_long_name"] = "My feature template 1 !!"
values["template_description"] = """A very nice FEature 1"""
class MyFeature2(dabmodel.BaseFeature):
@dabmodel.default_values_override
@classmethod
def __override_config__(cls, values):
print("!!! CONFIG Feature 2")
values["template_id"] = "421d61cb-e364-46d8-9b77-ec439f1fa666"
values["template_short_name"] = "my-feature-2"
values["template_long_name"] = "My feature template 2 !!"
values["template_description"] = """A very nice FEature 2"""
@dabmodel.default_values_override
@classmethod
def __override_config__(cls, values):
print("!!! CONFIG Appliance 1")
print(f"!!!! {values['rootfs_size']}")
values["template_id"] = "421d61cb-e364-46d8-9b64-ec439f1faae8"
values["template_short_name"] = "my-app- tem 1"
values["template_long_name"] = "My appliance template 1 !!"
values["template_description"] = """A very nice Appliance 1"""
values["ram_size"] = 1024
cls.add_feature(cls.MyFeature())
cls.add_feature(cls.MyFeature2())
class MyAppliance2(MyAppliance):
@dabmodel.default_values_override
@classmethod
def __override_config__(cls, values):
print("!!! CONFIG Appliance 2")
print(f"!!!! {values['template_id']}")
values["template_id"] = "421d61cb-e664-46d8-9b64-ec439f1fafff"
values["template_short_name"] = "my-app- tem 2"
values["template_long_name"] = "My appliance template 2 !!"
values["template_description"] = """A very nice Appliance 2"""
cls.del_feature(MyAppliance.MyFeature)
# values["features"]["MyFeature2"].template_description = """Override feature desc"""
class MyAppliance3(dabmodel.BaseAppliance):
class MyFeature6(MyAppliance.MyFeature):
# testtt: Annotated[MyAppliance.MyFeature, Field(MyAppliance.MyFeature())] # error case
test_integer: Annotated[int, dabmodel.DABField(200, ge=0)]
@dabmodel.default_values_override
@classmethod
def __override_config__(cls, values):
print("!!! CONFIG Feature 1 (modified)")
values["template_id"] = "421d61cb-e364-46d8-9b77-ec439f1fa778"
values["template_short_name"] = "my-feature-1-bis"
values["test_integer"] = 666
class MyFeature7(dabmodel.BaseFeature):
# testtt: Annotated[MyAppliance.MyFeature, Field(MyAppliance.MyFeature())] # error case
test_integer_2: Annotated[int, dabmodel.DABField(759, ge=0)]
@dabmodel.default_values_override
@classmethod
def __override_config__(cls, values):
print("!!! CONFIG Feature 7")
values["template_id"] = "421d61cb-e364-46d8-ac55-ec439f1fa778"
values["template_short_name"] = "my-feature-7"
values["template_long_name"] = "My appliance template 7 !!"
values["template_description"] = """A very nice Appliance 7"""
values["test_integer_2"] = 3844
# testtt: Annotated[MyAppliance.MyFeature, Field(MyAppliance.MyFeature())] # error case
@dabmodel.default_values_override
@classmethod
def __override_config__(cls, values):
print("!!! CONFIG Appliance 3")
values["template_id"] = "421d61cb-e364-46d8-9b64-ec439f1faaaa"
values["template_short_name"] = "my-app- tem 3"
values["template_long_name"] = "My appliance template 3 !!"
values["template_description"] = """A very nice Appliance 3"""
values["ram_size"] = 3076
print("CREATE FEATURE")
cls.add_feature(cls.MyFeature6())
cls.add_feature(cls.MyFeature7())
print("!!! CONFIG Appliance 3 DONE")
class MyAppliance4(MyAppliance):
class MyFeature8(dabmodel.BaseFeature):
# testtt: Annotated[MyAppliance.MyFeature, Field(MyAppliance.MyFeature())] # error case (nested feature)
# test_integer_10: Annotated[int, dabmodel.DABField(3189, ge=0, toto="tata")] # error case (extra field)
test_integer_10: Annotated[int, dabmodel.DABField(3189, ge=0, toto="tata")]
@dabmodel.default_values_override
@classmethod
def __override_config__(cls, values):
print("!!! CONFIG Feature 8")
values["template_id"] = "421d61cb-e364-46d8-ac55-ec4398888778"
values["template_short_name"] = "my-feature-8"
values["template_long_name"] = "My appliance template 8 !!"
values["template_description"] = """A very nice Appliance 8"""
values["test_integer_10"] = 951753
# values["tete"] = 1 # error case (extra field in feature)
# testtt: Annotated[MyAppliance.MyFeature, Field(MyAppliance.MyFeature())] # error case (feature not in features[] list)
@dabmodel.default_values_override
@classmethod
def __override_config__(cls, values):
print("!!! CONFIG Appliance 4")
values["template_id"] = "421d1234-e364-46d8-9b64-ec439f1faaaa"
values["template_short_name"] = "my-app-tem 4"
values["template_long_name"] = "My appliance template 4 !!"
values["template_description"] = """A very nice Appliance 4"""
values["ram_size"] = 954
print("CREATE FEATURE")
cls.add_feature(cls.MyFeature8())
print("!!! CONFIG Appliance 4 DONE")
class TestModel(unittest.TestCase):
def setUp(self) -> None:
chdir(testdir_path.parent.resolve())
def test_version(self):
self.assertNotEqual(dabmodel.__version__, "?.?.?")
def test_model(self):
feature1 = MyAppliance.MyFeature()
print(feature1)
print(MyAppliance.MyFeature)
print(MyAppliance.MyFeature.__name__)
print(MyAppliance.MyFeature.__class__)
print("==")
print(feature1.model_dump_json(indent=1))
app = MyAppliance(dabinst_short_name="my-app-1", app_specifi_integer_arg=123)
app2 = MyAppliance2(dabinst_short_name="my-app-2", app_specifi_integer_arg=654)
app3 = MyAppliance3(dabinst_short_name="my-app-3", template_description="FORCED")
print(app.model_dump_json(indent=1))
print(app2.model_dump_json(indent=1))
print(app3.model_dump_json(indent=1))
app3 = MyAppliance3(dabinst_short_name="my-app-3", template_description="FORCED")
tmp_json = app3.dict()
tmp_json["features"]["MyFeature7"]["test_integer_2"] = 123
print(tmp_json)
recreated_obj = MyAppliance3.model_validate_json(json.dumps(tmp_json, cls=UUIDEncoder))
print(recreated_obj)
print(recreated_obj.model_dump_json(indent=1))
app4 = MyAppliance4(dabinst_short_name="my-app-4", template_description="FORCED2")
tmp_json = app4.dict()
tmp_json["features"]["MyFeature"]["template_description"] = "blablabla"
tmp_json["features"]["MyFeature2"]["template_description"] = "blablabla2"
print(tmp_json)
recreated_obj = MyAppliance4.model_validate_json(json.dumps(tmp_json, cls=UUIDEncoder))
print(recreated_obj)
print(recreated_obj.model_dump_json(indent=1))
# tmp_json["non-existing"] = "test" # error case
# tmp_json["features"]["non-existing"] = "test" # error case
# tmp_json["features"]["MyFeature"]["132"] = "test" # error case
recreated_obj = MyAppliance4.model_validate_json(json.dumps(tmp_json, cls=UUIDEncoder))
# app3.add_feature(MyAppliance.MyFeature()) # error case (add_feature not callable from instance)
for name in globals().keys():
print(name)