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 9, 2025
2 parents b2fee74 + 8217413 commit ecf03f4
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 20 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.7.0-blue)
![Current Release](https://img.shields.io/badge/release-v2.7.3-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
11 changes: 10 additions & 1 deletion osbot_utils/type_safe/Type_Safe.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# todo: find a way to add these documentations strings to a separate location so that
# the data is available in IDE's code complete

import inspect
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)
Expand Down Expand Up @@ -198,6 +198,15 @@ def __default__value__(cls, var_type):
import typing
from osbot_utils.type_safe.Type_Safe__List import Type_Safe__List
from osbot_utils.type_safe.Type_Safe__Dict import Type_Safe__Dict
if get_origin(var_type) is type: # Special handling for Type[T] # todo: reuse the get_origin value
type_args = get_args(var_type)
if type_args:
if isinstance(type_args[0], ForwardRef):
forward_name = type_args[0].__forward_arg__
for base_cls in inspect.getmro(cls):
if base_cls.__name__ == forward_name:
return cls # note: in this case we return the cls, and not the base_cls (which makes sense since this happens when the cls class uses base_cls as base, which has a ForwardRef to base_cls )
return type_args[0] # Return the actual type as the default value

if var_type is typing.Set: # todo: refactor the dict, set and list logic, since they are 90% the same
return set()
Expand Down
3 changes: 3 additions & 0 deletions osbot_utils/utils/Objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,9 +469,12 @@ def value_type_matches_obj_annotation_for_attr(target, attr_name, value):
origin_attr_type = get_origin(attr_type) # to handle when type definition contains a generic
if origin_attr_type is type: # Add handling for Type[T]
type_arg = get_args(attr_type)[0] # Get T from Type[T]
if type_arg == value:
return True
if isinstance(type_arg, (str, ForwardRef)): # Handle forward reference
type_arg = target.__class__ # If it's a forward reference, the target class should be the containing class
return isinstance(value, type) and issubclass(value, type_arg) # Check that value is a type and is subclass of type_arg

if origin_attr_type is Annotated: # if the type is Annotated
args = get_args(attr_type)
origin_attr_type = args[0]
Expand Down
2 changes: 1 addition & 1 deletion osbot_utils/version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v2.7.0
v2.7.3
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.7.0"
version = "v2.7.3"
description = "OWASP Security Bot - Utils"
authors = ["Dinis Cruz <[email protected]>"]
license = "MIT"
Expand Down
5 changes: 1 addition & 4 deletions tests/unit/type_safe/bugs/test_Type_Safe__bugs.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import re
import sys
import pytest
from typing import Optional, Union, Dict, Type
from typing import Optional, Union, Dict
from unittest import TestCase
from osbot_utils.helpers.Random_Guid import Random_Guid
from osbot_utils.utils.Objects import __
from osbot_utils.type_safe.Type_Safe import Type_Safe
from osbot_utils.base_classes.Kwargs_To_Self import Kwargs_To_Self

Expand Down
161 changes: 153 additions & 8 deletions tests/unit/type_safe/regression/test_Type_Safe__regression.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import pytest
import sys
from decimal import Decimal
from typing import Optional, Union, List, Dict, get_origin, Type
from typing import Optional, Union, List, Dict, get_origin, Type, ForwardRef, Any
from unittest import TestCase
from unittest.mock import patch
from osbot_utils.helpers.Timestamp_Now import Timestamp_Now
Expand All @@ -19,16 +19,161 @@
from osbot_utils.utils.Misc import list_set, is_guid
from osbot_utils.utils.Objects import default_value, __, all_annotations


class test_Type_Safe__regression(TestCase):

def test__bug__forward_refs_in_type(self):
def test__regression__forward_ref_handling_in_type_matches(self):
class Base_Node(Type_Safe):
node_type: Type['Base_Node'] # Forward reference to self
value: str

# Test base class
base_node = Base_Node()
assert base_node.node_type is Base_Node # Default value should be the Base_Node

# Should be able to set it to the class itself
base_node.node_type = Base_Node
assert base_node.node_type is Base_Node

