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.
Files changed (37) hide show
  1. osbot_utils/context_managers/capture_duration.py +19 -12
  2. osbot_utils/helpers/CPrint.py +0 -1
  3. osbot_utils/helpers/trace/Trace_Call.py +1 -2
  4. osbot_utils/helpers/trace/Trace_Call__Handler.py +14 -14
  5. osbot_utils/helpers/xml/Xml__File.py +1 -1
  6. osbot_utils/helpers/xml/Xml__File__To_Dict.py +1 -1
  7. osbot_utils/helpers/xml/Xml__File__To_Xml.py +1 -1
  8. osbot_utils/testing/performance/Performance_Measure__Session.py +108 -0
  9. osbot_utils/testing/performance/__init__.py +0 -0
  10. osbot_utils/testing/performance/models/Model__Performance_Measure__Measurement.py +14 -0
  11. osbot_utils/testing/performance/models/Model__Performance_Measure__Result.py +10 -0
  12. osbot_utils/testing/performance/models/__init__.py +0 -0
  13. osbot_utils/type_safe/Type_Safe.py +35 -418
  14. osbot_utils/type_safe/Type_Safe__Base.py +8 -24
  15. osbot_utils/type_safe/Type_Safe__Dict.py +9 -8
  16. osbot_utils/type_safe/shared/Type_Safe__Annotations.py +29 -0
  17. osbot_utils/type_safe/shared/Type_Safe__Cache.py +143 -0
  18. osbot_utils/type_safe/shared/Type_Safe__Convert.py +46 -0
  19. osbot_utils/type_safe/shared/Type_Safe__Not_Cached.py +24 -0
  20. osbot_utils/type_safe/shared/Type_Safe__Raise_Exception.py +14 -0
  21. osbot_utils/type_safe/shared/Type_Safe__Shared__Variables.py +4 -0
  22. osbot_utils/type_safe/shared/Type_Safe__Validation.py +246 -0
  23. osbot_utils/type_safe/shared/__init__.py +0 -0
  24. osbot_utils/type_safe/steps/Type_Safe__Step__Class_Kwargs.py +110 -0
  25. osbot_utils/type_safe/steps/Type_Safe__Step__Default_Kwargs.py +42 -0
  26. osbot_utils/type_safe/steps/Type_Safe__Step__Default_Value.py +74 -0
  27. osbot_utils/type_safe/steps/Type_Safe__Step__From_Json.py +138 -0
  28. osbot_utils/type_safe/steps/Type_Safe__Step__Init.py +24 -0
  29. osbot_utils/type_safe/steps/Type_Safe__Step__Set_Attr.py +92 -0
  30. osbot_utils/type_safe/steps/__init__.py +0 -0
  31. osbot_utils/utils/Objects.py +27 -232
  32. osbot_utils/utils/Status.py +0 -2
  33. osbot_utils/version +1 -1
  34. {osbot_utils-2.11.0.dist-info → osbot_utils-2.12.0.dist-info}/METADATA +2 -2
  35. {osbot_utils-2.11.0.dist-info → osbot_utils-2.12.0.dist-info}/RECORD +37 -17
  36. {osbot_utils-2.11.0.dist-info → osbot_utils-2.12.0.dist-info}/LICENSE +0 -0
  37. {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 inspect
4
- import sys
5
- import types
6
- from osbot_utils.utils.Objects import default_value # todo: remove test mocking requirement for this to be here (instead of on the respective method)
7
- from osbot_utils.utils.Objects import all_annotations
8
-
9
- # Backport implementations of get_origin and get_args for Python 3.7
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
- for (key, value) in self.__cls_kwargs__().items(): # assign all default values to self
48
- if value is not None: # when the value is explicitly set to None on the class static vars, we can't check for type safety
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
- from osbot_utils.utils.Objects import convert_dict_to_value_from_obj_annotation
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, include_base_classes=True): # Return current class dictionary of class level variables and their values
140
- import functools
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
- import typing
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
- return kwargs
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
- """Return a dictionary of the current instance's attribute values including inherited class defaults."""
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
- """Return a dictionary of the current instance's attribute values."""
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
- def bytes(self):
299
- from osbot_utils.utils.Json import json_to_bytes
300
-
301
- return json_to_bytes(self.json())
302
-
303
- def bytes_gz(self):
304
- from osbot_utils.utils.Json import json_to_gz
305
-
306
- return json_to_gz(self.json())
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
- from osbot_utils.utils.Objects import value_type_matches_obj_annotation_for_attr
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 value_type_matches_obj_annotation_for_attr(self, key, value) is False:
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.utils.Json import json_parse
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 get_origin, get_args, Union, Optional, Any, ForwardRef
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
- raise NotImplemented
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 type_str, Type_Safe__Base
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
- for k, v in self.items(): # check type-safety of ctor arguments
11
- self.is_instance_of_type(k, self.expected_key_type )
12
- self.is_instance_of_type(v, self.expected_value_type)
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
- key_type_name = type_str(self.expected_key_type)
21
- value_type_name = type_str(self.expected_value_type)
22
- return f"dict[{key_type_name}, {value_type_name}] with {len(self)} entries"
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
+