diff --git a/Lib/code.py b/Lib/code.py index f28371fc5c9..3554b35de97 100644 --- a/Lib/code.py +++ b/Lib/code.py @@ -1,4 +1,7 @@ -"""Utilities dealing with code objects.""" +"""Utilities dealing with code objects. + +Inspired by similar code by Jeff Epler and Fredrik Lundh. +""" import sys import string @@ -13,14 +16,19 @@ def compile_command(source, filename="", symbol="single"): filename -- optional filename from which source was read; default "" symbol -- optional grammar start symbol; "single" (default) or "eval" - Return value / exception raised: + Return value / exceptions raised: - Return a code object if the command is complete and valid - Return None if the command is incomplete - - Raise SyntaxError if the command is a syntax error + - Raise SyntaxError or OverflowError if the command is a syntax error + (OverflowError if the error is in a numeric constant) Approach: - + + First, check if the source consists entirely of blank lines and + comments; if so, replace it with 'pass', because the built-in + parser doesn't always do the right thing for these. + Compile three times: as is, with \n, and with \n\n appended. If it compiles as is, it's complete. If it compiles with one \n appended, we expect more. If it doesn't compile either way, we @@ -28,10 +36,27 @@ def compile_command(source, filename="", symbol="single"): If the errors are the same, the code is broken. But if the errors are different, we expect more. Not intuitive; not even guaranteed to hold in future releases; but this matches the compiler's - behavior in Python 1.4 and 1.5. + behavior from Python 1.4 through 1.5.2, at least. + + Caveat: + + It is possible (but not likely) that the parser stops parsing + with a successful outcome before reaching the end of the source; + in this case, trailing symbols may be ignored instead of causing an + error. For example, a backslash followed by two newlines may be + followed by arbitrary garbage. This will be fixed once the API + for the parser is better. """ + # Check for source consisting of only blank lines and comments + for line in string.split(source, "\n"): + line = string.strip(line) + if line and line[0] != '#': + break # Leave it alone + else: + source = "pass" # Replace it with a 'pass' statement + err = err1 = err2 = None code = code1 = code2 = None @@ -64,32 +89,189 @@ def compile_command(source, filename="", symbol="single"): raise SyntaxError, err1 -class InteractiveConsole: - """Closely emulate the behavior of the interactive Python interpreter. +class InteractiveInterpreter: + """Base class for InteractiveConsole. + + This class deals with parsing and interpreter state (the user's + namespace); it doesn't deal with input buffering or prompting or + input file naming (the filename is always passed in explicitly). - After code by Jeff Epler and Fredrik Lundh. """ - def __init__(self, filename="", locals=None): + def __init__(self, locals=None): """Constructor. - The optional filename argument specifies the (file)name of the - input stream; it will show up in tracebacks. It defaults to - ''. + The optional 'locals' argument specifies the dictionary in + which code will be executed; it defaults to a newly created + dictionary with key "__name__" set to "__console__" and key + "__doc__" set to None. """ - self.filename = filename if locals is None: - locals = {} + locals = {"__name__": "__console__", "__doc__": None} self.locals = locals + + def runsource(self, source, filename="", symbol="single"): + """Compile and run some source in the interpreter. + + Arguments are as for compile_command(). + + One several things can happen: + + 1) The input is incorrect; compile_command() raised an + exception (SyntaxError or OverflowError). A syntax traceback + will be printed by calling the showsyntaxerror() method. + + 2) The input is incomplete, and more input is required; + compile_command() returned None. Nothing happens. + + 3) The input is complete; compile_command() returned a code + object. The code is executed by calling self.runcode() (which + also handles run-time exceptions, except for SystemExit). + + The return value is 1 in case 2, 0 in the other cases (unless + an exception is raised). The return value can be used to + decide whether to use sys.ps1 or sys.ps2 to prompt the next + line. + + """ + try: + code = compile_command(source, filename, symbol) + except (OverflowError, SyntaxError): + # Case 1 + self.showsyntaxerror(filename) + return 0 + + if code is None: + # Case 2 + return 1 + + # Case 3 + self.runcode(code) + return 0 + + def runcode(self, code): + """Execute a code object. + + When an exception occurs, self.showtraceback() is called to + display a traceback. All exceptions are caught except + SystemExit, which is reraised. + + A note about KeyboardInterrupt: this exception may occur + elsewhere in this code, and may not always be caught. The + caller should be prepared to deal with it. + + """ + try: + exec code in self.locals + except SystemExit: + raise + except: + self.showtraceback() + + def showsyntaxerror(self, filename=None): + """Display the syntax error that just occurred. + + This doesn't display a stack trace because there isn't one. + + If a filename is given, it is stuffed in the exception instead + of what was there before (because Python's parser always uses + "" when reading from a string). + + The output is written by self.write(), below. + + """ + type, value, sys.last_traceback = sys.exc_info() + sys.last_type = type + sys.last_value = value + if filename and type is SyntaxError: + # Work hard to stuff the correct filename in the exception + try: + msg, (dummy_filename, lineno, offset, line) = value + except: + # Not the format we expect; leave it alone + pass + else: + # Stuff in the right filename + try: + # Assume SyntaxError is a class exception + value = SyntaxError(msg, (filename, lineno, offset, line)) + except: + # If that failed, assume SyntaxError is a string + value = msg, (filename, lineno, offset, line) + list = traceback.format_exception_only(type, value) + map(self.write, list) + + def showtraceback(self): + """Display the exception that just occurred. + + We remove the first stack item because it is our own code. + + The output is written by self.write(), below. + + """ + try: + type, value, tb = sys.exc_info() + sys.last_type = type + sys.last_value = value + sys.last_traceback = tb + tblist = traceback.extract_tb(tb) + del tblist[:1] + list = traceback.format_list(tblist) + if list: + list.insert(0, "Traceback (innermost last):\n") + list[len(list):] = traceback.format_exception_only(type, value) + finally: + tblist = tb = None + map(self.write, list) + + def write(self, data): + """Write a string. + + The base implementation writes to sys.stderr; a subclass may + replace this with a different implementation. + + """ + sys.stderr.write(data) + + +class InteractiveConsole(InteractiveInterpreter): + """Closely emulate the behavior of the interactive Python interpreter. + + This class builds on InteractiveInterpreter and adds prompting + using the familiar sys.ps1 and sys.ps2, and input buffering. + + """ + + def __init__(self, locals=None, filename=""): + """Constructor. + + The optional locals argument will be passed to the + InteractiveInterpreter base class. + + The optional filename argument should specify the (file)name + of the input stream; it will show up in tracebacks. + + """ + InteractiveInterpreter.__init__(self, locals) + self.filename = filename self.resetbuffer() def resetbuffer(self): - """Reset the input buffer (but not the variables!).""" + """Reset the input buffer.""" self.buffer = [] def interact(self, banner=None): - """Closely emulate the interactive Python console.""" + """Closely emulate the interactive Python console. + + The optional banner argument specify the banner to print + before the first interaction; by default it prints a banner + similar to the one printed by the real Python interpreter, + followed by the current class name in parentheses (so as not + to confuse this with the real interpreter -- since it's so + close!). + + """ try: sys.ps1 except AttributeError: @@ -126,103 +308,23 @@ class InteractiveConsole: def push(self, line): """Push a line to the interpreter. - The line should not have a trailing newline. - - One of three things will happen: - - 1) The input is incorrect; compile_command() raised - SyntaxError. A syntax traceback will be printed. - - 2) The input is incomplete, and more input is required; - compile_command() returned None. - - 3) The input is complete; compile_command() returned a code - object. The code is executed. When an exception occurs, a - traceback is printed. All exceptions are caught except - SystemExit, which is reraised. - - The return value is 1 in case 2, 0 in the other cases. (The - return value can be used to decide whether to use sys.ps1 or - sys.ps2 to prompt the next line.) - - A note about KeyboardInterrupt: this exception may occur - elsewhere in this code, and will not always be caught. The - caller should be prepared to deal with it. + The line should not have a trailing newline; it may have + internal newlines. The line is appended to a buffer and the + interpreter's runsource() method is called with the + concatenated contents of the buffer as source. If this + indicates that the command was executed or invalid, the buffer + is reset; otherwise, the command is incomplete, and the buffer + is left as it was after the line was appended. The return + value is 1 if more input is required, 0 if the line was dealt + with in some way (this is the same as runsource()). """ self.buffer.append(line) - - try: - x = compile_command(string.join(self.buffer, "\n"), - filename=self.filename) - except SyntaxError: - # Case 1 - self.showsyntaxerror() + source = string.join(self.buffer, "\n") + more = self.runsource(source, self.filename) + if not more: self.resetbuffer() - return 0 - - if x is None: - # Case 2 - return 1 - - # Case 3 - try: - exec x in self.locals - except SystemExit: - raise - except: - self.showtraceback() - self.resetbuffer() - return 0 - - def showsyntaxerror(self): - """Display the syntax error that just occurred. - - This doesn't display a stack trace because there isn't one. - - The output is written by self.write(), below. - - """ - type, value = sys.exc_info()[:2] - # Work hard to stuff the correct filename in the exception - try: - msg, (filename, lineno, offset, line) = value - except: - pass - else: - try: - value = SyntaxError(msg, (self.filename, lineno, offset, line)) - except: - value = msg, (self.filename, lineno, offset, line) - list = traceback.format_exception_only(type, value) - map(self.write, list) - - def showtraceback(self): - """Display the exception that just occurred. - - We remove the first stack item because it is our own code. - - The output is written by self.write(), below. - - """ - try: - type, value, tb = sys.exc_info() - tblist = traceback.extract_tb(tb) - del tblist[0] - list = traceback.format_list(tblist) - list[len(list):] = traceback.format_exception_only(type, value) - finally: - tblist = tb = None - map(self.write, list) - - def write(self, data): - """Write a string. - - The base implementation writes to sys.stderr; a subclass may - replace this with a different implementation. - - """ - sys.stderr.write(data) + return more def raw_input(self, prompt=""): """Write a prompt and read a line. @@ -242,24 +344,26 @@ def interact(banner=None, readfunc=None, locals=None): """Closely emulate the interactive Python interpreter. This is a backwards compatible interface to the InteractiveConsole - class. It attempts to import the readline module to enable GNU - readline if it is available. + class. When readfunc is not specified, it attempts to import the + readline module to enable GNU readline if it is available. Arguments (all optional, all default to None): banner -- passed to InteractiveConsole.interact() readfunc -- if not None, replaces InteractiveConsole.raw_input() - locals -- passed to InteractiveConsole.__init__() + locals -- passed to InteractiveInterpreter.__init__() """ - try: - import readline - except: - pass - console = InteractiveConsole(locals=locals) + console = InteractiveConsole(locals) if readfunc is not None: console.raw_input = readfunc + else: + try: + import readline + except: + pass console.interact(banner) - + + if __name__ == '__main__': interact()