Skip to content

Commit 7917d82

Browse files
authored
Merge pull request #13 from ErwinJunge/feature/is_mapping_with
Add mapping_includes matcher
2 parents d96038f + d4476ce commit 7917d82

File tree

4 files changed

+77
-5
lines changed

4 files changed

+77
-5
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+
* ``mapping_includes(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, mapping_includes({
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, mapping_includes
99
from .results import indent as _indent
1010

1111

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

precisely/mapping_matchers.py

+20-4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ def is_mapping(matchers):
1111

1212

1313
class IsMappingMatcher(Matcher):
14+
_describe_message = "mapping with items:{0}"
15+
_allow_extra = False
16+
1417
def __init__(self, matchers):
1518
self._matchers = matchers
1619

@@ -25,14 +28,27 @@ def match(self, actual):
2528
if not value_result.is_match:
2629
return unmatched("value for key {0!r} mismatched:{1}".format(key, indented_list([value_result.explanation])))
2730

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)))))
31+
if not self._allow_extra:
32+
extra_keys = set(actual.keys()) - set(self._matchers.keys())
33+
if extra_keys:
34+
return unmatched("had extra keys:{0}".format(indented_list(sorted(map(repr, extra_keys)))))
3135

3236
return matched()
3337

3438
def describe(self):
35-
return "mapping with items:{0}".format(indented_list(sorted(
39+
return self._describe_message.format(indented_list(sorted(
3640
"{0!r}: {1}".format(key, matcher.describe())
3741
for key, matcher in self._matchers.items()
3842
)))
43+
44+
45+
def mapping_includes(matchers):
46+
return MappingIncludesMatcher(dict(
47+
(key, to_matcher(matcher))
48+
for key, matcher in matchers.items()
49+
))
50+
51+
52+
class MappingIncludesMatcher(IsMappingMatcher):
53+
_describe_message = "mapping including items:{0}"
54+
_allow_extra = True

tests/mapping_includes_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, mapping_includes
4+
from precisely.results import matched, unmatched
5+
6+
7+
@istest
8+
def matches_when_keys_and_values_match():
9+
matcher = mapping_includes({"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 = mapping_includes({"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 = mapping_includes({"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 = mapping_includes({"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 = mapping_includes({"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 = mapping_includes({"a": equal_to(1), "b": equal_to(2)})
43+
assert_equal("mapping including items:\n * 'a': 1\n * 'b': 2", matcher.describe())

0 commit comments

Comments
 (0)