# Subclass should work now too
class Custom_Node(Base_Node): pass

custom_node = Custom_Node() # This should no longer raise TypeError


# Should accept either base or subclass
custom_node.node_type = Custom_Node
assert custom_node.node_type is Custom_Node

custom_node.node_type = Custom_Node
assert custom_node.node_type is Custom_Node

# Should reject invalid types
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'>'"

# Test with more complex case (like Schema__MGraph__Node)
from typing import Dict, Any

class Node_Config(Type_Safe):
node_id: Random_Guid

class Complex_Node(Type_Safe):
attributes : Dict[Random_Guid, str] # Simplified for test
node_config : Node_Config
node_type : Type['Complex_Node'] # ForwardRef
value : Any

class Custom_Complex(Complex_Node): pass

# Both should work now
complex_node = Complex_Node()
custom_complex = Custom_Complex() # Should not raise TypeError

# And type checking should work properly
with self.assertRaises(ValueError):
custom_complex.node_type = Complex_Node # Doesn't Allow base class
custom_complex.node_type = Custom_Complex # Allow self

with self.assertRaises(ValueError):
custom_complex.node_type = Other_Class # Reject invalid type

def test__regression__forward_ref_in_subclass_fails(self):
class Base_Node(Type_Safe):
node_type: Type['Base_Node'] # Forward reference to self
value : str

base_node = Base_Node() # This works fine
assert base_node.node_type is Base_Node

class Custom_Node(Base_Node): pass # But subclassing causes a TypeError



# with pytest.raises(TypeError, match="Invalid type for attribute 'node_type'. Expected 'None' but got '<class 'typing.ForwardRef'>'"):
# Custom_Node() # Fixed; BUG: This fails with TypeError
assert Custom_Node().node_type == Custom_Node

class Node_Config(Type_Safe): # To demonstrate more complex case (like in Schema__MGraph__Node)
node_id: Random_Guid

class Complex_Node(Type_Safe):
attributes : Dict[Random_Guid, str] # Simplified for test
node_config: Node_Config
node_type : Type['Complex_Node'] # ForwardRef that causes issues
value : Any

complex_node = Complex_Node() # Base class works
assert complex_node.node_type is Complex_Node

class Custom_Complex_Node(Complex_Node): pass # But custom subclass fails
# with pytest.raises(TypeError, match="Invalid type for attribute 'node_type'. Expected 'None' but got '<class 'typing.ForwardRef'>'"):
# Custom_Complex_Node() # Fixed; BUG This raises TypeError

assert Custom_Complex_Node().node_type == Custom_Complex_Node # node_type is the parent class, in this case Custom_Complex_Node




def test__regression__type_annotations_with_forward_ref(self):
class An_Class_1(Type_Safe):
an_type__forward_ref: Type['An_Class_1'] # Forward reference to self
an_type__direct: Type[Type_Safe] # Direct reference for comparison

# with pytest.raises(TypeError, match=re.escape("Invalid type for attribute 'an_type__forward_ref'. Expected 'typing.Type[ForwardRef('An_Class_1')]' but got '<class 'typing.ForwardRef'>'")):
# test_class = An_Class_1() # Fixed BUG should not have raised
test_class = An_Class_1()
assert test_class.an_type__forward_ref is An_Class_1
assert test_class.an_type__direct is Type_Safe

assert test_class.__annotations__['an_type__forward_ref'] == Type[ForwardRef('An_Class_1')] # Confirm forward ref is correct
assert test_class.__annotations__['an_type__direct' ] == Type[Type_Safe] # Confirm direct ref is correct
#
# The bug manifests when trying to set default values for these types
#assert test_class.an_type__forward_ref is None # Fixed BUG: This fails with TypeError
assert test_class.an_type__forward_ref is An_Class_1
assert test_class.an_type__direct is Type_Safe # Direct reference works fine


def test__regression__type_annotations_default_to_none(self):
class Schema__Base(Type_Safe): pass # Define base class

class Schema__Default__Types(Type_Safe):
base_type: Type[Schema__Base] # Type annotation that should default to Schema__Base

