├── .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 |
--------------------------------------------------------------------------------