Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🎉 Functional flow execution #98

Merged
merged 7 commits into from
Dec 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
865 changes: 865 additions & 0 deletions examples/flow.ipyg

Large diffs are not rendered by default.

318 changes: 194 additions & 124 deletions examples/mnist.ipyg

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion opencodeblocks/blocks/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@ def get_socket_pos(self, socket: OCBSocket) -> Tuple[float]:
y = y_offset
else:
side_lenght = self.height - y_offset - 2 * socket.radius - self.edge_size
y = y_offset + side_lenght * sockets.index(socket) / (len(sockets) - 1)
y = y_offset + side_lenght * \
sockets.index(socket) / (len(sockets) - 1)
return x, y

def update_sockets(self):
Expand Down
119 changes: 105 additions & 14 deletions opencodeblocks/blocks/codeblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@

""" Module for the base OCB Code Block. """

from typing import OrderedDict
from typing import List, OrderedDict
from PyQt5.QtWidgets import QPushButton, QTextEdit

from ansi2html import Ansi2HTMLConverter
from networkx.algorithms.traversal.breadth_first_search import bfs_edges

from opencodeblocks.blocks.block import OCBBlock
from opencodeblocks.graphics.socket import OCBSocket
from opencodeblocks.graphics.pyeditor import PythonEditor
from opencodeblocks.graphics.worker import Worker

conv = Ansi2HTMLConverter()

Expand All @@ -34,6 +34,7 @@ def __init__(self, **kwargs):
Initialize all the child widgets specific to this block type
"""
self.source_editor = PythonEditor(self)
self._source = ""

super().__init__(**kwargs)

Expand All @@ -42,8 +43,9 @@ def __init__(self, **kwargs):
self._min_source_editor_height = 20

self.output_closed = True
self._splitter_size = [0, 0]
self._splitter_size = [1, 1]
self._cached_stdout = ""
self.has_been_run = False

# Add exectution flow sockets
exe_sockets = (
Expand All @@ -56,6 +58,7 @@ def __init__(self, **kwargs):
# Add output pannel
self.output_panel = self.init_output_panel()
self.run_button = self.init_run_button()
self.run_all_button = self.init_run_all_button()

# Add splitter between source_editor and panel
self.splitter.addWidget(self.source_editor)
Expand All @@ -80,20 +83,97 @@ def init_run_button(self):
run_button = QPushButton(">", self.root)
run_button.move(int(self.edge_size), int(self.edge_size / 2))
run_button.setFixedSize(int(3 * self.edge_size), int(3 * self.edge_size))
run_button.clicked.connect(self.run_code)
run_button.clicked.connect(self.run_left)
return run_button

def init_run_all_button(self):
"""Initialize the run all button"""
run_all_button = QPushButton(">>", self.root)
run_all_button.setFixedSize(int(3 * self.edge_size), int(3 * self.edge_size))
run_all_button.clicked.connect(self.run_right)
run_all_button.raise_()

return run_all_button

def run_code(self):
"""Run the code in the block"""
# Erase previous output
self.stdout = ""
# Reset stdout
self._cached_stdout = ""
self.source = self.source_editor.text()
# Create a worker to handle execution
worker = Worker(self.source_editor.kernel, self.source)
worker.signals.stdout.connect(self.handle_stdout)
worker.signals.image.connect(self.handle_image)
self.source_editor.threadpool.start(worker)

# Set button text to ...
self.run_button.setText("...")
self.run_all_button.setText("...")

# Run code by adding to code to queue
code = self.source_editor.text()
self.source = code
kernel = self.source_editor.kernel
kernel.execution_queue.append((self, code))
if kernel.busy is False:
kernel.run_queue()
self.has_been_run = True

def reset_buttons(self):
"""Reset the text of the run buttons"""
self.run_button.setText(">")
self.run_all_button.setText(">>")

def has_input(self) -> bool:
"""Checks whether a block has connected input blocks"""
for input_socket in self.sockets_in:
if len(input_socket.edges) != 0:
return True
return False

def has_output(self) -> bool:
"""Checks whether a block has connected output blocks"""
for output_socket in self.sockets_out:
if len(output_socket.edges) != 0:
return True
return False

def run_left(self, in_right_button=False):
"""
Run all of the block's dependencies and then run the block
"""
# If no dependencies
if not self.has_input():
return self.run_code()

# Create the graph from the scene
graph = self.scene().create_graph()
# BFS through the input graph
edges = bfs_edges(graph, self, reverse=True)
# Run the blocks found except self
blocks_to_run: List["OCBCodeBlock"] = [v for _, v in edges]
for block in blocks_to_run[::-1]:
if not block.has_been_run:
block.run_code()

if in_right_button:
# If run_left was called inside of run_right
# self is not necessarily the block that was clicked
# which means that self does not need to be run
if not self.has_been_run:
self.run_code()
else:
# On the contrary if run_left was called outside of run_right
# self is the block that was clicked
# so self needs to be run
self.run_code()

def run_right(self):
"""Run all of the output blocks and all their dependencies"""
# If no output, run left
if not self.has_output():
return self.run_left(in_right_button=True)

# Same as run_left but instead of running the blocks, we'll use run_left
graph = self.scene().create_graph()
edges = bfs_edges(graph, self)
blocks_to_run: List["OCBCodeBlock"] = [self] + [v for _, v in edges]
for block in blocks_to_run[::-1]:
block.run_left(in_right_button=True)

def update_title(self):
"""Change the geometry of the title widget"""
Expand All @@ -112,19 +192,30 @@ def update_output_panel(self):
self.output_closed = True
self.splitter.setSizes([1, 0])

def update_run_all_button(self):
"""Change the geometry of the run all button"""
self.run_all_button.move(
int(self.width - self.edge_size - self.run_button.width()),
int(self.edge_size / 2),
)

def update_all(self):
"""Update the code block parts"""
super().update_all()
self.update_output_panel()
self.update_run_all_button()
AlexandreSajus marked this conversation as resolved.
Show resolved Hide resolved

@property
def source(self) -> str:
"""Source code"""
return self.source_editor.text()
return self._source

@source.setter
def source(self, value: str):
self.source_editor.setText(value)
if value != self._source:
self.has_been_run = False
self.source_editor.setText(value)
self._source = value

@property
def stdout(self) -> str:
Expand Down
29 changes: 29 additions & 0 deletions opencodeblocks/graphics/kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@
from typing import Tuple
from jupyter_client.manager import start_new_kernel

from opencodeblocks.graphics.worker import Worker


class Kernel():

"""jupyter_client kernel used to execute code and return output"""

def __init__(self):
self.kernel_manager, self.client = start_new_kernel()
self.execution_queue = []
self.busy = False

def message_to_output(self, message: dict) -> Tuple[str, str]:
"""
Expand Down Expand Up @@ -47,6 +51,31 @@ def message_to_output(self, message: dict) -> Tuple[str, str]:
out = ''
return out, message_type

