Skip to content

Commit

Permalink
added cache for an the list of an class Immutable variables
Browse files Browse the repository at this point in the history
refactored Type_Safe__Step__Set_Attr
  • Loading branch information
DinisCruz committed Jan 20, 2025
1 parent 5d87aa1 commit c7c1814
Show file tree
Hide file tree
Showing 12 changed files with 287 additions and 91 deletions.
80 changes: 49 additions & 31 deletions osbot_utils/type_safe/shared/Type_Safe__Cache.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,62 @@
import inspect
from typing import get_origin
from weakref import WeakKeyDictionary
from typing import get_origin
from weakref import WeakKeyDictionary
from osbot_utils.type_safe.shared.Type_Safe__Shared__Variables import IMMUTABLE_TYPES


class Type_Safe__Cache:

_annotations_cache : WeakKeyDictionary
_cls_kwargs_cache : WeakKeyDictionary
_get_origin_cache : WeakKeyDictionary
_mro_cache : WeakKeyDictionary
_valid_vars_cache : WeakKeyDictionary
_cls__annotations_cache : WeakKeyDictionary
_cls__immutable_vars : WeakKeyDictionary
_cls__kwargs_cache : WeakKeyDictionary
_get_origin_cache : WeakKeyDictionary
_mro_cache : WeakKeyDictionary
_valid_vars_cache : WeakKeyDictionary

cache_hit__annotations : int = 0
cache_hit__cls_kwargs : int = 0
cache_hit__get_origin : int = 0
cache_hit__mro : int = 0
cache_hit__valid_vars : int = 0
skip_cache : bool = False
cache_hit__cls__annotations : int = 0
cache_hit__cls__kwargs : int = 0
cache_hit__cls__immutable_vars: int = 0
cache_hit__get_origin : int = 0
cache_hit__mro : int = 0
cache_hit__valid_vars : int = 0
skip_cache : bool = False


# Caching system for Type_Safe methods
def __init__(self):
self._annotations_cache = WeakKeyDictionary() # Cache for class annotations
self._cls_kwargs_cache = WeakKeyDictionary() # Cache for class kwargs
self._get_origin_cache = WeakKeyDictionary() # Cache for get_origin results
self._mro_cache = WeakKeyDictionary() # Cache for Method Resolution Order
self._valid_vars_cache = WeakKeyDictionary()
self._cls__annotations_cache = WeakKeyDictionary() # Cache for class annotations
self._cls__immutable_vars = WeakKeyDictionary() # Cache for class immutable vars
self._cls__kwargs_cache = WeakKeyDictionary() # Cache for class kwargs
self._get_origin_cache = WeakKeyDictionary() # Cache for get_origin results
self._mro_cache = WeakKeyDictionary() # Cache for Method Resolution Order
self._valid_vars_cache = WeakKeyDictionary()

def get_cls_kwargs(self, cls):
if self.skip_cache or cls not in self._cls_kwargs_cache:
if self.skip_cache or cls not in self._cls__kwargs_cache:
return None
else:
self.cache_hit__cls_kwargs += 1
return self._cls_kwargs_cache.get(cls)
self.cache_hit__cls__kwargs += 1
return self._cls__kwargs_cache.get(cls)

def get_class_annotations(self, cls):
if self.skip_cache or cls not in self._annotations_cache:
self._annotations_cache[cls] = cls.__annotations__.items()
annotations = self._cls__annotations_cache.get(cls) # this is a more efficient cache retrieval pattern (we only get the data from the dict once)
if not annotations: # todo: apply this to the other cache getters
if self.skip_cache or cls not in self._cls__annotations_cache:
annotations = cls.__annotations__.items()
self._cls__annotations_cache[cls] = annotations
else:
self.cache_hit__annotations += 1
return self._annotations_cache[cls]
self.cache_hit__cls__annotations += 1
return annotations

