├── .gitignore ├── setup.py ├── UNLICENSE ├── test.py ├── README.md └── assert_changes.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info/ 2 | *.pyc 3 | *.pyo 4 | .DS_Store 5 | MANIFEST 6 | build/ 7 | dist/ 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from distutils.core import setup 5 | 6 | setup( 7 | name='assert_changes', 8 | version='0.0.1', 9 | description='Check expected changes of monitored values in tests.', 10 | author='Zachary Voase', 11 | author_email='z@zacharyvoase.com', 12 | url='http://github.com/zacharyvoase/assert_changes', 13 | py_modules=['assert_changes'], 14 | ) 15 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import with_statement 4 | 5 | import unittest 6 | 7 | from assert_changes import assert_changes, assert_constant 8 | 9 | 10 | class AssertChangesTest(unittest.TestCase): 11 | 12 | def test_passes_if_equal_to_new(self): 13 | value = 123 14 | with assert_changes(lambda: value, new=lambda old: old + 1): 15 | value += 1 16 | 17 | def test_fails_if_not_equal_to_new(self): 18 | def no_change(): 19 | value = 123 20 | with assert_changes(lambda: value, new=lambda old: old + 1): 21 | pass 22 | self.assertRaises(AssertionError, no_change) 23 | 24 | def test_passes_if_cmp_returns_True(self): 25 | value = 'AbcDef' 26 | with assert_changes(lambda: value, 27 | cmp=lambda old, new: old.lower() == new.lower()): 28 | value = 'aBCdEF' 29 | 30 | def test_fails_if_cmp_returns_False(self): 31 | def unacceptable_change(): 32 | value = 'AbcDef' 33 | with assert_changes(lambda: value, 34 | cmp=lambda old, new: old.lower() == new.lower()): 35 | value = 'GhiJkl' 36 | self.assertRaises(AssertionError, unacceptable_change) 37 | 38 | 39 | class AssertConstantTest(unittest.TestCase): 40 | 41 | def test_passes_if_value_does_not_change(self): 42 | value = 123 43 | with assert_constant(lambda: value): 44 | pass 45 | 46 | def test_passes_if_value_does_not_change(self): 47 | def changes(): 48 | value = 123 49 | with assert_constant(lambda: value): 50 | value += 1 51 | self.assertRaises(AssertionError, changes) 52 | 53 | 54 | if __name__ == '__main__': 55 | unittest.main() 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # assert_changes 2 | 3 | Track simple changes in values from the start to end of a context, and raise an 4 | `AssertionError` if the final value was not as expected. 5 | 6 | Simply pass `assert_changes()` two functions: one which returns a snapshot of 7 | the monitored value, and another which represents the expected change. 8 | 9 | You can also pass in a function which compares the old and new versions, 10 | returning `True` if the change is acceptable and `False` otherwise. This allows 11 | you to use operators other than simple equality for your tests. 12 | 13 | ## Installation 14 | 15 | pip install assert_changes 16 | 17 | ## Usage 18 | 19 | 20 | Using the `new` parameter: 21 | 22 | >>> value = 123 23 | >>> with assert_changes(lambda: value, new=lambda x: x + 1): 24 | ... value = 124 25 | 26 | >>> value = 123 27 | >>> with assert_changes(lambda: value, new=lambda x: x + 1): 28 | ... value = 122 29 | Traceback (most recent call last): 30 | ... 31 | AssertionError: Value changed from 123 to 123 (expected: 124) 32 | 33 | Using the `cmp` parameter: 34 | 35 | >>> import operator 36 | >>> value = 123 37 | >>> with assert_changes(lambda: value, cmp=operator.lt): 38 | ... value += 4 39 | 40 | >>> value = 123 41 | >>> with assert_changes(lambda: value, cmp=operator.gt): 42 | ... value += 4 43 | Traceback (most recent call last): 44 | ... 45 | AssertionError: operator.gt(123, 127) not True 46 | 47 | You can even use an assertion as your comparison function: 48 | 49 | >>> value = 123 50 | >>> with assert_changes(lambda: value, cmp=assert_greater_than): 51 | ... value += 4 52 | Traceback (most recent call last): 53 | ... 54 | AssertionError: 123 not greater than 127 55 | 56 | 57 | ## (Un)license 58 | 59 | This is free and unencumbered software released into the public domain. 60 | 61 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute this 62 | software, either in source code form or as a compiled binary, for any purpose, 63 | commercial or non-commercial, and by any means. 64 | 65 | In jurisdictions that recognize copyright laws, the author or authors of this 66 | software dedicate any and all copyright interest in the software to the public 67 | domain. We make this dedication for the benefit of the public at large and to 68 | the detriment of our heirs and successors. We intend this dedication to be an 69 | overt act of relinquishment in perpetuity of all present and future rights to 70 | this software under copyright law. 71 | 72 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 73 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 74 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE 75 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 76 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 77 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 78 | 79 | For more information, please refer to 80 | -------------------------------------------------------------------------------- /assert_changes.py: -------------------------------------------------------------------------------- 1 | """ 2 | Track simple changes in values using context managers. 3 | """ 4 | 5 | from contextlib import contextmanager 6 | import re 7 | 8 | 9 | __all__ = ['assert_changes', 'assert_constant'] 10 | 11 | 12 | @contextmanager 13 | def assert_changes(value_func, new=None, cmp=None, msg=None): 14 | 15 | """ 16 | Assert that the value returned by `value_func()` changes in a certain way. 17 | 18 | `value_func()` needs to be a 0-ary callable which returns the monitored 19 | value. You can then specify either `new` or `cmp`, depending on the check 20 | you wish to perform. 21 | 22 | If `new` is provided, it should be an unary function. It will be called on 23 | the old value of `value_func()` and should return the expected new value. 24 | This expected value will be compared to the *actual* value after executing 25 | the body of the ``with`` block using a simple equality check. An 26 | AssertionError will be raised if the new value is not as expected. 27 | 28 | Alternatively, if `cmp` is provided, it should be a binary function over 29 | both the old and new values, returning ``True`` if the value changed as 30 | expected, or ``False`` otherwise. Note that it may also raise an 31 | ``AssertionError`` itself, so you could use a function like 32 | ``nose.tools.assert_greater_than()`` instead of ``operator.gt()``. 33 | 34 | Pass `msg` to override the default assertion message in the case of 35 | failure. 36 | """ 37 | 38 | if cmp is None and new is None: 39 | raise TypeError("Requires either a `new` or `cmp` argument") 40 | elif not (cmp is None or new is None): 41 | raise TypeError("Cannot provide both `new` and `cmp` arguments") 42 | 43 | old_value = value_func() 44 | yield 45 | new_value = value_func() 46 | 47 | if new: 48 | expected_value = new(old_value) 49 | assert new_value == expected_value, (msg or 50 | "Value changed from %r to %r (expected: %r)" % ( 51 | new_value, old_value, expected_value)) 52 | elif cmp: 53 | assert cmp(old_value, new_value), (msg or 54 | "%r(%r, %r) is not True" % (function_repr(cmp), old_value, 55 | new_value)) 56 | 57 | 58 | @contextmanager 59 | def assert_constant(value_func, msg=None): 60 | 61 | """ 62 | Assert that a monitored value does not change. 63 | 64 | `value_func` should return the monitored value. The new and old values will 65 | be compared via a simple equality check; if they are not equal, an 66 | AssertionError will be raised. Pass `msg` to override the default message. 67 | """ 68 | 69 | old_value = value_func() 70 | yield 71 | new_value = value_func() 72 | assert new_value == old_value, (msg or 73 | "Value changed: from %r to %r" % (old_value, new_value)) 74 | 75 | 76 | def function_repr(func): 77 | 78 | """ 79 | Get a more readable representation of a function object. 80 | 81 | >>> import operator 82 | >>> repr(operator.gt) 83 | '' 84 | >>> function_repr(operator.gt) 85 | 'operator.gt' 86 | 87 | >>> repr('a'.lower) 88 | '' 89 | >>> function_repr('a'.lower) 90 | "'a'.lower" 91 | """ 92 | 93 | initial_repr = repr(func) 94 | if not re.match(r'^\<.*\>$', initial_repr): 95 | return initial_repr 96 | 97 | if hasattr(func, '__self__'): 98 | return '%s.%s' % (repr(func.__self__), func.__name__) 99 | elif hasattr(func, '__module__'): 100 | return '%s.%s' % (func.__module__, func.__name__) 101 | else: 102 | return func.__name__ 103 | --------------------------------------------------------------------------------