]> granicus.if.org Git - python/commitdiff
Reworked it quite a bit. There are now two classes: a base class,
authorGuido van Rossum <guido@python.org>
Tue, 22 Sep 1998 20:38:53 +0000 (20:38 +0000)
committerGuido van Rossum <guido@python.org>
Tue, 22 Sep 1998 20:38:53 +0000 (20:38 +0000)
InteractiveInterpreter, which handles parsing and interpreter state
but doesn't know deal with buffering or prompting or input file
naming.  And a derived class, InteractiveConsole, which adds buffering
and prompting and supports setting the filename once.  Also tweak the
algorithm in compile_command() a bit so that input consisting of all
blank lines or comments always succeeds immediately, and note the fact
that apart from SyntaxError it can also raise OverflowError.

Lib/code.py

index f28371fc5c98ab9de5cca52f12146c69d354d838..3554b35de9798dbbe375759f43b8e5eec689ea07 100644 (file)
@@ -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="<input>", symbol="single"):
     filename -- optional filename from which source was read; default "<input>"
     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="<input>", 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,136 +89,116 @@ def compile_command(source, filename="<input>", 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="<console>", 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
-        '<console>'.
+        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
-        self.resetbuffer()
 
-    def resetbuffer(self):
-        """Reset the input buffer (but not the variables!)."""
-        self.buffer = []
+    def runsource(self, source, filename="<input>", symbol="single"):
+        """Compile and run some source in the interpreter.
 
-    def interact(self, banner=None):
-        """Closely emulate the interactive Python console."""
-        try:
-            sys.ps1
-        except AttributeError:
-            sys.ps1 = ">>> "
-        try:
-            sys.ps2
-        except AttributeError:
-            sys.ps2 = "... "
-        if banner is None:
-            self.write("Python %s on %s\n%s\n(%s)\n" %
-                       (sys.version, sys.platform, sys.copyright,
-                        self.__class__.__name__))
-        else:
-            self.write("%s\n" % str(banner))
-        more = 0
-        while 1:
-            try:
-                if more:
-                    prompt = sys.ps2
-                else:
-                    prompt = sys.ps1
-                try:
-                    line = self.raw_input(prompt)
-                except EOFError:
-                    self.write("\n")
-                    break
-                else:
-                    more = self.push(line)
-            except KeyboardInterrupt:
-                self.write("\nKeyboardInterrupt\n")
-                self.resetbuffer()
-                more = 0
+        Arguments are as for compile_command().
 
-    def push(self, line):
-        """Push a line to the interpreter.
-
-        The line should not have a trailing newline.
+        One several things can happen:
 
-        One of three things will happen:
-
-        1) The input is incorrect; compile_command() raised
-        SyntaxError.  A syntax traceback will be printed.
+        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.
+        compile_command() returned None.  Nothing happens.
 
         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.
+        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.  (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 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.
 
         """
-        self.buffer.append(line)
-
         try:
-            x = compile_command(string.join(self.buffer, "\n"),
-                                filename=self.filename)
-        except SyntaxError:
+            code = compile_command(source, filename, symbol)
+        except (OverflowError, SyntaxError):
             # Case 1
-            self.showsyntaxerror()
-            self.resetbuffer()
+            self.showsyntaxerror(filename)
             return 0
 
-        if x is None:
+        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 x in self.locals
+            exec code in self.locals
         except SystemExit:
             raise
         except:
             self.showtraceback()
-        self.resetbuffer()
-        return 0
 
-    def showsyntaxerror(self):
+    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
+        "<string>" when reading from a string).
+
         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:
+        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:
-                value = SyntaxError(msg, (self.filename, lineno, offset, line))
+                msg, (dummy_filename, lineno, offset, line) = value
             except:
-                value = msg, (self.filename, lineno, offset, line)
+                # 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)
 
@@ -207,9 +212,14 @@ class InteractiveConsole:
         """
         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[0]
+            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
@@ -224,6 +234,98 @@ class InteractiveConsole:
         """
         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="<console>"):
+        """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."""
+        self.buffer = []
+
+    def interact(self, banner=None):
+        """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:
+            sys.ps1 = ">>> "
+        try:
+            sys.ps2
+        except AttributeError:
+            sys.ps2 = "... "
+        if banner is None:
+            self.write("Python %s on %s\n%s\n(%s)\n" %
+                       (sys.version, sys.platform, sys.copyright,
+                        self.__class__.__name__))
+        else:
+            self.write("%s\n" % str(banner))
+        more = 0
+        while 1:
+            try:
+                if more:
+                    prompt = sys.ps2
+                else:
+                    prompt = sys.ps1
+                try:
+                    line = self.raw_input(prompt)
+                except EOFError:
+                    self.write("\n")
+                    break
+                else:
+                    more = self.push(line)
+            except KeyboardInterrupt:
+                self.write("\nKeyboardInterrupt\n")
+                self.resetbuffer()
+                more = 0
+
+    def push(self, line):
+        """Push a line to the interpreter.
+
+        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)
+        source = string.join(self.buffer, "\n")
+        more = self.runsource(source, self.filename)
+        if not more:
+            self.resetbuffer()
+        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()