Skip to content

Commit 1f2d291

Browse files
committed
Merge dev into main
2 parents 1e7b74a + c86aa59 commit 1f2d291

File tree

11 files changed

+362
-32
lines changed

11 files changed

+362
-32
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# OSBot-Utils
22

3-
![Current Release](https://img.shields.io/badge/release-v3.50.0-blue)
3+
![Current Release](https://img.shields.io/badge/release-v3.50.1-blue)
44
![Python](https://img.shields.io/badge/python-3.8+-green)
55
![Type-Safe](https://img.shields.io/badge/Type--Safe-✓-brightgreen)
66
![Caching](https://img.shields.io/badge/Caching-Built--In-orange)

osbot_utils/helpers/cache_on_self/Cache_Storage.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from weakref import WeakKeyDictionary
2-
from typing import Any, Dict, List
2+
from typing import Any, List
33

44
class Cache_Storage: # Handles all cache storage without polluting instance attributes
55

osbot_utils/type_safe/type_safe_core/shared/Type_Safe__Validation.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,14 @@ def check_if__type_matches__obj_annotation__for_attr(self, target, attr_name, va
208208
if type_arg == value:
209209
return True
210210
if isinstance(type_arg, (str, ForwardRef)): # Handle forward reference
211-
type_arg = target.__class__ # If it's a forward reference, the target class should be the containing class
211+
forward_name = type_arg.__forward_arg__ if isinstance(type_arg, ForwardRef) else type_arg
212+
resolved_type = None # Walk MRO to find the actual class
213+
for base_cls in target.__class__.__mro__:
214+
if base_cls.__name__ == forward_name:
215+
resolved_type = base_cls
216+
break
217+
type_arg = resolved_type if resolved_type else target.__class__ # Fallback to instance class if not found
218+
212219
return isinstance(value, type) and issubclass(value, type_arg) # Check that value is a type and is subclass of type_arg
213220
else:
214221
return isinstance(value, type)

osbot_utils/type_safe/type_safe_core/steps/Type_Safe__Step__Class_Kwargs.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
from typing import Dict, Any, Type
2-
3-
from osbot_utils.type_safe.Type_Safe__Primitive import Type_Safe__Primitive
4-
from osbot_utils.type_safe.primitives.domains.identifiers.Obj_Id import Obj_Id
5-
from osbot_utils.type_safe.primitives.domains.identifiers.Random_Guid import Random_Guid
2+
from osbot_utils.type_safe.Type_Safe__Primitive import Type_Safe__Primitive
63
from osbot_utils.type_safe.type_safe_core.shared.Type_Safe__Cache import type_safe_cache, Type_Safe__Cache
74
from osbot_utils.type_safe.type_safe_core.shared.Type_Safe__Shared__Variables import IMMUTABLE_TYPES
85
from osbot_utils.type_safe.type_safe_core.shared.Type_Safe__Validation import type_safe_validation
@@ -100,15 +97,23 @@ def handle_defined_var(self, base_cls : Type ,
10097
type_safe_validation.validate_variable_type(base_cls, var_name, var_type, var_value)
10198
type_safe_validation.validate_type_immutability(var_name, var_type)
10299

103-
def process_annotation(self, cls : Type , # Process single annotation
100+
def process_annotation(self, cls : Type ,
104101
base_cls : Type ,
105102
kwargs : Dict[str, Any] ,
106103
var_name : str ,
107-
var_type : Type )\
108-
-> None:
109-
if not hasattr(base_cls, var_name): # Handle undefined variables
104+
var_type : Type ):
105+
106+
class_declares_annotation = var_name in getattr(base_cls, '__annotations__', {})
107+
108+
if not hasattr(base_cls, var_name):
110109
self.handle_undefined_var(cls, kwargs, var_name, var_type)
111-
else: # Handle defined variables
110+
elif class_declares_annotation and base_cls is cls:
111+
origin = type_safe_cache.get_origin(var_type) # Only recalculate default for Type[T] annotations
112+
if origin is type:
113+
self.handle_undefined_var(cls, kwargs, var_name, var_type)
114+
else:
115+
self.handle_defined_var(base_cls, var_name, var_type)
116+
else:
112117
self.handle_defined_var(base_cls, var_name, var_type)
113118

114119
def process_annotations(self, cls : Type , # Process all annotations
@@ -119,6 +124,7 @@ def process_annotations(self, cls : Type ,
119124
for var_name, var_type in type_safe_cache.get_class_annotations(base_cls):
120125
self.process_annotation(cls, base_cls, kwargs, var_name, var_type)
121126

127+
122128
def process_mro_class(self, base_cls : Type , # Process class in MRO chain
123129
kwargs : Dict[str, Any] )\
124130
-> None:

osbot_utils/version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v3.50.0
1+
v3.50.1

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "osbot_utils"
3-
version = "v3.50.0"
3+
version = "v3.50.1"
44
description = "OWASP Security Bot - Utils"
55
authors = ["Dinis Cruz <[email protected]>"]
66
license = "MIT"

tests/unit/type_safe/primitives/domains/identifiers/test_Obj_Id.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
from unittest import TestCase
1+
import re
22
import pytest
3+
from unittest import TestCase
34
from osbot_utils.helpers.duration.decorators.capture_duration import capture_duration
45
from osbot_utils.testing.__ import __
56
from osbot_utils.testing.performance.Performance_Measure__Session import Performance_Measure__Session
@@ -119,7 +120,8 @@ def test__new_obj_id__format(self):
119120

120121
assert len(obj_id) == 8
121122
assert is_obj_id(obj_id) is True
122-
assert obj_id.islower() is True # Always lowercase
123+
assert re.fullmatch(r"[a-z0-9]{8}", obj_id) # Always lowercase
124+
#assert obj_id.islower() is True # we can't use this , since when obj_id is all numbers, islower returns false
123125

124126
def test__new_obj_id__uniqueness(self): # Test new_obj_id generates unique values
125127
ids = [new_obj_id() for _ in range(1000)]

tests/unit/type_safe/test_Type_Safe.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1085,17 +1085,6 @@ def label(self, value):
10851085
test_class.data = 123 # confirm that type safety is still working on the main class
10861086

10871087
def test_validate_type_immutability(self): # Tests type immutability validation
1088-
# class Simple_Type(Type_Safe):
1089-
# valid_int : int = 42 # valid immutable type
1090-
# valid_str : str = 'abc' # valid immutable type
1091-
# valid_bool : bool = True # valid immutable type
1092-
# valid_tuple : tuple = (1,2) # valid immutable type
1093-
#
1094-
# simple = Simple_Type() # Should work fine with valid types
1095-
# assert simple.valid_int == 42
1096-
# assert simple.valid_str == 'abc'
1097-
# assert simple.valid_bool == True
1098-
# assert simple.valid_tuple == (1,2)
10991088

11001089
with pytest.raises(ValueError, match= "variable 'invalid_list' is defined as type '<class 'list'>' which is not supported by Type_Safe" ): # Test invalid mutable type
11011090
class Invalid_Type(Type_Safe):

tests/unit/type_safe/type_safe_core/_bugs/test_Type_Safe__bugs.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import re
12
import pytest
3+
from typing import Type
24
from unittest import TestCase
35
from osbot_utils.type_safe.Type_Safe import Type_Safe
46

@@ -69,3 +71,31 @@ def __init__(self): # this will make the __annotations__ to
6971
assert an_class.an_str == 'new_value'
7072
assert an_class.an_bool == False
7173

74+
def test__bug__type_annotation__non_none_parent_default(self):
75+
# What happens when parent has a non-None default?
76+
# This combines BOTH bugs:
77+
# 1. Subclass inherits parent's value (Base_Handler) instead of auto-assigning Extended_Handler
78+
# 2. Then validation fails because Base_Handler is not a subclass of Extended_Handler
79+
80+
class Base_Handler(Type_Safe):
81+
pass
82+
83+
class Extended_Handler(Base_Handler):
84+
pass
85+
86+
class Base_Config(Type_Safe):
87+
handler_type: Type[Base_Handler] = Base_Handler # Non-None default
88+
89+
class Extended_Config(Base_Config):
90+
handler_type: Type[Extended_Handler] # Re-declare with more specific type
91+
92+
# Parent default is Base_Handler
93+
with Base_Config() as _:
94+
assert _.handler_type is Base_Handler # Correct
95+
96+
# BUG: Subclass inherits parent's value (Base_Handler), then validation fails
97+
# because Base_Handler is not a subclass of Extended_Handler
98+
error_message = "On Extended_Config, invalid type for attribute 'handler_type'. Expected 'typing.Type["
99+
with pytest.raises(ValueError, match=re.escape(error_message)):
100+
Extended_Config() # BUG: should auto-assign Extended_Handler
101+

0 commit comments

Comments
 (0)