Skip to content

Commit 81c94b0

Browse files
committed
added more fixes for some edge cases on the serialisation of Type_Safe__Dict , __List, __Set and __List
1 parent 29ca3d4 commit 81c94b0

File tree

12 files changed

+676
-59
lines changed

12 files changed

+676
-59
lines changed

osbot_utils/type_safe/type_safe_core/collections/Type_Safe__Dict.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from osbot_utils.type_safe.Type_Safe__Base import Type_Safe__Base
44
from osbot_utils.type_safe.Type_Safe__Primitive import Type_Safe__Primitive
55
from osbot_utils.type_safe.type_safe_core.collections.Type_Safe__List import Type_Safe__List
6-
from osbot_utils.utils.Objects import class_full_name
6+
from osbot_utils.utils.Objects import class_full_name, serialize_to_dict
77

88

99
class Type_Safe__Dict(Type_Safe__Base, dict):
@@ -54,7 +54,7 @@ def serialize_value(v):
5454
elif isinstance(v, dict):
5555
# Recursively handle nested dictionaries
5656
return {k2: serialize_value(v2) for k2, v2 in v.items()}
57-
elif isinstance(v, (list, tuple, set)):
57+
elif isinstance(v, (list, tuple, set, frozenset)):
5858
# Recursively handle sequences
5959
serialized = [serialize_value(item) for item in v]
6060
if isinstance(v, list):
@@ -64,7 +64,8 @@ def serialize_value(v):
6464
else: # set
6565
return set(serialized)
6666
else:
67-
return v
67+
return serialize_to_dict(v) # Use serialize_to_dict for unknown types (so that we don't return a non json object)
68+
6869

6970
result = {}
7071
for key, value in self.items():

osbot_utils/type_safe/type_safe_core/collections/Type_Safe__List.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from enum import Enum
22
from typing import Type
3-
from osbot_utils.utils.Objects import class_full_name
3+
from osbot_utils.utils.Objects import class_full_name, serialize_to_dict
44
from osbot_utils.type_safe.Type_Safe__Primitive import Type_Safe__Primitive
55
from osbot_utils.type_safe.Type_Safe__Base import Type_Safe__Base, type_str
66

@@ -82,12 +82,14 @@ def json(self): # Convert the list to a JSON-serializable format.
8282
result.append(item.json())
8383
elif isinstance(item, Type_Safe__Primitive):
8484
result.append(item.__to_primitive__())
85-
elif isinstance(item, (list, tuple)):
86-
result.append([x.json() if isinstance(x, Type_Safe) else x for x in item])
85+
elif isinstance(item, (list, tuple, frozenset)):
86+
result.append([x.json() if isinstance(x, Type_Safe) else serialize_to_dict(x) for x in item])
87+
#result.append([x.json() if isinstance(x, Type_Safe) else x for x in item]) # BUG here
8788
elif isinstance(item, dict):
88-
result.append({k: v.json() if isinstance(v, Type_Safe) else v for k, v in item.items()})
89+
result.append(serialize_to_dict(item)) # leverage serialize_to_dict since that method already knows how to handle
90+
#result.append({k: v.json() if isinstance(v, Type_Safe) else v for k, v in item.items()})
8991
elif isinstance(item, type):
9092
result.append(class_full_name(item))
9193
else:
92-
result.append(item)
94+
result.append(serialize_to_dict(item)) # also Use serialize_to_dict for unknown types (so that we don't return a non json object)
9395
return result

osbot_utils/type_safe/type_safe_core/collections/Type_Safe__Set.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from osbot_utils.utils.Objects import class_full_name
1+
from osbot_utils.utils.Objects import class_full_name, serialize_to_dict
22
from osbot_utils.type_safe.Type_Safe__Base import Type_Safe__Base, type_str
33
from osbot_utils.type_safe.Type_Safe__Primitive import Type_Safe__Primitive
44

@@ -52,14 +52,14 @@ def json(self):
5252
result.append(item.json())
5353
elif isinstance(item, Type_Safe__Primitive):
5454
result.append(item.__to_primitive__())
55-
elif isinstance(item, (list, tuple, set)):
56-
result.append([x.json() if isinstance(x, Type_Safe) else x for x in item])
57-
elif isinstance(item, dict):
58-
result.append({k: v.json() if isinstance(v, Type_Safe) else v for k, v in item.items()})
55+
elif isinstance(item, (list, tuple, set, frozenset)):
56+
result.append([x.json() if isinstance(x, Type_Safe) else serialize_to_dict(x) for x in item])
57+
# elif isinstance(item, dict):
58+
# result.append({k: v.json() if isinstance(v, Type_Safe) else v for k, v in item.items()})
5959
elif isinstance(item, type):
6060
result.append(class_full_name(item))
6161
else:
62-
result.append(item)
62+
result.append(serialize_to_dict(item)) # Use serialize_to_dict for unknown types (so that we don't return a non json object)
6363
return result
6464

6565
def __eq__(self, other): # todo: see if this is needed

