Skip to content

Commit c2ba7f6

Browse files
committed
couple edge case fixes with @type_safe decorator
1 parent 92d1ae7 commit c2ba7f6

File tree

4 files changed

+99
-1
lines changed

4 files changed

+99
-1
lines changed

osbot_utils/type_safe/type_safe_core/methods/Type_Safe__Method.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,17 @@ def validate_list_type(self, param_name: str,
195195
if not callable(item): # Check item is callable
196196
raise ValueError(f"List item at index {i} expected callable but got {type(item)}") # Raise error if not callable
197197
# Note: Full signature validation would require is_callable_compatible method
198+
elif item_origin is type: # Handle Type[T] items (e.g., List[Type[BaseClass]])
199+
type_args = get_args(item_type) # Get T from Type[T]
200+
for i, item in enumerate(param_value): # Validate each type in list
201+
if not isinstance(item, type): # Check item is actually a type/class
202+
raise ValueError(f"List item at index {i} expected a type/class but got {type(item).__name__}")
203+
if type_args: # If Type[T] has type argument (e.g., Type[BaseClass])
204+
required_base = type_args[0] # Get the required base type
205+
if isinstance(required_base, type): # Only validate if we have a concrete type
206+
if not issubclass(item, required_base): # Check item is subclass of T
207+
raise ValueError(f"List item at index {i} expected subclass of {required_base.__name__}, "
208+
f"but got {item.__name__}")
198209
elif item_origin is not None: # Handle other subscripted types
199210
raise NotImplementedError(f"Validation for list items with subscripted type"
200211
f" '{item_type}' is not yet supported "

osbot_utils/type_safe/type_safe_core/steps/Type_Safe__Step__From_Json.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,9 @@ def convert_item_to_type(self, expected_type, item):
157157
else:
158158
return expected_type(**item)
159159
else:
160+
if isinstance(type_safe_cache.get_origin(expected_type), type):
161+
resolved_type = self.deserialize_type__using_value(item)
162+
return resolved_type
160163
return expected_type(item)
161164

162165
def handle_special_types(self, _self, key, annotation, value): # Handle special types like enums and Type_Safe__Primitive subclasses."""

tests/unit/type_safe/type_safe_core/decorators/test__decorator__type_safe__bugs.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,6 @@ def create_openapi_spec(servers: List[Dict[str, str]]):
116116

117117

118118

119+
120+
121+

tests/unit/type_safe/type_safe_core/decorators/test__decorator__type_safe__regression.py

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -570,4 +570,85 @@ def method_with_kwargs(self, name: str, **kwargs): # The bug is caused
570570
# assert result != expected # BUG: This is what we expect
571571
# assert result == current # BUG: This is what we get
572572
assert result == expected
573-
assert result != with_bug
573+
assert result != with_bug
574+
575+
def test__regression__list_of_type_T__not_supported(self): # Document bug where List[Type[SomeClass]] fails validation in @type_safe decorator
576+
577+
# Define a base class that we want to use in Type[T]
578+
class Base_Handler(Type_Safe):
579+
name: str = "base"
580+
581+
class Handler_A(Base_Handler):
582+
name: str = "handler_a"
583+
584+
class Handler_B(Base_Handler):
585+
name: str = "handler_b"
586+
587+
# Define a class with a @type_safe decorated method that takes List[Type[Base_Handler]]
588+
class Executor(Type_Safe):
589+
590+
@type_safe
591+
def execute(self,
592+
handler_classes: List[Type[Base_Handler]], # This is the problematic type
593+
name: str = "test"
594+
) -> int:
595+
return len(handler_classes)
596+
597+
# BUG: This should work but raises NotImplementedError
598+
with Executor() as executor:
599+
# Empty list should work
600+
#with pytest.raises(NotImplementedError, match="Validation for list items with subscripted type"):
601+
# executor.execute(handler_classes=[]) # BUG: fails even with empty list
602+
assert executor.execute(handler_classes=[]) == 0 # FIXED
603+
604+
# List with valid classes should work
605+
# with pytest.raises(NotImplementedError, match="Validation for list items with subscripted type"):
606+
# executor.execute(handler_classes=[Handler_A, Handler_B]) # BUG: fails
607+
assert executor.execute(handler_classes=[Handler_A, Handler_B]) == 2 # FIXED
608+
609+
def test__regression__list_of_type_T__expected_behavior(self): # Document what SHOULD happen when bug is fixed
610+
611+
class Base_Handler(Type_Safe):
612+
name: str = "base"
613+
614+
class Handler_A(Base_Handler):
615+
name: str = "handler_a"
616+
617+
class Handler_B(Base_Handler):
618+
name: str = "handler_b"
619+
620+
class Unrelated_Class(Type_Safe):
621+
value: int = 0
622+
623+
class Executor(Type_Safe):
624+
625+
@type_safe
626+
def execute(self,
627+
handler_classes: List[Type[Base_Handler]],
628+
name: str = "test"
629+
) -> int:
630+
return len(handler_classes)
631+
632+
executor = Executor()
633+
634+
# When fixed, these should be the expected behaviors:
635+
#
636+
# 1. Empty list should work:
637+
assert executor.execute(handler_classes=[]) == 0 # Should return 0
638+
#
639+
# 2. List with valid subclasses should work:
640+
assert executor.execute(handler_classes=[Handler_A, Handler_B]) == 2 # Should return 2
641+
642+
# 3. List with base class itself should work:
643+
assert executor.execute(handler_classes=[Base_Handler]) == 1 # Should return 1
644+
645+
#
646+
# 4. List with non-subclass should raise ValueError:
647+
error_message_1 = "List item at index 0 expected subclass of Base_Handler, but got Unrelated_Class"
648+
with pytest.raises(ValueError,match=error_message_1):
649+
executor.execute(handler_classes=[Unrelated_Class])
650+
651+
# 5. List with non-type should raise ValueError:
652+
error_message_2 = "List item at index 0 expected a type/class but got Handler_A"
653+
with pytest.raises(ValueError,match=error_message_2):
654+
executor.execute(handler_classes=[Handler_A()]) # instance, not class

0 commit comments

Comments
 (0)