def run_block(self, block, code: str):
"""
Runs code on a separate thread and sends the output to the block
Also calls run_queue when finished

Args:
block: OCBCodeBlock to send the output to
code: String representing a piece of Python code to execute
"""
worker = Worker(self, code)
worker.signals.stdout.connect(block.handle_stdout)
worker.signals.image.connect(block.handle_image)
worker.signals.finished.connect(self.run_queue)
worker.signals.finished_block.connect(block.reset_buttons)
block.source_editor.threadpool.start(worker)

def run_queue(self):
""" Runs the next code in the queue """
self.busy = True
if self.execution_queue == []:
self.busy = False
return None
block, code = self.execution_queue.pop(0)
self.run_block(block, code)

def execute(self, code: str) -> str:
"""
Executes code in the kernel and returns the output of the last message sent by the kernel
Expand Down
1 change: 1 addition & 0 deletions opencodeblocks/graphics/pyeditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,5 @@ def mousePressEvent(self, event: QMouseEvent) -> None:
def focusOutEvent(self, event: QFocusEvent):
""" PythonEditor reaction to PyQt focusOut events. """
self.mode = "NOOP"
self.block.source = self.text()
return super().focusOutEvent(event)
4 changes: 4 additions & 0 deletions opencodeblocks/graphics/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ class WorkerSignals(QObject):
""" Defines the signals available from a running worker thread. """
stdout = pyqtSignal(str)
image = pyqtSignal(str)
finished = pyqtSignal()
finished_block = pyqtSignal()


class Worker(QRunnable):
Expand Down Expand Up @@ -38,6 +40,8 @@ async def run_code(self):
self.signals.stdout.emit(output)
elif output_type == 'image':
self.signals.image.emit(output)
self.signals.finished.emit()
self.signals.finished_block.emit()

def run(self):
""" Execute the run_code method asynchronously. """
Expand Down
21 changes: 18 additions & 3 deletions opencodeblocks/scene/scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
from opencodeblocks.scene.clipboard import SceneClipboard
from opencodeblocks.scene.history import SceneHistory

import networkx as nx


class OCBScene(QGraphicsScene, Serializable):

Expand Down Expand Up @@ -180,6 +182,18 @@ def serialize(self) -> OrderedDict:
('edges', [edge.serialize() for edge in edges]),
])

def create_graph(self) -> nx.DiGraph:
""" Create a networkx graph from the scene. """
edges = []
for item in self.items():
if isinstance(item, OCBEdge):
edges.append(item)
graph = nx.DiGraph()
for edge in edges:
graph.add_edge(edge.source_socket.block,
edge.destination_socket.block)
return graph

def create_block_from_file(
self, filepath: str, x: float = 0, y: float = 0):
""" Create a new block from a .ocbb file """
Expand All @@ -199,13 +213,14 @@ def create_block(self, data: OrderedDict, hashmap: dict = None,
block_files = blocks.__dict__

for block_name in block_files:
block_module = getattr(blocks,block_name)
block_module = getattr(blocks, block_name)
if isinstance(block_module, ModuleType):
if hasattr(block_module, data['block_type']):
block_constructor = getattr(blocks,data['block_type'])
block_constructor = getattr(blocks, data['block_type'])

if block_constructor is None:
raise NotImplementedError(f"{data['block_type']} is not a known block type")
raise NotImplementedError(
f"{data['block_type']} is not a known block type")

block = block_constructor()
block.deserialize(data, hashmap, restore_id)
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ jupyter_client>=7.0.6
ipykernel>=6.5.0
ansi2html>=1.6.0
markdown>=3.3.6
pyqtwebengine>=5.15.5
pyqtwebengine>=5.15.5
networkx >= 2.6.2