Skip to content

Commit 2a95766

Browse files
committed
Add new-style JSON field presence checks
Add `Present` and `Absent` classes and deprecate `Exists`.
1 parent be49c43 commit 2a95766

File tree

6 files changed

+88
-13
lines changed

6 files changed

+88
-13
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,17 @@ python-asserts adheres to [semantic versioning](https://semver.org/).
77
### Added
88

99
- Add support for Python 3.12.
10+
- Add `Present` and `Absent` for absence checks in `assert_json_subset()`.
1011

1112
### Removed
1213

1314
- Drop support for Python 3.7.
1415

16+
### Deprecated
17+
18+
- Deprecate `Exists` in favor of `Present` and `Absent` in
19+
`assert_json_subset()`.
20+
1521
## [0.12.0]
1622

1723
### Added

asserts/__init__.py

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
from typing import Any, Callable, Set
3030
from warnings import WarningMessage, catch_warnings
3131

32+
from typing_extensions import deprecated
33+
3234

3335
def fail(msg=None):
3436
"""Raise an AssertionError with the given message.
@@ -1354,8 +1356,8 @@ def assert_json_subset(first, second):
13541356
...
13551357
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
13561358
1357-
In objects, the special name `Exists` can be used to check for the
1358-
existence or non-existence of a specific key:
1359+
In objects, the special classes `Present` and `Absent` can be used to
1360+
check for the presence or absence of a specific key:
13591361
13601362
>>> assert_json_subset({Exists("foo"): True}, '{"foo": "bar"}')
13611363
>>> assert_json_subset({Exists("foo"): True}, '{}')
@@ -1401,6 +1403,8 @@ def assert_(self):
14011403
self._assert_dicts_equal()
14021404
elif isinstance(self._expected, list):
14031405
self._assert_arrays_equal()
1406+
elif _is_present(self._expected):
1407+
pass
14041408
else:
14051409
self._assert_fundamental_values_equal()
14061410

@@ -1413,6 +1417,8 @@ def _types_differ(self):
14131417
return self._actual is not None
14141418
elif isinstance(self._expected, (int, float)):
14151419
return not isinstance(self._actual, (int, float))
1420+
elif _is_present(self._expected):
1421+
return False
14161422
for type_ in [bool, str, _Str, list, dict]:
14171423
if isinstance(self._expected, type_):
14181424
return not isinstance(self._actual, type_)
@@ -1436,24 +1442,29 @@ def _assert_all_expected_keys_in_actual_dict(self) -> None:
14361442

14371443
def _assert_no_wrong_keys(self) -> None:
14381444
for name in self._expected:
1445+
if isinstance(name, str) and _is_absent(self._expected[name]):
1446+
if name in self._actual:
1447+
self._raise_assertion_error(
1448+
f"spurious member '{name}' in object {{path}}"
1449+
)
14391450
if isinstance(name, Exists) and not self._expected[name]:
14401451
if name.member_name in self._actual:
14411452
self._raise_assertion_error(
1442-
f"spurious member '{name.member_name}' in "
1443-
f"object {{path}}"
1453+
f"spurious member '{name.member_name}' in object {{path}}"
14441454
)
14451455

14461456
def _assert_dict_values_equal(self) -> None:
14471457
for name in self._expected:
1448-
if isinstance(name, str):
1458+
if isinstance(name, str) and not _is_absent(self._expected[name]):
14491459
self._assert_json_value_equals_with_item(name)
14501460

14511461
@property
14521462
def _expected_key_names(self) -> Set[str]:
14531463
keys: Set[str] = set()
14541464
for k in self._expected.keys():
14551465
if isinstance(k, str):
1456-
keys.add(k)
1466+
if not _is_absent(self._expected[k]):
1467+
keys.add(k)
14571468
elif isinstance(k, Exists) and self._expected[k]:
14581469
keys.add(k.member_name)
14591470
return keys
@@ -1523,6 +1534,23 @@ def append(self, item):
15231534
return _JSONPath("{0}[{1}]".format(self._path, repr(item)))
15241535

15251536

1537+
class Present:
1538+
"""Helper class for presence checks in assert_json_subset()."""
1539+
1540+
1541+
def _is_present(o: object) -> bool:
1542+
return o is Present or isinstance(o, Present)
1543+
1544+
1545+
class Absent:
1546+
"""Helper class for absence checks in assert_json_subset()."""
1547+
1548+
1549+
def _is_absent(o: object) -> bool:
1550+
return o is Absent or isinstance(o, Absent)
1551+
1552+
1553+
@deprecated("Use Present and Absent instead.")
15261554
class Exists:
15271555
"""Helper class for existence checks in assert_json_subset()."""
15281556

asserts/__init__.pyi

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ from re import Pattern
55
from types import TracebackType
66
from typing import Any, Generic, NoReturn, TypeVar
77

8+
from typing_extensions import deprecated
9+
810
_E = TypeVar("_E", bound=BaseException)
911
_S = TypeVar("_S")
1012

@@ -163,6 +165,10 @@ def assert_json_subset(
163165
second: dict[str, Any] | list[Any] | str | bytes,
164166
) -> None: ...
165167

168+
class Present: ...
169+
class Absent: ...
170+
171+
@deprecated("Use Present and Absent instead.")
166172
class Exists:
167173
member_name: str
168174
def __init__(self, member_name: str) -> None: ...

poetry.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ include = ["*/py.typed", "*.pyi"]
2323

2424
[tool.poetry.dependencies]
2525
python = ">=3.8.1"
26+
typing-extensions = "^4.10.0"
2627

2728
[tool.poetry.group.dev.dependencies]
2829
mypy = "~1.9.0"

test_asserts.py

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
from datetime import datetime, timedelta
77
from json import JSONDecodeError
88
from unittest import TestCase
9-
from warnings import catch_warnings, warn
9+
from warnings import catch_warnings, simplefilter, warn
1010

1111
from asserts import (
12+
Absent,
1213
Exists,
14+
Present,
1315
assert_almost_equal,
1416
assert_between,
1517
assert_boolean_false,
@@ -1483,18 +1485,50 @@ def test_assert_json_subset__element_name_not_str(self) -> None:
14831485
):
14841486
assert_json_subset({12: 34}, "{}")
14851487

1486-
def test_assert_json_subset__existence_check(self) -> None:
1488+
def test_assert_json_subset__presence_check(self) -> None:
14871489
with assert_succeeds(AssertionError):
1488-
assert_json_subset({Exists("foo"): True}, {"foo": "bar"})
1490+
assert_json_subset({"foo": Present}, {"foo": "bar"})
1491+
with assert_succeeds(AssertionError):
1492+
assert_json_subset({"foo": Present()}, {"foo": "bar"})
1493+
with assert_raises_regex(
1494+
AssertionError,
1495+
r"element 'foo' missing from element \$",
1496+
):
1497+
assert_json_subset({"foo": Present}, {})
14891498
with assert_raises_regex(
14901499
AssertionError,
14911500
r"element 'foo' missing from element \$",
14921501
):
1493-
assert_json_subset({Exists("foo"): True}, {})
1502+
assert_json_subset({"foo": Present()}, {})
1503+
with assert_succeeds(AssertionError):
1504+
assert_json_subset({"foo": Absent}, {})
14941505
with assert_succeeds(AssertionError):
1495-
assert_json_subset({Exists("foo"): False}, {})
1506+
assert_json_subset({"foo": Absent()}, {})
14961507
with assert_raises_regex(
14971508
AssertionError,
14981509
r"spurious member 'foo' in object \$",
14991510
):
1500-
assert_json_subset({Exists("foo"): False}, {"foo": "bar"})
1511+
assert_json_subset({"foo": Absent}, {"foo": "bar"})
1512+
with assert_raises_regex(
1513+
AssertionError,
1514+
r"spurious member 'foo' in object \$",
1515+
):
1516+
assert_json_subset({"foo": Absent()}, {"foo": "bar"})
1517+
1518+
def test_assert_json_subset__existence_check_old(self) -> None:
1519+
with catch_warnings(category=DeprecationWarning):
1520+
simplefilter("ignore")
1521+
with assert_succeeds(AssertionError):
1522+
assert_json_subset({Exists("foo"): True}, {"foo": "bar"})
1523+
with assert_raises_regex(
1524+
AssertionError,
1525+
r"element 'foo' missing from element \$",
1526+
):
1527+
assert_json_subset({Exists("foo"): True}, {})
1528+
with assert_succeeds(AssertionError):
1529+
assert_json_subset({Exists("foo"): False}, {})
1530+
with assert_raises_regex(
1531+
AssertionError,
1532+
r"spurious member 'foo' in object \$",
1533+
):
1534+
assert_json_subset({Exists("foo"): False}, {"foo": "bar"})

0 commit comments

Comments
 (0)