Skip to content

Commit c159cd8

Browse files
committed
Merge dev into main
2 parents f4ef1c1 + 5790787 commit c159cd8

File tree

8 files changed

+780
-34
lines changed

8 files changed

+780
-34
lines changed

osbot_utils/helpers/generators/Generator_Manager.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import threading
2-
from _thread import RLock # Reentrant lock for thread-safe access
3-
from types import GeneratorType
4-
from typing import Dict # Typing imports for type hints
5-
from typing import Union
6-
from osbot_utils.type_safe.Type_Safe import Type_Safe # Type_Safe base class for type-safe attributes
7-
from osbot_utils.type_safe.primitives.domains.identifiers.Random_Guid import Random_Guid # Helper for generating unique IDs
8-
from osbot_utils.helpers.generators.Generator_Context_Manager import Generator_Context_Manager
9-
from osbot_utils.helpers.generators.Model__Generator_State import Model__Generator_State
10-
from osbot_utils.helpers.generators.Model__Generator_Target import Model__Generator_Target
11-
from osbot_utils.utils.Lists import list_group_by
2+
from _thread import RLock # Reentrant lock for thread-safe access
3+
from types import GeneratorType
4+
from typing import Dict # Typing imports for type hints
5+
from typing import Union
6+
from osbot_utils.type_safe.Type_Safe import Type_Safe # Type_Safe base class for type-safe attributes
7+
from osbot_utils.type_safe.primitives.domains.identifiers.Random_Guid import Random_Guid # Helper for generating unique IDs
8+
from osbot_utils.helpers.generators.Generator_Context_Manager import Generator_Context_Manager
9+
from osbot_utils.helpers.generators.Model__Generator_State import Model__Generator_State
10+
from osbot_utils.helpers.generators.Model__Generator_Target import Model__Generator_Target
11+
from osbot_utils.utils.Lists import list_group_by
1212

1313

1414
class Generator_Manager(Type_Safe): # Class for managing multiple generator targets

osbot_utils/type_safe/Type_Safe__Base.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +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
127+
125128
if expected_type is Any: # Handle Any type
126129
return value
127130

@@ -132,6 +135,17 @@ def try_convert(self, value, expected_type): # Try to convert value to expect
132135
if isinstance(value, expected_type): # If already correct type, return as-is
133136
return value
134137

138+
139+
if (isinstance(expected_type, type) and # Handle dict → Type_Safe conversion
140+
issubclass(expected_type, Type_Safe) and
141+
isinstance(value, dict)):
142+
return expected_type.from_json(value)
143+
144+
if (isinstance(expected_type, type) and # Handle dict → Type_Safe__Primitive conversion
145+
issubclass(expected_type, Type_Safe__Primitive) and
146+
type(value) in [str, int, float]):
147+
return expected_type(value)
148+
135149
if isinstance(expected_type, type) and type(value) in [str, int, float]: # For types that are subclasses of built-ins (like Safe_Id extends str)
136150
if issubclass(expected_type, type(value)): # Only convert if the value's type is a base class of expected_type. e.g., str → Safe_Id (ok), but not int → str (not ok)
137151
return expected_type(value) # convert value to expected_type

osbot_utils/type_safe/type_safe_core/collections/Type_Safe__Dict.py

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from enum import Enum
1+
from enum import Enum
22
from typing import Type
33
from osbot_utils.testing.__ import __
44
from osbot_utils.type_safe.Type_Safe__Base import Type_Safe__Base
@@ -8,11 +8,31 @@
88

99

1010
class Type_Safe__Dict(Type_Safe__Base, dict):
11-
def __init__(self, expected_key_type, expected_value_type, *args, **kwargs):
12-
super().__init__(*args, **kwargs)
11+
expected_key_type : Type = None # Class-level defaults
12+
expected_value_type : Type = None
1313

