Skip to content

Commit 3306d64

Browse files
committed
Merge dev into main
2 parents dc70206 + b942b43 commit 3306d64

17 files changed

+850
-63
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.43.0-blue)
3+
![Current Release](https://img.shields.io/badge/release-v3.43.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: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,74 @@ def update(self, other=None, **kwargs):
142142
# Handle keyword arguments
143143
for key, value in kwargs.items():
144144
self[key] = value
145+
146+
def setdefault(self, key, default=None):
147+
if key not in self:
148+
self[key] = default # Delegates to __setitem__ which validates
149+
return self[key]
150+
151+
def __ior__(self, other):
152+
# Handle |= operator (Python 3.9+)
153+
if hasattr(other, 'items'):
154+
for key, value in other.items():
155+
self[key] = value # Delegates to __setitem__ which validates
156+
else:
157+
for key, value in other:
158+
self[key] = value
159+
return self
160+
161+
def copy(self):
162+
# Return a copy of the same subclass type, not Type_Safe__Dict
163+
result = self.__class__(
164+
expected_key_type=self.expected_key_type,
165+
expected_value_type=self.expected_value_type
166+
)
167+
for key, value in self.items():
168+
result[key] = value
169+
return result
170+
171+
def __or__(self, other):
172+
# Handle | operator (Python 3.9+) - returns new instance of same subclass
173+
result = self.__class__(
174+
expected_key_type=self.expected_key_type,
175+
expected_value_type=self.expected_value_type
176+
)
177+
for key, value in self.items():
178+
result[key] = value
179+
if hasattr(other, 'items'):
180+
for key, value in other.items():
181+
result[key] = value
182+
else:
183+
for key, value in other:
184+
result[key] = value
185+
return result
186+
187+
def __ror__(self, other):
188+
# Handle reverse | operator - returns same subclass type
189+
result = self.__class__(
190+
expected_key_type=self.expected_key_type,
191+
expected_value_type=self.expected_value_type
192+
)
193+
if hasattr(other, 'items'):
194+
for key, value in other.items():
195+
result[key] = value
196+
else:
197+
for key, value in other:
198+
result[key] = value
199+
for key, value in self.items():
200+
result[key] = value
201+
return result
202+
203+
@classmethod
204+
def fromkeys(cls, keys, value=None, expected_key_type=None, expected_value_type=None):
205+
expected_key_type = expected_key_type or cls.expected_key_type # Use cls to create instance of the actual subclass
206+
expected_value_type = expected_value_type or cls.expected_value_type
207+
208+
if expected_key_type is None or expected_value_type is None:
209+
raise ValueError(f"{cls.__name__}.fromkeys() requires expected_key_type and expected_value_type")
210+
211+
result = cls(expected_key_type = expected_key_type ,
212+
expected_value_type = expected_value_type)
213+
for key in keys:
214+
result[key] = value
215+
return result

osbot_utils/type_safe/type_safe_core/collections/Type_Safe__List.py

Lines changed: 80 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77

88

99
class Type_Safe__List(Type_Safe__Base, list):
10-
expected_type : Type
10+
expected_type : Type = None # Class-level default
1111

12-
def __init__(self, expected_type, *args):
12+
def __init__(self, expected_type=None, *args):
1313
super().__init__(*args)
14-
self.expected_type = expected_type
14+
self.expected_type = expected_type or self.__class__.expected_type
15+
if self.expected_type is None:
16+
raise ValueError(f"{self.__class__.__name__} requires expected_type")
1517

1618
def __contains__(self, item):
1719
if super().__contains__(item): # First try direct lookup
@@ -43,37 +45,55 @@ def __repr__(self):
4345
def __enter__(self): return self
4446
def __exit__ (self, type, value, traceback): pass
4547

46-
def append(self, item):
47-
from osbot_utils.type_safe.Type_Safe import Type_Safe # to prevent circular imports
48+
def _validate_and_convert_item(self, item): # Validate and convert an item to the expected type."
49+
from osbot_utils.type_safe.Type_Safe import Type_Safe
4850

49-
if type(self.expected_type) is type and issubclass(self.expected_type, Type_Safe) and type(item) is dict: # Handle Type_Safe objects from dicts
51+
if type(self.expected_type) is type and issubclass(self.expected_type, Type_Safe) and type(item) is dict:
5052
item = self.expected_type.from_json(item)
51-
elif type(self.expected_type) is type and issubclass(self.expected_type, Type_Safe__Primitive): # Handle Type_Safe__Primitive conversions (str -> Safe_Str, etc.)
53+
elif type(self.expected_type) is type and issubclass(self.expected_type, Type_Safe__Primitive):
5254
if not isinstance(item, self.expected_type):
5355
try:
5456
item = self.expected_type(item)
5557
except (ValueError, TypeError) as e:
56-
# Re-raise with more context about what failed
5758
raise TypeError(f"In Type_Safe__List: Could not convert {type(item).__name__} to {self.expected_type.__name__}: {e}") from None
5859

59-
elif hasattr(self.expected_type, '__bases__') and any(base.__name__ == 'Enum' for base in self.expected_type.__bases__): # Handle Enums
60-
60+
elif hasattr(self.expected_type, '__bases__') and any(base.__name__ == 'Enum' for base in self.expected_type.__bases__):
6161
if isinstance(self.expected_type, type) and issubclass(self.expected_type, Enum):
6262
if isinstance(item, str):
63-
if item in self.expected_type.__members__: # Try to convert string to enum
63+
if item in self.expected_type.__members__:
6464
item = self.expected_type[item]
6565
elif hasattr(self.expected_type, '_value2member_map_') and item in self.expected_type._value2member_map_:
6666
item = self.expected_type._value2member_map_[item]
6767

68-
try: # Now validate the (possibly converted) item
68+
try:
6969
self.is_instance_of_type(item, self.expected_type)
7070
except TypeError as e:
7171
raise TypeError(f"In Type_Safe__List: Invalid type for item: {e}") from None
7272

73+
return item
74+
75+
def append(self, item):
76+
item = self._validate_and_convert_item(item)
7377
super().append(item)
7478

79+
def __setitem__(self, index, item):
80+
item = self._validate_and_convert_item(item)
81+
super().__setitem__(index, item)
82+
83+
def __iadd__(self, items):
84+
for item in items:
85+
self.append(item)
86+
return self
87+
88+
def insert(self, index, item):
89+
item = self._validate_and_convert_item(item)
90+
super().insert(index, item)
7591

76-
def json(self): # Convert the list to a JSON-serializable format.
92+
def extend(self, items):
93+
for item in items:
94+
self.append(item)
95+
96+
def json(self): # Convert the list to a JSON-serializable format.
7797
from osbot_utils.type_safe.Type_Safe import Type_Safe # Import here to avoid circular imports
7898

7999
result = []
@@ -84,12 +104,56 @@ def json(self): # Convert the list to a JSON-serializable format.
84104
result.append(item.__to_primitive__())
85105
elif isinstance(item, (list, tuple, frozenset)):
86106
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
88107
elif isinstance(item, dict):
89108
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()})
91109
elif isinstance(item, type):
92110
result.append(class_full_name(item))
93111
else:
94112
result.append(serialize_to_dict(item)) # also Use serialize_to_dict for unknown types (so that we don't return a non json object)
95-
return result
113+
return result
114+
115+
def copy(self):
116+
# Return a copy of the same subclass type
117+
result = self.__class__(expected_type=self.expected_type)
118+
for item in self:
119+
result.append(item)
120+
return result
121+
122+
def __add__(self, other):
123+
# Handle list1 + list2 - returns new instance of same subclass
124+
result = self.__class__(expected_type=self.expected_type)
125+
for item in self:
126+
result.append(item)
127+
for item in other:
128+
result.append(item) # Validates each item
129+
return result
130+
131+
def __radd__(self, other):
132+
# Handle list + type_safe_list - returns new instance of same subclass
133+
result = self.__class__(expected_type=self.expected_type)
134+
for item in other:
135+
result.append(item) # Validates each item
136+
for item in self:
137+
result.append(item)
138+
return result
139+
140+
def __mul__(self, n):
141+
# Handle list * n - returns new instance of same subclass
142+
result = self.__class__(expected_type=self.expected_type)
143+
for _ in range(n):
144+
for item in self:
145+
result.append(item)
146+
return result
147+
148+
def __rmul__(self, n):
149+
# Handle n * list - same as __mul__
150+
return self.__mul__(n)
151+
152+
def __imul__(self, n):
153+
# Handle list *= n - modifies in place, type already correct
154+
items = list(self)
155+
self.clear()
156+
for _ in range(n):
157+
for item in items:
158+
self.append(item)
159+
return self

