Skip to content

Commit

Permalink
Merge dev into main
Browse files Browse the repository at this point in the history
  • Loading branch information
DinisCruz committed Dec 26, 2024
2 parents 49a856b + 6d8f975 commit cfb861f
Show file tree
Hide file tree
Showing 14 changed files with 888 additions and 187 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Powerful Python util methods and classes that simplify common apis and tasks.

![Current Release](https://img.shields.io/badge/release-v1.88.0-blue)
![Current Release](https://img.shields.io/badge/release-v1.88.2-blue)
[![codecov](https://codecov.io/gh/owasp-sbot/OSBot-Utils/graph/badge.svg?token=GNVW0COX1N)](https://codecov.io/gh/owasp-sbot/OSBot-Utils)


Expand Down
32 changes: 24 additions & 8 deletions osbot_utils/base_classes/Type_Safe.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import sys
import types
from osbot_utils.utils.Objects import default_value # todo: remove test mocking requirement for this to be here (instead of on the respective method)
from osbot_utils.utils.Objects import default_value # todo: remove test mocking requirement for this to be here (instead of on the respective method)

# Backport implementations of get_origin and get_args for Python 3.7
if sys.version_info < (3, 8): # pragma: no cover
Expand All @@ -23,7 +23,7 @@ def get_args(tp):
else:
return ()
else:
from typing import get_origin, get_args
from typing import get_origin, get_args, ForwardRef

if sys.version_info >= (3, 10):
NoneType = types.NoneType
Expand Down Expand Up @@ -148,6 +148,7 @@ def __cls_kwargs__(cls, include_base_classes=True): # Return cu
def __default__value__(cls, var_type):
import typing
from osbot_utils.base_classes.Type_Safe__List import Type_Safe__List
from osbot_utils.base_classes.Type_Safe__Dict import Type_Safe__Dict

if var_type is typing.Set: # todo: refactor the dict, set and list logic, since they are 90% the same
return set()
Expand All @@ -156,13 +157,28 @@ def __default__value__(cls, var_type):

if var_type is typing.Dict:
return {}
if get_origin(var_type) is dict:
return {} # todo: add Type_Safe__Dict

if get_origin(var_type) is dict: # e.g. Dict[key_type, value_type]
key_type, value_type = get_args(var_type)
if isinstance(key_type, ForwardRef): # Handle forward references on key_type ---
forward_name = key_type.__forward_arg__
if forward_name == cls.__name__:
key_type = cls
if isinstance(value_type, ForwardRef): # Handle forward references on value_type ---
forward_name = value_type.__forward_arg__
if forward_name == cls.__name__:
value_type = cls
return Type_Safe__Dict(expected_key_type=key_type, expected_value_type=value_type)

if var_type is typing.List:
return [] # handle case when List was used with no type information provided
return [] # handle case when List was used with no type information provided

if get_origin(var_type) is list: # if we have list defined as list[type]
item_type = get_args(var_type)[0] # get the type that was defined
if isinstance(item_type, ForwardRef): # handle the case when the type is a forward reference
forward_name = item_type.__forward_arg__
if forward_name == cls.__name__: # if the forward reference is to the current class (simple name check)
item_type = cls # set the item_type to the current class
return Type_Safe__List(expected_type=item_type) # and used it as expected_type in Type_Safe__List
else:
return default_value(var_type) # for all other cases call default_value, which will try to create a default instance
Expand Down Expand Up @@ -258,16 +274,16 @@ def update_from_kwargs(self, **kwargs): # Update instanc
return self

def deserialize_dict__using_key_value_annotations(self, key, value):
from osbot_utils.base_classes.Type_Safe__Dict import Type_Safe__Dict

dict_annotations_tuple = get_args(self.__annotations__[key])
if not dict_annotations_tuple: # happens when the value is a dict/Dict with no annotations
return value
if not type(value) is dict:
return value
#key_class = get_args(self.__annotations__[key])[0]
#value_class = get_args(self.__annotations__[key])[1]
key_class = dict_annotations_tuple[0]
value_class = dict_annotations_tuple[1]
new_value = {}
new_value = Type_Safe__Dict(expected_key_type=key_class, expected_value_type=value_class)

for dict_key, dict_value in value.items():
if issubclass(key_class, Type_Safe):
Expand Down
117 changes: 117 additions & 0 deletions osbot_utils/base_classes/Type_Safe__Base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
from typing import get_origin, get_args, Union, Optional, Any, ForwardRef

EXACT_TYPE_MATCH = (int, float, str, bytes, bool, complex)

class Type_Safe__Base:
def is_instance_of_type(self, item, expected_type):
if expected_type is Any:
return True
if isinstance(expected_type, ForwardRef): # todo: add support for ForwardRef
return True
origin = get_origin(expected_type)
args = get_args(expected_type)
if origin is None:
if expected_type in EXACT_TYPE_MATCH:
if type(item) is expected_type:
return True
else:
expected_type_name = type_str(expected_type)
actual_type_name = type_str(type(item))
raise TypeError(f"Expected '{expected_type_name}', but got '{actual_type_name}'")
else:
if isinstance(item, expected_type): # Non-parameterized type
return True
else:
expected_type_name = type_str(expected_type)
actual_type_name = type_str(type(item))
raise TypeError(f"Expected '{expected_type_name}', but got '{actual_type_name}'")

elif origin is list and args: # Expected type is List[...]
(item_type,) = args
if not isinstance(item, list):
expected_type_name = type_str(expected_type)
actual_type_name = type_str(type(item))
raise TypeError(f"Expected '{expected_type_name}', but got '{actual_type_name}'")
for idx, elem in enumerate(item):
try:
self.is_instance_of_type(elem, item_type)
except TypeError as e:
raise TypeError(f"In list at index {idx}: {e}")
return True
elif origin is dict and args: # Expected type is Dict[...]
key_type, value_type = args
if not isinstance(item, dict):
expected_type_name = type_str(expected_type)
actual_type_name = type_str(type(item))
raise TypeError(f"Expected '{expected_type_name}', but got '{actual_type_name}'")
for k, v in item.items():
try:
self.is_instance_of_type(k, key_type)
except TypeError as e:
raise TypeError(f"In dict key '{k}': {e}")
try:
self.is_instance_of_type(v, value_type)
except TypeError as e:
raise TypeError(f"In dict value for key '{k}': {e}")
return True
elif origin is tuple:
if not isinstance(item, tuple):
expected_type_name = type_str(expected_type)
actual_type_name = type_str(type(item))
raise TypeError(f"Expected '{expected_type_name}', but got '{actual_type_name}'")
if len(args) != len(item):
raise TypeError(f"Expected tuple of length {len(args)}, but got {len(item)}")
for idx, (elem, elem_type) in enumerate(zip(item, args)):
try:
self.is_instance_of_type(elem, elem_type)
except TypeError as e:
raise TypeError(f"In tuple at index {idx}: {e}")
return True
elif origin is Union or expected_type is Optional: # Expected type is Union[...]
for arg in args:
try:
self.is_instance_of_type(item, arg)
return True
except TypeError:
continue
expected_type_name = type_str(expected_type)
actual_type_name = type_str(type(item))
raise TypeError(f"Expected '{expected_type_name}', but got '{actual_type_name}'")
else:
if isinstance(item, origin):
return True
else:
expected_type_name = type_str(expected_type)
actual_type_name = type_str(type(item))
raise TypeError(f"Expected '{expected_type_name}', but got '{actual_type_name}'")

# todo: see if we should/can move this to the Objects.py file
def type_str(tp):
origin = get_origin(tp)
if origin is None:
if hasattr(tp, '__name__'):
return tp.__name__
else:
return str(tp)
else:
args = get_args(tp)
args_str = ', '.join(type_str(arg) for arg in args)
return f"{origin.__name__}[{args_str}]"

def get_object_type_str(obj):
if isinstance(obj, dict):
if not obj:
return "Dict[Empty]"
key_types = set(type(k).__name__ for k in obj.keys())
value_types = set(type(v).__name__ for v in obj.values())
key_type_str = ', '.join(sorted(key_types))
value_type_str = ', '.join(sorted(value_types))
return f"Dict[{key_type_str}, {value_type_str}]"
elif isinstance(obj, list):
if not obj:
return "List[Empty]"
elem_types = set(type(e).__name__ for e in obj)
elem_type_str = ', '.join(sorted(elem_types))
return f"List[{elem_type_str}]"
else:
return type(obj).__name__
22 changes: 22 additions & 0 deletions osbot_utils/base_classes/Type_Safe__Dict.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from osbot_utils.base_classes.Type_Safe__Base import type_str, Type_Safe__Base

class Type_Safe__Dict(Type_Safe__Base, dict):
def __init__(self, expected_key_type, expected_value_type, *args, **kwargs):
super().__init__(*args, **kwargs)

self.expected_key_type = expected_key_type
self.expected_value_type = expected_value_type

for k, v in self.items(): # check type-safety of ctor arguments
self.is_instance_of_type(k, self.expected_key_type )
self.is_instance_of_type(v, self.expected_value_type)

def __setitem__(self, key, value): # Check type-safety before allowing assignment.
self.is_instance_of_type(key, self.expected_key_type)
self.is_instance_of_type(value, self.expected_value_type)
super().__setitem__(key, value)

def __repr__(self):
key_type_name = type_str(self.expected_key_type)
value_type_name = type_str(self.expected_value_type)
return f"dict[{key_type_name}, {value_type_name}] with {len(self)} entries"
115 changes: 2 additions & 113 deletions osbot_utils/base_classes/Type_Safe__List.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from typing import get_origin, get_args, Union, Optional, Any, ForwardRef
from osbot_utils.base_classes.Type_Safe__Base import Type_Safe__Base, type_str

EXACT_TYPE_MATCH = (int, float, str, bytes, bool, complex)

class Type_Safe__List(list):
class Type_Safe__List(Type_Safe__Base, list):

def __init__(self, expected_type, *args):
super().__init__(*args)
Expand All @@ -19,115 +18,5 @@ def append(self, item):
raise TypeError(f"In Type_Safe__List: Invalid type for item: {e}")
super().append(item)

def is_instance_of_type(self, item, expected_type):
if expected_type is Any:
return True
if isinstance(expected_type, ForwardRef): # todo: add support for ForwardRef
return True
origin = get_origin(expected_type)
args = get_args(expected_type)
if origin is None:
if expected_type in EXACT_TYPE_MATCH:
if type(item) is expected_type:
return True
else:
expected_type_name = type_str(expected_type)
actual_type_name = type_str(type(item))
raise TypeError(f"Expected '{expected_type_name}', but got '{actual_type_name}'")
else:
if isinstance(item, expected_type): # Non-parameterized type
return True
else:
expected_type_name = type_str(expected_type)
actual_type_name = type_str(type(item))
raise TypeError(f"Expected '{expected_type_name}', but got '{actual_type_name}'")

elif origin is list and args: # Expected type is List[...]
(item_type,) = args
if not isinstance(item, list):
expected_type_name = type_str(expected_type)
actual_type_name = type_str(type(item))
raise TypeError(f"Expected '{expected_type_name}', but got '{actual_type_name}'")
for idx, elem in enumerate(item):
try:
self.is_instance_of_type(elem, item_type)
except TypeError as e:
raise TypeError(f"In list at index {idx}: {e}")
return True
elif origin is dict and args: # Expected type is Dict[...]
key_type, value_type = args
if not isinstance(item, dict):
expected_type_name = type_str(expected_type)
actual_type_name = type_str(type(item))
raise TypeError(f"Expected '{expected_type_name}', but got '{actual_type_name}'")
for k, v in item.items():
try:
self.is_instance_of_type(k, key_type)
except TypeError as e:
raise TypeError(f"In dict key '{k}': {e}")
try:
self.is_instance_of_type(v, value_type)
except TypeError as e:
raise TypeError(f"In dict value for key '{k}': {e}")
return True
elif origin is tuple:
if not isinstance(item, tuple):
expected_type_name = type_str(expected_type)
actual_type_name = type_str(type(item))
raise TypeError(f"Expected '{expected_type_name}', but got '{actual_type_name}'")
if len(args) != len(item):
raise TypeError(f"Expected tuple of length {len(args)}, but got {len(item)}")
for idx, (elem, elem_type) in enumerate(zip(item, args)):
try:
self.is_instance_of_type(elem, elem_type)
except TypeError as e:
raise TypeError(f"In tuple at index {idx}: {e}")
return True
elif origin is Union or expected_type is Optional: # Expected type is Union[...]
for arg in args:
try:
self.is_instance_of_type(item, arg)
return True
except TypeError:
continue
expected_type_name = type_str(expected_type)
actual_type_name = type_str(type(item))
raise TypeError(f"Expected '{expected_type_name}', but got '{actual_type_name}'")
else:
if isinstance(item, origin):
return True
else:
expected_type_name = type_str(expected_type)
actual_type_name = type_str(type(item))
raise TypeError(f"Expected '{expected_type_name}', but got '{actual_type_name}'")

# todo: see if we should/can move this to the Objects.py file
def type_str(tp):
origin = get_origin(tp)
if origin is None:
if hasattr(tp, '__name__'):
return tp.__name__
else:
return str(tp)
else:
args = get_args(tp)
args_str = ', '.join(type_str(arg) for arg in args)
return f"{origin.__name__}[{args_str}]"

def get_object_type_str(obj):
if isinstance(obj, dict):
if not obj:
return "Dict[Empty]"
key_types = set(type(k).__name__ for k in obj.keys())
value_types = set(type(v).__name__ for v in obj.values())
key_type_str = ', '.join(sorted(key_types))
value_type_str = ', '.join(sorted(value_types))
return f"Dict[{key_type_str}, {value_type_str}]"
elif isinstance(obj, list):
if not obj:
return "List[Empty]"
elem_types = set(type(e).__name__ for e in obj)
elem_type_str = ', '.join(sorted(elem_types))
return f"List[{elem_type_str}]"
else:
return type(obj).__name__
Loading

0 comments on commit cfb861f

Please sign in to comment.