Skip to content

Commit 530ff87

Browse files
committed
fixed another edge case with use of enum in nested dicts
1 parent 63daef5 commit 530ff87

File tree

3 files changed

+95
-11
lines changed

3 files changed

+95
-11
lines changed

osbot_utils/type_safe/type_safe_core/collections/Type_Safe__Dict.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,23 +41,32 @@ def __setitem__(self, key, value): # Ch
4141
def __enter__(self): return self
4242
def __exit__ (self, type, value, traceback): pass
4343

44-
def json(self):
45-
from osbot_utils.type_safe.Type_Safe import Type_Safe
44+
# todo: this method needs to be refactored into smaller parts, it is getting to complex:
45+
# the use of the inner method serialize_value
46+
# the circular dependency on Type_Safe
47+
# the inner for loops to handle nested dictionaries
48+
# the enum edges cases (like the nested dictionaries case)
49+
# .
50+
# good news is that we have tons of tests and edge cases detection (so we should be able to do this
51+
# refactoring with no side effects
52+
def json(self): # Recursively serialize values, handling nested structures
53+
from osbot_utils.type_safe.Type_Safe import Type_Safe # needed here due to circular dependencies
4654

4755
def serialize_value(v):
48-
"""Recursively serialize values, handling nested structures"""
4956
if isinstance(v, Type_Safe):
5057
return v.json()
5158
elif isinstance(v, Type_Safe__Primitive):
5259
return v.__to_primitive__()
5360
elif isinstance(v, type):
5461
return class_full_name(v)
5562
elif isinstance(v, dict):
56-
# Recursively handle nested dictionaries
57-
return {k2: serialize_value(v2) for k2, v2 in v.items()}
63+
return { # Recursively handle nested dictionaries (with enum support)
64+
(k2.value if isinstance(k2, Enum) else k2): serialize_value(v2)
65+
for k2, v2 in v.items()
66+
}
67+
#return {k2: serialize_value(v2) for k2, v2 in v.items()} # Recursively handle nested dictionaries
5868
elif isinstance(v, (list, tuple, set, frozenset)):
59-
# Recursively handle sequences
60-
serialized = [serialize_value(item) for item in v]
69+
serialized = [serialize_value(item) for item in v] # Recursively handle sequences
6170
if isinstance(v, list):
6271
return serialized
6372
elif isinstance(v, tuple):
@@ -95,4 +104,4 @@ def obj(self) -> __:
95104
return dict_to_obj(self.json())
96105

97106
def values(self) -> Type_Safe__List:
98-
return Type_Safe__List(self.expected_value_type, super().values())
107+
return Type_Safe__List(self.expected_value_type, super().values())

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66

77
class test_Type_Safe__Dict__bugs(TestCase):
88

9-
10-
119
def test__bug__type_safe_list_with_dict_any_type__and__error_message_is_confusing(self): # Document bug where Dict[str, any] fails in List ,and the error message doesn't mention that Any works
1210

