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