osbot_utils/type_safe/type_safe_core/collections/Type_Safe__Tuple.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from osbot_utils.utils.Objects import class_full_name
1+
from osbot_utils.utils.Objects import class_full_name, serialize_to_dict
22
from osbot_utils.type_safe.Type_Safe__Base import Type_Safe__Base, type_str
33

44
class Type_Safe__Tuple(Type_Safe__Base, tuple):
@@ -66,12 +66,12 @@ def json(self):
6666
result.append(item.json())
6767
elif isinstance(item, Type_Safe__Primitive):
6868
result.append(item.__to_primitive__()) # Convert primitives to base types
69-
elif isinstance(item, (list, tuple)):
70-
result.append([x.json() if isinstance(x, Type_Safe) else x for x in item])
69+
elif isinstance(item, (list, tuple, frozenset)):
70+
result.append([x.json() if isinstance(x, Type_Safe) else serialize_to_dict(x) for x in item])
7171
elif isinstance(item, dict):
72-
result.append({k: v.json() if isinstance(v, Type_Safe) else v for k, v in item.items()})
72+
result.append(serialize_to_dict(item))
7373
elif isinstance(item, type):
7474
result.append(class_full_name(item))
7575
else:
76-
result.append(item)
76+
result.append(serialize_to_dict(item)) # Use serialize_to_dict for unknown types (so that we don't return a non json object)
7777
return result

osbot_utils/utils/Objects.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ def serialize_to_dict(obj):
275275
elif isinstance(obj, Enum):
276276
if isinstance(obj.value, (str, int, float, bool, type(None))): # Check if the enum value is directly serializable
277277
return obj.value
278-
elif isinstance(obj.value, (list, tuple, dict, set)): # Recursively serialize complex values
278+
elif isinstance(obj.value, (list, tuple, dict, set, frozenset)): # Recursively serialize complex values
279279
return serialize_to_dict(obj.value)
280280
else:
281281
return obj.name # Fallback to name for non-serializable values
@@ -285,7 +285,7 @@ def serialize_to_dict(obj):
285285
return f"{obj.__module__}.{obj.__name__}" # save the full type name
286286
elif isinstance(obj, (list, tuple, List)): # Added tuple here
287287
return [serialize_to_dict(item) for item in obj]
288-
elif isinstance(obj, set):
288+
elif isinstance(obj, (set, frozenset)):
289289
return [serialize_to_dict(item) for item in obj]
290290
elif isinstance(obj, dict):
291291
serialized_dict = {} # todo: refactor to separate method

tests/unit/type_safe/test_Type_Safe.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from osbot_utils.type_safe.primitives.domains.identifiers.Guid import Guid
1414
from osbot_utils.type_safe.primitives.domains.identifiers.Random_Guid import Random_Guid
1515
from osbot_utils.type_safe.Type_Safe import Type_Safe
16-
from osbot_utils.type_safe.type_safe_core.collections.Type_Safe__Dict import Type_Safe__Dict
16+
from osbot_utils.type_safe.type_safe_core.collections.Type_Safe__Dict import Type_Safe__Dict
1717
from osbot_utils.type_safe.type_safe_core.collections.Type_Safe__List import Type_Safe__List
1818
from osbot_utils.testing.Catch import Catch
1919
from osbot_utils.testing.Stdout import Stdout
Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from typing import List
22
from unittest import TestCase
33
from osbot_utils.type_safe.Type_Safe import Type_Safe
4-
from osbot_utils.type_safe.type_safe_core.collections.Type_Safe__List import Type_Safe__List
54

65

76
class test_Type_Safe__List__bugs(TestCase):
@@ -19,28 +18,3 @@ def invalid_func(x: str) -> int: # Invalid callable (wrong si
1918
return len(x)
2019

2120
an_class.an_list__callable.append(invalid_func) # BUG doesn't raise (i.e. at the moment we are not detecting the callable signature and return type)
22-
23-
def test__bug__json_with_nested_dicts(self):
24-
class TestType(Type_Safe):
25-
value: str
26-
27-
def __init__(self, value):
28-
self.value = value
29-
30-
dict_list = Type_Safe__List(dict)
31-
dict_list.append({"simple": "value"})
32-
dict_list.append({
33-
"normal": 42,
34-
"safe": TestType("test"),
35-
"nested": {"deep": TestType("deep")}
36-
})
37-
38-
expected = [
39-
{"simple": "value"},
40-
{
41-
"normal": 42,
42-
"safe": {"value": "test"},
43-
"nested": {"deep": {"value": "deep"}}
44-
}
45-
]
46-
assert dict_list.json() != expected

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,4 +231,30 @@ class An_Class(Type_Safe):
231231
# with pytest.raises(AttributeError, match = "Type_Safe__List' object has no attribute 'json'"):
232232
# assert an_class.an_list.json()
233233

