Skip to content

Commit

Permalink
started refactoring Type_Safe__Step__Class_Kwargs into separate class…
Browse files Browse the repository at this point in the history
…es Type_Safe__Cache, Type_Safe__Raise_Exception, Type_Safe__Step__Class_Kwargs
  • Loading branch information
DinisCruz committed Jan 20, 2025
1 parent dd59291 commit 1794818
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 55 deletions.
37 changes: 37 additions & 0 deletions osbot_utils/type_safe/steps/Type_Safe__Cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import inspect
from weakref import WeakKeyDictionary


class Type_Safe__Cache:

_annotations_cache : WeakKeyDictionary
_mro_cache : WeakKeyDictionary
_valid_vars_cache : WeakKeyDictionary

# Caching system for Type_Safe methods
def __init__(self):
self._annotations_cache = WeakKeyDictionary() # Cache for class annotations
self._mro_cache = WeakKeyDictionary() # Cache for Method Resolution Order
self._valid_vars_cache = WeakKeyDictionary()

def get_class_annotations(self, cls):
if cls not in self._annotations_cache:
self._annotations_cache[cls] = cls.__annotations__.items()
return self._annotations_cache[cls]

def get_class_mro(self, cls):
if cls not in self._mro_cache:
self._mro_cache[cls] = inspect.getmro(cls)
return self._mro_cache[cls]

# todo: see if we have cache misses and invalid hits based on the validator (we might need more validator specific methods)
def get_valid_class_variables(self, cls, validator): # Returns a dictionary of valid class variables that should be processed. Filters out internal variables, methods, and other non-data attributes.
if cls not in self._valid_vars_cache:
valid_variables = {}
for name, value in vars(cls).items():
if not validator(name, value):
valid_variables[name] = value
self._valid_vars_cache[cls] = valid_variables
return self._valid_vars_cache[cls]

type_safe_cache = Type_Safe__Cache()
16 changes: 16 additions & 0 deletions osbot_utils/type_safe/steps/Type_Safe__Raise_Exception.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from typing import Any

from osbot_utils.type_safe.steps.Type_Safe__Validation import IMMUTABLE_TYPES


class Type_Safe__Raise_Exception:

def type_mismatch_error(self, var_name: str, expected_type: Any, actual_value: Any) -> None: # Raises formatted error for type validation failures
exception_message = f"variable '{var_name}' is defined as type '{expected_type}' but has value '{actual_value}' of type '{type(actual_value)}'"
raise ValueError(exception_message)

def immutable_type_error(self, var_name, var_type):
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}'"
raise ValueError(exception_message)

type_safe_raise_exception = Type_Safe__Raise_Exception()
60 changes: 14 additions & 46 deletions osbot_utils/type_safe/steps/Type_Safe__Step__Class_Kwargs.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,24 @@
import types
import inspect
from enum import EnumMeta
from typing import Dict, Any, Type, get_origin, Annotated, get_args
from weakref import WeakKeyDictionary
from osbot_utils.type_safe.steps.Type_Safe__Step__Default_Value import type_safe_step_default_value
from osbot_utils.utils.Objects import obj_is_type_union_compatible

IMMUTABLE_TYPES = (bool, int, float, complex, str, tuple, frozenset, bytes, types.NoneType, EnumMeta, type)
from enum import EnumMeta
from typing import Dict, Any, Type, get_origin, Annotated, get_args
from osbot_utils.type_safe.steps.Type_Safe__Cache import Type_Safe__Cache, type_safe_cache
from osbot_utils.type_safe.steps.Type_Safe__Raise_Exception import type_safe_raise_exception
from osbot_utils.type_safe.steps.Type_Safe__Step__Default_Value import type_safe_step_default_value
from osbot_utils.type_safe.steps.Type_Safe__Validation import IMMUTABLE_TYPES
from osbot_utils.utils.Objects import obj_is_type_union_compatible

class Type_Safe__Step__Class_Kwargs: # Cache for class-level keyword arguments and related information."""

_annotations_cache : WeakKeyDictionary
_mro_cache : WeakKeyDictionary
_valid_vars_cache : WeakKeyDictionary
type_safe_cache : Type_Safe__Cache

def __init__(self):
self._annotations_cache = WeakKeyDictionary()
self._mro_cache = WeakKeyDictionary()
self._valid_vars_cache = WeakKeyDictionary()

def base_cls_annotations(self, base_cls):
if base_cls not in self._annotations_cache:
self._annotations_cache[base_cls] = base_cls.__annotations__.items()
return self._annotations_cache[base_cls]
self.type_safe_cache = type_safe_cache

def get_cls_kwargs(self, cls: Type, include_base_classes: bool = True) -> Dict[str, Any]:
if not hasattr(cls, '__mro__'):
return {}

base_classes = self.get_mro_classes(cls)
base_classes = type_safe_cache.get_class_mro(cls)
if not include_base_classes:
base_classes = base_classes[:1]