osbot_utils/type_safe/type_safe_core/collections/Type_Safe__Set.py

Lines changed: 142 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1+
from typing import Type
12
from osbot_utils.utils.Objects import class_full_name, serialize_to_dict
23
from osbot_utils.type_safe.Type_Safe__Base import Type_Safe__Base, type_str
34
from osbot_utils.type_safe.Type_Safe__Primitive import Type_Safe__Primitive
45

56

67
class Type_Safe__Set(Type_Safe__Base, set):
7-
def __init__(self, expected_type, *args):
8+
expected_type : Type = None # Class-level default
9+
10+
def __init__(self, expected_type=None, *args):
811
super().__init__(*args)
9-
self.expected_type = expected_type
12+
self.expected_type = expected_type or self.__class__.expected_type
13+
if self.expected_type is None:
14+
raise ValueError(f"{self.__class__.__name__} requires expected_type")
1015

1116
def __contains__(self, item):
1217
if super().__contains__(item): # First try direct lookup
@@ -65,4 +70,138 @@ def json(self):
6570
def __eq__(self, other): # todo: see if this is needed
6671
if isinstance(other, (set, Type_Safe__Set)):
6772
return set(self) == set(other)
68-
return False
73+
return False
74+
75+
def update(self, *others):
76+
for other in others:
77+
for item in other:
78+
self.add(item) # Delegates to add() which validates
79+
80+
def copy(self):
81+
# Return a copy of the same subclass type
82+
result = self.__class__(expected_type=self.expected_type)
83+
for item in self:
84+
result.add(item)
85+
return result
86+
87+
def __or__(self, other):
88+
# Handle | operator - returns new instance of same subclass
89+
result = self.__class__(expected_type=self.expected_type)
90+
for item in self:
91+
result.add(item)
92+
for item in other:
93+
result.add(item) # Validates
94+
return result
95+
96+
def __ror__(self, other):
97+
# Handle reverse | operator - returns same subclass type
98+
result = self.__class__(expected_type=self.expected_type)
99+
for item in other:
100+
result.add(item) # Validates
101+
for item in self:
102+
result.add(item)
103+
return result
104+
105+
def __and__(self, other):
106+
# Handle & operator (intersection) - returns same subclass type
107+
result = self.__class__(expected_type=self.expected_type)
108+
for item in super().__and__(other):
109+
result.add(item)
110+
return result
111+
112+
def __rand__(self, other):
113+
# Handle reverse & operator
114+
return self.__and__(other)
115+
116+
def __sub__(self, other):
117+
# Handle - operator (difference) - returns same subclass type
118+
result = self.__class__(expected_type=self.expected_type)
119+
for item in super().__sub__(other):
120+
result.add(item)
121+
return result
122+
123+
def __rsub__(self, other):
124+
# Handle reverse - operator
125+
result = self.__class__(expected_type=self.expected_type)
126+
for item in set(other) - set(self):
127+
result.add(item) # Validates items from other
128+
return result
129+
130+
def __xor__(self, other):
131+
# Handle ^ operator (symmetric difference) - returns same subclass type
132+
result = self.__class__(expected_type=self.expected_type)
133+
for item in set(self) - set(other):
134+
result.add(item)
135+
for item in set(other) - set(self):
136+
result.add(item) # Validates
137+
return result
138+
139+
def __rxor__(self, other):
140+
# Handle reverse ^ operator
141+
return self.__xor__(other)
142+
143+
def union(self, *others):
144+
# Return same subclass type with validation
145+
result = self.__class__(expected_type=self.expected_type)
146+
for item in self:
147+
result.add(item)
148+
for other in others:
149+
for item in other:
150+
result.add(item) # Validates
151+
return result
152+
153+
def intersection(self, *others):
154+
# Return same subclass type
155+
result = self.__class__(expected_type=self.expected_type)
156+
base_result = set(self)
157+
for other in others:
158+
base_result &= set(other)
159+
for item in base_result:
160+
result.add(item)
161+
return result
162+
163+
def difference(self, *others):
164+
# Return same subclass type
165+
result = self.__class__(expected_type=self.expected_type)
166+
base_result = set(self)
167+
for other in others:
168+
base_result -= set(other)
169+
for item in base_result:
170+
result.add(item)
171+
return result
172+
173+
def symmetric_difference(self, other):
174+
# Return same subclass type with validation for items from other
175+
result = self.__class__(expected_type=self.expected_type)
176+
for item in set(self) - set(other):
177+
result.add(item)
178+
for item in set(other) - set(self):
179+
result.add(item) # Validates
180+
return result
181+
182+
def __ior__(self, other):
183+
# Handle |= operator
184+
for item in other:
185+
self.add(item) # Delegates to add() which validates
186+
return self
187+
188+
def __iand__(self, other):
189+
# Handle &= operator (intersection) - only keeps existing valid items
190+
super().__iand__(other)
191+
return self
192+
193+
def __isub__(self, other):
194+
# Handle -= operator (difference) - only removes items
195+
super().__isub__(other)
196+
return self
197+
198+
def __ixor__(self, other):
199+
# Handle ^= operator (symmetric difference)
200+
# Items from other need validation before being added
201+
to_add = set(other) - set(self)
202+
to_remove = set(self) & set(other)
203+
for item in to_remove:
204+
self.discard(item)
205+
for item in to_add:
206+
self.add(item) # Validates
207+
return self

0 commit comments

Comments
 (0)