asteval icon indicating copy to clipboard operation
asteval copied to clipboard

Improve the propagation of Exception information (the bad name for a NameError)

Open erik-mansson opened this issue 2 years ago • 2 comments

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 avatar Jul 08 '22 12:07 erik-mansson

@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.

newville avatar Jul 09 '22 05:07 newville

@erik-mansson fixed in the master branch.

newville avatar Sep 01 '22 16:09 newville