├── .gitignore ├── LICENSE.md ├── README.md ├── affirm.py ├── setup.py ├── test.sh └── tests ├── __init__.py └── test_assert.py /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | *.egg-info/ 4 | *.pyc 5 | *.sublime* 6 | temp.py 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | =============== 3 | 4 | Copyright (c) 2016 Eli Finer 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | affirm 2 | ====== 3 | 4 | This small library improves Python `assert` error messages to contain more useful information. 5 | 6 | I like to use `assert`'s liberaly thoughout my code to document my assumptions and when one of them fails, I really like to know as much as possible about what failed and why. 7 | 8 | Installation 9 | ------------ 10 | 11 | $ pip install affirm 12 | 13 | Using the `assert` statement 14 | ---------------------------- 15 | 16 | Before affirm, if you run a simple script in Python: 17 | 18 | a = 1 19 | b = 2 20 | assert a > b 21 | 22 | The result will be: 23 | 24 | Traceback (most recent call last): 25 | File "test.py", line 3, in 26 | assert a > b 27 | AssertionError 28 | 29 | We can see the traceback, but not the actuall values that caused the assert to fail, which is not very useful for debugging. 30 | 31 | If you `import affirm` at the top of the script, like so: 32 | 33 | import affirm 34 | a = 1 35 | b = 2 36 | assert a > b 37 | 38 | You'll get: 39 | 40 | Traceback (most recent call last): 41 | File "test.py", line 4, in 42 | assert a > b 43 | AssertionError: assertion (a > b) failed with a=1, b=2 44 | 45 | Which is much more useful. 46 | 47 | Note that the standard behaviour of supplying a message with the assert statement still works: 48 | 49 | import affirm 50 | a = 1 51 | b = 2 52 | assert a > b, "something went wrong" 53 | 54 | Will give you: 55 | 56 | Traceback (most recent call last): 57 | File "test.py", line 4, in 58 | assert a > b 59 | AssertionError: something went wrong 60 | 61 | Using the `affirm` function 62 | --------------------------- 63 | 64 | There's only one problem with using the standard `assert` statement. If you catch the exception and print it yourself: 65 | 66 | import affirm 67 | a = 1 68 | b = 2 69 | try: 70 | assert a > b 71 | except Exception as e: 72 | print(str(e)) 73 | 74 | The result will be: 75 | 76 | ``` 77 | ``` 78 | 79 | Yes, absolutely nothing. Calling `str` on `AssertionError` results in an empty string. 80 | 81 | If you want to be able to catch the assertion errors and print the messages into e.g. log, you'll need to use the `affirm` function instead of the `assert` statement, like so: 82 | 83 | from affirm import affirm 84 | a = 1 85 | b = 2 86 | try: 87 | affirm(a > b) 88 | except Exception as e: 89 | print(str(e)) 90 | 91 | Now we get the expected: 92 | 93 | assertion (a > b) failed with a=1, b=2 94 | -------------------------------------------------------------------------------- /affirm.py: -------------------------------------------------------------------------------- 1 | # affirm.py / Eli Finer / 2016 2 | # 3 | # This script causes assert statements to output much better default error messages 4 | # that include the tested condition and the values for any variables referenced in it. 5 | # 6 | # Here are some examples: 7 | # 8 | # >>> assert 1 > 2 9 | # AssertionError: assertion (1 > 2) failed 10 | # 11 | # >>> a = 1; b = 2; c = 'foo'; d = None 12 | # >>> assert a > b 13 | # AssertionError: assertion (a > b) failed with a=1, b=2 14 | # 15 | # >>> assert c is None 16 | # AssertionError: assertion (c is None) failed with c='foo' 17 | # 18 | # >>> assert a == b == c 19 | # AssertionError: assertion (a == b == c) failed with a=1, b=2, c='foo' 20 | 21 | import re 22 | import ast 23 | import types 24 | import inspect 25 | from collections import OrderedDict 26 | 27 | def make_assert_message(frame, regex): 28 | def extract_condition(): 29 | code_context = inspect.getframeinfo(frame)[3] 30 | if not code_context: 31 | return '' 32 | match = re.search(regex, code_context[0]) 33 | if not match: 34 | return '' 35 | return match.group(1).strip() 36 | 37 | class ReferenceFinder(ast.NodeVisitor): 38 | def __init__(self): 39 | self.names = [] 40 | def find(self, tree, frame): 41 | self.visit(tree) 42 | nothing = object() 43 | deref = OrderedDict() 44 | for name in self.names: 45 | value = frame.f_locals.get(name, nothing) or frame.f_globals.get(name, nothing) 46 | if value is not nothing and not isinstance(value, (types.ModuleType, types.FunctionType)): 47 | deref[name] = repr(value) 48 | return deref 49 | def visit_Name(self, node): 50 | self.names.append(node.id) 51 | 52 | condition = extract_condition() 53 | if not condition: 54 | return 55 | deref = ReferenceFinder().find(ast.parse(condition), frame) 56 | deref_str = '' 57 | if deref: 58 | deref_str = ' with ' + ', '.join('{}={}'.format(k, v) for k, v in deref.items()) 59 | return 'assertion {} failed{}'.format(condition, deref_str) 60 | 61 | import sys 62 | _old_excepthook = sys.excepthook 63 | def assert_excepthook(type, value, traceback): 64 | if type == AssertionError: 65 | from traceback import print_exception 66 | if not value.args: 67 | top = traceback 68 | while top.tb_next and top.tb_next.tb_frame: 69 | top = top.tb_next 70 | message = make_assert_message(top.tb_frame, r'assert\s+([^#]+)') 71 | value = AssertionError(message) 72 | print_exception(type, value, traceback) 73 | else: 74 | _old_excepthook(type, value, traceback) 75 | sys.excepthook = assert_excepthook 76 | 77 | def affirm(condition): 78 | if not __debug__: 79 | return 80 | if condition: 81 | return 82 | else: 83 | message = make_assert_message(inspect.currentframe().f_back, r'affirm\s*\(\s*(.+)\s*\)') 84 | raise AssertionError(message) 85 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | try: 2 | from setuptools import setup 3 | except ImportError: 4 | from distutils.core import setup 5 | 6 | config = { 7 | 'name': 'affirm', 8 | 'url': 'https://github.com/gooli/affirm', 9 | 'author': 'Eli Finer', 10 | 'author_email': 'eli.finer@gmail.com', 11 | 'version': '0.9.2', 12 | 'py_modules': ['affirm'], 13 | 'description': 'Improved error messages for Python assert statements' 14 | } 15 | 16 | setup(**config) 17 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | python2.7 tests/test_assert.py 3 | python3.5 tests/test_assert.py 4 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifiner/affirm/79cf60380be49dfa7b2dfa01ebb5412e06f57f45/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_assert.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import subprocess 4 | import unittest 5 | 6 | # Tests are executed in a separate Python process because there's no way to capture 7 | # the excepthook results within the same process (we need to let the exception fall 8 | # through to the interpreter to get it to the excepthook.) 9 | # 10 | # This is ugly but it works. 11 | 12 | PACKAGE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) 13 | TEMPFILE = os.path.join(PACKAGE_DIR, '_temp_test.py') 14 | 15 | SOURCE = ''' 16 | import math 17 | import affirm 18 | a = 1 19 | b = 2 20 | c = None 21 | d = 'foo' 22 | {statement} 23 | ''' 24 | 25 | class TestAssert(unittest.TestCase): 26 | def __init__(self, statement, expected_message, *args, **kwargs): 27 | super(TestAssert, self).__init__(*args, **kwargs) 28 | self.statement = statement 29 | self.expected_message = expected_message 30 | 31 | def runTest(self): 32 | with open(TEMPFILE, 'w') as f: 33 | f.write(SOURCE.format(statement=self.statement)) 34 | result = subprocess.Popen([sys.executable, TEMPFILE], stderr=subprocess.PIPE) 35 | assert_message = result.stderr.read().decode('utf8').splitlines()[-1] 36 | self.assertEqual(assert_message, self.expected_message) 37 | 38 | def __str__(self): 39 | return self.statement 40 | 41 | suite = unittest.TestSuite() 42 | def test(statement, expected_message): 43 | suite.addTest(TestAssert(statement, expected_message)) 44 | 45 | test('assert 1 > 2', 'AssertionError: assertion 1 > 2 failed') 46 | test('assert a > b', 'AssertionError: assertion a > b failed with a=1, b=2') 47 | test('assert a + b > a + b * 2', 'AssertionError: assertion a + b > a + b * 2 failed with a=1, b=2') 48 | test('assert a is None', 'AssertionError: assertion a is None failed with a=1') 49 | test('assert c is not None', 'AssertionError: assertion c is not None failed with c=None') 50 | test('assert d == "bar"', 'AssertionError: assertion d == "bar" failed with d=\'foo\'') 51 | test('assert sum([a, b]) == 1', 'AssertionError: assertion sum([a, b]) == 1 failed with a=1, b=2') 52 | test('assert math.log(a, b) == 1', 'AssertionError: assertion math.log(a, b) == 1 failed with a=1, b=2') 53 | test('assert 1 == 2, "some message"', 'AssertionError: some message') 54 | 55 | if __name__ == '__main__': 56 | runner = unittest.TextTestRunner(verbosity=3) 57 | runner.run(suite) 58 | os.unlink(TEMPFILE) 59 | --------------------------------------------------------------------------------