def get_class_immutable_vars(self, cls):
immutable_vars = self._cls__immutable_vars.get(cls)
if self.skip_cache or not immutable_vars:
annotations = self.get_class_annotations(cls)
immutable_vars = [key for key, value in annotations if value in IMMUTABLE_TYPES]
self._cls__immutable_vars[cls] = immutable_vars
else:
self.cache_hit__cls__immutable_vars += 1
return immutable_vars

def get_class_mro(self, cls):
if self.skip_cache or cls not in self._mro_cache:
Expand Down Expand Up @@ -69,17 +86,18 @@ def get_valid_class_variables(self, cls, validator):
return self._valid_vars_cache[cls]

def set_cache__cls_kwargs(self, cls, kwargs):
self._cls_kwargs_cache[cls] = kwargs
self._cls__kwargs_cache[cls] = kwargs
return kwargs

def print_cache_hits(self):
print()
print("###### Type_Safe_Cache Hits ########")
print()
print(f" annotations : {self.cache_hit__annotations}")
print(f" cls_kwargs : {self.cache_hit__cls_kwargs }")
print(f" get_origin : {self.cache_hit__get_origin }")
print(f" mro : {self.cache_hit__mro }")
print(f" valid_vars : {self.cache_hit__valid_vars }")
print(f" annotations : {self.cache_hit__cls__annotations }")
print(f" cls__kwargs : {self.cache_hit__cls__kwargs }")
print(f" cls__immutable_vars: {self.cache_hit__cls__immutable_vars }")
print(f" get_origin : {self.cache_hit__get_origin }")
print(f" mro : {self.cache_hit__mro }")
print(f" valid_vars : {self.cache_hit__valid_vars }")

type_safe_cache = Type_Safe__Cache()
2 changes: 0 additions & 2 deletions osbot_utils/type_safe/shared/Type_Safe__Shared__Variables.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import types
from enum import EnumMeta


#IMMUTABLE_TYPES = (bool, int, float, complex, str, tuple, frozenset, bytes, types.NoneType, EnumMeta, type)
IMMUTABLE_TYPES = (bool, int, float, complex, str, bytes, types.NoneType, EnumMeta, type)
14 changes: 7 additions & 7 deletions osbot_utils/type_safe/shared/Type_Safe__Validation.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import types
from enum import EnumMeta
from typing import Any, Annotated

from osbot_utils.type_safe.shared.Type_Safe__Cache import type_safe_cache
from osbot_utils.type_safe.shared.Type_Safe__Shared__Variables import IMMUTABLE_TYPES
from osbot_utils.utils.Objects import obj_is_type_union_compatible
from osbot_utils.type_safe.shared.Type_Safe__Raise_Exception import type_safe_raise_exception
from enum import EnumMeta
from typing import Any, Annotated
from osbot_utils.type_safe.shared.Type_Safe__Cache import type_safe_cache
from osbot_utils.type_safe.shared.Type_Safe__Shared__Variables import IMMUTABLE_TYPES
from osbot_utils.utils.Objects import obj_is_type_union_compatible
from osbot_utils.type_safe.shared.Type_Safe__Raise_Exception import type_safe_raise_exception


class Type_Safe__Validation:
Expand All @@ -27,6 +26,7 @@ def should_skip_var(self, var_name: str, var_value: Any) -> bool:
return True
return False

