Skip to content

Commit bd2124d

Browse files
committed
Merge dev into main
2 parents a05114d + dd30fdb commit bd2124d

File tree

10 files changed

+149
-96
lines changed

10 files changed

+149
-96
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.42.0-blue)
3+
![Current Release](https://img.shields.io/badge/release-v3.42.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/type_safe/type_safe_core/decorators/type_safe.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,27 @@
55

66

77
def type_safe(func): # Main decorator function
8-
type_checker = Type_Safe__Method(func) # Create type checker instance
8+
type_checker = Type_Safe__Method(func).setup() # Create type checker instance
99
return_type = func.__annotations__.get('return')
1010

1111
validator = Type_Safe__Base() if return_type else None
1212
has_only_self = len(type_checker.params) == 1 and type_checker.params[0] == 'self' # Check if method has only 'self' parameter or no parameters
1313
has_no_params = len(type_checker.params) == 0
1414
direct_execution = has_no_params or has_only_self # these are major performance optimisation where this @type_safe had an overhead of 250x (even on methods with no params) to now having an over head of ~5x
15+
has_var_keyword = type_checker.var_keyword_param is not None # Pre-calculate if we need to handle VAR_KEYWORD parameters (performance optimization)
16+
1517

1618
@functools.wraps(func) # Preserve function metadata
1719
def wrapper(*args, **kwargs): # Wrapper function
1820
if direct_execution:
1921
result = func(*args, **kwargs)
2022
else:
2123
bound_args = type_checker.handle_type_safety(args, kwargs) # Validate type safety
22-
result = func(**bound_args.arguments) # Call original function
24+
if has_var_keyword: # Only call prepare_function_arguments if needed
25+
regular_args, var_kwargs = type_checker.prepare_function_arguments(bound_args)
26+
result = func(**regular_args, **var_kwargs)
27+
else:
28+
result = func(**bound_args.arguments)
2329

2430
if return_type is not None and result is not None: # Validate return type using existing type checking infrastructure
2531
if isinstance(return_type, type) and issubclass(return_type, Type_Safe__Primitive): # Try to convert Type_Safe__Primitive types

osbot_utils/type_safe/type_safe_core/methods/Type_Safe__Method.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,20 @@
1010

1111
class Type_Safe__Method: # Class to handle method type safety validation
1212
def __init__(self, func): # Initialize with function
13-
self.func = func # Store original function
14-
self.sig = inspect.signature(func) # Get function signature
15-
self.annotations = func.__annotations__ # Get function annotations
16-
self.params = list(self.sig.parameters.keys())
13+
self.func = func # Store original function
14+
self.sig = None # Get function signature
15+
self.annotations = None # Get function annotations
16+
self.params = None
17+
self.var_keyword_param = None
18+
19+
def setup(self):
20+
self.sig = inspect.signature(self.func) # Get function signature
21+
self.annotations = self.func.__annotations__ # Get function annotations
22+
self.params = list(self.sig.parameters.keys())
23+
self.var_keyword_param = next((name for name, param in self.sig.parameters.items() # Pre-calculate VAR_KEYWORD parameter name for performance optimization
24+
if param.kind is inspect.Parameter.VAR_KEYWORD),
25+
None)
26+
return self
1727

1828
def check_for_any_use(self):
1929
for param_name, type_hint in self.annotations.items():
@@ -72,6 +82,19 @@ def bind_args(self, args: tuple, kwargs: dict):
7282
bound_args.apply_defaults() # Apply default values
7383
return bound_args # Return bound arguments
7484

85+
def prepare_function_arguments(self, bound_args): # Prepare arguments for function call, handling VAR_KEYWORD parameters correctly
86+
87+
if self.var_keyword_param is None:
88+
return bound_args.arguments, {} # Optimized path if no **kwargs
89+
90+
regular_args = {k: v for k, v in bound_args.arguments.items()
91+
if k != self.var_keyword_param} # Exclude **kwargs param
92+
93+
var_kwargs = bound_args.arguments.get(self.var_keyword_param, {}) # Extract **kwargs
94+
95+
return regular_args, var_kwargs
96+
97+
7598
def validate_parameter(self, param_name: str, param_value: Any, bound_args): # Validate a single parameter
7699
self.validate_immutable_parameter(param_name, param_value) # Validata the param_value (make sure if it set it is on of IMMUTABLE_TYPES)
77100
if param_name in self.annotations: # Check if parameter is annotated

osbot_utils/version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v3.42.0
1+
v3.42.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.42.0"
3+
version = "v3.42.1"
44
description = "OWASP Security Bot - Utils"
55
authors = ["Dinis Cruz <[email protected]>"]
66
license = "MIT"

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

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -63,27 +63,3 @@ def create_openapi_spec(servers: List[Dict[str, str]]):
6363

6464

6565

66-
def test__bug__kwargs_not_properly_returned_in_type_safe(self):
67-
68-
class Test_Class(Type_Safe):
69-
@type_safe
70-
def method_with_kwargs(self, name: str, **kwargs): # The bug is caused by the non handling correctly of the **kwargs parameter
71-
return {"name": name, "kwargs": kwargs} # We expect kwargs to contain all extra parameters
72-
73-
test_obj = Test_Class()
74-
with self.assertRaises(ValueError) as context: # This works as expected - type safety catches the error
75-
test_obj.method_with_kwargs(name=b'123') # Wrong type for 'name'
76-
77-
assert str(context.exception) == "Parameter 'name' expected type <class 'str'>, but got <class 'bytes'>"
78-
79-
result = test_obj.method_with_kwargs(name="test", extra=True, another="value")
80-
expected = {"kwargs": {"extra": True, "another": "value"},
81-
"name" : "test",}
82-
current = {'kwargs': { "kwargs": {"extra": True, "another": "value"}}, # # BUG: there is an extra kwargs added to the return value
83-
"name" : "test",}
84-
85-
assert result != expected # BUG: This is what we expect
86-
assert result == current # BUG: This is what we get
87-
88-
89-

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def with_check(self):
9494
# ) -> None:
9595
# pass
9696
#
97-
# checker = Type_Safe__Method(simple_func)
97+
# checker = Type_Safe__Method(simple_func).setup()
9898
#
9999
# # Measure validation time
100100
# def validate():
@@ -118,7 +118,7 @@ def with_check(self):
118118
# ) -> None:
119119
# pass
120120
#
121-
# checker = Type_Safe__Method(complex_func)
121+
# checker = Type_Safe__Method(complex_func).setup()
122122
#
123123
# # Measure validation time with all parameters
124124
# def validate():
@@ -143,7 +143,7 @@ def with_check(self):
143143
# def list_func(items: List[int]) -> None:
144144
# pass
145145
#
146-
# checker = Type_Safe__Method(list_func)
146+
# checker = Type_Safe__Method(list_func).setup()
147147
#
148148
# for size in [10, 100, 1000]:
149149
# test_list = list(range(size))
@@ -161,7 +161,7 @@ def with_check(self):
161161
# def dict_func(data: Dict[str, int]) -> None:
162162
# pass
163163
#
164-
# checker = Type_Safe__Method(dict_func)
164+
# checker = Type_Safe__Method(dict_func).setup()
165165
#
166166
# for size in [10, 100, 1000]:
167167
# test_dict = {f"key{i}": i for i in range(size)}

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,3 +523,51 @@ def return_self(self) -> 'An_Class':
523523
# with pytest.raises(TypeError, match=re.escape(error_message)):
524524
# An_Class().return_self() # BUG
525525
assert type(An_Class().return_self()) is An_Class # FIXED
526+
527+
def test__regression__type_safe__decorator__params_value(self):
528+
529+
from osbot_utils.type_safe.type_safe_core.decorators.type_safe import type_safe
530+
531+
@type_safe
532+
def an_method( **params):
533+
return params
534+
535+
# assert an_method() == {'params': {}} # BUG
536+
# assert an_method(a=42) == {'params': {'a': 42}} # BUG
537+
assert an_method() == {}
538+
assert an_method(a=42) == {'a': 42}
539+
540+
@type_safe
541+
def an_method_2( **params_2):
542+
return params_2
543+
544+
#assert an_method_2() == {'params_2': {}} # BUG
545+
#assert an_method_2(a=42) == {'params_2': {'a': 42}} # BUG
546+
547+
assert an_method_2() == {}
548+
assert an_method_2(a=42) == {'a': 42}
549+
550+
551+
def test__regression__kwargs_not_properly_returned_in_type_safe(self):
552+
553+
class Test_Class(Type_Safe):
554+
@type_safe
555+
def method_with_kwargs(self, name: str, **kwargs): # The bug is caused by the non handling correctly of the **kwargs parameter
556+
return {"name": name, "kwargs": kwargs} # We expect kwargs to contain all extra parameters
557+
558+
test_obj = Test_Class()
559+
with self.assertRaises(ValueError) as context: # This works as expected - type safety catches the error
560+
test_obj.method_with_kwargs(name=b'123') # Wrong type for 'name'
561+
562+
assert str(context.exception) == "Parameter 'name' expected type <class 'str'>, but got <class 'bytes'>"
563+
564+
result = test_obj.method_with_kwargs(name="test", extra=True, another="value")
565+
expected = {"kwargs": {"extra": True, "another": "value"},
566+
"name" : "test",}
567+
with_bug = {'kwargs': { "kwargs": {"extra": True, "another": "value"}}, # # BUG: there is an extra kwargs added to the return value
568+
"name" : "test",}
569+
570+
# assert result != expected # BUG: This is what we expect
571+
# assert result == current # BUG: This is what we get
572+
assert result == expected
573+
assert result != with_bug

0 commit comments

Comments
 (0)