pilasengine/interprete/lanas.py
# -*- encoding: utf-8 -*-
# pilas engine: un motor para hacer videojuegos
#
# Copyright 2010-2014 - Hugo Ruscitti
# License: LGPLv3 (see http://www.gnu.org/licenses/lgpl.html)
#
# Website - http://www.pilas-engine.com.ar
import sys
import inspect
import os
import code
import traceback
os.environ['lanas'] = 'enabled'
from PyQt4.QtGui import (QWidget, QDesktopWidget, QPalette,
QColor, QTextCursor, QTextEdit,
QInputDialog, QApplication,
QKeyEvent)
from PyQt4.QtCore import Qt, QTimer
from editorbase import editor_base
import lanas_ui
import io_lanas
class PythonInterpreter(code.InteractiveInterpreter):
def __init__(self, localVars, interprete_lanas):
code.InteractiveInterpreter.__init__(self, localVars)
self.interprete_lanas = interprete_lanas
self.error_io = io_lanas.ErrorOutput(interprete_lanas)
self.normal_io = io_lanas.NormalOutput(interprete_lanas)
def runcode(self, cd):
self.sobreescribir_salida_por_consola(sys)
# redirecting stdout to our method write before calling code cd
code.InteractiveInterpreter.runcode(self, cd)
# redirecting back to normal stdout
self.restaurar_salida_por_consola(sys)
def imprimir_en_pantalla(self, *arg):
self.sobreescribir_salida_por_consola(sys)
print ' '.join([str(x) for x in arg])
self.restaurar_salida_por_consola(sys)
def sobreescribir_salida_por_consola(self, sys):
sys.stdout = self.normal_io
sys.stderr = self.error_io
def restaurar_salida_por_consola(self, sys):
sys.stdout = sys.__stdout__
sys.stderr = sys.__stderr__
def showsyntaxerror(self, filename=None):
self.error_io.write("Error de sintaxis en el codigo anterior.")
def write(self, data):
self.error_io.write(data)
def es_metodo(self, linea):
codigo = "type(%s).__name__" %(linea)
try:
tipo_de_dato = eval(codigo, self.locals)
except Exception, e:
return None
comparacion = tipo_de_dato == "instancemethod"
return comparacion
class WidgetLanas(QWidget, lanas_ui.Ui_Lanas):
def __init__(self, parent=None, scope=None, codigo_inicial=None):
super(WidgetLanas, self).__init__(parent)
self.setupUi(self)
self.lanas = InterpreteLanas(self, codigo_inicial)
self.lanas.init(scope)
self.widget_interprete.addWidget(self.lanas)
self.resize(650, 300)
self.center_on_screen()
def obtener_scope(self):
return self.lanas.interpreterLocals
def center_on_screen(self):
resolution = QDesktopWidget().screenGeometry()
self.move((resolution.width() / 2) - (self.frameSize().width() / 2),
(resolution.height() / 2) - (self.frameSize().height() / 2))
def closeEvent(self, event):
sys.exit(0)
def alternar_log(self):
if self.log_widget.isHidden():
self.log_widget.show()
else:
self.log_widget.hide()
class InterpreteLanas(editor_base.EditorBase):
"""Representa el widget del interprete.
Esta instancia tiene como atributo "self.ventana" al
al QWidget representado por la clase Ventana.
"""
def __init__(self, parent, codigo_inicial=None):
super(InterpreteLanas, self).__init__()
self.ventana = parent
self.refreshMarker = False
self.multiline = False
self.command = ''
self.history = []
self.historyIndex = -1
self.interpreterLocals = {'raw_input': self.raw_input,
'input': self.input,
'sys': sys,
'help': self.help,
'ayuda': self.help}
palette = QPalette()
palette.setColor(QPalette.Text, QColor(0, 0, 0))
self.setPalette(palette)
if codigo_inicial:
self.insertar_codigo_falso(codigo_inicial)
self.marker()
self.setUndoRedoEnabled(False)
self.setContextMenuPolicy(Qt.NoContextMenu)
self.timer_cursor = QTimer()
self.timer_cursor.start(1000)
self.timer_cursor.timeout.connect(self.marker_si_es_necesario)
def insertFromMimeData(self, source):
QTextEdit.insertPlainText(self, source.text())
def funcion_valores_autocompletado(self, texto):
scope = self.interpreterLocals
texto = texto.replace('(', ' ').split(' ')[-1]
resultados = []
if '.' in texto:
palabras = texto.split('.')
ultima = palabras.pop()
prefijo = '.'.join(palabras)
try:
sentencia_para_obtener_autocompletado = "[(x, callable(getattr(eval('%s'), x))) for x in dir(%s)]" %(prefijo, prefijo)
items = eval(sentencia_para_obtener_autocompletado, scope)
elementos = []
for (x, invocable) in items:
if invocable:
elementos.append(x + '(')
else:
elementos.append(x)
except:
# TODO: notificar este error de autocompletado en algun lado...
return []
resultados = [a for a in elementos if a.lower().startswith(ultima.lower())]
else:
resultados = [a for a in scope.keys() if a.lower().startswith(texto.lower())]
return resultados
def canInsertFromMimeData(self, *k):
return False
def imprimir_linea(self, linea):
self.insertPlainText(linea)
def insertar_error(self, mensaje):
mensajes = mensaje.split('\n')
for m in mensajes:
m = m.replace('Traceback (most recent call last)', 'Traza del error (las llamadas mas recientes al final)')
m = m.replace('File "<string>"', 'Archivo actual')
m = m.replace('File ', 'Archivo ')
m = m.replace(' line ', 'linea ')
m = m.replace('in ', 'en ')
m = m.replace('\t', ' ')
for x in range(10):
m = m.replace(' ', ' ')
self.insertHtml(u" <b style='color: #F00000'> ×</b> # %s <br>" %(m.decode('utf-8')))
def insertar_error_desde_exception(self, e):
self.insertPlainText('\n')
self.insertar_error("%s: %s" %(e.__class__.__name__, e.message))
tb = traceback.format_exc()
if tb:
self.insertar_error(" ")
self.insertar_error(tb)
self._mover_cursor_al_final()
def insertar_mensaje(self, mensaje):
self.insertPlainText('\n')
self.insertHtml(u" <b style='color: green'> %s </b><br>" %(mensaje))
self.insertPlainText('\n')
self._mover_cursor_al_final()
def insertar_codigo_falso(self, codigo):
for line in codigo.splitlines():
self.marker()
self.insertHtml(line)
self.insertPlainText('\n')
def marker(self):
if self.multiline:
self.insertHtml(u"<b style='color: #FF0000'>‥</b> ")
self.insertPlainText(" ")
else:
self.insertHtml(u"<b style='color: #3583FC'>»</b> ")
def init(self, interpreter_locals):
# Mover este metodo dentro de __init__ ?
if interpreter_locals:
interpreter_locals.update(self.interpreterLocals)
self.interpreterLocals = interpreter_locals
if 'inspect' not in self.interpreterLocals:
self.interpreterLocals['inspect'] = inspect
self.interpreter = PythonInterpreter(self.interpreterLocals, self)
def clearCurrentBlock(self):
block = self.document().lastBlock().text()[2:]
for char in block:
self.textCursor().deletePreviousChar()
def recall_history(self):
self._mover_cursor_al_final()
self.clearCurrentBlock()
if self.historyIndex != -1:
self.insertPlainText(self.history[self.historyIndex])
def _get_entered_line(self):
textCursor = self.textCursor()
position = len(self.document().toPlainText())
textCursor.setPosition(position)
self.setTextCursor(textCursor)
line = unicode(self.document().lastBlock().text())[2:]
line.rstrip()
return line
def _get_position_in_block(self):
tc = self.textCursor()
position = tc.positionInBlock() - 3
return position
def _get_current_block_prefix(self):
tc = self.textCursor()
tc.select(QTextCursor.BlockUnderCursor)
current_block = tc.selectedText()
words = current_block.split(" ")
prefix = words[-1]
if '(' in prefix:
prefix = prefix.split('(')[-1]
if '.' in prefix:
prefixes = prefix.split('.')
prefixes = prefixes[:-1]
return '.'.join([str(s) for s in prefixes])
else:
return prefix
def _set_current_word(self, word):
tc = self.textCursor()
tc.select(QTextCursor.WordUnderCursor)
tc.removeSelectedText()
tc.insertText(word)
def _mover_cursor_al_final(self):
textCursor = self.textCursor()
textCursor.movePosition(QTextCursor.End)
self.setTextCursor(textCursor)
return textCursor
def _get_current_line(self):
"Obtiene la linea en donde se encuentra el cursor."
tc = self.textCursor()
tc.select(QTextCursor.LineUnderCursor)
return tc.selectedText()[2:]
def keyReleaseEvent(self, event):
if event.key() == Qt.Key_ParenLeft:
self._autocompletar_argumentos_si_corresponde()
def keyPressEvent(self, event):
# Permite usar tab como seleccionador de la palabra actual
# en el popup de autocompletado.
if event.key() in [Qt.Key_Tab]:
if self.completer and self.completer.popup().isVisible():
event.ignore()
nuevo_evento = QKeyEvent(QKeyEvent.KeyPress, Qt.Key_Return, Qt.NoModifier)
try:
if self.autocomplete(nuevo_evento):
return None
except UnicodeEncodeError:
pass
return None
if editor_base.EditorBase.keyPressEvent(self, event):
return None
textCursor = self.textCursor()
# Permite mantener pulsada la tecla CTRL para copiar o pegar.
if event.modifiers() & Qt.ControlModifier:
# Ignorando pegar texto si cursor está en medio de consola.
if textCursor.blockNumber() != self.document().blockCount() - 1:
if event.key() == Qt.Key_V:
textCursor = self._mover_cursor_al_final()
return
# Ignorando pegar texto si cursor está sobre el prompt de consola.
elif textCursor.positionInBlock() < 2:
if event.key() == Qt.Key_V:
textCursor = self._mover_cursor_al_final()
return
return QTextEdit.keyPressEvent(self, event)
# Ignorando la pulsación de tecla si está en medio de la consola.
if textCursor.blockNumber() != self.document().blockCount() - 1:
textCursor = self._mover_cursor_al_final()
return
# Ignora el evento si está sobre el cursor de la consola.
if textCursor.positionInBlock() < 2:
textCursor = self._mover_cursor_al_final()
return
if event.key() in [Qt.Key_Left, Qt.Key_Backspace]:
if self.textCursor().positionInBlock() == 2:
return
# Elimina los pares de caracteres especiales si los encuentra
if event.key() == Qt.Key_Backspace:
self._eliminar_pares_de_caracteres()
# navegar por el historial
if event.key() == Qt.Key_Down:
if self.historyIndex == len(self.history):
self.historyIndex -= 1
try:
if self.historyIndex > -1:
self.historyIndex -= 1
self.recall_history()
else:
self.clearCurrentBlock()
except:
pass
return None
# navegar por el historial
if event.key() == Qt.Key_Up:
try:
if len(self.history) - 1 > self.historyIndex:
self.historyIndex += 1
self.recall_history()
else:
self.historyIndex = len(self.history)
except:
pass
return None
# ir al primer caracter del interprete cuando pulsa HOME
if event.key() == Qt.Key_Home:
blockLength = len(self.document().lastBlock().text()[2:])
lineLength = len(self.document().toPlainText())
position = lineLength - blockLength
textCursor = self.textCursor()
textCursor.setPosition(position)
self.setTextCursor(textCursor)
return None
try:
if self.autocomplete(event):
return None
except UnicodeEncodeError:
pass
if event.key() in [Qt.Key_Return, Qt.Key_Enter]:
self.limpiar_consejo() # Limpiando consejo si existe
line = self._get_entered_line()
self.historyIndex = -1
if line == "clear":
self.limpiar()
return
try:
line[-1]
self.haveLine = True
if line[-1] in [':', ',', '[', '(']:
self.multiline = True
self.history.insert(0, line)
except:
self.haveLine = False
# Cuando pulsa ENTER luego de haber hecho un texto multilinea y borrado todos los caracteres.
if self.multiline and (not self.haveLine or self._ha_ingresado_solo_espacios(line)): # multi line done
self.append('') # move down one line
try:
self.interpreter.runsource(self.command)
except Exception, e:
self.insertar_error_desde_exception(e)
self.command = '' # clear command
self.multiline = False # back to single line
self.marker() # handle marker style
return None
if self.haveLine and self.multiline: # multi line command
self.command += line + '\n' # + command and line
self.append('')
self.marker()
return None
# Permite escribir lineas terminas con '?' para consultar la documentacion
# de manera similar a como lo hace ipython.
if line.endswith('?'):
line = 'pilas.ver(' + line[:-1] + ')'
if self.haveLine and not self.multiline: # one line command
self.command = line # line is the command
self.append('') # move down one line
if '=' in line:
primer_parte = line.split('=')[0]
if self.interpreter.es_metodo(primer_parte):
print "ES METOPDO"
self.insertar_error("No puedes sobre-escribir un metodo, lo siento.")
self.command = '' # clear command
self.marker() # handle marker style
return None
try:
self.interpreter.runsource(self.command)
except Exception, e:
self.insertar_error_desde_exception(e)
self.command = '' # clear command
self.marker() # handle marker style
return None
# Cuando pulsa ENTER sin texto.
if not self.haveLine and not self.multiline:
self.append('')
self.marker()
return None
return None
return QTextEdit.keyPressEvent(self, event)
def limpiar(self):
self.clear()
self.marker()
return
def _autocompletar_argumentos_si_corresponde(self):
"""Muestra un mensaje con la documentación de una función ejecutar.
Se invoca mientras que el usuario escribe, siempre y cuando
se vea un paréntisis.
Por ejemplo, si el usuario escribe 'os.system(' esta función
intentará mostrar un tooltip con el contenido de 'os.system.__doc__'
"""
linea = unicode(self.document().lastBlock().text())[2:]
linea.rstrip()
self.limpiar_consejo()
if '(' in linea:
principio = linea.split('(')[0]
principio = principio.split(' ')[-1]
# Obtiene el mensaje a mostrar y lo despliega en el tooltip
texto_consejo = self._obtener_firma_de_funcion(principio)
self.mostrar_consejo(texto_consejo)
def limpiar_consejo(self):
self.mostrar_consejo("")
def _obtener_firma_de_funcion(self, texto):
codigo = self.ver_codigo(texto)
if codigo:
posicion = codigo.find(":")
codigo = codigo[:posicion].replace("def ", "").replace(' ', '').replace('self, ', '').replace('self', '').replace('\n', ' ')
return codigo
else:
return ""
def ver_codigo(self, texto):
codigo = ""
try:
try:
texto_a_ejecutar = 'inspect.isclass(' + texto + ")"
es_clase = eval(texto_a_ejecutar, self.interpreterLocals)
if es_clase:
texto = texto + ".__init__"
texto_a_ejecutar = 'inspect.getsource(' + texto + ")"
codigo = eval(texto_a_ejecutar, self.interpreterLocals)
except TypeError, e:
pass
except Exception, e:
pass
return codigo
def mostrar_consejo(self, linea):
self.ventana.consejo.setText(linea)
def guardar_contenido_con_dialogo(self):
ruta = self.abrir_dialogo_guardar_archivo()
if ruta:
self.guardar_contenido_en_el_archivo(ruta)
self.nombre_de_archivo_sugerido = ruta
#self.mensaje_contenido_guardado()
def _ha_ingresado_solo_espacios(self, linea):
# TODO: Reemplazar por una expresion regular para
# detectar lineas donde solo hay espacios en blanco.
if linea == " ":
return True
def obtener_contenido(self):
texto = self.document().toPlainText()
texto = texto.replace(u'‥ ', '')
texto = texto.replace(u'» ', '')
return unicode(texto)
def marker_si_es_necesario(self):
line = unicode(self.document().lastBlock().text())
if not line.startswith(u'» ') and not line.startswith(u'‥ '):
self.insertPlainText("\n")
self.marker()
self._mover_cursor_al_final()
def print_en_consola_de_texto(self, texto):
#self.stdout_original.write(texto + '\n')
pass
def raw_input(self, mensaje):
text, state = QInputDialog.getText(self, "raw_input", mensaje)
return str(text)
def input(self, expresion):
text, state = QInputDialog.getText(self, "raw_input", expresion)
return eval(str(text))
def help(self, objeto=None):
if objeto:
self.interpreterLocals['pilas'].ver(objeto)
else:
print "Escribe help(objeto) para obtener ayuda sobre ese objeto."