234-
assert an_class.an_list.json() == [{'an_list': [], 'an_str': ''}]
234+
assert an_class.an_list.json() == [{'an_list': [], 'an_str': ''}]
235+
236+
def test__regression__json_with_nested_dicts(self):
237+
class TestType(Type_Safe):
238+
value: str
239+
240+
def __init__(self, value):
241+
self.value = value
242+
243+
dict_list = Type_Safe__List(dict)
244+
dict_list.append({"simple": "value"})
245+
dict_list.append({
246+
"normal": 42,
247+
"safe": TestType("test"),
248+
"nested": {"deep": TestType("deep")}
249+
})
250+
251+
expected = [
252+
{"simple": "value"},
253+
{
254+
"normal": 42,
255+
"safe": {"value": "test"},
256+
"nested": {"deep": {"value": "deep"}}
257+
}
258+
]
259+
#assert dict_list.json() != expected # BUG
260+
assert dict_list.json() == expected # FIXED

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

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import re
2+
import socket
23
import sys
4+
import threading
35
import pytest
46
from unittest import TestCase
57
from typing import Dict, Union, Optional, Any, Callable, List, Tuple
8+
from osbot_utils.testing.__ import __
69
from osbot_utils.type_safe.primitives.domains.identifiers.Obj_Id import Obj_Id
710
from osbot_utils.type_safe.Type_Safe import Type_Safe
811
from osbot_utils.type_safe.Type_Safe__Primitive import Type_Safe__Primitive
@@ -536,3 +539,92 @@ class An_Class(Type_Safe):
536539

537540

538541

542+
def test_json__with_unserializable_values(self): # Test that Type_Safe__Dict.json() handles unserializable values gracefully
543+
config = Type_Safe__Dict(expected_key_type=str, expected_value_type=object) # Create dict with mixed serializable and unserializable values
544+
config['host' ] = 'localhost'
545+
config['port' ] = 8080
546+
config['active' ] = True
547+
config['_lock' ] = threading.RLock() # Unserializable
548+
config['_complex'] = 3 + 4j # Unserializable
549+
config['_socket' ] = socket.socket() # Unserializable
550+
551+
result = config.json()
552+
553+
# Serializable values preserved
554+
assert result['host'] == 'localhost'
555+
assert result['port'] == 8080
556+
assert result['active'] is True
557+
558+
# Unserializable values become None
559+
assert result['_lock'] is None
560+
assert result['_complex'] is None
561+
assert result['_socket'] is None
562+
563+
config['_socket'].close() # Clean up
564+
565+
def test_json__with_nested_unserializable_in_dict_values(self): # Test Type_Safe__Dict with nested structures containing unserializable objects"""
566+
567+
settings = Type_Safe__Dict(expected_key_type=str, expected_value_type=object)
568+
settings['database'] = {
569+
'host': 'db.example.com',
570+
'port': 5432,
571+
'_lock': threading.RLock() # Nested unserializable
572+
}
573+
settings['cache'] = {
574+
'enabled': True,
575+
'ttl': 300,
576+
'_connection': 3 + 4j # Nested unserializable
577+
}
578+
579+
result = settings.json()
580+
581+
# Check nested structure is preserved with None for unserializable
582+
assert result['database']['host'] == 'db.example.com'
583+
assert result['database']['port'] == 5432
584+
assert result['database']['_lock'] is None
585+
586+
assert result['cache']['enabled'] is True
587+
assert result['cache']['ttl'] == 300
588+
assert result['cache']['_connection'] is None
589+
590+
def test_json__with_unserializable_in_list_values(self): # Test Type_Safe__Dict with list values containing unserializable objects
591+
592+
data = Type_Safe__Dict(expected_key_type=str, expected_value_type=object)
593+
data['items'] = [
594+
'valid_string',
595+
123,
596+
threading.RLock(), # Unserializable in list
597+
{'key': 'value'},
598+
3 + 4j # Another unserializable
599+
]
600+
601+
result = data.json()
602+
603+
assert result['items'][0] == 'valid_string'
604+
assert result['items'][1] == 123
605+
assert result['items'][2] is None # RLock becomes None
606+
assert result['items'][3] == {'key': 'value'}
607+
assert result['items'][4] is None # Complex number becomes None
608+
609+
def test_obj__with_unserializable_values(self): # Test that Type_Safe__Dict.obj() works with unserializable values
610+
611+
config = Type_Safe__Dict(expected_key_type=str, expected_value_type=object)
612+
config['url'] = 'http://api.example.com'
613+
config['timeout'] = 30
614+
config['_lock'] = threading.RLock()
615+
616+
result = config.obj()
617+
618+
assert result == __( url = 'http://api.example.com',
619+
timeout = 30,
620+
_lock = None) # Unserializable becomes None
621+
622+
def test_json__with_nested_frozenset_in_dict(self): # Test frozenset as dict value
623+
624+
data = Type_Safe__Dict(expected_key_type=str, expected_value_type=object)
625+
data['tags'] = frozenset(['python', 'testing', 'type-safe'])
626+
627+
result = data.json()
628+
629+
# Should serialize frozenset to list
630+
assert set(result['tags']) == {'python', 'testing', 'type-safe'}

0 commit comments

Comments
 (0)