# todo: see if need to add cache support to this method (it looks like this method is not called very often)
def validate_type_immutability(self, var_name: str, var_type: Any) -> None: # Validates that type is immutable or in supported format
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
if obj_is_type_union_compatible(var_type, IMMUTABLE_TYPES) is False: # if var_type is not something like Optional[Union[int, str]]
Expand Down
9 changes: 4 additions & 5 deletions osbot_utils/type_safe/steps/Type_Safe__Step__Class_Kwargs.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,13 @@ def process_annotations(self, cls : Type ,
def process_mro_class(self, base_cls : Type , # Process class in MRO chain
kwargs : Dict[str, Any] )\
-> None:
if base_cls is object: # Skip object class
if base_cls is object: # Skip object class
return

class_variables = type_safe_cache.get_valid_class_variables( # Get valid class variables
base_cls,
type_safe_validation.should_skip_var)
class_variables = type_safe_cache.get_valid_class_variables(base_cls ,
type_safe_validation.should_skip_var) # Get valid class variables

for name, value in class_variables.items(): # Add non-existing variables
for name, value in class_variables.items(): # Add non-existing variables
if name not in kwargs:
kwargs[name] = value

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def default_value(self, _cls, var_type):

if var_type is typing.Set: # todo: refactor the dict, set and list logic, since they are 90% the same
return set()

if get_origin(var_type) is set:
return set() # todo: add Type_Safe__Set

Expand Down
2 changes: 0 additions & 2 deletions osbot_utils/type_safe/steps/Type_Safe__Step__Init.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ class Type_Safe__Step__Init:

def init(self, __self, __class_kwargs, **kwargs):

#__class_kwargs = type_safe_step_class_kwargs.get_cls_kwargs(cls) # todo: figure out why this doesn't work here on 1% of the tests (like the ones in CPrint)

for (key, value) in __class_kwargs.items(): # assign all default values to target
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
raise_exception_on_obj_type_annotation_mismatch(__self, key, value)
Expand Down
103 changes: 70 additions & 33 deletions osbot_utils/type_safe/steps/Type_Safe__Step__Set_Attr.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import get_origin, Annotated, get_args
from osbot_utils.type_safe.shared.Type_Safe__Cache import type_safe_cache
from osbot_utils.utils.Objects import all_annotations
from osbot_utils.utils.Objects import convert_dict_to_value_from_obj_annotation
from osbot_utils.utils.Objects import convert_to_value_from_obj_annotation
Expand All @@ -9,48 +10,84 @@

class Type_Safe__Step__Set_Attr:

def setattr(self, _super, _self, name, value):
def verify_value(self, _self, annotations, name, value): # refactor the logic of this method since it is confusing
check_1 = value_type_matches_obj_annotation_for_attr (_self, name, value)
check_2 = value_type_matches_obj_annotation_for_union_and_annotated(_self, name, value)
if (check_1 is False and check_2 is None or
check_1 is None and check_2 is False or
check_1 is False and check_2 is False ): # fix for type safety assigment on Union vars
raise ValueError(f"Invalid type for attribute '{name}'. Expected '{annotations.get(name)}' but got '{type(value)}'")

annotations = all_annotations(_self)
if not annotations: # can't do type safety checks if the class does not have annotations
return _super.__setattr__(name, value)

if value is not None:
if type(value) is dict:
value = convert_dict_to_value_from_obj_annotation(_self, name, value)
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)
value = convert_to_value_from_obj_annotation (_self, name, value)
else:
origin = get_origin(value)
if origin is not None:
value = origin
check_1 = value_type_matches_obj_annotation_for_attr (_self, name, value)
check_2 = value_type_matches_obj_annotation_for_union_and_annotated(_self, name, value)
if (check_1 is False and check_2 is None or
check_1 is None and check_2 is False or
check_1 is False and check_2 is False ): # fix for type safety assigment on Union vars
raise ValueError(f"Invalid type for attribute '{name}'. Expected '{annotations.get(name)}' but got '{type(value)}'")
def resolve_value(self, _self, annotations, name, value):
if type(value) is dict:
value = self.resolve_value__dict(_self, name, value)
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)
value = self.resolve_value__int_str(_self, name, value)
else:
if hasattr(_self, name) and annotations.get(name) : # don't allow previously set variables to be set to None
if getattr(_self, name) is not None: # unless it is already set to None
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)}'")
value = self.resolve_value__from_origin(value)

self.verify_value(_self, annotations, name, value)
return value

def resolve_value__dict(self, _self, name, value):
return convert_dict_to_value_from_obj_annotation(_self, name, value)

def resolve_value__int_str(self, _self, name, value):
immutable_vars = type_safe_cache.get_class_immutable_vars(_self.__class__) # get the cached value of immutable vars for this class

if name in immutable_vars: # we only need to do the conversion if the variable is immutable
return value