14-
self.expected_key_type = expected_key_type
15-
self.expected_value_type = expected_value_type
14+
def __init__(self, expected_key_type=None, expected_value_type=None, initial_data=None, **kwargs):
15+
super().__init__() # Initialize empty dict first
16+
17+
18+
if isinstance(expected_key_type, dict) and expected_value_type is None: # Smart detection: if expected_key_type is a dict and expected_value_type is None, then the user is trying to pass initial data in the old style
19+
initial_data = expected_key_type # They're using the pattern: Hash_Mapping({'key': 'value'})
20+
expected_key_type = None # Move the dict to initial_data
21+
22+
self.expected_key_type = expected_key_type or self.__class__.expected_key_type # Use provided types, or fall back to class-level attributes
23+
self.expected_value_type = expected_value_type or self.__class__.expected_value_type
24+
25+
if self.expected_key_type is None or self.expected_value_type is None: # Validate that we have types set (either from args or class)
26+
raise ValueError(f"{self.__class__.__name__} requires expected_key_type and expected_value_type")
27+
28+
if initial_data is not None: # Process initial data through our type-safe __setitem__
29+
if not isinstance(initial_data, dict):
30+
raise TypeError(f"Initial data must be a dict, got {type(initial_data).__name__}")
31+
for key, value in initial_data.items():
32+
self[key] = value # Goes through __setitem__ with validation
33+
34+
for key, value in kwargs.items(): # Also handle keyword arguments (e.g., Hash_Mapping(key1='val1', key2='val2'))
35+
self[key] = value
1636

1737
def __contains__(self, key):
1838
if super().__contains__(key): # First try direct lookup
@@ -105,3 +125,20 @@ def obj(self) -> __:
105125

106126
def values(self) -> Type_Safe__List:
107127
return Type_Safe__List(self.expected_value_type, super().values())
128+
129+
def update(self, other=None, **kwargs):
130+
"""Override update to ensure type safety through __setitem__"""
131+
# Handle dict-like object or iterable of key-value pairs
132+
if other is not None:
133+
if hasattr(other, 'items'):
134+
# Dict-like object
135+
for key, value in other.items():
136+
self[key] = value # Goes through __setitem__
137+
else:
138+
# Iterable of (key, value) pairs
139+
for key, value in other:
140+
self[key] = value
141+
142+
# Handle keyword arguments
143+
for key, value in kwargs.items():
144+
self[key] = value

osbot_utils/type_safe/type_safe_core/steps/Type_Safe__Step__From_Json.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ def deserialize_attribute(self, _self, key, value):
4949
return self.deserialize_type__using_value(value)
5050

5151
if value is not None and type(value) is dict: # Handle forward references first
52+
53+
if isinstance(annotation, type) and issubclass(annotation, Type_Safe__Dict): # Handle Type_Safe__Dict subclasses BEFORE forward references
54+
if hasattr(annotation, 'expected_key_type') and hasattr(annotation, 'expected_value_type'): # Get the expected types from the subclass
55+
if annotation.expected_key_type and annotation.expected_value_type:
56+
dict_instance = annotation(value) # Create instance and populate it
57+
return dict_instance
5258
forward_ref_result = self.handle_forward_references(_self, annotation, value)
5359
if forward_ref_result is not None:
5460
return forward_ref_result

tests/unit/helpers/generators/test_Generator_Manager.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import pytest
22
import _thread
33
import threading
4-
from types import GeneratorType
5-
from unittest import TestCase
6-
from threading import Thread, Event
7-
from time import sleep
8-
from osbot_utils.type_safe.primitives.domains.identifiers.Random_Guid import Random_Guid
9-
from osbot_utils.helpers.generators.Generator_Context_Manager import Generator_Context_Manager
10-
from osbot_utils.helpers.generators.Generator_Manager import Generator_Manager
11-
from osbot_utils.helpers.generators.Model__Generator_State import Model__Generator_State
12-
from osbot_utils.utils.Env import not_in_github_action
13-
from osbot_utils.utils.Misc import is_guid
4+
from types import GeneratorType
5+
from unittest import TestCase
6+
from threading import Thread, Event
7+
from time import sleep
8+
from osbot_utils.testing.Pytest import skip_if_in_github_action
9+
from osbot_utils.type_safe.primitives.domains.identifiers.Random_Guid import Random_Guid
10+
from osbot_utils.helpers.generators.Generator_Context_Manager import Generator_Context_Manager
11+
from osbot_utils.helpers.generators.Generator_Manager import Generator_Manager
12+
from osbot_utils.helpers.generators.Model__Generator_State import Model__Generator_State
13+
from osbot_utils.utils.Env import not_in_github_action
14+
from osbot_utils.utils.Misc import is_guid
1415

