Skip to content

Commit 1e99894

Browse files
committed
Merge dev into main
2 parents 5d26f65 + fc9f35a commit 1e99894

File tree

7 files changed

+90
-27
lines changed

7 files changed

+90
-27
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.41.0-blue)
3+
![Current Release](https://img.shields.io/badge/release-v3.41.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/primitives/domains/files/safe_str/Safe_Str__File__Path.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
TYPE_SAFE_STR__FILE_PATH__REGEX = re.compile(r'[^a-zA-Z0-9_\-./\\ ]') # Allow alphanumerics, underscores, hyphens, dots, slashes, and spaces
55
TYPE_SAFE_STR__FILE_PATH__MAX_LENGTH = 1024
66

7+
# todo:: this doesn't prevent path like "../../../etc/passwd" since those are valid paths
8+
# look at what we can do there (in terms of adding methods for __add__ + __radd__ , having a different class with that type of protection (i.e. does not allow ../ paths)
79
class Safe_Str__File__Path(Safe_Str):
810
regex = TYPE_SAFE_STR__FILE_PATH__REGEX
911
max_length = TYPE_SAFE_STR__FILE_PATH__MAX_LENGTH

osbot_utils/utils/Objects.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -273,12 +273,12 @@ def serialize_to_dict(obj):
273273
elif hasattr(obj, '__primitive_base__') and isinstance(obj, (str, int, float)):
274274
return obj.__primitive_base__(obj)
275275
elif isinstance(obj, Enum):
276-
if isinstance(obj.value, (str, int, float, bool, type(None))): # Check if the enum value is directly serializable
277-
return obj.value
278-
elif isinstance(obj.value, (list, tuple, dict, set, frozenset)): # Recursively serialize complex values
279-
return serialize_to_dict(obj.value)
276+
if isinstance(obj.value, (str, int, float, bool, type(None))): # Check if the enum value is directly serializable
277+
return obj.value # todo: question could this cover all Type_Safe__Primitive classes?
278+
# elif isinstance(obj.value, (list, tuple, dict, set, frozenset)): # Recursively serialize complex values
279+
# return serialize_to_dict(obj.value) # removed this since this was causing side effects in some roundtrips
280280
else:
281-
return obj.name # Fallback to name for non-serializable values
281+
return obj.name # it is better to use the enum name (which roundtrips ok)
282282
elif isinstance(obj, (str, int, float, bool, bytes, Decimal)) or obj is None: # todo: add support for objects like datetime
283283
return obj
284284
elif isinstance(obj, type):

osbot_utils/version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v3.41.0
1+
v3.41.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.41.0"
3+
version = "v3.41.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__List__bugs.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
import re
12
from typing import List
23
from unittest import TestCase
4+
import pytest
5+
from osbot_utils.testing.__ import __
36
from osbot_utils.type_safe.Type_Safe import Type_Safe
47

58

69
class test_Type_Safe__List__bugs(TestCase):
710

8-
911
def test__bug__type_safe_list_with_callable(self):
1012
from typing import Callable
1113

tests/unit/utils/test_Objects.py

Lines changed: 77 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -750,29 +750,38 @@ class ComplexEnum(Enum):
750750
assert type(ComplexEnum.SET_VAL.value ) is set
751751

752752
# Should recursively serialize complex values
753-
assert serialize_to_dict(ComplexEnum.LIST_VAL) == [1, 2, 3]
754-
assert serialize_to_dict(ComplexEnum.DICT_VAL) == {'key': 'value', 'count': 42}
755-
assert serialize_to_dict(ComplexEnum.TUPLE_VAL) != ('a', 'b', 'c') # we can't convert to tuple since json doesn't support it
756-
assert serialize_to_dict(ComplexEnum.TUPLE_VAL) == ['a', 'b', 'c'] # so the tuple becomes a list
757-
assert sorted(serialize_to_dict(ComplexEnum.SET_VAL)) == sorted(['x', 'y', 'z']) # same for set
753+
assert serialize_to_dict(ComplexEnum.LIST_VAL ) == 'LIST_VAL'
754+
assert serialize_to_dict(ComplexEnum.DICT_VAL ) == 'DICT_VAL'
755+
assert serialize_to_dict(ComplexEnum.TUPLE_VAL) == 'TUPLE_VAL'
756+
assert serialize_to_dict(ComplexEnum.SET_VAL ) == 'SET_VAL'
758757

759-
# Verify types
760-
assert type(serialize_to_dict(ComplexEnum.LIST_VAL)) is list
761-
assert type(serialize_to_dict(ComplexEnum.DICT_VAL)) is dict
762-
assert type(serialize_to_dict(ComplexEnum.TUPLE_VAL)) is not tuple # we can't convert to tuple since json doesn't support it
763-
assert type(serialize_to_dict(ComplexEnum.TUPLE_VAL)) is list # so the tuple becomes a list
764-
assert type(serialize_to_dict(ComplexEnum.SET_VAL )) is list # same for set
758+
759+
def test_from_json__Enum_tuple_support(self):
760+
class An_Enum(tuple, Enum):
761+
AB = ('a', 'b')
762+
CD = ('c', 'd')
763+
764+
class An_Class(Type_Safe):
765+
an_enum : An_Enum # ← Stores the ENUM itself
766+
767+
# This is the real round-trip test:
768+
an_class = An_Class(an_enum='AB')
769+
assert an_class.json() == {'an_enum': 'AB'} # ✅ Serializes to name
770+
771+
restored = An_Class.from_json(an_class.json())
772+
assert restored.an_enum == An_Enum.AB # ✅ Should deserialize back to enum
773+
assert restored.an_enum == ('a', 'b') # ✅ Enum value comparison works
765774

766775
def test_enum_with_nested_complex_values(self): # Test nested serialization
767776
class NestedEnum(Enum):
768777
NESTED_LIST = [1, [2, 3], {'inner': 'dict'}]
769778
NESTED_DICT = {'list': [1, 2], 'dict': {'a': 'b'}}
770779

771780
result_list = serialize_to_dict(NestedEnum.NESTED_LIST)
772-
assert result_list == [1, [2, 3], {'inner': 'dict'}]
781+
assert result_list == 'NESTED_LIST' #[1, [2, 3], {'inner': 'dict'}]
773782

774783
result_dict = serialize_to_dict(NestedEnum.NESTED_DICT)
775-
assert result_dict == {'list': [1, 2], 'dict': {'a': 'b'}}
784+
assert result_dict == 'NESTED_DICT' # {'list': [1, 2], 'dict': {'a': 'b'}}
776785

777786
def test_enum_with_unsupported_value_types(self): # Test fallback to .name
778787
class CustomClass:
@@ -1042,10 +1051,15 @@ class ComplexEnum(Enum):
10421051
serialized_set = serialize_to_dict(ComplexEnum.SET_VAL)
10431052

10441053
# Verify serialization results
1045-
assert serialized_list == [1, 2, 3]
1046-
assert serialized_dict == {'key': 'value', 'count': 42}
1047-
assert serialized_tuple == ['a', 'b', 'c'] # Tuple becomes list
1048-
assert sorted(serialized_set) == ['x', 'y', 'z'] # Set becomes list
1054+
#assert serialized_list == [1, 2, 3]
1055+
#assert serialized_dict == {'key': 'value', 'count': 42}
1056+
#assert serialized_tuple == ['a', 'b', 'c'] # Tuple becomes list
1057+
#assert sorted(serialized_set) == ['x', 'y', 'z'] # Set becomes list
1058+
1059+
assert serialized_list == 'LIST_VAL'
1060+
assert serialized_dict == 'DICT_VAL'
1061+
assert serialized_tuple == 'TUPLE_VAL'
1062+
assert serialized_set == 'SET_VAL'
10491063

10501064
# Round-trip test - Type_Safe with enum
10511065
class Schema__Config(Type_Safe):
@@ -1145,4 +1159,49 @@ class Schema__Settings(Type_Safe):
11451159
assert sorted(restored.active_tags) == sorted(list(DataEnum.DEFAULT_TAGS.value))
11461160
assert restored.config_map == DataEnum.DEFAULT_MAP.value
11471161
assert restored.values == DataEnum.DEFAULT_LIST.value
1148-
assert restored.sequence == list(DataEnum.DEFAULT_TUPLE.value)
1162+
assert restored.sequence == list(DataEnum.DEFAULT_TUPLE.value)
1163+
1164+
def test__regression__from_json__enum_tuple_support(self):
1165+
from enum import Enum
1166+
1167+
class An_Enum(tuple, Enum):
1168+
AB = ('a', 'b')
1169+
CD = ('c', 'd')
1170+
1171+
class An_Class(Type_Safe):
1172+
an_enum : An_Enum
1173+
1174+
assert An_Class().obj() == __() # is this a bug?
1175+
assert An_Class().obj() == __(an_enum = None)
1176+
assert An_Class().json() == {'an_enum': None}
1177+
assert An_Class(an_enum='AB' ).an_enum == An_Enum.AB
1178+
assert An_Class(an_enum=An_Enum.AB).an_enum == An_Enum.AB
1179+
assert An_Class(an_enum=An_Enum.AB).an_enum == ('a', 'b')
1180+
assert An_Class(an_enum='AB' ).an_enum == ('a', 'b')
1181+
assert An_Class(an_enum=An_Enum.AB).an_enum != An_Enum.CD
1182+
1183+
assert An_Class.from_json(An_Class().json()).json() == {'an_enum': None}
1184+
#error_message = "unhashable type: 'list'"
1185+
#with pytest.raises(TypeError, match=error_message):
1186+
# An_Class.from_json(An_Class(an_enum='AB').json()) # BUG, this should have worked
1187+
An_Class.from_json(An_Class(an_enum='AB').json())
1188+
1189+
an_class = An_Class(an_enum=An_Enum.CD)
1190+
# with pytest.raises(TypeError, match=error_message):
1191+
# An_Class.from_json(an_class.json()) # BUG, this should have worked
1192+
An_Class.from_json(an_class.json())
1193+
#with pytest.raises(TypeError, match=error_message):
1194+
#assert an_class.json() == {'an_enum': ['c', 'd']} # BUG this be shoud be 'CD'?
1195+
assert an_class.json() == {'an_enum': 'CD' }
1196+
1197+
# error_message_2 = ("assert {'an_enum': ['c', 'd']} == {'an_enum': <An_Enum.CD: ('c', 'd')>}\n \n "
1198+
# "Differing items:\n {'an_enum': ['c', 'd']} != "
1199+
# "{'an_enum': <An_Enum.CD: ('c', 'd')>}\n \n "
1200+
# "Full diff:\n {\n - 'an_enum': <An_Enum.CD: ('c', 'd')>,\n + "
1201+
# " 'an_enum': [\n + 'c',\n + 'd',\n + ],\n }")
1202+
# with pytest.raises(AssertionError, match=re.escape(error_message_2)):
1203+
# assert an_class.json() == {'an_enum': An_Enum.CD} #
1204+
assert an_class.json() == {'an_enum': 'CD'}
1205+
assert An_Class.from_json({'an_enum': 'CD'} ).an_enum == ('c', 'd')
1206+
assert An_Class.from_json({'an_enum': An_Enum.CD}).an_enum == ('c', 'd')
1207+
assert An_Class.from_json(An_Class(an_enum='AB').json()).json() == {'an_enum': 'AB'}

0 commit comments

Comments
 (0)