← Back to team overview

testtools-dev team mailing list archive

[Merge] lp:~lifeless/testtools/haslength into lp:testtools

 

Robert Collins has proposed merging lp:~lifeless/testtools/haslength into lp:testtools.

Requested reviews:
  testtools committers (testtools-committers)

For more details, see:
https://code.launchpad.net/~lifeless/testtools/haslength/+merge/144578

I keep wanting a HasLength. And MatchesWithPredicate can't do it well. So, new helper to do it well, and an implementation.
-- 
https://code.launchpad.net/~lifeless/testtools/haslength/+merge/144578
Your team testtools developers is subscribed to branch lp:testtools.
=== modified file 'NEWS'
--- NEWS	2013-01-21 19:37:00 +0000
+++ NEWS	2013-01-23 20:10:27 +0000
@@ -9,6 +9,12 @@
 Improvements
 ------------
 
+* New matcher ``HasLength`` for matching the length of a collection.
+  (Robert Collins)
+
+* New matcher ``MatchesPredicateWithParams`` make it still easier to create
+  adhoc matchers. (Robert Collins)
+
 * We have a simpler release process in future - see doc/hacking.rst.
   (Robert Collins)
 

=== modified file 'doc/for-test-authors.rst'
--- doc/for-test-authors.rst	2013-01-18 09:17:19 +0000
+++ doc/for-test-authors.rst	2013-01-23 20:10:27 +0000
@@ -521,6 +521,14 @@
   self.assertThat('greetings.txt', FileContains(matcher=Contains('!')))
 
 
+HasLength
+~~~~~~~~~
+
+Check the length of a collection.  For example::
+
+  self.assertThat([1, 2, 3], HasLength(2))
+
+
 HasPermissions
 ~~~~~~~~~~~~~~
 
@@ -780,6 +788,35 @@
   MismatchError: 42 is not prime.
 
 
+MatchesPredicateWithParams
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Sometimes you can't use a trivial predicate and instead need to pass in some
+parameters each time. In that case, MatchesPredicateWithParams is your go-to
+tool for creating adhoc matchers. MatchesPredicateWithParams takes a predicate
+function and message and returns a factory to produce matchers from that. The
+predicate needs to return a boolean (or any truthy object), and accept the
+object to match + whatever was passed into the factory.
+
+For example, you might have an ``divisible`` function and want to make a
+matcher based on it::
+
+  def test_divisible_numbers(self):
+      IsDisivibleBy = MatchesPredicateWithParams(
+          divisible, '{0} is not divisible by {1}')
+      self.assertThat(7, IsDivisibleBy(1))
+      self.assertThat(7, IsDivisibleBy(7))
+      self.assertThat(7, IsDivisibleBy(2)))
+      # This will fail.
+
+Which will produce the error message::
+
+  Traceback (most recent call last):
+    File "...", line ..., in test_divisible
+      self.assertThat(7, IsDivisibleBy(2))
+  MismatchError: 7 is not divisible by 2.
+
+
 Raises
 ~~~~~~
 

=== modified file 'testtools/matchers/__init__.py'
--- testtools/matchers/__init__.py	2012-10-25 14:20:44 +0000
+++ testtools/matchers/__init__.py	2013-01-23 20:10:27 +0000
@@ -28,6 +28,7 @@
     'FileContains',
     'FileExists',
     'GreaterThan',
+    'HasLength',
     'HasPermissions',
     'Is',
     'IsInstance',
@@ -39,6 +40,7 @@
     'MatchesException',
     'MatchesListwise',
     'MatchesPredicate',
+    'MatchesPredicateWithParams',
     'MatchesRegex',
     'MatchesSetwise',
     'MatchesStructure',
@@ -57,6 +59,7 @@
     EndsWith,
     Equals,
     GreaterThan,
+    HasLength,
     Is,
     IsInstance,
     LessThan,
@@ -101,6 +104,7 @@
     MatchesAll,
     MatchesAny,
     MatchesPredicate,
+    MatchesPredicateWithParams,
     Not,
     )
 

=== modified file 'testtools/matchers/_basic.py'
--- testtools/matchers/_basic.py	2012-09-10 11:37:46 +0000
+++ testtools/matchers/_basic.py	2013-01-23 20:10:27 +0000
@@ -5,6 +5,7 @@
     'EndsWith',
     'Equals',
     'GreaterThan',
+    'HasLength',
     'Is',
     'IsInstance',
     'LessThan',
@@ -24,7 +25,10 @@
     text_repr,
     )
 from ..helpers import list_subtract
