@@ -96,33 +96,36 @@ def test_contains__with_invalid_input(self):
9696 def test_diff (self ): # Test diff method for debugging
9797 with __ (a = 1 , b = 'test' , c = 3.14 ) as obj1 :
9898 with __ (a = 1 , b = 'test' , c = 3.14 ) as obj2 :
99- assert obj1 .diff (obj2 ) is None # No differences
99+ assert obj1 .diff (obj2 ) == __ () # No differences
100100
101101 with __ (a = 2 , b = 'test' , c = 3.14 ) as obj3 :
102102 diff = obj1 .diff (obj3 )
103- assert diff == {'a' : {'actual' : 1 , 'expected' : 2 }} # Single difference
103+ assert diff == __ (a = __ (actual = 1 , expected = 2 ))
104+ #assert diff == {'a': {'actual': 1, 'expected': 2}} # Single difference
104105
105106 with __ (a = 2 , b = 'changed' , d = 4 ) as obj4 :
106107 diff = obj1 .diff (obj4 )
107- assert 'a' in diff # Changed field
108- assert 'b' in diff # Another changed field
109- assert 'c' in diff # Missing in obj4
110- assert 'd' in diff # Extra in obj4
108+ assert diff == __ (a = __ (actual = 1 , expected = 2 ),
109+ b = __ (actual = 'test' , expected = 'changed' ),
110+ d = __ (actual = __MISSING__ , expected = 4 ),
111+ c = __ (actual = 3.14 , expected = __MISSING__ ))
112+ # assert 'a' in diff # Changed field
113+ # assert 'b' in diff # Another changed field
114+ # assert 'c' in diff # Missing in obj4
115+ # assert 'd' in diff # Extra in obj4
111116
112- def test_diff__with_dict (self ): # Test diff with dict comparison
113- with __ (a = 1 , b = 2 ) as _ :
114- diff = _ .diff ({'a' : 1 , 'b' : 3 })
115- assert diff == {'b' : {'actual' : 2 , 'expected' : 3 }} # Dict comparison works
116117
117118 def test_diff__with_missing_fields (self ): # Test diff with missing fields
118119 with __ (a = 1 , b = 2 ) as obj1 :
119120 with __ (a = 1 ) as obj2 :
120121 diff = obj1 .diff (obj2 )
121- assert diff == {'b' : {'actual' : 2 , 'expected' : __MISSING__ }} # Missing marked correctly
122+ assert diff == __ (b = __ (actual = 2 , expected = __MISSING__ ))
123+ #assert diff == {'b': {'actual': 2, 'expected': __MISSING__}} # Missing marked correctly
122124
123125 with __ (a = 1 , b = 2 , c = 3 ) as obj3 :
124126 diff = obj1 .diff (obj3 )
125- assert diff == {'c' : {'actual' : __MISSING__ , 'expected' : 3 }} # Extra field detected
127+ assert diff == __ (c = __ (actual = __MISSING__ , expected = 3 ))
128+ #assert diff == {'c': {'actual': __MISSING__, 'expected': 3}} # Extra field detected
126129
127130 def test_excluding (self ): # Test excluding fields from comparison
128131 with __ (id = '123' , name = 'Test' , created_at = '2024-01-01' , updated_at = '2024-01-02' ) as _ :
@@ -570,7 +573,9 @@ def test__diff_with_operators(self):
570573 diff = actual .diff (expected )
571574 # diff will show the actual values vs the operator tuples
572575 assert diff is not None
573- assert 'score' in diff or 'duration' in diff # At least one field differs
576+ assert diff == __ (duration = __ (actual = 0.234 , expected = ('lt' , 0.1 )),
577+ score = __ (actual = 85 , expected = ('gt' , 90 )))
578+ #assert 'score' in diff or 'duration' in diff # At least one field differs
574579
575580
576581 # Test operators work with contains() method
@@ -809,15 +814,19 @@ def test__operators_mixed_with_skip_in_nested(self):
809814
810815 def test__diff_shows_operator_tuples (self ): # Test diff reveals operator structure
811816 with __ (score = 85 , duration = 0.234 ) as actual :
812- expected = __ (score = __GREATER_THAN__ ( 90 ) , duration = __LESS_THAN__ ( 0.1 ) )
817+ expected_1 = __ (score = 90 , duration = 0.1 )
813818
814- diff = actual .diff (expected )
819+ diff_1 = actual .diff (expected_1 )
820+ assert type (diff_1 ) is __
821+ assert diff_1 == __ (duration = __ (actual = 0.234 , expected = 0.1 ),
822+ score = __ (actual = 85 , expected = 90 )) # this passes ok
823+
824+ expected_2 = __ (score = __GREATER_THAN__ (90 ), duration = __LESS_THAN__ (0.1 ))
825+ diff_2 = actual .diff (expected_2 )
826+
827+ assert diff_2 == __ (duration = __ (actual = 0.234 , expected = __LESS_THAN__ (0.1 )),
828+ score = __ (actual = 85 , expected = __GREATER_THAN__ (90 ))) # this fails here, but the results are the same
815829
816- # diff should show actual values vs operator tuples
817- assert diff is not None
818- assert 'score' in diff
819- assert diff ['score' ]['actual' ] == 85
820- assert diff ['score' ]['expected' ] == ('gt' , 90 ) # Shows operator structure
821830
822831 def test__diff_with_close_to_tolerance (self ): # Test diff shows CLOSE_TO details
823832 with __ (value = 1.0 ) as actual :
@@ -826,7 +835,8 @@ def test__diff_with_close_to_tolerance(self):
826835 diff = actual .diff (expected )
827836
828837 # diff shows the operator tuple with tolerance
829- assert diff ['value' ]['expected' ] == ('close_to' , 2.0 , 0.5 )
838+ assert diff == __ (value = __ (actual = 1.0 , expected = ('close_to' , 2.0 , 0.5 )))
839+ #assert diff['value']['expected'] == ('close_to', 2.0, 0.5)
830840
831841 # Test operators with real-world patterns
832842
@@ -1189,4 +1199,212 @@ def test_contains__with_kwargs__integration_test(self):
11891199 status = __BETWEEN__ (200 , 299 ),
11901200 request_id = __SKIP__ ,
11911201 metrics = __ (score = __GREATER_THAN__ (85 ))
1192- )
1202+ )
1203+
1204+ def test_contains__with_missing_marker (self ): # Test contains behavior with __MISSING__ marker
1205+ with __ (a = 1 , b = 2 ) as _ :
1206+ assert _ .contains (c = __MISSING__ ) is True # confirms that 'c' is missing
1207+
1208+
1209+ def test__empty_nested_objects (self ): # Test comparison with empty nested __ objects
1210+ with __ (data = __ ()) as obj1 :
1211+ assert obj1 == __ (data = __ ())
1212+ assert obj1 .contains (__ (data = __ ()))
1213+
1214+ diff = obj1 .diff (__ (data = __ (x = 1 )))
1215+ assert diff == __ (data = __ (actual = __ (), expected = __ (x = 1 )))
1216+
1217+ def test__deeply_nested_mixed_operators_and_skip (self ):
1218+ """Test 4+ levels of nesting with operators and skip"""
1219+ complex = __ (
1220+ level1 = __ (
1221+ level2 = __ (
1222+ level3 = __ (
1223+ level4 = __ (value = 42 , timestamp = 999 )
1224+ )
1225+ )
1226+ )
1227+ )
1228+
1229+ assert complex == __ (
1230+ level1 = __ (
1231+ level2 = __ (
1232+ level3 = __ (
1233+ level4 = __ (
1234+ value = __GREATER_THAN__ (40 ),
1235+ timestamp = __SKIP__
1236+ )
1237+ )
1238+ )
1239+ )
1240+ )
1241+
1242+
1243+ def test__diff_return_value_consistency (self ): # Test diff returns __() for no differences
1244+ with __ (a = 1 ) as obj1 :
1245+ with __ (a = 1 ) as obj2 :
1246+ diff = obj1 .diff (obj2 )
1247+ assert diff == __ () # there were no differences
1248+
1249+ def test__diff_ignores_skip_markers (self ): # Test that __SKIP__ values don't appear in diff results
1250+
1251+ # Case 1: __SKIP__ in left side
1252+ with __ (a = 1 , b = __SKIP__ , c = 3 ) as obj1 :
1253+ with __ (a = 1 , b = 2 , c = 3 ) as obj2 :
1254+ diff = obj1 .diff (obj2 )
1255+ assert diff == __ () # No diff because b is skipped
1256+
1257+ # Case 2: __SKIP__ in right side (more common in tests)
1258+ with __ (a = 1 , b = 2 , c = 3 ) as obj1 :
1259+ with __ (a = 1 , b = __SKIP__ , c = 3 ) as obj2 :
1260+ diff = obj1 .diff (obj2 )
1261+ assert diff == __ () # No diff because b is skipped
1262+
1263+ # Case 3: __SKIP__ on both sides
1264+ with __ (a = 1 , b = __SKIP__ ) as obj1 :
1265+ with __ (a = 1 , b = __SKIP__ ) as obj2 :
1266+ diff = obj1 .diff (obj2 )
1267+ assert diff == __ ()
1268+
1269+ # Case 4: Other fields still show differences
1270+ with __ (a = 1 , b = __SKIP__ , c = 3 ) as obj1 :
1271+ with __ (a = 2 , b = 2 , c = 3 ) as obj2 :
1272+ diff = obj1 .diff (obj2 )
1273+ assert diff == __ (a = __ (actual = 1 , expected = 2 )) # Only 'a' differs, 'b' ignored
1274+
1275+
1276+ def test_contains__with_missing_marker__basic (self ):
1277+ """Test __MISSING__ confirms field absence"""
1278+ with __ (a = 1 , b = 2 ) as _ :
1279+ assert _ .contains (c = __MISSING__ ) # Field 'c' doesn't exist - passes
1280+ assert _ .contains (d = __MISSING__ ) # Field 'd' doesn't exist - passes
1281+ assert not _ .contains (a = __MISSING__ ) # Field 'a' EXISTS - fails
1282+ assert not _ .contains (b = __MISSING__ ) # Field 'b' EXISTS - fails
1283+
1284+ def test_contains__with_missing_marker__mixed_with_existing (self ):
1285+ """Test __MISSING__ combined with regular field checks"""
1286+ with __ (a = 1 , b = 2 , c = 3 ) as _ :
1287+ # Check some fields exist AND some don't exist
1288+ assert _ .contains (a = 1 , d = __MISSING__ ) # 'a' exists with value 1, 'd' missing
1289+ assert _ .contains (b = 2 , c = 3 , x = __MISSING__ ) # Multiple exist, one missing
1290+ assert not _ .contains (a = 1 , b = __MISSING__ ) # 'b' exists, expected missing - fails
1291+
1292+ def test_contains__with_missing_marker__nested_objects (self ):
1293+ """Test __MISSING__ with nested structures"""
1294+ with __ (user = __ (id = 'u1' , name = 'Alice' ), config = __ (timeout = 30 )) as _ :
1295+ # Check top-level field is missing
1296+ assert _ .contains (settings = __MISSING__ ) # 'settings' doesn't exist
1297+
1298+ # Check nested field exists, but another top-level is missing
1299+ assert _ .contains (user = __ (id = 'u1' ), metadata = __MISSING__ )
1300+
1301+ # Field exists when expected missing - fails
1302+ assert not _ .contains (user = __MISSING__ ) # 'user' EXISTS
1303+
1304+ def test_contains__with_missing_marker__with_operators (self ):
1305+ """Test __MISSING__ combined with comparison operators"""
1306+ with __ (score = 85 , duration = 0.234 ) as _ :
1307+ assert _ .contains (
1308+ score = __GREATER_THAN__ (80 ), # Field exists and passes operator
1309+ latency = __MISSING__ # Field doesn't exist
1310+ )
1311+
1312+ assert _ .contains (
1313+ duration = __LESS_THAN__ (0.5 ),
1314+ timestamp = __MISSING__ ,
1315+ request_id = __MISSING__
1316+ )
1317+
1318+ def test_contains__with_missing_marker__all_missing (self ):
1319+ """Test checking multiple missing fields"""
1320+ with __ (a = 1 ) as _ :
1321+ assert _ .contains (b = __MISSING__ , c = __MISSING__ , d = __MISSING__ ) # All missing
1322+ assert not _ .contains (a = __MISSING__ , b = __MISSING__ ) # 'a' exists
1323+
1324+ def test_contains__with_missing_marker__empty_object (self ):
1325+ """Test __MISSING__ on completely empty object"""
1326+ with __ () as _ :
1327+ assert _ .contains (a = __MISSING__ ) # Everything is missing in empty object
1328+ assert _ .contains (x = __MISSING__ , y = __MISSING__ )
1329+
1330+ def test_contains__with_missing_marker__real_world_validation (self ):
1331+ """Real-world pattern: validate deprecated fields removed"""
1332+ api_response_v2 = __ (
1333+ status = 200 ,
1334+ data = __ (items = [1 , 2 , 3 ], total = 3 ),
1335+ metadata = __ (version = '2.0' )
1336+ )
1337+
1338+ # Validate new fields exist AND old deprecated fields are gone
1339+ assert api_response_v2 .contains (
1340+ status = 200 , # New field exists
1341+ data = __ (total = 3 ), # Nested field exists
1342+ deprecated_user_id = __MISSING__ , # Old field removed
1343+ legacy_timestamp = __MISSING__ # Old field removed
1344+ )
1345+
1346+ def test_contains__with_missing_marker__with_skip (self ):
1347+ """Test __MISSING__ combined with __SKIP__"""
1348+ with __ (a = 1 , b = 2 , c = 3 ) as _ :
1349+ assert _ .contains (
1350+ a = __SKIP__ , # Don't care about 'a' value
1351+ d = __MISSING__ , # 'd' must not exist
1352+ b = 2 # 'b' must equal 2
1353+ )
1354+
1355+ def test_contains__with_missing_marker__validates_field_removal (self ):
1356+ """Test pattern for validating fields were removed in transformation"""
1357+ original = __ (id = '123' , name = 'Test' , password = 'secret' , ssn = '123-45-6789' )
1358+
1359+ # After sanitization
1360+ sanitized = __ (id = '123' , name = 'Test' )
1361+
1362+ # Validate sensitive fields removed
1363+ assert sanitized .contains (
1364+ id = '123' , # Keep public fields
1365+ name = 'Test' ,
1366+ password = __MISSING__ , # Sensitive removed
1367+ ssn = __MISSING__ # Sensitive removed
1368+ )
1369+
1370+ # Original should NOT contain missing (fields exist)
1371+ assert not original .contains (password = __MISSING__ )
1372+ assert not original .contains (ssn = __MISSING__ )
1373+
1374+ def test_contains__with_missing_marker__object_syntax (self ):
1375+ """Test __MISSING__ with __ object syntax (not kwargs)"""
1376+ with __ (a = 1 , b = 2 ) as _ :
1377+ assert _ .contains (__ (c = __MISSING__ )) # Object syntax
1378+ assert _ .contains (__ (a = 1 , d = __MISSING__ )) # Mixed
1379+ assert not _ .contains (__ (a = __MISSING__ )) # Field exists
1380+
1381+
1382+
1383+
1384+ ##############
1385+ # KNOWN bugs
1386+
1387+ # this documents this scenario
1388+ def test__bug__circular_reference_handling__recursion_is_not_handled (self ): # Test behavior with circular references
1389+ obj1 = __ (a = 1 , b = 2 )
1390+ obj1 .self_ref = obj1 # Circular reference
1391+
1392+ obj2 = __ (a = 1 , b = 2 )
1393+ obj2 .self_ref = obj2
1394+ error_message = "maximum recursion depth exceeded"
1395+ with pytest .raises (RecursionError , match = error_message ): # Should this handle gracefully or infinite loop?
1396+ assert obj1 == obj2
1397+
1398+ def test__bug__diff_with_nested_operators (self ): # Test diff with operators in nested structures
1399+ actual = __ (user = __ (score = 85 , name = 'Alice' ),
1400+ stats = __ (wins = 10 ) )
1401+
1402+ expected = __ (user = __ (score = __GREATER_THAN__ (90 ), name = 'Alice' ),
1403+ stats = __ (wins = __BETWEEN__ (5 , 15 )))
1404+
1405+ diff = actual .diff (expected )
1406+
1407+ assert diff == __ (stats = __ (actual = __ (wins = 10 ),
1408+ expected = __ (wins = ('between' , 5 , 15 ))), # BUG
1409+ user = __ (actual = __ (score = 85 , name = 'Alice' ),
1410+ expected = __ (score = ('gt' , 90 ), name = 'Alice' ))) # Correct
0 commit comments