osbot-utils 2.11.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/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.11.0.dist-info → osbot_utils-2.12.0.dist-info}/METADATA +2 -2
- {osbot_utils-2.11.0.dist-info → osbot_utils-2.12.0.dist-info}/RECORD +37 -17
- {osbot_utils-2.11.0.dist-info → osbot_utils-2.12.0.dist-info}/LICENSE +0 -0
- {osbot_utils-2.11.0.dist-info → osbot_utils-2.12.0.dist-info}/WHEEL +0 -0
@@ -1,134 +1,26 @@
|
|
1
1
|
# todo: find a way to add these documentations strings to a separate location so that
|
2
2
|
# the data is available in IDE's code complete
|
3
|
-
import
|
4
|
-
import
|
5
|
-
import
|
6
|
-
from osbot_utils.
|
7
|
-
from osbot_utils.
|
8
|
-
|
9
|
-
|
10
|
-
if sys.version_info < (3, 8): # pragma: no cover
|
11
|
-
def get_origin(tp):
|
12
|
-
import typing
|
13
|
-
if isinstance(tp, typing._GenericAlias):
|
14
|
-
return tp.__origin__
|
15
|
-
elif tp is typing.Generic:
|
16
|
-
return typing.Generic
|
17
|
-
else:
|
18
|
-
return None
|
19
|
-
|
20
|
-
def get_args(tp):
|
21
|
-
import typing
|
22
|
-
if isinstance(tp, typing._GenericAlias):
|
23
|
-
return tp.__args__
|
24
|
-
else:
|
25
|
-
return ()
|
26
|
-
else:
|
27
|
-
from typing import get_origin, get_args, ForwardRef, Any
|
28
|
-
from osbot_utils.helpers.python_compatibility.python_3_8 import Annotated
|
29
|
-
|
30
|
-
if sys.version_info >= (3, 10):
|
31
|
-
NoneType = types.NoneType
|
32
|
-
else: # pragma: no cover
|
33
|
-
NoneType = type(None)
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
#todo: see if we can also add type safety to method execution
|
39
|
-
# for example if we have an method like def add_node(self, title: str, call_index: int):
|
40
|
-
# throw an exception if the type of the value passed in is not the same as the one defined in the method
|
3
|
+
from osbot_utils.type_safe.shared.Type_Safe__Validation import type_safe_validation
|
4
|
+
from osbot_utils.type_safe.steps.Type_Safe__Step__Class_Kwargs import type_safe_step_class_kwargs
|
5
|
+
from osbot_utils.type_safe.steps.Type_Safe__Step__Default_Kwargs import type_safe_step_default_kwargs
|
6
|
+
from osbot_utils.type_safe.steps.Type_Safe__Step__Default_Value import type_safe_step_default_value
|
7
|
+
from osbot_utils.type_safe.steps.Type_Safe__Step__Init import type_safe_step_init
|
8
|
+
from osbot_utils.type_safe.steps.Type_Safe__Step__Set_Attr import type_safe_step_set_attr
|
9
|
+
from osbot_utils.utils.Objects import serialize_to_dict
|
41
10
|
|
42
11
|
class Type_Safe:
|
43
12
|
|
44
13
|
def __init__(self, **kwargs):
|
45
|
-
from osbot_utils.utils.Objects import raise_exception_on_obj_type_annotation_mismatch
|
46
14
|
|
47
|
-
|
48
|
-
|
49
|
-
raise_exception_on_obj_type_annotation_mismatch(self, key, value)
|
50
|
-
if hasattr(self, key):
|
51
|
-
existing_value = getattr(self, key)
|
52
|
-
if existing_value is not None:
|
53
|
-
setattr(self, key, existing_value)
|
54
|
-
continue
|
55
|
-
setattr(self, key, value)
|
15
|
+
class_kwargs = self.__cls_kwargs__()
|
16
|
+
type_safe_step_init.init(self, class_kwargs, **kwargs)
|
56
17
|
|
57
|
-
for (key, value) in kwargs.items(): # overwrite with values provided in ctor
|
58
|
-
if hasattr(self, key):
|
59
|
-
if value is not None: # prevent None values from overwriting existing values, which is quite common in default constructors
|
60
|
-
setattr(self, key, value)
|
61
|
-
else:
|
62
|
-
raise ValueError(f"{self.__class__.__name__} has no attribute '{key}' and cannot be assigned the value '{value}'. "
|
63
|
-
f"Use {self.__class__.__name__}.__default_kwargs__() see what attributes are available")
|
64
18
|
|
65
19
|
def __enter__(self): return self
|
66
20
|
def __exit__(self, exc_type, exc_val, exc_tb): pass
|
67
21
|
|
68
|
-
# def __getattr__(self, name): # Called when an attribute is not found through normal attribute access
|
69
|
-
# if name.startswith(("set_", "get_")): # Check if the requested attribute is a getter or setter method
|
70
|
-
# prefix = name[:4] # Extract "set_" or "get_" from the method name
|
71
|
-
# attr_name = name[4:] # Get the actual attribute name by removing the prefix
|
72
|
-
#
|
73
|
-
# if hasattr(self, attr_name): # Verify that the target attribute actually exists on the object
|
74
|
-
# if prefix == "set_": # Handle setter method creation
|
75
|
-
# def setter(value): # Create a dynamic setter function that takes a value parameter
|
76
|
-
# setattr(self, attr_name, value) # Set the attribute value using type-safe setattr from Type_Safe
|
77
|
-
# return self # Return self for method chaining
|
78
|
-
# return setter # Return the setter function
|
79
|
-
# else: # get_ # Handle getter method creation
|
80
|
-
# def getter(): # Create a dynamic getter function with no parameters
|
81
|
-
# return getattr(self, attr_name) # Return the attribute value using Python's built-in getattr
|
82
|
-
# return getter # Return the getter function
|
83
|
-
#
|
84
|
-
# raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'") # Raise error if attribute is not a valid getter/setter
|
85
|
-
|
86
22
|
def __setattr__(self, name, value):
|
87
|
-
|
88
|
-
from osbot_utils.utils.Objects import convert_to_value_from_obj_annotation
|
89
|
-
from osbot_utils.utils.Objects import value_type_matches_obj_annotation_for_attr
|
90
|
-
from osbot_utils.utils.Objects import value_type_matches_obj_annotation_for_union_and_annotated
|
91
|
-
from osbot_utils.type_safe.validators.Type_Safe__Validator import Type_Safe__Validator
|
92
|
-
|
93
|
-
annotations = all_annotations(self)
|
94
|
-
if not annotations: # can't do type safety checks if the class does not have annotations
|
95
|
-
return super().__setattr__(name, value)
|
96
|
-
|
97
|
-
if value is not None:
|
98
|
-
if type(value) is dict:
|
99
|
-
value = convert_dict_to_value_from_obj_annotation(self, name, value)
|
100
|
-
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)
|
101
|
-
value = convert_to_value_from_obj_annotation (self, name, value)
|
102
|
-
else:
|
103
|
-
origin = get_origin(value)
|
104
|
-
if origin is not None:
|
105
|
-
value = origin
|
106
|
-
check_1 = value_type_matches_obj_annotation_for_attr (self, name, value)
|
107
|
-
check_2 = value_type_matches_obj_annotation_for_union_and_annotated(self, name, value)
|
108
|
-
if (check_1 is False and check_2 is None or
|
109
|
-
check_1 is None and check_2 is False or
|
110
|
-
check_1 is False and check_2 is False ): # fix for type safety assigment on Union vars
|
111
|
-
raise ValueError(f"Invalid type for attribute '{name}'. Expected '{annotations.get(name)}' but got '{type(value)}'")
|
112
|
-
else:
|
113
|
-
if hasattr(self, name) and annotations.get(name) : # don't allow previously set variables to be set to None
|
114
|
-
if getattr(self, name) is not None: # unless it is already set to None
|
115
|
-
raise ValueError(f"Can't set None, to a variable that is already set. Invalid type for attribute '{name}'. Expected '{self.__annotations__.get(name)}' but got '{type(value)}'")
|
116
|
-
|
117
|
-
# todo: refactor this to separate method
|
118
|
-
if hasattr(annotations, 'get'):
|
119
|
-
annotation = annotations.get(name)
|
120
|
-
if annotation:
|
121
|
-
annotation_origin = get_origin(annotation)
|
122
|
-
if annotation_origin is Annotated:
|
123
|
-
annotation_args = get_args(annotation)
|
124
|
-
target_type = annotation_args[0]
|
125
|
-
for attribute in annotation_args[1:]:
|
126
|
-
if isinstance(attribute, Type_Safe__Validator):
|
127
|
-
attribute.validate(value=value, field_name=name, target_type=target_type)
|
128
|
-
elif annotation_origin is dict:
|
129
|
-
value = self.deserialize_dict__using_key_value_annotations(name, value)
|
130
|
-
|
131
|
-
super().__setattr__(name, value)
|
23
|
+
type_safe_step_set_attr.setattr(super(), self, name, value)
|
132
24
|
|
133
25
|
def __attr_names__(self):
|
134
26
|
from osbot_utils.utils.Misc import list_set
|
@@ -136,179 +28,41 @@ class Type_Safe:
|
|
136
28
|
return list_set(self.__locals__())
|
137
29
|
|
138
30
|
@classmethod
|
139
|
-
def __cls_kwargs__(cls
|
140
|
-
|
141
|
-
import inspect
|
142
|
-
from enum import EnumMeta
|
143
|
-
from osbot_utils.utils.Objects import obj_is_type_union_compatible
|
144
|
-
|
145
|
-
IMMUTABLE_TYPES = (bool, int, float, complex, str, tuple, frozenset, bytes, NoneType, EnumMeta, type)
|
146
|
-
|
147
|
-
|
148
|
-
kwargs = {}
|
149
|
-
|
150
|
-
for base_cls in inspect.getmro(cls):
|
151
|
-
if base_cls is object: # Skip the base 'object' class
|
152
|
-
continue
|
153
|
-
for k, v in vars(base_cls).items():
|
154
|
-
# todo: refactor this logic since it is weird to start with a if not..., and then if ... continue (all these should be if ... continue )
|
155
|
-
if not k.startswith('__') and not isinstance(v, types.FunctionType): # remove instance functions
|
156
|
-
if isinstance(v, classmethod): # also remove class methods
|
157
|
-
continue
|
158
|
-
if type(v) is functools._lru_cache_wrapper: # todo, find better way to handle edge cases like this one (which happens when the @cache decorator is used in a instance method that uses Kwargs_To_Self)
|
159
|
-
continue
|
160
|
-
if isinstance(v, property): # skip property descriptors since they should not be handled here
|
161
|
-
continue
|
162
|
-
if (k in kwargs) is False: # do not set the value is it has already been set
|
163
|
-
kwargs[k] = v
|
164
|
-
|
165
|
-
if hasattr(base_cls,'__annotations__'): # can only do type safety checks if the class does not have annotations
|
166
|
-
for var_name, var_type in base_cls.__annotations__.items():
|
167
|
-
if hasattr(base_cls, var_name) is False: # only add if it has not already been defined
|
168
|
-
if var_name in kwargs:
|
169
|
-
continue
|
170
|
-
var_value = cls.__default__value__(var_type)
|
171
|
-
kwargs[var_name] = var_value
|
172
|
-
else:
|
173
|
-
var_value = getattr(base_cls, var_name)
|
174
|
-
if var_value is not None: # allow None assignments on ctor since that is a valid use case
|
175
|
-
if get_origin(var_type) is Annotated:
|
176
|
-
continue
|
177
|
-
if get_origin(var_type) is type: # Special handling for Type[T]
|
178
|
-
if not isinstance(var_value, type):
|
179
|
-
exception_message = f"variable '{var_name}' is defined as Type[T] but has value '{var_value}' which is not a type"
|
180
|
-
raise ValueError(exception_message)
|
181
|
-
type_arg = get_args(var_type)[0]
|
182
|
-
if not issubclass(var_value, type_arg):
|
183
|
-
exception_message = f"variable '{var_name}' is defined as {var_type} but value {var_value} is not a subclass of {type_arg}"
|
184
|
-
raise ValueError(exception_message)
|
185
|
-
elif var_type and not isinstance(var_value, var_type): # check type
|
186
|
-
exception_message = f"variable '{var_name}' is defined as type '{var_type}' but has value '{var_value}' of type '{type(var_value)}'"
|
187
|
-
raise ValueError(exception_message)
|
188
|
-
if var_type not in IMMUTABLE_TYPES and var_name.startswith('__') is False: # if var_type is not one of the IMMUTABLE_TYPES or is an __ internal
|
189
|
-
#todo: fix type safety bug that I believe is caused here
|
190
|
-
if obj_is_type_union_compatible(var_type, IMMUTABLE_TYPES) is False: # if var_type is not something like Optional[Union[int, str]]
|
191
|
-
if type(var_type) not in IMMUTABLE_TYPES:
|
192
|
-
exception_message = f"variable '{var_name}' is defined as type '{var_type}' which is not supported by Type_Safe, with only the following immutable types being supported: '{IMMUTABLE_TYPES}'"
|
193
|
-
raise ValueError(exception_message)
|
194
|
-
if include_base_classes is False:
|
195
|
-
break
|
196
|
-
return kwargs
|
31
|
+
def __cls_kwargs__(cls): # Return current class dictionary of class level variables and their values
|
32
|
+
return type_safe_step_class_kwargs.get_cls_kwargs(cls)
|
197
33
|
|
198
34
|
@classmethod
|
199
35
|
def __default__value__(cls, var_type):
|
200
|
-
|
201
|
-
from osbot_utils.type_safe.Type_Safe__List import Type_Safe__List
|
202
|
-
from osbot_utils.type_safe.Type_Safe__Dict import Type_Safe__Dict
|
203
|
-
if get_origin(var_type) is type: # Special handling for Type[T] # todo: reuse the get_origin value
|
204
|
-
type_args = get_args(var_type)
|
205
|
-
if type_args:
|
206
|
-
if isinstance(type_args[0], ForwardRef):
|
207
|
-
forward_name = type_args[0].__forward_arg__
|
208
|
-
for base_cls in inspect.getmro(cls):
|
209
|
-
if base_cls.__name__ == forward_name:
|
210
|
-
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 )
|
211
|
-
return type_args[0] # Return the actual type as the default value
|
212
|
-
|
213
|
-
if var_type is typing.Set: # todo: refactor the dict, set and list logic, since they are 90% the same
|
214
|
-
return set()
|
215
|
-
if get_origin(var_type) is set:
|
216
|
-
return set() # todo: add Type_Safe__Set
|
217
|
-
|
218
|
-
if var_type is typing.Dict:
|
219
|
-
return {}
|
220
|
-
|
221
|
-
if get_origin(var_type) is dict: # e.g. Dict[key_type, value_type]
|
222
|
-
key_type, value_type = get_args(var_type)
|
223
|
-
if isinstance(key_type, ForwardRef): # Handle forward references on key_type ---
|
224
|
-
forward_name = key_type.__forward_arg__
|
225
|
-
if forward_name == cls.__name__:
|
226
|
-
key_type = cls
|
227
|
-
if isinstance(value_type, ForwardRef): # Handle forward references on value_type ---
|
228
|
-
forward_name = value_type.__forward_arg__
|
229
|
-
if forward_name == cls.__name__:
|
230
|
-
value_type = cls
|
231
|
-
return Type_Safe__Dict(expected_key_type=key_type, expected_value_type=value_type)
|
232
|
-
|
233
|
-
if var_type is typing.List:
|
234
|
-
return [] # handle case when List was used with no type information provided
|
235
|
-
|
236
|
-
if get_origin(var_type) is list: # if we have list defined as list[type]
|
237
|
-
item_type = get_args(var_type)[0] # get the type that was defined
|
238
|
-
if isinstance(item_type, ForwardRef): # handle the case when the type is a forward reference
|
239
|
-
forward_name = item_type.__forward_arg__
|
240
|
-
if forward_name == cls.__name__: # if the forward reference is to the current class (simple name check)
|
241
|
-
item_type = cls # set the item_type to the current class
|
242
|
-
return Type_Safe__List(expected_type=item_type) # and used it as expected_type in Type_Safe__List
|
243
|
-
else:
|
244
|
-
return default_value(var_type) # for all other cases call default_value, which will try to create a default instance
|
245
|
-
|
246
|
-
def __default_kwargs__(self): # Return entire (including base classes) dictionary of class level variables and their values.
|
247
|
-
import inspect
|
248
|
-
kwargs = {}
|
249
|
-
cls = type(self)
|
250
|
-
for base_cls in inspect.getmro(cls): # Traverse the inheritance hierarchy and collect class-level attributes
|
251
|
-
if base_cls is object: # Skip the base 'object' class
|
252
|
-
continue
|
253
|
-
for k, v in vars(base_cls).items():
|
254
|
-
if not k.startswith('__') and not isinstance(v, types.FunctionType): # remove instance functions
|
255
|
-
if not isinstance(v, classmethod):
|
256
|
-
kwargs[k] = v
|
257
|
-
# add the vars defined with the annotations
|
258
|
-
if hasattr(base_cls,'__annotations__'): # can only do type safety checks if the class does not have annotations
|
259
|
-
for var_name, var_type in base_cls.__annotations__.items():
|
260
|
-
var_value = getattr(self, var_name)
|
261
|
-
kwargs[var_name] = var_value
|
36
|
+
return type_safe_step_default_value.default_value(cls, var_type)
|
262
37
|
|
263
|
-
|
38
|
+
def __default_kwargs__(self): # Return entire (including base classes) dictionary of class level variables and their values.
|
39
|
+
return type_safe_step_default_kwargs.default_kwargs(self)
|
264
40
|
|
265
|
-
def __kwargs__(self):
|
266
|
-
|
267
|
-
kwargs = {}
|
268
|
-
# Update with instance-specific values
|
269
|
-
for key, value in self.__default_kwargs__().items():
|
270
|
-
kwargs[key] = self.__getattribute__(key)
|
271
|
-
# if hasattr(self, key):
|
272
|
-
# kwargs[key] = self.__getattribute__(key)
|
273
|
-
# else:
|
274
|
-
# kwargs[key] = value # todo: see if this is stil a valid scenario
|
275
|
-
return kwargs
|
41
|
+
def __kwargs__(self): # Return a dictionary of the current instance's attribute values including inherited class defaults.
|
42
|
+
return type_safe_step_default_kwargs.kwargs(self)
|
276
43
|
|
277
44
|
|
278
|
-
def __locals__(self):
|
279
|
-
|
280
|
-
kwargs = self.__kwargs__()
|
281
|
-
|
282
|
-
if not isinstance(vars(self), types.FunctionType):
|
283
|
-
for k, v in vars(self).items():
|
284
|
-
if not isinstance(v, types.FunctionType) and not isinstance(v,classmethod):
|
285
|
-
if k.startswith('__') is False:
|
286
|
-
kwargs[k] = v
|
287
|
-
return kwargs
|
288
|
-
|
289
|
-
@classmethod
|
290
|
-
def __schema__(cls):
|
291
|
-
if hasattr(cls,'__annotations__'): # can only do type safety checks if the class does not have annotations
|
292
|
-
return cls.__annotations__
|
293
|
-
return {}
|
45
|
+
def __locals__(self): # Return a dictionary of the current instance's attribute values.
|
46
|
+
return type_safe_step_default_kwargs.locals(self)
|
294
47
|
|
295
48
|
# global methods added to any class that base classes this
|
296
49
|
# todo: see if there should be a prefix on these methods, to make it easier to spot them
|
297
50
|
# of if these are actually that useful that they should be added like this
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
51
|
+
# todo: these methods should not be here
|
52
|
+
# def bytes(self):
|
53
|
+
# from osbot_utils.utils.Json import json_to_bytes
|
54
|
+
#
|
55
|
+
# return json_to_bytes(self.json())
|
56
|
+
#
|
57
|
+
# def bytes_gz(self):
|
58
|
+
# from osbot_utils.utils.Json import json_to_gz
|
59
|
+
#
|
60
|
+
# return json_to_gz(self.json())
|
307
61
|
|
308
62
|
def json(self):
|
309
63
|
return self.serialize_to_dict()
|
310
64
|
|
311
|
-
|
65
|
+
# todo: see if we still need this. now that Type_Safe handles base types, there should be no need for this
|
312
66
|
def merge_with(self, target):
|
313
67
|
original_attrs = {k: v for k, v in self.__dict__.items() if k not in target.__dict__} # Store the original attributes of self that should be retained.
|
314
68
|
self.__dict__ = target.__dict__ # Set the target's __dict__ to self, now self and target share the same __dict__.
|
@@ -323,124 +77,17 @@ class Type_Safe:
|
|
323
77
|
for k,v in self.__cls_kwargs__().items():
|
324
78
|
setattr(self, k, v)
|
325
79
|
|
80
|
+
# todo: see if we still need this here in this class
|
326
81
|
def update_from_kwargs(self, **kwargs): # Update instance attributes with values from provided keyword arguments.
|
327
|
-
|
82
|
+
|
328
83
|
for key, value in kwargs.items():
|
329
84
|
if value is not None:
|
330
85
|
if hasattr(self,'__annotations__'): # can only do type safety checks if the class does not have annotations
|
331
|
-
if
|
86
|
+
if type_safe_validation.check_if__type_matches__obj_annotation__for_attr(self, key, value) is False:
|
332
87
|
raise ValueError(f"Invalid type for attribute '{key}'. Expected '{self.__annotations__.get(key)}' but got '{type(value)}'")
|
333
88
|
setattr(self, key, value)
|
334
89
|
return self
|
335
90
|
|
336
|
-
def deserialize_type__using_value(self, value):
|
337
|
-
if value:
|
338
|
-
try:
|
339
|
-
module_name, type_name = value.rsplit('.', 1)
|
340
|
-
if module_name == 'builtins' and type_name == 'NoneType': # Special case for NoneType (which serialises as builtins.* , but it actually in types.* )
|
341
|
-
value = types.NoneType
|
342
|
-
else:
|
343
|
-
module = __import__(module_name, fromlist=[type_name])
|
344
|
-
value = getattr(module, type_name)
|
345
|
-
except (ValueError, ImportError, AttributeError) as e:
|
346
|
-
raise ValueError(f"Could not reconstruct type from '{value}': {str(e)}")
|
347
|
-
return value
|
348
|
-
|
349
|
-
def deserialize_dict__using_key_value_annotations(self, key, value):
|
350
|
-
from osbot_utils.type_safe.Type_Safe__Dict import Type_Safe__Dict
|
351
|
-
annotations = all_annotations(self)
|
352
|
-
dict_annotations_tuple = get_args(annotations.get(key))
|
353
|
-
if not dict_annotations_tuple: # happens when the value is a dict/Dict with no annotations
|
354
|
-
return value
|
355
|
-
if not type(value) is dict:
|
356
|
-
return value
|
357
|
-
key_class = dict_annotations_tuple[0]
|
358
|
-
value_class = dict_annotations_tuple[1]
|
359
|
-
new_value = Type_Safe__Dict(expected_key_type=key_class, expected_value_type=value_class)
|
360
|
-
|
361
|
-
for dict_key, dict_value in value.items():
|
362
|
-
if issubclass(key_class, Type_Safe):
|
363
|
-
new__dict_key = key_class().deserialize_from_dict(dict_key)
|
364
|
-
else:
|
365
|
-
new__dict_key = key_class(dict_key)
|
366
|
-
|
367
|
-
if type(dict_value) == value_class: # if the value is already the target, then just use it
|
368
|
-
new__dict_value = dict_value
|
369
|
-
elif issubclass(value_class, Type_Safe):
|
370
|
-
new__dict_value = value_class().deserialize_from_dict(dict_value)
|
371
|
-
elif value_class is Any:
|
372
|
-
new__dict_value = dict_value
|
373
|
-
else:
|
374
|
-
new__dict_value = value_class(dict_value)
|
375
|
-
new_value[new__dict_key] = new__dict_value
|
376
|
-
|
377
|
-
return new_value
|
378
|
-
|
379
|
-
# todo: this needs refactoring, since the logic and code is getting quite complex (to be inside methods like this)
|
380
|
-
def deserialize_from_dict(self, data, raise_on_not_found=False):
|
381
|
-
from decimal import Decimal
|
382
|
-
from enum import EnumMeta
|
383
|
-
from osbot_utils.type_safe.Type_Safe__List import Type_Safe__List
|
384
|
-
from osbot_utils.helpers.Random_Guid import Random_Guid
|
385
|
-
from osbot_utils.helpers.Random_Guid_Short import Random_Guid_Short
|
386
|
-
from osbot_utils.utils.Objects import obj_is_attribute_annotation_of_type
|
387
|
-
from osbot_utils.utils.Objects import obj_attribute_annotation
|
388
|
-
from osbot_utils.utils.Objects import enum_from_value
|
389
|
-
from osbot_utils.helpers.Safe_Id import Safe_Id
|
390
|
-
from osbot_utils.helpers.Timestamp_Now import Timestamp_Now
|
391
|
-
|
392
|
-
if hasattr(data, 'items') is False:
|
393
|
-
raise ValueError(f"Expected a dictionary, but got '{type(data)}'")
|
394
|
-
|
395
|
-
for key, value in data.items():
|
396
|
-
if hasattr(self, key) and isinstance(getattr(self, key), Type_Safe):
|
397
|
-
getattr(self, key).deserialize_from_dict(value) # if the attribute is a Type_Safe object, then also deserialize it
|
398
|
-
else:
|
399
|
-
if hasattr(self, '__annotations__'): # can only do type safety checks if the class does not have annotations
|
400
|
-
if hasattr(self, key) is False: # make sure we are now adding new attributes to the class
|
401
|
-
if raise_on_not_found:
|
402
|
-
raise ValueError(f"Attribute '{key}' not found in '{self.__class__.__name__}'")
|
403
|
-
else:
|
404
|
-
continue
|
405
|
-
if obj_attribute_annotation(self, key) == type: # Handle type objects
|
406
|
-
value = self.deserialize_type__using_value(value)
|
407
|
-
elif obj_is_attribute_annotation_of_type(self, key, dict): # handle the case when the value is a dict
|
408
|
-
value = self.deserialize_dict__using_key_value_annotations(key, value)
|
409
|
-
elif obj_is_attribute_annotation_of_type(self, key, list): # handle the case when the value is a list
|
410
|
-
attribute_annotation = obj_attribute_annotation(self, key) # get the annotation for this variable
|
411
|
-
attribute_annotation_args = get_args(attribute_annotation)
|
412
|
-
if attribute_annotation_args:
|
413
|
-
expected_type = get_args(attribute_annotation)[0] # get the first arg (which is the type)
|
414
|
-
type_safe_list = Type_Safe__List(expected_type) # create a new instance of Type_Safe__List
|
415
|
-
for item in value: # next we need to convert all items (to make sure they all match the type)
|
416
|
-
if type(item) is dict:
|
417
|
-
new_item = expected_type(**item) # create new object
|
418
|
-
else:
|
419
|
-
new_item = expected_type(item)
|
420
|
-
type_safe_list.append(new_item) # and add it to the new type_safe_list obejct
|
421
|
-
value = type_safe_list # todo: refactor out this create list code, maybe to an deserialize_from_list method
|
422
|
-
else:
|
423
|
-
if value is not None:
|
424
|
-
if obj_is_attribute_annotation_of_type(self, key, EnumMeta): # Handle the case when the value is an Enum
|
425
|
-
enum_type = getattr(self, '__annotations__').get(key)
|
426
|
-
if type(value) is not enum_type: # If the value is not already of the target type
|
427
|
-
value = enum_from_value(enum_type, value) # Try to resolve the value into the enum
|
428
|
-
|
429
|
-
# todo: refactor these special cases into a separate method to class
|
430
|
-
elif obj_is_attribute_annotation_of_type(self, key, Decimal): # handle Decimals
|
431
|
-
value = Decimal(value)
|
432
|
-
elif obj_is_attribute_annotation_of_type(self, key, Safe_Id): # handle Safe_Id
|
433
|
-
value = Safe_Id(value)
|
434
|
-
elif obj_is_attribute_annotation_of_type(self, key, Random_Guid): # handle Random_Guid
|
435
|
-
value = Random_Guid(value)
|
436
|
-
elif obj_is_attribute_annotation_of_type(self, key, Random_Guid_Short): # handle Random_Guid_Short
|
437
|
-
value = Random_Guid_Short(value)
|
438
|
-
elif obj_is_attribute_annotation_of_type(self, key, Timestamp_Now): # handle Timestamp_Now
|
439
|
-
value = Timestamp_Now(value)
|
440
|
-
setattr(self, key, value) # Direct assignment for primitive types and other structures
|
441
|
-
|
442
|
-
return self
|
443
|
-
|
444
91
|
def obj(self):
|
445
92
|
from osbot_utils.utils.Objects import dict_to_obj
|
446
93
|
|
@@ -456,37 +103,7 @@ class Type_Safe:
|
|
456
103
|
|
457
104
|
@classmethod
|
458
105
|
def from_json(cls, json_data, raise_on_not_found=False):
|
459
|
-
from osbot_utils.
|
460
|
-
|
461
|
-
if type(json_data) is str:
|
462
|
-
json_data = json_parse(json_data)
|
463
|
-
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)
|
464
|
-
return cls().deserialize_from_dict(json_data,raise_on_not_found=raise_on_not_found)
|
465
|
-
return cls()
|
466
|
-
|
467
|
-
# todo: see if it is possible to add recursive protection to this logic
|
468
|
-
def serialize_to_dict(obj):
|
469
|
-
from decimal import Decimal
|
470
|
-
from enum import Enum
|
471
|
-
from typing import List
|
106
|
+
from osbot_utils.type_safe.steps.Type_Safe__Step__From_Json import type_safe_step_from_json # circular dependency on Type_Safe
|
107
|
+
return type_safe_step_from_json.from_json(cls, json_data, raise_on_not_found)
|
472
108
|
|
473
|
-
if isinstance(obj, (str, int, float, bool, bytes, Decimal)) or obj is None:
|
474
|
-
return obj
|
475
|
-
elif isinstance(obj, Enum):
|
476
|
-
return obj.name
|
477
|
-
elif isinstance(obj, type):
|
478
|
-
return f"{obj.__module__}.{obj.__name__}" # save the full type name
|
479
|
-
elif isinstance(obj, list) or isinstance(obj, List):
|
480
|
-
return [serialize_to_dict(item) for item in obj]
|
481
|
-
elif isinstance(obj, dict):
|
482
|
-
return {key: serialize_to_dict(value) for key, value in obj.items()}
|
483
|
-
elif hasattr(obj, "__dict__"):
|
484
|
-
data = {} # todo: look at a more advanced version which saved the type of the object, for example with {'__type__': type(obj).__name__}
|
485
|
-
for key, value in obj.__dict__.items():
|
486
|
-
if key.startswith('__') is False: # don't process internal variables (for example the ones set by @cache_on_self)
|
487
|
-
data[key] = serialize_to_dict(value) # Recursive call for complex types
|
488
|
-
return data
|
489
|
-
else:
|
490
|
-
raise TypeError(f"Type {type(obj)} not serializable")
|
491
|
-
#return f"UNSERIALIZABLE({type(obj).__name__})" # todo: see if there are valid use cases for this
|
492
109
|
|
@@ -1,4 +1,6 @@
|
|
1
|
-
from typing import
|
1
|
+
from typing import get_args, Union, Optional, Any, ForwardRef
|
2
|
+
|
3
|
+
from osbot_utils.type_safe.shared.Type_Safe__Cache import type_safe_cache
|
2
4
|
|
3
5
|
EXACT_TYPE_MATCH = (int, float, str, bytes, bool, complex)
|
4
6
|
|
@@ -8,7 +10,7 @@ class Type_Safe__Base:
|
|
8
10
|
return True
|
9
11
|
if isinstance(expected_type, ForwardRef): # todo: add support for ForwardRef
|
10
12
|
return True
|
11
|
-
origin = get_origin(expected_type)
|
13
|
+
origin = type_safe_cache.get_origin(expected_type)
|
12
14
|
args = get_args(expected_type)
|
13
15
|
if origin is None:
|
14
16
|
if expected_type in EXACT_TYPE_MATCH:
|
@@ -85,12 +87,12 @@ class Type_Safe__Base:
|
|
85
87
|
actual_type_name = type_str(type(item))
|
86
88
|
raise TypeError(f"Expected '{expected_type_name}', but got '{actual_type_name}'")
|
87
89
|
|
88
|
-
def json(self):
|
89
|
-
|
90
|
+
# def json(self):
|
91
|
+
# pass
|
90
92
|
|
91
93
|
# todo: see if we should/can move this to the Objects.py file
|
92
94
|
def type_str(tp):
|
93
|
-
origin = get_origin(tp)
|
95
|
+
origin = type_safe_cache.get_origin(tp)
|
94
96
|
if origin is None:
|
95
97
|
if hasattr(tp, '__name__'):
|
96
98
|
return tp.__name__
|
@@ -99,22 +101,4 @@ def type_str(tp):
|
|
99
101
|
else:
|
100
102
|
args = get_args(tp)
|
101
103
|
args_str = ', '.join(type_str(arg) for arg in args)
|
102
|
-
return f"{origin.__name__}[{args_str}]"
|
103
|
-
|
104
|
-
def get_object_type_str(obj):
|
105
|
-
if isinstance(obj, dict):
|
106
|
-
if not obj:
|
107
|
-
return "Dict[Empty]"
|
108
|
-
key_types = set(type(k).__name__ for k in obj.keys())
|
109
|
-
value_types = set(type(v).__name__ for v in obj.values())
|
110
|
-
key_type_str = ', '.join(sorted(key_types))
|
111
|
-
value_type_str = ', '.join(sorted(value_types))
|
112
|
-
return f"Dict[{key_type_str}, {value_type_str}]"
|
113
|
-
elif isinstance(obj, list):
|
114
|
-
if not obj:
|
115
|
-
return "List[Empty]"
|
116
|
-
elem_types = set(type(e).__name__ for e in obj)
|
117
|
-
elem_type_str = ', '.join(sorted(elem_types))
|
118
|
-
return f"List[{elem_type_str}]"
|
119
|
-
else:
|
120
|
-
return type(obj).__name__
|
104
|
+
return f"{origin.__name__}[{args_str}]"
|
@@ -1,4 +1,4 @@
|
|
1
|
-
from osbot_utils.type_safe.Type_Safe__Base import
|
1
|
+
from osbot_utils.type_safe.Type_Safe__Base import Type_Safe__Base
|
2
2
|
|
3
3
|
class Type_Safe__Dict(Type_Safe__Base, dict):
|
4
4
|
def __init__(self, expected_key_type, expected_value_type, *args, **kwargs):
|
@@ -7,19 +7,20 @@ class Type_Safe__Dict(Type_Safe__Base, dict):
|
|
7
7
|
self.expected_key_type = expected_key_type
|
8
8
|
self.expected_value_type = expected_value_type
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
# todo: see if we need to do this, since there was not code coverage hitting it
|
11
|
+
# for k, v in self.items(): # check type-safety of ctor arguments
|
12
|
+
# self.is_instance_of_type(k, self.expected_key_type )
|
13
|
+
# self.is_instance_of_type(v, self.expected_value_type)
|
13
14
|
|
14
15
|
def __setitem__(self, key, value): # Check type-safety before allowing assignment.
|
15
16
|
self.is_instance_of_type(key, self.expected_key_type)
|
16
17
|
self.is_instance_of_type(value, self.expected_value_type)
|
17
18
|
super().__setitem__(key, value)
|
18
19
|
|
19
|
-
def __repr__(self):
|
20
|
-
|
21
|
-
|
22
|
-
|
20
|
+
# def __repr__(self):
|
21
|
+
# key_type_name = type_str(self.expected_key_type)
|
22
|
+
# value_type_name = type_str(self.expected_value_type)
|
23
|
+
# return f"dict[{key_type_name}, {value_type_name}] with {len(self)} entries"
|
23
24
|
|
24
25
|
def json(self): # Convert the dictionary to a JSON-serializable format.
|
25
26
|
from osbot_utils.type_safe.Type_Safe import Type_Safe # can only import this here to avoid circular imports
|
@@ -0,0 +1,29 @@
|
|
1
|
+
from osbot_utils.type_safe.shared.Type_Safe__Cache import type_safe_cache
|
2
|
+
|
3
|
+
|
4
|
+
class Type_Safe__Annotations:
|
5
|
+
|
6
|
+
def all_annotations(self, target):
|
7
|
+
return type_safe_cache.get_obj_annotations(target) # use cache
|
8
|
+
|
9
|
+
def all_annotations__in_class(self, cls):
|
10
|
+
return type_safe_cache.get_class_annotations(cls)
|
11
|
+
|
12
|
+
def obj_attribute_annotation(self, target, attr_name):
|
13
|
+
return self.all_annotations(target).get(attr_name) # use cache
|
14
|
+
|
15
|
+
def obj_is_attribute_annotation_of_type(self, target, attr_name, expected_type):
|
16
|
+
attribute_annotation = self.obj_attribute_annotation(target, attr_name)
|
17
|
+
if expected_type is attribute_annotation:
|
18
|
+
return True
|
19
|
+
if expected_type is type(attribute_annotation):
|
20
|
+
return True
|
21
|
+
if expected_type is type_safe_cache.get_origin(attribute_annotation): # handle genericAlias
|
22
|
+
return True
|
23
|
+
return False
|
24
|
+
|
25
|
+
def get_origin(self, var_type):
|
26
|
+
return type_safe_cache.get_origin(var_type)
|
27
|
+
|
28
|
+
type_safe_annotations = Type_Safe__Annotations()
|
29
|
+
|