asteval
asteval copied to clipboard
Improve the propagation of Exception information (the bad name for a NameError)
I have some suggestions relating to how exceptions (NameError) are raised by asteval when evaluating a Module object (whatever that is...). This happens when used by lmfit.Parameters, but to my mind the issue is on the asteval side so I create the feature request here.
Demonstration of current behaviour
from asteval import Interpreter
from lmfit import Parameter, Parameters
try:
print('§ 1. Directly with asteval', flush=True)
interpreter = Interpreter()
# Using two lines to both see the printout and raise an exception, as via lmfit.Parameters:
interpreter.eval('1 + unknown', show_errors=True)
interpreter.raise_exception(None)
print('§ Continued executing.', flush=True)
except NameError as e:
print(f'§ Caught {repr(e)}\n-----'.replace('\n', '\n§ '), flush=True)
time.sleep(1) # Wait for the printout by asteval.Interpreter.eval() to appear
print('§ =========================', flush=True)
try:
print('§ 2. Via LMFIT Parameters', flush=True)
params = Parameters()
params.add('example', expr='1 + unknown')
print(f'§ Error-holder has the msg: {interpreter.error[0].msg}', flush=True)
check_ast_errors(interpreter)
print('§ Continued executing.', flush=True)
except NameError as e:
print(f'§ Caught {repr(e)}\n-----'.replace('\n', '\n§ '), flush=True)
The above block of code outputs the following (lines not starting with § were printed directly by asteval):
§ 1. Directly with asteval § Caught NameError("at expr='1 + unknown'") § ----- NameError 1 + unknown ^^^ name 'unknown' is not defined § ========================= § 2. Via LMFIT Parameters § Caught NameError("at expr='<_ast.Module object at 0x0000020BC0DE3EC8>'") § ----- NameError <_ast.Module object at 0x0000020BC0DE3EC8> ^^^ name 'unknown' is not defined
The issue
So the text expression where one can see which undefined name actually caused the exception is only contained in the NameError-instance when run as in case 1. In case 2, the NameErorr we get only has a non-informative string representation of some Module object.
Especially when happening within a new lmfit.Parameters-instance (e.g. within lmfit.Model.guess()) the caller who can catch the NameError does not have any reference to the asteval Interpreter, so even if the more informative msg-string might be held there within an ExceptionHolder-instance) the user cannot access it. (Well, maybe if the error-writer was modified to capture and parse text printed by asteval, but then wasting the chance to include info in the Exception object.) I would therefore wish to improve the raised Exception object to include the text expression even when evaluated via a Module.
Suggestions
Here's a copy of asteval.Interpreter.raise_exception() with comments added based on what I deduced from debugging with a breakpoint.
def raise_exception(self, node, exc=None, msg='', expr=None, lineno=None):
"""Add an exception."""
if self.error is None:
self.error = []
if expr is None:
expr = self.expr
# HINT: Apparently expr can be an _ast.Module object instead of str.
# Could we get its string expression somehow?
if len(self.error) > 0 and not isinstance(node, ast.Module):
msg = '%s' % msg
err = ExceptionHolder(node, exc=exc, msg=msg, expr=expr, lineno=lineno)
self._interrupt = ast.Raise()
self.error.append(err)
if self.error_msg is None:
self.error_msg = "at expr='%s'" % (self.expr)
# TODO: Although there can be good info in msg (e.g. "name 'unknown' is not defined")
# we throw away the value of msg in this case. Suggestion:
# if len(msg) > 0:
# self.error_msg = "%s at expr='%s'" % (msg, self.expr)
# else:
# self.error_msg = "at expr='%s'" % (self.expr)
elif len(msg) > 0:
self.error_msg = msg
if exc is None:
try:
exc = self.error[0].exc
except:
exc = RuntimeError
raise exc(self.error_msg)
So, my suggested improvement is in the comment beginning with TODO.
As far as I've seen, asteval does not offer a way to get the string form back from an arbitrary node, so I don't know whether the comment beginning with HINT can be handled too. It isn't needed for the kind of exception (NameError) I'm demonstrating, where the msg-string already contains enough information.
@erik-mansson At first pass, it all looks very helpful. I'm on travel this week, so I won't be able to look at it closely for a while, but I think getting better error messages would be awesome.... So thanks for this and sorry for not being able to review it closely at the moment.
@erik-mansson fixed in the master branch.