Skip to content

Commit f976a38

Browse files
committed
Merge dev into main
2 parents 63daef5 + 82d1e8f commit f976a38

File tree

6 files changed

+101
-14
lines changed

6 files changed

+101
-14
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.26.0-blue)
3+
![Current Release](https://img.shields.io/badge/release-v3.26.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/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())

osbot_utils/version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v3.26.0
1+
v3.26.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.26.0"
3+
version = "v3.26.1"
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: 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: 81 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
@@ -10,6 +12,7 @@
1012
from osbot_utils.type_safe.primitives.domains.identifiers.safe_str.Safe_Str__Id import Safe_Str__Id
1113
from osbot_utils.type_safe.type_safe_core.collections.Type_Safe__Dict import Type_Safe__Dict
1214
from osbot_utils.type_safe.type_safe_core.collections.Type_Safe__List import Type_Safe__List
15+
from osbot_utils.utils.Env import not_in_github_action
1316
from osbot_utils.utils.Objects import base_classes
1417
from osbot_utils.type_safe.primitives.domains.identifiers.Safe_Id import Safe_Id
1518
from osbot_utils.type_safe.Type_Safe import Type_Safe
@@ -380,4 +383,81 @@ class An_Class(Type_Safe):
380383
assert An_Class(an_dict={'A': 42}).json() == { 'an_dict': { 'A': 42}} # FIXED
381384
assert An_Class.from_json(An_Class(an_dict={'A': 42}).json()).obj() == __(an_dict=__(A=42)) # FIXED
382385
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)) #
386+
assert An_Class.from_json({ 'an_dict': { 'A' : 42}} ).obj() == __(an_dict=__(A=42)) #
387+
388+
def test__regression__nested_dict_enum_keys__obj_vs_json_inconsistency(self):
389+
"""
390+
BUG: .obj() and .json() are inconsistent for nested Dict with Enum keys.
391+
.json() uses enum.value, .obj() uses transformed enum.name
392+
"""
393+
class Status(str, Enum):
394+
ACTIVE = 'active'
395+
INACTIVE = 'inactive'
396+
397+
class Container(Type_Safe):
398+
nested: Dict[str, Dict[Status, int]]
399+
400+
container = Container(nested={'key': {Status.ACTIVE: 100}})
401+
402+
# .json() uses enum VALUE
403+
assert container.json() == {'nested': {'key': {'active': 100}}}
404+
405+
# BUG: .obj() should also use 'active' but uses 'Status_ACTIVE'
406+
#assert container.obj() == __(nested=__(key=__(Status_ACTIVE=100))) # BUG Current behavior
407+
#assert container.obj() != __(nested=__(key=__(active=100))) # BUG Expected behavior
408+
assert container.obj() == __(nested=__(key=__(active=100))) # FIXED
409+
410+
# assert container.json() == {'nested': {'key': {Status.ACTIVE: 100}}} # BUG
411+
# error_message = ("assert {'nested': {'key': {<Status.ACTIVE: 'active'>: 100}}} == {}\n \n "
412+
# "Left contains 1 more item:\n "
413+
# "{'nested': {'key': {<Status.ACTIVE: 'active'>: 100}}}\n \n "
414+
# "Full diff:\n - {}\n + {\n + 'nested': {\n + 'key': "
415+
# "{\n + <Status.ACTIVE: 'active'>: 100,\n + },\n + "
416+
# "},\n + }") # BUG
417+
assert container.json() == {'nested': {'key': {Status.ACTIVE: 100}}} # this works due to auto conversion of enum into it's string value
418+
assert container.json() == {'nested': {'key': {'active': 100}}} # FIXED: this is what we wanted to happen
419+
420+
if not_in_github_action(): # GH actions has different spacing
421+
error_message = ("assert {'nested': {'key': {'active': 100}}} == {}\n \n "
422+
"Left contains 1 more item:\n "
423+
"{'nested': {'key': {'active': 100}}}\n \n " # FIXED: now we get the 'active' string (instead of the Enum representation)
424+
"Full diff:\n - {}\n + {\n + 'nested': "
425+
"{\n + 'key': {\n + "
426+
"'active': 100,\n + },\n + },\n + }")
427+
with pytest.raises(AssertionError, match=re.escape(error_message)):
428+
assert container.json() == {} # FIXED this is the error message we should get
429+
430+
error_message_2 = 'assert __(nested=__(key=__(active=100))) == __()\n '
431+
with pytest.raises(AssertionError, match=re.escape(error_message_2)):
432+
assert container.obj() == __()
433+
434+
# couple more edge cases tests
435+
json_str = json.dumps(container.json())
436+
assert json_str == '{"nested": {"key": {"active": 100}}}'
437+
assert json.loads(json_str) == {'nested': {'key': {'active': 100}}}
438+
439+
container2 = Container(nested={'key': {Status.ACTIVE: 100, Status.INACTIVE: 50}})
440+
assert container2.json() == {'nested': {'key': {'active': 100, 'inactive': 50}}}
441+
assert container2.obj() == __(nested=__(key=__(active=100, inactive=50)))
442+
443+
# Test round-trip consistency
444+
container3 = Container.from_json(container.json())
445+
assert container3.json() == container.json()
446+
assert container3.obj() == container.obj()
447+
448+
def test__regression__simple_dict_enum_keys__now__works_correctly(self):
449+
450+
class Status(str, Enum):
451+
ACTIVE = 'active'
452+
INACTIVE = 'inactive'
453+
454+
class SimpleContainer(Type_Safe):
455+
data: Dict[Status, int]
456+
457+
simple = SimpleContainer(data={Status.ACTIVE: 100})
458+
459+
# Single-level Dict works correctly - uses enum value
460+
assert simple.json() == {'data': {'active': 100}} # ✓ Correct
461+
462+
# And it's JSON-serializable
463+
assert json.dumps(simple.json()) == '{"data": {"active": 100}}' # ✓ Works

0 commit comments

Comments
 (0)