osbot-utils 2.11.0__py3-none-any.whl → 2.13.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/Obj_Id.py +29 -0
- 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 +128 -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 +47 -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 +114 -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.13.0.dist-info}/METADATA +2 -2
- {osbot_utils-2.11.0.dist-info → osbot_utils-2.13.0.dist-info}/RECORD +38 -17
- {osbot_utils-2.11.0.dist-info → osbot_utils-2.13.0.dist-info}/LICENSE +0 -0
- {osbot_utils-2.11.0.dist-info → osbot_utils-2.13.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,47 @@
|
|
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
|
+
from osbot_utils.helpers.Obj_Id import Obj_Id
|
24
|
+
|
25
|
+
TYPE_SAFE__CONVERT_VALUE__SUPPORTED_TYPES = [Guid, Random_Guid, Safe_Id, Str_ASCII, Timestamp_Now, Obj_Id]
|
26
|
+
|
27
|
+
if target is not None and attr_name is not None:
|
28
|
+
if hasattr(target, '__annotations__'):
|
29
|
+
obj_annotations = target.__annotations__
|
30
|
+
if hasattr(obj_annotations,'get'):
|
31
|
+
attribute_annotation = obj_annotations.get(attr_name)
|
32
|
+
if attribute_annotation:
|
33
|
+
origin = type_safe_cache.get_origin(attribute_annotation) # Add handling for Type[T] annotations
|
34
|
+
if origin is type and isinstance(value, str):
|
35
|
+
try: # Convert string path to actual type
|
36
|
+
if len(value.rsplit('.', 1)) > 1:
|
37
|
+
module_name, class_name = value.rsplit('.', 1)
|
38
|
+
module = __import__(module_name, fromlist=[class_name])
|
39
|
+
return getattr(module, class_name)
|
40
|
+
except (ValueError, ImportError, AttributeError) as e:
|
41
|
+
raise ValueError(f"Could not convert '{value}' to type: {str(e)}")
|
42
|
+
|
43
|
+
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
|
44
|
+
return attribute_annotation(value)
|
45
|
+
return value
|
46
|
+
|
47
|
+
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,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,114 @@
|
|
1
|
+
from typing import Dict, Any, Type
|
2
|
+
|
3
|
+
from osbot_utils.helpers.Obj_Id import Obj_Id
|
4
|
+
from osbot_utils.helpers.Random_Guid import Random_Guid
|
5
|
+
from osbot_utils.type_safe.shared.Type_Safe__Cache import Type_Safe__Cache, type_safe_cache
|
6
|
+
from osbot_utils.type_safe.shared.Type_Safe__Shared__Variables import IMMUTABLE_TYPES
|
7
|
+
from osbot_utils.type_safe.shared.Type_Safe__Validation import type_safe_validation
|
8
|
+
from osbot_utils.type_safe.steps.Type_Safe__Step__Default_Value import type_safe_step_default_value
|
9
|
+
|
10
|
+
|
11
|
+
|
12
|
+
class Type_Safe__Step__Class_Kwargs: # Handles class-level keyword arguments processing
|
13
|
+
|
14
|
+
type_safe_cache : Type_Safe__Cache # Cache component reference
|
15
|
+
|
16
|
+
def __init__(self):
|
17
|
+
self.type_safe_cache = type_safe_cache # Initialize with singleton cache
|
18
|
+
|
19
|
+
def get_cls_kwargs(self, cls : Type )\
|
20
|
+
-> Dict[str, Any]: # Main entry point for getting class kwargs, returns dict of class kwargs
|
21
|
+
|
22
|
+
if not hasattr(cls, '__mro__'): # Handle non-class inputs
|
23
|
+
return {}
|
24
|
+
|
25
|
+
kwargs = type_safe_cache.get_cls_kwargs(cls) # see if we have cached data for this class
|
26
|
+
|
27
|
+
if kwargs is not None:
|
28
|
+
return kwargs
|
29
|
+
else:
|
30
|
+
kwargs = {}
|
31
|
+
|
32
|
+
base_classes = type_safe_cache.get_class_mro(cls)
|
33
|
+
for base_cls in base_classes:
|
34
|
+
self.process_mro_class (base_cls, kwargs) # Handle each class in MRO
|
35
|
+
self.process_annotations(cls, base_cls, kwargs) # Process its annotations
|
36
|
+
|
37
|
+
if self.is_kwargs_cacheable(cls, kwargs): # if we can cache it (i.e. only IMMUTABLE_TYPES vars)
|
38
|
+
type_safe_cache.set_cache__cls_kwargs(cls, kwargs) # cache it
|
39
|
+
# else:
|
40
|
+
# pass # todo:: see how we can cache more the cases when the data is clean (i.e. default values)
|
41
|
+
return kwargs
|
42
|
+
|
43
|
+
def is_kwargs_cacheable(self, cls, kwargs: Dict[str, Any]) -> bool:
|
44
|
+
annotations = type_safe_cache.get_class_annotations(cls)
|
45
|
+
match = all(isinstance(value, IMMUTABLE_TYPES) for value in kwargs.values())
|
46
|
+
|
47
|
+
if match: # check for special cases that we can't cache (like Random_Guid)
|
48
|
+
annotations_types = list(dict(annotations).values())
|
49
|
+
if Random_Guid in annotations_types: # todo: need to add the other special cases (like Timestamp_Now)
|
50
|
+
return False
|
51
|
+
if Obj_Id in annotations_types: # we can't cache Obj_id, since this would give us the same ID everutime
|
52
|
+
return False
|
53
|
+
return match
|
54
|
+
|
55
|
+
|
56
|
+
def handle_undefined_var(self, cls : Type , # Handle undefined class variables
|
57
|
+
kwargs : Dict[str, Any] ,
|
58
|
+
var_name : str ,
|
59
|
+
var_type : Type )\
|
60
|
+
-> None:
|
61
|
+
if var_name in kwargs: # Skip if already defined
|
62
|
+
return
|
63
|
+
var_value = type_safe_step_default_value.default_value(cls, var_type) # Get default value
|
64
|
+
kwargs[var_name] = var_value # Store in kwargs
|
65
|
+
|
66
|
+
def handle_defined_var(self, base_cls : Type , # Handle defined class variables
|
67
|
+
var_name : str ,
|
68
|
+
var_type : Type )\
|
69
|
+
-> None:
|
70
|
+
var_value = getattr(base_cls, var_name) # Get current value
|
71
|
+
if var_value is None: # Allow None assignments
|
72
|
+
return
|
73
|
+
|
74
|
+
if type_safe_validation.should_skip_type_check(var_type): # Skip validation if needed
|
75
|
+
return
|
76
|
+
|
77
|
+
type_safe_validation.validate_variable_type (var_name, var_type, var_value) # Validate type
|
78
|
+
type_safe_validation.validate_type_immutability(var_name, var_type) # Validate immutability
|
79
|
+
|
80
|
+
def process_annotation(self, cls : Type , # Process single annotation
|
81
|
+
base_cls : Type ,
|
82
|
+
kwargs : Dict[str, Any] ,
|
83
|
+
var_name : str ,
|
84
|
+
var_type : Type )\
|
85
|
+
-> None:
|
86
|
+
if not hasattr(base_cls, var_name): # Handle undefined variables
|
87
|
+
self.handle_undefined_var(cls, kwargs, var_name, var_type)
|
88
|
+
else: # Handle defined variables
|
89
|
+
self.handle_defined_var(base_cls, var_name, var_type)
|
90
|
+
|
91
|
+
def process_annotations(self, cls : Type , # Process all annotations
|
92
|
+
base_cls : Type ,
|
93
|
+
kwargs : Dict[str, Any] )\
|
94
|
+
-> None:
|
95
|
+
if hasattr(base_cls, '__annotations__'): # Process if annotations exist
|
96
|
+
for var_name, var_type in type_safe_cache.get_class_annotations(base_cls):
|
97
|
+
self.process_annotation(cls, base_cls, kwargs, var_name, var_type)
|
98
|
+
|
99
|
+
def process_mro_class(self, base_cls : Type , # Process class in MRO chain
|
100
|
+
kwargs : Dict[str, Any] )\
|
101
|
+
-> None:
|
102
|
+
if base_cls is object: # Skip object class
|
103
|
+
return
|
104
|
+
|
105
|
+
class_variables = type_safe_cache.get_valid_class_variables(base_cls ,
|
106
|
+
type_safe_validation.should_skip_var) # Get valid class variables
|
107
|
+
|
108
|
+
for name, value in class_variables.items(): # Add non-existing variables
|
109
|
+
if name not in kwargs:
|
110
|
+
kwargs[name] = value
|
111
|
+
|
112
|
+
|
113
|
+
# Create singleton instance
|
114
|
+
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
|
+
|