return convert_to_value_from_obj_annotation(_self, name, value)



# todo: refactor this to separate method
def resolve_value__from_origin(self, value):
origin = get_origin(value)
if origin is not None:
value = origin
return value

def validate_if_value_has_been_set(self, _self, annotations, name, value):
if hasattr(_self, name) and annotations.get(name) : # don't allow previously set variables to be set to None
if getattr(_self, name) is not None: # unless it is already set to None
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)}'")

def handle_get_class__annotated(self, annotation, name, value):
annotation_args = get_args(annotation)
target_type = annotation_args[0]
for attribute in annotation_args[1:]:
if isinstance(attribute, Type_Safe__Validator):
attribute.validate(value=value, field_name=name, target_type=target_type)

def handle_get_class__dict(self, _self, name, value):
# todo: refactor how this actually works since it is not good to having to use the deserialize_dict__using_key_value_annotations from here
from osbot_utils.type_safe.steps.Type_Safe__Step__From_Json import Type_Safe__Step__From_Json # here because of circular dependencies
value = Type_Safe__Step__From_Json().deserialize_dict__using_key_value_annotations(_self, name, value)
return value

def handle_get_class(self, _self, annotations, name, value):
if hasattr(annotations, 'get'):
annotation = annotations.get(name)
if annotation:
annotation_origin = get_origin(annotation)
if annotation_origin is Annotated:
annotation_args = get_args(annotation)
target_type = annotation_args[0]
for attribute in annotation_args[1:]:
if isinstance(attribute, Type_Safe__Validator):
attribute.validate(value=value, field_name=name, target_type=target_type)
self.handle_get_class__annotated(annotation, name, value)
elif annotation_origin is dict:
# todo: refactor how this actually works since it is not good to having to use the deserialize_dict__using_key_value_annotations from here
from osbot_utils.type_safe.steps.Type_Safe__Step__From_Json import Type_Safe__Step__From_Json
value = Type_Safe__Step__From_Json().deserialize_dict__using_key_value_annotations(_self, name, value)
#value = _self.deserialize_dict__using_key_value_annotations(name, value)
value = self.handle_get_class__dict(_self, name, value)
return value

def setattr(self, _super, _self, name, value):

annotations = all_annotations(_self)
if not annotations: # can't do type safety checks if the class does not have annotations
return _super.__setattr__(name, value)

if value is not None:
value = self.resolve_value (_self, annotations, name, value)
value = self.handle_get_class(_self, annotations, name, value)
else:
self.validate_if_value_has_been_set(_self, annotations, name, value)

_super.__setattr__(name, value)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def setUpClass(cls):
pytest.skip("Skipping tests in Github Actions")
cls.time_0_ns = 0
cls.time_100_ns = 100
cls.time_200_ns = 200
cls.time_1_kns = 1_000
cls.time_2_kns = 2_000
cls.time_3_kns = 3_000
Expand Down Expand Up @@ -52,7 +53,7 @@ class An_Class_6(Type_Safe):
with Performance_Measure__Session(assert_enabled=True) as _:
_.measure(str ).print().assert_time(self.time_100_ns, self.time_0_ns )
_.measure(Random_Guid).print().assert_time(self.time_3_kns , self.time_5_kns, self.time_6_kns , self.time_7_kns )
_.measure(An_Class_1 ).print().assert_time(self.time_100_ns )
_.measure(An_Class_1 ).print().assert_time(self.time_100_ns, self.time_200_ns )
_.measure(An_Class_2 ).print().assert_time(self.time_1_kns , self.time_2_kns , self.time_3_kns , self.time_4_kns , self.time_5_kns , self.time_6_kns, self.time_7_kns )
_.measure(An_Class_3 ).print().assert_time(self.time_8_kns , self.time_9_kns ,self.time_10_kns, self.time_20_kns )
_.measure(An_Class_4 ).print().assert_time(self.time_8_kns , self.time_9_kns ,self.time_10_kns, self.time_20_kns )
Expand Down
Loading

0 comments on commit c7c1814

Please sign in to comment.