#!/usr/bin/env python
"""
PythonConsole.
Copyright (C) 2006 Olivier Mehani <shtrom@ssji.net>

$Id$
A Python class implementing a simple console outputing everything into a string
for easy interaction with the user from higher level Python code. This is
mostly useful for debugging, hence the prompt.

The compile() method and some other snippets have been taken verbatim from [0].

This can be used both as a useless program (see below) or as a useful class to
get a Python prompt during the execution of a program (see the input_loop() and
single_input() methods).

    $ ./pythonconsole.py 
    dbg> print 1
    1
    dbg> for i in xrange(2):
    ....   print i
    0
    1
    dbg> ^D

Obviously, the single_input() method is the most useful as it returns the
textual output as a string which can easily be displayed to the user.

[0] http://lfw.org/python/Console.py


This program is free software; you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation; either version 2 of the License, or (at your option) any later
version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.  See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with
this program; if not, write to the Free Software Foundation, Inc., 59 Temple
Place, Suite 330, Boston, MA  02111-1307  USA

"""

__version__ = "$Revision$"

import sys
import traceback

class PythonConsole:
    def __init__(self, globals={}):
	self.command = ""
	self.eof = 0
	self.dict = globals
	self.status = ""

    def input_loop(self):
	"""
	Enter a parse-and-execute loop ending when an EOF is caught. This is
	the method which is called when the run as a standalone python script

	Usage example:
	    >>> from pythonconsole import PythonConsole
	    >>> pc=PythonConsole(globals())
	    >>> a=2
	    >>> b=[3]
	    >>> c={a: b}
	    >>> pc.input_loop()
	    dbg> a
	    2
	    dbg> b
	    [3]
	    dbg> c
	    {2: [3]}
	    dbg> 
	    dbg> ^D
	    >>>

	"""
	self.status = "okay"
	self.eof = 0
	while not self.eof:
		try:
		    output = self.single_input()
		except EOFError:
		    print "^D"
		    self.eof = 1
		else:
		    if self.status == "okay" and output != "":
			print output,

    def single_input(self):
	"""
	Parse and execute a complete (possibly multiline) Python instruction and
	return the "answer" of the interpreter as a string.

	Usage example:
	    >>> from pythonconsole import PythonConsole
	    >>> pc=PythonConsole(globals())
	    >>> a=2
	    >>> b=[3]
	    >>> c={a: b}
	    >>> pc.single_input()
	    dbg> dir()
	    "['PythonConsole', '__builtins__', '__doc__', '__file__', '__name__', 'a', 'b', 'c', 'pc']\n"
	    dbg> for i in xrange(2):
	    ....   print i
	    '0\n1\n'
	    >>> pc.single_input()
	    dbg> Traceback (most recent call last):
	      File "<stdin>", line 1, in ?
	      File "pythonconsole.py", line 86, in single_input
		
	    EOFError
	    >>> dir()
	    ['PythonConsole', '__builtins__', '__doc__', '__file__', '__name__', 'a', 'b', 'c', 'pc']
	    >>> pc.single_input()
	    dbg> import sys
	    ''
	    >>> dir()
	    ['PythonConsole', '__builtins__', '__doc__', '__file__', '__name__', 'a', 'b', 'c', 'pc', 'sys']
	    >>> pc.single_input()
	    dbg> sys.exit()
	    # Here, the python interpreter cleanly exited.

	"""
	self.status="more"
	output = None
	prompt = "dbg> "
	while self.status == "more":

	    try:
		line = raw_input(prompt)
	    except KeyboardInterrupt:
		print
	    except EOFError:
		raise
	    else:
		output = self.exec_if_possible(line)
	    
	    prompt = ".... "

	return output

    def exec_if_possible(self, line):
	output = None
	self.command += line
	self.status, code = self.compile(self.command)
	if self.status == "okay":
	    oldstdout = sys.stdout
	    oldstderr = sys.stderr

	    sp = StringPipe()
	    sys.stdout = sp
	    sys.stderr = sp

	    try:
		exec code in self.dict
	    except SystemExit:
		raise
	    except:
		sys.last_type = sys.exc_type
		sys.last_value = sys.exc_value
		sys.last_traceback = sys.exc_traceback.tb_next
		self.intraceback = 1
		traceback.print_exception(
		    sys.last_type, sys.last_value, sys.last_traceback)
		self.intraceback = 0

	    output = sp.read()

	    self.command = ""
	    sys.stdout = oldstdout 
	    sys.stderr = oldstderr 
	elif self.status == "more":
	    self.command += "\n"
	else:
	    self.command = ""
	return output

    @staticmethod
    def compile(source):
	"""Try to compile a piece of source code, returning a status code
	and the compiled result.  If the status code is "okay" the code is
	complete and compiled successfully; if it is "more" then the code
	can be compiled, but an interactive session should wait for more
	input; if it is "bad" then there is a syntax error in the code and
	the second returned value is the error message."""
	err = err1 = err2 = None
	code = code1 = code2 = None

	try:
	    code = compile(source, "<simpleconsole>", "single")
	except SyntaxError, err:
	    pass
	else:
	    return "okay", code

	try:
	    code1 = compile(source + "\n", "<simpleconsole>", "single")
	except SyntaxError, err1:
	    pass
	else:
	    return "more", code1

	try:
	    code2 = compile(source + "\n\n", "<simpleconsole>", "single")
	except SyntaxError, err2:
	    pass

	try:
	    code3 = compile(source + "\n", "<simpleconsole>", "exec")
	except SyntaxError, err3:
	    pass
	else:
	    return "okay", code3

	try:
	    code4 = compile(source + "\n\n", "<simpleconsole>", "exec")
	except SyntaxError, err4:
	    pass

	if err3[1][2] != err4[1][2]:
	    return "more", None

	if err1[1][2] != err2[1][2]:
	    return "more", None

	return "bad", err1

class StringPipe:
    """
    Small class behaving as a file but doing the reading/writing operation
    from/to a string.

    """
    string = ""
    closed = 0

    def close(self):
	self.closed = 1

    def read(self):
	if not self.closed:
	    str = self.string
	    self.string = ""
	    return str

    def write(self, str):
	if not self.closed:
	    self.string += str


if __name__ == '__main__':
    pc = PythonConsole(globals())
    pc.input_loop()
