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 Jan 23, 2025
2 parents 3237336 + 0de453a commit 4f8c727
Show file tree
Hide file tree
Showing 9 changed files with 119 additions and 12 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-v2.15.0-blue)
![Current Release](https://img.shields.io/badge/release-v2.15.1-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
41 changes: 41 additions & 0 deletions osbot_utils/type_safe/Type_Safe__Tuple.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from osbot_utils.type_safe.Type_Safe__Base import Type_Safe__Base, type_str

class Type_Safe__Tuple(Type_Safe__Base, tuple):

def __new__(cls, expected_types, items=None):
items = items or tuple()
instance = super().__new__(cls, items)
instance.expected_types = expected_types
return instance

def __init__(self, expected_types, items=None): # todo: see if we should be assining expected_types to self here
if items:
self.validate_items(items)

def validate_items(self, items):
if len(items) != len(self.expected_types):
raise ValueError(f"Expected {len(self.expected_types)} elements, got {len(items)}")
for item, expected_type in zip(items, self.expected_types):
try:
self.is_instance_of_type(item, expected_type)
except TypeError as e:
raise TypeError(f"In Type_Safe__Tuple: Invalid type for item: {e}")

def __repr__(self):
types_str = ', '.join(type_str(t) for t in self.expected_types)
return f"tuple[{types_str}] with {len(self)} elements"

def json(self):
from osbot_utils.type_safe.Type_Safe import Type_Safe

result = []
for item in self:
if isinstance(item, Type_Safe):
result.append(item.json())
elif isinstance(item, (list, tuple)):
result.append([x.json() if isinstance(x, Type_Safe) else x for x in item])
elif isinstance(item, dict):
result.append({k: v.json() if isinstance(v, Type_Safe) else v for k, v in item.items()})
else:
result.append(item)
return result
21 changes: 14 additions & 7 deletions osbot_utils/type_safe/steps/Type_Safe__Step__Default_Value.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
import inspect
import typing

from osbot_utils.type_safe.Type_Safe__Set import Type_Safe__Set
from osbot_utils.type_safe.shared.Type_Safe__Cache import type_safe_cache
from osbot_utils.utils.Objects import default_value
from osbot_utils.type_safe.Type_Safe__List import Type_Safe__List
from osbot_utils.type_safe.Type_Safe__Dict import Type_Safe__Dict
from osbot_utils.type_safe.Type_Safe__Set import Type_Safe__Set
from osbot_utils.type_safe.Type_Safe__Tuple import Type_Safe__Tuple
from osbot_utils.type_safe.shared.Type_Safe__Cache import type_safe_cache
from osbot_utils.utils.Objects import default_value
from osbot_utils.type_safe.Type_Safe__List import Type_Safe__List
from osbot_utils.type_safe.Type_Safe__Dict import Type_Safe__Dict


# Backport implementations of get_args for Python 3.7 # todo: refactor into separate class (focused on past python version compatibility)
Expand All @@ -27,6 +28,11 @@ class Type_Safe__Step__Default_Value:
def default_value(self, _cls, var_type):

origin = type_safe_cache.get_origin(var_type) # todo: refactor this to use the get_origin method

if origin is tuple:
item_types = get_args(var_type)
return Type_Safe__Tuple(expected_types=item_types)

if origin is type: # Special handling for Type[T] # todo: reuse the get_origin value
type_args = get_args(var_type)
if type_args:
Expand Down Expand Up @@ -73,8 +79,9 @@ def default_value(self, _cls, var_type):
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


return default_value(var_type) # for all other cases call default_value, which will try to create a default instance


type_safe_step_default_value = Type_Safe__Step__Default_Value()
4 changes: 4 additions & 0 deletions osbot_utils/type_safe/steps/Type_Safe__Step__From_Json.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ def deserialize_from_dict(self, _self, data, raise_on_not_found=False):
value = self.deserialize_type__using_value(value)
elif annotation_origin == type: # Handle type objects inside ForwardRef
value = self.deserialize_type__using_value(value)
if annotation_origin is tuple and isinstance(value, list):
# item_types = get_args(annotation) # todo: see if we should do type safety here
value = tuple(value)
elif type_safe_annotations.obj_is_attribute_annotation_of_type(_self, key, dict): # handle the case when the value is a dict
value = self.deserialize_dict__using_key_value_annotations(_self, key, value)
elif type_safe_annotations.obj_is_attribute_annotation_of_type(_self, key, set): # handle the case when the value is a list
Expand Down Expand Up @@ -99,6 +102,7 @@ def deserialize_from_dict(self, _self, data, raise_on_not_found=False):
value = Random_Guid_Short(value)
elif type_safe_annotations.obj_is_attribute_annotation_of_type(_self, key, Timestamp_Now): # handle Timestamp_Now
value = Timestamp_Now(value)

setattr(_self, key, value) # Direct assignment for primitive types and other structures

return _self
Expand Down
2 changes: 1 addition & 1 deletion osbot_utils/utils/Objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ def serialize_to_dict(obj):
return obj.name
elif isinstance(obj, type):
return f"{obj.__module__}.{obj.__name__}" # save the full type name
elif isinstance(obj, list) or isinstance(obj, List):
elif isinstance(obj, (list, tuple, List)): # Added tuple here
return [serialize_to_dict(item) for item in obj]
elif isinstance(obj, set):
return [serialize_to_dict(item) for item in obj]
Expand Down
2 changes: 1 addition & 1 deletion osbot_utils/version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v2.15.0
v2.15.1
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "osbot_utils"
version = "v2.15.0"
version = "v2.15.1"
description = "OWASP Security Bot - Utils"
authors = ["Dinis Cruz <[email protected]>"]
license = "MIT"
Expand Down
55 changes: 55 additions & 0 deletions tests/unit/type_safe/_bugs/test_Type_Safe__bugs.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,66 @@
from unittest import TestCase
from osbot_utils.type_safe.Type_Safe import Type_Safe
from osbot_utils.base_classes.Kwargs_To_Self import Kwargs_To_Self
from osbot_utils.type_safe.Type_Safe__Dict import Type_Safe__Dict
from osbot_utils.type_safe.shared.Type_Safe__Convert import type_safe_convert


class test_Type_Safe__bugs(TestCase):

def test__bug__roundtrip_tuple_support(self):
class Inner_Data(Type_Safe):
tuple_1 : tuple[str, str]
node_ids: Dict[str, tuple[str, str]]
edge_ids: Dict[str, set[str]]

class An_Class(Type_Safe):
data: Inner_Data

an_class = An_Class()
an_class.data = Inner_Data()
an_class.data.node_ids = {"a": ("id1", "id2")}
an_class.data.edge_ids = {"b": {"id3"}}
an_class.data.tuple_1 = ("id1", "id2")

expected_error = "In tuple at index 0: Expected 'str', but got 'int'"
with pytest.raises(TypeError, match=expected_error):
an_class.data.node_ids = {"b": (123, '123')} # confirm type safety in tuples
# with pytest.raises(TypeError, match=expected_error):
# an_class.data.tuple_1 = (123, '123') # BUG should have raised

assert type(an_class.data.tuple_1) is tuple # BUG this should be Type_Safe__Tuple
an_class.data.tuple_1 = (123, '123') # BUG should have raised
assert type(an_class.data.tuple_1) is tuple

assert type(an_class.data.node_ids) is Type_Safe__Dict # this should not be a dict
assert type(an_class.data.edge_ids) is Type_Safe__Dict # correct this should be a dict


# expected_error = "Type <class 'tuple'> not serializable"
# with pytest.raises(TypeError, match=expected_error): # BUG, should have worked
# an_class.json()
assert an_class.json() == {'data': {'edge_ids': {'b': ['id3']},
'node_ids': {'a': ['id1', 'id2']},
'tuple_1': [123, '123']}} # BUG: this should have not happened

roundtrip_obj = An_Class.from_json(an_class.json())

assert roundtrip_obj.json() == an_class.json()
assert roundtrip_obj.data.node_ids == {"a": ("id1", "id2")}
assert roundtrip_obj.data.edge_ids == {"b": {"id3"}}
assert roundtrip_obj.data.tuple_1 == (123, "123")
assert type(roundtrip_obj.data.tuple_1) is tuple


# These assertions will help verify the fix once implemented
# assert json_data == {'data': {'node_ids': {'a': ['id1', 'id2']},
# 'edge_ids': {'b': ['id3', 'id4']}}}
#
# an_class_round_trip = An_Class.from_json(json_data)
# assert type(an_class_round_trip.data.node_ids['a']) is tuple
# assert type(an_class_round_trip.data.edge_ids['b']) is Type_Safe__Set
# assert an_class_round_trip.data.node_ids['a'] == ('id1', 'id2')
# assert an_class_round_trip.data.edge_ids['b'] == {'id3', 'id4'}
def test__bug__property_descriptor_handling__doesnt_enforce_type_safety(self):

class Test_Class(Type_Safe):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from osbot_utils.type_safe.Type_Safe__List import Type_Safe__List
from osbot_utils.decorators.methods.cache_on_self import cache_on_self
from osbot_utils.helpers.Random_Guid import Random_Guid
from osbot_utils.type_safe.Type_Safe__Set import Type_Safe__Set
from osbot_utils.type_safe.Type_Safe__Set import Type_Safe__Set
from osbot_utils.type_safe.shared.Type_Safe__Annotations import type_safe_annotations
from osbot_utils.type_safe.validators.Validator__Min import Min
from osbot_utils.utils.Json import json_to_str, str_to_json
Expand Down

0 comments on commit 4f8c727

Please sign in to comment.