Skip to content

Commit 0ba8e4d

Browse files
committed
Merge dev into main
2 parents a4a0747 + 01755c6 commit 0ba8e4d

File tree

9 files changed

+367
-13
lines changed

9 files changed

+367
-13
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.40.0-blue)
3+
![Current Release](https://img.shields.io/badge/release-v3.40.2-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__Base.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,9 @@ def is_instance_of_type(self, item, expected_type):
122122

123123
def try_convert(self, value, expected_type): # Try to convert value to expected type using Type_Safe conversion logic.
124124

125-
from osbot_utils.type_safe.Type_Safe import Type_Safe
126-
from osbot_utils.type_safe.Type_Safe__Primitive import Type_Safe__Primitive
125+
from osbot_utils.type_safe.Type_Safe import Type_Safe
126+
from osbot_utils.type_safe.Type_Safe__Primitive import Type_Safe__Primitive
127+
from osbot_utils.type_safe.type_safe_core.collections.Type_Safe__Dict import Type_Safe__Dict
127128

128129
if expected_type is Any: # Handle Any type
129130
return value
@@ -135,12 +136,16 @@ def try_convert(self, value, expected_type): # Try to convert value to expect
135136
if isinstance(value, expected_type): # If already correct type, return as-is
136137
return value
137138

138-
139139
if (isinstance(expected_type, type) and # Handle dict → Type_Safe conversion
140140
issubclass(expected_type, Type_Safe) and
141141
isinstance(value, dict)):
142142
return expected_type.from_json(value)
143143

144+
if (isinstance(expected_type, type) and # Handle dict → Type_Safe__Dict conversion (not working as expected)
145+
issubclass(expected_type, Type_Safe__Dict) and
146+
isinstance(value, dict)):
147+
return expected_type(value)
148+
144149
if (isinstance(expected_type, type) and # Handle dict → Type_Safe__Primitive conversion
145150
issubclass(expected_type, Type_Safe__Primitive) and
146151
type(value) in [str, int, float]):

osbot_utils/type_safe/type_safe_core/shared/Type_Safe__Convert.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
from typing import get_args
2-
from osbot_utils.type_safe.type_safe_core.shared.Type_Safe__Cache import type_safe_cache
3-
from osbot_utils.utils.Objects import base_classes_names
1+
from typing import get_args
2+
from osbot_utils.type_safe.type_safe_core.shared.Type_Safe__Cache import type_safe_cache
3+
from osbot_utils.utils.Objects import base_classes_names
4+
from osbot_utils.type_safe.type_safe_core.collections.Type_Safe__Dict import Type_Safe__Dict
45

56

67
class Type_Safe__Convert:
@@ -14,6 +15,9 @@ def convert_dict_to_value_from_obj_annotation(self, target, attr_name, value):
1415
if len(args) == 2 and args[1] is type(None): # todo: find a better way to do this, since this is handling an edge case when origin_attr_type is Optional (which is an shorthand for Union[X, None] )
1516
attribute_annotation = args[0]
1617

18+
if isinstance(attribute_annotation, type) and issubclass(attribute_annotation, Type_Safe__Dict):
19+
return attribute_annotation(value) # Convert plain dict to subclass
20+
1721
if 'Type_Safe' in base_classes_names(attribute_annotation):
1822
return attribute_annotation(**value)
1923
return value

osbot_utils/type_safe/type_safe_core/shared/Type_Safe__Validation.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ def check_if__type_matches__obj_annotation__for_attr(self, target, attr_name, va
200200
if origin_attr_type is set:
201201
if type(value) is list:
202202
return True # if the attribute is a set and the value is a list, then they are compatible
203+
203204
if origin_attr_type is type: # Add handling for Type[T]
204205
type_args = get_args(attr_type)
205206
if type_args:

osbot_utils/type_safe/type_safe_core/steps/Type_Safe__Step__Set_Attr.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import get_origin, Annotated, get_args, Literal, Union
22
from osbot_utils.type_safe.Type_Safe__Primitive import Type_Safe__Primitive
3+
from osbot_utils.type_safe.type_safe_core.collections.Type_Safe__Dict import Type_Safe__Dict
34
from osbot_utils.type_safe.type_safe_core.collections.Type_Safe__List import Type_Safe__List
45
from osbot_utils.type_safe.type_safe_core.shared.Type_Safe__Cache import type_safe_cache
56
from osbot_utils.type_safe.type_safe_core.shared.Type_Safe__Convert import type_safe_convert
@@ -11,6 +12,8 @@ class Type_Safe__Step__Set_Attr:
1112
def resolve_value(self, _self, annotations, name, value):
1213
if type(value) is dict:
1314
value = self.resolve_value__dict(_self, name, value)
15+
elif isinstance(value, Type_Safe__Dict):
16+
value = self.convert_type_safe_dict(annotations, name, value)
1417
elif type(value) is list:
1518
value = self.resolve_value__list(_self, name, value)
1619
elif type(value) is tuple:
@@ -28,6 +31,18 @@ def resolve_value(self, _self, annotations, name, value):
2831
type_safe_validation.validate_type_compatibility(_self, annotations, name, value)
2932
return value
3033

34+
def convert_type_safe_dict(self, annotations, name, value): # Convert between Type_Safe__Dict subclasses
35+
36+
expected_type = annotations.get(name)
37+
38+
if not (isinstance(expected_type, type) and issubclass(expected_type, Type_Safe__Dict)):
39+
return value
40+
41+
if type(value) is expected_type:
42+
return value
43+
44+
return expected_type(dict(value)) # Create new instance - validation happens automatically
45+
3146
def resolve_value__dict(self, _self, name, value):
3247
return type_safe_convert.convert_dict_to_value_from_obj_annotation(_self, name, value)
3348

@@ -70,6 +85,7 @@ def resolve_value__tuple(self, _self, name, value): # Convert re
7085
return Type_Safe__Tuple(expected_types=args, items=value)
7186

7287
return value
88+
7389
def resolve_value__from_origin(self, value):
7490
#origin = type_safe_cache.get_origin(value) # todo: figure out why this is the only place that the type_safe_cache.get_origin doesn't work (due to WeakKeyDictionary key error on value)
7591
origin = get_origin(value)

osbot_utils/version

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

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.40.0"
3+
version = "v3.40.2"
44
description = "OWASP Security Bot - Utils"
55
authors = ["Dinis Cruz <[email protected]>"]
66
license = "MIT"

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

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import re
22
import pytest
3-
from typing import Dict, List, Any
4-
from unittest import TestCase
5-
from osbot_utils.type_safe.Type_Safe import Type_Safe
3+
from typing import Dict, List, Any
4+
from unittest import TestCase
5+
from osbot_utils.type_safe.Type_Safe import Type_Safe
6+
from osbot_utils.type_safe.primitives.domains.cryptography.safe_str.Safe_Str__Hash import Safe_Str__Hash
7+
from osbot_utils.type_safe.type_safe_core.collections.Type_Safe__Dict import Type_Safe__Dict
8+
69

710
class test_Type_Safe__Dict__bugs(TestCase):
811

@@ -32,3 +35,18 @@ class Schema__Order__Alternative(Type_Safe):
3235
with Schema__Order__Alternative() as alt_order:
3336
alt_order.items = [{'product': 'laptop', 'qty': 1}] # Works
3437
assert alt_order.items == [{'product': 'laptop', 'qty': 1}]
38+
39+
def test__bug__list_of_dict_subclasses_from_plain_dicts(self):
40+
from typing import List
41+
42+
class Hash_Mapping(Type_Safe__Dict):
43+
expected_key_type = Safe_Str__Hash
44+
expected_value_type = str
45+
46+
class Container(Type_Safe):
47+
mappings: List[Hash_Mapping]
48+
49+
error_message = "Invalid type for item: Expected 'Hash_Mapping', but got 'dict'"
50+
with pytest.raises(TypeError, match=re.escape(error_message)):
51+
container = Container(mappings=[{'abc1234567': 'first'},
52+
{'def4567890': 'second'}]) # BUG: Lists are currently not supported

0 commit comments

Comments
 (0)