-from ._higherorder import PostfixedMismatch
+from ._higherorder import (
+    MatchesPredicateWithParams,
+    PostfixedMismatch,
+    )
 from ._impl import (
     Matcher,
     Mismatch,
@@ -313,3 +317,10 @@
             pattern = pattern.encode("unicode_escape").decode("ascii")
             return Mismatch("%r does not match /%s/" % (
                     value, pattern.replace("\\\\", "\\")))
+
+
+def has_len(x, y):
+    return len(x) == y
+
+
+HasLength = MatchesPredicateWithParams(has_len, "len({0}) != {1}", "HasLength")

=== modified file 'testtools/matchers/_higherorder.py'
--- testtools/matchers/_higherorder.py	2012-12-13 15:01:41 +0000
+++ testtools/matchers/_higherorder.py	2013-01-23 20:10:27 +0000
@@ -287,3 +287,79 @@
     def match(self, x):
         if not self.predicate(x):
             return Mismatch(self.message % x)
+
+
+def MatchesPredicateWithParams(predicate, message, name=None):
+    """Match if a given parameterised function returns True.
+
+    It is reasonably common to want to make a very simple matcher based on a
+    function that you already have that returns True or False given some
+    arguments. This matcher makes it very easy to do so. e.g.::
+
+      HasLength = MatchesPredicate(
+          lambda x, y: len(x) == y, 'len({0}) is not {1}')
+      self.assertThat([1, 2], HasLength(3))
+
+    Note that unlike MatchesPredicate MatchesPredicateWithParams returns a
+    factory which you then customise to use by constructing an actual matcher
+    from it.
+
+    The predicate function should take the object to match as its first
+    parameter. Any additional parameters supplied when constructing a matcher
+    are supplied to the predicate as additional parameters when checking for a
+    match.
+
+    :param predicate: The predicate function.
+    :param message: A format string for describing mis-matches.
+    :param name: Optional replacement name for the matcher.
+    """
+    def construct_matcher(*args, **kwargs):
+        return _MatchesPredicateWithParams(
+            predicate, message, name, *args, **kwargs)
+    return construct_matcher
+
+
+class _MatchesPredicateWithParams(Matcher):
+
+    def __init__(self, predicate, message, name, *args, **kwargs):
+        """Create a ``MatchesPredicateWithParams`` matcher.
+
+        :param predicate: A function that takes an object to match and
+            additional params as given in *args and **kwargs. The result of the
+            function will be interpreted as a boolean to determine a match.
+        :param message: A message to describe a mismatch.  It will be formatted
+            with .format() and be given a tuple containing whatever was passed
+            to ``match()`` + *args in *args, and whatever was passed to
+            **kwargs as its **kwargs.
+
+            For instance, to format a single parameter::
+
+                "{0} is not a {1}"
+
+            To format a keyword arg::
+
+                "{0} is not a {type_to_check}"
+        :param name: What name to use for the matcher class. Pass None to use
+            the default.
+        """
+        self.predicate = predicate
+        self.message = message
+        self.name = name
+        self.args = args
+        self.kwargs = kwargs
+
+    def __str__(self):
+        args = [str(arg) for arg in self.args]
+        kwargs = ["%s=%s" % item for item in self.kwargs.items()]
+        args = ", ".join(args + kwargs)
+        if self.name is None:
+            name = 'MatchesPredicateWithParams(%r, %r)' % (
+                self.predicate, self.message)
+        else:
+            name = self.name
+        return '%s(%s)' % (name, args)
+
+    def match(self, x):
+        if not self.predicate(x, *self.args, **self.kwargs):
+            return Mismatch(
+                self.message.format(*((x,) + self.args), **self.kwargs))

=== modified file 'testtools/tests/matchers/test_basic.py'
--- testtools/tests/matchers/test_basic.py	2012-09-08 17:21:06 +0000
+++ testtools/tests/matchers/test_basic.py	2013-01-23 20:10:27 +0000
@@ -19,6 +19,7 @@
     IsInstance,
     LessThan,
     GreaterThan,
+    HasLength,
     MatchesRegex,
     NotEquals,
     SameMembers,
@@ -369,6 +370,21 @@
         ]
 
 
+class TestHasLength(TestCase, TestMatchersInterface):
+
+    matches_matcher = HasLength(2)
+    matches_matches = [[1, 2]]
+    matches_mismatches = [[], [1], [3, 2, 1]]
+
+    str_examples = [
+        ("HasLength(2)", HasLength(2)),
+        ]
+
+    describe_examples = [
+        ("len([]) != 1", [], HasLength(1)),
+        ]
+
+
 def test_suite():
     from unittest import TestLoader
     return TestLoader().loadTestsFromName(__name__)

=== modified file 'testtools/tests/matchers/test_higherorder.py'
--- testtools/tests/matchers/test_higherorder.py	2012-12-13 15:01:41 +0000
+++ testtools/tests/matchers/test_higherorder.py	2013-01-23 20:10:27 +0000
@@ -18,6 +18,7 @@
     MatchesAny,
     MatchesAll,
     MatchesPredicate,
+    MatchesPredicateWithParams,
     Not,
     )
 from testtools.tests.helpers import FullStackRunTest
@@ -222,6 +223,32 @@
         ]
 
 
+def between(x, low, high):
+    return low < x < high
+
+
+class TestMatchesPredicateWithParams(TestCase, TestMatchersInterface):
+
+    matches_matcher = MatchesPredicateWithParams(
+        between, "{0} is not between {1} and {2}")(1, 9)
+    matches_matches = [2, 4, 6, 8]
+    matches_mismatches = [0, 1, 9, 10]
+
+    str_examples = [
+        ("MatchesPredicateWithParams(%r, %r)(%s)" % (
+            between, "{0} is not between {1} and {2}", "1, 2"),
+         MatchesPredicateWithParams(
+            between, "{0} is not between {1} and {2}")(1, 2)),
+        ("Between(1, 2)", MatchesPredicateWithParams(
+            between, "{0} is not between {1} and {2}", "Between")(1, 2)),
+        ]
+
+    describe_examples = [
+        ('1 is not between 2 and 3', 1, MatchesPredicateWithParams(
+            between, "{0} is not between {1} and {2}")(2, 3)),
+        ]
+
+
 def test_suite():
     from unittest import TestLoader
     return TestLoader().loadTestsFromName(__name__)


Follow ups