diff --git a/osbot_utils/type_safe/shared/Type_Safe__Cache.py b/osbot_utils/type_safe/shared/Type_Safe__Cache.py index 326f4462..5637f308 100644 --- a/osbot_utils/type_safe/shared/Type_Safe__Cache.py +++ b/osbot_utils/type_safe/shared/Type_Safe__Cache.py @@ -52,7 +52,7 @@ def get_class_immutable_vars(self, cls): immutable_vars = self._cls__immutable_vars.get(cls) if self.skip_cache or not immutable_vars: annotations = self.get_class_annotations(cls) - immutable_vars = [key for key, value in annotations if value in IMMUTABLE_TYPES] + immutable_vars = {key: value for key, value in annotations if value in IMMUTABLE_TYPES} self._cls__immutable_vars[cls] = immutable_vars else: self.cache_hit__cls__immutable_vars += 1 diff --git a/osbot_utils/type_safe/shared/Type_Safe__Raise_Exception.py b/osbot_utils/type_safe/shared/Type_Safe__Raise_Exception.py index aa181f69..c8ee73d9 100644 --- a/osbot_utils/type_safe/shared/Type_Safe__Raise_Exception.py +++ b/osbot_utils/type_safe/shared/Type_Safe__Raise_Exception.py @@ -4,8 +4,8 @@ class Type_Safe__Raise_Exception: - def type_mismatch_error(self, var_name: str, expected_type: Any, actual_value: Any) -> None: # Raises formatted error for type validation failures - exception_message = f"variable '{var_name}' is defined as type '{expected_type}' but has value '{actual_value}' of type '{type(actual_value)}'" + def type_mismatch_error(self, var_name: str, expected_type: type, actual_type: type) -> None: # Raises formatted error for type validation failures + exception_message = f"Invalid type for attribute '{var_name}'. Expected '{expected_type}' but got '{actual_type}'" raise ValueError(exception_message) def immutable_type_error(self, var_name, var_type): diff --git a/osbot_utils/type_safe/shared/Type_Safe__Validation.py b/osbot_utils/type_safe/shared/Type_Safe__Validation.py index b364899d..916b48e8 100644 --- a/osbot_utils/type_safe/shared/Type_Safe__Validation.py +++ b/osbot_utils/type_safe/shared/Type_Safe__Validation.py @@ -36,6 +36,6 @@ def validate_type_immutability(self, var_name: str, var_type: Any) -> None: def validate_variable_type(self, var_name, var_type, var_value): # Validate type compatibility if var_type and not isinstance(var_value, var_type): - type_safe_raise_exception.type_mismatch_error(var_name, var_type, var_value) + type_safe_raise_exception.type_mismatch_error(var_name, var_type, type(var_value)) type_safe_validation = Type_Safe__Validation() diff --git a/osbot_utils/type_safe/steps/Type_Safe__Step__Set_Attr.py b/osbot_utils/type_safe/steps/Type_Safe__Step__Set_Attr.py index bf31c141..059f087d 100644 --- a/osbot_utils/type_safe/steps/Type_Safe__Step__Set_Attr.py +++ b/osbot_utils/type_safe/steps/Type_Safe__Step__Set_Attr.py @@ -1,11 +1,12 @@ -from typing import get_origin, Annotated, get_args -from osbot_utils.type_safe.shared.Type_Safe__Cache import type_safe_cache -from osbot_utils.utils.Objects import all_annotations -from osbot_utils.utils.Objects import convert_dict_to_value_from_obj_annotation -from osbot_utils.utils.Objects import convert_to_value_from_obj_annotation -from osbot_utils.utils.Objects import value_type_matches_obj_annotation_for_attr -from osbot_utils.utils.Objects import value_type_matches_obj_annotation_for_union_and_annotated -from osbot_utils.type_safe.validators.Type_Safe__Validator import Type_Safe__Validator +from typing import get_origin, Annotated, get_args, _SpecialGenericAlias +from osbot_utils.type_safe.shared.Type_Safe__Cache import type_safe_cache +from osbot_utils.type_safe.shared.Type_Safe__Raise_Exception import type_safe_raise_exception +from osbot_utils.utils.Objects import all_annotations, are_types_compatible_for_assigment +from osbot_utils.utils.Objects import convert_dict_to_value_from_obj_annotation +from osbot_utils.utils.Objects import convert_to_value_from_obj_annotation +from osbot_utils.utils.Objects import value_type_matches_obj_annotation_for_attr +from osbot_utils.utils.Objects import value_type_matches_obj_annotation_for_union_and_annotated +from osbot_utils.type_safe.validators.Type_Safe__Validator import Type_Safe__Validator class Type_Safe__Step__Set_Attr: @@ -78,6 +79,17 @@ def handle_get_class(self, _self, annotations, name, value): return value def setattr(self, _super, _self, name, value): + if value is not None and type(value) is not _SpecialGenericAlias: # todo: refactor this section into a separate method + immutable_vars = type_safe_cache.get_class_immutable_vars(_self.__class__) + if name in immutable_vars: + expected_type = immutable_vars[name] + if value is not type: + current_type = type(value) + else: + current_type = type + if not are_types_compatible_for_assigment(current_type, expected_type): + type_safe_raise_exception.type_mismatch_error(name, expected_type, current_type) + return _super.__setattr__(name, value) annotations = all_annotations(_self) if not annotations: # can't do type safety checks if the class does not have annotations diff --git a/tests/unit/testing/performance/test_Performance_Measure__Session.py b/tests/unit/testing/performance/test_Performance_Measure__Session.py index 55b99942..1bf97b71 100644 --- a/tests/unit/testing/performance/test_Performance_Measure__Session.py +++ b/tests/unit/testing/performance/test_Performance_Measure__Session.py @@ -55,10 +55,10 @@ class An_Class_6(Type_Safe): _.measure(Random_Guid).print().assert_time(self.time_3_kns , self.time_5_kns, self.time_6_kns , self.time_7_kns ) _.measure(An_Class_1 ).print().assert_time(self.time_100_ns, self.time_200_ns ) _.measure(An_Class_2 ).print().assert_time(self.time_1_kns , self.time_2_kns , self.time_3_kns , self.time_4_kns , self.time_5_kns , self.time_6_kns, self.time_7_kns ) - _.measure(An_Class_3 ).print().assert_time(self.time_8_kns , self.time_9_kns ,self.time_10_kns, self.time_20_kns ) - _.measure(An_Class_4 ).print().assert_time(self.time_8_kns , self.time_9_kns ,self.time_10_kns, self.time_20_kns ) - _.measure(An_Class_5 ).print().assert_time(self.time_8_kns , self.time_9_kns ,self.time_10_kns, self.time_20_kns ) - _.measure(An_Class_6 ).print().assert_time(self.time_7_kns , self.time_8_kns ,self.time_9_kns , self.time_10_kns, self.time_20_kns ) + _.measure(An_Class_3 ).print().assert_time(self.time_4_kns , self.time_8_kns , self.time_9_kns ,self.time_10_kns, self.time_20_kns ) + _.measure(An_Class_4 ).print().assert_time(self.time_4_kns , self.time_8_kns , self.time_9_kns ,self.time_10_kns, self.time_20_kns ) + _.measure(An_Class_5 ).print().assert_time(self.time_4_kns , self.time_8_kns , self.time_9_kns ,self.time_10_kns, self.time_20_kns ) + _.measure(An_Class_6 ).print().assert_time(self.time_4_kns , self.time_7_kns , self.time_8_kns ,self.time_9_kns , self.time_10_kns, self.time_20_kns ) type_safe_cache.print_cache_hits() diff --git a/tests/unit/type_safe/_bugs/test_Type_Safe__bugs.py b/tests/unit/type_safe/_bugs/test_Type_Safe__bugs.py index 2429e460..b79e0bcd 100644 --- a/tests/unit/type_safe/_bugs/test_Type_Safe__bugs.py +++ b/tests/unit/type_safe/_bugs/test_Type_Safe__bugs.py @@ -137,7 +137,7 @@ class An_Class__With_Bad_Values(Kwargs_To_Self): an_class = An_Class__With_Correct_Values() # should create ok and values should match the type assert an_class.__locals__() == {'an_bool': an_bool_value, 'an_int': an_int_value, 'an_str': an_str_value} - expected_message = "variable 'an_str' is defined as type '' but has value 'True' of type ''" + expected_message = "Invalid type for attribute 'an_str'. Expected '' but got ''" with self.assertRaises(Exception) as context: An_Class__With_Bad_Values() assert context.exception.args[0] == expected_message @@ -174,7 +174,7 @@ class An_Class__With_Bad_Values(Kwargs_To_Self): an_int : int = an_bool_value # BUG: should have thrown exception here (bool should be allowed on int) an_str : str = an_bool_value # will throw exception here - expected_message = "variable 'an_str' is defined as type '' but has value 'True' of type ''" + expected_message = "Invalid type for attribute 'an_str'. Expected '' but got ''" with self.assertRaises(Exception) as context: An_Class__With_Bad_Values() assert context.exception.args[0] == expected_message diff --git a/tests/unit/type_safe/steps/test_Type_Safe__Step__Set_Attr.py b/tests/unit/type_safe/steps/test_Type_Safe__Step__Set_Attr.py index 706197fc..355e92c8 100644 --- a/tests/unit/type_safe/steps/test_Type_Safe__Step__Set_Attr.py +++ b/tests/unit/type_safe/steps/test_Type_Safe__Step__Set_Attr.py @@ -4,7 +4,7 @@ from osbot_utils.utils.Objects import all_annotations from osbot_utils.type_safe.steps.Type_Safe__Step__Set_Attr import Type_Safe__Step__Set_Attr -from osbot_utils.helpers.trace.Trace_Call import trace_calls +from osbot_utils.helpers.trace.Trace_Call import trace_calls class test_Type_Safe__Step__Set_Attr(TestCase): @@ -13,6 +13,10 @@ class test_Type_Safe__Step__Set_Attr(TestCase): def setUpClass(cls): cls.step_set_attr = Type_Safe__Step__Set_Attr() + # def setUp(self): + # print() + + # @trace_calls(include = ['*'], # show_internals = True , # show_duration = True , @@ -27,8 +31,9 @@ class Class__One_int: assert all_annotations(one_int) == {'an_int': int} assert self.step_set_attr.setattr(one_int, one_int, 'an_int', 42) is None assert one_int.an_int == 42 - + #pprint() assert self.step_set_attr.setattr(one_int, one_int, 'an_int', 42) is None + #pprint() assert self.step_set_attr.setattr(one_int, one_int, 'an_int', 42) is None def test_class__random_guid(self): diff --git a/tests/unit/type_safe/test_Type_Safe.py b/tests/unit/type_safe/test_Type_Safe.py index 6f35882f..02aaddcb 100644 --- a/tests/unit/type_safe/test_Type_Safe.py +++ b/tests/unit/type_safe/test_Type_Safe.py @@ -551,12 +551,12 @@ def test__default_kwargs__picks_up_bad_types(self): class An_Bad_Type(Type_Safe): not_an_int: int = "an str" - expected_error= "variable 'not_an_int' is defined as type '' but has value 'an str' of type ''" + expected_error= "Invalid type for attribute 'not_an_int'. Expected '' but got ''" #with Catch(expect_exception=True, expected_error=expected_error): with pytest.raises(ValueError, match=expected_error ): An_Bad_Type().__default_kwargs__() - expected_error = "variable 'not_an_int' is defined as type '' but has value 'an str' of type ''" + expected_error = "Invalid type for attribute 'not_an_int'. Expected '' but got ''" with pytest.raises(ValueError, match=expected_error ): An_Bad_Type().__default_kwargs__()