Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,18 @@ For instance, ``has_attrs(name="bob")`` is equivalent to ``has_attrs(name=equal_
"b": equal_to(4),
}))

* ``mapping_includes(matchers)``: matches a mapping, such as a ``dict``, if it has the same keys with matching values.
An error will be raised if the mapping is missing any keys, but allows extra keys.
For instance:

.. code:: python

result = {"a": 1, "b": 4, "c": 5}
assert_that(result, mapping_includes({
"a": equal_to(1),
"b": equal_to(4),
}))

* ``anything``: matches all values.

* ``is_instance(type)``: matches any value where ``isinstance(value, type)``.
Expand Down
3 changes: 2 additions & 1 deletion precisely/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from .iterable_matchers import all_elements, contains_exactly, includes, is_sequence
from .feature_matchers import has_feature
from .function_matchers import raises
from .mapping_matchers import is_mapping
from .mapping_matchers import is_mapping, mapping_includes
from .results import indent as _indent


Expand Down Expand Up @@ -35,6 +35,7 @@
"is_sequence",
"has_feature",
"is_mapping",
"mapping_includes",
"raises",
]

Expand Down
24 changes: 20 additions & 4 deletions precisely/mapping_matchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ def is_mapping(matchers):


class IsMappingMatcher(Matcher):
_describe_message = "mapping with items:{0}"
_allow_extra = False

def __init__(self, matchers):
self._matchers = matchers

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

extra_keys = set(actual.keys()) - set(self._matchers.keys())
if extra_keys:
return unmatched("had extra keys:{0}".format(indented_list(sorted(map(repr, extra_keys)))))
if not self._allow_extra:
extra_keys = set(actual.keys()) - set(self._matchers.keys())
if extra_keys:
return unmatched("had extra keys:{0}".format(indented_list(sorted(map(repr, extra_keys)))))

return matched()

def describe(self):
return "mapping with items:{0}".format(indented_list(sorted(
return self._describe_message.format(indented_list(sorted(
"{0!r}: {1}".format(key, matcher.describe())
for key, matcher in self._matchers.items()
)))


def mapping_includes(matchers):
return MappingIncludesMatcher(dict(
(key, to_matcher(matcher))
for key, matcher in matchers.items()
))


class MappingIncludesMatcher(IsMappingMatcher):
_describe_message = "mapping including items:{0}"
_allow_extra = True
43 changes: 43 additions & 0 deletions tests/mapping_includes_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from nose.tools import istest, assert_equal

from precisely import equal_to, mapping_includes
from precisely.results import matched, unmatched


@istest
def matches_when_keys_and_values_match():
matcher = mapping_includes({"a": equal_to(1), "b": equal_to(2)})
assert_equal(matched(), matcher.match({"a": 1, "b": 2}))


@istest
def values_are_coerced_to_matchers():
matcher = mapping_includes({"a": 1, "b": 2})
assert_equal(matched(), matcher.match({"a": 1, "b": 2}))


@istest
def does_not_match_when_value_does_not_match():
matcher = mapping_includes({"a": equal_to(1), "b": equal_to(2)})
assert_equal(
unmatched("value for key 'b' mismatched:\n * was 3"),
matcher.match({"a": 1, "b": 3, "c": 4}),
)


@istest
def does_not_match_when_keys_are_missing():
matcher = mapping_includes({"a": equal_to(1), "b": equal_to(2)})
assert_equal(unmatched("was missing key: 'b'"), matcher.match({"a": 1}))


@istest
def matches_when_there_are_extra_keys():
matcher = mapping_includes({"a": equal_to(1)})
assert_equal(matched(), matcher.match({"a": 1, "b": 1, "c": 1}))


@istest
def description_describes_keys_and_value_matchers():
matcher = mapping_includes({"a": equal_to(1), "b": equal_to(2)})
assert_equal("mapping including items:\n * 'a': 1\n * 'b': 2", matcher.describe())