Skip to content

Commit 219d48e

Browse files
committed
Add is_mapping_with matcher
1 parent d6481ca commit 219d48e

File tree

4 files changed

+69
-7
lines changed

4 files changed

+69
-7
lines changed

README.rst

+12
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,18 @@ For instance, ``has_attrs(name="bob")`` is equivalent to ``has_attrs(name=equal_
117117
"b": equal_to(4),
118118
}))
119119
120+
* ``is_mapping_with(**matchers)``: matches a mapping, such as a ``dict``, if it has the same keys with matching values.
121+
An error will be raised if the mapping is missing any keys, but allows extra keys.
122+
For instance:
123+
124+
.. code:: python
125+
126+
result = {"a": 1, "b": 4, "c": 5}
127+
assert_that(result, is_mapping_with({
128+
"a": equal_to(1),
129+
"b": equal_to(4),
130+
}))
131+
120132
* ``anything``: matches all values.
121133

122134
* ``is_instance(type)``: matches any value where ``isinstance(value, type)``.

precisely/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from .iterable_matchers import all_elements, contains_exactly, includes, is_sequence
66
from .feature_matchers import has_feature
77
from .function_matchers import raises
8-
from .mapping_matchers import is_mapping
8+
from .mapping_matchers import is_mapping, is_mapping_with
99
from .results import indent as _indent
1010

1111

@@ -35,6 +35,7 @@
3535
"is_sequence",
3636
"has_feature",
3737
"is_mapping",
38+
"is_mapping_with",
3839
"raises",
3940
]
4041

precisely/mapping_matchers.py

+12-6
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,21 @@
33
from .results import matched, unmatched, indented_list
44

55

6-
def is_mapping(matchers):
6+
def is_mapping(matchers, allow_extra=False):
77
return IsMappingMatcher(dict(
88
(key, to_matcher(matcher))
99
for key, matcher in matchers.items()
10-
))
10+
), allow_extra)
11+
12+
13+
def is_mapping_with(matchers):
14+
return is_mapping(matchers, allow_extra=True)
1115

1216

1317
class IsMappingMatcher(Matcher):
14-
def __init__(self, matchers):
18+
def __init__(self, matchers, allow_extra):
1519
self._matchers = matchers
20+
self._allow_extra = allow_extra
1621

1722
def match(self, actual):
1823
undefined = object()
@@ -25,9 +30,10 @@ def match(self, actual):
2530
if not value_result.is_match:
2631
return unmatched("value for key {0!r} mismatched:{1}".format(key, indented_list([value_result.explanation])))
2732

28-
extra_keys = set(actual.keys()) - set(self._matchers.keys())
29-
if extra_keys:
30-
return unmatched("had extra keys:{0}".format(indented_list(sorted(map(repr, extra_keys)))))
33+
if not self._allow_extra:
34+
extra_keys = set(actual.keys()) - set(self._matchers.keys())
35+
if extra_keys:
36+
return unmatched("had extra keys:{0}".format(indented_list(sorted(map(repr, extra_keys)))))
3137

3238
return matched()
3339

tests/is_mapping_with_tests.py

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from nose.tools import istest, assert_equal
2+
3+
from precisely import equal_to, is_mapping_with
4+
from precisely.results import matched, unmatched
5+
6+
7+
@istest
8+
def matches_when_keys_and_values_match():
9+
matcher = is_mapping_with({"a": equal_to(1), "b": equal_to(2)})
10+
assert_equal(matched(), matcher.match({"a": 1, "b": 2}))
11+
12+
13+
@istest
14+
def values_are_coerced_to_matchers():
15+
matcher = is_mapping_with({"a": 1, "b": 2})
16+
assert_equal(matched(), matcher.match({"a": 1, "b": 2}))
17+
18+
19+
@istest
20+
def does_not_match_when_value_does_not_match():
21+
matcher = is_mapping_with({"a": equal_to(1), "b": equal_to(2)})
22+
assert_equal(
23+
unmatched("value for key 'b' mismatched:\n * was 3"),
24+
matcher.match({"a": 1, "b": 3, "c": 4}),
25+
)
26+
27+
28+
@istest
29+
def does_not_match_when_keys_are_missing():
30+
matcher = is_mapping_with({"a": equal_to(1), "b": equal_to(2)})
31+
assert_equal(unmatched("was missing key: 'b'"), matcher.match({"a": 1}))
32+
33+
34+
@istest
35+
def matches_when_there_are_extra_keys():
36+
matcher = is_mapping_with({"a": equal_to(1)})
37+
assert_equal(matched(), matcher.match({"a": 1, "b": 1, "c": 1}))
38+
39+
40+
@istest
41+
def description_describes_keys_and_value_matchers():
42+
matcher = is_mapping_with({"a": equal_to(1), "b": equal_to(2)})
43+
assert_equal("mapping with items:\n * 'a': 1\n * 'b': 2", matcher.describe())

0 commit comments

Comments
 (0)