Skip to content

Commit 54dec02

Browse files
committed
fixed bug in Type_Safe
1 parent a8bc157 commit 54dec02

File tree

7 files changed

+77
-31
lines changed

7 files changed

+77
-31
lines changed

osbot_utils/type_safe/shared/Type_Safe__Convert.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,17 @@ def convert_to_value_from_obj_annotation(self, target, attr_name, value):
3232
if attribute_annotation:
3333
origin = type_safe_cache.get_origin(attribute_annotation) # Add handling for Type[T] annotations
3434
if origin is type and isinstance(value, str):
35-
try: # Convert string path to actual type
36-
if len(value.rsplit('.', 1)) > 1:
37-
module_name, class_name = value.rsplit('.', 1)
38-
module = __import__(module_name, fromlist=[class_name])
39-
return getattr(module, class_name)
40-
except (ValueError, ImportError, AttributeError) as e:
41-
raise ValueError(f"Could not convert '{value}' to type: {str(e)}")
42-
35+
return self.get_class_from_class_name(value)
4336
if attribute_annotation in TYPE_SAFE__CONVERT_VALUE__SUPPORTED_TYPES: # for now hard-coding this to just these types until we understand the side effects
4437
return attribute_annotation(value)
4538
return value
4639

40+
def get_class_from_class_name(self, value):
41+
try: # Convert string path to actual type
42+
if len(value.rsplit('.', 1)) > 1:
43+
module_name, class_name = value.rsplit('.', 1)
44+
module = __import__(module_name, fromlist=[class_name])
45+
return getattr(module, class_name)
46+
except (ValueError, ImportError, AttributeError) as e:
47+
raise ValueError(f"Could not convert '{value}' to type: {str(e)}")
4748
type_safe_convert = Type_Safe__Convert()

