diff --git a/blocks/container.ocbb b/blocks/container.ocbb
new file mode 100644
index 00000000..e543d0b6
--- /dev/null
+++ b/blocks/container.ocbb
@@ -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
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/linear_classifier.ipyg b/examples/linear_classifier.ipyg
new file mode 100644
index 00000000..a02f6f4a
--- /dev/null
+++ b/examples/linear_classifier.ipyg
@@ -0,0 +1,541 @@
+{
+ "id": 2034509196736,
+ "blocks": [
+ {
+ "id": 2034638878464,
+ "title": "",
+ "block_type": "OCBMarkdownBlock",
+ "splitter_pos": [
+ 0,
+ 200
+ ],
+ "position": [
+ -940.0,
+ -467.0
+ ],
+ "width": 677,
+ "height": 253,
+ "metadata": {
+ "title_metadata": {
+ "color": "white",
+ "font": "Ubuntu",
+ "size": 10
+ }
+ },
+ "sockets": [],
+ "text": "# Linear regressions\r\n\r\nThe example showcases how a graph notebook can be used to teach\r\nhow linear regressions work."
+ },
+ {
+ "id": 2034686482320,
+ "title": "Show the data",
+ "block_type": "OCBCodeBlock",
+ "splitter_pos": [
+ 0,
+ 272
+ ],
+ "position": [
+ 53.875000000000085,
+ 212.25
+ ],
+ "width": 439,
+ "height": 325,
+ "metadata": {
+ "title_metadata": {
+ "color": "white",
+ "font": "Ubuntu",
+ "size": 10
+ }
+ },
+ "sockets": [
+ {
+ "id": 2034686483184,
+ "type": "input",
+ "position": [
+ 0.0,
+ 40.0
+ ],
+ "metadata": {
+ "color": "#FF55FFF0",
+ "linecolor": "#FF000000",
+ "linewidth": 1.0,
+ "radius": 10.0
+ }
+ },
+ {
+ "id": 2034686483328,
+ "type": "output",
+ "position": [
+ 439.0,
+ 40.0
+ ],
+ "metadata": {
+ "color": "#FF55FFF0",
+ "linecolor": "#FF000000",
+ "linewidth": 1.0,
+ "radius": 10.0
+ }
+ }
+ ],
+ "source": "import matplotlib.pyplot as plt\r\n\r\num = 10 * m\r\nub = 2 * b\r\nuy = [um*i+ub for i in x]\r\n\r\nplt.plot(x,uy)\r\nplt.scatter(x,y)",
+ "stdout": "
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\n"
+ },
+ {
+ "id": 2034723533728,
+ "title": "Generate some data to plot",
+ "block_type": "OCBCodeBlock",
+ "splitter_pos": [
+ 189,
+ 85
+ ],
+ "position": [
+ -929.7499999999999,
+ -121.31250000000003
+ ],
+ "width": 706,
+ "height": 327,
+ "metadata": {
+ "title_metadata": {
+ "color": "white",
+ "font": "Ubuntu",
+ "size": 10
+ }
+ },
+ "sockets": [
+ {
+ "id": 2034723534592,
+ "type": "input",
+ "position": [
+ 0.0,
+ 40.0
+ ],
+ "metadata": {
+ "color": "#FF55FFF0",
+ "linecolor": "#FF000000",
+ "linewidth": 1.0,
+ "radius": 10.0
+ }
+ },
+ {
+ "id": 2034723534736,
+ "type": "output",
+ "position": [
+ 706.0,
+ 40.0
+ ],
+ "metadata": {
+ "color": "#FF55FFF0",
+ "linecolor": "#FF000000",
+ "linewidth": 1.0,
+ "radius": 10.0
+ }
+ }
+ ],
+ "source": "from random import seed, random\r\n\r\nseed(132)\r\nx = [3 + random() * 10 for i in range(40)]\r\ny = [4.15 * i + random() * 2 for i in x]\r\n\r\nprint(f\"Generated {len(x)} examples\")",
+ "stdout": "Generated 40 examples\n"
+ },
+ {
+ "id": 2034723677808,
+ "title": "Slider",
+ "block_type": "OCBSliderBlock",
+ "splitter_pos": [],
+ "position": [
+ -890.625,
+ 258.50000000000006
+ ],
+ "width": 618,
+ "height": 184,
+ "metadata": {
+ "title_metadata": {
+ "color": "white",
+ "font": "Ubuntu",
+ "size": 10
+ }
+ },
+ "sockets": [
+ {
+ "id": 2034723678672,
+ "type": "input",
+ "position": [
+ 0.0,
+ 42.0
+ ],
+ "metadata": {
+ "color": "#FF55FFF0",
+ "linecolor": "#FF000000",
+ "linewidth": 1.0,
+ "radius": 10.0
+ }
+ },
+ {
+ "id": 2034723678816,
+ "type": "output",
+ "position": [
+ 618.0,
+ 42.0
+ ],
+ "metadata": {
+ "color": "#FF55FFF0",
+ "linecolor": "#FF000000",
+ "linewidth": 1.0,
+ "radius": 10.0
+ }
+ }
+ ],
+ "value": "0.82",
+ "var_name": "b"
+ },
+ {
+ "id": 2034723714816,
+ "title": "Slider",
+ "block_type": "OCBSliderBlock",
+ "splitter_pos": [],
+ "position": [
+ -901.1874999999999,
+ 471.8750000000001
+ ],
+ "width": 618,
+ "height": 184,
+ "metadata": {
+ "title_metadata": {
+ "color": "white",
+ "font": "Ubuntu",
+ "size": 10
+ }
+ },
+ "sockets": [
+ {
+ "id": 2034723715680,
+ "type": "input",
+ "position": [
+ 0.0,
+ 42.0
+ ],
+ "metadata": {
+ "color": "#FF55FFF0",
+ "linecolor": "#FF000000",
+ "linewidth": 1.0,
+ "radius": 10.0
+ }
+ },
+ {
+ "id": 2034723715824,
+ "type": "output",
+ "position": [
+ 618.0,
+ 42.0
+ ],
+ "metadata": {
+ "color": "#FF55FFF0",
+ "linecolor": "#FF000000",
+ "linewidth": 1.0,
+ "radius": 10.0
+ }
+ }
+ ],
+ "value": "0.41",
+ "var_name": "m"
+ },
+ {
+ "id": 2034879162976,
+ "title": "Regression",
+ "block_type": "OCBCodeBlock",
+ "splitter_pos": [
+ 0,
+ 276
+ ],
+ "position": [
+ 782.4375000000001,
+ -676.0000000000001
+ ],
+ "width": 434,
+ "height": 329,
+ "metadata": {
+ "title_metadata": {
+ "color": "white",
+ "font": "Ubuntu",
+ "size": 10
+ }
+ },
+ "sockets": [
+ {
+ "id": 2034879163840,
+ "type": "input",
+ "position": [
+ 0.0,
+ 40.0
+ ],
+ "metadata": {
+ "color": "#FF55FFF0",
+ "linecolor": "#FF000000",
+ "linewidth": 1.0,
+ "radius": 10.0
+ }
+ },
+ {
+ "id": 2034879163984,
+ "type": "output",
+ "position": [
+ 434.0,
+ 40.0
+ ],
+ "metadata": {
+ "color": "#FF55FFF0",
+ "linecolor": "#FF000000",
+ "linewidth": 1.0,
+ "radius": 10.0
+ }
+ }
+ ],
+ "source": "import matplotlib.pyplot as plt\r\nmy = reg.predict([[i] for i in x])\r\nprint(my)\r\nplt.plot(x,my)\r\nplt.scatter(x,y)",
+ "stdout": "
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\n"
+ },
+ {
+ "id": 2034879286288,
+ "title": "Show user input",
+ "block_type": "OCBCodeBlock",
+ "splitter_pos": [
+ 0,
+ 220
+ ],
+ "position": [
+ 611.6875000000001,
+ 251.81250000000006
+ ],
+ "width": 685,
+ "height": 273,
+ "metadata": {
+ "title_metadata": {
+ "color": "white",
+ "font": "Ubuntu",
+ "size": 10
+ }
+ },
+ "sockets": [
+ {
+ "id": 2034879287152,
+ "type": "input",
+ "position": [
+ 0.0,
+ 40.0
+ ],
+ "metadata": {
+ "color": "#FF55FFF0",
+ "linecolor": "#FF000000",
+ "linewidth": 1.0,
+ "radius": 10.0
+ }
+ },
+ {
+ "id": 2034879197248,
+ "type": "output",
+ "position": [
+ 685.0,
+ 40.0
+ ],
+ "metadata": {
+ "color": "#FF55FFF0",
+ "linecolor": "#FF000000",
+ "linewidth": 1.0,
+ "radius": 10.0
+ }
+ }
+ ],
+ "source": "print(\"Your manual regression: \")\r\nprint(f\"y = x * {um} + {ub}\")\r\n\r\nprint(\"Accuracy:\")\r\nu = sum([(y[i] - (x[i]*um+ub)) ** 2 for i in range(len(x))])\r\ny_mean = sum(y) / len(y)\r\nv = sum([(y_i - y_mean) ** 2 for y_i in y])\r\nprint(1 - u/v)\r\n",
+ "stdout": "Your manual regression: \ny = x * 3.9000000000000004 + 1.64\nAccuracy:\n0.9724161281048267\n"
+ },
+ {
+ "id": 2034886210608,
+ "title": "Create a new linear model",
+ "block_type": "OCBCodeBlock",
+ "splitter_pos": [
+ 90,
+ 85
+ ],
+ "position": [
+ -160.3125,
+ -374.50000000000006
+ ],
+ "width": 840,
+ "height": 228,
+ "metadata": {
+ "title_metadata": {
+ "color": "white",
+ "font": "Ubuntu",
+ "size": 10
+ }
+ },
+ "sockets": [
+ {
+ "id": 2034886211472,
+ "type": "input",
+ "position": [
+ 0.0,
+ 40.0
+ ],
+ "metadata": {
+ "color": "#FF55FFF0",
+ "linecolor": "#FF000000",
+ "linewidth": 1.0,
+ "radius": 10.0
+ }
+ },
+ {
+ "id": 2034886211616,
+ "type": "output",
+ "position": [
+ 840.0,
+ 40.0
+ ],
+ "metadata": {
+ "color": "#FF55FFF0",
+ "linecolor": "#FF000000",
+ "linewidth": 1.0,
+ "radius": 10.0
+ }
+ }
+ ],
+ "source": "from sklearn import linear_model\r\nreg = linear_model.LinearRegression()\r\nreg.fit([[i] for i in x],y)",
+ "stdout": "LinearRegression()"
+ },
+ {
+ "id": 2136886539168,
+ "title": "Show user input",
+ "block_type": "OCBCodeBlock",
+ "splitter_pos": [
+ 0,
+ 278
+ ],
+ "position": [
+ 767.1875000000002,
+ -279.9375
+ ],
+ "width": 816,
+ "height": 331,
+ "metadata": {
+ "title_metadata": {
+ "color": "white",
+ "font": "Ubuntu",
+ "size": 10
+ }
+ },
+ "sockets": [
+ {
+ "id": 2136886540752,
+ "type": "input",
+ "position": [
+ 0.0,
+ 40.0
+ ],
+ "metadata": {
+ "color": "#FF55FFF0",
+ "linecolor": "#FF000000",
+ "linewidth": 1.0,
+ "radius": 10.0
+ }
+ },
+ {
+ "id": 2136886540896,
+ "type": "output",
+ "position": [
+ 816.0,
+ 40.0
+ ],
+ "metadata": {
+ "color": "#FF55FFF0",
+ "linecolor": "#FF000000",
+ "linewidth": 1.0,
+ "radius": 10.0
+ }
+ }
+ ],
+ "source": "print(\"Automatic regression: \")\r\nprint(f\"y = x * {reg.coef_[0]} + {reg.predict([[0]])[0]}\")\r\n\r\nprint(\"Accuracy: (closer to 1 = better)\")\r\nprint(reg.score([[i] for i in x],y))",
+ "stdout": "Automatic regression: \ny = x * 4.171128552280276 + 0.8246691322815138\nAccuracy: (closer to 1 = better)\n0.9977264063505547\n"
+ }
+ ],
+ "edges": [
+ {
+ "id": 2034686599952,
+ "path_type": "bezier",
+ "source": {
+ "block": 2034723533728,
+ "socket": 2034723534736
+ },
+ "destination": {
+ "block": 2034686482320,
+ "socket": 2034686483184
+ }
+ },
+ {
+ "id": 2034879160672,
+ "path_type": "bezier",
+ "source": {
+ "block": 2034723714816,
+ "socket": 2034723715824
+ },
+ "destination": {
+ "block": 2034686482320,
+ "socket": 2034686483184
+ }
+ },
+ {
+ "id": 2034879161104,
+ "path_type": "bezier",
+ "source": {
+ "block": 2034723677808,
+ "socket": 2034723678816
+ },
+ "destination": {
+ "block": 2034686482320,
+ "socket": 2034686483184
+ }
+ },
+ {
+ "id": 2034882738640,
+ "path_type": "bezier",
+ "source": {
+ "block": 2034723533728,
+ "socket": 2034723534736
+ },
+ "destination": {
+ "block": 2034886210608,
+ "socket": 2034886211472
+ }
+ },
+ {
+ "id": 2034882739360,
+ "path_type": "bezier",
+ "source": {
+ "block": 2034886210608,
+ "socket": 2034886211616
+ },
+ "destination": {
+ "block": 2034879162976,
+ "socket": 2034879163840
+ }
+ },
+ {
+ "id": 2034884170944,
+ "path_type": "bezier",
+ "source": {
+ "block": 2034686482320,
+ "socket": 2034686483328
+ },
+ "destination": {
+ "block": 2034879286288,
+ "socket": 2034879287152
+ }
+ },
+ {
+ "id": 2136887093136,
+ "path_type": "bezier",
+ "source": {
+ "block": 2034886210608,
+ "socket": 2034886211616
+ },
+ "destination": {
+ "block": 2136886539168,
+ "socket": 2136886540752
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/opencodeblocks/blocks/__init__.py b/opencodeblocks/blocks/__init__.py
index 82fed7bc..3a2eca69 100644
--- a/opencodeblocks/blocks/__init__.py
+++ b/opencodeblocks/blocks/__init__.py
@@ -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
diff --git a/opencodeblocks/blocks/block.py b/opencodeblocks/blocks/block.py
index d5a3563b..9aafddf7 100644
--- a/opencodeblocks/blocks/block.py
+++ b/opencodeblocks/blocks/block.py
@@ -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"],
@@ -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.
@@ -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 = []
diff --git a/opencodeblocks/blocks/codeblock.py b/opencodeblocks/blocks/codeblock.py
index e6681a22..db6ce55a 100644
--- a/opencodeblocks/blocks/codeblock.py
+++ b/opencodeblocks/blocks/codeblock.py
@@ -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
@@ -28,21 +28,25 @@ 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
@@ -50,16 +54,6 @@ def __init__(self, **kwargs):
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()
@@ -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
@@ -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(
diff --git a/opencodeblocks/blocks/containerblock.py b/opencodeblocks/blocks/containerblock.py
new file mode 100644
index 00000000..df00a483
--- /dev/null
+++ b/opencodeblocks/blocks/containerblock.py
@@ -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)
diff --git a/opencodeblocks/blocks/drawingblock.py b/opencodeblocks/blocks/drawingblock.py
index 98794f5f..e4ec7a46 100644
--- a/opencodeblocks/blocks/drawingblock.py
+++ b/opencodeblocks/blocks/drawingblock.py
@@ -4,10 +4,10 @@
import json
from typing import OrderedDict
-from PyQt5.QtCore import Qt
+from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtGui import QColor, QMouseEvent, QPaintEvent, QPainter
from PyQt5.QtWidgets import QPushButton, QWidget
-from opencodeblocks.blocks.block import OCBBlock
+from opencodeblocks.blocks.executableblock import OCBExecutableBlock
eps = 1
@@ -16,6 +16,8 @@
class DrawableWidget(QWidget):
"""A drawable widget is a canvas like widget on which you can doodle"""
+ on_value_changed = pyqtSignal()
+
def __init__(self, parent: QWidget):
"""Create a new Drawable widget"""
super().__init__(parent)
@@ -27,14 +29,14 @@ def __init__(self, parent: QWidget):
for _ in range(self.pixel_width):
self.color_buffer.append([])
for _ in range(self.pixel_height):
- # color hex encoded as AARRGGBB
- self.color_buffer[-1].append(0xFFFFFFFF)
+ # 0 = white, 1 = black
+ self.color_buffer[-1].append(0)
def clearDrawing(self):
"""Clear the drawing"""
for i in range(self.pixel_width):
for j in range(self.pixel_height):
- self.color_buffer[i][j] = 0xFFFFFFFF
+ self.color_buffer[i][j] = 0
def paintEvent(self, evt: QPaintEvent):
"""Draw the content of the widget"""
@@ -50,7 +52,10 @@ def paintEvent(self, evt: QPaintEvent):
h * j,
w + eps,
h + eps,
- QColor.fromRgb(self.color_buffer[i][j]),
+ # hex color encoded as AARRGGBB
+ QColor.fromRgb(
+ 0xFF000000 if self.color_buffer[i][j] else 0xFFFFFFFF
+ ),
)
def mouseMoveEvent(self, evt: QMouseEvent):
@@ -59,8 +64,9 @@ def mouseMoveEvent(self, evt: QMouseEvent):
x = floor(evt.x() / self.width() * self.pixel_width)
y = floor(evt.y() / self.height() * self.pixel_height)
if 0 <= x < self.pixel_width and 0 <= y < self.pixel_height:
- self.color_buffer[x][y] = 0xFF000000
+ self.color_buffer[x][y] = 1
self.repaint()
+ self.on_value_changed.emit()
def mousePressEvent(self, evt: QMouseEvent):
"""Signal that the drawing starts"""
@@ -71,7 +77,8 @@ def mouseReleaseEvent(self, evt: QMouseEvent):
self.mouse_down = False
-class OCBDrawingBlock(OCBBlock):
+class OCBDrawingBlock(OCBExecutableBlock):
+
"""An OCBBlock on which you can draw, to test your CNNs for example"""
def __init__(self, **kwargs):
@@ -79,6 +86,8 @@ def __init__(self, **kwargs):
super().__init__(**kwargs)
self.draw_area = DrawableWidget(self.root)
+ self.draw_area.on_value_changed.connect(self.valueChanged)
+ self.var_name = "drawing"
self.splitter.addWidget(self.draw_area) # QGraphicsView
self.run_button = QPushButton("Clear", self.root)
@@ -106,6 +115,22 @@ def serialize(self):
return base_dict
+ def valueChanged(self):
+ """Called when the content of the drawing block changes."""
+ # Make sure that the slider is initialized before trying to run it.
+ if self.scene() is not None:
+ self.run_right()
+
+ @property
+ def source(self):
+ """The "source code" of the drawingblock i.e an assignement to the drawing buffer"""
+ python_code = f"{self.var_name} = {repr(self.draw_area.color_buffer)}"
+ return python_code
+
+ @source.setter
+ def source(self, value: str):
+ raise RuntimeError("The source of a drawingblock is read-only.")
+
def deserialize(
self, data: OrderedDict, hashmap: dict = None, restore_id: bool = True
):
diff --git a/opencodeblocks/blocks/executableblock.py b/opencodeblocks/blocks/executableblock.py
new file mode 100644
index 00000000..5f228142
--- /dev/null
+++ b/opencodeblocks/blocks/executableblock.py
@@ -0,0 +1,157 @@
+""" Module for the executable block class """
+
+from typing import List, OrderedDict
+from abc import abstractmethod
+
+from networkx.algorithms.traversal.breadth_first_search import bfs_edges
+
+from opencodeblocks.blocks.block import OCBBlock
+from opencodeblocks.graphics.socket import OCBSocket
+
+
+class OCBExecutableBlock(OCBBlock):
+
+ """
+ Executable Block
+
+ This block type is not meant to be instanciated !
+
+ It's an abstract class that represents blocks that can be executed like:
+ - OCBCodeBlock
+ - OCBSlider
+
+ """
+
+ def __init__(self, **kwargs):
+ """
+ Create a new executable block.
+ Do not call this method except when inheriting from this class.
+ """
+ super().__init__(**kwargs)
+
+ self.has_been_run = False
+ self.is_running = False
+
+ # Add execution 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)
+
+ if type(self) == OCBExecutableBlock:
+ raise RuntimeError("OCBExecutableBlock should not be instanciated directly")
+
+ 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_code(self):
+ """Run the code in the block"""
+
+ # Queue the code to execute
+ code = self.source
+ kernel = self.scene().kernel
+ kernel.execution_queue.append((self, code))
+
+ self.is_running = True
+
+ if kernel.busy is False:
+ kernel.run_queue()
+ self.has_been_run = True
+
+ def execution_finished(self):
+ """
+ Method called when the execution of the block is finished.
+ Implement the behavior you want here.
+ """
+ self.is_running = False
+
+ def _interrupt_execution(self):
+ """Interrupt an execution, reset the blocks in the queue"""
+ kernel = self.scene().kernel
+ for block, _ in kernel.execution_queue:
+ # Reset the blocks that have not been run
+ block.execution_finished()
+ block.has_been_run = False
+ # Clear the queue
+ kernel.execution_queue = []
+ # Interrupt the kernel
+ kernel.kernel_manager.interrupt_kernel()
+
+ def run_left(self):
+ """
+ Run all of the block's dependencies and then run the block
+ """
+
+ if self.has_input():
+ # 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["OCBExecutableBlock"] = [v for _, v in edges]
+ for block in blocks_to_run[::-1]:
+ if not block.has_been_run:
+ block.run_code()
+
+ if self.is_running:
+ return
+ 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():
+ self.run_left()
+ return
+
+ # 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["OCBExecutableBlock"] = [self] + [v for _, v in edges]
+ for block in blocks_to_run[::-1]:
+ block.run_left()
+
+ def reset_has_been_run(self):
+ """Called when the output is an error"""
+ self.has_been_run = False
+
+ @property
+ @abstractmethod
+ def source(self) -> str:
+ """Source code"""
+ raise NotImplementedError("source(self) should be overriden")
+
+ @source.setter
+ @abstractmethod
+ def source(self, value: str):
+ raise NotImplementedError("source(self) should be overriden")
+
+ def handle_stdout(self, value: str):
+ """Handle the stdout signal"""
+
+ def handle_image(self, image: str):
+ """Handle the image signal"""
+
+ def serialize(self):
+ """Return a serialized version of this block"""
+ return super().serialize()
+
+ def deserialize(
+ self, data: OrderedDict, hashmap: dict = None, restore_id: bool = True
+ ):
+ """Restore a codeblock from it's serialized state"""
+ super().deserialize(data, hashmap, restore_id)
diff --git a/opencodeblocks/blocks/sliderblock.py b/opencodeblocks/blocks/sliderblock.py
index 123034a6..0fa580b2 100644
--- a/opencodeblocks/blocks/sliderblock.py
+++ b/opencodeblocks/blocks/sliderblock.py
@@ -7,10 +7,10 @@
from typing import OrderedDict
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QHBoxLayout, QLabel, QLineEdit, QSlider, QVBoxLayout
-from opencodeblocks.blocks.block import OCBBlock
+from opencodeblocks.blocks.executableblock import OCBExecutableBlock
-class OCBSliderBlock(OCBBlock):
+class OCBSliderBlock(OCBExecutableBlock):
"""
Features a slider ranging from 0 to 1 and an area to choose what value to assign the slider to.
"""
@@ -21,9 +21,8 @@ def __init__(self, **kwargs):
self.layout = QVBoxLayout(self.root)
self.slider = QSlider(Qt.Horizontal)
- self.slider.valueChanged.connect(self.valueChanged)
- self.variable_layout = QHBoxLayout(self.root)
+ self.variable_layout = QHBoxLayout()
self.variable_text = QLineEdit("slider_value")
self.variable_value = QLabel(f"{self.slider.value()/100}")
@@ -41,16 +40,26 @@ def __init__(self, **kwargs):
self.layout.addWidget(self.slider)
self.layout.addLayout(self.variable_layout)
+ self.slider.valueChanged.connect(self.valueChanged)
+
self.holder.setWidget(self.root)
def valueChanged(self):
"""This is called when the value of the slider changes"""
- python_code = f"{self.var_name} = {self.value}"
self.variable_value.setText(f"{self.value}")
+ # Make sure that the slider is initialized before trying to run it.
+ if self.scene() is not None:
+ self.run_right()
+
+ @property
+ def source(self):
+ """The "source code" of the slider i.e an assignement to the value of the slider"""
+ python_code = f"{self.var_name} = {self.value}"
+ return python_code
- # The code execution part will be added when the execution flow is merged.
- # We print for now
- print(python_code)
+ @source.setter
+ def source(self, value: str):
+ raise RuntimeError("The source of a sliderblock is read-only.")
@property
def value(self):
diff --git a/opencodeblocks/graphics/kernel.py b/opencodeblocks/graphics/kernel.py
index 3114e0a8..c2f3b446 100644
--- a/opencodeblocks/graphics/kernel.py
+++ b/opencodeblocks/graphics/kernel.py
@@ -67,9 +67,9 @@ def run_block(self, block, code: str):
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)
+ worker.signals.finished.connect(block.execution_finished)
worker.signals.error.connect(block.reset_has_been_run)
- block.source_editor.threadpool.start(worker)
+ block.scene().threadpool.start(worker)
def run_queue(self):
"""Runs the next code in the queue"""
diff --git a/opencodeblocks/graphics/pyeditor.py b/opencodeblocks/graphics/pyeditor.py
index 3f9c33d7..19b02c82 100644
--- a/opencodeblocks/graphics/pyeditor.py
+++ b/opencodeblocks/graphics/pyeditor.py
@@ -4,7 +4,7 @@
""" Module for OCB in block python editor. """
from typing import TYPE_CHECKING, List
-from PyQt5.QtCore import QThreadPool, Qt
+from PyQt5.QtCore import Qt
from PyQt5.QtGui import (
QFocusEvent,
QFont,
@@ -17,10 +17,6 @@
from opencodeblocks.graphics.theme_manager import theme_manager
from opencodeblocks.blocks.block import OCBBlock
-from opencodeblocks.graphics.kernel import Kernel
-
-kernel = Kernel()
-threadpool = QThreadPool()
if TYPE_CHECKING:
from opencodeblocks.graphics.view import OCBView
@@ -42,8 +38,6 @@ def __init__(self, block: OCBBlock):
super().__init__(None)
self._mode = "NOOP"
self.block = block
- self.kernel = kernel
- self.threadpool = threadpool
self.update_theme()
theme_manager().themeChanged.connect(self.update_theme)
diff --git a/opencodeblocks/graphics/view.py b/opencodeblocks/graphics/view.py
index 30a927ac..152f192c 100644
--- a/opencodeblocks/graphics/view.py
+++ b/opencodeblocks/graphics/view.py
@@ -16,7 +16,8 @@
from opencodeblocks.scene import OCBScene
from opencodeblocks.graphics.socket import OCBSocket
from opencodeblocks.graphics.edge import OCBEdge
-from opencodeblocks.blocks import OCBBlock, OCBCodeBlock
+from opencodeblocks.blocks.block import OCBBlock
+from opencodeblocks.blocks.codeblock import OCBCodeBlock
EPS: float = 1e-10 # To check if blocks are of size 0
@@ -315,6 +316,12 @@ def retreiveBlockTypes(self) -> List[Tuple[str]]:
def contextMenuEvent(self, event: QContextMenuEvent):
"""Displays the context menu when inside a view"""
+ super().contextMenuEvent(event)
+ # If somebody has already accepted the event, don't handle it.
+ if event.isAccepted():
+ return
+ event.setAccepted(True)
+
menu = QMenu(self)
actionPool = []
for filepath, block_name in self.retreiveBlockTypes():
diff --git a/opencodeblocks/graphics/widget.py b/opencodeblocks/graphics/widget.py
index a3910319..d5025355 100644
--- a/opencodeblocks/graphics/widget.py
+++ b/opencodeblocks/graphics/widget.py
@@ -62,6 +62,7 @@ def save(self):
self.scene.save(self.savepath)
def saveAsJupyter(self):
+ """Save the current graph notebook as a regular python notebook"""
self.scene.save_to_ipynb(self.savepath)
def load(self, filepath: str):
diff --git a/opencodeblocks/graphics/worker.py b/opencodeblocks/graphics/worker.py
index 774aa3c4..ea339952 100644
--- a/opencodeblocks/graphics/worker.py
+++ b/opencodeblocks/graphics/worker.py
@@ -44,8 +44,8 @@ async def run_code(self):
self.signals.image.emit(output)
elif output_type == "error":
self.signals.error.emit()
+ self.signals.stdout.emit(output)
self.signals.finished.emit()
- self.signals.finished_block.emit()
def run(self):
"""Execute the run_code method asynchronously."""
diff --git a/opencodeblocks/scene/from_ipynb_conversion.py b/opencodeblocks/scene/from_ipynb_conversion.py
index 7c2ba094..3165d690 100644
--- a/opencodeblocks/scene/from_ipynb_conversion.py
+++ b/opencodeblocks/scene/from_ipynb_conversion.py
@@ -9,10 +9,14 @@
from opencodeblocks.graphics.pyeditor import POINT_SIZE
-def ipynb_to_ipyg(data: OrderedDict) -> OrderedDict:
- """Convert ipynb data (ipynb file, as ordered dict) into ipyg data (ipyg, as ordered dict)"""
+def ipynb_to_ipyg(data: OrderedDict, use_theme_font: bool = True) -> OrderedDict:
+ """
+ Convert ipynb data (ipynb file, as ordered dict) into ipyg data (ipyg, as ordered dict)
+ - use_theme_font: should the height of the blocks be computed based on the current
+ font selected.
+ """
- blocks_data: List[OrderedDict] = get_blocks_data(data)
+ blocks_data: List[OrderedDict] = get_blocks_data(data, use_theme_font)
edges_data: List[OrderedDict] = get_edges_data(blocks_data)
return {
@@ -21,7 +25,9 @@ def ipynb_to_ipyg(data: OrderedDict) -> OrderedDict:
}
-def get_blocks_data(data: OrderedDict) -> List[OrderedDict]:
+def get_blocks_data(
+ data: OrderedDict, use_theme_font: bool = True
+) -> List[OrderedDict]:
"""
Get the blocks corresponding to a ipynb file,
Returns them in the ipyg ordered dict format
@@ -31,12 +37,14 @@ def get_blocks_data(data: OrderedDict) -> List[OrderedDict]:
return []
# Get the font metrics to determine the size fo the blocks
- font = QFont()
- font.setFamily(theme_manager().recommended_font_family)
- font.setFixedPitch(True)
- font.setPointSize(POINT_SIZE)
- fontmetrics = QFontMetrics(font)
-
+ fontmetrics = None
+ if use_theme_font:
+ font = QFont()
+ font.setFamily(theme_manager().recommended_font_family)
+ font.setFixedPitch(True)
+ font.setPointSize(POINT_SIZE)
+ fontmetrics = QFontMetrics(font)
+
blocks_data: List[OrderedDict] = []
next_block_x_pos: float = 0
@@ -50,15 +58,27 @@ def get_blocks_data(data: OrderedDict) -> List[OrderedDict]:
block_type: str = cell["cell_type"]
text: str = cell["source"]
+
+ boundingWidth = 10
+ if use_theme_font:
+ boundingWidth = fontmetrics.boundingRect(line).width()
text_width: float = (
- max(fontmetrics.boundingRect(line).width() for line in text)
+ max(boundingWidth for line in text)
if len(text) > 0
else 0
)
block_width: float = max(text_width + MARGIN_X, BLOCK_MIN_WIDTH)
+
+ lineSpacing = 2
+ lineWidth = 10
+
+ if use_theme_font:
+ lineSpacing = fontmetrics.lineSpacing()
+ lineWidth = fontmetrics.lineWidth()
+
text_height: float = len(text) * (
- fontmetrics.lineSpacing() + fontmetrics.lineWidth()
+ lineSpacing + lineWidth
)
block_height: float = text_height + MARGIN_Y
diff --git a/opencodeblocks/scene/scene.py b/opencodeblocks/scene/scene.py
index ba1693b6..a1a7bf9c 100644
--- a/opencodeblocks/scene/scene.py
+++ b/opencodeblocks/scene/scene.py
@@ -5,23 +5,25 @@
import math
import json
+from os import path
from types import FunctionType, ModuleType
from typing import List, OrderedDict, Union
-from PyQt5.QtCore import QLine, QRectF
+from PyQt5.QtCore import QLine, QRectF, QThreadPool
from PyQt5.QtGui import QColor, QPainter, QPen
from PyQt5.QtWidgets import QGraphicsScene
-from opencodeblocks import blocks
-
from opencodeblocks.core.serializable import Serializable
from opencodeblocks.blocks.block import OCBBlock
from opencodeblocks.graphics.edge import OCBEdge
from opencodeblocks.scene.clipboard import SceneClipboard
from opencodeblocks.scene.history import SceneHistory
+from opencodeblocks.graphics.kernel import Kernel
from opencodeblocks.scene.from_ipynb_conversion import ipynb_to_ipyg
from opencodeblocks.scene.to_ipynb_conversion import ipyg_to_ipynb
+from opencodeblocks import blocks
+
import networkx as nx
@@ -59,6 +61,9 @@ def __init__(
self.history = SceneHistory(self)
self.clipboard = SceneClipboard(self)
+ self.kernel = Kernel()
+ self.threadpool = QThreadPool()
+
@property
def has_been_modified(self):
"""True if the scene has been modified, False otherwise."""
@@ -176,8 +181,15 @@ def load(self, filepath: str):
self.history.checkpoint("Loaded scene")
self.has_been_modified = False
+ # Add filepath to kernel path
+ dir_path = repr(path.abspath(path.dirname(filepath)))
+ setup_path_code = f'__import__("os").chdir({dir_path})'
+ self.kernel.execute(setup_path_code)
+
def load_from_json(self, filepath: str) -> OrderedDict:
- """Load the json data into an ordered dict
+ """
+ Load the json data into an ordered dict
+
Args:
filepath: Path to the file to load.
"""
diff --git a/tests/assets/data.txt b/tests/assets/data.txt
new file mode 100644
index 00000000..0bce9e3a
--- /dev/null
+++ b/tests/assets/data.txt
@@ -0,0 +1,7 @@
+1
+2
+3
+4
+5
+6
+7
\ No newline at end of file
diff --git a/tests/assets/example_graph1.ipyg b/tests/assets/example_graph1.ipyg
index 7158ae33..c96688ed 100644
--- a/tests/assets/example_graph1.ipyg
+++ b/tests/assets/example_graph1.ipyg
@@ -2,227 +2,33 @@
"id": 2205665405400,
"blocks": [
{
- "id": 2443477874008,
- "title": "Model Train",
+ "id": 1523300599264,
+ "title": "test1",
"block_type": "OCBCodeBlock",
- "source": "print(\"training \")\r\nmodel.fit(x=x_train,y=y_train, epochs=10)\r\n\r\n",
- "stdout": "",
- "image": "",
- "position": [
- 2202.0742187499986,
- -346.82031249999983
- ],
- "width": 1644.8125,
- "height": 481.4375,
- "metadata": {
- "title_metadata": {
- "color": "white",
- "font": "Ubuntu",
- "size": 10,
- "padding": 4.0
- }
- },
- "sockets": [
- {
- "id": 2443477875016,
- "type": "input",
- "position": [
- 0.0,
- 42.0
- ],
- "metadata": {
- "color": "#e02c2c",
- "linecolor": "#FF000000",
- "linewidth": 1.0,
- "radius": 6.0
- }
- },
- {
- "id": 2443477875160,
- "type": "output",
- "position": [
- 1644.8125,
- 42.0
- ],
- "metadata": {
- "color": "#35bc31",
- "linecolor": "#FF000000",
- "linewidth": 1.0,
- "radius": 6.0
- }
- }
- ]
- },
- {
- "id": 2443477924600,
- "title": "Keras Model Predict",
- "block_type": "OCBCodeBlock",
- "source": "prediction = model.predict(x_test[9].reshape(1, 28, 28, 1))",
- "stdout": "",
- "image": "",
- "position": [
- 4207.046874999999,
- -244.57812499999991
- ],
- "width": 1239.6875,
- "height": 305.9374999999999,
- "metadata": {
- "title_metadata": {
- "color": "white",
- "font": "Ubuntu",
- "size": 10,
- "padding": 4.0
- }
- },
- "sockets": [
- {
- "id": 2443477925608,
- "type": "input",
- "position": [
- 0.0,
- 42.0
- ],
- "metadata": {
- "color": "#FF55FFF0",
- "linecolor": "#FF000000",
- "linewidth": 1.0,
- "radius": 6.0
- }
- },
- {
- "id": 2443477925752,
- "type": "output",
- "position": [
- 1239.6875,
- 42.0
- ],
- "metadata": {
- "color": "#FF55FFF0",
- "linecolor": "#FF000000",
- "linewidth": 1.0,
- "radius": 6.0
- }
- }
- ]
- },
- {
- "id": 2443477997032,
- "title": "Keras Model eval",
- "block_type": "OCBCodeBlock",
- "source": "model.evaluate(x_test, y_test)\r\n",
- "stdout": "",
- "image": "",
- "position": [
- 4204.085937499997,
- -707.0546874999997
- ],
- "width": 1628.375,
- "height": 209.875,
- "metadata": {
- "title_metadata": {
- "color": "white",
- "font": "Ubuntu",
- "size": 10,
- "padding": 4.0
- }
- },
- "sockets": [
- {
- "id": 2443477997896,
- "type": "input",
- "position": [
- 0.0,
- 42.0
- ],
- "metadata": {
- "color": "#FF55FFF0",
- "linecolor": "#FF000000",
- "linewidth": 1.0,
- "radius": 6.0
- }
- },
- {
- "id": 2443477998184,
- "type": "output",
- "position": [
- 1628.375,
- 42.0
- ],
- "metadata": {
- "color": "#FF55FFF0",
- "linecolor": "#FF000000",
- "linewidth": 1.0,
- "radius": 6.0
- }
- }
- ]
- },
- {
- "id": 2443478874872,
- "title": "Load MNIST Dataset",
- "block_type": "OCBCodeBlock",
- "source": "print(\"Hello, world\")\r\nfrom tensorflow.keras.datasets import mnist\r\n(x_train, y_train), (x_test, y_test) = mnist.load_data()\r\n",
- "stdout": "",
- "image": "",
- "position": [
- -535.75,
- -687.0625
+ "splitter_pos": [
+ 292,
+ 0
],
- "width": 739.5,
- "height": 343.5,
- "metadata": {
- "title_metadata": {
- "color": "white",
- "font": "Ubuntu",
- "size": 10,
- "padding": 4.0
- }
- },
- "sockets": [
- {
- "id": 2443478910728,
- "type": "output",
- "position": [
- 739.5,
- 42.0
- ],
- "metadata": {
- "color": "#FF55FFF0",
- "linecolor": "#FF000000",
- "linewidth": 1.0,
- "radius": 6.0
- }
- }
- ]
- },
- {
- "id": 2443478982728,
- "title": "Normalize Image Dataset",
- "block_type": "OCBCodeBlock",
- "source": "x_train = x_train.astype('float32') / 255.0\r\nx_test = x_test.astype('float32') / 255.0\r\n\r\n\r\nx_train = x_train.reshape(x_train.shape[0], 28, 28, 1)\r\nx_test = x_test.reshape(x_test.shape[0], 28, 28, 1)\r\n\r\nprint('train:', x_train.shape, '|test:', x_test.shape)",
- "stdout": "",
- "image": "",
"position": [
- 281.2500000000002,
- -149.74999999999977
+ 1192.0,
+ 292.79999999999995
],
- "width": 705.7499999999998,
- "height": 357.25,
+ "width": 707,
+ "height": 351,
"metadata": {
"title_metadata": {
"color": "white",
"font": "Ubuntu",
- "size": 10,
- "padding": 4.0
+ "size": 10
}
},
"sockets": [
{
- "id": 2443478983592,
+ "id": 1523350963536,
"type": "input",
"position": [
0.0,
- 42.0
+ 45.0
],
"metadata": {
"color": "#FF55FFF0",
@@ -232,11 +38,11 @@
}
},
{
- "id": 2443478983880,
+ "id": 1523350963680,
"type": "output",
"position": [
- 705.7499999999998,
- 42.0
+ 707.0,
+ 45.0
],
"metadata": {
"color": "#FF55FFF0",
@@ -245,171 +51,10 @@
"radius": 6.0
}
}
- ]
- },
- {
- "id": 2443479017656,
- "title": "Build Keras CNN",
- "block_type": "OCBCodeBlock",
- "source": "import tensorflow as tf\r\nfrom tensorflow.keras.layers import Dense, Flatten, Conv2D, MaxPooling2D, Dropout\r\nfrom tensorflow.keras.models import Sequential\r\n\r\nmodel = Sequential()\r\nmodel.add(Conv2D(28, kernel_size=(3,3), input_shape=x_train.shape[1:]))\r\nmodel.add(MaxPooling2D(pool_size=(2, 2)))\r\nmodel.add(Flatten())\r\nmodel.add(Dense(128, activation=tf.nn.relu))\r\nmodel.add(Dropout(0.2))\r\nmodel.add(Dense(10,activation=tf.nn.softmax))\r\nprint(\"..\")\r\nmodel.compile(optimizer='adam', \r\n loss='sparse_categorical_crossentropy', \r\n metrics=['accuracy'])\r\n",
- "stdout": "",
- "image": "",
- "position": [
- 1316.25,
- -517.6249999999998
],
- "width": 680.0,
- "height": 468.75,
- "metadata": {
- "title_metadata": {
- "color": "white",
- "font": "Ubuntu",
- "size": 10,
- "padding": 4.0
- }
- },
- "sockets": [
- {
- "id": 2443479018520,
- "type": "input",
- "position": [
- 0.0,
- 42.0
- ],
- "metadata": {
- "color": "#FF55FFF0",
- "linecolor": "#FF000000",
- "linewidth": 1.0,
- "radius": 6.0
- }
- },
- {
- "id": 2443479018808,
- "type": "output",
- "position": [
- 680.0,
- 42.0
- ],
- "metadata": {
- "color": "#FF55FFF0",
- "linecolor": "#FF000000",
- "linewidth": 1.0,
- "radius": 6.0
- }
- }
- ]
- },
- {
- "id": 2828158533848,
- "title": "Plot Image Dataset Example",
- "block_type": "OCBCodeBlock",
- "source": "import matplotlib.pyplot as plt\r\nimport numpy as np\r\n\r\n# Display an example from the dataset\r\nrd_index = np.random.randint(len(x_train))\r\nplt.imshow(x_train[rd_index], cmap='gray')\r\nplt.title('Class '+ str(y_train[0]))\r\n",
- "stdout": "",
- "image": "",
- "position": [
- 433.375,
- -1221.75
- ],
- "width": 778.9375,
- "height": 763.25,
- "metadata": {
- "title_metadata": {
- "color": "white",
- "font": "Ubuntu",
- "size": 10,
- "padding": 4.0
- }
- },
- "sockets": [
- {
- "id": 2828158535432,
- "type": "input",
- "position": [
- 0.0,
- 42.0
- ],
- "metadata": {
- "color": "#FF55FFF0",
- "linecolor": "#FF000000",
- "linewidth": 1.0,
- "radius": 6.0
- }
- }
- ]
+ "source": "content = open(\"data.txt\").read()\r\nprint(content)",
+ "stdout": ""
}
],
- "edges": [
- {
- "id": 1643571233840,
- "path_type": "bezier",
- "source": {
- "block": 2443479017656,
- "socket": 2443479018808
- },
- "destination": {
- "block": 2443477874008,
- "socket": 2443477875016
- }
- },
- {
- "id": 2006783605056,
- "path_type": "bezier",
- "source": {
- "block": 2443478874872,
- "socket": 2443478910728
- },
- "destination": {
- "block": 2828158533848,
- "socket": 2828158535432
- }
- },
- {
- "id": 2006783606064,
- "path_type": "bezier",
- "source": {
- "block": 2443477874008,
- "socket": 2443477875160
- },
- "destination": {
- "block": 2443477924600,
- "socket": 2443477925608
- }
- },
- {
- "id": 2111730223424,
- "path_type": "bezier",
- "source": {
- "block": 2443478982728,
- "socket": 2443478983880
- },
- "destination": {
- "block": 2443479017656,
- "socket": 2443479018520
- }
- },
- {
- "id": 2111730224144,
- "path_type": "bezier",
- "source": {
- "block": 2443477874008,
- "socket": 2443477875160
- },
- "destination": {
- "block": 2443477997032,
- "socket": 2443477997896
- }
- },
- {
- "id": 2111730844864,
- "path_type": "bezier",
- "source": {
- "block": 2443478874872,
- "socket": 2443478910728
- },
- "destination": {
- "block": 2443478982728,
- "socket": 2443478983592
- }
- }
- ]
+ "edges": []
}
\ No newline at end of file
diff --git a/tests/assets/flow_test.ipyg b/tests/assets/flow_test.ipyg
index 414cfd1c..0a20333b 100644
--- a/tests/assets/flow_test.ipyg
+++ b/tests/assets/flow_test.ipyg
@@ -749,7 +749,7 @@
}
},
"sockets": [],
- "text": "**Test flow tests the general behavior of flow execution:**\r\n\r\n\tExecuting 6 left should output 6\r\n\r\n\tExecuting 6 right should output 21 in block 8\r\n"
+ "text": "**Test flow tests the general behavior of flow execution:**\r\n\r\n\tExecuting 6 left should output 4\r\n\r\n\tExecuting 6 right should output 21 in block 8\r\n"
}
],
"edges": [
diff --git a/tests/integration/blocks/test_block.py b/tests/integration/blocks/test_block.py
index 02e6253e..93d42bce 100644
--- a/tests/integration/blocks/test_block.py
+++ b/tests/integration/blocks/test_block.py
@@ -11,21 +11,16 @@
from PyQt5.QtCore import QPointF
-from opencodeblocks.blocks.codeblock import OCBBlock
-from opencodeblocks.graphics.window import OCBWindow
-from opencodeblocks.graphics.widget import OCBWidget
+from opencodeblocks.blocks.block import OCBBlock
-from tests.integration.utils import apply_function_inapp, CheckingQueue
+from tests.integration.utils import apply_function_inapp, CheckingQueue, start_app
class TestBlocks:
@pytest.fixture(autouse=True)
def setup(self):
"""Setup reused variables."""
- self.window = OCBWindow()
- self.ocb_widget = OCBWidget()
- self.subwindow = self.window.mdiArea.addSubWindow(self.ocb_widget)
- self.subwindow.show()
+ start_app(self)
self.block = OCBBlock(title="Testing block")
def test_create_blocks(self, qtbot: QtBot):
@@ -78,3 +73,6 @@ def testing_drag(msgQueue: CheckingQueue):
msgQueue.stop()
apply_function_inapp(self.window, testing_drag)
+
+ def test_finish(self):
+ self.window.close()
diff --git a/tests/integration/blocks/test_codeblock.py b/tests/integration/blocks/test_codeblock.py
index db64f844..a6f79a7d 100644
--- a/tests/integration/blocks/test_codeblock.py
+++ b/tests/integration/blocks/test_codeblock.py
@@ -5,29 +5,25 @@
Integration tests for the OCBCodeBlocks.
"""
+import time
+import os
import pyautogui
import pytest
-from pytestqt.qtbot import QtBot
from PyQt5.QtCore import QPointF
from opencodeblocks.blocks.codeblock import OCBCodeBlock
-from opencodeblocks.graphics.window import OCBWindow
-from opencodeblocks.graphics.widget import OCBWidget
-from tests.integration.utils import apply_function_inapp, CheckingQueue
+from tests.integration.utils import apply_function_inapp, CheckingQueue, start_app
class TestCodeBlocks:
@pytest.fixture(autouse=True)
def setup(self):
"""Setup reused variables."""
- self.window = OCBWindow()
- self.ocb_widget = OCBWidget()
- self.subwindow = self.window.mdiArea.addSubWindow(self.ocb_widget)
- self.subwindow.show()
+ start_app(self)
- def test_run_python(self, qtbot: QtBot):
+ def test_run_python(self):
"""run source code when run button is pressed."""
# Add a block with the source to the window
@@ -55,13 +51,52 @@ def testing_run(msgQueue: CheckingQueue):
pyautogui.mouseDown(button="left")
pyautogui.mouseUp(button="left")
- # qtbot.mouseMove(test_block.run_button)
- # qtbot.mousePress(test_block.run_button,
- # Qt.MouseButton.LeftButton, delay=1)
- # qtbot.mouseRelease(test_block.run_button, Qt.MouseButton.LeftButton)
+ time.sleep(0.5)
- # When the execution becomes non-blocking for the UI, a refactor will be needed here.
msgQueue.check_equal(test_block.stdout.strip(), expected_result)
msgQueue.stop()
apply_function_inapp(self.window, testing_run)
+
+ def test_run_block_with_path(self):
+ """runs blocks with the correct working directory for the kernel"""
+ file_example_path = "./tests/assets/example_graph1.ipyg"
+ asset_path = "./tests/assets/data.txt"
+ self.ocb_widget.scene.load(os.path.abspath(file_example_path))
+
+ def testing_path(msgQueue: CheckingQueue):
+ block_of_test: OCBCodeBlock = None
+ for item in self.ocb_widget.scene.items():
+ if isinstance(item, OCBCodeBlock) and item.title == "test1":
+ block_of_test = item
+ break
+ msgQueue.check_equal(
+ block_of_test is not None,
+ True,
+ "example_graph1 contains a block titled test1",
+ )
+
+ def run_block():
+ block_of_test.run_code()
+
+ msgQueue.run_lambda(run_block)
+ time.sleep(0.1) # wait for the lambda to complete.
+ while block_of_test.is_running:
+ time.sleep(0.1) # wait for the execution to finish.
+
+ time.sleep(0.1)
+
+ file_content = open(asset_path).read()
+
+ msgQueue.check_equal(
+ block_of_test.stdout.strip(),
+ file_content,
+ "The asset file is read properly",
+ )
+
+ msgQueue.stop()
+
+ apply_function_inapp(self.window, testing_path)
+
+ def test_finish(self):
+ self.window.close()
diff --git a/tests/integration/blocks/test_flow.py b/tests/integration/blocks/test_flow.py
index 3e298b2d..2f041aa3 100644
--- a/tests/integration/blocks/test_flow.py
+++ b/tests/integration/blocks/test_flow.py
@@ -9,85 +9,74 @@
import time
from opencodeblocks.blocks.codeblock import OCBCodeBlock
-from opencodeblocks.graphics.window import OCBWindow
-from opencodeblocks.graphics.widget import OCBWidget
-from tests.integration.utils import apply_function_inapp, CheckingQueue
+from tests.integration.utils import apply_function_inapp, CheckingQueue, start_app
-class TestExecutionFlow:
- """Execution flow"""
-
+class TestCodeBlocks:
@pytest.fixture(autouse=True)
def setup(self):
"""Setup reused variables."""
- self.window = OCBWindow()
- self.ocb_widget = OCBWidget()
- self.subwindow = self.window.mdiArea.addSubWindow(self.ocb_widget)
- self.subwindow.show()
+ start_app(self)
self.ocb_widget.scene.load("tests/assets/flow_test.ipyg")
self.titles = [
"Test flow 5",
"Test flow 4",
- "Test flow 8",
"Test no connection 1",
+ "Test input only 2",
+ "Test output only 1",
]
- self.blocks_to_run = [None] * len(self.titles)
+ self.blocks_to_run = [None] * 5
for item in self.ocb_widget.scene.items():
if isinstance(item, OCBCodeBlock):
if item.title in self.titles:
self.blocks_to_run[self.titles.index(item.title)] = item
- def test_flow_left(self):
- """run block and previous blocks when pressing left run."""
+ def test_duplicated_run(self):
+ """Don't run a block twice when the execution flows"""
+ for b in self.blocks_to_run:
+ b.stdout = ""
- def testing_run(msgQueue: CheckingQueue):
+ def testing_no_duplicates(msgQueue: CheckingQueue):
- block_to_run: OCBCodeBlock = self.blocks_to_run[
- self.titles.index("Test flow 5")
- ]
- block_to_not_run: OCBCodeBlock = self.blocks_to_run[
- self.titles.index("Test flow 4")
- ]
+ block_to_run: OCBCodeBlock = self.blocks_to_run[0]
def run_block():
- block_to_run.run_left()
+ block_to_run.run_right()
- # Run the execution in a separate thread
- # to give time for the outputs to show before checking them
msgQueue.run_lambda(run_block)
- time.sleep(0.5)
+ time.sleep(0.1)
+ while block_to_run.is_running:
+ time.sleep(0.1) # wait for the execution to finish.
+ # 6 and not 6\n6
msgQueue.check_equal(block_to_run.stdout.strip(), "6")
- msgQueue.check_equal(block_to_not_run.stdout.strip(), "")
msgQueue.stop()
- apply_function_inapp(self.window, testing_run)
+ apply_function_inapp(self.window, testing_no_duplicates)
- def test_flow_right(self):
- """run block and next blocks when pressing right run."""
+ def test_flow_left(self):
+ """Correct flow when pressing left run"""
+
+ for b in self.blocks_to_run:
+ b.stdout = ""
def testing_run(msgQueue: CheckingQueue):
- block_to_run: OCBCodeBlock = self.blocks_to_run[
- self.titles.index("Test flow 5")
- ]
- block_output: OCBCodeBlock = self.blocks_to_run[
- self.titles.index("Test flow 8")
- ]
- block_to_not_run: OCBCodeBlock = self.blocks_to_run[
- self.titles.index("Test flow 4")
- ]
+ block_to_run: OCBCodeBlock = self.blocks_to_run[0]
+ block_to_not_run: OCBCodeBlock = self.blocks_to_run[1]
def run_block():
- block_to_run.run_right()
+ block_to_run.run_left()
msgQueue.run_lambda(run_block)
- time.sleep(0.5)
+ time.sleep(0.1)
+ while block_to_run.is_running:
+ time.sleep(0.1) # wait for the execution to finish.
- msgQueue.check_equal(block_output.stdout.strip(), "21")
+ msgQueue.check_equal(block_to_run.stdout.strip(), "6")
msgQueue.check_equal(block_to_not_run.stdout.strip(), "")
msgQueue.stop()
@@ -105,8 +94,13 @@ def testing_run(msgQueue: CheckingQueue):
def run_block():
block_to_run.run_left()
+ print("About to run !")
+
msgQueue.run_lambda(run_block)
- time.sleep(0.5)
+ time.sleep(0.1)
+ while block_to_run.is_running:
+ print("wait ...")
+ time.sleep(0.1)
msgQueue.check_equal(block_to_run.stdout.strip(), "1")
msgQueue.stop()
@@ -126,9 +120,14 @@ def run_block():
block_to_run.run_right()
msgQueue.run_lambda(run_block)
- time.sleep(0.5)
+ time.sleep(0.1)
+ while block_to_run.is_running:
+ time.sleep(0.1)
# Just check that it doesn't crash
msgQueue.stop()
apply_function_inapp(self.window, testing_run)
+
+ def test_finish(self):
+ self.window.close()
diff --git a/tests/integration/test_window.py b/tests/integration/test_window.py
index 2544b81f..06a084bd 100644
--- a/tests/integration/test_window.py
+++ b/tests/integration/test_window.py
@@ -18,14 +18,14 @@ def setup(self, mocker: MockerFixture):
"""Setup reused variables."""
self.window = OCBWindow()
- def test_window_close(self, qtbot):
- """closes"""
- self.window.close()
-
- def test_open_file(self):
+ def test_open_file(self, qtbot):
"""loads files"""
wnd = OCBWindow()
file_example_path = "./tests/assets/example_graph1.ipyg"
subwnd = wnd.createNewMdiChild(os.path.abspath(file_example_path))
subwnd.show()
wnd.close()
+
+ def test_window_close(self, qtbot):
+ """closes"""
+ self.window.close()
diff --git a/tests/integration/utils.py b/tests/integration/utils.py
index 41d1ea44..53a5ab1c 100644
--- a/tests/integration/utils.py
+++ b/tests/integration/utils.py
@@ -5,14 +5,18 @@
Utilities functions for integration testing.
"""
-import os
-import asyncio
from typing import Callable
+import os
+import asyncio
import threading
+import time
from queue import Queue
+
from qtpy.QtWidgets import QApplication
import pytest_check as check
+import warnings
+from opencodeblocks.graphics.widget import OCBWidget
from opencodeblocks.graphics.window import OCBWindow
@@ -32,6 +36,37 @@ def stop(self):
self.put([STOP_MSG])
+class ExceptionForwardingThread(threading.Thread):
+ """A Thread class that forwards the exceptions to the calling thread"""
+
+ def __init__(self, *args, **kwargs):
+ """Create an exception forwarding thread"""
+ super().__init__(*args, **kwargs)
+ self.e = None
+
+ def run(self):
+ """Code ran in another thread"""
+ try:
+ super().run()
+ except Exception as e:
+ self.e = e
+
+ def join(self):
+ """Used to sync the thread with the caller"""
+ super().join()
+ print("except: ", self.e)
+ if self.e != None:
+ raise self.e
+
+
+def start_app(obj):
+ """Create a new app for testing"""
+ obj.window = OCBWindow()
+ obj.ocb_widget = OCBWidget()
+ obj.subwindow = obj.window.mdiArea.addSubWindow(obj.ocb_widget)
+ obj.subwindow.show()
+
+
def apply_function_inapp(window: OCBWindow, run_func: Callable):
if os.name == "nt": # If on windows
@@ -39,11 +74,14 @@ def apply_function_inapp(window: OCBWindow, run_func: Callable):
QApplication.processEvents()
msgQueue = CheckingQueue()
- t = threading.Thread(target=run_func, args=(msgQueue,))
+ t = ExceptionForwardingThread(target=run_func, args=(msgQueue,))
t.start()
stop = False
+ deadCounter = 0
+
while not stop:
+ time.sleep(1 / 30) # 30 fps
QApplication.processEvents()
if not msgQueue.empty():
msg = msgQueue.get()
@@ -53,5 +91,13 @@ def apply_function_inapp(window: OCBWindow, run_func: Callable):
stop = True
elif msg[0] == RUN_MSG:
msg[1](*msg[2], **msg[3])
+
+ if not t.is_alive() and not stop:
+ deadCounter += 1
+ if deadCounter >= 3:
+ # Test failed, close was not called
+ warnings.warn(
+ "Warning: you need to call CheckingQueue.stop() at the end of your test !"
+ )
+ break
t.join()
- window.close()
diff --git a/tests/unit/scene/test_ipynb_conversion.py b/tests/unit/scene/test_ipynb_conversion.py
index d73f357d..88d1a4cf 100644
--- a/tests/unit/scene/test_ipynb_conversion.py
+++ b/tests/unit/scene/test_ipynb_conversion.py
@@ -15,7 +15,7 @@ class TestIpynbConversion:
def test_empty_data(self, mocker: MockerFixture):
"""should return empty ipyg graph for empty data."""
- check.equal(ipynb_to_ipyg({}), {"blocks": [], "edges": []})
+ check.equal(ipynb_to_ipyg({}, False), {"blocks": [], "edges": []})
def test_empty_notebook_data(self, mocker: MockerFixture):
"""should return expected graph for a real empty notebook data."""
@@ -51,15 +51,16 @@ def test_is_title(self, mocker: MockerFixture):
def real_notebook_conversion_is_coherent(file_path: str):
"""Checks that the conversion of the ipynb notebook gives a coherent result.
-
+
Args:
file_path: the path to a .ipynb file
"""
ipynb_data = load_json(file_path)
- ipyg_data = ipynb_to_ipyg(ipynb_data)
+ ipyg_data = ipynb_to_ipyg(ipynb_data, False)
check_conversion_coherence(ipynb_data, ipyg_data)
-def check_conversion_coherence(ipynb_data: OrderedDict, ipyg_data:OrderedDict):
+
+def check_conversion_coherence(ipynb_data: OrderedDict, ipyg_data: OrderedDict):
"""Checks that the ipyg data is coherent with the ipynb data.
The conversion from ipynb to ipyg should return