Skip to content

Commit

Permalink
🔀 Merge pull request #113 from Bycelium/feature/nested_node
Browse files Browse the repository at this point in the history
🛠️Execution Refactor & 🎉 Nested Nodes
  • Loading branch information
vanyle authored Dec 20, 2021
2 parents 3f28dec + 2817584 commit 9ab9d32
Show file tree
Hide file tree
Showing 25 changed files with 1,081 additions and 614 deletions.
16 changes: 16 additions & 0 deletions blocks/container.ocbb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"title": "Container",
"block_type": "OCBContainerBlock",
"source": "",
"splitter_pos": [88,41],
"width": 618,
"height": 184,
"metadata": {
"title_metadata": {
"color": "white",
"font": "Ubuntu",
"size": 10,
"padding": 4.0
}
}
}
541 changes: 541 additions & 0 deletions examples/linear_classifier.ipyg

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion opencodeblocks/blocks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
""" Module for the OCB Blocks of different types. """

from opencodeblocks.blocks.sliderblock import OCBSliderBlock
from opencodeblocks.blocks.block import OCBBlock
from opencodeblocks.blocks.codeblock import OCBCodeBlock
from opencodeblocks.blocks.markdownblock import OCBMarkdownBlock
from opencodeblocks.blocks.drawingblock import OCBDrawingBlock
from opencodeblocks.blocks.containerblock import OCBContainerBlock
4 changes: 0 additions & 4 deletions opencodeblocks/blocks/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ class OCBBlock(QGraphicsItem, Serializable):
def __init__(
self,
block_type: str = "base",
source: str = "",
position: tuple = (0, 0),
width: int = DEFAULT_DATA["width"],
height: int = DEFAULT_DATA["height"],
Expand All @@ -57,7 +56,6 @@ def __init__(
Args:
block_type: Block type.
source: Block source text.
position: Block position in the scene.
width: Block width.
height: Block height.
Expand All @@ -70,8 +68,6 @@ def __init__(
Serializable.__init__(self)

self.block_type = block_type
self.source = source
self.stdout = ""
self.setPos(QPointF(*position))
self.sockets_in = []
self.sockets_out = []
Expand Down
148 changes: 34 additions & 114 deletions opencodeblocks/blocks/codeblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@

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

from typing import List, OrderedDict
from typing import 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.blocks.executableblock import OCBExecutableBlock
from opencodeblocks.graphics.socket import OCBSocket
from opencodeblocks.graphics.pyeditor import PythonEditor

conv = Ansi2HTMLConverter()


class OCBCodeBlock(OCBBlock):
class OCBCodeBlock(OCBExecutableBlock):

"""
Code Block
Expand All @@ -28,38 +28,32 @@ class OCBCodeBlock(OCBBlock):
"""

DEFAULT_DATA = {
**OCBBlock.DEFAULT_DATA,
"source": "",
}
MANDATORY_FIELDS = OCBBlock.MANDATORY_FIELDS
def __init__(self, source: str = "", **kwargs):

def __init__(self, **kwargs):
"""
Create a new OCBCodeBlock.
Initialize all the child widgets specific to this block type
"""
DEFAULT_DATA = {
**OCBBlock.DEFAULT_DATA,
"source": "",
}
MANDATORY_FIELDS = OCBBlock.MANDATORY_FIELDS

super().__init__(**kwargs)
self.source_editor = PythonEditor(self)

self._source = ""
self._stdout = ""

super().__init__(**kwargs)
self.source = source

self.output_panel_height = self.height / 3
self._min_output_panel_height = 20
self._min_source_editor_height = 20

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

# Add exectution flow sockets
exe_sockets = (
OCBSocket(self, socket_type="input", flow_type="exe"),
OCBSocket(self, socket_type="output", flow_type="exe"),
)
for socket in exe_sockets:
self.add_socket(socket)

# Add output pannel
self.output_panel = self.init_output_panel()
Expand Down Expand Up @@ -89,18 +83,32 @@ 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_left)
run_button.clicked.connect(self.handle_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.clicked.connect(self.handle_run_right)
run_all_button.raise_()

return run_all_button

def handle_run_right(self):
"""Called when the button for "Run All" was pressed"""
if self.is_running:
self._interrupt_execution()
else:
self.run_right()

def handle_run_left(self):
"""Called when the button for "Run Left" was pressed"""
if self.is_running:
self._interrupt_execution()
else:
self.run_left()

def run_code(self):
"""Run the code in the block"""
# Reset stdout
Expand All @@ -110,102 +118,14 @@ def run_code(self):
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
super().run_code() # actually run the code

def reset_buttons(self):
def execution_finished(self):
"""Reset the text of the run buttons"""
super().execution_finished()
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 _interrupt_execution(self):
"""Interrupt an execution, reset the blocks in the queue"""
for block, _ in self.source_editor.kernel.execution_queue:
# Reset the blocks that have not been run
block.reset_buttons()
block.has_been_run = False
# Clear the queue
self.source_editor.kernel.execution_queue = []
# Interrupt the kernel
self.source_editor.kernel.kernel_manager.interrupt_kernel()

def run_left(self, in_right_button=False):
"""
Run all of the block's dependencies and then run the block
"""
# If the user presses left run when running, cancel the execution
if self.run_button.text() == "..." and not in_right_button:
self._interrupt_execution()
return

# 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 the user presses right run when running, cancel the execution
if self.run_all_button.text() == "...":
self._interrupt_execution()
return

# 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 reset_has_been_run(self):
"""Reset has_been_run, is called when the output is an error"""
self.has_been_run = False

def update_title(self):
"""Change the geometry of the title widget"""
self.title_widget.setGeometry(
Expand Down
38 changes: 38 additions & 0 deletions opencodeblocks/blocks/containerblock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""
Exports OCBContainerBlock.
"""

from PyQt5.QtWidgets import QVBoxLayout
from opencodeblocks.blocks.block import OCBBlock


class OCBContainerBlock(OCBBlock):
"""
A block that can contain other blocks.
"""

def __init__(self, **kwargs):
super().__init__(**kwargs)

# Defer import to prevent circular dependency.
# Due to the overall structure of the code, this cannot be removed, as the
# scene should be able to serialize blocks.
# This is not due to bad code design and should not be removed.
from opencodeblocks.graphics.view import (
OCBView,
) # pylint: disable=cyclic-import
from opencodeblocks.scene.scene import OCBScene # pylint: disable=cyclic-import

self.layout = QVBoxLayout(self.root)
self.layout.setContentsMargins(
self.edge_size * 2,
self.title_widget.height() + self.edge_size * 2,
self.edge_size * 2,
self.edge_size * 2,
)

self.child_scene = OCBScene()
self.child_view = OCBView(self.child_scene)
self.layout.addWidget(self.child_view)

self.holder.setWidget(self.root)
Loading

0 comments on commit 9ab9d32

Please sign in to comment.