Permalink
Browse files

Initial import.

  • Loading branch information...
0 parents commit 831faf5374fec6c21d27d03c88abf3502f7759cc @zacharyvoase committed Dec 13, 2011
Showing with 281 additions and 0 deletions.
  1. +7 −0 .gitignore
  2. +79 −0 README.md
  3. +24 −0 UNLICENSE
  4. +102 −0 assert_changes.py
  5. +14 −0 setup.py
  6. +55 −0 test.py
@@ -0,0 +1,7 @@
+*.egg-info/
+*.pyc
+*.pyo
+.DS_Store
+MANIFEST
+build/
+dist/
@@ -0,0 +1,79 @@
+# assert_changes
+
+Track simple changes in values from the start to end of a context, and raise an
+`AssertionError` if the final value was not as expected.
+
+Simply pass `assert_changes()` two functions: one which returns a snapshot of
+the monitored value, and another which represents the expected change.
+
+You can also pass in a function which compares the old and new versions,
+returning `True` if the change is acceptable and `False` otherwise. This allows
+you to use operators other than simple equality for your tests.
+
+## Installation
+
+ pip install assert_changes
+
+## Usage
+
+
+Using the `new` parameter:
+
+ >>> value = 123
+ >>> with assert_changes(lambda: value, new=lambda x: x + 1):
+ ... value = 124
+
+ >>> value = 123
+ >>> with assert_changes(lambda: value, new=lambda x: x + 1):
+ ... value = 122
+ Traceback (most recent call last):
+ ...
+ AssertionError: Value changed from 123 to 123 (expected: 124)
+
+Using the `cmp` parameter:
+
+ >>> import operator
+ >>> value = 123
+ >>> with assert_changes(lambda: value, cmp=operator.lt):
+ ... value += 4
+
+ >>> value = 123
+ >>> with assert_changes(lambda: value, cmp=operator.gt):
+ ... value += 4
+ Traceback (most recent call last):
+ ...
+ AssertionError: operator.gt(123, 127) not True
+
+You can even use an assertion as your comparison function:
+
+ >>> value = 123
+ >>> with assert_changes(lambda: value, cmp=assert_greater_than):
+ ... value += 4
+ Traceback (most recent call last):
+ ...
+ AssertionError: 123 not greater than 127
+
+
+## (Un)license
+
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
+software, either in source code form or as a compiled binary, for any purpose,
+commercial or non-commercial, and by any means.
+
+In jurisdictions that recognize copyright laws, the author or authors of this
+software dedicate any and all copyright interest in the software to the public
+domain. We make this dedication for the benefit of the public at large and to
+the detriment of our heirs and successors. We intend this dedication to be an
+overt act of relinquishment in perpetuity of all present and future rights to
+this software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
+CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, please refer to <http://unlicense.org/>
@@ -0,0 +1,24 @@
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, please refer to <http://unlicense.org/>
@@ -0,0 +1,102 @@
+"""
+Track simple changes in values using context managers.
+"""
+
+from contextlib import contextmanager
+import re
+
+
+__all__ = ['assert_changes', 'assert_constant']
+
+
+@contextmanager
+def assert_changes(value_func, new=None, cmp=None, msg=None):
+
+ """
+ Assert that the value returned by `value_func()` changes in a certain way.
+
+ `value_func()` needs to be a 0-ary callable which returns the monitored
+ value. You can then specify either `new` or `cmp`, depending on the check
+ you wish to perform.
+
+ If `new` is provided, it should be an unary function. It will be called on
+ the old value of `value_func()` and should return the expected new value.
+ This expected value will be compared to the *actual* value after executing
+ the body of the ``with`` block using a simple equality check. An
+ AssertionError will be raised if the new value is not as expected.
+
+ Alternatively, if `cmp` is provided, it should be a binary function over
+ both the old and new values, returning ``True`` if the value changed as
+ expected, or ``False`` otherwise. Note that it may also raise an
+ ``AssertionError`` itself, so you could use a function like
+ ``nose.tools.assert_greater_than()`` instead of ``operator.gt()``.
+
+ Pass `msg` to override the default assertion message in the case of
+ failure.
+ """
+
+ if cmp is None and new is None:
+ raise TypeError("Requires either a `new` or `cmp` argument")
+ elif not (cmp is None or new is None):
+ raise TypeError("Cannot provide both `new` and `cmp` arguments")
+
+ old_value = value_func()
+ yield
+ new_value = value_func()
+
+ if new:
+ expected_value = new(old_value)
+ assert new_value == expected_value, (msg or
+ "Value changed from %r to %r (expected: %r)" % (
+ new_value, old_value, expected_value))
+ elif cmp:
+ assert cmp(old_value, new_value), (msg or
+ "%r(%r, %r) is not True" % (function_repr(cmp), old_value,
+ new_value))
+
+
+@contextmanager
+def assert_constant(value_func, msg=None):
+
+ """
+ Assert that a monitored value does not change.
+
+ `value_func` should return the monitored value. The new and old values will
+ be compared via a simple equality check; if they are not equal, an
+ AssertionError will be raised. Pass `msg` to override the default message.
+ """
+
+ old_value = value_func()
+ yield
+ new_value = value_func()
+ assert new_value == old_value, (msg or
+ "Value changed: from %r to %r" % (old_value, new_value))
+
+
+def function_repr(func):
+
+ """
+ Get a more readable representation of a function object.
+
+ >>> import operator
+ >>> repr(operator.gt)
+ '<built-in function gt>'
+ >>> function_repr(operator.gt)
+ 'operator.gt'
+
+ >>> repr('a'.lower)
+ '<built-in method lower of str object at 0x...>'
+ >>> function_repr('a'.lower)
+ "'a'.lower"
+ """
+
+ initial_repr = repr(func)
+ if not re.match(r'^\<.*\>$', initial_repr):
+ return initial_repr
+
+ if hasattr(func, '__self__'):
+ return '%s.%s' % (repr(func.__self__), func.__name__)
+ elif hasattr(func, '__module__'):
+ return '%s.%s' % (func.__module__, func.__name__)
+ else:
+ return func.__name__
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from distutils.core import setup
+
+setup(
+ name='assert_changes',
+ version='0.0.1',
+ description='Check expected changes of monitored values in tests.',
+ author='Zachary Voase',
+ author_email='z@zacharyvoase.com',
+ url='http://github.com/zacharyvoase/assert_changes',
+ py_modules=['assert_changes'],
+)
55 test.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import with_statement
+
+import unittest
+
+from assert_changes import assert_changes, assert_constant
+
+
+class AssertChangesTest(unittest.TestCase):
+
+ def test_passes_if_equal_to_new(self):
+ value = 123
+ with assert_changes(lambda: value, new=lambda old: old + 1):
+ value += 1
+
+ def test_fails_if_not_equal_to_new(self):
+ def no_change():
+ value = 123
+ with assert_changes(lambda: value, new=lambda old: old + 1):
+ pass
+ self.assertRaises(AssertionError, no_change)
+
+ def test_passes_if_cmp_returns_True(self):
+ value = 'AbcDef'
+ with assert_changes(lambda: value,
+ cmp=lambda old, new: old.lower() == new.lower()):
+ value = 'aBCdEF'
+
+ def test_fails_if_cmp_returns_False(self):
+ def unacceptable_change():
+ value = 'AbcDef'
+ with assert_changes(lambda: value,
+ cmp=lambda old, new: old.lower() == new.lower()):
+ value = 'GhiJkl'
+ self.assertRaises(AssertionError, unacceptable_change)
+
+
+class AssertConstantTest(unittest.TestCase):
+
+ def test_passes_if_value_does_not_change(self):
+ value = 123
+ with assert_constant(lambda: value):
+ pass
+
+ def test_passes_if_value_does_not_change(self):
+ def changes():
+ value = 123
+ with assert_constant(lambda: value):
+ value += 1
+ self.assertRaises(AssertionError, changes)
+
+
+if __name__ == '__main__':
+ unittest.main()

0 comments on commit 831faf5

Please sign in to comment.