Compare commits

..

59 Commits
0.0.1 ... dev

Author SHA1 Message Date
cclecle
85a2ca2753 work 2025-10-12 22:45:56 +02:00
chacha
df50632458 work 2025-09-29 21:14:24 +02:00
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
cclecle
4c17436cea work 2024-12-07 21:57:12 +01:00
cclecle
a90ab4885b implement 2024-10-24 20:57:25 +01:00
cclecle
192dcc74f8 ok serialize 2024-10-21 23:46:07 +01:00
cclecle
171a2f1617 improve 2024-10-21 21:22:03 +01:00
cclecle
001ffbbbf1 good progress 2024-10-21 10:28:06 +01:00
cclecle
8ab6c8e179 first comit 2024-10-20 12:25:41 +01:00
28 changed files with 5861 additions and 99 deletions

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>{{project_name}}</name>
<name>dabmodel</name>
<comment></comment>
<projects>
</projects>

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 .test_module import test_function
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,43 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# 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/>.
"""Phasellus tellus lectus, volutpat eu dapibus ut, suscipit vel augue.
Tips:
Aliquam non leo vel libero sagittis viverra. Quisque lobortis nunc sit amet augue euismod laoreet.
Note:
Maecenas volutpat porttitor pretium. Aliquam suscipit quis nisi non imperdiet.
Note:
Vivamus et efficitur lorem, eget imperdiet tortor. Integer vel interdum sem.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING: # Only imports the below statements during type checking
pass
def test_function(testvar: int) -> int:
""" A test function that return testvar+1 and print "Hello world !"
Proin eget sapien eget ipsum efficitur mollis nec ac nibh.
Note:
Morbi id lectus maximus, condimentum nunc eget, porta felis. In tristique velit tortor.
Args:
testvar: any integer
Returns:
testvar+1
"""
print("Hello world !")
return testvar+1

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

@@ -0,0 +1,75 @@
"""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

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

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,35 +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 io import StringIO
from contextlib import redirect_stdout,redirect_stderr
from pathlib import Path
print(__name__)
print(__package__)
from src import dabmodel
testdir_path = Path(__file__).parent.resolve()
chdir(testdir_path.parent.resolve())
class Testtest_module(unittest.TestCase):
def setUp(self) -> None:
chdir(testdir_path.parent.resolve())
def test_version(self):
self.assertNotEqual(dabmodel.__version__,"?.?.?")
def test_test_module(self):
with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr:
self.assertEqual(dabmodel.test_function(41),42)
self.assertEqual(len(capted_stderr.getvalue()),0)
self.assertEqual(capted_stdout.getvalue().strip(),"Hello world !")