osbot_utils/type_safe/shared/Type_Safe__Validation.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,10 @@ def validate_type_compatibility(self, target : Any ,
228228

229229
if is_invalid:
230230
expected_type = annotations.get(name)
231-
actual_type = type(value)
231+
if type(value) is type:
232+
actual_type = value
233+
else:
234+
actual_type = type(value)
232235
raise ValueError(f"Invalid type for attribute '{name}'. Expected '{expected_type}' but got '{actual_type}'")
233236

234237
# todo: see if need to add cache support to this method (it looks like this method is not called very often)

osbot_utils/type_safe/steps/Type_Safe__Step__Class_Kwargs.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
from typing import Dict, Any, Type
2-
3-
from osbot_utils.helpers.Obj_Id import Obj_Id
4-
from osbot_utils.helpers.Random_Guid import Random_Guid
5-
from osbot_utils.type_safe.shared.Type_Safe__Cache import Type_Safe__Cache, type_safe_cache
6-
from osbot_utils.type_safe.shared.Type_Safe__Shared__Variables import IMMUTABLE_TYPES
7-
from osbot_utils.type_safe.shared.Type_Safe__Validation import type_safe_validation
8-
from osbot_utils.type_safe.steps.Type_Safe__Step__Default_Value import type_safe_step_default_value
1+
from typing import Dict, Any, Type
2+
from osbot_utils.helpers.Obj_Id import Obj_Id
3+
from osbot_utils.helpers.Random_Guid import Random_Guid
4+
from osbot_utils.type_safe.shared.Type_Safe__Cache import Type_Safe__Cache, type_safe_cache
5+
from osbot_utils.type_safe.shared.Type_Safe__Shared__Variables import IMMUTABLE_TYPES
6+
from osbot_utils.type_safe.shared.Type_Safe__Validation import type_safe_validation
7+
from osbot_utils.type_safe.steps.Type_Safe__Step__Default_Value import type_safe_step_default_value
98

109

1110

osbot_utils/type_safe/steps/Type_Safe__Step__From_Json.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from osbot_utils.helpers.Random_Guid_Short import Random_Guid_Short
99
from osbot_utils.type_safe.shared.Type_Safe__Annotations import type_safe_annotations
1010
from osbot_utils.type_safe.shared.Type_Safe__Cache import type_safe_cache
11+
from osbot_utils.type_safe.shared.Type_Safe__Convert import type_safe_convert
1112
from osbot_utils.utils.Objects import enum_from_value
1213
from osbot_utils.helpers.Safe_Id import Safe_Id
1314
from osbot_utils.helpers.Timestamp_Now import Timestamp_Now
@@ -43,7 +44,13 @@ def deserialize_from_dict(self, _self, data, raise_on_not_found=False):
4344
raise ValueError(f"Attribute '{key}' not found in '{_self.__class__.__name__}'")
4445
else:
4546
continue
46-
if type_safe_annotations.obj_attribute_annotation(_self, key) == type: # Handle type objects
47+
annotation = type_safe_annotations.obj_attribute_annotation(_self, key)
48+
annotation_origin = type_safe_cache.get_origin(annotation)
49+
50+
51+
if annotation == type: # Handle type objects
52+
value = self.deserialize_type__using_value(value)
53+
elif annotation_origin == type: # Handle type objects inside ForwardRef
4754
value = self.deserialize_type__using_value(value)
4855
elif type_safe_annotations.obj_is_attribute_annotation_of_type(_self, key, dict): # handle the case when the value is a dict
4956
value = self.deserialize_dict__using_key_value_annotations(_self, key, value)
@@ -117,6 +124,9 @@ def deserialize_dict__using_key_value_annotations(self, _self, key, value):
117124
if type(dict_value) == value_class: # if the value is already the target, then just use it
118125
new__dict_value = dict_value
119126
elif issubclass(value_class, Type_Safe):
127+
if 'node_type' in dict_value:
128+
value_class = type_safe_convert.get_class_from_class_name(dict_value['node_type'])
129+
120130
new__dict_value = self.deserialize_from_dict(value_class(), dict_value)
121131
elif value_class is Any:
122132
new__dict_value = dict_value

tests/unit/type_safe/_bugs/test_Type_Safe__bugs.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import re
21
import sys
32
import pytest
43
from typing import Optional, Union, Dict
@@ -72,10 +71,22 @@ class Child_Class(Parent_Class):
7271
assert Parent_Class().json() == Parent_Class.from_json(Parent_Class().json()).json() # Round trip of Parent_Class works
7372

7473
# current buggy workflow
75-
with pytest.raises(ValueError, match=re.escape("Invalid type for attribute 'class_type'. Expected 'typing.Type[int]' but got '<class 'str'>'")):
76-
assert Child_Class .from_json(Child_Class().json()) # BUG should not have raised an exception
74+
# with pytest.raises(ValueError, match=re.escape("Invalid type for attribute 'class_type'. Expected 'typing.Type[int]' but got '<class 'str'>'")):
75+
# assert Child_Class .from_json(Child_Class().json()) # BUG should not have raised an exception
76+
77+
assert Child_Class.from_json(Child_Class().json()).json() == Child_Class().json() # Fixed : works now :BUG this should work
78+
79+
80+
81+
82+
83+
84+
85+
86+
87+
88+
7789

78-
#assert Child_Class.from_json(Child_Class().json()).json() == Child_Class().json() # BUG this should work
7990

8091

8192
def test__bug__in__convert_dict_to_value_from_obj_annotation(self):

tests/unit/type_safe/_regression/test_Type_Safe__regression.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,26 @@
2222

2323
class test_Type_Safe__regression(TestCase):
2424

25+
def test__regression__forward_ref_type(self):
26+
class Base__Type(Type_Safe):
27+
ref_type: Type['Base__Type']
28+
29+
30+
class Type__With__Forward__Ref(Base__Type):
31+
pass
32+
33+
target = Type__With__Forward__Ref()
34+
35+
json_data = target.json() # This serializes ref_type as string
36+
#error_message = "Invalid type for attribute 'ref_type'. Expected 'typing.Type[ForwardRef('Base__Type')]' but got '<class 'str'>'"
37+
error_message = "Could not reconstruct type from 'test_Type_Safe__regression.Type__With__Forward__Ref': module 'test_Type_Safe__regression' has no attribute 'Type__With__Forward__Ref'"
38+
#
39+
assert json_data == {'ref_type': 'test_Type_Safe__regression.Type__With__Forward__Ref'}
40+
41+
with pytest.raises(ValueError, match=re.escape(error_message)):
42+
Type__With__Forward__Ref.from_json(json_data) # Fixed we are now raising the correct exception BUG: exception should have not been raised
43+
44+
2545
def test__regression__property_descriptor_handling(self):
2646

2747
class Regular_Class: # First case: Normal Python class without Type_Safe
@@ -119,7 +139,7 @@ class Other_Class: pass
119139
with self.assertRaises(ValueError) as context:
120140
custom_node.node_type = Other_Class
121141

122-
assert str(context.exception) == "Invalid type for attribute 'node_type'. Expected 'typing.Type[ForwardRef('Base_Node')]' but got '<class 'type'>'"
142+
assert str(context.exception) == "Invalid type for attribute 'node_type'. Expected 'typing.Type[ForwardRef('Base_Node')]' but got '<class 'test_Type_Safe__regression.test_Type_Safe__regression.test__regression__forward_ref_handling_in_type_matches.<locals>.Other_Class'>'"
123143

124144
# Test with more complex case (like Schema__MGraph__Node)
125145
from typing import Dict, Any
@@ -243,14 +263,14 @@ class An_Class_1(Type_Safe):
243263

244264
an_class.an_type__str = str
245265
an_class.an_type__str = Random_Guid
246-
with pytest.raises(ValueError, match=re.escape("Invalid type for attribute 'an_type__str'. Expected 'typing.Type[str]' but got '<class 'type'>'")) :
266+
with pytest.raises(ValueError, match=re.escape("Invalid type for attribute 'an_type__str'. Expected 'typing.Type[str]' but got '<class 'int'>'")) :
247267
an_class.an_type__str = int
248268

249269
#with pytest.raises(TypeError, match=re.escape("issubclass() arg 2 must be a class, a tuple of classes, or a union")):
250270
# an_class.an_type__forward_ref = An_Class_1 # Fixed; BUG: this should have worked
251271

252272
an_class.an_type__forward_ref = An_Class_1
253-
with pytest.raises(ValueError, match=re.escape("Invalid type for attribute 'an_type__forward_ref'. Expected 'typing.Type[ForwardRef('An_Class_1')]' but got '<class 'type'>'")):
273+
with pytest.raises(ValueError, match=re.escape("Invalid type for attribute 'an_type__forward_ref'. Expected 'typing.Type[ForwardRef('An_Class_1')]' but got '<class 'str'>'")):
254274
an_class.an_type__forward_ref = str
255275

256276
class An_Class_2(An_Class_1):
@@ -347,13 +367,13 @@ class An_Class(Type_Safe):
347367
an_class.an_type_str = str
348368
an_class.an_type_int = int
349369

350-
with pytest.raises(ValueError, match=re.escape("Invalid type for attribute 'an_type_str'. Expected 'typing.Type[str]' but got '<class 'type'>")):
370+
with pytest.raises(ValueError, match=re.escape("Invalid type for attribute 'an_type_str'. Expected 'typing.Type[str]' but got '<class 'int'>")):
351371
an_class.an_type_str = int # Fixed: BUG: should have raised exception
352372

353-
with pytest.raises(ValueError, match=re.escape("Invalid type for attribute 'an_type_int'. Expected 'typing.Type[int]' but got '<class 'type'>")):
373+
with pytest.raises(ValueError, match=re.escape("Invalid type for attribute 'an_type_int'. Expected 'typing.Type[int]' but got '<class 'str'>")):
354374
an_class.an_type_int = str # Fixed: BUG: should have raised exception
355375

356-
with pytest.raises(ValueError, match=re.escape("Invalid type for attribute 'an_type_str'. Expected 'typing.Type[str]' but got '<class 'str'>'")):
376+
with pytest.raises(ValueError, match=re.escape("Invalid type for attribute 'an_type_str'. Expected 'typing.Type[str]' but got '<class 'NoneType'>'")):
357377
an_class.an_type_str = 'a'
358378

359379

tests/unit/type_safe/test_Type_Safe.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import types
44
import pytest
55
from enum import Enum, auto
6-
from typing import Union, Optional, Type, List
6+
from typing import Union, Optional, Type
77
from unittest import TestCase
88
from osbot_utils.helpers.Timestamp_Now import Timestamp_Now
99
from osbot_utils.helpers.Guid import Guid
@@ -691,7 +691,7 @@ class Type_Safety(Type_Safe): # Test type safety with Type
691691
type_safety = Type_Safety()
692692
type_safety.str_type = str # OK: str matches Type[str]
693693

694-
with pytest.raises(ValueError, match=re.escape("Invalid type for attribute 'str_type'. Expected 'typing.Type[str]' but got '<class 'type'>'")): # Should fail: int is not a subclass of str
694+
with pytest.raises(ValueError, match=re.escape("Invalid type for attribute 'str_type'. Expected 'typing.Type[str]' but got '<class 'int'>'")): # Should fail: int is not a subclass of str
695695
type_safety.str_type = int
696696

697697

@@ -976,7 +976,9 @@ class Should_Fail(Type_Safe):
976976
An_Class(node_type=Child_Type_2)
977977
An_Class(node_type=Child_Type_3)
978978
An_Class(node_type=Child_Type_4)
979-
with pytest.raises(ValueError,match=re.escape("Invalid type for attribute 'node_type'. Expected 'typing.Type[ForwardRef('An_Class')]' but got '<class 'type'>'")):
979+
980+
error_message = "Invalid type for attribute 'node_type'. Expected 'typing.Type[ForwardRef('An_Class')]' but got '<class 'test_Type_Safe.test_Type_Safe.test_type_checks_on__forward_ref__works_on_multiple_levels.<locals>.Should_Fail'>'"
981+
with pytest.raises(ValueError,match=re.escape(error_message)):
980982
An_Class(node_type=Should_Fail)
981983

982984
assert issubclass(Child_Type_1, An_Class)

0 commit comments

Comments
 (0)