Skip to content

Commit eed0538

Browse files
committed
Add existence check support to assert_json_subset()
1 parent 8e023e7 commit eed0538

File tree

5 files changed

+91
-8
lines changed

5 files changed

+91
-8
lines changed

NEWS.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
## News in asserts 0.11.1
2+
3+
### API Additions
4+
5+
* `assert_json_subset()` can now check for the existence or non-existence
6+
of object members using the new `Exists` helper.
7+
* Non-string (or `Exists`) object member names in the first argument to
8+
`assert_json_subset()` now raise a `TypeError`.
9+
110
## News in asserts 0.11.0
211

312
### API-Incompatible Changes

asserts/__init__.py

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import sys
2525
from datetime import datetime, timedelta
2626
from json import loads as json_loads
27+
from typing import Set
2728
from 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

asserts/__init__.pyi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,7 @@ def assert_succeeds(exception: Type[BaseException], msg_fmt: Text = ...) -> Cont
9393
def assert_warns(warning_type: Type[Warning], msg_fmt: Text = ...) -> AssertWarnsContext: ...
9494
def assert_warns_regex(warning_type: Type[Warning], regex: Text, msg_fmt: Text = ...) -> AssertWarnsContext: ...
9595
def assert_json_subset(first: Union[dict, list], second: Union[dict, list, str, bytes]) -> None: ...
96+
97+
class Exists:
98+
member_name: str
99+
def __init__(self, member_name: str) -> None: ...

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def read(fname):
1010

1111
setup(
1212
name="asserts",
13-
version="0.11.0",
13+
version="0.11.1",
1414
description="Stand-alone Assertions",
1515
long_description=read("README.md"),
1616
long_description_content_type="text/markdown",

test_asserts.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
assert_succeeds,
4444
assert_warns,
4545
assert_warns_regex,
46-
assert_json_subset,
46+
assert_json_subset, Exists,
4747
)
4848

4949

@@ -1433,3 +1433,23 @@ def test_assert_json_subset__invalid_type(self):
14331433
TypeError, "second must be dict, list, str, or bytes"
14341434
):
14351435
assert_json_subset({}, 42) # type: ignore
1436+
1437+
def test_assert_json_subset__element_name_not_str(self) -> None:
1438+
with assert_raises_regex(
1439+
TypeError, f"12 is not a valid object member name",
1440+
):
1441+
assert_json_subset({12: 34}, "{}")
1442+
1443+
def test_assert_json_subset__existence_check(self) -> None:
1444+
with assert_succeeds(AssertionError):
1445+
assert_json_subset({Exists("foo"): True}, {"foo": "bar"})
1446+
with assert_raises_regex(
1447+
AssertionError, r"element 'foo' missing from element \$",
1448+
):
1449+
assert_json_subset({Exists("foo"): True}, {})
1450+
with assert_succeeds(AssertionError):
1451+
assert_json_subset({Exists("foo"): False}, {})
1452+
with assert_raises_regex(
1453+
AssertionError, r"spurious member 'foo' in object \$",
1454+
):
1455+
assert_json_subset({Exists("foo"): False}, {"foo": "bar"})

0 commit comments

Comments
 (0)