osbot-utils 2.10.0__py3-none-any.whl → 2.12.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- osbot_utils/context_managers/capture_duration.py +19 -12
- osbot_utils/helpers/CPrint.py +0 -1
- osbot_utils/helpers/trace/Trace_Call.py +1 -2
- osbot_utils/helpers/trace/Trace_Call__Handler.py +14 -14
- osbot_utils/helpers/xml/Xml__File.py +1 -1
- osbot_utils/helpers/xml/Xml__File__To_Dict.py +1 -1
- osbot_utils/helpers/xml/Xml__File__To_Xml.py +1 -1
- osbot_utils/testing/performance/Performance_Measure__Session.py +108 -0
- osbot_utils/testing/performance/__init__.py +0 -0
- osbot_utils/testing/performance/models/Model__Performance_Measure__Measurement.py +14 -0
- osbot_utils/testing/performance/models/Model__Performance_Measure__Result.py +10 -0
- osbot_utils/testing/performance/models/__init__.py +0 -0
- osbot_utils/type_safe/Type_Safe.py +35 -418
- osbot_utils/type_safe/Type_Safe__Base.py +8 -24
- osbot_utils/type_safe/Type_Safe__Dict.py +9 -8
- osbot_utils/type_safe/methods/type_safe_property.py +1 -1
- osbot_utils/type_safe/shared/Type_Safe__Annotations.py +29 -0
- osbot_utils/type_safe/shared/Type_Safe__Cache.py +143 -0
- osbot_utils/type_safe/shared/Type_Safe__Convert.py +46 -0
- osbot_utils/type_safe/shared/Type_Safe__Not_Cached.py +24 -0
- osbot_utils/type_safe/shared/Type_Safe__Raise_Exception.py +14 -0
- osbot_utils/type_safe/shared/Type_Safe__Shared__Variables.py +4 -0
- osbot_utils/type_safe/shared/Type_Safe__Validation.py +246 -0
- osbot_utils/type_safe/shared/__init__.py +0 -0
- osbot_utils/type_safe/steps/Type_Safe__Step__Class_Kwargs.py +110 -0
- osbot_utils/type_safe/steps/Type_Safe__Step__Default_Kwargs.py +42 -0
- osbot_utils/type_safe/steps/Type_Safe__Step__Default_Value.py +74 -0
- osbot_utils/type_safe/steps/Type_Safe__Step__From_Json.py +138 -0
- osbot_utils/type_safe/steps/Type_Safe__Step__Init.py +24 -0
- osbot_utils/type_safe/steps/Type_Safe__Step__Set_Attr.py +92 -0
- osbot_utils/type_safe/steps/__init__.py +0 -0
- osbot_utils/utils/Objects.py +27 -232
- osbot_utils/utils/Status.py +0 -2
- osbot_utils/version +1 -1
- {osbot_utils-2.10.0.dist-info → osbot_utils-2.12.0.dist-info}/METADATA +3 -3
- {osbot_utils-2.10.0.dist-info → osbot_utils-2.12.0.dist-info}/RECORD +38 -18
- {osbot_utils-2.10.0.dist-info → osbot_utils-2.12.0.dist-info}/WHEEL +1 -1
- {osbot_utils-2.10.0.dist-info → osbot_utils-2.12.0.dist-info}/LICENSE +0 -0
@@ -0,0 +1,74 @@
|
|
1
|
+
|
2
|
+
import sys
|
3
|
+
import inspect
|
4
|
+
import typing
|
5
|
+
|
6
|
+
from osbot_utils.type_safe.shared.Type_Safe__Cache import type_safe_cache
|
7
|
+
from osbot_utils.utils.Objects import default_value
|
8
|
+
from osbot_utils.type_safe.Type_Safe__List import Type_Safe__List
|
9
|
+
from osbot_utils.type_safe.Type_Safe__Dict import Type_Safe__Dict
|
10
|
+
|
11
|
+
|
12
|
+
# Backport implementations of get_args for Python 3.7 # todo: refactor into separate class (focused on past python version compatibility)
|
13
|
+
if sys.version_info < (3, 8): # pragma: no cover
|
14
|
+
|
15
|
+
def get_args(tp):
|
16
|
+
if isinstance(tp, typing._GenericAlias):
|
17
|
+
return tp.__args__
|
18
|
+
else:
|
19
|
+
return ()
|
20
|
+
else:
|
21
|
+
from typing import get_args, ForwardRef
|
22
|
+
|
23
|
+
|
24
|
+
class Type_Safe__Step__Default_Value:
|
25
|
+
|
26
|
+
def default_value(self, _cls, var_type):
|
27
|
+
|
28
|
+
origin = type_safe_cache.get_origin(var_type) # todo: refactor this to use the get_origin method
|
29
|
+
if origin is type: # Special handling for Type[T] # todo: reuse the get_origin value
|
30
|
+
type_args = get_args(var_type)
|
31
|
+
if type_args:
|
32
|
+
if isinstance(type_args[0], ForwardRef):
|
33
|
+
forward_name = type_args[0].__forward_arg__
|
34
|
+
for base_cls in inspect.getmro(_cls):
|
35
|
+
if base_cls.__name__ == forward_name:
|
36
|
+
return _cls # note: in this case we return the cls, and not the base_cls (which makes sense since this happens when the cls class uses base_cls as base, which has a ForwardRef to base_cls )
|
37
|
+
return type_args[0] # Return the actual type as the default value
|
38
|
+
|
39
|
+
if var_type is typing.Set: # todo: refactor the dict, set and list logic, since they are 90% the same
|
40
|
+
return set()
|
41
|
+
|
42
|
+
if origin is set:
|
43
|
+
return set() # todo: add Type_Safe__Set
|
44
|
+
|
45
|
+
if var_type is typing.Dict:
|
46
|
+
return {}
|
47
|
+
|
48
|
+
if origin is dict: # e.g. Dict[key_type, value_type]
|
49
|
+
key_type, value_type = get_args(var_type)
|
50
|
+
if isinstance(key_type, ForwardRef): # Handle forward references on key_type ---
|
51
|
+
forward_name = key_type.__forward_arg__
|
52
|
+
if forward_name == _cls.__name__:
|
53
|
+
key_type = _cls
|
54
|
+
if isinstance(value_type, ForwardRef): # Handle forward references on value_type ---
|
55
|
+
forward_name = value_type.__forward_arg__
|
56
|
+
if forward_name == _cls.__name__:
|
57
|
+
value_type = _cls
|
58
|
+
return Type_Safe__Dict(expected_key_type=key_type, expected_value_type=value_type)
|
59
|
+
|
60
|
+
if var_type is typing.List:
|
61
|
+
return [] # handle case when List was used with no type information provided
|
62
|
+
|
63
|
+
if origin is list: # if we have list defined as list[type]
|
64
|
+
item_type = get_args(var_type)[0] # get the type that was defined
|
65
|
+
if isinstance(item_type, ForwardRef): # handle the case when the type is a forward reference
|
66
|
+
forward_name = item_type.__forward_arg__
|
67
|
+
if forward_name == _cls.__name__: # if the forward reference is to the current class (simple name check)
|
68
|
+
item_type = _cls # set the item_type to the current class
|
69
|
+
return Type_Safe__List(expected_type=item_type) # and used it as expected_type in Type_Safe__List
|
70
|
+
else:
|
71
|
+
return default_value(var_type) # for all other cases call default_value, which will try to create a default instance
|
72
|
+
|
73
|
+
|
74
|
+
type_safe_step_default_value = Type_Safe__Step__Default_Value()
|
@@ -0,0 +1,138 @@
|
|
1
|
+
import sys
|
2
|
+
import types
|
3
|
+
from decimal import Decimal
|
4
|
+
from enum import EnumMeta
|
5
|
+
from osbot_utils.type_safe.Type_Safe import Type_Safe
|
6
|
+
from osbot_utils.type_safe.Type_Safe__List import Type_Safe__List
|
7
|
+
from osbot_utils.helpers.Random_Guid import Random_Guid
|
8
|
+
from osbot_utils.helpers.Random_Guid_Short import Random_Guid_Short
|
9
|
+
from osbot_utils.type_safe.shared.Type_Safe__Annotations import type_safe_annotations
|
10
|
+
from osbot_utils.type_safe.shared.Type_Safe__Cache import type_safe_cache
|
11
|
+
from osbot_utils.utils.Objects import enum_from_value
|
12
|
+
from osbot_utils.helpers.Safe_Id import Safe_Id
|
13
|
+
from osbot_utils.helpers.Timestamp_Now import Timestamp_Now
|
14
|
+
|
15
|
+
# todo; refactor all this python compatibility into the python_3_8 class
|
16
|
+
if sys.version_info < (3, 8): # pragma: no cover
|
17
|
+
|
18
|
+
def get_args(tp):
|
19
|
+
import typing
|
20
|
+
if isinstance(tp, typing._GenericAlias):
|
21
|
+
return tp.__args__
|
22
|
+
else:
|
23
|
+
return ()
|
24
|
+
else:
|
25
|
+
from typing import get_args, Any
|
26
|
+
|
27
|
+
|
28
|
+
class Type_Safe__Step__From_Json:
|
29
|
+
|
30
|
+
# todo: this needs refactoring, since the logic and code is getting quite complex (to be inside methods like this)
|
31
|
+
def deserialize_from_dict(self, _self, data, raise_on_not_found=False):
|
32
|
+
|
33
|
+
if hasattr(data, 'items') is False:
|
34
|
+
raise ValueError(f"Expected a dictionary, but got '{type(data)}'")
|
35
|
+
|
36
|
+
for key, value in data.items():
|
37
|
+
if hasattr(_self, key) and isinstance(getattr(_self, key), Type_Safe):
|
38
|
+
self.deserialize_from_dict(getattr(_self, key), value) # if the attribute is a Type_Safe object, then also deserialize it
|
39
|
+
else:
|
40
|
+
if hasattr(_self, '__annotations__'): # can only do type safety checks if the class does not have annotations
|
41
|
+
if hasattr(_self, key) is False: # make sure we are now adding new attributes to the class
|
42
|
+
if raise_on_not_found:
|
43
|
+
raise ValueError(f"Attribute '{key}' not found in '{_self.__class__.__name__}'")
|
44
|
+
else:
|
45
|
+
continue
|
46
|
+
if type_safe_annotations.obj_attribute_annotation(_self, key) == type: # Handle type objects
|
47
|
+
value = self.deserialize_type__using_value(value)
|
48
|
+
elif type_safe_annotations.obj_is_attribute_annotation_of_type(_self, key, dict): # handle the case when the value is a dict
|
49
|
+
value = self.deserialize_dict__using_key_value_annotations(_self, key, value)
|
50
|
+
elif type_safe_annotations.obj_is_attribute_annotation_of_type(_self, key, list): # handle the case when the value is a list
|
51
|
+
attribute_annotation = type_safe_annotations.obj_attribute_annotation(_self, key) # get the annotation for this variable
|
52
|
+
attribute_annotation_args = get_args(attribute_annotation)
|
53
|
+
if attribute_annotation_args:
|
54
|
+
expected_type = get_args(attribute_annotation)[0] # get the first arg (which is the type)
|
55
|
+
type_safe_list = Type_Safe__List(expected_type) # create a new instance of Type_Safe__List
|
56
|
+
for item in value: # next we need to convert all items (to make sure they all match the type)
|
57
|
+
if type(item) is dict:
|
58
|
+
new_item = expected_type(**item) # create new object
|
59
|
+
else:
|
60
|
+
new_item = expected_type(item)
|
61
|
+
type_safe_list.append(new_item) # and add it to the new type_safe_list obejct
|
62
|
+
value = type_safe_list # todo: refactor out this create list code, maybe to an deserialize_from_list method
|
63
|
+
else:
|
64
|
+
if value is not None:
|
65
|
+
if type_safe_annotations.obj_is_attribute_annotation_of_type(_self, key, EnumMeta): # Handle the case when the value is an Enum
|
66
|
+
enum_type = getattr(_self, '__annotations__').get(key)
|
67
|
+
if type(value) is not enum_type: # If the value is not already of the target type
|
68
|
+
value = enum_from_value(enum_type, value) # Try to resolve the value into the enum
|
69
|
+
|
70
|
+
# todo: refactor these special cases into a separate method to class
|
71
|
+
elif type_safe_annotations.obj_is_attribute_annotation_of_type(_self, key, Decimal): # handle Decimals
|
72
|
+
value = Decimal(value)
|
73
|
+
elif type_safe_annotations.obj_is_attribute_annotation_of_type(_self, key, Safe_Id): # handle Safe_Id
|
74
|
+
value = Safe_Id(value)
|
75
|
+
elif type_safe_annotations.obj_is_attribute_annotation_of_type(_self, key, Random_Guid): # handle Random_Guid
|
76
|
+
value = Random_Guid(value)
|
77
|
+
elif type_safe_annotations.obj_is_attribute_annotation_of_type(_self, key, Random_Guid_Short): # handle Random_Guid_Short
|
78
|
+
value = Random_Guid_Short(value)
|
79
|
+
elif type_safe_annotations.obj_is_attribute_annotation_of_type(_self, key, Timestamp_Now): # handle Timestamp_Now
|
80
|
+
value = Timestamp_Now(value)
|
81
|
+
setattr(_self, key, value) # Direct assignment for primitive types and other structures
|
82
|
+
|
83
|
+
return _self
|
84
|
+
|
85
|
+
def deserialize_type__using_value(self, value):
|
86
|
+
if value:
|
87
|
+
try:
|
88
|
+
module_name, type_name = value.rsplit('.', 1)
|
89
|
+
if module_name == 'builtins' and type_name == 'NoneType': # Special case for NoneType (which serialises as builtins.* , but it actually in types.* )
|
90
|
+
value = types.NoneType
|
91
|
+
else:
|
92
|
+
module = __import__(module_name, fromlist=[type_name])
|
93
|
+
value = getattr(module, type_name)
|
94
|
+
except (ValueError, ImportError, AttributeError) as e:
|
95
|
+
raise ValueError(f"Could not reconstruct type from '{value}': {str(e)}")
|
96
|
+
return value
|
97
|
+
|
98
|
+
def deserialize_dict__using_key_value_annotations(self, _self, key, value):
|
99
|
+
from osbot_utils.type_safe.Type_Safe__Dict import Type_Safe__Dict
|
100
|
+
|
101
|
+
annotations = type_safe_cache.get_obj_annotations(_self)
|
102
|
+
dict_annotations_tuple = get_args(annotations.get(key))
|
103
|
+
if not dict_annotations_tuple: # happens when the value is a dict/Dict with no annotations
|
104
|
+
return value
|
105
|
+
if not type(value) is dict:
|
106
|
+
return value
|
107
|
+
key_class = dict_annotations_tuple[0]
|
108
|
+
value_class = dict_annotations_tuple[1]
|
109
|
+
new_value = Type_Safe__Dict(expected_key_type=key_class, expected_value_type=value_class)
|
110
|
+
|
111
|
+
for dict_key, dict_value in value.items():
|
112
|
+
if issubclass(key_class, Type_Safe):
|
113
|
+
new__dict_key = self.deserialize_from_dict(key_class(), dict_key)
|
114
|
+
else:
|
115
|
+
new__dict_key = key_class(dict_key)
|
116
|
+
|
117
|
+
if type(dict_value) == value_class: # if the value is already the target, then just use it
|
118
|
+
new__dict_value = dict_value
|
119
|
+
elif issubclass(value_class, Type_Safe):
|
120
|
+
new__dict_value = self.deserialize_from_dict(value_class(), dict_value)
|
121
|
+
elif value_class is Any:
|
122
|
+
new__dict_value = dict_value
|
123
|
+
else:
|
124
|
+
new__dict_value = value_class(dict_value)
|
125
|
+
new_value[new__dict_key] = new__dict_value
|
126
|
+
|
127
|
+
return new_value
|
128
|
+
|
129
|
+
def from_json(self, _cls, json_data, raise_on_not_found=False):
|
130
|
+
from osbot_utils.utils.Json import json_parse
|
131
|
+
|
132
|
+
if type(json_data) is str:
|
133
|
+
json_data = json_parse(json_data)
|
134
|
+
if json_data: # if there is no data or is {} then don't create an object (since this could be caused by bad data being provided)
|
135
|
+
return self.deserialize_from_dict(_cls(), json_data,raise_on_not_found=raise_on_not_found)
|
136
|
+
return _cls()
|
137
|
+
|
138
|
+
type_safe_step_from_json = Type_Safe__Step__From_Json()
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Type_Safe__Step__Init:
|
2
|
+
|
3
|
+
def init(self, __self ,
|
4
|
+
__class_kwargs ,
|
5
|
+
**kwargs
|
6
|
+
) -> None:
|
7
|
+
|
8
|
+
for (key, value) in __class_kwargs.items(): # assign all default values to target
|
9
|
+
if hasattr(__self, key):
|
10
|
+
existing_value = getattr(__self, key)
|
11
|
+
if existing_value is not None:
|
12
|
+
setattr(__self, key, existing_value)
|
13
|
+
continue
|
14
|
+
setattr(__self, key, value)
|
15
|
+
|
16
|
+
for (key, value) in kwargs.items(): # overwrite with values provided in ctor
|
17
|
+
if hasattr(__self, key):
|
18
|
+
if value is not None: # prevent None values from overwriting existing values, which is quite common in default constructors
|
19
|
+
setattr(__self, key, value)
|
20
|
+
else:
|
21
|
+
raise ValueError(f"{__self.__class__.__name__} has no attribute '{key}' and cannot be assigned the value '{value}'. "
|
22
|
+
f"Use {__self.__class__.__name__}.__default_kwargs__() see what attributes are available")
|
23
|
+
|
24
|
+
type_safe_step_init = Type_Safe__Step__Init()
|
@@ -0,0 +1,92 @@
|
|
1
|
+
from typing import get_origin, Annotated, get_args
|
2
|
+
from osbot_utils.type_safe.shared.Type_Safe__Cache import type_safe_cache
|
3
|
+
from osbot_utils.type_safe.shared.Type_Safe__Convert import type_safe_convert
|
4
|
+
from osbot_utils.type_safe.shared.Type_Safe__Validation import type_safe_validation
|
5
|
+
from osbot_utils.type_safe.validators.Type_Safe__Validator import Type_Safe__Validator
|
6
|
+
|
7
|
+
class Type_Safe__Step__Set_Attr:
|
8
|
+
|
9
|
+
def resolve_value(self, _self, annotations, name, value):
|
10
|
+
if type(value) is dict:
|
11
|
+
value = self.resolve_value__dict(_self, name, value)
|
12
|
+
elif type(value) in [int, str]: # for now only a small number of str and int classes are supported (until we understand the full implications of this)
|
13
|
+
value = self.resolve_value__int_str(_self, name, value)
|
14
|
+
else:
|
15
|
+
value = self.resolve_value__from_origin(value)
|
16
|
+
|
17
|
+
type_safe_validation.validate_type_compatibility(_self, annotations, name, value)
|
18
|
+
return value
|
19
|
+
|
20
|
+
def resolve_value__dict(self, _self, name, value):
|
21
|
+
return type_safe_convert.convert_dict_to_value_from_obj_annotation(_self, name, value)
|
22
|
+
|
23
|
+
def resolve_value__int_str(self, _self, name, value):
|
24
|
+
immutable_vars = type_safe_cache.get_class_immutable_vars(_self.__class__) # get the cached value of immutable vars for this class
|
25
|
+
|
26
|
+
if name in immutable_vars: # we only need to do the conversion if the variable is immutable
|
27
|
+
return value
|
28
|
+
|
29
|
+
return type_safe_convert.convert_to_value_from_obj_annotation(_self, name, value)
|
30
|
+
|
31
|
+
def resolve_value__from_origin(self, value):
|
32
|
+
#origin = type_safe_cache.get_origin(value) # todo: figure out why this is the only place that the type_safe_cache.get_origin doesn't work (due to WeakKeyDictionary key error on value)
|
33
|
+
origin = get_origin(value)
|
34
|
+
|
35
|
+
if origin is not None:
|
36
|
+
value = origin
|
37
|
+
return value
|
38
|
+
|
39
|
+
def handle_get_class__annotated(self, annotation, name, value):
|
40
|
+
annotation_args = get_args(annotation)
|
41
|
+
target_type = annotation_args[0]
|
42
|
+
for attribute in annotation_args[1:]:
|
43
|
+
if isinstance(attribute, Type_Safe__Validator):
|
44
|
+
attribute.validate(value=value, field_name=name, target_type=target_type)
|
45
|
+
|
46
|
+
def handle_get_class__dict(self, _self, name, value):
|
47
|
+
if value: # todo: see side effects of doing this here (since going into deserialize_dict__using_key_value_annotations has performance hit)
|
48
|
+
from osbot_utils.type_safe.steps.Type_Safe__Step__From_Json import Type_Safe__Step__From_Json # here because of circular dependencies
|
49
|
+
value = Type_Safe__Step__From_Json().deserialize_dict__using_key_value_annotations(_self, name, value) # todo: refactor how this actually works since it is not good to having to use the deserialize_dict__using_key_value_annotations from here
|
50
|
+
return value
|
51
|
+
|
52
|
+
def handle_get_class(self, _self, annotations, name, value):
|
53
|
+
if hasattr(annotations, 'get'):
|
54
|
+
annotation = annotations.get(name)
|
55
|
+
if annotation:
|
56
|
+
annotation_origin = type_safe_cache.get_origin(annotation)
|
57
|
+
if annotation_origin is Annotated:
|
58
|
+
self.handle_get_class__annotated(annotation, name, value)
|
59
|
+
elif annotation_origin is dict:
|
60
|
+
value = self.handle_get_class__dict(_self, name, value)
|
61
|
+
return value
|
62
|
+
|
63
|
+
def handle_special_generic_alias(self, _super, _self, name, value):
|
64
|
+
immutable_vars = type_safe_cache.get_class_immutable_vars(_self.__class__) # todo: refactor this section into a separate method
|
65
|
+
if name in immutable_vars:
|
66
|
+
expected_type = immutable_vars[name]
|
67
|
+
current_type = type if value is type else type(value)
|
68
|
+
type_safe_validation.validate_if__types_are_compatible_for_assigment(name, current_type, expected_type)
|
69
|
+
_super.__setattr__(name, value)
|
70
|
+
return True
|
71
|
+
return False
|
72
|
+
|
73
|
+
def setattr(self, _super, _self, name, value):
|
74
|
+
if type_safe_validation.check_if__value_is__special_generic_alias(value):
|
75
|
+
if self.handle_special_generic_alias(_super, _self, name, value):
|
76
|
+
return
|
77
|
+
|
78
|
+
annotations = dict(type_safe_cache.get_obj_annotations(_self))
|
79
|
+
|
80
|
+
if not annotations: # can't do type safety checks if the class does not have annotations
|
81
|
+
return _super.__setattr__(name, value)
|
82
|
+
|
83
|
+
if value is not None:
|
84
|
+
value = self.resolve_value (_self, annotations, name, value)
|
85
|
+
value = self.handle_get_class(_self, annotations, name, value)
|
86
|
+
else:
|
87
|
+
type_safe_validation.validate_if_value_has_been_set(_self, annotations, name, value)
|
88
|
+
|
89
|
+
_super.__setattr__(name, value)
|
90
|
+
|
91
|
+
|
92
|
+
type_safe_step_set_attr = Type_Safe__Step__Set_Attr()
|
File without changes
|
osbot_utils/utils/Objects.py
CHANGED
@@ -1,73 +1,10 @@
|
|
1
1
|
# todo add tests
|
2
2
|
import sys
|
3
|
-
from types
|
4
|
-
from osbot_utils.helpers.python_compatibility.python_3_8 import Annotated
|
5
|
-
|
3
|
+
from types import SimpleNamespace
|
6
4
|
|
7
5
|
class __(SimpleNamespace):
|
8
6
|
pass
|
9
7
|
|
10
|
-
# Backport implementations of get_origin and get_args for Python 3.7
|
11
|
-
if sys.version_info < (3, 8):
|
12
|
-
def get_origin(tp):
|
13
|
-
import typing
|
14
|
-
if isinstance(tp, typing._GenericAlias):
|
15
|
-
return tp.__origin__
|
16
|
-
elif tp is typing.Generic:
|
17
|
-
return typing.Generic
|
18
|
-
else:
|
19
|
-
return None
|
20
|
-
|
21
|
-
def get_args(tp):
|
22
|
-
import typing
|
23
|
-
if isinstance(tp, typing._GenericAlias):
|
24
|
-
return tp.__args__
|
25
|
-
else:
|
26
|
-
return ()
|
27
|
-
else:
|
28
|
-
from typing import get_origin, get_args, List, Tuple, Dict, Type, _GenericAlias, ForwardRef
|
29
|
-
|
30
|
-
|
31
|
-
def are_types_compatible_for_assigment(source_type, target_type):
|
32
|
-
import types
|
33
|
-
import typing
|
34
|
-
|
35
|
-
if isinstance(target_type, str): # If the "target_type" is a forward reference (string), handle it here.
|
36
|
-
if target_type == source_type.__name__: # Simple check: does the string match the actual class name
|
37
|
-
return True
|
38
|
-
if source_type is target_type:
|
39
|
-
return True
|
40
|
-
if source_type is int and target_type is float:
|
41
|
-
return True
|
42
|
-
if target_type in source_type.__mro__: # this means that the source_type has the target_type has of its base types
|
43
|
-
return True
|
44
|
-
if target_type is callable: # handle case where callable was used as the target type
|
45
|
-
if source_type is types.MethodType: # and a method or function was used as the source type
|
46
|
-
return True
|
47
|
-
if source_type is types.FunctionType:
|
48
|
-
return True
|
49
|
-
if source_type is staticmethod:
|
50
|
-
return True
|
51
|
-
if target_type is typing.Any:
|
52
|
-
return True
|
53
|
-
return False
|
54
|
-
|
55
|
-
def are_types_magic_mock(source_type, target_type):
|
56
|
-
from unittest.mock import MagicMock
|
57
|
-
if isinstance(source_type, MagicMock):
|
58
|
-
return True
|
59
|
-
if isinstance(target_type, MagicMock):
|
60
|
-
return True
|
61
|
-
if source_type is MagicMock:
|
62
|
-
return True
|
63
|
-
if target_type is MagicMock:
|
64
|
-
return True
|
65
|
-
# if class_full_name(source_type) == 'unittest.mock.MagicMock':
|
66
|
-
# return True
|
67
|
-
# if class_full_name(target_type) == 'unittest.mock.MagicMock':
|
68
|
-
# return True
|
69
|
-
return False
|
70
|
-
|
71
8
|
def base_classes(cls):
|
72
9
|
if type(cls) is type:
|
73
10
|
target = cls
|
@@ -106,46 +43,6 @@ def class_full_name(target):
|
|
106
43
|
type_name = type_target.__name__
|
107
44
|
return f'{type_module}.{type_name}'
|
108
45
|
|
109
|
-
def convert_dict_to_value_from_obj_annotation(target, attr_name, value): # todo: refactor this with code from convert_str_to_value_from_obj_annotation since it is mostly the same
|
110
|
-
if target is not None and attr_name is not None:
|
111
|
-
if hasattr(target, '__annotations__'):
|
112
|
-
obj_annotations = target.__annotations__
|
113
|
-
if hasattr(obj_annotations,'get'):
|
114
|
-
attribute_annotation = obj_annotations.get(attr_name)
|
115
|
-
if 'Type_Safe' in base_classes_names(attribute_annotation):
|
116
|
-
return attribute_annotation(**value)
|
117
|
-
return value
|
118
|
-
|
119
|
-
def convert_to_value_from_obj_annotation(target, attr_name, value): # todo: see the side effects of doing this for all ints and floats
|
120
|
-
|
121
|
-
from osbot_utils.helpers.Guid import Guid
|
122
|
-
from osbot_utils.helpers.Timestamp_Now import Timestamp_Now
|
123
|
-
from osbot_utils.helpers.Random_Guid import Random_Guid
|
124
|
-
from osbot_utils.helpers.Safe_Id import Safe_Id
|
125
|
-
from osbot_utils.helpers.Str_ASCII import Str_ASCII
|
126
|
-
|
127
|
-
TYPE_SAFE__CONVERT_VALUE__SUPPORTED_TYPES = [Guid, Random_Guid, Safe_Id, Str_ASCII, Timestamp_Now]
|
128
|
-
|
129
|
-
if target is not None and attr_name is not None:
|
130
|
-
if hasattr(target, '__annotations__'):
|
131
|
-
obj_annotations = target.__annotations__
|
132
|
-
if hasattr(obj_annotations,'get'):
|
133
|
-
attribute_annotation = obj_annotations.get(attr_name)
|
134
|
-
if attribute_annotation:
|
135
|
-
origin = get_origin(attribute_annotation) # Add handling for Type[T] annotations
|
136
|
-
if origin is type and isinstance(value, str):
|
137
|
-
try: # Convert string path to actual type
|
138
|
-
if len(value.rsplit('.', 1)) > 1:
|
139
|
-
module_name, class_name = value.rsplit('.', 1)
|
140
|
-
module = __import__(module_name, fromlist=[class_name])
|
141
|
-
return getattr(module, class_name)
|
142
|
-
except (ValueError, ImportError, AttributeError) as e:
|
143
|
-
raise ValueError(f"Could not convert '{value}' to type: {str(e)}")
|
144
|
-
|
145
|
-
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
|
146
|
-
return attribute_annotation(value)
|
147
|
-
return value
|
148
|
-
|
149
46
|
|
150
47
|
def default_value(target : type):
|
151
48
|
try:
|
@@ -358,93 +255,6 @@ def obj_get_value(target=None, key=None, default=None):
|
|
358
255
|
def obj_values(target=None):
|
359
256
|
return list(obj_dict(target).values())
|
360
257
|
|
361
|
-
def raise_exception_on_obj_type_annotation_mismatch(target, attr_name, value):
|
362
|
-
if value_type_matches_obj_annotation_for_attr(target, attr_name, value) is False: # handle case with normal types
|
363
|
-
if value_type_matches_obj_annotation_for_union_and_annotated(target, attr_name, value) is True: # handle union cases
|
364
|
-
return # this is done like this because value_type_matches_obj_annotation_for_union_attr will return None when there is no Union objects
|
365
|
-
raise TypeError(f"Invalid type for attribute '{attr_name}'. Expected '{target.__annotations__.get(attr_name)}' but got '{type(value)}'")
|
366
|
-
|
367
|
-
def obj_attribute_annotation(target, attr_name):
|
368
|
-
if target is not None and attr_name is not None:
|
369
|
-
if hasattr(target, '__annotations__'):
|
370
|
-
obj_annotations = target.__annotations__
|
371
|
-
if hasattr(obj_annotations,'get'):
|
372
|
-
attribute_annotation = obj_annotations.get(attr_name)
|
373
|
-
return attribute_annotation
|
374
|
-
return None
|
375
|
-
|
376
|
-
def obj_is_attribute_annotation_of_type(target, attr_name, expected_type):
|
377
|
-
attribute_annotation = obj_attribute_annotation(target, attr_name)
|
378
|
-
if expected_type is attribute_annotation:
|
379
|
-
return True
|
380
|
-
if expected_type is type(attribute_annotation):
|
381
|
-
return True
|
382
|
-
if expected_type is get_origin(attribute_annotation): # handle genericAlias
|
383
|
-
return True
|
384
|
-
return False
|
385
|
-
|
386
|
-
def obj_is_type_union_compatible(var_type, compatible_types):
|
387
|
-
from typing import Union
|
388
|
-
|
389
|
-
origin = get_origin(var_type)
|
390
|
-
if isinstance(var_type, _GenericAlias) and origin is type: # Add handling for Type[T]
|
391
|
-
return type in compatible_types # Allow if 'type' is in compatible types
|
392
|
-
if origin is Union: # For Union types, including Optionals
|
393
|
-
args = get_args(var_type) # Get the argument types
|
394
|
-
for arg in args: # Iterate through each argument in the Union
|
395
|
-
if not (arg in compatible_types or arg is type(None)): # Check if the argument is either in the compatible_types or is type(None)
|
396
|
-
return False # If any arg doesn't meet the criteria, return False immediately
|
397
|
-
return True # If all args are compatible, return True
|
398
|
-
return var_type in compatible_types or var_type is type(None) # Check for direct compatibility or type(None) for non-Union types
|
399
|
-
|
400
|
-
|
401
|
-
def value_type_matches_obj_annotation_for_union_and_annotated(target, attr_name, value):
|
402
|
-
from osbot_utils.helpers.python_compatibility.python_3_8 import Annotated
|
403
|
-
from typing import Union, get_origin, get_args
|
404
|
-
|
405
|
-
value_type = type(value)
|
406
|
-
attribute_annotation = obj_attribute_annotation(target, attr_name)
|
407
|
-
origin = get_origin(attribute_annotation)
|
408
|
-
|
409
|
-
if origin is Union: # Handle Union types (including Optional)
|
410
|
-
args = get_args(attribute_annotation)
|
411
|
-
return value_type in args
|
412
|
-
|
413
|
-
# todo: refactor the logic below to a separate method (and check for duplicate code with other get_origin usage)
|
414
|
-
if origin is Annotated: # Handle Annotated types
|
415
|
-
args = get_args(attribute_annotation)
|
416
|
-
base_type = args[0] # First argument is the base type
|
417
|
-
base_origin = get_origin(base_type)
|
418
|
-
|
419
|
-
if base_origin is None: # Non-container types
|
420
|
-
return isinstance(value, base_type)
|
421
|
-
|
422
|
-
if base_origin in (list, List): # Handle List types
|
423
|
-
if not isinstance(value, list):
|
424
|
-
return False
|
425
|
-
item_type = get_args(base_type)[0]
|
426
|
-
return all(isinstance(item, item_type) for item in value)
|
427
|
-
|
428
|
-
if base_origin in (tuple, Tuple): # Handle Tuple types
|
429
|
-
if not isinstance(value, tuple):
|
430
|
-
return False
|
431
|
-
item_types = get_args(base_type)
|
432
|
-
return len(value) == len(item_types) and all(
|
433
|
-
isinstance(item, item_type)
|
434
|
-
for item, item_type in zip(value, item_types)
|
435
|
-
)
|
436
|
-
|
437
|
-
if base_origin in (dict, Dict): # Handle Dict types
|
438
|
-
if not isinstance(value, dict):
|
439
|
-
return False
|
440
|
-
key_type, value_type = get_args(base_type)
|
441
|
-
return all(isinstance(k, key_type) and isinstance(v, value_type)
|
442
|
-
for k, v in value.items())
|
443
|
-
|
444
|
-
# todo: add support for for other typing constructs
|
445
|
-
return None # if it is not a Union or Annotated types just return None (to give an indication to the caller that the comparison was not made)
|
446
|
-
|
447
|
-
|
448
258
|
def pickle_save_to_bytes(target: object) -> bytes:
|
449
259
|
import pickle
|
450
260
|
return pickle.dumps(target)
|
@@ -457,47 +267,32 @@ def pickle_load_from_bytes(pickled_data: bytes):
|
|
457
267
|
except Exception:
|
458
268
|
return {}
|
459
269
|
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
elif origin_attr_type is typing.Union:
|
487
|
-
args = get_args(attr_type)
|
488
|
-
if len(args)==2 and args[1] is type(None): # todo: find a better way to do this, since this is handling an edge case when origin_attr_type is Optional (which is an shorthand for Union[X, None] )
|
489
|
-
attr_type = args[0]
|
490
|
-
origin_attr_type = get_origin(attr_type)
|
491
|
-
|
492
|
-
if origin_attr_type:
|
493
|
-
attr_type = origin_attr_type
|
494
|
-
value_type = type(value)
|
495
|
-
if are_types_compatible_for_assigment(source_type=value_type, target_type=attr_type):
|
496
|
-
return True
|
497
|
-
if are_types_magic_mock(source_type=value_type, target_type=attr_type):
|
498
|
-
return True
|
499
|
-
return value_type is attr_type
|
500
|
-
return None
|
270
|
+
# todo: see if it is possible to add recursive protection to this logic
|
271
|
+
def serialize_to_dict(obj):
|
272
|
+
from decimal import Decimal
|
273
|
+
from enum import Enum
|
274
|
+
from typing import List
|
275
|
+
|
276
|
+
if isinstance(obj, (str, int, float, bool, bytes, Decimal)) or obj is None:
|
277
|
+
return obj
|
278
|
+
elif isinstance(obj, Enum):
|
279
|
+
return obj.name
|
280
|
+
elif isinstance(obj, type):
|
281
|
+
return f"{obj.__module__}.{obj.__name__}" # save the full type name
|
282
|
+
elif isinstance(obj, list) or isinstance(obj, List):
|
283
|
+
return [serialize_to_dict(item) for item in obj]
|
284
|
+
elif isinstance(obj, dict):
|
285
|
+
return {key: serialize_to_dict(value) for key, value in obj.items()}
|
286
|
+
elif hasattr(obj, "__dict__"):
|
287
|
+
data = {} # todo: look at a more advanced version which saved the type of the object, for example with {'__type__': type(obj).__name__}
|
288
|
+
for key, value in obj.__dict__.items():
|
289
|
+
if key.startswith('__') is False: # don't process internal variables (for example the ones set by @cache_on_self)
|
290
|
+
data[key] = serialize_to_dict(value) # Recursive call for complex types
|
291
|
+
return data
|
292
|
+
else:
|
293
|
+
raise TypeError(f"Type {type(obj)} not serializable")
|
294
|
+
|
295
|
+
|
501
296
|
|
502
297
|
|
503
298
|
|
osbot_utils/utils/Status.py
CHANGED
osbot_utils/version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
v2.
|
1
|
+
v2.12.0
|