Source code for eye.widgets.eval_console

# this project is licensed under the WTFPLv2, see COPYING.txt for details

"""Interactive Python evaluator console
"""

import code
import codecs
from importlib import reload
from io import StringIO
import logging
import os
import rlcompleter
import sys

from PyQt5.QtCore import Qt, QStringListModel
from PyQt5.QtWidgets import QVBoxLayout, QLineEdit, QPlainTextEdit, QWidget, QAction, QCompleter

from eye.app import qApp
from eye.qt import Signal, Slot
from eye.utils import exception_logging
from eye.widgets.helpers import WidgetMixin

__all__ = (
	'EvalConsole', 'NAMESPACE', 'register_console_symbol',
)


LOGGER = logging.getLogger(__name__)


NAMESPACE = {}

"""Additional shared namespace between all :any:`EvalConsole` objects.

Example::

	def hello():
		print('Hello world')

	eye.widgets.eval_console.NAMESPACE['hello'] = hello

Then, the `hello()` function can be called in the console.
"""

NAMESPACE["reload"] = reload


class PythonCompleter(QCompleter):
	def __init__(self, *args, **kwargs):
		super().__init__(*args, **kwargs)
		self.setModel(QStringListModel())

	def splitPath(self, path):
		# hack, this function seems called everytime
		# so we can force our custom completion
		completer = rlcompleter.Completer(self.widget().parent().namespace)
		text = self.widget().text()
		text = text[:self.widget().cursorPosition()]
		i = 0
		comps = []
		while True:
			comp = completer.complete(text, i)
			if comp is None:
				break
			comps.append(comp)
			i += 1

		self.model().setStringList(comps)

		return super().splitPath(path)


class HistoryLine(QLineEdit):
	submitted = Signal(str)

	def __init__(self, **kwargs):
		super().__init__(**kwargs)
		self.history = []
		self.history_path = None
		self.idx = None
		self.returnPressed.connect(self.submit)
		self.setCompleter(PythonCompleter())

	@Slot()
	def submit(self):
		text = self.text()

		self._add_history(text)
		self.setText('')
		self.submitted.emit(text)

	def _add_history(self, text):
		self.idx = None
		if not text or (self.history and self.history[-1] == text):
			return
		self.history.append(text)

		if self.history_path:
			with exception_logging(reraise=False, logger=LOGGER):
				with codecs.open(self.history_path, 'a', 'utf-8') as fd:
					print(text, file=fd)

	def set_history_file(self, path):
		if path is not None:
			self.history = []
			if os.path.exists(path):
				with open(path, 'r+') as fd:
					self.history = [line.strip() for line in fd]
		self.history_path = path

	def keyPressEvent(self, ev):
		if ev.key() == Qt.Key_Up:
			if self.idx is None:
				if not self.history:
					return
				self.idx = len(self.history) - 1
			elif self.idx > 0:
				self.idx -= 1
			else:
				return

			self.setText(self.history[self.idx])
		elif ev.key() == Qt.Key_Down:
			if self.idx is None:
				return
			elif self.idx + 1 >= len(self.history):
				self.idx = None
				self.setText('')
				return
			else:
				self.idx += 1

			self.setText(self.history[self.idx])
		else:
			super().keyPressEvent(ev)


class PlainTextEdit(QPlainTextEdit):
	def contextMenuEvent(self, ev):
		menu = self.createStandardContextMenu()
		menu.addSeparator()
		for action in self.actions():
			menu.addAction(action)
		menu.exec_(ev.globalPos())


[docs] class EvalConsole(QWidget, WidgetMixin): """Interactive evaluator console widget Text typed in the console will be executed as Python code, in the context of the EYE app, which allows to do some operations on widgets directly from this console. The `editor` variable is automatically set to the last focused editor widget in the current window. The `window` variable is set to current window. The `eye` module is imported, and so are the submodules already imported by the configuration files. During execution of a line, stdout and stderr are captured and are output to this widget console. Do not execute statemements taking a lot of time as it would freeze the UI. This widget can typically added as a dock widget. """ def __init__(self, **kwargs): super().__init__(**kwargs) self.namespace = {} self.interpreter = code.InteractiveInterpreter(self.namespace) layout = QVBoxLayout() self.setLayout(layout) self.display = PlainTextEdit(self) self.display.setReadOnly(True) clearAction = QAction(self.tr('Clear'), self.display) clearAction.triggered.connect(self.display.clear) self.display.addAction(clearAction) self.line = HistoryLine() self.line.submitted.connect(self.exec_code) self.line.installEventFilter(self) self.line.completer().popup().installEventFilter(self) self.setFocusProxy(self.line) layout.addWidget(self.display) layout.addWidget(self.line) self.setWindowTitle(self.tr('Eval console')) self.add_category('eval_console')
[docs] def import_all_qt(self): self.interpreter.runsource('from eye.helpers.qt_all import *')
[docs] @Slot(str) def exec_code(self, code): # TODO be able to define functions, do ifs, fors self.set_namespace() try: output = '>>> %s\n' % code output += capture_output(self.interpreter.runsource, code) self.display.appendPlainText(output) finally: self.protect_namespace()
[docs] def set_namespace(self): import eye self.namespace['eye'] = eye self.namespace['app'] = qApp() self.namespace['window'] = qApp().last_window self.namespace['editor'] = self.namespace['window'].current_buffer() self.namespace['import_all_qt'] = self.import_all_qt self.namespace.update(NAMESPACE)
[docs] def protect_namespace(self): # avoid retaining references to widgets self.namespace.pop('window', None) self.namespace.pop('editor', None)
[docs] def eventFilter(self, obj, event): if event.type() in (event.FocusIn, event.FocusOut): if self.line.hasFocus(): self.set_namespace() else: self.protect_namespace() return False
def capture_output(cb, *args, **kwargs): sio = StringIO() old = sys.stdout, sys.stderr sys.stdout, sys.stderr = sio, sio try: res = cb(*args, **kwargs) finally: sys.stdout, sys.stderr = old res = sio.getvalue() if isinstance(res, bytes): res = res.decode('utf-8', 'replace') return res
[docs] def register_console_symbol(name=None): """Decorator to register a function on the eval console Example:: @register_console_symbol('foo') def foo(): pass Is equivalent to:: def foo(): pass NAMESPACE['foo'] = foo """ def decorator(cb): cb_name = name if cb_name is None: cb_name = cb.__name__ NAMESPACE[cb_name] = cb return cb return decorator