13

Let's say I have the following multi-line string:

cmd = """
    a = 1 + 1
    b = [
       2 + 2,
       4 + 4,
    ]
    bork bork bork
"""

and I want to execute it in a particular scope:

scope = {}
exec( cmd, scope )
print scope[ 'b' ]

There's a SyntaxError at line 6 of the command, and I want to be able to report that to the user. How do I get the line number? I've tried this:

try:
    exec( cmd, scope )  # <-- let's say this is on line 123 of the source file
except Exception, err:
    a, b, c = sys.exc_info()
    line_number = c.tb_lineno  # <-- this gets me 123,  not 6
    print "%s at line %d (%s)" % ( a, line_number, b.message )

...but I get the line number of the exec statement, not the line number within the multi-line command.

Update: it turns out the handling of the type of exception that I arbitrarily chose for this example, the SyntaxError, is different from the handling of any other type. To clarify, I'm looking a solution that copes with any kind of exception.

jez
  • 14,867
  • 5
  • 37
  • 64

1 Answers1

16

For syntax errors, the source line number is available as the lineno flag on the exception object itself, in your case stored in err. This is specific to syntax errors where the line number is an integral part of the error:

>>> cmd = """
... 1 \ +
... 2 * "
... """
>>> try:
...   exec cmd
... except SyntaxError as err:
...   print err.lineno
... 
2

If you want to also handle other errors, add a new except block except Exception, err, and use the traceback module to compute the line number for the runtime error.

import sys
import traceback

class InterpreterError(Exception): pass

def my_exec(cmd, globals=None, locals=None, description='source string'):
    try:
        exec(cmd, globals, locals)
    except SyntaxError as err:
        error_class = err.__class__.__name__
        detail = err.args[0]
        line_number = err.lineno
    except Exception as err:
        error_class = err.__class__.__name__
        detail = err.args[0]
        cl, exc, tb = sys.exc_info()
        line_number = traceback.extract_tb(tb)[-1][1]
    else:
        return
    raise InterpreterError("%s at line %d of %s: %s" % (error_class, line_number, description, detail))

Examples:

>>> my_exec("1+1")  # no exception
>>>
>>> my_exec("1+1\nbork")
...
InterpreterError: NameError at line 2 of source string: name 'bork' is not defined
>>>
>>> my_exec("1+1\nbork bork bork")
...
InterpreterError: SyntaxError at line 2 of source string: invalid syntax
>>>
>>> my_exec("1+1\n'''")
...
InterpreterError: SyntaxError at line 2 of source string: EOF while scanning triple-quoted string
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
user4815162342
  • 141,790
  • 18
  • 296
  • 355
  • Interesting! I hadn't expected `SyntaxError` to be different from other types. Of course, I want to be able to report other errors too. Your answer put me on the right track. So that I can mark it as accepted, if you don't mind I'll edit your answer to include the full solution that this led me to (and so you can improve it further if you think it should be). – jez Mar 03 '15 at 21:38
  • 3
    @jez Normally edits are reserved for fixes, not for large interventions in the answer. I would at least make the code abide by PEP 8. The likely reason why `SyntaxError` is different is that it comes from the compiler before the code has ever had a chance to run. Other errors come from the run-time engine and the line number must be extracted form the traceback. You can think of it like the difference between a compile-time and a run-time error in a static language - they are different because they get raised by very different mechanisms, and at different times. – user4815162342 Mar 03 '15 at 22:52
  • Noted on all counts. Normally I laugh in the face of pep8 but I figure, your space your rules. And of course delete it if you think it inappropriate—I just wanted to accept your answer since it led me to the solution, and wanted the accepted answer to contain a complete solution (feels wrong to use your suggested approach to write my own answer which I then accept) – jez Mar 04 '15 at 13:33
  • @jez Thanks. Out of curiosity, why "laugh in the face of pep8"? I tend to abide by "when in Rome..." - for example, when writing Java, I camelCaseMyMethods, and so on. – user4815162342 Mar 04 '15 at 14:12
  • I don't think of myself as being *in* Rome every time I choose to speak Latin. There are plenty of common-sense things on pep8 but also plenty of highly subjective ones, where one man's "more readable" is another man's "less readable", and even one or two issues where I find myself on the other side of the "holy war" (e.g. tabs vs spaces). My code needs to be maximally readable/maintainable by me, not by Guido—and as for third parties, I figure they're as likely to prefer one one arbitrary set of conventions as another. – jez Mar 04 '15 at 20:02
  • @jez Yeah, all coding standards are about subjective, not (oinly) objective common-sense things everyone agrees on. And if you're the only person maintaining your code, of course you need none of it. – user4815162342 Mar 05 '15 at 06:51
  • Unfortunately the traceback.extract() method is giving me line numbers in a function called within the string, not the string itself. – Jonathan Stray Jan 11 '18 at 00:55
  • I was able to get the line within the string by using `line_number = traceback.extract_tb(tb)[1][1]` – Jonathan Stray Jan 11 '18 at 01:01
  • The problem is that; I am not able to capture the lines before the error; for example: printing 5 before an error results with only the error. However, I also need to be able to access the printed 5 value as well for my use case. Any idea on how? @user4815162342 – Can Arda Aydin May 15 '22 at 22:15
  • @CanArdaAydin That should be asked as a separate question because the requirement to capture the output significantly affects the solutions. – user4815162342 May 16 '22 at 04:20