1311
class Schema__Order__Bug(Type_Safe):

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

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import json
2+
import re
13
import pytest
24
from enum import Enum
35
from unittest import TestCase
@@ -380,4 +382,79 @@ class An_Class(Type_Safe):
380382
assert An_Class(an_dict={'A': 42}).json() == { 'an_dict': { 'A': 42}} # FIXED
381383
assert An_Class.from_json(An_Class(an_dict={'A': 42}).json()).obj() == __(an_dict=__(A=42)) # FIXED
382384
assert An_Class.from_json({ 'an_dict': { An_Enum.A: 42}} ).obj() == __(an_dict=__(A=42)) #
383-
assert An_Class.from_json({ 'an_dict': { 'A' : 42}} ).obj() == __(an_dict=__(A=42)) #
385+
assert An_Class.from_json({ 'an_dict': { 'A' : 42}} ).obj() == __(an_dict=__(A=42)) #
386+
387+
def test__regression__nested_dict_enum_keys__obj_vs_json_inconsistency(self):
388+
"""
389+
BUG: .obj() and .json() are inconsistent for nested Dict with Enum keys.
390+
.json() uses enum.value, .obj() uses transformed enum.name
391+
"""
392+
class Status(str, Enum):
393+
ACTIVE = 'active'
394+
INACTIVE = 'inactive'
395+
396+
class Container(Type_Safe):
397+
nested: Dict[str, Dict[Status, int]]
398+
399+
container = Container(nested={'key': {Status.ACTIVE: 100}})
400+
401+
# .json() uses enum VALUE
402+
assert container.json() == {'nested': {'key': {'active': 100}}}
403+
404+
# BUG: .obj() should also use 'active' but uses 'Status_ACTIVE'
405+
#assert container.obj() == __(nested=__(key=__(Status_ACTIVE=100))) # BUG Current behavior
406+
#assert container.obj() != __(nested=__(key=__(active=100))) # BUG Expected behavior
407+
assert container.obj() == __(nested=__(key=__(active=100))) # FIXED
408+
409+
# assert container.json() == {'nested': {'key': {Status.ACTIVE: 100}}} # BUG
410+
# error_message = ("assert {'nested': {'key': {<Status.ACTIVE: 'active'>: 100}}} == {}\n \n "
411+
# "Left contains 1 more item:\n "
412+
# "{'nested': {'key': {<Status.ACTIVE: 'active'>: 100}}}\n \n "
413+
# "Full diff:\n - {}\n + {\n + 'nested': {\n + 'key': "
414+
# "{\n + <Status.ACTIVE: 'active'>: 100,\n + },\n + "
415+
# "},\n + }") # BUG
416+
assert container.json() == {'nested': {'key': {Status.ACTIVE: 100}}} # this works due to auto conversion of enum into it's string value
417+
assert container.json() == {'nested': {'key': {'active': 100}}} # FIXED: this is what we wanted to happen
418+
error_message = ("assert {'nested': {'key': {'active': 100}}} == {}\n \n "
419+
"Left contains 1 more item:\n "
420+
"{'nested': {'key': {'active': 100}}}\n \n " # FIXED: now we get the 'active' string (instead of the Enum representation)
421+
"Full diff:\n - {}\n + {\n + 'nested': "
422+
"{\n + 'key': {\n + "
423+
"'active': 100,\n + },\n + },\n + }")
424+
with pytest.raises(AssertionError, match=re.escape(error_message)):
425+
assert container.json() == {} # FIXED this is the error message we should get
426+
427+
error_message_2 = 'assert __(nested=__(key=__(active=100))) == __()\n '
428+
with pytest.raises(AssertionError, match=re.escape(error_message_2)):
429+
assert container.obj() == __()
430+
431+
# couple more edge cases tests
432+
json_str = json.dumps(container.json())
433+
assert json_str == '{"nested": {"key": {"active": 100}}}'
434+
assert json.loads(json_str) == {'nested': {'key': {'active': 100}}}
435+
436+
container2 = Container(nested={'key': {Status.ACTIVE: 100, Status.INACTIVE: 50}})
437+
assert container2.json() == {'nested': {'key': {'active': 100, 'inactive': 50}}}
438+
assert container2.obj() == __(nested=__(key=__(active=100, inactive=50)))
439+
440+
# Test round-trip consistency
441+
container3 = Container.from_json(container.json())
442+
assert container3.json() == container.json()
443+
assert container3.obj() == container.obj()
444+
445+
def test__regression__simple_dict_enum_keys__now__works_correctly(self):
446+
447+
class Status(str, Enum):
448+
ACTIVE = 'active'
449+
INACTIVE = 'inactive'
450+
451+
class SimpleContainer(Type_Safe):
452+
data: Dict[Status, int]
453+
454+
simple = SimpleContainer(data={Status.ACTIVE: 100})
455+
456+
# Single-level Dict works correctly - uses enum value
457+
assert simple.json() == {'data': {'active': 100}} # ✓ Correct
458+
459+
# And it's JSON-serializable
460+
assert json.dumps(simple.json()) == '{"data": {"active": 100}}' # ✓ Works

0 commit comments

Comments
 (0)