Skip to content

Commit

Permalink
Merge pull request #24 from leon-thomm/builtin-console
Browse files Browse the repository at this point in the history
I just implemented a complete built-in python console interpreter, allowing REPLs and direct access to individual NodeInstances. This way, the user can very efficiently customize nodes and keep track on internal variables. Altough this initially might seem like just another nice feature, it is definitely going to become one of the major ones, the possibilities are endless. More in the upcoming release.
  • Loading branch information
leon-thomm authored Nov 5, 2020
2 parents b4f2e72 + 65444b6 commit 42d5f41
Show file tree
Hide file tree
Showing 8 changed files with 341 additions and 34 deletions.
24 changes: 21 additions & 3 deletions Ryven/Ryven.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import os
import sys

import custom_src.Console.MainConsole as MainConsole
from custom_src.startup_dialog.StartupDialog import StartupDialog
from custom_src.MainWindow import MainWindow
from PySide2.QtWidgets import QApplication
from contextlib import redirect_stdout, redirect_stderr

if __name__ == '__main__':
os.chdir(os.path.dirname(os.path.realpath(__file__)))
Expand All @@ -13,7 +15,23 @@
sw.exec_()

if not sw.editor_startup_configuration == {}:
mw = MainWindow(sw.editor_startup_configuration)
mw.show()

sys.exit(app.exec_())
if MainConsole.main_console_enabled:
# initialize console
MainConsole.init_main_console()
console_stdout_redirect = MainConsole.RedirectOutput(MainConsole.main_console.write)
console_errout_redirect = MainConsole.RedirectOutput(MainConsole.main_console.errorwrite)

with redirect_stdout(console_stdout_redirect), \
redirect_stderr(console_errout_redirect):

# init whole UI
mw = MainWindow(sw.editor_startup_configuration)
mw.show()
sys.exit(app.exec_())

else: # just for some debugging
# init whole UI
mw = MainWindow(sw.editor_startup_configuration)
mw.show()
sys.exit(app.exec_())
228 changes: 228 additions & 0 deletions Ryven/custom_src/Console/MainConsole.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import code
import re
from PySide2.QtWidgets import QWidget, QLineEdit, QGridLayout, QPlainTextEdit, QLabel, QPushButton
from PySide2.QtCore import Signal, QEvent, Qt
from PySide2.QtGui import QTextCharFormat, QBrush, QColor, QFont


class MainConsole(QWidget):
"""Complete console interpreter.
One instance will be created at the end of this file, when being imported in Ryven.py."""

def __init__(
self,
context=locals(), # context for interpreter
history: int = 100, # max lines in history buffer
blockcount: int = 5000 # max lines in output buffer
):

super(MainConsole, self).__init__()

# CREATE UI

self.content_layout = QGridLayout(self)
self.content_layout.setContentsMargins(0, 0, 0, 0)
self.content_layout.setSpacing(0)

# reset scope button
self.reset_scope_button = QPushButton('reset console scope')
self.reset_scope_button.clicked.connect(self.reset_scope_clicked)
self.content_layout.addWidget(self.reset_scope_button, 0, 0, 1, 2)
self.reset_scope_button.hide()

# display for output
self.out_display = ConsoleDisplay(blockcount, self)
self.content_layout.addWidget(self.out_display, 1, 0, 1, 2)

# colors to differentiate input, output and stderr
self.inpfmt = self.out_display.currentCharFormat()
self.inpfmt.setForeground(QBrush(QColor('white')))
self.outfmt = QTextCharFormat(self.inpfmt)
self.outfmt.setForeground(QBrush(QColor('#A9D5EF')))
self.errfmt = QTextCharFormat(self.inpfmt)
self.errfmt.setForeground(QBrush(QColor('#B55730')))

# display input prompt left besides input edit
self.prompt_label = QLabel('> ', self)
self.prompt_label.setFixedWidth(15)
self.content_layout.addWidget(self.prompt_label, 2, 0)

# command line
self.inpedit = LineEdit(max_history=history)
self.inpedit.returned.connect(self.push)
self.content_layout.addWidget(self.inpedit, 2, 1)


self.interp = None
self.reset_interpreter()

self.buffer = []
self.num_added_object_contexts = 0


def setprompt(self, text: str):
self.prompt_label.setText(text)

def reset_scope_clicked(self):
self.reset_interpreter()

