diff --git a/README.rst b/README.rst index 62d435c..ffbfb75 100644 --- a/README.rst +++ b/README.rst @@ -88,6 +88,14 @@ For instance, ``has_attrs(name="bob")`` is equivalent to ``has_attrs(name=equal_ assert_that(result, is_sequence("a", "b")) # Matches ["a", "b"] but not ["b", "a"] +* ``is_sequence_with(*args)``: matches an iterable if it has the same elements in the same order, accepts extra items. + For instance: + + .. code:: python + + assert_that(result, is_sequence_with("a", "b")) + # Matches ["a", "b"], ["c", "a", "b"] and ["a", "b", "c"], but not ["b", "a"] + * ``includes(*args)``: matches an iterable if it includes all of the elements. For instance: diff --git a/precisely/__init__.py b/precisely/__init__.py index 4477613..103dfae 100644 --- a/precisely/__init__.py +++ b/precisely/__init__.py @@ -2,7 +2,7 @@ from .comparison_matchers import contains_string, greater_than, greater_than_or_equal_to, less_than, less_than_or_equal_to, starts_with, close_to from .core_matchers import equal_to, anything, all_of, any_of, not_ from .object_matchers import has_attr, has_attrs, is_instance -from .iterable_matchers import all_elements, contains_exactly, includes, is_sequence +from .iterable_matchers import all_elements, contains_exactly, includes, is_sequence, is_sequence_with from .feature_matchers import has_feature from .function_matchers import raises from .mapping_matchers import is_mapping @@ -33,6 +33,7 @@ "includes", "is_same_sequence", "is_sequence", + "is_sequence_with", "has_feature", "is_mapping", "raises", diff --git a/precisely/iterable_matchers.py b/precisely/iterable_matchers.py index 3c37b67..a036d74 100644 --- a/precisely/iterable_matchers.py +++ b/precisely/iterable_matchers.py @@ -6,6 +6,7 @@ from .base import Matcher from .results import matched, unmatched, indented_list, indexed_indented_list, Result from .coercion import to_matcher +from .utils import window def contains_exactly(*matchers): @@ -115,17 +116,35 @@ def match_remaining(self): ))) -def is_sequence(*matchers): - return IsSequenceMatcher([to_matcher(matcher) for matcher in matchers]) +def is_sequence(*matchers, **kwargs): + allow_extra = kwargs.pop('allow_extra', False) # workaround for python 2 + return IsSequenceMatcher([to_matcher(matcher) for matcher in matchers], allow_extra) + + +def is_sequence_with(*matchers): + return is_sequence(*matchers, allow_extra=True) class IsSequenceMatcher(Matcher): _missing = object() - def __init__(self, matchers): + def __init__(self, matchers, allow_extra): self._matchers = matchers + self._allow_extra = allow_extra def match(self, actual): + if self._allow_extra: + for subsequence in window(_to_list_or_mismatch(actual), len(self._matchers)): + response = self._match(subsequence) + if response.is_match: + break + if not response.is_match: + response = self._match(actual) + else: + response = self._match(actual) + return response + + def _match(self, actual): values = _to_list_or_mismatch(actual) if isinstance(values, Result): diff --git a/precisely/utils.py b/precisely/utils.py new file mode 100644 index 0000000..2ba4966 --- /dev/null +++ b/precisely/utils.py @@ -0,0 +1,13 @@ +from itertools import islice + + +def window(seq, n): + "Returns a sliding window (of width n) over data from the iterable" + " s -> (s0,s1,...s[n-1]), (s1,s2,...,sn), ... " + it = iter(seq) + result = tuple(islice(it, n)) + if len(result) <= n: + yield result + for elem in it: + result = result[1:] + (elem,) + yield result diff --git a/tests/is_sequence_with_tests.py b/tests/is_sequence_with_tests.py new file mode 100644 index 0000000..055002d --- /dev/null +++ b/tests/is_sequence_with_tests.py @@ -0,0 +1,111 @@ +from nose.tools import istest, assert_equal + +from precisely import is_sequence_with, equal_to +from precisely.results import matched, unmatched + + +@istest +def matches_when_all_submatchers_match_one_item_with_no_items_leftover(): + matcher = is_sequence_with(equal_to("apple"), equal_to("banana")) + + assert_equal(matched(), matcher.match(["apple", "banana"])) + + +@istest +def mismatches_when_actual_is_not_iterable(): + matcher = is_sequence_with(equal_to("apple")) + + assert_equal( + unmatched("was not iterable\nwas 0"), + matcher.match(0) + ) + + +@istest +def mismatches_when_items_are_in_wrong_order(): + matcher = is_sequence_with(equal_to("apple"), equal_to("banana")) + + assert_equal( + unmatched("element at index 0 mismatched:\n * was 'banana'"), + matcher.match(["banana", "apple"]) + ) + + +@istest +def mismatches_when_item_is_missing(): + matcher = is_sequence_with(equal_to("apple"), equal_to("banana"), equal_to("coconut")) + + assert_equal( + unmatched("element at index 2 was missing"), + matcher.match(["apple", "banana"]) + ) + + +@istest +def mismatches_when_item_is_expected_but_iterable_is_empty(): + matcher = is_sequence_with(equal_to("apple")) + + assert_equal( + unmatched("iterable was empty"), + matcher.match([]) + ) + + +@istest +def when_empty_iterable_is_expected_then_empty_iterable_matches(): + matcher = is_sequence_with() + + assert_equal( + matched(), + matcher.match([]) + ) + + +@istest +def matches_when_contains_extra_item_after(): + matcher = is_sequence_with(equal_to("apple"), equal_to("pear")) + + assert_equal( + matched(), + matcher.match(["apple", "pear", "coconut"]) + ) + + +@istest +def matches_when_contains_extra_item_before(): + matcher = is_sequence_with(equal_to("apple"), equal_to("pear")) + + assert_equal( + matched(), + matcher.match(["coconut", "apple", "pear"]) + ) + + +@istest +def when_there_are_zero_submatchers_then_description_is_of_empty_iterable(): + matcher = is_sequence_with() + + assert_equal( + "empty iterable", + matcher.describe() + ) + + +@istest +def description_contains_descriptions_of_submatchers(): + matcher = is_sequence_with(equal_to("apple"), equal_to("banana")) + + assert_equal( + "iterable containing in order:\n 0: 'apple'\n 1: 'banana'", + matcher.describe() + ) + + +@istest +def elements_are_coerced_to_matchers(): + matcher = is_sequence_with("apple", "banana") + + assert_equal( + "iterable containing in order:\n 0: 'apple'\n 1: 'banana'", + matcher.describe() + )