Compare commits
51 Commits
master
...
0.0.1.post
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25d5339946 | ||
|
|
6efd914de1 | ||
|
|
2e81b3f0e6 | ||
|
|
5d206ef266 | ||
|
|
ba993b14b9 | ||
|
|
5232ad66fc | ||
|
|
44554006db | ||
|
|
1e84fd691e | ||
|
|
2ac931edeb | ||
|
|
7ffe05f514 | ||
|
|
100d2cadcb | ||
|
|
71faefcf68 | ||
|
|
23f14a042d | ||
|
|
19a6c802bb | ||
|
|
0537d2d912 | ||
|
|
09237ff8cd | ||
|
|
ff55ef18d1 | ||
|
|
827e5e3f55 | ||
|
|
b9f5b83690 | ||
|
|
616a53578c | ||
|
|
d20712a72f | ||
|
|
2837b6439f | ||
|
|
b4d6ed6130 | ||
|
|
cd69fc22a8 | ||
|
|
9aec2d66cd | ||
|
|
af81ec5fd3 | ||
|
|
26e32a004f | ||
|
|
b7cbc50f79 | ||
|
|
86eee2e378 | ||
|
|
3e0defc574 | ||
|
|
f6e581381d | ||
|
|
981c5201a9 | ||
|
|
ab11052c8f | ||
|
|
4f5dade949 | ||
|
|
cce260bc5e | ||
|
|
915a4332ee | ||
|
|
4dca3eb9d1 | ||
|
|
e11c541139 | ||
|
|
637b50b325 | ||
|
|
f45c9cc8f3 | ||
|
|
95b0c298ce | ||
|
|
04a4cf7b36 | ||
|
|
f42a839cff | ||
|
|
7f3a4ef545 | ||
|
|
608c8a1010 | ||
|
|
210781f086 | ||
|
|
df966ccac4 | ||
|
|
29827b51bc | ||
|
|
7440731135 | ||
|
|
87682c2c9c | ||
|
|
0eef35e36f |
@@ -1,13 +1,20 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<?eclipse-pydev version="1.0"?><pydev_project>
|
<?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_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">
|
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python interpreter</pydev_property>
|
||||||
<path>/${PROJECT_DIR_NAME}/src</path>
|
|
||||||
<path>/${PROJECT_DIR_NAME}</path>
|
|
||||||
</pydev_pathproperty>
|
<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
|
||||||
|
|
||||||
</pydev_project>
|
<path>/${PROJECT_DIR_NAME}/src</path>
|
||||||
|
|
||||||
|
<path>/${PROJECT_DIR_NAME}</path>
|
||||||
|
|
||||||
|
</pydev_pathproperty>
|
||||||
|
|
||||||
|
|
||||||
|
</pydev_project>
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ classifiers = [
|
|||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
'importlib-metadata; python_version<"3.9"',
|
'importlib-metadata; python_version<"3.9"',
|
||||||
'packaging'
|
'packaging',
|
||||||
|
'frozendict',
|
||||||
|
'typeguard'
|
||||||
]
|
]
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
|
|
||||||
@@ -78,7 +80,7 @@ test = ["chacha_cicd_helper"]
|
|||||||
coverage-check = ["chacha_cicd_helper"]
|
coverage-check = ["chacha_cicd_helper"]
|
||||||
complexity-check = ["chacha_cicd_helper"]
|
complexity-check = ["chacha_cicd_helper"]
|
||||||
quality-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"]
|
doc-gen = ["chacha_cicd_helper"]
|
||||||
|
|
||||||
# [project.scripts]
|
# [project.scripts]
|
||||||
|
|||||||
@@ -11,4 +11,35 @@ Main module __init__ file.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from .__metadata__ import __version__, __Summuary__, __Name__
|
from .__metadata__ import __version__, __Summuary__, __Name__
|
||||||
from .model import DABField, BaseFeature, BaseAppliance, default_values_override
|
|
||||||
|
|
||||||
|
from .meta.element import ClassMutable, ObjectMutable
|
||||||
|
from .element import Element
|
||||||
|
from .lam_field.lam_field import LAMField
|
||||||
|
from .lam_field.lam_field_info import LAMFieldInfo
|
||||||
|
|
||||||
|
# from .LAMFields.FrozenLAMField import FrozenLAMField
|
||||||
|
from .appliance import Appliance
|
||||||
|
from .feature import Feature
|
||||||
|
|
||||||
|
|
||||||
|
from .exception import (
|
||||||
|
DABModelException,
|
||||||
|
MultipleInheritanceForbidden,
|
||||||
|
BrokenInheritance,
|
||||||
|
ReadOnlyField,
|
||||||
|
NotAnnotatedField,
|
||||||
|
ReadOnlyFieldAnnotation,
|
||||||
|
InvalidFieldValue,
|
||||||
|
InvalidFieldAnnotation,
|
||||||
|
IncompletelyAnnotatedField,
|
||||||
|
ImportForbidden,
|
||||||
|
FunctionForbidden,
|
||||||
|
InvalidFeatureInheritance,
|
||||||
|
FeatureNotBound,
|
||||||
|
UnsupportedFieldType,
|
||||||
|
NonExistingField,
|
||||||
|
InvalidFieldName,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [name for name in globals() if not name.startswith("_")]
|
||||||
|
|||||||
@@ -15,20 +15,26 @@ import warnings
|
|||||||
|
|
||||||
try: # pragma: no cover
|
try: # pragma: no cover
|
||||||
__version__ = version("dabmodel")
|
__version__ = version("dabmodel")
|
||||||
except PackageNotFoundError: # pragma: no cover
|
except PackageNotFoundError: # pragma: no cover
|
||||||
warnings.warn("can not read __version__, assuming local test context, setting it to ?.?.?")
|
warnings.warn(
|
||||||
|
"can not read __version__, assuming local test context, setting it to ?.?.?"
|
||||||
|
)
|
||||||
__version__ = "?.?.?"
|
__version__ = "?.?.?"
|
||||||
|
|
||||||
try: # pragma: no cover
|
try: # pragma: no cover
|
||||||
dist = distribution("dabmodel")
|
dist = distribution("dabmodel")
|
||||||
__Summuary__ = dist.metadata["Summary"]
|
__Summuary__ = dist.metadata["Summary"]
|
||||||
except PackageNotFoundError: # pragma: no cover
|
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"
|
__Summuary__ = "dabmodel description"
|
||||||
|
|
||||||
try: # pragma: no cover
|
try: # pragma: no cover
|
||||||
dist = distribution("dabmodel")
|
dist = distribution("dabmodel")
|
||||||
__Name__ = dist.metadata["Name"]
|
__Name__ = dist.metadata["Name"]
|
||||||
except PackageNotFoundError: # pragma: no cover
|
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"
|
__Name__ = "dabmodel"
|
||||||
|
|||||||
71
src/dabmodel/appliance.py
Normal file
71
src/dabmodel/appliance.py
Normal 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)
|
||||||
253
src/dabmodel/base_element.py
Normal file
253
src/dabmodel/base_element.py
Normal 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
87
src/dabmodel/defines.py
Normal 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
7
src/dabmodel/element.py
Normal 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
146
src/dabmodel/exception.py
Normal 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
50
src/dabmodel/feature.py
Normal 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
|
||||||
39
src/dabmodel/interfaces.py
Normal file
39
src/dabmodel/interfaces.py
Normal 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
|
||||||
0
src/dabmodel/lam_field/__init__.py
Normal file
0
src/dabmodel/lam_field/__init__.py
Normal file
17
src/dabmodel/lam_field/constraint.py
Normal file
17
src/dabmodel/lam_field/constraint.py
Normal 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
|
||||||
176
src/dabmodel/lam_field/lam_field.py
Normal file
176
src/dabmodel/lam_field/lam_field.py
Normal 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)
|
||||||
27
src/dabmodel/lam_field/lam_field_info.py
Normal file
27
src/dabmodel/lam_field/lam_field_info.py
Normal 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
|
||||||
0
src/dabmodel/meta/__init__.py
Normal file
0
src/dabmodel/meta/__init__.py
Normal file
242
src/dabmodel/meta/appliance.py
Normal file
242
src/dabmodel/meta/appliance.py
Normal 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)
|
||||||
844
src/dabmodel/meta/element.py
Normal file
844
src/dabmodel/meta/element.py
Normal 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)
|
||||||
24
src/dabmodel/meta/feature.py
Normal file
24
src/dabmodel/meta/feature.py
Normal 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)
|
||||||
@@ -1,267 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from abc import ABC, ABCMeta, abstractmethod
|
|
||||||
from uuid import uuid4
|
|
||||||
from typing import Annotated, ClassVar, Any, Self, TypeVar, TypeAlias, Generic, Union
|
|
||||||
from datetime import datetime
|
|
||||||
from copy import deepcopy, copy
|
|
||||||
from typing_extensions import dataclass_transform, get_origin
|
|
||||||
from pydantic import (
|
|
||||||
ConfigDict,
|
|
||||||
BaseModel,
|
|
||||||
StrictInt,
|
|
||||||
StrictStr,
|
|
||||||
constr,
|
|
||||||
ByteSize,
|
|
||||||
AwareDatetime,
|
|
||||||
UUID4,
|
|
||||||
model_validator,
|
|
||||||
field_validator,
|
|
||||||
field_serializer,
|
|
||||||
SerializeAsAny,
|
|
||||||
)
|
|
||||||
from pydantic.fields import Field, _Unset, PydanticUndefined
|
|
||||||
from pydantic._internal._model_construction import ModelMetaclass, PydanticModelField
|
|
||||||
from pydantic._internal._generics import PydanticGenericMetadata
|
|
||||||
from pydantic._internal._decorators import ensure_classmethod_based_on_signature
|
|
||||||
import pytz
|
|
||||||
import inspect
|
|
||||||
|
|
||||||
from runtype import issubclass as runtype_issubclass
|
|
||||||
|
|
||||||
|
|
||||||
class NoInstanceMethod:
|
|
||||||
"""Descriptor to forbid that other descriptors can be looked up on an instance"""
|
|
||||||
|
|
||||||
def __init__(self, descr, name=None):
|
|
||||||
self.descr = descr
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
def __set_name__(self, owner, name):
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
def __get__(self, instance, owner):
|
|
||||||
# enforce the instance cannot look up the attribute at all
|
|
||||||
if instance is not None:
|
|
||||||
raise AttributeError(f"{type(instance).__name__!r} has no attribute {self.name!r}")
|
|
||||||
# invoke any descriptor we are wrapping
|
|
||||||
return self.descr.__get__(instance, owner)
|
|
||||||
|
|
||||||
|
|
||||||
def DABField(default: Any = PydanticUndefined, *, final: bool | None = _Unset, finalize_only: bool | None = _Unset, **kwargs):
|
|
||||||
return Field(default, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
T_BaseElement = TypeVar("T_BaseElement", bound="BaseElement")
|
|
||||||
T_BaseElement_ConfigMethod_Arg: TypeAlias = dict[str, Any]
|
|
||||||
T_BaseElement_ConfigMethod: TypeAlias = "classmethod[T_BaseElement, [T_BaseElement_ConfigMethod_Arg], T_BaseElement_ConfigMethod_Arg]"
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigElement:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.default_values_override_methods: dict[T_BaseElement_ConfigMethod, None] = {}
|
|
||||||
self.main_build_method: dict[T_BaseElement_ConfigMethod, None] = {}
|
|
||||||
|
|
||||||
def __copy__(self) -> Self:
|
|
||||||
# we cannot deepcopy because of classmethods, so we do a manual enhanced copy
|
|
||||||
cls = self.__class__
|
|
||||||
result = cls.__new__(cls)
|
|
||||||
for k, v in self.__dict__.items():
|
|
||||||
setattr(result, k, copy(v))
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class IBaseElement(BaseModel, ABC):
|
|
||||||
_config_element: ClassVar[ConfigElement] = ConfigElement()
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass_transform(kw_only_default=True, field_specifiers=(PydanticModelField,))
|
|
||||||
class BaseElementMeta(ModelMetaclass, ABCMeta):
|
|
||||||
def __new__(
|
|
||||||
mcs,
|
|
||||||
cls_name: str,
|
|
||||||
bases: tuple[type[Any], ...],
|
|
||||||
namespace: dict[str, Any],
|
|
||||||
__pydantic_generic_metadata__: PydanticGenericMetadata | None = None,
|
|
||||||
__pydantic_reset_parent_namespace__: bool = True,
|
|
||||||
_create_model_module: str | None = None,
|
|
||||||
**kwargs: Any,
|
|
||||||
) -> type:
|
|
||||||
result = super().__new__(
|
|
||||||
mcs,
|
|
||||||
cls_name,
|
|
||||||
bases,
|
|
||||||
namespace,
|
|
||||||
__pydantic_generic_metadata__,
|
|
||||||
__pydantic_reset_parent_namespace__,
|
|
||||||
_create_model_module,
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
print(cls_name)
|
|
||||||
|
|
||||||
assert issubclass(result, IBaseElement), "Only IBaseElement subclasses are supported"
|
|
||||||
|
|
||||||
# forcing all Fields to be frozen
|
|
||||||
for _, field_val in result.model_fields.items():
|
|
||||||
field_val.frozen = True
|
|
||||||
|
|
||||||
# copying/forwarding base classes default-configs
|
|
||||||
if "_config_element" not in result.__dict__:
|
|
||||||
assert result.__base__ is not None, "Only IBaseElement subclasses are supported"
|
|
||||||
if issubclass(result.__base__, IBaseElement):
|
|
||||||
result._config_element = copy(result.__base__._config_element)
|
|
||||||
else:
|
|
||||||
result._config_element = ConfigElement()
|
|
||||||
|
|
||||||
# searching and storing current class default-configs
|
|
||||||
for _, method in result.__dict__.items():
|
|
||||||
if isinstance(method, classmethod):
|
|
||||||
if hasattr(method, "default_values_override"):
|
|
||||||
result._config_element.default_values_override_methods[method] = None
|
|
||||||
|
|
||||||
# todo: find a way to 'lock' and add restriction to a field after inheritance
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class BaseElement(
|
|
||||||
IBaseElement,
|
|
||||||
ABC,
|
|
||||||
validate_assignment=True,
|
|
||||||
# revalidate_instances="subclass-instances", # pydantic issue #10681
|
|
||||||
validate_default=True,
|
|
||||||
extra="forbid",
|
|
||||||
metaclass=BaseElementMeta,
|
|
||||||
):
|
|
||||||
class Config:
|
|
||||||
ignored_types = (NoInstanceMethod,)
|
|
||||||
|
|
||||||
template_id: Annotated[UUID4, DABField(..., repr=True)]
|
|
||||||
template_short_name: Annotated[
|
|
||||||
StrictStr, constr(strip_whitespace=True, to_lower=True, strict=True, max_length=16), DABField(..., repr=True)
|
|
||||||
]
|
|
||||||
template_long_name: Annotated[StrictStr | None, DABField()]
|
|
||||||
template_description: Annotated[StrictStr | None, DABField()]
|
|
||||||
_saved_default_value: ClassVar[dict[str, Any]]
|
|
||||||
|
|
||||||
@model_validator(mode="before")
|
|
||||||
@classmethod
|
|
||||||
def __default_values_override_hook__(cls, values: T_BaseElement_ConfigMethod_Arg) -> T_BaseElement_ConfigMethod_Arg:
|
|
||||||
# extracting default values that were set in model fields
|
|
||||||
cls._saved_default_value = dict()
|
|
||||||
for field_key, field_val in cls.model_fields.items():
|
|
||||||
assert field_val.annotation is not None, "all fields must have annotation"
|
|
||||||
assert not runtype_issubclass(
|
|
||||||
field_val.annotation, BaseFeature
|
|
||||||
), "Features can only be in Appliance's features[] dict attribute"
|
|
||||||
"""
|
|
||||||
if field_key == "features":
|
|
||||||
cls._saved_default_value[field_key] = dict()
|
|
||||||
for feat_key, feat_value in field_val.items():
|
|
||||||
cls._saved_default_value[field_key][feat_key] = feat_value.dict()
|
|
||||||
"""
|
|
||||||
if field_val.default != PydanticUndefined:
|
|
||||||
cls._saved_default_value[field_key] = deepcopy(field_val.default)
|
|
||||||
for method, _ in cls._config_element.default_values_override_methods.items():
|
|
||||||
method.__func__(cls, cls._saved_default_value)
|
|
||||||
|
|
||||||
cls._default_values_override_hook__input_apply__(values)
|
|
||||||
|
|
||||||
return cls._saved_default_value
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
@abstractmethod
|
|
||||||
def _default_values_override_hook__input_apply__(
|
|
||||||
cls,
|
|
||||||
values: T_BaseElement_ConfigMethod_Arg,
|
|
||||||
): ...
|
|
||||||
|
|
||||||
|
|
||||||
class BaseFeature(BaseElement, ABC):
|
|
||||||
@NoInstanceMethod
|
|
||||||
@classmethod
|
|
||||||
def _default_values_override_hook__input_apply__(
|
|
||||||
cls,
|
|
||||||
values: T_BaseElement_ConfigMethod_Arg,
|
|
||||||
):
|
|
||||||
print(f"BaseFeature._default_values_override_hook__input_apply__ {cls}")
|
|
||||||
# applying user-defined values
|
|
||||||
for attr_key, attr_val in values.items():
|
|
||||||
assert attr_key in cls.model_fields, f"given feature attribute does not exist ({attr_key})"
|
|
||||||
cls._saved_default_value[attr_key] = attr_val
|
|
||||||
print(f"BaseFeature._default_values_override_hook__input_apply__ {cls} DONE")
|
|
||||||
|
|
||||||
|
|
||||||
def default_values_override(func: T_BaseElement_ConfigMethod) -> T_BaseElement_ConfigMethod:
|
|
||||||
func = ensure_classmethod_based_on_signature(func)
|
|
||||||
setattr(func, "default_values_override", lambda: True)
|
|
||||||
return func
|
|
||||||
|
|
||||||
|
|
||||||
T_Feature = TypeVar("T_Feature", bound=BaseFeature)
|
|
||||||
|
|
||||||
|
|
||||||
def get_discriminator_value(v: Any) -> str:
|
|
||||||
if isinstance(v, dict):
|
|
||||||
return v.get("fruit", v.get("filling"))
|
|
||||||
return getattr(v, "fruit", getattr(v, "filling", None))
|
|
||||||
|
|
||||||
|
|
||||||
class BaseAppliance(Generic[T_Feature], BaseElement, ABC):
|
|
||||||
|
|
||||||
cpu_cnt: Annotated[StrictInt, DABField(1, gt=0)]
|
|
||||||
ram_size: Annotated[ByteSize, DABField(256, gt=128)]
|
|
||||||
swap_size: Annotated[ByteSize, DABField(200, ge=0)]
|
|
||||||
|
|
||||||
rootfs_size: Annotated[ByteSize, DABField(2048, ge=2048)]
|
|
||||||
|
|
||||||
dabinst_id: Annotated[UUID4, DABField(uuid4(), repr=True)]
|
|
||||||
dabinst_short_name: Annotated[
|
|
||||||
StrictStr, constr(strip_whitespace=True, to_lower=True, strict=True, max_length=16), DABField(..., repr=True)
|
|
||||||
]
|
|
||||||
dabinst_long_name: Annotated[StrictStr | None, DABField("")]
|
|
||||||
dabinst_description: Annotated[StrictStr | None, DABField("")]
|
|
||||||
dabinst_creationdate: Annotated[AwareDatetime | None, DABField(datetime.now(tz=pytz.utc))]
|
|
||||||
|
|
||||||
features: SerializeAsAny[dict[str, T_Feature]] = DABField({})
|
|
||||||
|
|
||||||
@NoInstanceMethod
|
|
||||||
@classmethod
|
|
||||||
def add_feature(cls, feat: T_Feature):
|
|
||||||
cls._saved_default_value["features"][type(feat).__name__] = feat.dict()
|
|
||||||
|
|
||||||
@NoInstanceMethod
|
|
||||||
@classmethod
|
|
||||||
def del_feature(cls, type_feat: type[T_Feature]):
|
|
||||||
del cls._saved_default_value["features"][type_feat.__name__]
|
|
||||||
|
|
||||||
@NoInstanceMethod
|
|
||||||
@classmethod
|
|
||||||
def get_feature(cls, type_feat: type[T_Feature]) -> T_Feature:
|
|
||||||
return cls._saved_default_value["features"][type_feat.__name__]
|
|
||||||
|
|
||||||
@NoInstanceMethod
|
|
||||||
@classmethod
|
|
||||||
def _default_values_override_hook__input_apply__(
|
|
||||||
cls,
|
|
||||||
values: T_BaseElement_ConfigMethod_Arg,
|
|
||||||
):
|
|
||||||
print(f"BaseAppliance._default_values_override_hook__input_apply__ {cls}")
|
|
||||||
# applying user-defined values
|
|
||||||
for attr_key, attr_val in values.items():
|
|
||||||
if attr_key == "features":
|
|
||||||
if cls._saved_default_value["features"] is None:
|
|
||||||
cls._saved_default_value["features"] = {}
|
|
||||||
for feature_key, feature_val in attr_val.items():
|
|
||||||
print(f"searching feature: {feature_key}")
|
|
||||||
assert hasattr(cls, feature_key), f"feature not found ({feature_key})"
|
|
||||||
cls_feature = getattr(cls, feature_key)
|
|
||||||
assert (
|
|
||||||
cls_feature is not None and inspect.isclass(cls_feature) and issubclass(cls_feature, BaseFeature),
|
|
||||||
"The requested feature does not exist in the current Appliance class tree",
|
|
||||||
)
|
|
||||||
cls._saved_default_value["features"][feature_key] = cls_feature(**feature_val)
|
|
||||||
else:
|
|
||||||
assert attr_key in cls.model_fields, f"given attribute does not exist ({attr_key})"
|
|
||||||
cls._saved_default_value[attr_key] = attr_val
|
|
||||||
print(f"BaseAppliance._default_values_override_hook__input_apply__ {cls} DONE")
|
|
||||||
974
src/dabmodel/tools.py
Normal file
974
src/dabmodel/tools.py
Normal file
@@ -0,0 +1,974 @@
|
|||||||
|
"""library's internal tools"""
|
||||||
|
|
||||||
|
from collections import ChainMap
|
||||||
|
from typing import Any, Annotated, get_origin, get_args, Union, Self, Optional, List, Dict, Tuple, Set, FrozenSet, Mapping, Callable
|
||||||
|
import typing
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from types import UnionType, NoneType
|
||||||
|
import types
|
||||||
|
from uuid import UUID
|
||||||
|
from datetime import datetime
|
||||||
|
import json
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
|
||||||
|
from frozendict import deepfreeze, frozendict
|
||||||
|
|
||||||
|
from .defines import ALLOWED_ANNOTATIONS, ALLOWED_MODEL_FIELDS_TYPES
|
||||||
|
from .exception import IncompletelyAnnotatedField, UnsupportedFieldType
|
||||||
|
|
||||||
|
|
||||||
|
class LAMJSONEncoder(json.JSONEncoder):
|
||||||
|
"""allows to JSON encode non supported data type"""
|
||||||
|
|
||||||
|
def default(self, o):
|
||||||
|
if isinstance(o, UUID):
|
||||||
|
# if the o is uuid, we simply return the value of uuid
|
||||||
|
return o.hex
|
||||||
|
if isinstance(o, datetime):
|
||||||
|
return str(o)
|
||||||
|
return json.JSONEncoder.default(self, o)
|
||||||
|
|
||||||
|
|
||||||
|
def LAMdeepfreeze(value):
|
||||||
|
"""recursive freeze helper function"""
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return deepfreeze(value)
|
||||||
|
if isinstance(value, set):
|
||||||
|
return frozenset(LAMdeepfreeze(v) for v in value)
|
||||||
|
if isinstance(value, list):
|
||||||
|
return tuple(LAMdeepfreeze(v) for v in value)
|
||||||
|
if isinstance(value, tuple):
|
||||||
|
return tuple(LAMdeepfreeze(v) for v in value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def is_data_attribute(name: str, value: any) -> bool:
|
||||||
|
if name.startswith("_"):
|
||||||
|
return False
|
||||||
|
if isinstance(value, (staticmethod, classmethod, property)):
|
||||||
|
return False
|
||||||
|
if inspect.isfunction(value) or inspect.ismethoddescriptor(value):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_annotation(ann):
|
||||||
|
if isinstance(ann, str):
|
||||||
|
# Safe eval against a **whitelist** only
|
||||||
|
return eval(ann, {"__builtins__": {}}, ALLOWED_ANNOTATIONS) # pylint: disable=eval-used
|
||||||
|
return ann
|
||||||
|
|
||||||
|
|
||||||
|
class AnnotationWalkerCtx:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
origin: Any,
|
||||||
|
args: Any,
|
||||||
|
layer: int,
|
||||||
|
parent: Optional[Self] = None,
|
||||||
|
allowed_types: set[type, ...] = frozenset(),
|
||||||
|
allowed_annotations: dict[str, Any] = {},
|
||||||
|
):
|
||||||
|
self.__origin = origin
|
||||||
|
self.args = args
|
||||||
|
self.__layer = layer
|
||||||
|
self.__parent = parent
|
||||||
|
|
||||||
|
self.__allowed_types: set[type, ...] = allowed_types
|
||||||
|
self.__allowed_annotations: dict[str, Any] = allowed_annotations
|
||||||
|
self.__ext: dict[Any, ChainMap] = {} # per-trigger namespaces (lazy)
|
||||||
|
|
||||||
|
# NEW: edge routing metadata (walker sets; triggers read)
|
||||||
|
self.__edge_role: str | None = None # 'elem' | 'key' | 'val' | 'branch' | 'annotated' | 'arg'
|
||||||
|
self.__edge_token: Any | None = None # index/key/branch-id/etc.
|
||||||
|
self.__arg_index: int | None = None # which schema arg of the parent this child is
|
||||||
|
|
||||||
|
@property
|
||||||
|
def origin(self) -> Any:
|
||||||
|
return self.__origin
|
||||||
|
|
||||||
|
@property
|
||||||
|
def layer(self) -> int:
|
||||||
|
return self.__layer
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parent(self) -> Self:
|
||||||
|
return self.__parent
|
||||||
|
|
||||||
|
@property
|
||||||
|
def allowed_types(self) -> FrozenSet[type]:
|
||||||
|
return self.__allowed_types
|
||||||
|
|
||||||
|
@property
|
||||||
|
def allowed_annotations(self) -> Mapping[str, Any]:
|
||||||
|
return self.__allowed_annotations
|
||||||
|
|
||||||
|
# NEW: read-only edge metadata for routing inside triggers
|
||||||
|
@property
|
||||||
|
def edge_role(self) -> str | None:
|
||||||
|
return self.__edge_role
|
||||||
|
|
||||||
|
@property
|
||||||
|
def edge_token(self) -> Any | None:
|
||||||
|
return self.__edge_token
|
||||||
|
|
||||||
|
@property
|
||||||
|
def arg_index(self) -> int | None:
|
||||||
|
return self.__arg_index
|
||||||
|
|
||||||
|
def ns(self, owner: Any) -> ChainMap:
|
||||||
|
"""
|
||||||
|
A per-trigger overlay namespace that inherits from parent ctx.
|
||||||
|
Use as: bag = ctx.ns(self); bag['whatever'] = ...
|
||||||
|
Lookups fall back to parent's bag automatically.
|
||||||
|
"""
|
||||||
|
if owner in self.__ext:
|
||||||
|
return self.__ext[owner]
|
||||||
|
|
||||||
|
# Determine the parent chain for this owner
|
||||||
|
parent_chain = ()
|
||||||
|
if self.__parent is not None and hasattr(self.__parent, "_AnnotationWalkerCtx__ext"):
|
||||||
|
parent_map = self.__parent._AnnotationWalkerCtx__ext.get(owner) # access private attr intentionally
|
||||||
|
if isinstance(parent_map, ChainMap):
|
||||||
|
# IMPORTANT: preserve the whole chain (not the ChainMap object as a single mapping)
|
||||||
|
parent_chain = tuple(parent_map.maps)
|
||||||
|
elif isinstance(parent_map, dict):
|
||||||
|
parent_chain = (parent_map,)
|
||||||
|
else:
|
||||||
|
parent_chain = ()
|
||||||
|
|
||||||
|
# Build a new ChainMap whose first map is this node's local overlay
|
||||||
|
cm = ChainMap({}, *parent_chain)
|
||||||
|
self.__ext[owner] = cm
|
||||||
|
return cm
|
||||||
|
|
||||||
|
# INTERNAL (walker only): set edge metadata on a child ctx
|
||||||
|
def _set_edge(self, *, role: str | None, token: Any | None, arg_index: int | None) -> None:
|
||||||
|
self.__edge_role = role
|
||||||
|
self.__edge_token = token
|
||||||
|
self.__arg_index = arg_index
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class TriggerResult:
|
||||||
|
# If provided, children won't be walked and this value is returned.
|
||||||
|
replace_with: Any | None = None
|
||||||
|
# If true, skip walking children but don't replace current node value.
|
||||||
|
skip_children: bool = False
|
||||||
|
# If provided, walker will restart processing with the given value
|
||||||
|
restart_with: Any | None = None # NEW
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def passthrough() -> Self:
|
||||||
|
return TriggerResult()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def replace(value: Any) -> Self:
|
||||||
|
return TriggerResult(replace_with=value, skip_children=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def skip() -> Self:
|
||||||
|
return TriggerResult(skip_children=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def restart(value: Any) -> Self:
|
||||||
|
print("Doo!")
|
||||||
|
return TriggerResult(restart_with=value)
|
||||||
|
|
||||||
|
|
||||||
|
class AnnotationTrigger:
|
||||||
|
|
||||||
|
def init_trigger(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def process_annotated(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process_union(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process_dict(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process_tuple(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process_list(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process_set(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process_unknown(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process_allowed(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process_exit(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def end_trigger(self, ctx: Optional[AnnotationWalkerCtx]) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class LAMSchemaValidation(AnnotationTrigger):
|
||||||
|
def init_trigger(self) -> None:
|
||||||
|
print(f"Initializing {self.__class__.__name__}")
|
||||||
|
|
||||||
|
def process_annotated(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
|
||||||
|
print("process_annotated")
|
||||||
|
print(ctx.origin)
|
||||||
|
print(ctx.args)
|
||||||
|
if len(ctx.args) != 2:
|
||||||
|
raise UnsupportedFieldType("Annotated[T,x] requires 2 parameters")
|
||||||
|
if ctx.parent is not None:
|
||||||
|
raise UnsupportedFieldType("Annotated[T,x] is only supported as parent annotation")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process_union(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
|
||||||
|
print("process_union")
|
||||||
|
print(ctx.args)
|
||||||
|
if (len(ctx.args) != 2) or (type(None) not in list(ctx.args)):
|
||||||
|
raise UnsupportedFieldType("Union[] is only supported to implement Optional[] (takes 2 parameters, including None)")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process_dict(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
|
||||||
|
print("process_dict")
|
||||||
|
if len(ctx.args) != 2:
|
||||||
|
raise IncompletelyAnnotatedField(f"Dict Annotation requires 2 inner definitions: {ctx.origin}")
|
||||||
|
if not ctx.args[0] in ctx.allowed_types:
|
||||||
|
raise IncompletelyAnnotatedField(f"Dict Key must be simple builtin: {ctx.origin}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process_tuple(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
|
||||||
|
print("process_tuple")
|
||||||
|
if len(ctx.args) == 0:
|
||||||
|
raise IncompletelyAnnotatedField(f"Annotation requires inner definition: {ctx.origin}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process_list(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
|
||||||
|
print("process_list")
|
||||||
|
if len(ctx.args) == 0:
|
||||||
|
raise IncompletelyAnnotatedField(f"Annotation requires inner definition: {ctx.origin}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process_set(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
|
||||||
|
print("process_set")
|
||||||
|
if len(ctx.args) == 0:
|
||||||
|
raise IncompletelyAnnotatedField(f"Annotation requires inner definition: {ctx.origin}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process_allowed(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
|
||||||
|
print("process_allowed")
|
||||||
|
if ctx.origin is type(None) or ctx.origin is None:
|
||||||
|
if ctx.parent is None or not (ctx.parent.origin is Union or ctx.parent.origin is UnionType):
|
||||||
|
raise IncompletelyAnnotatedField(f"None is only accepted with Union, to implement Optional[]")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process_exit(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
|
||||||
|
print(f"process_exit: {ctx.origin}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def end_trigger(self, ctx: AnnotationWalkerCtx):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DataValidation(AnnotationTrigger):
|
||||||
|
def __init__(self, value: Any) -> None:
|
||||||
|
self._root = value
|
||||||
|
|
||||||
|
def init_trigger(self) -> None:
|
||||||
|
self._seeded = False
|
||||||
|
|
||||||
|
def _bag(self, ctx: AnnotationWalkerCtx):
|
||||||
|
bag = ctx.ns(self)
|
||||||
|
if not self._seeded:
|
||||||
|
bag["value"] = self._root
|
||||||
|
bag["path"] = ()
|
||||||
|
self._seeded = True
|
||||||
|
return bag
|
||||||
|
|
||||||
|
def process_annotated(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
|
||||||
|
b = self._bag(ctx)
|
||||||
|
|
||||||
|
def process_union(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
|
||||||
|
b = self._bag(ctx)
|
||||||
|
|
||||||
|
def process_dict(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
|
||||||
|
b = self._bag(ctx)
|
||||||
|
|
||||||
|
def process_tuple(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
|
||||||
|
b = self._bag(ctx)
|
||||||
|
|
||||||
|
def process_list(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
|
||||||
|
b = self._bag(ctx)
|
||||||
|
|
||||||
|
def process_set(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
|
||||||
|
b = self._bag(ctx)
|
||||||
|
|
||||||
|
def process_unknown(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
|
||||||
|
b = self._bag(ctx)
|
||||||
|
|
||||||
|
def process_allowed(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
|
||||||
|
b = self._bag(ctx)
|
||||||
|
|
||||||
|
def process_exit(self, ctx: AnnotationWalkerCtx) -> None | TriggerResult:
|
||||||
|
b = self._bag(ctx)
|
||||||
|
|
||||||
|
def end_trigger(self, ctx: AnnotationWalkerCtx):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SchemaValidationError(TypeError):
|
||||||
|
def __init__(self, path: tuple[Any, ...], msg: str):
|
||||||
|
dotted = "".join(f"[{p}]" if isinstance(p, int) else (f".{p}" if path and i else str(p)) for i, p in enumerate(path)) or "<root>"
|
||||||
|
super().__init__(f"{dotted}: {msg}")
|
||||||
|
self.path = path
|
||||||
|
|
||||||
|
|
||||||
|
class HorizontalValidationTrigger(AnnotationTrigger):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
value: Any,
|
||||||
|
*,
|
||||||
|
strict_bool: bool = False,
|
||||||
|
collect_all: bool = True,
|
||||||
|
union_summary_max_per_branch: int = 3, # NEW
|
||||||
|
show_branch_types: bool = True, # NEW
|
||||||
|
):
|
||||||
|
self._root_value = value
|
||||||
|
self._strict_bool = strict_bool
|
||||||
|
self._collect_all = collect_all
|
||||||
|
self._union_summary_max = union_summary_max_per_branch
|
||||||
|
self._show_branch_types = show_branch_types
|
||||||
|
self._seeded = False
|
||||||
|
self._id_counter = 0
|
||||||
|
|
||||||
|
# ---------- utilities ----------
|
||||||
|
|
||||||
|
# ---------- utilities ----------
|
||||||
|
|
||||||
|
def _mk_cand(self, value: Any, path: tuple[Any, ...]):
|
||||||
|
cid = self._id_counter
|
||||||
|
self._id_counter += 1
|
||||||
|
return {"id": cid, "value": value, "path": path}
|
||||||
|
|
||||||
|
def _spawn_from(self, bag, parent_cand: dict, value: Any, path: tuple[Any, ...]):
|
||||||
|
"""
|
||||||
|
Create a child candidate inheriting union root id when inside a union branch.
|
||||||
|
"""
|
||||||
|
nc = self._mk_cand(value, path)
|
||||||
|
if "__union_branch_id" in bag:
|
||||||
|
# Ensure every descendant carries union-root id
|
||||||
|
nc["union_cid"] = parent_cand.get("union_cid", parent_cand["id"])
|
||||||
|
elif "union_cid" in parent_cand:
|
||||||
|
# Preserve if already present (e.g., nested under a branch)
|
||||||
|
nc["union_cid"] = parent_cand["union_cid"]
|
||||||
|
return nc
|
||||||
|
|
||||||
|
def _bag(self, ctx: AnnotationWalkerCtx):
|
||||||
|
bag = ctx.ns(self)
|
||||||
|
local = bag.maps[0]
|
||||||
|
parent_bag = ctx.parent.ns(self) if ctx.parent is not None else None
|
||||||
|
|
||||||
|
if not self._seeded and ctx.parent is None:
|
||||||
|
local["candidates"] = [self._mk_cand(self._root_value, ())]
|
||||||
|
local["errors"] = []
|
||||||
|
self._seeded = True
|
||||||
|
|
||||||
|
if "errors" not in local and parent_bag is not None:
|
||||||
|
local["errors"] = parent_bag.get("errors", [])
|
||||||
|
|
||||||
|
if ctx.parent is not None:
|
||||||
|
role = ctx.edge_role
|
||||||
|
if role == "elem":
|
||||||
|
local["candidates"] = list(parent_bag.get("pending_elem", []))
|
||||||
|
elif role == "key":
|
||||||
|
local["candidates"] = list(parent_bag.get("pending_key", []))
|
||||||
|
elif role == "val":
|
||||||
|
local["candidates"] = list(parent_bag.get("pending_val", []))
|
||||||
|
elif role == "arg":
|
||||||
|
local["candidates"] = list(parent_bag.get(f"pending_arg_{ctx.arg_index}", []))
|
||||||
|
elif role in ("branch", "annotated"):
|
||||||
|
local["candidates"] = list(parent_bag.get("candidates", []))
|
||||||
|
else:
|
||||||
|
local["candidates"] = list(parent_bag.get("candidates", []))
|
||||||
|
|
||||||
|
# If immediate child of a Union (branch), switch to branch-local errors
|
||||||
|
if ctx.parent is not None and ctx.parent.origin is UnionType and ctx.edge_role == "branch":
|
||||||
|
ubag = ctx.parent.ns(self)
|
||||||
|
branches = ubag.setdefault("union_branches_state", {})
|
||||||
|
bid = ctx.edge_token
|
||||||
|
bstate = branches.setdefault(bid, {"failed_ids": set(), "errors": []})
|
||||||
|
local["errors"] = bstate["errors"]
|
||||||
|
local["__union_branch_id"] = bid
|
||||||
|
local["__union_state_ref"] = ubag
|
||||||
|
# NEW: propagate the union scope id to this branch
|
||||||
|
local["__union_scope_id"] = ubag.get("__union_scope_id")
|
||||||
|
|
||||||
|
# Stamp union_cid for all candidates at branch entry
|
||||||
|
stamped = []
|
||||||
|
for c in local.get("candidates", []):
|
||||||
|
c2 = dict(c)
|
||||||
|
c2.setdefault("union_cid", c2["id"])
|
||||||
|
stamped.append(c2)
|
||||||
|
local["candidates"] = stamped
|
||||||
|
|
||||||
|
return bag
|
||||||
|
|
||||||
|
def _mark_branch_fail(self, bag, cand: dict):
|
||||||
|
if "__union_branch_id" in bag and "__union_state_ref" in bag:
|
||||||
|
bid = bag["__union_branch_id"]
|
||||||
|
uref = bag["__union_state_ref"]
|
||||||
|
branches = uref.setdefault("union_branches_state", {})
|
||||||
|
bstate = branches.setdefault(bid, {"failed_ids": set(), "errors": []})
|
||||||
|
root_id = cand.get("union_cid", cand["id"])
|
||||||
|
bstate["failed_ids"].add(root_id)
|
||||||
|
|
||||||
|
def _err(self, bag, cand, msg: str):
|
||||||
|
"""
|
||||||
|
Record an error. If we are inside a union branch, attach (cid, scope) so the
|
||||||
|
outer union can attribute it correctly to its branch/candidate.
|
||||||
|
"""
|
||||||
|
e = SchemaValidationError(cand["path"], msg)
|
||||||
|
|
||||||
|
# Find nearest union scope (walk ChainMap parents if needed)
|
||||||
|
def nearest_union_scope(b):
|
||||||
|
if "__union_scope_id" in b:
|
||||||
|
return b["__union_scope_id"]
|
||||||
|
# ChainMap.get() already searches parents; use get() with a sentinel
|
||||||
|
sentinel = object()
|
||||||
|
v = b.get("__union_scope_id", sentinel)
|
||||||
|
return None if v is sentinel else v
|
||||||
|
|
||||||
|
in_union_branch = ("__union_branch_id" in bag) and ("__union_state_ref" in bag)
|
||||||
|
if in_union_branch:
|
||||||
|
# union-root candidate id for attribution (propagated by spawn_from / branch entry)
|
||||||
|
root_id = cand.get("union_cid", cand["id"])
|
||||||
|
scope = nearest_union_scope(bag)
|
||||||
|
# Branch-local error bucket lives under union state
|
||||||
|
uref = bag["__union_state_ref"]
|
||||||
|
bid = bag["__union_branch_id"]
|
||||||
|
branches = uref.setdefault("union_branches_state", {})
|
||||||
|
bstate = branches.setdefault(bid, {"failed_ids": set(), "errors": []})
|
||||||
|
bstate["failed_ids"].add(root_id)
|
||||||
|
bstate["errors"].append({"cid": root_id, "scope": scope, "err": e})
|
||||||
|
return
|
||||||
|
|
||||||
|
# Outside any union branch: add to global errors (and optional fail-fast)
|
||||||
|
bag["errors"].append(e)
|
||||||
|
if not self._collect_all:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
def _check_leaf(self, bag, cand: dict, T: type):
|
||||||
|
v = cand["value"]
|
||||||
|
if T is type(None):
|
||||||
|
if v is not None:
|
||||||
|
self._err(bag, cand, "expected None")
|
||||||
|
return
|
||||||
|
if self._strict_bool and T is bool and type(v) is not bool:
|
||||||
|
self._err(bag, cand, f"expected bool, got {type(v).__name__}")
|
||||||
|
return
|
||||||
|
if v is None or not isinstance(v, T):
|
||||||
|
got = "None" if v is None else type(v).__name__
|
||||||
|
self._err(bag, cand, f"expected {T.__name__}, got {got}")
|
||||||
|
|
||||||
|
# ---------- entry hooks (seed pending; validate leaves; no aggregation here) ----------
|
||||||
|
|
||||||
|
def process_allowed(self, ctx: AnnotationWalkerCtx):
|
||||||
|
bag = self._bag(ctx)
|
||||||
|
T = ctx.origin
|
||||||
|
for cand in bag.get("candidates", []):
|
||||||
|
self._check_leaf(bag, cand, T)
|
||||||
|
|
||||||
|
def process_list(self, ctx: AnnotationWalkerCtx):
|
||||||
|
bag = self._bag(ctx)
|
||||||
|
cands = bag.get("candidates", [])
|
||||||
|
if len(ctx.args) != 1:
|
||||||
|
for cand in cands:
|
||||||
|
self._err(bag, cand, "List[T] requires 1 argument")
|
||||||
|
return
|
||||||
|
pending = []
|
||||||
|
for cand in cands:
|
||||||
|
v = cand["value"]
|
||||||
|
if not isinstance(v, list):
|
||||||
|
self._err(bag, cand, f"expected list, got {type(v).__name__}")
|
||||||
|
continue
|
||||||
|
base = cand["path"]
|
||||||
|
for i in range(len(v)):
|
||||||
|
pending.append(self._spawn_from(bag, cand, v[i], base + (i,)))
|
||||||
|
bag["pending_elem"] = pending
|
||||||
|
|
||||||
|
def process_tuple(self, ctx: AnnotationWalkerCtx):
|
||||||
|
bag = self._bag(ctx)
|
||||||
|
cands = bag.get("candidates", [])
|
||||||
|
vararg = len(ctx.args) == 2 and ctx.args[1] is Ellipsis
|
||||||
|
if vararg:
|
||||||
|
pending = []
|
||||||
|
for cand in cands:
|
||||||
|
v = cand["value"]
|
||||||
|
if not isinstance(v, tuple):
|
||||||
|
self._err(bag, cand, f"expected tuple, got {type(v).__name__}")
|
||||||
|
continue
|
||||||
|
base = cand["path"]
|
||||||
|
for i in range(len(v)):
|
||||||
|
pending.append(self._spawn_from(bag, cand, v[i], base + (i,)))
|
||||||
|
bag["pending_elem"] = pending
|
||||||
|
else:
|
||||||
|
arity = len(ctx.args)
|
||||||
|
slots: dict[int, list] = {}
|
||||||
|
for cand in cands:
|
||||||
|
v = cand["value"]
|
||||||
|
if not isinstance(v, tuple):
|
||||||
|
self._err(bag, cand, f"expected tuple, got {type(v).__name__}")
|
||||||
|
continue
|
||||||
|
if len(v) != arity:
|
||||||
|
self._err(bag, cand, f"expected tuple len {arity}, got {len(v)}")
|
||||||
|
continue
|
||||||
|
base = cand["path"]
|
||||||
|
for i, elem in enumerate(v):
|
||||||
|
slots.setdefault(i, []).append(self._spawn_from(bag, cand, elem, base + (i,)))
|
||||||
|
for i, group in slots.items():
|
||||||
|
bag[f"pending_arg_{i}"] = group
|
||||||
|
|
||||||
|
def process_set(self, ctx: AnnotationWalkerCtx):
|
||||||
|
bag = self._bag(ctx)
|
||||||
|
cands = bag.get("candidates", [])
|
||||||
|
if len(ctx.args) != 1:
|
||||||
|
for cand in cands:
|
||||||
|
self._err(bag, cand, "Set[T]/FrozenSet[T] requires 1 argument")
|
||||||
|
return
|
||||||
|
pending = []
|
||||||
|
for cand in cands:
|
||||||
|
v = cand["value"]
|
||||||
|
if not isinstance(v, (set, frozenset)):
|
||||||
|
self._err(bag, cand, f"expected set/frozenset, got {type(v).__name__}")
|
||||||
|
continue
|
||||||
|
base = cand["path"]
|
||||||
|
for e in v:
|
||||||
|
pending.append(self._spawn_from(bag, cand, e, base + ("<elem>",)))
|
||||||
|
bag["pending_elem"] = pending
|
||||||
|
|
||||||
|
def process_dict(self, ctx: AnnotationWalkerCtx):
|
||||||
|
bag = self._bag(ctx)
|
||||||
|
cands = bag.get("candidates", [])
|
||||||
|
if len(ctx.args) != 2:
|
||||||
|
for cand in cands:
|
||||||
|
self._err(bag, cand, "Dict[K,V] requires 2 arguments")
|
||||||
|
return
|
||||||
|
pkeys, pvals = [], []
|
||||||
|
for cand in cands:
|
||||||
|
v = cand["value"]
|
||||||
|
if not isinstance(v, dict):
|
||||||
|
self._err(bag, cand, f"expected dict, got {type(v).__name__}")
|
||||||
|
continue
|
||||||
|
base = cand["path"]
|
||||||
|
for k, val in v.items():
|
||||||
|
pkeys.append(self._spawn_from(bag, cand, k, base + ("<key>",)))
|
||||||
|
pvals.append(self._spawn_from(bag, cand, val, base + (k,)))
|
||||||
|
bag["pending_key"] = pkeys
|
||||||
|
bag["pending_val"] = pvals
|
||||||
|
|
||||||
|
def process_annotated(self, ctx: AnnotationWalkerCtx):
|
||||||
|
# No checks here; inner T will validate routed candidates.
|
||||||
|
self._bag(ctx)
|
||||||
|
|
||||||
|
def process_union(self, ctx: AnnotationWalkerCtx):
|
||||||
|
bag = self._bag(ctx)
|
||||||
|
# Candidates arriving at THIS union node (e.g., per list element when union is element type)
|
||||||
|
cands = bag.get("candidates", [])
|
||||||
|
# Track these exact candidate ids for this union scope
|
||||||
|
bag["union_candidate_ids"] = [c["id"] for c in cands]
|
||||||
|
# One entry per branch; each branch accumulates scoped errors keyed by union-root cid
|
||||||
|
bag.setdefault("union_branches_state", {}) # bid -> {"failed_ids": set(), "errors": []}
|
||||||
|
bag["union_branch_labels"] = [self._pretty_type(a) for a in ctx.args]
|
||||||
|
# Scope id distinguishes nested unions; only errors tagged with this scope count here
|
||||||
|
bag["__union_scope_id"] = id(ctx)
|
||||||
|
|
||||||
|
# ---------- exit (aggregation only) ----------
|
||||||
|
|
||||||
|
def process_exit(self, ctx: AnnotationWalkerCtx):
|
||||||
|
if ctx.origin is not UnionType:
|
||||||
|
return None
|
||||||
|
|
||||||
|
bag = ctx.ns(self)
|
||||||
|
root_errors = bag.get("errors", [])
|
||||||
|
branches = bag.get("union_branches_state", {})
|
||||||
|
cand_ids = bag.get("union_candidate_ids", [])
|
||||||
|
labels = bag.get("union_branch_labels", [])
|
||||||
|
scope_id = bag.get("__union_scope_id") # THIS union's scope
|
||||||
|
|
||||||
|
# Build: per_branch_errors[bid][cid] -> [SchemaValidationError, ...] filtered to THIS union scope
|
||||||
|
per_branch_errors: dict[int, dict[int, list[SchemaValidationError]]] = {}
|
||||||
|
for bid, state in branches.items():
|
||||||
|
berrs = state.get("errors", [])
|
||||||
|
m: dict[int, list[SchemaValidationError]] = {}
|
||||||
|
for item in berrs:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
# Only errors tagged for THIS union scope
|
||||||
|
if item.get("scope") != scope_id:
|
||||||
|
continue
|
||||||
|
cid = item.get("cid")
|
||||||
|
err = item.get("err")
|
||||||
|
if cid is not None and isinstance(err, SchemaValidationError):
|
||||||
|
m.setdefault(cid, []).append(err)
|
||||||
|
elif isinstance(item, SchemaValidationError):
|
||||||
|
# Legacy/plain error: attribute to all failed ids of this branch
|
||||||
|
for cid in state.get("failed_ids", set()):
|
||||||
|
m.setdefault(cid, []).append(item)
|
||||||
|
# Dedupe identical (path, text)
|
||||||
|
for cid, lst in m.items():
|
||||||
|
seen = set()
|
||||||
|
uniq = []
|
||||||
|
for e in lst:
|
||||||
|
key = (e.path, str(e))
|
||||||
|
if key not in seen:
|
||||||
|
seen.add(key)
|
||||||
|
uniq.append(e)
|
||||||
|
m[cid] = uniq
|
||||||
|
per_branch_errors[bid] = m
|
||||||
|
|
||||||
|
# Decide per candidate of THIS union
|
||||||
|
for cid in cand_ids:
|
||||||
|
branch_ok = any(len(per_branch_errors.get(bid, {}).get(cid, [])) == 0 for bid in branches.keys())
|
||||||
|
if branch_ok:
|
||||||
|
continue # some branch matched, good
|
||||||
|
|
||||||
|
# Pretty summary for this failing candidate
|
||||||
|
# Get the path for this union's candidate; fall back to <root> if absent
|
||||||
|
cands_here = bag.get("candidates", [])
|
||||||
|
path = next((c["path"] for c in cands_here if c.get("id") == cid), ())
|
||||||
|
|
||||||
|
lines = ["no union branch matched; tried:"]
|
||||||
|
for bid, state in branches.items():
|
||||||
|
label = labels[bid] if bid < len(labels) else f"branch {bid}"
|
||||||
|
errs = per_branch_errors.get(bid, {}).get(cid, [])
|
||||||
|
if not errs:
|
||||||
|
lines.append(f" - {label}: mismatch")
|
||||||
|
continue
|
||||||
|
N = self._union_summary_max
|
||||||
|
shown = errs[:N]
|
||||||
|
lines.append(f" - {label}: {len(errs)} issue(s)")
|
||||||
|
for e in shown:
|
||||||
|
lines.append(f" - {e}")
|
||||||
|
if len(errs) > N:
|
||||||
|
lines.append(f" - (+{len(errs) - N} more)")
|
||||||
|
|
||||||
|
root_errors.append(SchemaValidationError(path, "\n".join(lines)))
|
||||||
|
|
||||||
|
# Also, if THIS union is nested inside an enclosing union branch, and ALL branches
|
||||||
|
# of THIS union failed for a candidate, bubble a single 'mismatch' up to that parent
|
||||||
|
# so the parent union can mark its branch as failed for that union-root cid.
|
||||||
|
if ("__union_state_ref" in bag) and ("__union_branch_id" in bag):
|
||||||
|
uref = bag["__union_state_ref"] # enclosing union's bag
|
||||||
|
up_bid = bag["__union_branch_id"] # which branch we're in
|
||||||
|
up_branches = uref.setdefault("union_branches_state", {})
|
||||||
|
up_state = up_branches.setdefault(up_bid, {"failed_ids": set(), "errors": []})
|
||||||
|
up_scope = uref.get("__union_scope_id")
|
||||||
|
|
||||||
|
# Map THIS union's local cid to the enclosing union's root cid via union_cid
|
||||||
|
local_cands = bag.get("candidates", [])
|
||||||
|
# Fallback map: if local_cands missing, assume 1:1
|
||||||
|
fallback_map = {cid: cid for cid in cand_ids}
|
||||||
|
map_local_to_outer = {}
|
||||||
|
for lc in local_cands:
|
||||||
|
local_id = lc.get("id")
|
||||||
|
outer_root = lc.get("union_cid", local_id)
|
||||||
|
map_local_to_outer[local_id] = outer_root
|
||||||
|
|
||||||
|
for cid in cand_ids:
|
||||||
|
branch_ok = any(len(per_branch_errors.get(bid, {}).get(cid, [])) == 0 for bid in branches.keys())
|
||||||
|
if branch_ok:
|
||||||
|
continue # do not bubble success
|
||||||
|
|
||||||
|
outer_cid = map_local_to_outer.get(cid, fallback_map[cid])
|
||||||
|
up_state["failed_ids"].add(outer_cid)
|
||||||
|
# lightweight marker so parent has at least one error on record for this cid
|
||||||
|
up_state["errors"].append({"cid": outer_cid, "scope": up_scope, "err": SchemaValidationError((), "mismatch")})
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def end_trigger(self, ctx: Optional[AnnotationWalkerCtx]):
|
||||||
|
if ctx is None:
|
||||||
|
return
|
||||||
|
errors = ctx.ns(self).get("errors", [])
|
||||||
|
if errors:
|
||||||
|
# Raise them together; swap for your preferred error carrier if needed
|
||||||
|
raise ExceptionGroup("schema validation failed", errors)
|
||||||
|
|
||||||
|
# --- pretty type for messages (best-effort) ---
|
||||||
|
def _pretty_type(self, t: Any) -> str:
|
||||||
|
origin = get_origin(t) or t
|
||||||
|
args = get_args(t)
|
||||||
|
try:
|
||||||
|
if origin is UnionType or origin is Union:
|
||||||
|
return " | ".join(self._pretty_type(a) for a in args)
|
||||||
|
if origin in (list, tuple, set, frozenset, dict, Annotated):
|
||||||
|
if origin is dict and len(args) == 2:
|
||||||
|
return f"dict[{self._pretty_type(args[0])}, {self._pretty_type(args[1])}]"
|
||||||
|
if origin in (list, set, frozenset) and len(args) == 1:
|
||||||
|
name = "list" if origin is list else ("set" if origin is set else "frozenset")
|
||||||
|
return f"{name}[{self._pretty_type(args[0])}]"
|
||||||
|
if origin is tuple:
|
||||||
|
if len(args) == 2 and args[1] is Ellipsis:
|
||||||
|
return f"tuple[{self._pretty_type(args[0])}, ...]"
|
||||||
|
return f"tuple[{', '.join(self._pretty_type(a) for a in args)}]"
|
||||||
|
if origin is Annotated and args:
|
||||||
|
return f"Annotated[{self._pretty_type(args[0])}, ...]"
|
||||||
|
if isinstance(origin, type):
|
||||||
|
return origin.__name__
|
||||||
|
return str(t)
|
||||||
|
except Exception:
|
||||||
|
return repr(t)
|
||||||
|
|
||||||
|
|
||||||
|
class AnnotationWalker:
|
||||||
|
DEFAULT_ALLOWED_TYPES = frozenset({str, int, float, complex, bool, bytes, NoneType})
|
||||||
|
DEFAULT_ALLOWED_ANNOTATIONS: dict[str, Any] = frozendict(
|
||||||
|
{
|
||||||
|
"Union": Union,
|
||||||
|
"Optional": Optional,
|
||||||
|
"List": List,
|
||||||
|
"Dict": Dict,
|
||||||
|
"Tuple": Tuple,
|
||||||
|
"Set": Set,
|
||||||
|
# "FrozenSet": FrozenSet,
|
||||||
|
"Annotated": Annotated,
|
||||||
|
# builtins:
|
||||||
|
"int": int,
|
||||||
|
"str": str,
|
||||||
|
"float": float,
|
||||||
|
"bool": bool,
|
||||||
|
"complex": complex,
|
||||||
|
"bytes": bytes,
|
||||||
|
"None": type(None),
|
||||||
|
"list": list,
|
||||||
|
"dict": dict,
|
||||||
|
"set": set,
|
||||||
|
# "frozenset": frozenset,
|
||||||
|
"tuple": tuple,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, ann: Any, triggers: tuple[AnnotationTrigger, ...], **kwargs):
|
||||||
|
if not triggers:
|
||||||
|
raise RuntimeError("AnnotationWalker requires trigger(s)")
|
||||||
|
|
||||||
|
# Normalize triggers into instances
|
||||||
|
insts: list[AnnotationTrigger] = []
|
||||||
|
for t in triggers if isinstance(triggers, tuple) else (triggers,):
|
||||||
|
if isinstance(t, AnnotationTrigger):
|
||||||
|
insts.append(t)
|
||||||
|
elif isinstance(t, type) and issubclass(t, AnnotationTrigger):
|
||||||
|
insts.append(t())
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"Unsupported trigger: {t}")
|
||||||
|
self._triggers = tuple(insts)
|
||||||
|
|
||||||
|
# Allowed types / annotations
|
||||||
|
atypes = set(type(self).DEFAULT_ALLOWED_TYPES)
|
||||||
|
if "ex_allowed_types" in kwargs:
|
||||||
|
atypes.update(kwargs["ex_allowed_types"])
|
||||||
|
self._allowed_types = frozenset(atypes)
|
||||||
|
|
||||||
|
annots = dict(type(self).DEFAULT_ALLOWED_ANNOTATIONS)
|
||||||
|
if "ex_allowed_annotations" in kwargs:
|
||||||
|
annots.update(kwargs["ex_allowed_annotations"])
|
||||||
|
self._allowed_annotations = frozendict(annots)
|
||||||
|
|
||||||
|
# Annotation can be string
|
||||||
|
self.__ann = ann
|
||||||
|
if isinstance(ann, str):
|
||||||
|
self.__ann = eval(ann, {"__builtins__": {}}, self._allowed_annotations)
|
||||||
|
|
||||||
|
def run(self) -> TriggerResult:
|
||||||
|
for t in self._triggers:
|
||||||
|
t.init_trigger()
|
||||||
|
self.root_ctx = None
|
||||||
|
self._walk(self.__ann, None)
|
||||||
|
for t in self._triggers:
|
||||||
|
t.end_trigger(self.root_ctx)
|
||||||
|
|
||||||
|
# --- Helpers ---
|
||||||
|
|
||||||
|
def _new_ctx(self, origin, args, layer, parent):
|
||||||
|
return AnnotationWalkerCtx(origin, args, layer, parent, self._allowed_types, self._allowed_annotations)
|
||||||
|
|
||||||
|
def _apply_triggers(self, method: str, ctx: AnnotationWalkerCtx) -> TriggerResult:
|
||||||
|
final = TriggerResult.passthrough()
|
||||||
|
for t in self._triggers:
|
||||||
|
res = getattr(t, method)(ctx)
|
||||||
|
if not res:
|
||||||
|
continue
|
||||||
|
if res.restart_with is not None:
|
||||||
|
return res # short-circuit on restart
|
||||||
|
if res.replace_with is not None:
|
||||||
|
final = TriggerResult.replace(res.replace_with)
|
||||||
|
if res.skip_children:
|
||||||
|
final = TriggerResult(
|
||||||
|
replace_with=final.replace_with,
|
||||||
|
skip_children=True,
|
||||||
|
)
|
||||||
|
return final
|
||||||
|
|
||||||
|
def _apply_exits(self, ctx: AnnotationWalkerCtx) -> Any | None:
|
||||||
|
"""
|
||||||
|
Run exits in order: type-specific (if implemented) then generic `process_exit`.
|
||||||
|
Only `replace_with` is honored at exit; last `replace_with` wins.
|
||||||
|
"""
|
||||||
|
final = None
|
||||||
|
for t in self._triggers:
|
||||||
|
res = t.process_exit(ctx)
|
||||||
|
if res and (res.replace_with is not None):
|
||||||
|
final = res.replace_with
|
||||||
|
return final
|
||||||
|
|
||||||
|
def _handle_with_triggers(
|
||||||
|
self,
|
||||||
|
trigger_name: str,
|
||||||
|
ctx: AnnotationWalkerCtx,
|
||||||
|
args_handler: Callable[[AnnotationWalkerCtx], Any] | None = None,
|
||||||
|
) -> Any:
|
||||||
|
# ENTER
|
||||||
|
res = self._apply_triggers(trigger_name, ctx)
|
||||||
|
if res.restart_with is not None:
|
||||||
|
return self._walk(res.restart_with, ctx.parent)
|
||||||
|
if res.replace_with is not None:
|
||||||
|
exit_val = self._apply_exits(ctx)
|
||||||
|
return exit_val if exit_val is not None else res.replace_with
|
||||||
|
|
||||||
|
node_value = None
|
||||||
|
if not res.skip_children:
|
||||||
|
if args_handler:
|
||||||
|
node_value = args_handler(ctx)
|
||||||
|
else:
|
||||||
|
# DEFAULT: descend once per schema arg; mark as positional arg
|
||||||
|
node_value = tuple(self._walk_child(a, ctx, arg_index=i, role="arg", token=i) for i, a in enumerate(ctx.args))
|
||||||
|
|
||||||
|
# EXIT
|
||||||
|
exit_val = self._apply_exits(ctx)
|
||||||
|
return exit_val if exit_val is not None else node_value
|
||||||
|
|
||||||
|
def _walk_args_tuple(self, ctx: AnnotationWalkerCtx):
|
||||||
|
# Tuple[T, ...] (variadic): one schema child, mark role='elem'
|
||||||
|
if len(ctx.args) == 2 and ctx.args[1] is Ellipsis:
|
||||||
|
return (self._walk_child(ctx.args[0], ctx, arg_index=0, role="elem", token=None), Ellipsis)
|
||||||
|
# Fixed tuple: each positional arg gets role='arg'
|
||||||
|
return tuple(self._walk_child(a, ctx, arg_index=i, role="arg", token=i) for i, a in enumerate(ctx.args))
|
||||||
|
|
||||||
|
# --- Dispatcher ---
|
||||||
|
|
||||||
|
def _walk(self, type_: Any, parent_ctx: Optional[AnnotationWalkerCtx]) -> Any:
|
||||||
|
# For logs only: show the calling layer
|
||||||
|
print(f"[{parent_ctx.layer if parent_ctx else 0}] walking through: {type_}")
|
||||||
|
|
||||||
|
origin = get_origin(type_) or type_
|
||||||
|
if origin is None:
|
||||||
|
origin = NoneType
|
||||||
|
if origin is Union:
|
||||||
|
origin = UnionType
|
||||||
|
if not isinstance(origin, type):
|
||||||
|
raise RuntimeError("Annotation must be using type(s), not instances")
|
||||||
|
|
||||||
|
args = get_args(type_)
|
||||||
|
|
||||||
|
# IMPORTANT:
|
||||||
|
# If caller (_walk_child) already constructed a child ctx with edge metadata,
|
||||||
|
# reuse that object as *the* ctx for this node instead of allocating a new one.
|
||||||
|
if isinstance(parent_ctx, AnnotationWalkerCtx) and parent_ctx.origin is origin and parent_ctx.args == args:
|
||||||
|
ctx = parent_ctx
|
||||||
|
else:
|
||||||
|
# Root or internal calls that didn't prebuild the ctx
|
||||||
|
layer = 0 if parent_ctx is None else parent_ctx.layer + 1
|
||||||
|
ctx = self._new_ctx(origin, args, layer, parent_ctx)
|
||||||
|
|
||||||
|
# Remember root ctx for end_trigger()
|
||||||
|
if ctx.parent is None:
|
||||||
|
self.root_ctx = ctx
|
||||||
|
|
||||||
|
print(origin)
|
||||||
|
match origin:
|
||||||
|
case typing.Annotated:
|
||||||
|
# inner type gets role='annotated'
|
||||||
|
return self._handle_with_triggers(
|
||||||
|
"process_annotated",
|
||||||
|
ctx,
|
||||||
|
args_handler=lambda c: self._walk_child(c.args[0], c, arg_index=0, role="annotated", token=None) if c.args else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
case types.UnionType:
|
||||||
|
# branches get role='branch' and token=branch index
|
||||||
|
return self._handle_with_triggers(
|
||||||
|
"process_union",
|
||||||
|
ctx,
|
||||||
|
args_handler=lambda c: tuple(self._walk_child(a, c, arg_index=i, role="branch", token=i) for i, a in enumerate(c.args)),
|
||||||
|
)
|
||||||
|
|
||||||
|
case _ if issubclass(origin, dict):
|
||||||
|
# arg0=key (role='key'), arg1=value (role='val')
|
||||||
|
return self._handle_with_triggers(
|
||||||
|
"process_dict",
|
||||||
|
ctx,
|
||||||
|
args_handler=lambda c: (
|
||||||
|
self._walk_child(c.args[0], c, arg_index=0, role="key", token=None),
|
||||||
|
self._walk_child(c.args[1], c, arg_index=1, role="val", token=None),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
case _ if issubclass(origin, tuple):
|
||||||
|
return self._handle_with_triggers("process_tuple", ctx, self._walk_args_tuple)
|
||||||
|
|
||||||
|
case _ if issubclass(origin, list):
|
||||||
|
# single child T with role='elem'
|
||||||
|
return self._handle_with_triggers(
|
||||||
|
"process_list",
|
||||||
|
ctx,
|
||||||
|
args_handler=lambda c: self._walk_child(c.args[0], c, arg_index=0, role="elem", token=None) if c.args else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
case _ if issubclass(origin, set):
|
||||||
|
# single child T with role='elem'
|
||||||
|
return self._handle_with_triggers(
|
||||||
|
"process_set",
|
||||||
|
ctx,
|
||||||
|
args_handler=lambda c: self._walk_child(c.args[0], c, arg_index=0, role="elem", token=None) if c.args else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
case _ if origin in self._allowed_types:
|
||||||
|
return self._handle_with_triggers("process_allowed", ctx)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
res = self._apply_triggers("process_unknown", ctx)
|
||||||
|
if res.restart_with is not None:
|
||||||
|
return self._walk(res.restart_with, ctx.parent)
|
||||||
|
if res.replace_with is not None:
|
||||||
|
return res.replace_with
|
||||||
|
raise UnsupportedFieldType(f"Not supported Field: {ctx.origin}, Supported list: {self._allowed_types}")
|
||||||
|
|
||||||
|
def _walk_child(
|
||||||
|
self,
|
||||||
|
type_expr: Any,
|
||||||
|
parent_ctx: AnnotationWalkerCtx,
|
||||||
|
*,
|
||||||
|
arg_index: int,
|
||||||
|
role: str | None,
|
||||||
|
token: Any | None,
|
||||||
|
) -> Any:
|
||||||
|
origin = get_origin(type_expr) or type_expr
|
||||||
|
if origin is None:
|
||||||
|
origin = NoneType
|
||||||
|
if origin is Union:
|
||||||
|
origin = UnionType
|
||||||
|
if not isinstance(origin, type):
|
||||||
|
raise RuntimeError("Annotation must be using type(s), not instances")
|
||||||
|
|
||||||
|
args = get_args(type_expr)
|
||||||
|
child = self._new_ctx(origin, args, parent_ctx.layer + 1, parent_ctx)
|
||||||
|
# stamp routing metadata for triggers
|
||||||
|
child._set_edge(role=role, token=token, arg_index=arg_index)
|
||||||
|
|
||||||
|
# IMPORTANT: recurse with the CHILD as the parent_ctx for the next step,
|
||||||
|
# so the walker uses this child ctx (with edge metadata & incremented layer).
|
||||||
|
return self._walk(type_expr, child)
|
||||||
53
test/test_AnnotationTool.py
Normal file
53
test/test_AnnotationTool.py
Normal 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
1825
test/test_appliance.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,37 +0,0 @@
|
|||||||
from pydantic import BaseModel, SerializeAsAny
|
|
||||||
|
|
||||||
|
|
||||||
class commonbase(
|
|
||||||
BaseModel,
|
|
||||||
revalidate_instances="subclass-instances", # toogle to generate error
|
|
||||||
): ...
|
|
||||||
|
|
||||||
|
|
||||||
class basechild(commonbase):
|
|
||||||
test_val: int = 1
|
|
||||||
|
|
||||||
|
|
||||||
class derivedchild(basechild):
|
|
||||||
test_val2: int = 2
|
|
||||||
|
|
||||||
|
|
||||||
class container(commonbase):
|
|
||||||
|
|
||||||
ct_child_1: dict[str, basechild] = {}
|
|
||||||
ct_child_2: SerializeAsAny[dict[str, basechild]] = {}
|
|
||||||
ct_child_3: dict[str, SerializeAsAny[basechild]] = {}
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
test_val = container(
|
|
||||||
ct_child_1={"test1": derivedchild()},
|
|
||||||
ct_child_2={"test2": derivedchild()},
|
|
||||||
ct_child_3={"test3": derivedchild()},
|
|
||||||
)
|
|
||||||
|
|
||||||
print(test_val.model_dump_json(indent=1))
|
|
||||||
|
|
||||||
print(test_val.model_dump())
|
|
||||||
assert "test_val2" not in test_val.model_dump()["ct_child_1"]["test1"]
|
|
||||||
assert "test_val2" in test_val.model_dump()["ct_child_2"]["test2"]
|
|
||||||
assert "test_val2" in test_val.model_dump()["ct_child_3"]["test3"]
|
|
||||||
862
test/test_element.py
Normal file
862
test/test_element.py
Normal 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
830
test/test_feature.py
Normal 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
166
test/test_feature_bind.py
Normal 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()
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
# dabmodel (c) by chacha
|
|
||||||
#
|
|
||||||
# dabmodel is licensed under a
|
|
||||||
# Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Unported License.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the license along with this
|
|
||||||
# work. If not, see <https://creativecommons.org/licenses/by-nc-sa/4.0/>.
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
from os import chdir
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from pydantic import StrictInt, model_validator
|
|
||||||
from pydantic.fields import Field
|
|
||||||
|
|
||||||
print(__name__)
|
|
||||||
print(__package__)
|
|
||||||
|
|
||||||
from src import dabmodel
|
|
||||||
from typing import Annotated, Any
|
|
||||||
from uuid import uuid4
|
|
||||||
import json
|
|
||||||
from uuid import UUID
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
testdir_path = Path(__file__).parent.resolve()
|
|
||||||
chdir(testdir_path.parent.resolve())
|
|
||||||
|
|
||||||
|
|
||||||
class UUIDEncoder(json.JSONEncoder):
|
|
||||||
def default(self, obj):
|
|
||||||
if isinstance(obj, UUID):
|
|
||||||
# if the obj is uuid, we simply return the value of uuid
|
|
||||||
return obj.hex
|
|
||||||
elif isinstance(obj, datetime):
|
|
||||||
return str(obj)
|
|
||||||
return json.JSONEncoder.default(self, obj)
|
|
||||||
|
|
||||||
|
|
||||||
class MyAppliance(dabmodel.BaseAppliance):
|
|
||||||
app_specifi_integer_arg: Annotated[StrictInt | None, dabmodel.DABField(42)]
|
|
||||||
|
|
||||||
class MyFeature(dabmodel.BaseFeature):
|
|
||||||
@dabmodel.default_values_override
|
|
||||||
@classmethod
|
|
||||||
def __override_config__(cls, values):
|
|
||||||
print("!!! CONFIG Feature 1")
|
|
||||||
values["template_id"] = "421d61cb-e364-46d8-9b77-ec439f1fa666"
|
|
||||||
values["template_short_name"] = "my-feature-1"
|
|
||||||
values["template_long_name"] = "My feature template 1 !!"
|
|
||||||
values["template_description"] = """A very nice FEature 1"""
|
|
||||||
|
|
||||||
class MyFeature2(dabmodel.BaseFeature):
|
|
||||||
@dabmodel.default_values_override
|
|
||||||
@classmethod
|
|
||||||
def __override_config__(cls, values):
|
|
||||||
print("!!! CONFIG Feature 2")
|
|
||||||
values["template_id"] = "421d61cb-e364-46d8-9b77-ec439f1fa666"
|
|
||||||
values["template_short_name"] = "my-feature-2"
|
|
||||||
values["template_long_name"] = "My feature template 2 !!"
|
|
||||||
values["template_description"] = """A very nice FEature 2"""
|
|
||||||
|
|
||||||
@dabmodel.default_values_override
|
|
||||||
@classmethod
|
|
||||||
def __override_config__(cls, values):
|
|
||||||
print("!!! CONFIG Appliance 1")
|
|
||||||
print(f"!!!! {values['rootfs_size']}")
|
|
||||||
values["template_id"] = "421d61cb-e364-46d8-9b64-ec439f1faae8"
|
|
||||||
values["template_short_name"] = "my-app- tem 1"
|
|
||||||
values["template_long_name"] = "My appliance template 1 !!"
|
|
||||||
values["template_description"] = """A very nice Appliance 1"""
|
|
||||||
values["ram_size"] = 1024
|
|
||||||
cls.add_feature(cls.MyFeature())
|
|
||||||
cls.add_feature(cls.MyFeature2())
|
|
||||||
|
|
||||||
|
|
||||||
class MyAppliance2(MyAppliance):
|
|
||||||
@dabmodel.default_values_override
|
|
||||||
@classmethod
|
|
||||||
def __override_config__(cls, values):
|
|
||||||
print("!!! CONFIG Appliance 2")
|
|
||||||
print(f"!!!! {values['template_id']}")
|
|
||||||
values["template_id"] = "421d61cb-e664-46d8-9b64-ec439f1fafff"
|
|
||||||
values["template_short_name"] = "my-app- tem 2"
|
|
||||||
values["template_long_name"] = "My appliance template 2 !!"
|
|
||||||
values["template_description"] = """A very nice Appliance 2"""
|
|
||||||
cls.del_feature(MyAppliance.MyFeature)
|
|
||||||
# values["features"]["MyFeature2"].template_description = """Override feature desc"""
|
|
||||||
|
|
||||||
|
|
||||||
class MyAppliance3(dabmodel.BaseAppliance):
|
|
||||||
|
|
||||||
class MyFeature6(MyAppliance.MyFeature):
|
|
||||||
# testtt: Annotated[MyAppliance.MyFeature, Field(MyAppliance.MyFeature())] # error case
|
|
||||||
test_integer: Annotated[int, dabmodel.DABField(200, ge=0)]
|
|
||||||
|
|
||||||
@dabmodel.default_values_override
|
|
||||||
@classmethod
|
|
||||||
def __override_config__(cls, values):
|
|
||||||
print("!!! CONFIG Feature 1 (modified)")
|
|
||||||
values["template_id"] = "421d61cb-e364-46d8-9b77-ec439f1fa778"
|
|
||||||
values["template_short_name"] = "my-feature-1-bis"
|
|
||||||
values["test_integer"] = 666
|
|
||||||
|
|
||||||
class MyFeature7(dabmodel.BaseFeature):
|
|
||||||
# testtt: Annotated[MyAppliance.MyFeature, Field(MyAppliance.MyFeature())] # error case
|
|
||||||
test_integer_2: Annotated[int, dabmodel.DABField(759, ge=0)]
|
|
||||||
|
|
||||||
@dabmodel.default_values_override
|
|
||||||
@classmethod
|
|
||||||
def __override_config__(cls, values):
|
|
||||||
print("!!! CONFIG Feature 7")
|
|
||||||
values["template_id"] = "421d61cb-e364-46d8-ac55-ec439f1fa778"
|
|
||||||
values["template_short_name"] = "my-feature-7"
|
|
||||||
values["template_long_name"] = "My appliance template 7 !!"
|
|
||||||
values["template_description"] = """A very nice Appliance 7"""
|
|
||||||
values["test_integer_2"] = 3844
|
|
||||||
|
|
||||||
# testtt: Annotated[MyAppliance.MyFeature, Field(MyAppliance.MyFeature())] # error case
|
|
||||||
|
|
||||||
@dabmodel.default_values_override
|
|
||||||
@classmethod
|
|
||||||
def __override_config__(cls, values):
|
|
||||||
print("!!! CONFIG Appliance 3")
|
|
||||||
values["template_id"] = "421d61cb-e364-46d8-9b64-ec439f1faaaa"
|
|
||||||
values["template_short_name"] = "my-app- tem 3"
|
|
||||||
values["template_long_name"] = "My appliance template 3 !!"
|
|
||||||
values["template_description"] = """A very nice Appliance 3"""
|
|
||||||
values["ram_size"] = 3076
|
|
||||||
print("CREATE FEATURE")
|
|
||||||
cls.add_feature(cls.MyFeature6())
|
|
||||||
cls.add_feature(cls.MyFeature7())
|
|
||||||
print("!!! CONFIG Appliance 3 DONE")
|
|
||||||
|
|
||||||
|
|
||||||
class MyAppliance4(MyAppliance):
|
|
||||||
|
|
||||||
class MyFeature8(dabmodel.BaseFeature):
|
|
||||||
# testtt: Annotated[MyAppliance.MyFeature, Field(MyAppliance.MyFeature())] # error case (nested feature)
|
|
||||||
# test_integer_10: Annotated[int, dabmodel.DABField(3189, ge=0, toto="tata")] # error case (extra field)
|
|
||||||
test_integer_10: Annotated[int, dabmodel.DABField(3189, ge=0, toto="tata")]
|
|
||||||
|
|
||||||
@dabmodel.default_values_override
|
|
||||||
@classmethod
|
|
||||||
def __override_config__(cls, values):
|
|
||||||
print("!!! CONFIG Feature 8")
|
|
||||||
values["template_id"] = "421d61cb-e364-46d8-ac55-ec4398888778"
|
|
||||||
values["template_short_name"] = "my-feature-8"
|
|
||||||
values["template_long_name"] = "My appliance template 8 !!"
|
|
||||||
values["template_description"] = """A very nice Appliance 8"""
|
|
||||||
values["test_integer_10"] = 951753
|
|
||||||
# values["tete"] = 1 # error case (extra field in feature)
|
|
||||||
|
|
||||||
# testtt: Annotated[MyAppliance.MyFeature, Field(MyAppliance.MyFeature())] # error case (feature not in features[] list)
|
|
||||||
|
|
||||||
@dabmodel.default_values_override
|
|
||||||
@classmethod
|
|
||||||
def __override_config__(cls, values):
|
|
||||||
print("!!! CONFIG Appliance 4")
|
|
||||||
values["template_id"] = "421d1234-e364-46d8-9b64-ec439f1faaaa"
|
|
||||||
values["template_short_name"] = "my-app-tem 4"
|
|
||||||
values["template_long_name"] = "My appliance template 4 !!"
|
|
||||||
values["template_description"] = """A very nice Appliance 4"""
|
|
||||||
values["ram_size"] = 954
|
|
||||||
print("CREATE FEATURE")
|
|
||||||
cls.add_feature(cls.MyFeature8())
|
|
||||||
print("!!! CONFIG Appliance 4 DONE")
|
|
||||||
|
|
||||||
|
|
||||||
class TestModel(unittest.TestCase):
|
|
||||||
def setUp(self) -> None:
|
|
||||||
chdir(testdir_path.parent.resolve())
|
|
||||||
|
|
||||||
def test_version(self):
|
|
||||||
self.assertNotEqual(dabmodel.__version__, "?.?.?")
|
|
||||||
|
|
||||||
def test_model(self):
|
|
||||||
|
|
||||||
feature1 = MyAppliance.MyFeature()
|
|
||||||
print(feature1)
|
|
||||||
print(MyAppliance.MyFeature)
|
|
||||||
print(MyAppliance.MyFeature.__name__)
|
|
||||||
print(MyAppliance.MyFeature.__class__)
|
|
||||||
print("==")
|
|
||||||
print(feature1.model_dump_json(indent=1))
|
|
||||||
|
|
||||||
app = MyAppliance(dabinst_short_name="my-app-1", app_specifi_integer_arg=123)
|
|
||||||
app2 = MyAppliance2(dabinst_short_name="my-app-2", app_specifi_integer_arg=654)
|
|
||||||
app3 = MyAppliance3(dabinst_short_name="my-app-3", template_description="FORCED")
|
|
||||||
|
|
||||||
print(app.model_dump_json(indent=1))
|
|
||||||
print(app2.model_dump_json(indent=1))
|
|
||||||
print(app3.model_dump_json(indent=1))
|
|
||||||
|
|
||||||
app3 = MyAppliance3(dabinst_short_name="my-app-3", template_description="FORCED")
|
|
||||||
tmp_json = app3.dict()
|
|
||||||
tmp_json["features"]["MyFeature7"]["test_integer_2"] = 123
|
|
||||||
print(tmp_json)
|
|
||||||
recreated_obj = MyAppliance3.model_validate_json(json.dumps(tmp_json, cls=UUIDEncoder))
|
|
||||||
print(recreated_obj)
|
|
||||||
print(recreated_obj.model_dump_json(indent=1))
|
|
||||||
|
|
||||||
app4 = MyAppliance4(dabinst_short_name="my-app-4", template_description="FORCED2")
|
|
||||||
tmp_json = app4.dict()
|
|
||||||
tmp_json["features"]["MyFeature"]["template_description"] = "blablabla"
|
|
||||||
tmp_json["features"]["MyFeature2"]["template_description"] = "blablabla2"
|
|
||||||
print(tmp_json)
|
|
||||||
recreated_obj = MyAppliance4.model_validate_json(json.dumps(tmp_json, cls=UUIDEncoder))
|
|
||||||
print(recreated_obj)
|
|
||||||
print(recreated_obj.model_dump_json(indent=1))
|
|
||||||
|
|
||||||
# tmp_json["non-existing"] = "test" # error case
|
|
||||||
# tmp_json["features"]["non-existing"] = "test" # error case
|
|
||||||
# tmp_json["features"]["MyFeature"]["132"] = "test" # error case
|
|
||||||
|
|
||||||
recreated_obj = MyAppliance4.model_validate_json(json.dumps(tmp_json, cls=UUIDEncoder))
|
|
||||||
|
|
||||||
# app3.add_feature(MyAppliance.MyFeature()) # error case (add_feature not callable from instance)
|
|
||||||
|
|
||||||
for name in globals().keys():
|
|
||||||
print(name)
|
|
||||||
Reference in New Issue
Block a user