def add_obj_context(self, context_obj):
"""adds the new context to the current context by initializing a new interpreter with both"""

old_context = {} if self.interp is None else self.interp.locals
name = 'obj' + (str(self.num_added_object_contexts+1) if self.num_added_object_contexts > 0 else '')
new_context = {name: context_obj}
context = {**old_context, **new_context} # merge dicts
self.interp = code.InteractiveConsole(context)
print('added as ' + name)

self.num_added_object_contexts += 1
self.reset_scope_button.show()

def reset_interpreter(self):
"""Initializes a new plain interpreter"""

context = locals()
self.num_added_object_contexts = 0
self.reset_scope_button.hide()
self.interp = code.InteractiveConsole(context)

def push(self, commands: str) -> None:
"""execute entered command which may span multiple lines when code was pasted"""

if commands == 'clear':
self.out_display.clear()
else:
lines = commands.split('\n') # usually just one entry

# clean and print commands
for line in lines:

# remove '> '-and '. ' prefixes which may remain from copy&paste
if re.match('^[\>\.] ', line):
line = line[2:]

# print input
self.writeoutput(self.prompt_label.text() + line, self.inpfmt)

# prepare for multi-line input
self.setprompt('. ')
self.buffer.append(line)

# merge commands
source = '\n'.join(self.buffer)
more = self.interp.runsource(source, '<console>')

if not more: # no more input required
self.setprompt('> ')
self.buffer = [] # reset buffer

def write(self, line: str) -> None:
"""capture stdout and print to outdisplay"""
if len(line) != 1 or ord(line[0]) != 10:
self.writeoutput(line.rstrip(), self.outfmt)

def errorwrite(self, line: str) -> None:
"""capture stderr and print to outdisplay"""
self.writeoutput(line, self.errfmt)

def writeoutput(self, line: str, fmt: QTextCharFormat = None) -> None:
"""prints to outdisplay"""
if fmt is not None:
self.out_display.setCurrentCharFormat(fmt)
self.out_display.appendPlainText(line.rstrip())


class LineEdit(QLineEdit):
"""Input line edit with a history buffer for recalling previous lines."""

returned = Signal(str)

def __init__(self, max_history: int = 100):
super().__init__()

self.setObjectName('ConsoleInputLineEdit')
self.max_hist = max_history
self.hist_index = 0
self.hist_list = []
self.prompt_pattern = re.compile('^[>\.]')
self.setFont(QFont('source code pro', 11))

def event(self, ev: QEvent) -> bool:
"""
Tab: Insert 4 spaces
Arrow Up/Down: select a line from the history buffer
Newline: Emit returned signal
"""
if ev.type() == QEvent.KeyPress:
if ev.key() == Qt.Key_Tab:
self.insert(' '*4)
return True
elif ev.key() == Qt.Key_Up:
self.recall(self.hist_index - 1)
return True
elif ev.key() == Qt.Key_Down:
self.recall(self.hist_index + 1)
return True
elif ev.key() == Qt.Key_Return:
self.returnkey()
return True

return super().event(ev)

def returnkey(self) -> None:
text = self.text()
self.record(text)
self.returned.emit(text)
self.setText('')

def recall(self, index: int) -> None:
"""select a line from the history list"""

if len(self.hist_list) > 0 and 0 <= index < len(self.hist_list):
self.setText(self.hist_list[index])
self.hist_index = index

def record(self, line: str) -> None:
"""store line in history buffer and update hist_index"""

while len(self.hist_list) >= self.max_hist - 1:
self.hist_list.pop()
self.hist_list.append(line)

if self.hist_index == len(self.hist_list)-1 or line != self.hist_list[self.hist_index]:
self.hist_index = len(self.hist_list)



class ConsoleDisplay(QPlainTextEdit):
def __init__(self, max_block_count, parent=None):
super(ConsoleDisplay, self).__init__(parent)

self.setObjectName('ConsoleDisplay')
self.setMaximumBlockCount(max_block_count)
self.setReadOnly(True)
self.setFont(QFont('Consolas', 8))


class RedirectOutput:
"""Just redirects 'write()'-calls to a specified method."""

def __init__(self, func):
self.func = func

def write(self, line):
self.func(line)


# CREATING ONE MAIN CONSOLE INSTANCE

# note that, for some reason idk, I need to access this variable using MainConsole.main_console. Otherwise all
# references made when it was None will still hold value None...
main_console = None
main_console_enabled = True


