Skip to content

Commit dc3efee

Browse files
committed
improved __().contains() method
1 parent 29fa627 commit dc3efee

File tree

2 files changed

+203
-2
lines changed

2 files changed

+203
-2
lines changed

osbot_utils/testing/__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,14 @@ def __eq__(self, other):
5454
return False
5555
return True
5656

57-
def contains(self, other):
57+
def contains(self, other=None, **kwargs):
58+
if other is not None and kwargs:
59+
raise ValueError("Cannot mix positional and keyword arguments in contains(). "
60+
"Use either _.contains(__(a=1, b=2)) or _.contains(a=1, b=2), not both.")
61+
62+
if kwargs: # If kwargs provided, use them instead of 'other'
63+
other = kwargs
64+
5865
other_dict = getattr(other, '__dict__', other) if hasattr(other, '__dict__') else other if isinstance(other, dict) else None
5966
if other_dict is None:
6067
return False

tests/unit/testing/test__.py

Lines changed: 195 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -995,4 +995,198 @@ def test__operators_preserve_original_object(self):
995995
_ = (original == __(score=__GREATER_THAN__(80), duration=__LESS_THAN__(0.5)))
996996

997997
assert original.score == 85 # Original unchanged
998-
assert original.duration == 0.234 # Original unchanged
998+
assert original.duration == 0.234 # Original unchanged
999+
1000+
def test_contains__with_kwargs__basic(self): # Test contains with kwargs syntax
1001+
with __(id='123', name='Test', age=25, status='active') as _:
1002+
assert _.contains(id='123') # Single field as kwarg
1003+
assert _.contains(id='123', name='Test') # Multiple fields as kwargs
1004+
assert _.contains(id='123', name='Test', age=25) # More fields as kwargs
1005+
assert not _.contains(id='456') # Wrong value
1006+
assert not _.contains(missing_field='value') # Non-existent field
1007+
1008+
def test_contains__with_kwargs__vs_object_syntax(self): # Test both syntaxes produce same results
1009+
with __(a=1, b=2, c=3) as _:
1010+
# Both syntaxes should work identically
1011+
assert _.contains(__(a=1, b=2)) == _.contains(a=1, b=2)
1012+
assert _.contains(__(a=1)) == _.contains(a=1)
1013+
assert _.contains(__(c=3)) == _.contains(c=3)
1014+
1015+
def test_contains__with_kwargs__with_operators(self): # Test kwargs with comparison operators
1016+
with __(score=85, duration=0.234, count=100) as _:
1017+
assert _.contains(score=__GREATER_THAN__(80)) # Single operator
1018+
assert _.contains(duration=__LESS_THAN__(0.5)) # Different operator
1019+
assert _.contains(count=__BETWEEN__(50, 150)) # Range operator
1020+
1021+
# Multiple operators
1022+
assert _.contains(score =__GREATER_THAN__(80 ),
1023+
duration=__LESS_THAN__ (0.5))
1024+
1025+
# Mix operators and exact values
1026+
assert _.contains(score=__GREATER_THAN__(80),
1027+
count=100)
1028+
1029+
def test_contains__with_kwargs__with_skip(self): # Test kwargs with __SKIP__ marker
1030+
with __(id='123', name='Test', timestamp=999) as _:
1031+
assert _.contains(id='123', timestamp=__SKIP__) # Skip timestamp
1032+
assert _.contains(id=__SKIP__, name='Test') # Skip id
1033+
assert _.contains(id=__SKIP__, name='Test', timestamp=__SKIP__) # Skip multiple
1034+
1035+
def test_contains__with_kwargs__nested_objects(self): # Test kwargs doesn't work with nested (by design)
1036+
with __(user=__(id='u1', name='Alice'), settings=__(theme='dark')) as _:
1037+
# Kwargs can only check top-level fields
1038+
assert _.contains(user=__(id='u1')) # Must use __ for nested
1039+
# Can't do: _.contains(user=id='u1') # This would fail - nested needs __
1040+
1041+
def test_contains__with_kwargs__empty(self): # Test contains with no arguments
1042+
with __(a=1, b=2) as _:
1043+
result = _.contains() # Empty kwargs should return False
1044+
assert result == False # Should match empty
1045+
1046+
def test_contains__kwargs_cannot_mix_with_positional(self): # Test you can't mix both syntaxes
1047+
with __(a=1, b=2, c=3) as _:
1048+
assert _.contains(__(a=1, b=2)) # Positional __ object
1049+
assert _.contains(a=1, b=2) # Kwargs
1050+
error_message = ("Cannot mix positional and keyword arguments in contains(). "
1051+
"Use either _.contains(__(a=1, b=2)) or _.contains(a=1, b=2), not both.")
1052+
with pytest.raises(ValueError, match=re.escape(error_message)) :
1053+
assert _.contains(__(a=1), b=2) # we can't use both techniques
1054+
1055+
1056+
def test_contains__with_kwargs__real_world_example(self): # Real-world pattern test
1057+
api_response = __(status=200,
1058+
headers=__(content_type='application/json', x_request_id='req-123'),
1059+
data=__(items=[1, 2, 3], total=3),
1060+
timing=__(duration=0.234, cached=False))
1061+
1062+
# Old syntax (still works)
1063+
assert api_response.contains(__(status=200))
1064+
1065+
# New syntax (cleaner!)
1066+
assert api_response.contains(status=200)
1067+
assert api_response.contains(status=200, timing=__(duration=__LESS_THAN__(0.5)))
1068+
1069+
def test_contains__with_kwargs__all_types(self): # Test kwargs with various Python types
1070+
with __(
1071+
string='test',
1072+
integer=42,
1073+
floating=3.14,
1074+
boolean=True,
1075+
none_val=None,
1076+
list_val=[1, 2, 3],
1077+
dict_val={'key': 'value'}
1078+
) as _:
1079+
assert _.contains(string='test') # String
1080+
assert _.contains(integer=42) # Int
1081+
assert _.contains(floating=3.14) # Float
1082+
assert _.contains(boolean=True) # Bool
1083+
assert _.contains(none_val=None) # None
1084+
assert _.contains(list_val=[1, 2, 3]) # List
1085+
assert _.contains(dict_val={'key': 'value'}) # Dict
1086+
1087+
def test_contains__with_kwargs__operators_all_types(self): # Test all operators with kwargs syntax
1088+
with __(
1089+
score=85,
1090+
duration=0.234,
1091+
probability=0.753,
1092+
count=42
1093+
) as _:
1094+
# Greater than
1095+
assert _.contains(score=__GREATER_THAN__(80))
1096+
1097+
# Less than
1098+
assert _.contains(duration=__LESS_THAN__(0.5))
1099+
1100+
# Between
1101+
assert _.contains(probability=__BETWEEN__(0.7, 0.8))
1102+
1103+
# Close to
1104+
assert _.contains(probability=__CLOSE_TO__(0.75, tolerance=0.01))
1105+
1106+
# Multiple operators
1107+
assert _.contains(score = __GREATER_THAN__(80 ),
1108+
duration = __LESS_THAN__ (0.5 ),
1109+
count = __BETWEEN__ (40, 50))
1110+
1111+
def test_contains__with_kwargs__partial_match(self): # Test partial matching with kwargs
1112+
with __(
1113+
id='test-123',
1114+
name='Test User',
1115+
1116+
age=25,
1117+
status='active',
1118+
created_at='2024-01-01'
1119+
) as _:
1120+
# Can check any subset of fields
1121+
assert _.contains(id='test-123') # Just one field
1122+
assert _.contains(name='Test User', status='active') # Two fields
1123+
assert _.contains(email='[email protected]', age=25, status='active') # Three fields
1124+
1125+
def test_contains__with_kwargs__comparison_with_object_syntax_operators(self): # Compare both syntaxes with operators
1126+
with __(score=85, duration=0.234, count=100, status='active') as _:
1127+
# Both syntaxes should behave identically
1128+
1129+
# Greater than
1130+
assert _.contains(__(score=__GREATER_THAN__(80)))
1131+
assert _.contains(score=__GREATER_THAN__(80))
1132+
1133+
# Multiple with operators
1134+
old_syntax = _.contains(__(
1135+
score=__GREATER_THAN__(80),
1136+
duration=__LESS_THAN__(0.5),
1137+
status='active'
1138+
))
1139+
1140+
new_syntax = _.contains(
1141+
score=__GREATER_THAN__(80),
1142+
duration=__LESS_THAN__(0.5),
1143+
status='active'
1144+
)
1145+
1146+
assert old_syntax == new_syntax == True
1147+
1148+
def test_contains__with_kwargs__failure_cases(self): # Test failure cases with kwargs
1149+
with __(score=85, name='test', status='active') as _:
1150+
assert not _.contains(score=90) # Wrong value
1151+
assert not _.contains(missing='value') # Missing field
1152+
assert not _.contains(score=__GREATER_THAN__(90)) # Operator fails
1153+
assert not _.contains(name='test', score=90) # One field wrong
1154+
1155+
def test_contains__with_kwargs__integration_test(self): # Integration test combining features
1156+
complex_obj = __(
1157+
request_id='req-abc-123',
1158+
timestamp=1234567890,
1159+
status=200,
1160+
timing=__(
1161+
total=0.456,
1162+
db_query=0.234,
1163+
render=0.111
1164+
),
1165+
metrics=__(
1166+
accuracy=0.953,
1167+
score=87.5,
1168+
count=1024
1169+
),
1170+
metadata=__(
1171+
version='2.1.0',
1172+
cached=False
1173+
)
1174+
)
1175+
1176+
# Old syntax still works
1177+
assert complex_obj.contains(__(status=200, request_id=__SKIP__))
1178+
1179+
# New syntax is cleaner for top-level checks
1180+
assert complex_obj.contains(status=200, request_id=__SKIP__)
1181+
assert complex_obj.contains(
1182+
status=200,
1183+
timestamp=__SKIP__,
1184+
timing=__(total=__LESS_THAN__(0.5))
1185+
)
1186+
1187+
# With operators
1188+
assert complex_obj.contains(
1189+
status=__BETWEEN__(200, 299),
1190+
request_id=__SKIP__,
1191+
metrics=__(score=__GREATER_THAN__(85))
1192+
)

0 commit comments

Comments
 (0)