defaults = Schema__Default__Types()

assert defaults.__annotations__ == {'base_type': Type[Schema__Base]} # Confirm annotation is correct
#assert defaults.base_type is None # Fixed BUG: This should be Schema__Base instead of None
assert defaults.base_type is Schema__Base
assert type(defaults.__class__.__annotations__['base_type']) == type(Type[Schema__Base])

# Also test in inheritance scenario to be thorough
class Schema__Child(Schema__Default__Types):
child_type: Type[Schema__Base]

child = Schema__Child()
assert all_annotations(child) == {'base_type' : Type[Schema__Base],
'child_type': Type[Schema__Base]} # Confirm both annotations exist
#assert child.base_type is None # Fixed BUG: Should be Schema__Base
#assert child.child_type is None # Fixed BUG: Should be Schema__Base
assert child.base_type is Schema__Base
assert child.child_type is Schema__Base

def test__regression__forward_refs_in_type(self):
class An_Class_1(Type_Safe):
an_type__str : Type[str]
an_type__forward_ref: Type['An_Class_1']

an_class = An_Class_1()
assert an_class.obj() == __(an_type__str=None, an_type__forward_ref=None)
assert an_class.an_type__str is str
assert an_class.an_type__forward_ref is An_Class_1
assert an_class.json() == { 'an_type__forward_ref': 'test_Type_Safe__regression.An_Class_1' ,
'an_type__str' : 'builtins.str' }
assert an_class.obj() == __(an_type__str='builtins.str', an_type__forward_ref='test_Type_Safe__regression.An_Class_1')

an_class.an_type__str = str
an_class.an_type__str = Random_Guid
Expand All @@ -55,8 +200,8 @@ class An_Class(Type_Safe):
# with pytest.raises(TypeError, match="Subscripted generics cannot be used with class and instance checks"):
# An_Class() # FXIED BUG

assert An_Class().obj() == __(an_guid = 'osbot_utils.helpers.Guid.Guid',
an_time_stamp = None )
assert An_Class().obj() == __(an_guid = 'osbot_utils.helpers.Guid.Guid' ,
an_time_stamp = 'osbot_utils.helpers.Timestamp_Now.Timestamp_Now' )

def test__regression__type_from_json(self):
class An_Class(Type_Safe):
Expand Down Expand Up @@ -131,8 +276,8 @@ class An_Class(Type_Safe):
an_type_int: Type[int]

an_class = An_Class()
assert an_class.an_type_str is None
assert an_class.an_type_int is None
assert an_class.an_type_str is str
assert an_class.an_type_int is int
an_class.an_type_str = str
an_class.an_type_int = int

Expand Down
8 changes: 4 additions & 4 deletions tests/unit/type_safe/test_Type_Safe.py
Original file line number Diff line number Diff line change
Expand Up @@ -922,7 +922,7 @@ class An_Typing_Class(Type_Safe):
assert default_value(round_trip.an_list) == []
assert default_value(round_trip.an_set ) == set()

def test__type__with_type__are_enforced(self):
def test__type__with_type__are_enforced__and_default_is_type(self):
class An_Class(Type_Safe):
an_type_str: Type[str]
an_type_int: Type[int]
Expand All @@ -934,8 +934,8 @@ class Timestamp_Now__Extra(Timestamp_Now):
pass

an_class = An_Class()
assert an_class.an_type_str is None
assert an_class.an_type_int is None
assert an_class.an_type_str is str
assert an_class.an_type_int is int
an_class.an_type_str = str
an_class.an_type_int = int
an_class.an_type_str = Guid
Expand All @@ -948,7 +948,7 @@ class An_Class_1(Type_Safe):
an_guid : Type[Guid]
an_time_stamp: Type[Timestamp_Now]

assert An_Class_1().json() == {'an_guid': None, 'an_time_stamp': None}
assert An_Class_1().json() == {'an_guid': 'osbot_utils.helpers.Guid.Guid', 'an_time_stamp': 'osbot_utils.helpers.Timestamp_Now.Timestamp_Now'}



Expand Down

0 comments on commit ecf03f4

Please sign in to comment.