This commit is contained in:
cclecle
2025-09-03 01:14:09 +02:00
parent 210781f086
commit 608c8a1010
3 changed files with 716 additions and 140 deletions

View File

@@ -11,4 +11,17 @@ Main module __init__ file.
"""
from .__metadata__ import __version__, __Summuary__, __Name__
from .model import BaseAppliance, BaseFeature
from .model import (
BaseAppliance,
BaseFeature,
DABModelException,
MultipleInheritanceForbidden,
BrokenInheritance,
ReadOnlyField,
NewFieldForbidden,
NotAnnotatedField,
ReadOnlyFieldAnnotation,
InvalidFieldValue,
InvalidFieldAnnotation,
IncompletelyAnnotatedField,
)

View File

@@ -1,61 +1,116 @@
# from __future__ import annotations
from typing import Optional, TypeVar, Generic, Union, get_origin, get_args, List, Dict, Any, Tuple, Set, Annotated, FrozenSet
from types import UnionType
from frozendict import deepfreeze
from copy import deepcopy, copy
from pprint import pprint
from typing import Optional, TypeVar, Generic, Union, get_origin, get_args, List, Dict, Any
from typeguard import check_type, TypeCheckError, CollectionCheckStrategy
from pprintpp import pprint
from copy import deepcopy
ALLOWED_ANNOTATIONS = {
"Union": Union,
"Optional": Optional,
"List": List,
"Dict": Dict,
"Tuple": Tuple,
"Set": Set,
"FrozenSet": FrozenSet,
"Annotated": Annotated,
# builtins:
"int": int,
"str": str,
"float": float,
"bool": bool,
"complex": complex,
"bytes": bytes,
"None": type(None),
"list": list,
"dict": dict,
"set": set,
"frozenset": frozenset,
"tuple": tuple,
}
from typeguard import check_type, TypeCheckError
from types import MappingProxyType
ALLOWED_MODEL_FIELDS_TYPES = (str, int, float, complex, bool, bytearray)
ALLOWED_MODEL_FIELDS_CONTAINERS = (dict, list, set, frozenset)
ALLOWED_MODEL_FIELDS_TYPES = (str, int, float, complex, bool, bytes)
ALLOWED_MODEL_FIELDS_CONTAINERS = (dict, list, set, frozenset, tuple)
TV_ALLOWED_MODEL_FIELDS_TYPES = TypeVar("TV_ALLOWED_MODEL_FIELDS_TYPES", *ALLOWED_MODEL_FIELDS_TYPES, *ALLOWED_MODEL_FIELDS_CONTAINERS)
class DABModelSchema: ...
class DABModelException(Exception): ...
def __check_type__(_type) -> bool:
class MultipleInheritanceForbidden(DABModelException): ...
class BrokenInheritance(DABModelException): ...
class ReadOnlyField(DABModelException): ...
class NewFieldForbidden(DABModelException): ...
class InvalidFieldAnnotation(DABModelException): ...
class NotAnnotatedField(InvalidFieldAnnotation): ...
class IncompletelyAnnotatedField(InvalidFieldAnnotation): ...
class ReadOnlyFieldAnnotation(DABModelException): ...
class InvalidFieldValue(DABModelException): ...
def _resolve_annotation(ann):
if isinstance(ann, str):
# Safe eval against a **whitelist** only
return eval(ann, {"__builtins__": {}}, ALLOWED_ANNOTATIONS)
return ann
def _peel_annotated(t: Any) -> Any:
# If you ever allow Annotated[T, ...], peel to T
while True:
origin = get_origin(t)
if origin is None:
return t
name = getattr(origin, "__name__", "") or getattr(origin, "__qualname__", "") or str(origin)
if "Annotated" in name:
args = get_args(t)
t = args[0] if args else t
else:
return t
def __check_annotation_definition__(_type) -> bool:
_type = _peel_annotated(_type)
# handle Optional[] and Union[None,...]
if get_origin(_type) is Union and type(None) in get_args(_type):
res = []
for inner_type in [_ for _ in get_args(_type) if _ is not type(None)]:
res.append(__check_type__(inner_type))
return all(res)
if (get_origin(_type) is Union or get_origin(_type) is UnionType) and type(None) in get_args(_type):
return all([__check_annotation_definition__(_) for _ in get_args(_type) if _ is not type(None)])
# handle List[...]
if get_origin(_type) is list:
inner = get_args(_type)
if len(inner) != 1:
return False
return __check_type__(inner)
# handle other Union[...]
if get_origin(_type) is Union or get_origin(_type) is UnionType:
return all([__check_annotation_definition__(_) for _ in get_args(_type)])
# handle Dict[...]
if get_origin(_type) is dict:
inner = get_args(_type)
if len(inner) != 2:
return False
key = inner[0] in ALLOWED_MODEL_FIELDS_TYPES
val = __check_type__(inner[1])
return key and val
raise IncompletelyAnnotatedField(f"Dict Annotation requires 2 inner definitions: {_type}")
return _peel_annotated(inner[0]) in ALLOWED_MODEL_FIELDS_TYPES and __check_annotation_definition__(inner[1])
# handle tuple (,)
if type(_type) is tuple:
res = []
for inner_type in _type:
res.append(__check_type__(inner_type))
return all(res)
# handle Set[],Tuple[],FrozenSet[]
if get_origin(_type) in [set, frozenset, tuple]:
res = []
for inner_type in get_args(_type):
res.append(__check_type__(inner_type))
return all(res)
# handle Set[],Tuple[],FrozenSet[],List[]
if get_origin(_type) in [set, frozenset, tuple, list]:
inner_types = get_args(_type)
if len(inner_types) == 0:
raise IncompletelyAnnotatedField(f"Annotation requires inner definition: {_type}")
return all([__check_annotation_definition__(_) for _ in inner_types])
if _type in ALLOWED_MODEL_FIELDS_TYPES:
return True
@@ -65,10 +120,24 @@ def __check_type__(_type) -> bool:
class Constraint: ...
def _deepfreeze(value):
if isinstance(value, dict):
return deepfreeze(value)
elif isinstance(value, set):
return frozenset(_deepfreeze(v) for v in value)
elif isinstance(value, list):
return tuple(_deepfreeze(v) for v in value)
elif isinstance(value, tuple):
return tuple(_deepfreeze(v) for v in value)
return value
class DABField(Generic[TV_ALLOWED_MODEL_FIELDS_TYPES]):
def __init__(self, name: str, v: Optional[TV_ALLOWED_MODEL_FIELDS_TYPES], a: Any):
self._constraints: List[Constraint] = []
self._name: str = name
self._source: Optional[type] = None
self._default_value: Optional[TV_ALLOWED_MODEL_FIELDS_TYPES] = v
self._value: Optional[TV_ALLOWED_MODEL_FIELDS_TYPES] = v
self._annotations: Any = a
self._documentation: str = ""
@@ -76,6 +145,9 @@ class DABField(Generic[TV_ALLOWED_MODEL_FIELDS_TYPES]):
def add_documentation(self, d: str) -> None:
self._documentation = d
def add_source(self, s: type) -> None:
self._source = s
def add_constraint(self, c: Constraint) -> None:
self._constraints.append(c)
@@ -83,32 +155,38 @@ class DABField(Generic[TV_ALLOWED_MODEL_FIELDS_TYPES]):
self._value = v
def render(self) -> TV_ALLOWED_MODEL_FIELDS_TYPES:
if isinstance(self._value, dict):
return MappingProxyType(self._value)
elif isinstance(self._value, set):
return frozenset(self._value)
return self._value
return _deepfreeze(self._value)
def render_default(self) -> TV_ALLOWED_MODEL_FIELDS_TYPES:
return _deepfreeze(self._default_value)
def get_annotation(self) -> Any:
return self._annotations
class BaseMeta(type):
def __new__(mcls, name, bases, namespace):
print("__NEW__ Defining:", name, "with keys:", list(namespace))
# print("__NEW__ Defining:", name, "with keys:", list(namespace))
if len(bases) > 1:
raise RuntimeError("Multiple inheritance is not supported by dabmodel")
raise MultipleInheritanceForbidden("Multiple inheritance is not supported by dabmodel")
elif len(bases) == 0: # base class (BaseElement)
...
namespace["__DABSchema__"] = dict()
else: # standard inheritance
for field in [_ for _ in dir(bases[0]) if isinstance(_, DABField)]:
print(field)
# check class tree origin
if "__DABSchema__" not in dir(bases[0]):
raise BrokenInheritance("__DABSchema__ not found in base class, broken inheritance chain.")
# copy inherited schema
namespace["__DABSchema__"] = copy(bases[0].__DABSchema__)
# force field without default value to be instantiated (with None)
if "__annotations__" in namespace:
for _funknown in [_ for _ in namespace["__annotations__"] if _ not in namespace.keys()]:
namespace[_funknown] = None
# iterating referenced / modified fields
new_values: Dict[str, Any] = {}
# iterating new and modified fields
modified_field: Dict[str, Any] = {}
new_fields: Dict[str, DABField] = {}
for _fname, _fvalue in namespace.items():
if _fname.startswith("__"):
pass
@@ -116,63 +194,94 @@ class BaseMeta(type):
...
# print("FOUND Constraints")
else:
# print(f"Found random argument: {_fname} / {_fvalue}")
if len(bases) == 1 and _fname in dir(bases[0]):
# print(f"modified field: {_fname}")
# print(f"Parsing Field: {_fname} / {_fvalue}")
# Modified fields
if len(bases) == 1 and _fname in namespace["__DABSchema__"].keys():
# print(f"Modified field: {_fname}")
if _fname in namespace["__annotations__"]:
raise RuntimeError("types cannot be modified on derived classes")
raise ReadOnlyFieldAnnotation("annotations cannot be modified on derived classes")
try:
check_type(_fvalue, bases[0].__annotations__[_fname])
except TypeCheckError as exp:
raise TypeError(
f"Field <{_fname}> overriden value is not of expected type {bases[0].__annotations__[_fname]}.", exp
check_type(
_fvalue,
namespace["__DABSchema__"][_fname].get_annotation(),
collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS,
)
new_values[_fname] = _fvalue
except TypeCheckError as exp:
raise InvalidFieldValue(
f"Field <{_fname}> New Field value is not of expected type {bases[0].__annotations__[_fname]}."
) from exp
modified_field[_fname] = _fvalue
# New fieds
else:
print(f"new field: {_fname}")
print(f"type is: {type(_fvalue)}")
# print(f"New field: {_fname}")
# print(f"type is: {type(_fvalue)}")
# print(f"value is: {_fvalue}")
# check if field is annotated
if _fname not in namespace["__annotations__"]:
raise RuntimeError(f"Every dabmodel Fields must be annotated ({_fname})")
if "__annotations__" not in namespace or _fname not in namespace["__annotations__"]:
raise NotAnnotatedField(f"Every dabmodel Fields must be annotated ({_fname})")
# check if annotation is allowed
if not __check_type__(namespace["__annotations__"][_fname]):
raise TypeError(f"Field <{_fname}> has not an allowed or valid annotation.")
if isinstance(namespace["__annotations__"][_fname], str):
namespace["__annotations__"][_fname] = _resolve_annotation(namespace["__annotations__"][_fname])
if not __check_annotation_definition__(namespace["__annotations__"][_fname]):
raise InvalidFieldAnnotation(f"Field <{_fname}> has not an allowed or valid annotation.")
# print(f"annotation is: {namespace['__annotations__'][_fname]}")
# check if value is valid
try:
check_type(_fvalue, namespace["__annotations__"][_fname])
check_type(
_fvalue, namespace["__annotations__"][_fname], collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS
)
except TypeCheckError as exp:
raise TypeError(f"Field <{_fname}> is not of expected type {namespace['__annotations__'][_fname]}.", exp)
namespace[_fname] = DABField(_fname, _fvalue, namespace["__annotations__"][_fname])
# print(_fvalue.__doc__)
raise InvalidFieldValue(
f"Value of Field <{_fname}> is not of expected type {namespace['__annotations__'][_fname]}."
) from exp
new_fields[_fname] = DABField(_fname, _fvalue, namespace["__annotations__"][_fname])
# namespace[_fname].add_documentation()
# removing modified fields from definition (will add them back later)
# __slots__ = []
for _fname, _fvalue in new_values.items():
namespace[_fname] = deepcopy(bases[0].__dict__[_fname])
# __slots__.append(_fname)
# setattr(cls, "__slots__", tuple(__slots__))
# namespace["__slots__"] = tuple(__slots__)
# namespace["__slots__"] = tuple()
# removing modified fields from class (will add them back later)
for _fname in new_fields.keys():
del namespace[_fname]
for _fname in modified_field.keys():
del namespace[_fname]
orig_setattr = namespace.get("__setattr__", object.__setattr__)
def guarded_setattr(self, key, value):
if key.startswith("_"): # allow private and dunder attrs
return orig_setattr(self, key, value)
# block writes after init if key is readonly
if key in self.__DABSchema__.keys():
if hasattr(self, key):
raise ReadOnlyField(f"{key} is read-only")
else:
raise NewFieldForbidden(f"creating new fields is not allowed")
return orig_setattr(self, key, value)
namespace["__setattr__"] = guarded_setattr
cls = super().__new__(mcls, name, bases, namespace)
# if "DABSchema"
for _fname, _fvalue in modified_field.items():
cls.__DABSchema__[_fname] = deepcopy(bases[0].__DABSchema__[_fname])
cls.__DABSchema__[_fname].update_value(_fvalue)
for _fname, _fvalue in new_fields.items():
_fvalue.add_source(cls)
cls.__DABSchema__[_fname] = _fvalue
for fname, fvalue in new_values.items():
cls.__dict__[fname].update_value(fvalue)
# cls.__DABModelSchema__ = DABModelSchema()
return cls
def __call__(cls, *args, **kw):
obj = super().__call__(*args, **kw)
for fname in dir(cls):
if not fname.startswith("__"):
attr = getattr(cls, fname)
if isinstance(attr, DABField):
setattr(obj, fname, attr.render())
for _fname in cls.__DABSchema__.keys():
setattr(obj, _fname, cls.__DABSchema__[_fname].render())
# obj.__DABSchema__ = deepfreeze(obj.__DABSchema__)
# setattr(obj, "__DABSchema__", deepfreeze(obj.__DABSchema__))
return obj

View File

@@ -7,9 +7,13 @@
# work. If not, see <https://creativecommons.org/licenses/by-nc-sa/4.0/>.
import unittest
from os import chdir
import sys
import subprocess
from os import chdir, environ
from pathlib import Path
from typing import List, Optional, Dict, Union, Tuple, Set, FrozenSet, TypeVar, Generic
import textwrap
from typing import List, Optional, Dict, Union, Tuple, Set, FrozenSet, TypeVar, Generic, Any, Annotated
from pprint import pprint
print(__name__)
@@ -22,56 +26,506 @@ testdir_path = Path(__file__).parent.resolve()
chdir(testdir_path.parent.resolve())
# ---------- Base models to reuse in many tests ----------
def create():
class Appliance1(dm.BaseAppliance):
MyStrVar: str = "default value"
MyStrVar2: str = "default value2"
class Constraints:
MyStrVar = None
class Appliance2(Appliance1):
MyStrVar = "modified value" # modified default value
MyStrVar3: Optional[str]
""" MyStrVar3 docstring"""
MyDictVar1: Optional[Dict[str, str]]
MyDictVar2: Dict[str, Optional[str]] = {"a": "aa"}
MyDictVar3: Dict[str, Union[None | str]] = {}
MyListVar1: List[int] = []
MyTupleVar1: Tuple[int, float] = (14, 12.3)
MyTupleVar2: Optional[Tuple[int, str]]
MySetVar1: Set[int] = {1, 7}
MySetVar2: Optional[Set[int]]
MyFrozenSetVar1: FrozenSet[int] = frozenset({1, 6})
MyFrozenSetVar2: Optional[FrozenSet[int]]
# MyStrVar2 = 18
T_TestCustomType = Dict[str, str]
class Appliance3(Appliance2):
MyStrVar10: Optional[str] = "coucou"
TestCustomType: T_TestCustomType = {}
app = Appliance3()
print(dir(app))
print(vars(app))
print(app.MyStrVar3)
app.totota = 14
print(app.MyDictVar2)
# app.MyDictVar2["a"] = 12
print(app.MyFrozenSetVar1)
print(app.MySetVar1)
print("done")
class TestConfigWithoutEnabledFlag(unittest.TestCase):
def test_1(self):
create()
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.__DABSchema__.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_immutable_fields(self):
"""Testing first appliance level, and Field types (simple)"""
# class can be created
class Appliance1(dm.BaseAppliance):
StrVar: str = "default value"
StrVar2: str = "default value2"
VarInt: int = 12
VarInt2: int = 21
VarFloat: float = 12.1
VarFloat2: float = 21.2
VarComplex: complex = complex(3, 5)
VarComplex2: complex = complex(8, 6)
VarBool: bool = True
VarBool2: bool = False
VarBytes: bytes = bytes.fromhex("2Ef0 F1f2 ")
VarBytes2: bytes = bytes.fromhex("2ff0 F7f2 ")
app1 = Appliance1()
self.immutable_vars__test_field(app1, "StrVar", "default value", "test")
self.immutable_vars__test_field(app1, "StrVar2", "default value2", "test2")
self.immutable_vars__test_field(app1, "VarInt", 12, 13)
self.immutable_vars__test_field(app1, "VarInt2", 21, 22)
self.immutable_vars__test_field(app1, "VarFloat", 12.1, 32)
self.immutable_vars__test_field(app1, "VarFloat2", 21.2, 42)
self.immutable_vars__test_field(app1, "VarComplex", complex(3, 5), complex(1, 2))
self.immutable_vars__test_field(app1, "VarComplex2", complex(8, 6), complex(3, 2))
self.immutable_vars__test_field(app1, "VarBool", True, False)
self.immutable_vars__test_field(app1, "VarBool2", False, True)
self.immutable_vars__test_field(app1, "VarBytes", bytes.fromhex("2Ef0 F1f2 "), bytes.fromhex("11f0 F1f2 "))
self.immutable_vars__test_field(app1, "VarBytes2", bytes.fromhex("2ff0 F7f2 "), bytes.fromhex("11f0 F1e2 "))
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: str = 12
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: int = "value"
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: float = "value"
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: complex = "value"
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: bool = "value"
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: bytes = "value"
def test_annotation(self):
"""Testing first appliance level, and Field types (simple annotations)"""
# class can be created if annotation is a string
class Appliance1(dm.BaseAppliance):
StrVar: "str" = "default value"
StrVar2: "str" = "default value2"
VarInt: "int" = 12
VarInt2: "int" = 21
VarFloat: "float" = 12.1
VarFloat2: "float" = 21.2
VarComplex: "complex" = complex(3, 5)
VarComplex2: "complex" = complex(8, 6)
VarBool: "bool" = True
VarBool2: "bool" = False
VarBytes: "bytes" = bytes.fromhex("2Ef0 F1f2 ")
VarBytes2: "bytes" = bytes.fromhex("2ff0 F7f2 ")
app1 = Appliance1()
self.immutable_vars__test_field(app1, "StrVar", "default value", "test")
self.immutable_vars__test_field(app1, "StrVar2", "default value2", "test2")
self.immutable_vars__test_field(app1, "VarInt", 12, 13)
self.immutable_vars__test_field(app1, "VarInt2", 21, 22)
self.immutable_vars__test_field(app1, "VarFloat", 12.1, 32)
self.immutable_vars__test_field(app1, "VarFloat2", 21.2, 42)
self.immutable_vars__test_field(app1, "VarComplex", complex(3, 5), complex(1, 2))
self.immutable_vars__test_field(app1, "VarComplex2", complex(8, 6), complex(3, 2))
self.immutable_vars__test_field(app1, "VarBool", True, False)
self.immutable_vars__test_field(app1, "VarBool2", False, True)
self.immutable_vars__test_field(app1, "VarBytes", bytes.fromhex("2Ef0 F1f2 "), bytes.fromhex("11f0 F1f2 "))
self.immutable_vars__test_field(app1, "VarBytes2", bytes.fromhex("2ff0 F7f2 "), bytes.fromhex("11f0 F1e2 "))
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: "str" = 12
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: "int" = "value"
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: "float" = "value"
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: "complex" = "value"
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: "bool" = "value"
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: "bytes" = "value"
# class cannot be created if not annotated field
with self.assertRaises(dm.NotAnnotatedField):
class _(dm.BaseAppliance):
_ = "default value"
def test_annotated(self):
"""Testing first appliance level, and Field types (annotated one)"""
# class can be created if annotation is a string
class Appliance1(dm.BaseAppliance):
StrVar: Annotated[str, "my string"] = "default value"
def test_optionnal(self):
"""Testing first appliance level, and Field types (Optionnal annotations)"""
# class can be created with Optionnal (and variant)
class Appliance1(dm.BaseAppliance):
StrVar: Optional[str] = "default value"
StrVar2: Optional[str]
StrVar3: Union[None | str] = "default value"
StrVar4: Union[None | str]
StrVar5: None | str = "default value"
StrVar6: None | str
app1 = Appliance1()
self.immutable_vars__test_field(app1, "StrVar", "default value", "123")
self.immutable_vars__test_field(app1, "StrVar2", None, "123")
self.immutable_vars__test_field(app1, "StrVar3", "default value", "123")
self.immutable_vars__test_field(app1, "StrVar4", None, "123")
self.immutable_vars__test_field(app1, "StrVar5", "default value", "123")
self.immutable_vars__test_field(app1, "StrVar6", None, "123")
# class can be created with Optionnal (and variant), as string annotation
class Appliance2(dm.BaseAppliance):
StrVar: "Optional[str]" = "default value"
StrVar2: "Optional[str]"
StrVar3: "Union[None | str]" = "default value"
StrVar4: "Union[None | str]"
StrVar5: "None | str" = "default value"
StrVar6: "None | str"
app2 = Appliance2()
self.immutable_vars__test_field(app2, "StrVar", "default value", "123")
self.immutable_vars__test_field(app2, "StrVar2", None, "123")
self.immutable_vars__test_field(app2, "StrVar3", "default value", "123")
self.immutable_vars__test_field(app2, "StrVar4", None, "123")
self.immutable_vars__test_field(app2, "StrVar5", "default value", "123")
self.immutable_vars__test_field(app2, "StrVar6", None, "123")
@unittest.skip
def test_containers__set(self):
"""Testing first appliance level, and Field types (Set)"""
# class can be created with set
class Appliance1(dm.BaseAppliance):
testVar: Set[int] = {1, 2}
testVar2: Set[str] = {"a", "b"}
testVar3: Set[float] = {0.5, 0.456, 12}
testVar4: "Set[str]" = {"a", "c"}
testVar5: set[str] = {"a", "b"}
testVar6: "set[str]" = {"a", "b"}
testVar7: set[int | str] = {1, 2, "abcd", "efg"}
app1 = Appliance1()
self.immutable_vars__test_field(app1, "testVar", {1, 2}, {1, 5})
self.assertEqual(app1.testVar, {2, 1})
self.immutable_vars__test_field(app1, "testVar2", {"a", "b"}, {"h", "c"})
self.assertEqual(app1.testVar2, {"b", "a"})
self.immutable_vars__test_field(app1, "testVar3", {0.5, 0.456, 12}, {0.9, 0.4156, 11})
self.assertEqual(app1.testVar3, {0.456, 0.5, 12})
self.immutable_vars__test_field(app1, "testVar4", {"a", "c"}, {"h", "e"})
self.immutable_vars__test_field(app1, "testVar5", {"a", "b"}, {"h", "c"})
self.immutable_vars__test_field(app1, "testVar6", {"a", "b"}, {"h", "c"})
self.immutable_vars__test_field(app1, "testVar7", {1, 2, "abcd", "efg"}, {"h", "c"})
# must work
sorted(app1.testVar)
with self.assertRaises(AttributeError):
app1.testVar.add(3)
with self.assertRaises(AttributeError):
app1.testVar4.add("coucou")
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: Set[int] = {"a"}
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: "Set[int]" = {"a"}
with self.assertRaises(dm.IncompletelyAnnotatedField):
class _(dm.BaseAppliance):
_: Set = {1, 2}
with self.assertRaises(dm.IncompletelyAnnotatedField):
class _(dm.BaseAppliance):
_: "Set" = {1, 2}
# Hacky part !
# set() are randomly ordered, and typeguard can be configured to only check 1st element of containers.
# we need to make sure it is properly configured, so we are expecting it to detect wrong type at any element
# this is why we need to run the code multiple time with a new interpreter that will use a different random seed
code = textwrap.dedent(
r"""
from src import dabmodel as dm
from typing import Set
try:
class Yours(dm.BaseAppliance):
My1: Set[int] = {99, 1, 3, "a", 5,6}
except dm.InvalidFieldValue as ex:
raise SystemExit(2)
raise SystemExit(0)
"""
)
for i in range(15):
env = environ.copy()
env["PYTHONHASHSEED"] = str(i)
res = subprocess.run([sys.executable, "-c", code], env=env)
self.assertEqual(res.returncode, 2)
@unittest.skip
def test_containers__frozenset(self):
"""Testing first appliance level, and Field types (FrozenSet)"""
# class can be created with set
class Appliance1(dm.BaseAppliance):
testVar: frozenset[int] = frozenset({1, 2})
testVar2: frozenset[str] = frozenset({"a", "b"})
testVar3: frozenset[float] = frozenset({0.5, 0.456, 12})
testVar4: "frozenset[str]" = frozenset({"a", "c"})
testVar5: FrozenSet[int] = frozenset({1, 2})
testVar6: "FrozenSet[int]" = frozenset({1, 2})
testVar7: FrozenSet[int | str] = frozenset({1, 2, "abcd", "efg"})
app1 = Appliance1()
self.immutable_vars__test_field(app1, "testVar", {1, 2}, {1, 5})
self.assertEqual(app1.testVar, {2, 1})
self.immutable_vars__test_field(app1, "testVar2", {"a", "b"}, {"h", "c"})
self.assertEqual(app1.testVar2, {"b", "a"})
self.immutable_vars__test_field(app1, "testVar3", {0.5, 0.456, 12}, {0.9, 0.4156, 11})
self.assertEqual(app1.testVar3, {0.456, 0.5, 12})
self.immutable_vars__test_field(app1, "testVar4", {"a", "c"}, {"h", "e"})
self.immutable_vars__test_field(app1, "testVar5", {1, 2}, {1, 5})
self.immutable_vars__test_field(app1, "testVar6", {1, 2}, {1, 5})
self.immutable_vars__test_field(app1, "testVar7", {1, 2, "abcd", "efg"}, {1, 5})
# must work
sorted(app1.testVar)
with self.assertRaises(AttributeError):
app1.testVar.add(3)
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: FrozenSet[int] = {"a"}
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: "FrozenSet[int]" = {"a"}
with self.assertRaises(dm.IncompletelyAnnotatedField):
class _(dm.BaseAppliance):
_: FrozenSet = {1, 2}
with self.assertRaises(dm.IncompletelyAnnotatedField):
class _(dm.BaseAppliance):
_: "FrozenSet" = {1, 2}
# Hacky part !
# set() are randomly ordered, and typeguard can be configured to only check 1st element of containers.
# we need to make sure it is properly configured, so we are expecting it to detect wrong type at any element
# this is why we need to run the code multiple time with a new interpreter that will use a different random seed
code = textwrap.dedent(
r"""
from src import dabmodel as dm
from typing import FrozenSet
try:
class Yours(dm.BaseAppliance):
My1: FrozenSet[int] = frozenset({99, 1, 3, "a", 5,6})
except dm.InvalidFieldValue as ex:
raise SystemExit(2)
raise SystemExit(0)
"""
)
for i in range(15):
env = environ.copy()
env["PYTHONHASHSEED"] = str(i)
res = subprocess.run([sys.executable, "-c", code], env=env)
self.assertEqual(res.returncode, 2)
def test_containers__list(self):
"""Testing first appliance level, and Field types (List)"""
# class can be created with list
class Appliance1(dm.BaseAppliance):
testVar: List[int] = [1, 2]
testVar2: List[str] = ["a", "b"]
testVar3: List[float] = [0.5, 0.456, 12]
testVar4: "List[str]" = ["a", "c"]
testVar5: list[str] = ["a", "b"]
testVar6: "list[str]" = ["a", "b"]
testVar7: List[Union[int, str]] = [1, 2, 3, "one", "two", "three"]
app1 = Appliance1()
# Note: lists are converted to tuples
self.immutable_vars__test_field(app1, "testVar", (1, 2), [1, 5])
self.immutable_vars__test_field(app1, "testVar2", ("a", "b"), ["h", "c"])
self.immutable_vars__test_field(app1, "testVar3", (0.5, 0.456, 12), [0.9, 0.4156, 11])
self.immutable_vars__test_field(app1, "testVar4", ("a", "c"), ["h", "e"])
self.immutable_vars__test_field(app1, "testVar5", ("a", "b"), ["h", "c"])
self.immutable_vars__test_field(app1, "testVar6", ("a", "b"), ["h", "c"])
self.immutable_vars__test_field(app1, "testVar7", (1, 2, 3, "one", "two", "three"), ["h", "c"])
# must work
sorted(app1.testVar)
with self.assertRaises(AttributeError):
app1.testVar.append(3)
with self.assertRaises(AttributeError):
app1.testVar.pop()
with self.assertRaises(AttributeError):
app1.testVar.sort()
with self.assertRaises(AttributeError):
app1.testVar4.append("coucou")
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: List[int] = ["a"]
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: "List[int]" = ["a"]
with self.assertRaises(dm.IncompletelyAnnotatedField):
class _(dm.BaseAppliance):
_: List = [1, 2]
with self.assertRaises(dm.IncompletelyAnnotatedField):
class _(dm.BaseAppliance):
_: "List" = [1, 2]
def test_containers__dict(self):
"""Testing first appliance level, and Field types (Dict)"""
# class can be created with dict
class Appliance1(dm.BaseAppliance):
testVar: Dict[int, str] = {1: "a", 2: "b"}
testVar2: "Dict[int, str]" = {1: "c", 99: "d"}
app1 = Appliance1()
self.immutable_vars__test_field(app1, "testVar", {1: "a", 2: "b"}, {1: "", 99: "i"})
self.immutable_vars__test_field(app1, "testVar2", {1: "c", 99: "d"}, {10: "", 50: "i"})
# TODO: wrap exception type
with self.assertRaises(TypeError):
app1.testVar[58] = "aaa"
# TODO: wrap exception type
with self.assertRaises(TypeError):
app1.testVar[1] = "ggg"
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: Dict[int, str] = {1: 64, 2: "b"}
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: "Dict[int, str]" = {1: 64, 2: "b"}
with self.assertRaises(dm.IncompletelyAnnotatedField):
class _(dm.BaseAppliance):
_: Dict = {1: 64, 2: "b"}
# annotation is parsed before the library can do anything, so the exception can only be TypeError
with self.assertRaises(TypeError):
class _(dm.BaseAppliance):
_: Dict[int] = {1: 64, 2: "b"}
# annotation is parsed before the library can do anything, so the exception can only be TypeError
with self.assertRaises(TypeError):
class _(dm.BaseAppliance):
_: "Dict[int]" = {1: 64, 2: "b"}
def test_containers__tuple(self):
"""Testing first appliance level, and Field types (Tuple)"""
# class can be created with list
class Appliance1(dm.BaseAppliance):
testVar: Tuple[int, ...] = (1, 2)
testVar2: Tuple[str, ...] = ("a", "b")
testVar3: Tuple[float, ...] = (0.5, 0.456, 12)
testVar4: "Tuple[str,...]" = ("a", "c")
testVar5: tuple[str, ...] = ("a", "b")
testVar6: "tuple[str,...]" = ("a", "b")
# testVar7: Tuple[Union[int, str]] = (1, 2, 3, "one", "two", "three")
app1 = Appliance1()
self.immutable_vars__test_field(app1, "testVar", (1, 2), (1, 5))
self.immutable_vars__test_field(app1, "testVar2", ("a", "b"), ("h", "c"))
self.immutable_vars__test_field(app1, "testVar3", (0.5, 0.456, 12), (0.9, 0.4156, 11))
self.immutable_vars__test_field(app1, "testVar4", ("a", "c"), ("h", "e"))
self.immutable_vars__test_field(app1, "testVar5", ("a", "b"), ("h", "c"))
self.immutable_vars__test_field(app1, "testVar6", ("a", "b"), ("h", "c"))
# self.immutable_vars__test_field(app1, "testVar7", (1, 2, 3, "one", "two", "three"), ("h", "c"))
# must work
sorted(app1.testVar)
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: Tuple[int] = "a"
with self.assertRaises(dm.InvalidFieldValue):
class _(dm.BaseAppliance):
_: "Tuple[int]" = "a"
with self.assertRaises(dm.IncompletelyAnnotatedField):
class _(dm.BaseAppliance):
_: Tuple = (1, 2)
with self.assertRaises(dm.IncompletelyAnnotatedField):
class _(dm.BaseAppliance):
_: "Tuple" = (1, 2)
# ---------- main ----------