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 22, 2025
2 parents f9e06b4 + 751a2ec commit 08a22b8
Show file tree
Hide file tree
Showing 10 changed files with 80 additions and 34 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.13.0-blue)
![Current Release](https://img.shields.io/badge/release-v2.13.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
17 changes: 9 additions & 8 deletions osbot_utils/type_safe/shared/Type_Safe__Convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,17 @@ def convert_to_value_from_obj_annotation(self, target, attr_name, value):
if attribute_annotation:
origin = type_safe_cache.get_origin(attribute_annotation) # Add handling for Type[T] annotations
if origin is type and isinstance(value, str):
try: # Convert string path to actual type
if len(value.rsplit('.', 1)) > 1:
module_name, class_name = value.rsplit('.', 1)
module = __import__(module_name, fromlist=[class_name])
return getattr(module, class_name)
except (ValueError, ImportError, AttributeError) as e:
raise ValueError(f"Could not convert '{value}' to type: {str(e)}")

return self.get_class_from_class_name(value)
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
return attribute_annotation(value)
return value

def get_class_from_class_name(self, value):
try: # Convert string path to actual type
if len(value.rsplit('.', 1)) > 1:
module_name, class_name = value.rsplit('.', 1)
module = __import__(module_name, fromlist=[class_name])
return getattr(module, class_name)
except (ValueError, ImportError, AttributeError) as e:
raise ValueError(f"Could not convert '{value}' to type: {str(e)}")
type_safe_convert = Type_Safe__Convert()
5 changes: 4 additions & 1 deletion osbot_utils/type_safe/shared/Type_Safe__Validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,10 @@ def validate_type_compatibility(self, target : Any ,

if is_invalid:
expected_type = annotations.get(name)
actual_type = type(value)
if type(value) is type:
actual_type = value
else:
actual_type = type(value)
raise ValueError(f"Invalid type for attribute '{name}'. Expected '{expected_type}' but got '{actual_type}'")

# todo: see if need to add cache support to this method (it looks like this method is not called very often)
Expand Down
15 changes: 7 additions & 8 deletions osbot_utils/type_safe/steps/Type_Safe__Step__Class_Kwargs.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
from typing import Dict, Any, Type

from osbot_utils.helpers.Obj_Id import Obj_Id
from osbot_utils.helpers.Random_Guid import Random_Guid
from osbot_utils.type_safe.shared.Type_Safe__Cache import Type_Safe__Cache, type_safe_cache
from osbot_utils.type_safe.shared.Type_Safe__Shared__Variables import IMMUTABLE_TYPES
from osbot_utils.type_safe.shared.Type_Safe__Validation import type_safe_validation
from osbot_utils.type_safe.steps.Type_Safe__Step__Default_Value import type_safe_step_default_value
from typing import Dict, Any, Type
from osbot_utils.helpers.Obj_Id import Obj_Id
from osbot_utils.helpers.Random_Guid import Random_Guid
from osbot_utils.type_safe.shared.Type_Safe__Cache import Type_Safe__Cache, type_safe_cache
from osbot_utils.type_safe.shared.Type_Safe__Shared__Variables import IMMUTABLE_TYPES
from osbot_utils.type_safe.shared.Type_Safe__Validation import type_safe_validation
from osbot_utils.type_safe.steps.Type_Safe__Step__Default_Value import type_safe_step_default_value



Expand Down
12 changes: 11 additions & 1 deletion osbot_utils/type_safe/steps/Type_Safe__Step__From_Json.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from osbot_utils.helpers.Random_Guid_Short import Random_Guid_Short
from osbot_utils.type_safe.shared.Type_Safe__Annotations import type_safe_annotations
from osbot_utils.type_safe.shared.Type_Safe__Cache import type_safe_cache
from osbot_utils.type_safe.shared.Type_Safe__Convert import type_safe_convert
from osbot_utils.utils.Objects import enum_from_value
from osbot_utils.helpers.Safe_Id import Safe_Id
from osbot_utils.helpers.Timestamp_Now import Timestamp_Now
Expand Down Expand Up @@ -43,7 +44,13 @@ def deserialize_from_dict(self, _self, data, raise_on_not_found=False):
raise ValueError(f"Attribute '{key}' not found in '{_self.__class__.__name__}'")
else:
continue
if type_safe_annotations.obj_attribute_annotation(_self, key) == type: # Handle type objects
annotation = type_safe_annotations.obj_attribute_annotation(_self, key)
annotation_origin = type_safe_cache.get_origin(annotation)


if annotation == type: # Handle type objects
value = self.deserialize_type__using_value(value)
elif annotation_origin == type: # Handle type objects inside ForwardRef
value = self.deserialize_type__using_value(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)
Expand Down Expand Up @@ -117,6 +124,9 @@ def deserialize_dict__using_key_value_annotations(self, _self, key, value):
if type(dict_value) == value_class: # if the value is already the target, then just use it
new__dict_value = dict_value
elif issubclass(value_class, Type_Safe):
if 'node_type' in dict_value:
value_class = type_safe_convert.get_class_from_class_name(dict_value['node_type'])

new__dict_value = self.deserialize_from_dict(value_class(), dict_value)
elif value_class is Any:
new__dict_value = dict_value
Expand Down
2 changes: 1 addition & 1 deletion osbot_utils/version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v2.13.0
v2.13.2
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.13.0"
version = "v2.13.2"
description = "OWASP Security Bot - Utils"
authors = ["Dinis Cruz <[email protected]>"]
license = "MIT"
Expand Down
19 changes: 15 additions & 4 deletions tests/unit/type_safe/_bugs/test_Type_Safe__bugs.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import re
import sys
import pytest
from typing import Optional, Union, Dict
Expand Down Expand Up @@ -72,10 +71,22 @@ class Child_Class(Parent_Class):
assert Parent_Class().json() == Parent_Class.from_json(Parent_Class().json()).json() # Round trip of Parent_Class works

# current buggy workflow
with pytest.raises(ValueError, match=re.escape("Invalid type for attribute 'class_type'. Expected 'typing.Type[int]' but got '<class 'str'>'")):
assert Child_Class .from_json(Child_Class().json()) # BUG should not have raised an exception
# with pytest.raises(ValueError, match=re.escape("Invalid type for attribute 'class_type'. Expected 'typing.Type[int]' but got '<class 'str'>'")):
# assert Child_Class .from_json(Child_Class().json()) # BUG should not have raised an exception

assert Child_Class.from_json(Child_Class().json()).json() == Child_Class().json() # Fixed : works now :BUG this should work












#assert Child_Class.from_json(Child_Class().json()).json() == Child_Class().json() # BUG this should work


def test__bug__in__convert_dict_to_value_from_obj_annotation(self):
Expand Down
32 changes: 26 additions & 6 deletions tests/unit/type_safe/_regression/test_Type_Safe__regression.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,26 @@

class test_Type_Safe__regression(TestCase):

def test__regression__forward_ref_type(self):
class Base__Type(Type_Safe):
ref_type: Type['Base__Type']


class Type__With__Forward__Ref(Base__Type):
pass

target = Type__With__Forward__Ref()

json_data = target.json() # This serializes ref_type as string
#error_message = "Invalid type for attribute 'ref_type'. Expected 'typing.Type[ForwardRef('Base__Type')]' but got '<class 'str'>'"
error_message = "Could not reconstruct type from 'test_Type_Safe__regression.Type__With__Forward__Ref': module 'test_Type_Safe__regression' has no attribute 'Type__With__Forward__Ref'"
#
assert json_data == {'ref_type': 'test_Type_Safe__regression.Type__With__Forward__Ref'}

with pytest.raises(ValueError, match=re.escape(error_message)):
Type__With__Forward__Ref.from_json(json_data) # Fixed we are now raising the correct exception BUG: exception should have not been raised


def test__regression__property_descriptor_handling(self):

class Regular_Class: # First case: Normal Python class without Type_Safe
Expand Down Expand Up @@ -119,7 +139,7 @@ class Other_Class: pass
with self.assertRaises(ValueError) as context:
custom_node.node_type = Other_Class

assert str(context.exception) == "Invalid type for attribute 'node_type'. Expected 'typing.Type[ForwardRef('Base_Node')]' but got '<class 'type'>'"
assert str(context.exception) == "Invalid type for attribute 'node_type'. Expected 'typing.Type[ForwardRef('Base_Node')]' but got '<class 'test_Type_Safe__regression.test_Type_Safe__regression.test__regression__forward_ref_handling_in_type_matches.<locals>.Other_Class'>'"

# Test with more complex case (like Schema__MGraph__Node)
from typing import Dict, Any
Expand Down Expand Up @@ -243,14 +263,14 @@ class An_Class_1(Type_Safe):

an_class.an_type__str = str
an_class.an_type__str = Random_Guid
with pytest.raises(ValueError, match=re.escape("Invalid type for attribute 'an_type__str'. Expected 'typing.Type[str]' but got '<class 'type'>'")) :
with pytest.raises(ValueError, match=re.escape("Invalid type for attribute 'an_type__str'. Expected 'typing.Type[str]' but got '<class 'int'>'")) :
an_class.an_type__str = int

#with pytest.raises(TypeError, match=re.escape("issubclass() arg 2 must be a class, a tuple of classes, or a union")):
# an_class.an_type__forward_ref = An_Class_1 # Fixed; BUG: this should have worked

an_class.an_type__forward_ref = An_Class_1
with pytest.raises(ValueError, match=re.escape("Invalid type for attribute 'an_type__forward_ref'. Expected 'typing.Type[ForwardRef('An_Class_1')]' but got '<class 'type'>'")):
with pytest.raises(ValueError, match=re.escape("Invalid type for attribute 'an_type__forward_ref'. Expected 'typing.Type[ForwardRef('An_Class_1')]' but got '<class 'str'>'")):
an_class.an_type__forward_ref = str

class An_Class_2(An_Class_1):
Expand Down Expand Up @@ -347,13 +367,13 @@ class An_Class(Type_Safe):
an_class.an_type_str = str
an_class.an_type_int = int

with pytest.raises(ValueError, match=re.escape("Invalid type for attribute 'an_type_str'. Expected 'typing.Type[str]' but got '<class 'type'>")):
with pytest.raises(ValueError, match=re.escape("Invalid type for attribute 'an_type_str'. Expected 'typing.Type[str]' but got '<class 'int'>")):
an_class.an_type_str = int # Fixed: BUG: should have raised exception

with pytest.raises(ValueError, match=re.escape("Invalid type for attribute 'an_type_int'. Expected 'typing.Type[int]' but got '<class 'type'>")):
with pytest.raises(ValueError, match=re.escape("Invalid type for attribute 'an_type_int'. Expected 'typing.Type[int]' but got '<class 'str'>")):
an_class.an_type_int = str # Fixed: BUG: should have raised exception

with pytest.raises(ValueError, match=re.escape("Invalid type for attribute 'an_type_str'. Expected 'typing.Type[str]' but got '<class 'str'>'")):
with pytest.raises(ValueError, match=re.escape("Invalid type for attribute 'an_type_str'. Expected 'typing.Type[str]' but got '<class 'NoneType'>'")):
an_class.an_type_str = 'a'


Expand Down
8 changes: 5 additions & 3 deletions tests/unit/type_safe/test_Type_Safe.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import types
import pytest
from enum import Enum, auto
from typing import Union, Optional, Type, List
from typing import Union, Optional, Type
from unittest import TestCase
from osbot_utils.helpers.Timestamp_Now import Timestamp_Now
from osbot_utils.helpers.Guid import Guid
Expand Down Expand Up @@ -691,7 +691,7 @@ class Type_Safety(Type_Safe): # Test type safety with Type
type_safety = Type_Safety()
type_safety.str_type = str # OK: str matches Type[str]

with pytest.raises(ValueError, match=re.escape("Invalid type for attribute 'str_type'. Expected 'typing.Type[str]' but got '<class 'type'>'")): # Should fail: int is not a subclass of str
with pytest.raises(ValueError, match=re.escape("Invalid type for attribute 'str_type'. Expected 'typing.Type[str]' but got '<class 'int'>'")): # Should fail: int is not a subclass of str
type_safety.str_type = int


Expand Down Expand Up @@ -976,7 +976,9 @@ class Should_Fail(Type_Safe):
An_Class(node_type=Child_Type_2)
An_Class(node_type=Child_Type_3)
An_Class(node_type=Child_Type_4)
with pytest.raises(ValueError,match=re.escape("Invalid type for attribute 'node_type'. Expected 'typing.Type[ForwardRef('An_Class')]' but got '<class 'type'>'")):

error_message = "Invalid type for attribute 'node_type'. Expected 'typing.Type[ForwardRef('An_Class')]' but got '<class 'test_Type_Safe.test_Type_Safe.test_type_checks_on__forward_ref__works_on_multiple_levels.<locals>.Should_Fail'>'"
with pytest.raises(ValueError,match=re.escape(error_message)):
An_Class(node_type=Should_Fail)

assert issubclass(Child_Type_1, An_Class)
Expand Down

0 comments on commit 08a22b8

Please sign in to comment.