2424import sys
2525from datetime import datetime , timedelta
2626from json import loads as json_loads
27+ from typing import Set
2728from warnings import catch_warnings
2829
2930
@@ -1305,7 +1306,7 @@ def assert_json_subset(first, second):
13051306
13061307 A JSON object is the subset of another JSON object if for each name/value
13071308 pair in the former there is a name/value pair in the latter with the same
1308- name. Additionally the value of the former pair must be a subset of the
1309+ name. Additionally, the value of the former pair must be a subset of the
13091310 value of the latter pair.
13101311
13111312 A JSON array is the subset of another JSON array, if they have the same
@@ -1328,6 +1329,20 @@ def assert_json_subset(first, second):
13281329 Traceback (most recent call last):
13291330 ...
13301331 json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
1332+
1333+ In objects, the special name `Exists` can be used to check for the
1334+ existence or non-existence of a specific key:
1335+
1336+ >>> assert_json_subset({Exists("foo"): True}, '{"foo": "bar"}')
1337+ >>> assert_json_subset({Exists("foo"): True}, '{}')
1338+ Traceback (most recent call last):
1339+ ...
1340+ AssertionError: element 'foo' missing from element $
1341+ >>> assert_json_subset({Exists("foo"): False}, '{}')
1342+ >>> assert_json_subset({Exists("foo"): False}, '{"foo": "bar"}')
1343+ Traceback (most recent call last):
1344+ ...
1345+ AssertionError: spurious member 'foo' in object $
13311346 """
13321347
13331348 if not isinstance (second , (dict , list , str , bytes )):
@@ -1380,16 +1395,45 @@ def _types_differ(self):
13801395 else :
13811396 raise TypeError ("unsupported type {}" .format (type (self ._expected )))
13821397
1383- def _assert_dicts_equal (self ):
1384- self ._assert_all_expected_keys_in_actual_dict ()
1398+ def _assert_dicts_equal (self ) -> None :
13851399 for name in self ._expected :
1386- self ._assert_json_value_equals_with_item (name )
1400+ if not isinstance (name , (str , Exists )):
1401+ raise TypeError (
1402+ f"{ repr (name )} is not a valid object member name" ,
1403+ )
1404+ self ._assert_all_expected_keys_in_actual_dict ()
1405+ self ._assert_no_wrong_keys ()
1406+ self ._assert_dict_values_equal ()
13871407
1388- def _assert_all_expected_keys_in_actual_dict (self ):
1389- keys = set ( self ._expected . keys ()) .difference (self ._actual .keys ())
1408+ def _assert_all_expected_keys_in_actual_dict (self ) -> None :
1409+ keys = self ._expected_key_names .difference (self ._actual .keys ())
13901410 if keys :
13911411 self ._raise_missing_element (keys )
13921412
1413+ def _assert_no_wrong_keys (self ) -> None :
1414+ for name in self ._expected :
1415+ if isinstance (name , Exists ) and not self ._expected [name ]:
1416+ if name .member_name in self ._actual :
1417+ self ._raise_assertion_error (
1418+ f"spurious member '{ name .member_name } ' in "
1419+ f"object {{path}}"
1420+ )
1421+
1422+ def _assert_dict_values_equal (self ) -> None :
1423+ for name in self ._expected :
1424+ if isinstance (name , str ):
1425+ self ._assert_json_value_equals_with_item (name )
1426+
1427+ @property
1428+ def _expected_key_names (self ) -> Set [str ]:
1429+ keys : Set [str ] = set ()
1430+ for k in self ._expected .keys ():
1431+ if isinstance (k , str ):
1432+ keys .add (k )
1433+ elif isinstance (k , Exists ) and self ._expected [k ]:
1434+ keys .add (k .member_name )
1435+ return keys
1436+
13931437 def _assert_arrays_equal (self ):
13941438 if len (self ._expected ) != len (self ._actual ):
13951439 self ._raise_different_sizes ()
@@ -1453,3 +1497,9 @@ def __str__(self):
14531497
14541498 def append (self , item ):
14551499 return _JSONPath ("{0}[{1}]" .format (self ._path , repr (item )))
1500+
1501+
1502+ class Exists :
1503+ """Helper class for existence checks in assert_json_subset()."""
1504+ def __init__ (self , member_name : str ) -> None :
1505+ self .member_name = member_name
0 commit comments