1516

1617
class test_Generator_Manager(TestCase):
@@ -206,6 +207,7 @@ def long_generator():
206207
assert self.manager.find_generator(gen).state == Model__Generator_State.COMPLETED
207208

208209
def test_capture_with_concurrent_stop(self):
210+
skip_if_in_github_action() # failed in non-deterministic way in GH actions
209211
stop_event = Event()
210212
values = []
211213

tests/unit/type_safe/type_safe_core/_regression/test_Type_Safe__regression.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -642,9 +642,12 @@ class An_Class_2_A(Type_Safe):
642642
'an_class_2_b': {'an_str': 'value_1'}}
643643

644644
assert An_Class_2_A.from_json(json_data_2).json() == json_data_2
645-
new_error = "Expected 'An_Class_2_B', but got 'dict'"
646-
with pytest.raises(TypeError, match=re.escape(new_error)):
647-
An_Class_2_A(**json_data_2)
645+
646+
assert An_Class_2_A(**json_data_2).obj() == __(an_dict=__(key_1=__(an_str='value_1')),
647+
an_class_2_b=__(an_str='value_1')) # and now this works
648+
649+
assert An_Class_2_A(**json_data_2).obj () == An_Class_2_A.from_json(json_data_2).obj() # and these now match
650+
assert An_Class_2_A(**json_data_2).json() == json_data_2
648651

649652
# so the code below started to fail after the fix to the dict object assissment (see above ""Expected 'An_Class_2_B', but got 'dict'"")
650653
# this feels ok since we should be using from_json to create new nester objects and not **json_data_2
@@ -1510,10 +1513,8 @@ class An_Class_2_A(Type_Safe):
15101513
assert type(an_class_from_json.an_dict['key_1']) is An_Class_2_B
15111514
assert an_class_from_json.an_dict['key_1'].an_str == 'value_1'
15121515

1513-
expected_error_1 = "Expected 'An_Class_2_B', but got 'dict'"
1514-
with pytest.raises(TypeError, match=re.escape(expected_error_1)): # this doesn't work
1515-
An_Class_2_A(**an_class_json) # but that is expected behavior
1516-
#an_class_2b = An_Class_2_B()
1516+
assert An_Class_2_A(**an_class_json).json() == an_class_json
1517+
15171518
an_class_data = {'an_dict': {'key_1': An_Class_2_B(**{'an_str': 'value_1'})}} # since this is how it needs to be done, which works
15181519
an_class_from_kwargs = An_Class_2_A(**an_class_data) # the idea is if a pure json import is needed,
15191520
assert an_class_from_kwargs.json() == an_class_json # then .from_json(..) is what should be used

tests/unit/type_safe/type_safe_core/collections/test_Type_Safe__Dict.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,19 @@ class An_Class(Type_Safe):
222222

223223
an_class = An_Class()
224224
an_class.an_dict_custom['ok'] = CustomType(a=1, b='abc')
225-
with pytest.raises(TypeError, match="Expected 'CustomType', but got 'dict'"):
226-
an_class.an_dict_custom['fail'] = {'a': 2, 'b': 'def'}
225+
226+
assert an_class.obj() == __(an_dict_custom=__(ok=__(a=1, b='abc')))
227+
an_class.an_dict_custom['also_works'] = {'a': 2, 'b': 'def'}
228+
assert an_class.obj() == __(an_dict_custom=__(ok = __(a=1, b='abc'),
229+
also_works = __(a=2, b='def')))
230+
231+
error_message = "On CustomType, invalid type for attribute 'b'. Expected '<class 'str'>' but got '<class 'int'>'"
232+
with pytest.raises(ValueError, match=re.escape(error_message)):
233+
an_class.an_dict_custom['fail'] = {'a': 2, 'b': 42}
234+
235+
error_message = "Expected 'CustomType', but got 'str'"
236+
with pytest.raises(TypeError, match=re.escape(error_message)):
237+
an_class.an_dict_custom['fail'] = "42"
227238

228239
def test__dict_with_empty_collections(self): # Check that empty dict fields can be assigned without issues.
229240
class An_Class(Type_Safe):

0 commit comments

Comments
 (0)