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
@@ -0,0 +1,143 @@
1
+ import inspect
2
+ from weakref import WeakKeyDictionary
3
+ from osbot_utils.type_safe.shared.Type_Safe__Not_Cached import type_safe_not_cached
4
+ from osbot_utils.type_safe.shared.Type_Safe__Shared__Variables import IMMUTABLE_TYPES
5
+
6
+
7
+ class Type_Safe__Cache:
8
+
9
+ _cls__annotations_cache : WeakKeyDictionary
10
+ _cls__immutable_vars : WeakKeyDictionary
11
+ _cls__kwargs_cache : WeakKeyDictionary
12
+ _obj__annotations_cache : WeakKeyDictionary
13
+ _type__get_origin_cache : WeakKeyDictionary
14
+ _mro_cache : WeakKeyDictionary
15
+ _valid_vars_cache : WeakKeyDictionary
16
+
17
+ cache__miss__cls__annotations : int = 0
18
+ cache__miss__cls__kwargs : int = 0
19
+ cache__miss__cls__immutable_vars: int = 0
20
+ cache__miss__obj__annotations : int = 0
21
+ cache__miss__type__get_origin : int = 0
22
+ cache__miss__mro : int = 0
23
+ cache__miss__valid_vars : int = 0
24
+
25
+ cache__hit__cls__annotations : int = 0
26
+ cache__hit__cls__kwargs : int = 0
27
+ cache__hit__cls__immutable_vars : int = 0
28
+ cache__hit__obj__annotations : int = 0
29
+ cache__hit__type__get_origin : int = 0
30
+ cache__hit__mro : int = 0
31
+ cache__hit__valid_vars : int = 0
32
+ skip_cache : bool = False
33
+
34
+
35
+ # Caching system for Type_Safe methods
36
+ def __init__(self):
37
+ self._cls__annotations_cache = WeakKeyDictionary() # Cache for class annotations
38
+ self._cls__immutable_vars = WeakKeyDictionary() # Cache for class immutable vars
39
+ self._cls__kwargs_cache = WeakKeyDictionary() # Cache for class kwargs
40
+ self._obj__annotations_cache = WeakKeyDictionary() # Cache for object annotations
41
+ self._type__get_origin_cache = WeakKeyDictionary() # Cache for tp (type) get_origin results
42
+ self._mro_cache = WeakKeyDictionary() # Cache for Method Resolution Order
43
+ self._valid_vars_cache = WeakKeyDictionary()
44
+
45
+ def get_cls_kwargs(self, cls):
46
+ cls_kwargs = self._cls__kwargs_cache.get(cls)
47
+
48
+ if cls_kwargs is None:
49
+ self.cache__miss__cls__kwargs += 1
50
+ else:
51
+ self.cache__hit__cls__kwargs += 1
52
+ return cls_kwargs
53
+
54
+ def get_obj_annotations(self, target):
55
+ if target is None:
56
+ return {}
57
+ annotations_key = target.__class__
58
+ annotations = self._obj__annotations_cache.get(annotations_key) # this is a more efficient cache retrieval pattern (we only get the data from the dict once)
59
+ if self.skip_cache or annotations is None:
60
+ annotations = dict(type_safe_not_cached.all_annotations(target).items())
61
+ self._obj__annotations_cache[annotations_key] = annotations
62
+ self.cache__miss__obj__annotations += 1
63
+ else:
64
+ self.cache__hit__obj__annotations += 1
65
+ return annotations
66
+
67
+ def get_class_annotations(self, cls):
68
+ annotations = self._cls__annotations_cache.get(cls) # this is a more efficient cache retrieval pattern (we only get the data from the dict once)
69
+ if self.skip_cache or annotations is None: # todo: apply this to the other cache getters
70
+ annotations = type_safe_not_cached.all_annotations__in_class(cls).items()
71
+ self._cls__annotations_cache[cls] = annotations
72
+ self.cache__miss__cls__annotations +=1
73
+ else:
74
+ self.cache__hit__cls__annotations += 1
75
+ return annotations
76
+
77
+ def get_class_immutable_vars(self, cls):
78
+ immutable_vars = self._cls__immutable_vars.get(cls)
79
+ if self.skip_cache or immutable_vars is None:
80
+ annotations = self.get_class_annotations(cls)
81
+ immutable_vars = {key: value for key, value in annotations if value in IMMUTABLE_TYPES}
82
+ self._cls__immutable_vars[cls] = immutable_vars
83
+ self.cache__miss__cls__immutable_vars += 1
84
+ else:
85
+ self.cache__hit__cls__immutable_vars += 1
86
+ return immutable_vars
87
+
88
+ def get_class_mro(self, cls):
89
+ if self.skip_cache or cls not in self._mro_cache:
90
+ self._mro_cache[cls] = inspect.getmro(cls)
91
+ self.cache__miss__mro += 1
92
+ else:
93
+ self.cache__hit__mro += 1
94
+ return self._mro_cache[cls]
95
+
96
+
97
+ def get_origin(self, var_type): # Cache expensive get_origin calls
98
+ if self.skip_cache or var_type not in self._type__get_origin_cache:
99
+ origin = type_safe_not_cached.get_origin(var_type)
100
+ try: # this is needed for the edge case when we can't create a key from the var_type in WeakKeyDictionary (see test test__regression__type_safe_is_not_enforced_on_dict_and_Dict for an example)
101
+ self._type__get_origin_cache[var_type] = origin
102
+ except TypeError:
103
+ pass
104
+ self.cache__miss__type__get_origin += 1
105
+ else:
106
+ origin = self._type__get_origin_cache[var_type]
107
+ self.cache__hit__type__get_origin += 1
108
+ return origin
109
+
110
+ # todo: see if we have cache misses and invalid hits based on the validator (we might need more validator specific methods)
111
+ def get_valid_class_variables(self, cls, validator):
112
+ if self.skip_cache or cls not in self._valid_vars_cache:
113
+ valid_variables = {}
114
+ for name, value in vars(cls).items():
115
+ if not validator(name, value):
116
+ valid_variables[name] = value
117
+ self._valid_vars_cache[cls] = valid_variables
118
+ self.cache__miss__valid_vars += 1
119
+ else:
120
+ self.cache__hit__valid_vars += 1
121
+ return self._valid_vars_cache[cls]
122
+
123
+ def set_cache__cls_kwargs(self, cls, kwargs):
124
+ if self.skip_cache is False:
125
+ self._cls__kwargs_cache[cls] = kwargs
126
+ return kwargs
127
+
128
+ def print_cache_hits(self):
129
+ print()
130
+ print("###### Type_Safe_Cache Hits ########")
131
+ print()
132
+ print( " cache name | hits | miss | size |")
133
+ print( "----------------------|--------|-------|-------|")
134
+ print(f" annotations | {self.cache__hit__cls__annotations :5} | {self.cache__miss__cls__annotations :5} | {len(self._obj__annotations_cache) :5} |")
135
+ print(f" cls__kwargs | {self.cache__hit__cls__kwargs :5} | {self.cache__miss__cls__kwargs :5} | {len(self._cls__kwargs_cache ) :5} |")
136
+ print(f" cls__immutable_vars | {self.cache__hit__cls__immutable_vars:5} | {self.cache__miss__cls__immutable_vars :5} | {len(self._cls__immutable_vars ) :5} |")
137
+ print(f" obj__annotations | {self.cache__hit__obj__annotations :5} | {self.cache__miss__obj__annotations :5} | {len(self._obj__annotations_cache) :5} |")
138
+ print(f" type__get_origin | {self.cache__hit__type__get_origin :5} | {self.cache__miss__type__get_origin :5} | {len(self._type__get_origin_cache) :5} |")
139
+ print(f" mro | {self.cache__hit__mro :5} | { self.cache__miss__mro :5} | {len(self._mro_cache ) :5} |")
140
+ print(f" valid_vars | {self.cache__hit__valid_vars :5} | {self.cache__miss__valid_vars :5} | {len(self._valid_vars_cache ) :5} |")
141
+
142
+ type_safe_cache = Type_Safe__Cache()
143
+
@@ -0,0 +1,46 @@
1
+ from osbot_utils.type_safe.shared.Type_Safe__Cache import type_safe_cache
2
+ from osbot_utils.utils.Objects import base_classes_names
3
+
4
+
5
+ class Type_Safe__Convert:
6
+ def convert_dict_to_value_from_obj_annotation(self, target, attr_name, value): # todo: refactor this with code from convert_str_to_value_from_obj_annotation since it is mostly the same
7
+ if target is not None and attr_name is not None:
8
+ if hasattr(target, '__annotations__'):
9
+ obj_annotations = target.__annotations__
10
+ if hasattr(obj_annotations,'get'):
11
+ attribute_annotation = obj_annotations.get(attr_name)
12
+ if 'Type_Safe' in base_classes_names(attribute_annotation):
13
+ return attribute_annotation(**value)
14
+ return value
15
+
16
+ def convert_to_value_from_obj_annotation(self, target, attr_name, value): # todo: see the side effects of doing this for all ints and floats
17
+
18
+ from osbot_utils.helpers.Guid import Guid
19
+ from osbot_utils.helpers.Timestamp_Now import Timestamp_Now
20
+ from osbot_utils.helpers.Random_Guid import Random_Guid
21
+ from osbot_utils.helpers.Safe_Id import Safe_Id
22
+ from osbot_utils.helpers.Str_ASCII import Str_ASCII
23
+
24
+ TYPE_SAFE__CONVERT_VALUE__SUPPORTED_TYPES = [Guid, Random_Guid, Safe_Id, Str_ASCII, Timestamp_Now]
25
+
26
+ if target is not None and attr_name is not None:
27
+ if hasattr(target, '__annotations__'):
28
+ obj_annotations = target.__annotations__
29
+ if hasattr(obj_annotations,'get'):
30
+ attribute_annotation = obj_annotations.get(attr_name)
31
+ if attribute_annotation:
32
+ origin = type_safe_cache.get_origin(attribute_annotation) # Add handling for Type[T] annotations
33
+ if origin is type and isinstance(value, str):
34
+ try: # Convert string path to actual type
35
+ if len(value.rsplit('.', 1)) > 1:
36
+ module_name, class_name = value.rsplit('.', 1)
37
+ module = __import__(module_name, fromlist=[class_name])
38
+ return getattr(module, class_name)
39
+ except (ValueError, ImportError, AttributeError) as e:
40
+ raise ValueError(f"Could not convert '{value}' to type: {str(e)}")
41
+
42
+ 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
43
+ return attribute_annotation(value)
44
+ return value
45
+
46
+ type_safe_convert = Type_Safe__Convert()
@@ -0,0 +1,24 @@
1
+ from typing import get_origin
2
+
3
+ class Type_Safe__Not_Cached:
4
+
5
+ def all_annotations(self, target):
6
+ annotations = {}
7
+ if hasattr(target.__class__, '__mro__'):
8
+ for base in reversed(target.__class__.__mro__):
9
+ if hasattr(base, '__annotations__'):
10
+ annotations.update(base.__annotations__)
11
+ return annotations
12
+
13
+ def all_annotations__in_class(self, target):
14
+ annotations = {}
15
+ if hasattr(target, '__mro__'):
16
+ for base in reversed(target.__mro__):
17
+ if hasattr(base, '__annotations__'):
18
+ annotations.update(base.__annotations__)
19
+ return annotations
20
+
21
+ def get_origin(self, var_type):
22
+ return get_origin(var_type)
23
+
24
+ type_safe_not_cached = Type_Safe__Not_Cached()
@@ -0,0 +1,14 @@
1
+ from osbot_utils.type_safe.shared.Type_Safe__Shared__Variables import IMMUTABLE_TYPES
2
+
3
+
4
+ class Type_Safe__Raise_Exception:
5
+
6
+ def type_mismatch_error(self, var_name: str, expected_type: type, actual_type: type) -> None: # Raises formatted error for type validation failures
7
+ exception_message = f"Invalid type for attribute '{var_name}'. Expected '{expected_type}' but got '{actual_type}'"
8
+ raise ValueError(exception_message)
9
+
10
+ def immutable_type_error(self, var_name, var_type):
11
+ 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}'"
12
+ raise ValueError(exception_message)
13
+
14
+ type_safe_raise_exception = Type_Safe__Raise_Exception()
@@ -0,0 +1,4 @@
1
+ import types
2
+ from enum import EnumMeta
3
+
4
+ IMMUTABLE_TYPES = (bool, int, float, complex, str, bytes, types.NoneType, EnumMeta, type)
@@ -0,0 +1,246 @@
1
+ import types
2
+ import typing
3
+ from enum import EnumMeta
4
+ from typing import Any, Annotated, Optional, get_args, get_origin, ForwardRef, Type, Dict, _GenericAlias
5
+ from osbot_utils.type_safe.shared.Type_Safe__Annotations import type_safe_annotations
6
+ from osbot_utils.type_safe.shared.Type_Safe__Cache import type_safe_cache
7
+ from osbot_utils.type_safe.shared.Type_Safe__Shared__Variables import IMMUTABLE_TYPES
8
+ from osbot_utils.type_safe.shared.Type_Safe__Raise_Exception import type_safe_raise_exception
9
+
10
+
11
+ class Type_Safe__Validation:
12
+
13
+ def are_types_compatible_for_assigment(self, source_type, target_type):
14
+ import types
15
+ import typing
16
+
17
+ if isinstance(target_type, str): # If the "target_type" is a forward reference (string), handle it here.
18
+ if target_type == source_type.__name__: # Simple check: does the string match the actual class name
19
+ return True
20
+ if source_type is target_type:
21
+ return True
22
+ if source_type is int and target_type is float:
23
+ return True
24
+ if target_type in source_type.__mro__: # this means that the source_type has the target_type has of its base types
25
+ return True
26
+ if target_type is callable: # handle case where callable was used as the target type
27
+ if source_type is types.MethodType: # and a method or function was used as the source type
28
+ return True
29
+ if source_type is types.FunctionType:
30
+ return True
31
+ if source_type is staticmethod:
32
+ return True
33
+ if target_type is typing.Any:
34
+ return True
35
+ return False
36
+
37
+ def are_types_magic_mock(self, source_type, target_type):
38
+ from unittest.mock import MagicMock
39
+ if isinstance(source_type, MagicMock):
40
+ return True
41
+ if isinstance(target_type, MagicMock):
42
+ return True
43
+ if source_type is MagicMock:
44
+ return True
45
+ if target_type is MagicMock:
46
+ return True
47
+ # if class_full_name(source_type) == 'unittest.mock.MagicMock':
48
+ # return True
49
+ # if class_full_name(target_type) == 'unittest.mock.MagicMock':
50
+ # return True
51
+ return False
52
+
53
+ def obj_is_type_union_compatible(self, var_type, compatible_types):
54
+ from typing import Union
55
+
56
+ origin = get_origin(var_type)
57
+ if isinstance(var_type, _GenericAlias) and origin is type: # Add handling for Type[T]
58
+ return type in compatible_types # Allow if 'type' is in compatible types
59
+ if origin is Union: # For Union types, including Optionals
60
+ args = get_args(var_type) # Get the argument types
61
+ for arg in args: # Iterate through each argument in the Union
62
+ 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)
63
+ return False # If any arg doesn't meet the criteria, return False immediately
64
+ return True # If all args are compatible, return True
65
+ return var_type in compatible_types or var_type is type(None) # Check for direct compatibility or type(None) for non-Union types
66
+
67
+
68
+ def check_if__type_matches__obj_annotation__for_union_and_annotated(self, target : Any , # Target object to check
69
+ attr_name : str , # Attribute name
70
+ value : Any )\
71
+ -> Optional[bool]: # Returns None if no match
72
+
73
+ from osbot_utils.helpers.python_compatibility.python_3_8 import Annotated
74
+ from typing import Union, get_origin, get_args
75
+
76
+ value_type = type(value)
77
+ attribute_annotation = type_safe_annotations.obj_attribute_annotation(target, attr_name)
78
+ origin = get_origin(attribute_annotation)
79
+
80
+ if origin is Union:
81
+ return self.check_if__type_matches__union_type(attribute_annotation, value_type)
82
+
83
+ if origin is Annotated:
84
+ return self.check_if__type_matches__annotated_type(attribute_annotation, value)
85
+
86
+ return None
87
+
88
+ def check_if__value_is__special_generic_alias(self, value):
89
+ from typing import _SpecialGenericAlias # todo see if there is a better way to do this since typing is showing as not having _SpecialGenericAlias (this is to handle case like List, Dict, etc...)
90
+ return value is not None and type(value) is not _SpecialGenericAlias
91
+
92
+ def check_if__type_matches__union_type(self, annotation : Any, # Union type annotation
93
+ value_type : Type
94
+ ) -> bool: # True if type matches
95
+ from typing import get_args
96
+ args = get_args(annotation)
97
+ return value_type in args
98
+
99
+ def check_if__type_matches__annotated_type(self, annotation : Any, # Annotated type annotation
100
+ value : Any # Value to check
101
+ ) -> bool: # True if type matches
102
+ from typing import get_args, get_origin
103
+ from typing import List, Dict, Tuple
104
+
105
+ args = get_args(annotation)
106
+ base_type = args[0] # First argument is base type
107
+ base_origin = get_origin(base_type)
108
+
109
+ if base_origin is None: # Handle non-container types
110
+ return isinstance(value, base_type)
111
+
112
+ if base_origin in (list, List): # Handle List types
113
+ return self.check_if__type_matches__list_type(value, base_type)
114
+
115
+ if base_origin in (tuple, Tuple): # Handle Tuple types
116
+ return self.check_if__type_matches__tuple_type(value, base_type)
117
+
118
+ if base_origin in (dict, Dict): # Handle Dict types
119
+ return self.check_if__type_matches_dict_type(value, base_type)
120
+
121
+ return False
122
+
123
+ def check_if__type_matches__list_type(self, value : Any, # Value to check
124
+ base_type : Any # List base type
125
+ ) -> bool: # True if valid list type
126
+ if not isinstance(value, list):
127
+ return False
128
+
129
+ item_type = get_args(base_type)[0]
130
+ return all(isinstance(item, item_type) for item in value)
131
+
132
+ def check_if__type_matches__tuple_type(self, value : Any, # Value to check
133
+ base_type : Any # Tuple base type
134
+ ) -> bool: # True if valid tuple type
135
+ if not isinstance(value, tuple):
136
+ return False
137
+
138
+ item_types = get_args(base_type)
139
+ return len(value) == len(item_types) and all(
140
+ isinstance(item, item_type)
141
+ for item, item_type in zip(value, item_types)
142
+ )
143
+
144
+ def check_if__type_matches_dict_type(self, value : Any, # Value to check
145
+ base_type : Any # Dict base type
146
+ ) -> bool: # True if valid dict type
147
+ if not isinstance(value, dict):
148
+ return False
149
+
150
+ key_type, value_type = get_args(base_type)
151
+ return all(isinstance(k, key_type) and isinstance(v, value_type)
152
+ for k, v in value.items()) # 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)
153
+
154
+ def check_if__type_matches__obj_annotation__for_attr(self, target,
155
+ attr_name,
156
+ value
157
+ ) -> Optional[bool]:
158
+ annotations = type_safe_cache.get_obj_annotations(target)
159
+ attr_type = annotations.get(attr_name)
160
+ if attr_type:
161
+ origin_attr_type = get_origin(attr_type) # to handle when type definition contains a generic
162
+ if origin_attr_type is type: # Add handling for Type[T]
163
+ type_arg = get_args(attr_type)[0] # Get T from Type[T]
164
+ if type_arg == value:
165
+ return True
166
+ if isinstance(type_arg, (str, ForwardRef)): # Handle forward reference
167
+ type_arg = target.__class__ # If it's a forward reference, the target class should be the containing class
168
+ return isinstance(value, type) and issubclass(value, type_arg) # Check that value is a type and is subclass of type_arg
169
+
170
+ if origin_attr_type is Annotated: # if the type is Annotated
171
+ args = get_args(attr_type)
172
+ origin_attr_type = args[0]
173
+
174
+ elif origin_attr_type is typing.Union:
175
+ args = get_args(attr_type)
176
+ 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] )
177
+ attr_type = args[0]
178
+ origin_attr_type = get_origin(attr_type)
179
+
180
+ if origin_attr_type:
181
+ attr_type = origin_attr_type
182
+ value_type = type(value)
183
+ if type_safe_validation.are_types_compatible_for_assigment(source_type=value_type, target_type=attr_type):
184
+ return True
185
+ if type_safe_validation.are_types_magic_mock(source_type=value_type, target_type=attr_type):
186
+ return True
187
+ return value_type is attr_type
188
+ return None
189
+
190
+ # todo: add cache support to this method
191
+ def should_skip_type_check(self, var_type): # Determine if type checking should be skipped
192
+ origin = type_safe_cache.get_origin(var_type) # Use cached get_origin
193
+ return (origin is Annotated or
194
+ origin is type )
195
+
196
+ def should_skip_var(self, var_name: str, var_value: Any) -> bool: # Determines if variable should be skipped during MRO processing
197
+ if var_name.startswith('__'): # skip internal variables
198
+ return True
199
+ if isinstance(var_value, types.FunctionType): # skip instance functions
200
+ return True
201
+ if isinstance(var_value, classmethod): # skip class methods
202
+ return True
203
+ if isinstance(var_value, property): # skip property descriptors
204
+ return True
205
+ return False
206
+
207
+ def validate_if_value_has_been_set(self, _self, annotations, name, value):
208
+ if hasattr(_self, name) and annotations.get(name) : # don't allow previously set variables to be set to None
209
+ if getattr(_self, name) is not None: # unless it is already set to None
210
+ 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)}'")
211
+
212
+ def validate_if__types_are_compatible_for_assigment(self, name, current_type, expected_type):
213
+ if not type_safe_validation.are_types_compatible_for_assigment(current_type, expected_type):
214
+ type_safe_raise_exception.type_mismatch_error(name, expected_type, current_type)
215
+
216
+ def validate_type_compatibility(self, target : Any , # Target object to validate
217
+ annotations : Dict[str, Any] , # Type annotations
218
+ name : str , # Attribute name
219
+ value : Any # Value to validate
220
+ ) -> None: # Raises ValueError if invalid
221
+
222
+ direct_type_match = type_safe_validation.check_if__type_matches__obj_annotation__for_attr(target, name, value)
223
+ union_type_match = type_safe_validation.check_if__type_matches__obj_annotation__for_union_and_annotated(target, name, value)
224
+
225
+ is_invalid = (direct_type_match is False and union_type_match is None) or \
226
+ (direct_type_match is None and union_type_match is False) or \
227
+ (direct_type_match is False and union_type_match is False)
228
+
229
+ if is_invalid:
230
+ expected_type = annotations.get(name)
231
+ actual_type = type(value)
232
+ raise ValueError(f"Invalid type for attribute '{name}'. Expected '{expected_type}' but got '{actual_type}'")
233
+
234
+ # todo: see if need to add cache support to this method (it looks like this method is not called very often)
235
+ def validate_type_immutability(self, var_name: str, var_type: Any) -> None: # Validates that type is immutable or in supported format
236
+ 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
237
+ if self.obj_is_type_union_compatible(var_type, IMMUTABLE_TYPES) is False: # if var_type is not something like Optional[Union[int, str]]
238
+ if var_type not in IMMUTABLE_TYPES or type(var_type) not in IMMUTABLE_TYPES:
239
+ if not isinstance(var_type, EnumMeta):
240
+ type_safe_raise_exception.immutable_type_error(var_name, var_type)
241
+
242
+ def validate_variable_type(self, var_name, var_type, var_value): # Validate type compatibility
243
+ if var_type and not isinstance(var_value, var_type):
244
+ type_safe_raise_exception.type_mismatch_error(var_name, var_type, type(var_value))
245
+
246
+ type_safe_validation = Type_Safe__Validation()
File without changes
@@ -0,0 +1,110 @@
1
+ from typing import Dict, Any, Type
2
+ from osbot_utils.helpers.Random_Guid import Random_Guid
3
+ from osbot_utils.type_safe.shared.Type_Safe__Cache import Type_Safe__Cache, type_safe_cache
4
+ from osbot_utils.type_safe.shared.Type_Safe__Shared__Variables import IMMUTABLE_TYPES
5
+ from osbot_utils.type_safe.shared.Type_Safe__Validation import type_safe_validation
6
+ from osbot_utils.type_safe.steps.Type_Safe__Step__Default_Value import type_safe_step_default_value
7
+
8
+
9
+
10
+ class Type_Safe__Step__Class_Kwargs: # Handles class-level keyword arguments processing
11
+
12
+ type_safe_cache : Type_Safe__Cache # Cache component reference
13
+
14
+ def __init__(self):
15
+ self.type_safe_cache = type_safe_cache # Initialize with singleton cache
16
+
17
+ def get_cls_kwargs(self, cls : Type )\
18
+ -> Dict[str, Any]: # Main entry point for getting class kwargs, returns dict of class kwargs
19
+
20
+ if not hasattr(cls, '__mro__'): # Handle non-class inputs
21
+ return {}
22
+
23
+ kwargs = type_safe_cache.get_cls_kwargs(cls) # see if we have cached data for this class
24
+
25
+ if kwargs is not None:
26
+ return kwargs
27
+ else:
28
+ kwargs = {}
29
+
30
+ base_classes = type_safe_cache.get_class_mro(cls)
31
+ for base_cls in base_classes:
32
+ self.process_mro_class (base_cls, kwargs) # Handle each class in MRO
33
+ self.process_annotations(cls, base_cls, kwargs) # Process its annotations
34
+
35
+ if self.is_kwargs_cacheable(cls, kwargs): # if we can cache it (i.e. only IMMUTABLE_TYPES vars)
36
+ type_safe_cache.set_cache__cls_kwargs(cls, kwargs) # cache it
37
+ # else:
38
+ # pass # todo:: see how we can cache more the cases when the data is clean (i.e. default values)
39
+ return kwargs
40
+
41
+ def is_kwargs_cacheable(self, cls, kwargs: Dict[str, Any]) -> bool:
42
+ annotations = type_safe_cache.get_class_annotations(cls)
43
+ match = all(isinstance(value, IMMUTABLE_TYPES) for value in kwargs.values())
44
+
45
+ if match: # check for special cases that we can't cache (like Random_Guid)
46
+ if Random_Guid in list(dict(annotations).values()): # todo: need to add the other special cases (like Timestamp_Now)
47
+
48
+ return False
49
+ return match
50
+
51
+
52
+ def handle_undefined_var(self, cls : Type , # Handle undefined class variables
53
+ kwargs : Dict[str, Any] ,
54
+ var_name : str ,
55
+ var_type : Type )\
56
+ -> None:
57
+ if var_name in kwargs: # Skip if already defined
58
+ return
59
+ var_value = type_safe_step_default_value.default_value(cls, var_type) # Get default value
60
+ kwargs[var_name] = var_value # Store in kwargs
61
+
62
+ def handle_defined_var(self, base_cls : Type , # Handle defined class variables
63
+ var_name : str ,
64
+ var_type : Type )\
65
+ -> None:
66
+ var_value = getattr(base_cls, var_name) # Get current value
67
+ if var_value is None: # Allow None assignments
68
+ return
69
+
70
+ if type_safe_validation.should_skip_type_check(var_type): # Skip validation if needed
71
+ return
72
+
73
+ type_safe_validation.validate_variable_type (var_name, var_type, var_value) # Validate type
74
+ type_safe_validation.validate_type_immutability(var_name, var_type) # Validate immutability
75
+
76
+ def process_annotation(self, cls : Type , # Process single annotation
77
+ base_cls : Type ,
78
+ kwargs : Dict[str, Any] ,
79
+ var_name : str ,
80
+ var_type : Type )\
81
+ -> None:
82
+ if not hasattr(base_cls, var_name): # Handle undefined variables
83
+ self.handle_undefined_var(cls, kwargs, var_name, var_type)
84
+ else: # Handle defined variables
85
+ self.handle_defined_var(base_cls, var_name, var_type)
86
+
87
+ def process_annotations(self, cls : Type , # Process all annotations
88
+ base_cls : Type ,
89
+ kwargs : Dict[str, Any] )\
90
+ -> None:
91
+ if hasattr(base_cls, '__annotations__'): # Process if annotations exist
92
+ for var_name, var_type in type_safe_cache.get_class_annotations(base_cls):
93
+ self.process_annotation(cls, base_cls, kwargs, var_name, var_type)
94
+
95
+ def process_mro_class(self, base_cls : Type , # Process class in MRO chain
96
+ kwargs : Dict[str, Any] )\
97
+ -> None:
98
+ if base_cls is object: # Skip object class
99
+ return
100
+
101
+ class_variables = type_safe_cache.get_valid_class_variables(base_cls ,
102
+ type_safe_validation.should_skip_var) # Get valid class variables
103
+
104
+ for name, value in class_variables.items(): # Add non-existing variables
105
+ if name not in kwargs:
106
+ kwargs[name] = value
107
+
108
+
109
+ # Create singleton instance
110
+ type_safe_step_class_kwargs = Type_Safe__Step__Class_Kwargs()
@@ -0,0 +1,42 @@
1
+ import types
2
+ import inspect
3
+
4
+ class Type_Safe__Step__Default_Kwargs:
5
+
6
+ def default_kwargs(self, _self):
7
+ kwargs = {}
8
+ cls = type(_self)
9
+ for base_cls in inspect.getmro(cls): # Traverse the inheritance hierarchy and collect class-level attributes
10
+ if base_cls is object: # Skip the base 'object' class
11
+ continue
12
+ for k, v in vars(base_cls).items():
13
+ if not k.startswith('__') and not isinstance(v, types.FunctionType): # remove instance functions
14
+ if not isinstance(v, classmethod):
15
+ kwargs[k] = v
16
+ # add the vars defined with the annotations
17
+ if hasattr(base_cls,'__annotations__'): # can only do type safety checks if the class does not have annotations
18
+ for var_name, var_type in base_cls.__annotations__.items():
19
+ var_value = getattr(_self, var_name)
20
+ kwargs[var_name] = var_value
21
+
22
+ return kwargs
23
+
24
+ def kwargs(self, _self):
25
+ kwargs = {}
26
+ for key, value in self.default_kwargs(_self).items(): # Update with instance-specific values
27
+ kwargs[key] = _self.__getattribute__(key)
28
+ return kwargs
29
+
30
+ def locals(self, _self):
31
+ """Return a dictionary of the current instance's attribute values."""
32
+ kwargs = self.kwargs(_self)
33
+
34
+ if not isinstance(vars(_self), types.FunctionType):
35
+ for k, v in vars(_self).items():
36
+ if not isinstance(v, types.FunctionType) and not isinstance(v,classmethod):
37
+ if k.startswith('__') is False:
38
+ kwargs[k] = v
39
+ return kwargs
40
+
41
+ type_safe_step_default_kwargs = Type_Safe__Step__Default_Kwargs()
42
+