I'm trying to implement an assert function. How can I get the text of the failing condition into the error message? If I have to parse it from the backtrace, can I portably rely on anything about the format of frames?
AssertionError is just like any other exception in python, and assert is a simple statement that is equivalent to
if __debug__:
if not expression: raise AssertionError
or
if __debug__:
if not expression1: raise AssertionError(expression2)
so you can add a second parameter to your assertion to have additional output
from sys import exc_info
from traceback import print_exception
# assertions are simply exceptions in Python
try:
assert False, "assert was false"
except AssertionError:
print_exception(*exc_info())
outputs
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
AssertionError: assert was false
If you're sure the expression to test is secure you could do something like this:
File my_assert.py:
import sys
def my_assert(condition):
caller = sys._getframe(1)
if not eval(condition, caller.f_globals, caller.f_locals):
raise AssertionError(repr(condition) + " on line " +
str(caller.f_lineno) + ' in ' +
caller.f_code.co_name)
File test_my_assert.py:
from my_assert import my_assert
global_var = 42
def test():
local_var = 17
my_assert('local_var*2 < global_var') # OK
my_assert('local_var > global_var')
test()
Output:
Traceback (most recent call last):
File "test_my_assert.py", line 10, in <module>
test()
File "test_my_assert.py", line 8, in test
my_assert('local_var > global_var')
File "my_assert.py", line 8, in my_assert
caller.f_code.co_name)
AssertionError: 'local_var > global_var' on line 8 in test
My very hackish solution:
def my_assert(condition):
if not eval(condition):
# error stuff
Then use it by placing the condition in quotation marks. It is then a string that can be printed in the error message.
Or, if you want it to actually raise an AssertionError:
def my_assert(condition):
if not eval(condition):
raise AssertionError(condition)
Related
I want to print an error's line number and error message in a nicely displayed way. The follow is my code, which uses linecache:
import linecache
def func():
if xx == 1:
print('ok')
try:
func()
except:
exc_type, exc_obj, tb = sys.exc_info()
f = tb.tb_frame
lineno = tb.tb_lineno
filename = f.f_code.co_filename
linecache.checkcache(filename)
line = linecache.getline(filename, lineno, f.f_globals)
print_('ERROR - (LINE {} "{}"): {}'.format(lineno, line.strip(), exc_obj))
However, this only gives where the func() is called:
ERROR - (LINE 8 ""): name 'xx' is not defined
Is there a way to print the line number where the error actually occured, which should be Line 4? Or even better, can I print Line 8 and then trace back to line 4? For example, if I do not use try - except, the code:
def func():
if xx == 1:
print('ok')
func()
will give me the following error message, which is much better to locate the error:
File "<input>", line 5, in <module>
File "<input>", line 2, in func
NameError: name 'xx' is not defined. Did you mean: 'xxx'?
You can use traceback and sys modules to get advanced traceback output like you are wishing for.
Here is an example:
import traceback
import sys
def func():
zeroDivide = 1 / 0
try:
func()
except Exception:
print(traceback.format_exc()) # This line is for getting traceback.
print(sys.exc_info()[2]) # This line is getting for the error type.
Output will be:
Traceback (most recent call last):
File "b:\abc\1234\pppp\main.py", line 10, in <module>
func()
File "b:\abc\1234\pppp\main.py", line 7, in func
zeroDivide = 1 / 0
ZeroDivisionError: division by zero
You can use the traceback module to get the line number of the error,
import traceback
def function():
try:
# code
except:
tb_list = traceback.extract_tb(sys.exc_info()[2])
line_number = tb_list[-1][1]
print("An error occurred on line:", line_number)
You can use the traceback.extract_tb() function. This function returns a list of traceback objects, each of which contain information about the stack trace. The last element of this list, tb_list[-1], holds information about the line where the exception occurred. To access the line number, you can use the second element of this tuple, tb_list[-1][1]. This value can then be printed using the print() function.
To get the line number as an int you can get the traceback as a list from traceback.extract_tb(). Looking at the last item gives you the line where the exception was raised:
#soPrintLineOfError2
import sys
import traceback
def func():
if xx == 1:
print('ok')
try:
func()
except Exception as e:
tb = sys.exc_info()[2]
ss = traceback.extract_tb(tb)
ss1 = ss[-1]
print(ss1.line)
print(ss1.lineno)
Output:
if xx == 1:
6
How can we print the values of arguments passed to the functions in the call stack when an error stack trace is printed? I would like the output to be exactly as in the example below.
Example:
Traceback (most recent call last):
File "./file.py", line 615, in func0 **(arg0) arg0 = 0 was passed**
result = func1(arg1, arg2)
File "./file.py", line 728, in func1 **(arg1, arg2) arg1 = 1 and arg2 = 2 was passed**
return int_value[25]
TypeError: 'int' object is not iterable
I'd like the information inside the ** **s above to also be printed in addition to the normal output in the stack trace. What I envision is that the debugger automatically prints the passed arguments as well. That would give a clear picture of the "functional pipeline" that the data was passed through and what happened to it in the pipeline and which function did not do what it was supposed to do. This would help debugging a lot.
I searched quite a bit and found these related questions:
How to print call stack with argument values?
How to print function arguments in sys.settrace?
but the answers to neither of them worked for me: The answer to the 1st one led to ModuleNotFoundError: No module named 'stackdump'. The answer to the 2nd one crashed my ipython interpreter with a very long stack trace.
I also looked up:
https://docs.python.org/3/library/sys.html#sys.settrace
https://docs.python.org/3/library/traceback.html
There seems to be a capture_locals variable for TracebackExceptions, but I didn't quite understand how to make it work.
Pure Python3
Talking about TracebackExceptions and it's capture_locals argument, we can use it as follows:
#!/usr/bin/env python3
import traceback
c = ['Help me!']
def bar(a = 3):
d = {1,2,3}
e = {}
foo(a)
def foo(a=4):
b = 4
if a != b:
raise Exception("a is not equal to 4")
try:
bar(3)
except Exception as ex:
tb = traceback.TracebackException.from_exception(ex, capture_locals=True)
print("".join(tb.format()))
Which prints all local variables for each frame:
$ ./test.py
Traceback (most recent call last):
File "./test.py", line 21, in <module>
bar(3)
__annotations__ = {}
__builtins__ = <module 'builtins' (built-in)>
__cached__ = None
__doc__ = None
__file__ = './test.py'
__loader__ = <_frozen_importlib_external.SourceFileLoader object at 0x7f81073704c0>
__name__ = '__main__'
__package__ = None
__spec__ = None
bar = <function bar at 0x7f81073b11f0>
c = ['Help me!']
ex = Exception('a is not equal to 4')
foo = <function foo at 0x7f810728a160>
traceback = <module 'traceback' from '/usr/lib/python3.8/traceback.py'>
File "./test.py", line 11, in bar
foo(a)
a = 3
d = {1, 2, 3}
e = {}
File "./test.py", line 17, in foo
raise Exception("a is not equal to 4")
a = 3
b = 4
Exception: a is not equal to 4
Looks a bit too verbose, but sometimes this data could be vital in debugging some crash.
Loguru
There is also a package loguru that prints "Fully descriptive exceptions":
2018-07-17 01:38:43.975 | ERROR | __main__:nested:10 - What?!
Traceback (most recent call last):
File "test.py", line 12, in <module>
nested(0)
└ <function nested at 0x7f5c755322f0>
> File "test.py", line 8, in nested
func(5, c)
│ └ 0
└ <function func at 0x7f5c79fc2e18>
File "test.py", line 4, in func
return a / b
│ └ 0
└ 5
ZeroDivisionError: division by zero
Probably exist better alternatives, but you can use a decorator for this:
def print_stack_arguments(func):
def new_func(*original_args, **original_kwargs):
try:
return func(*original_args, **original_kwargs)
except Exception as e:
print('Function: ', func.__name__)
print('Args: ', original_args)
print('Kwargs: ', original_kwargs)
print(e)
raise
return new_func
#print_stack_arguments
def print_error(value):
a = []
print(a[1])
#print_stack_arguments
def print_noerror(value):
print('No exception raised')
print_noerror('testing no exception')
print_error('testing exception')
I like to see some meaningful description at assertion failure.
Here is my code and its execution:
>cat /tmp/1.py
a="aaa" + "bbb"
print(a)
assert ("hello" + a) and 0
>python /tmp/1.py
aaabbb
Traceback (most recent call last):
File "/tmp/1.py", line 3, in <module>
assert ("hello" + a) and 0
AssertionError
I am using Python 3.7.
You know why "hello" + a is not evaluated first as a string concatenation? And how can I make it?
[UPDATE] Thanks for all the replies, here is what I am looking for:
>cat /tmp/1.py
a="aaa" + "bbb"
print(a)
assert 0, "hello" + a
According to the docs, the failure message follows a comma:
assert some_condition, "This is the assert failure message".
This is equivalent to:
if __debug__:
if not some_condition:
raise AssertionError("This is the assert failure message")
And as noted in the comments, assert is not a function call. Don't add parenthesis, or you may have odd results. assert(condition, message) will be interpreted as a tuple being used as a condition with no message, and will never fail.
You can add your description just after your assert statement with a comma.
Such as:
assert ("hello" + a) and 0, 'Your description'
The result will be:
aaabbb
Traceback (most recent call last):
File "test.py", line 6, in <module>
assert ("hello" + a) and 0, "Your description"
AssertionError: Your description
I want to check a string - is it an import command? I have tried
# Helper - analyses a string - is it an import string?
"""
fromlike - from foo import bar
classic - import foo
classic_as - import foo as baz
"""
def check_is_import(string):
importname = ''
fromlike = False
classic = False
classic_as = False
if string[0:4] is 'from':
fromlike = True
importname = ''
if not fromlike and (string[0:6] is 'import'):
classic = True
importname = string.split(' ')[1]
if classic:
commandlist = string.split(' ')
if commandlist[2] is 'as':
classic_as = True
importname = commandlist[3]
del commandlist
if fromlike:
return ('fromlike', importname)
elif classic and (not classic_as):
return ('classic', importname)
elif classic_as:
return ('classic_as', importname)
else:
return ('no_import', importname)
but it worked for "fromlike" imports. (Note: I'm not asking "why does this code don't work?", I'm just searching a solution) What code will sure detect all imports? Basically my code takes a slice of the string. If the [0:4] slice equals 'from', the string is a "fromlike import". Else: if the [0:6] slice equals 'import', the string is a "classic import". If it detects 'as', it will find the pseudo-name. This function must return a tuple which contains the import type under index 0 and imported module-name under index 1.
If you want to be sure to handle all Python import forms, have Python do the parsing. Use the ast.parse() function and use the resulting parse tree; you'll either get Import or ImportFrom objects:
| Import(alias* names)
| ImportFrom(identifier? module, alias* names, int? level)
Each alias consists of a name and optional identifier used to import the name as:
-- import name with optional 'as' alias.
alias = (identifier name, identifier? asname)
Note that there can be multiple imports! You either have classic or fromlike imports, and both can import multiple names. Your function needs to return a list of (type, name) tuples. For invalid inputs, raise an exception (ValueError is a good fit here):
import ast
def check_is_import(string):
try:
body = ast.parse(string).body
except SyntaxError:
# not valid Python
raise ValueError('No import found')
if len(body) > 1:
# not a single statement
raise ValueError('Multiple statements found')
if not isinstance(body[0], (ast.Import, ast.ImportFrom)):
raise ValueError('No import found')
type_ = 'classic' if isinstance(body[0], ast.Import) else 'fromlike'
results = []
for alias in body[0].names:
alias_type = type_
if alias.asname:
alias_type += '_as'
results.append((alias_type, alias.asname or alias.name))
return results
The method should probably be renamed to extract_import_names(), as that reflects what it does much better.
Demo:
>>> check_is_import('from foo import bar')
[('fromlike', 'bar')]
>>> check_is_import('import foo')
[('classic', 'foo')]
>>> check_is_import('import foo as baz')
[('classic_as', 'baz')]
>>> check_is_import('from foo import bar, baz as spam, monty as python')
[('fromlike', 'bar'), ('fromlike_as', 'spam'), ('fromlike_as', 'python')]
>>> check_is_import('import foo as baz, baz, spam as ham')
[('classic_as', 'baz'), ('classic', 'baz'), ('classic_as', 'ham')]
>>> check_is_import('invalid python')
Traceback (most recent call last):
File "<stdin>", line 3, in check_is_import
File "/Users/mjpieters/Development/Library/buildout.python/parts/opt/lib/python3.6/ast.py", line 35, in parse
return compile(source, filename, mode, PyCF_ONLY_AST)
File "<unknown>", line 1
invalid python
^
SyntaxError: invalid syntax
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 6, in check_is_import
ValueError: No import found
>>> check_is_import('import foo; import bar')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 9, in check_is_import
ValueError: Multiple statements found
>>> check_is_import('1 + 1 == 2')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 11, in check_is_import
ValueError: No import found
I've tried two different versions of the same function:
def position_of(self, table_name, column_name):
positions = self.heading_positions()
position = positions['{t}.{c}'.format(t=table_name, c=column_name)]
return position
-
def position_of(self, table_name, column_name):
positions = self.heading_positions()
try:
position = positions['{t}.{c}'.format(t=table_name, c=column_name)]
except KeyError:
raise RuntimeError('No heading found for {t}.{c} in import profile "{ip}"'.format(t=table_name, c=column_name, ip=self))
return position
With the first version, I get the following error, which is fine:
Traceback (most recent call last):
File "./import.py", line 15, in <module>
g.process()
File "/home/jason/projects/mcifdjango/mcif/models/generic_import.py", line 39, in process
row.process()
File "/home/jason/projects/mcifdjango/mcif/models/csv_row.py", line 18, in process
self.save()
File "/home/jason/projects/mcifdjango/mcif/models/csv_row.py", line 26, in save
self.output("Phone: " + self.value('customer', 'phone'));
File "/home/jason/projects/mcifdjango/mcif/models/csv_row.py", line 60, in value
print self.generic_import.import_profile.position_of(table_name, column_name)
File "/home/jason/projects/mcifdjango/mcif/models/import_profile.py", line 22, in position_of
position = positions['{t}.{c}'.format(t=table_name, c=column_name)]
KeyError: 'customer.phone'
But the second version - the one that has the more informative error description - fails silently. Why is this?
The second version of position_of works fine for me. I have turned it into a minimal complete program as follows:
class Test(object):
def heading_positions(self):
return {}
def position_of(self, table_name, column_name):
positions = self.heading_positions()
try:
position = positions['{t}.{c}'.format(t=table_name, c=column_name)]
except KeyError:
raise RuntimeError('No heading found for {t}.{c} in import profile "{ip}"'.format(t=table_name, c=column_name, ip=self))
return position
a = Test()
a.position_of('customer', 'phone')
When I run this (using Python 2.6.6 on MacOS X) I get the following error message as expected:
Traceback (most recent call last):
File "./a.py", line 17, in <module>
a.position_of('customer', 'phone')
File "./a.py", line 13, in position_of
raise RuntimeError('No heading found for {t}.{c} in import profile "{ip}"'.format(t=table_name, c=column_name, ip=self))
RuntimeError: No heading found for customer.phone in import profile "<__main__.Test object at 0x100426ad0>"
This shows that catching the KeyError and turning it into a RuntimeError works fine. Does this example work for you? As Sven already writes, a possible explanation would be if you catch RuntimeError but not KeyError somewhere in the call chain.