def init_main_console():
global main_console
main_console = MainConsole()
14 changes: 9 additions & 5 deletions Ryven/custom_src/MainWindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from PySide2.QtWidgets import QMainWindow, QFileDialog, QShortcut, QAction, QActionGroup, QMenu, QMessageBox

# parent UI
import custom_src.Console.MainConsole as MainConsole
from custom_src.builtin_nodes.Result_Node import Result_Node
from custom_src.builtin_nodes.Result_NodeInstance import Result_NodeInstance
from custom_src.builtin_nodes.Val_Node import Val_Node
Expand Down Expand Up @@ -32,6 +33,9 @@ def __init__(self, config):

self.ui = Ui_MainWindow()
self.ui.setupUi(self)
if MainConsole.main_console is not None:
self.ui.scripts_console_splitter.addWidget(MainConsole.main_console)
self.ui.scripts_console_splitter.setSizes([350, 350])
self.ui.splitter.setSizes([120, 800])
self.setWindowTitle('Ryven')
self.setWindowIcon(QIcon('../resources/pics/program_icon2.png'))
Expand Down Expand Up @@ -89,11 +93,11 @@ def __init__(self, config):
print('finished')

print('''
CONTROLS
placing nodes: right mouse
selecting components: left mouse
panning: middle mouse
saving: ctrl+s
CONTROLS
placing: right mouse
selecting: left mouse
panning: middle mouse
saving: ctrl+s
''')


Expand Down
12 changes: 10 additions & 2 deletions Ryven/custom_src/NodeInstance.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from PySide2.QtCore import Qt, QRectF, QPointF
from PySide2.QtGui import QColor

import custom_src.Console.MainConsole as MainConsole
from custom_src.GlobalAttributes import ViewportUpdateMode
from custom_src.NodeInstanceAction import NodeInstanceAction
from custom_src.NodeInstanceAnimator import NodeInstanceAnimator
Expand Down Expand Up @@ -41,7 +42,8 @@ def __init__(self, params):
# self.node_instance_painter = NodeInstancePainter(self)

self.default_actions = {'remove': {'method': self.action_remove},
'update shape': {'method': self.update_shape}} # for context menus
'update shape': {'method': self.update_shape},
'console ref': {'method': self.set_console_scope}} # for context menus
self.special_actions = {} # only gets written in custom NodeInstance-subclasses
self.personal_logs = []

Expand Down Expand Up @@ -214,7 +216,7 @@ def update(self, input_called=-1, output_called=-1):
try:
self.update_event(input_called)
except Exception as e:
Debugger.debug('EXCEPTION IN', self.parent_node.title, 'NI:', e)
Debugger.debugerr('EXCEPTION IN', self.parent_node.title, 'NI:', e)

def update_event(self, input_called=-1):
"""Gets called when an input received a signal. This is where the magic begins in subclasses."""
Expand Down Expand Up @@ -489,6 +491,12 @@ def unregister_var_receiver(self, name):
# --------------------------------------------------------------------------------------
# UI STUFF ----------------------------------------

def set_console_scope(self):
# extensive_dict = {} # unlike self.__dict__, it also includes methods to call! :)
# for att in dir(self):
# extensive_dict[att] = getattr(self, att)
MainConsole.main_console.add_obj_context(self)

def theme_changed(self, new_theme):
self.title_label.theme_changed(new_theme)
self.update_design()
Expand Down
12 changes: 5 additions & 7 deletions Ryven/custom_src/builtin_nodes/GetVar_NodeInstance.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,17 @@ def __init__(self, params):
def update_event(self, input_called=-1):
if self.input(0) != self.var_name:

vars_handler = self.flow.parent_script.variables_handler

if self.var_name != '': # disconnect old var val update connection
vars_handler.unregister_receiver(self, self.var_name)
self.unregister_var_receiver(self.var_name)

self.var_name = self.input(0)

# create new var update connection
vars_handler.register_receiver(self, self.var_name, M(self.var_val_changed))
self.register_var_receiver(self.var_name, M(self.var_val_changed))

var = vars_handler.get_var(self.input(0))
if var is not None:
self.set_output_val(0, var.val)
val = self.get_var_val(self.input(0))
if val is not None:
self.set_output_val(0, val)
else:
self.set_output_val(0, None)

Expand Down
Loading

0 comments on commit 42d5f41

Please sign in to comment.