Expand All @@ -38,20 +28,6 @@ def get_cls_kwargs(self, cls: Type, include_base_classes: bool = True) -> Dict[s
self.process_annotations(cls, base_cls, kwargs)
return kwargs

def get_mro_classes(self, cls):
if cls not in self._mro_cache:
self._mro_cache[cls] = inspect.getmro(cls)
return self._mro_cache[cls]

def get_valid_class_variables(self, cls): # Returns a dictionary of valid class variables that should be processed. Filters out internal variables, methods, and other non-data attributes.
if cls not in self._valid_vars_cache:
valid_variables = {}
for name, value in vars(cls).items():
if not self.should_skip_var(name, value):
valid_variables[name] = value
self._valid_vars_cache[cls] = valid_variables
return self._valid_vars_cache[cls]

def handle_undefined_var(self, cls, kwargs, var_name, var_type): # Handle variables not yet defined in base class
if var_name in kwargs:
return
Expand All @@ -69,8 +45,6 @@ def handle_defined_var(self, base_cls, var_name, var_type):
self.validate_variable_type (var_name, var_type, var_value)
self.validate_type_immutability(var_name, var_type)



def process_annotation(self, cls, base_cls, kwargs, var_name, var_type): # Process type annotations for class variables
if not hasattr(base_cls, var_name):
self.handle_undefined_var(cls, kwargs, var_name, var_type)
Expand All @@ -79,14 +53,14 @@ def process_annotation(self, cls, base_cls, kwargs, var_name, var_type):

def process_annotations(self, cls, base_cls, kwargs):
if hasattr(base_cls,'__annotations__'): # can only do type safety checks if the class does not have annotations
for var_name, var_type in self.base_cls_annotations(base_cls):
for var_name, var_type in type_safe_cache.get_class_annotations(base_cls):
self.process_annotation(cls, base_cls, kwargs, var_name, var_type)

def process_mro_class(self, base_cls, kwargs):
if base_cls is object: # Skip the base 'object' class
return

class_variables = self.get_valid_class_variables(base_cls)
class_variables = type_safe_cache.get_valid_class_variables(base_cls, self.should_skip_var)

for name, value in class_variables.items():
if name not in kwargs:
Expand All @@ -108,24 +82,18 @@ def should_skip_type_check(self, var_type):
get_origin(var_type) is type)


def raise_type_mismatch_error(self, var_name: str, expected_type: Any,actual_value: Any) -> None: # Raises formatted error for type validation failures
exception_message = f"variable '{var_name}' is defined as type '{expected_type}' but has value '{actual_value}' of type '{type(actual_value)}'"
raise ValueError(exception_message)

def raise_immutable_type_error(self, var_name, var_type):
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}'"
raise ValueError(exception_message)

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]]
if var_type not in IMMUTABLE_TYPES or type(var_type) not in IMMUTABLE_TYPES:
if not isinstance(var_type, EnumMeta):
self.raise_immutable_type_error(var_name, var_type)
type_safe_raise_exception.immutable_type_error(var_name, var_type)

def validate_variable_type(self, var_name, var_type, var_value): # Validate type compatibility
if var_type and not isinstance(var_value, var_type):
self.raise_type_mismatch_error(var_name, var_type, var_value)
type_safe_raise_exception.type_mismatch_error(var_name, var_type, var_value)


# Create singleton instance
Expand Down
10 changes: 10 additions & 0 deletions osbot_utils/type_safe/steps/Type_Safe__Validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import types
from enum import EnumMeta

IMMUTABLE_TYPES = (bool, int, float, complex, str, tuple, frozenset, bytes, types.NoneType, EnumMeta, type)

class Type_Safe__Validation:
pass


type_safe_validation = Type_Safe__Validation()
13 changes: 5 additions & 8 deletions tests/unit/decorators/methods/test_cache_on_self.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
from unittest import TestCase

from osbot_utils.type_safe.Type_Safe import Type_Safe

from osbot_utils.decorators.methods.cache_on_self import cache_on_self, cache_on_self__get_cache_in_key, \
CACHE_ON_SELF_KEY_PREFIX, cache_on_self__args_to_str, cache_on_self__kwargs_to_str
from osbot_utils.testing.Catch import Catch
from osbot_utils.utils.Objects import obj_data
from unittest import TestCase
from osbot_utils.type_safe.Type_Safe import Type_Safe
from osbot_utils.decorators.methods.cache_on_self import cache_on_self, cache_on_self__get_cache_in_key, CACHE_ON_SELF_KEY_PREFIX, cache_on_self__args_to_str, cache_on_self__kwargs_to_str
from osbot_utils.testing.Catch import Catch
from osbot_utils.utils.Objects import obj_data


class An_Class:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def setUpClass(cls):
cls.time_200_ns = 200
cls.time_300_ns = 300
cls.time_500_ns = 500
cls.time_700_ns = 700
cls.time_700_ns = 700
cls.time_800_ns = 800
cls.time_1_kns = 1_000
cls.time_2_kns = 2_000
Expand Down

0 comments on commit 1794818

Please sign in to comment.