diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d1379f8c..d8e8f557 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,9 +1,9 @@ -# OpenCodeBlocks - Contributing guide +# PyFlow - Contributing guide -[![Pytest badge](https://github.com/MathisFederico/OpenCodeBlocks/actions/workflows/python-tests.yml/badge.svg?branch=master)](https://github.com/MathisFederico/OpenCodeBlocks/actions/workflows/python-tests.yml) [![Pylint badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FMathisFederico%2F00ce73155619a4544884ca6d251954b3%2Fraw%2Fopencodeblocks_pylint_badge.json)](https://github.com/MathisFederico/OpenCodeBlocks/actions/workflows/python-pylint.yml) [![Unit coverage badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FMathisFederico%2F00ce73155619a4544884ca6d251954b3%2Fraw%2Fopencodeblocks_unit_coverage_badge.json)](https://github.com/MathisFederico/OpenCodeBlocks/actions/workflows/python-coverage.yml) [![Integration coverage badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FMathisFederico%2F00ce73155619a4544884ca6d251954b3%2Fraw%2Fopencodeblocks_integration_coverage_badge.json)](https://github.com/MathisFederico/OpenCodeBlocks/actions/workflows/python-coverage.yml) +[![Pytest badge](https://github.com/Bycelium/OpenCodeBlocks/actions/workflows/python-tests.yml/badge.svg?branch=master)](https://github.com/MathisFederico/OpenCodeBlocks/actions/workflows/python-tests.yml) [![Pylint badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FMathisFederico%2F00ce73155619a4544884ca6d251954b3%2Fraw%2Fopencodeblocks_pylint_badge.json)](https://github.com/MathisFederico/OpenCodeBlocks/actions/workflows/python-pylint.yml) [![Unit coverage badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FMathisFederico%2F00ce73155619a4544884ca6d251954b3%2Fraw%2Fopencodeblocks_unit_coverage_badge.json)](https://github.com/MathisFederico/OpenCodeBlocks/actions/workflows/python-coverage.yml) [![Integration coverage badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FMathisFederico%2F00ce73155619a4544884ca6d251954b3%2Fraw%2Fopencodeblocks_integration_coverage_badge.json)](https://github.com/MathisFederico/OpenCodeBlocks/actions/workflows/python-coverage.yml) -Whenever you encounter a :beetle: **bug** or have :tada: **feature request**, -report this via [GitHub issues](https://github.com/MathisFederico/OpenCodeBlocks/issues). +Whenever you encounter a :beetle: **bug** or have :tada: **feature request**, +report this via [GitHub issues](https://github.com/Bycelium/PyFlow/issues). We are happy to receive contributions in the form of **pull requests** via GitHub. @@ -11,10 +11,10 @@ Feel free to fork the repository, implement your changes and create a merge requ ## Getting started -First, make sure you have `python` installed. You will need to install all the `requirements` and the `requirements-dev` using the following commands: +First, make sure you have `python` installed. You will need to install all the `requirements` and the `requirements-dev` using the following commands: -* `pip install -r requirements.txt` -* `pip install -r requirements-dev.txt` +* `pip install -r requirements.txt` +* `pip install -r requirements-dev.txt` You can run the program with `python main.py` @@ -24,10 +24,10 @@ Before doing your **pull request**, check using `pylint` and `pytest` that there pylint .\opencodeblocks\ ``` -Some `pylint` issues can be fixed automatically using `autopep8`, with the following command: +Some `pylint` issues can be fixed automatically using `black`, with the following command: ```bash -autopep8 --in-place --recursive --aggressive opencodeblocks +black . ``` ```bash @@ -36,12 +36,13 @@ pytest --cov=opencodeblocks --cov-report=html tests/unit We want to keep the *Pylint* score above *9.0*. +The comments and docstrings should preferably follow [these](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) guidelines. + ## Git Commit Messages Commits should start with a Capital letter and should be written in present tense (e.g. ``:tada: Add cool new feature`` instead of ``:tada: Added cool new feature``). You should also start your commit message with one or two applicable emoji. This does not only look great but also makes you rethink what to add to a commit. Make many but small commits! - Emoji | Description -----------------|------------- :tada: `:tada:` | When you add a cool new feature @@ -71,4 +72,4 @@ This means, given a version number MAJOR.MINOR.PATCH, we will increment the: 1. MAJOR version when we make incompatible API changes, 2. MINOR version when we add functionality in a backwards compatible manner, and -3. PATCH version when we make backwards compatible bug fixes. \ No newline at end of file +3. PATCH version when we make backwards compatible bug fixes. diff --git a/README.md b/README.md index 61301f2a..222e8397 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# OpenCodeBlocks +# PyFlow [![Pytest badge](https://github.com/MathisFederico/OpenCodeBlocks/actions/workflows/python-tests.yml/badge.svg?branch=master)](https://github.com/MathisFederico/OpenCodeBlocks/actions/workflows/python-tests.yml) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/ddd03302fd7c4849b452959753bc0939)](https://www.codacy.com/gh/MathisFederico/OpenCodeBlocks/dashboard?utm_source=github.com&utm_medium=referral&utm_content=MathisFederico/OpenCodeBlocks&utm_campaign=Badge_Grade) @@ -9,7 +9,7 @@ [![Licence - GPLv3](https://img.shields.io/github/license/MathisFederico/Crafting?style=plastic)](https://www.gnu.org/licenses/) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) -OpenCodeBlocks is an open-source tool for modular visual programing in python ! +PyFlow is an open-source tool for modular visual programing in python ! Although for now the tool is in Beta and features are coming in bit by bit, stay tuned for the first release soon ! 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/__init__.py b/opencodeblocks/__init__.py index 1de73c48..8b299e47 100644 --- a/opencodeblocks/__init__.py +++ b/opencodeblocks/__init__.py @@ -4,6 +4,6 @@ """ OpenCodeBlocks: An open-source tool for modular visual programing in python """ -__appname__ = 'OpenCodeBlocks' -__author__ = 'Mathïs Fédérico' -__version__ = '0.0.1' +__appname__ = "OpenCodeBlocks" +__author__ = "Mathïs Fédérico" +__version__ = "0.0.1" 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 29763b57..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 = [] @@ -162,8 +158,7 @@ def get_socket_pos(self, socket: OCBSocket) -> Tuple[float]: y = y_offset else: side_lenght = self.height - y_offset - 2 * socket.radius - self.edge_size - y = y_offset + side_lenght * \ - sockets.index(socket) / (len(sockets) - 1) + y = y_offset + side_lenght * sockets.index(socket) / (len(sockets) - 1) return x, y def update_sockets(self): diff --git a/opencodeblocks/blocks/codeblock.py b/opencodeblocks/blocks/codeblock.py index f2dad9a0..229f5681 100644 --- a/opencodeblocks/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -3,20 +3,29 @@ """ Module for the base OCB Code Block. """ -from typing import List, OrderedDict -from PyQt5.QtWidgets import QPushButton, QTextEdit +from typing import OrderedDict, Optional +from PyQt5.QtWidgets import ( + QApplication, + QPushButton, + QTextEdit, + QWidget, + QStyleOptionGraphicsItem, +) +from PyQt5.QtCore import QTimer, Qt +from PyQt5.QtGui import QPen, QColor, QPainter, QPainterPath 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 +37,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 @@ -52,14 +65,16 @@ def __init__(self, **kwargs): self._splitter_size = [1, 1] self._cached_stdout = "" self.has_been_run = False + self.blocks_to_run = [] - # 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) + self._pen_outline = QPen(QColor("#7F000000")) + self._pen_outline_running = QPen(QColor("#FF0000")) + self._pen_outline_transmitting = QPen(QColor("#00ff00")) + self._pen_outlines = [ + self._pen_outline, + self._pen_outline_running, + self._pen_outline_transmitting, + ] # Add output pannel self.output_panel = self.init_output_panel() @@ -88,23 +103,36 @@ def init_run_button(self): """Initialize the run button""" 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.setFixedSize(int(3 * self.edge_size), int(3 * self.edge_size)) + 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.setFixedSize(int(3 * self.edge_size), int(3 * self.edge_size)) + 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.run_color != 0: + self._interrupt_execution() + else: + self.run_right() + + def handle_run_left(self): + """Called when the button for "Run Left" was pressed""" + if self.run_color != 0: + self._interrupt_execution() + else: + self.run_left() + def run_code(self): """Run the code in the block""" + # Reset stdout self._cached_stdout = "" @@ -112,102 +140,7 @@ 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 - - def reset_buttons(self): - """Reset the text of the run buttons""" - self.run_button.setText(">") - self.run_all_button.setText(">>") - - def has_input(self) -> bool: - """Checks whether a block has connected input blocks""" - for input_socket in self.sockets_in: - if len(input_socket.edges) != 0: - return True - return False - - def has_output(self) -> bool: - """Checks whether a block has connected output blocks""" - for output_socket in self.sockets_out: - if len(output_socket.edges) != 0: - return True - return False - - def _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 + super().run_code() # actually run the code def update_title(self): """Change the geometry of the title widget""" @@ -239,6 +172,35 @@ def update_all(self): self.update_output_panel() self.update_run_all_button() + def paint( + self, + painter: QPainter, + option: QStyleOptionGraphicsItem, + widget: Optional[QWidget] = None, + ): + """Paint the code block""" + path_content = QPainterPath() + path_content.setFillRule(Qt.FillRule.WindingFill) + path_content.addRoundedRect( + 0, 0, self.width, self.height, self.edge_size, self.edge_size + ) + painter.setPen(Qt.PenStyle.NoPen) + painter.setBrush(self._brush_background) + painter.drawPath(path_content.simplified()) + + # outline + path_outline = QPainterPath() + path_outline.addRoundedRect( + 0, 0, self.width, self.height, self.edge_size, self.edge_size + ) + painter.setPen( + self._pen_outline_selected + if self.isSelected() + else self._pen_outlines[self.run_color] + ) + painter.setBrush(Qt.BrushStyle.NoBrush) + painter.drawPath(path_outline.simplified()) + @property def source(self) -> str: """Source code""" @@ -251,6 +213,17 @@ def source(self, value: str): self.source_editor.setText(value) self._source = value + @property + def run_color(self) -> int: + """Run color""" + return self._run_color + + @run_color.setter + def run_color(self, value: int): + self._run_color = value + # Update to force repaint + self.update() + @property def stdout(self) -> str: """Access the content of the output panel of the block""" @@ -314,6 +287,7 @@ def handle_image(self, image: str): self.stdout = "" + image def serialize(self): + """Serialize the code block""" base_dict = super().serialize() base_dict["source"] = self.source base_dict["stdout"] = self.stdout 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 f45cfc62..e4ec7a46 100644 --- a/opencodeblocks/blocks/drawingblock.py +++ b/opencodeblocks/blocks/drawingblock.py @@ -4,20 +4,22 @@ 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 class DrawableWidget(QWidget): - """ A drawable widget is a canvas like widget on which you can doodle """ + """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 """ + """Create a new Drawable widget""" super().__init__(parent) self.setAttribute(Qt.WA_PaintOnScreen) self.pixel_width = 24 @@ -27,17 +29,17 @@ 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 """ + """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 """ + """Draw the content of the widget""" painter = QPainter(self) for i in range(self.pixel_width): @@ -50,48 +52,56 @@ def paintEvent(self, evt: QPaintEvent): h * j, w + eps, h + eps, + # hex color encoded as AARRGGBB QColor.fromRgb( - self.color_buffer[i][j])) + 0xFF000000 if self.color_buffer[i][j] else 0xFFFFFFFF + ), + ) def mouseMoveEvent(self, evt: QMouseEvent): - """ Change the drawing when dragging the mouse around""" + """Change the drawing when dragging the mouse around""" if self.mouse_down: 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 """ + """Signal that the drawing starts""" self.mouse_down = True def mouseReleaseEvent(self, evt: QMouseEvent): - """ Signal that the drawing stops """ + """Signal that the drawing stops""" self.mouse_down = False -class OCBDrawingBlock(OCBBlock): - """ An OCBBlock on which you can draw, to test your CNNs for example""" +class OCBDrawingBlock(OCBExecutableBlock): + + """An OCBBlock on which you can draw, to test your CNNs for example""" def __init__(self, **kwargs): - """ Create a new OCBBlock""" + """Create a new OCBBlock""" 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) - self.run_button.move(int(self.edge_size * 2), - int(self.title_widget.height() + self.edge_size * 2)) - self.run_button.setFixedSize( - int(8 * self.edge_size), int(3 * self.edge_size)) + self.run_button.move( + int(self.edge_size * 2), + int(self.title_widget.height() + self.edge_size * 2), + ) + self.run_button.setFixedSize(int(8 * self.edge_size), int(3 * self.edge_size)) self.run_button.clicked.connect(self.draw_area.clearDrawing) self.holder.setWidget(self.root) @property def drawing(self): - """ A json-encoded representation of the drawing """ + """A json-encoded representation of the drawing""" return json.dumps(self.draw_area.color_buffer) @drawing.setter @@ -99,16 +109,33 @@ def drawing(self, value: str): self.draw_area.color_buffer = json.loads(value) def serialize(self): - """ Return a serialized version of this widget """ + """Return a serialized version of this widget""" base_dict = super().serialize() base_dict["drawing"] = self.drawing return base_dict - def deserialize(self, data: OrderedDict, - hashmap: dict = None, restore_id: bool = True): - """ Restore a markdown block from it's serialized state """ - for dataname in ['drawing']: + 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 + ): + """Restore a markdown block from it's serialized state""" + for dataname in ["drawing"]: if dataname in data: setattr(self, dataname, data[dataname]) diff --git a/opencodeblocks/blocks/executableblock.py b/opencodeblocks/blocks/executableblock.py new file mode 100644 index 00000000..a6adb77e --- /dev/null +++ b/opencodeblocks/blocks/executableblock.py @@ -0,0 +1,334 @@ +""" Module for the executable block class """ + +from typing import OrderedDict +from abc import abstractmethod +from PyQt5.QtCore import QTimer +from PyQt5.QtWidgets import QApplication + +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 + + # 0 for normal, 1 for running, 2 for transmitting + self.run_color = 0 + + # Each element is a list of blocks/edges to be animated + # Running will paint each element one after the other + self.transmitting_queue = [] + # Controls the duration of the visual flow animation + self.transmitting_duration = 500 + + # 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)) + + if kernel.busy is False: + kernel.run_queue() + self.has_been_run = True + + def execution_finished(self): + """Reset the text of the run buttons""" + self.run_color = 0 + self.run_button.setText(">") + self.run_all_button.setText(">>") + self.blocks_to_run = [] + + def _interrupt_execution(self): + """Interrupt an execution, reset the blocks in the queue""" + for block, _ in self.scene().kernel.execution_queue: + # Reset the blocks that have not been run + block.reset_has_been_run() + block.execution_finished() + # Clear kernel execution queue + self.scene().kernel.execution_queue = [] + # Interrupt the kernel + self.scene().kernel.kernel_manager.interrupt_kernel() + # Clear local execution queue + self.blocks_to_run = [] + + def transmitting_animation_in(self): + """ + Animate the visual flow + Set color to transmitting and set a timer before switching to normal + """ + for elem in self.transmitting_queue[0]: + # Set color to transmitting + elem.run_color = 2 + QApplication.processEvents() + QTimer.singleShot(self.transmitting_delay, self.transmitting_animation_out) + + def transmitting_animation_out(self): + """ + Animate the visual flow + After the timer, set color to normal and move on with the queue + """ + for elem in self.transmitting_queue[0]: + # Reset color only if the block will not be run + if hasattr(elem, "has_been_run"): + if elem.has_been_run is True: + elem.run_color = 0 + else: + elem.run_color = 0 + + QApplication.processEvents() + self.transmitting_queue.pop(0) + if len(self.transmitting_queue) != 0: + # If the queue is not empty, move forward in the animation + self.transmitting_animation_in() + else: + # Else, run the blocks in the self.blocks_to_run + self.run_blocks() + + def custom_bfs(self, start_node, reverse=False): + """ + Graph traversal in BFS to find the blocks that are connected to the start_node + + Args: + start_node (Block): The block to start the traversal from + reverse (bool): If True, traverse in the direction of outputs + + Returns: + list: Blocks to run in topological order (reversed) + list: each element is a list of blocks/edges to animate in order + """ + # Blocks to run in topological order + blocks_to_run = [] + # List of lists of blocks/edges to animate in order + to_transmit = [[start_node]] + + to_visit = [start_node] + while len(to_visit) != 0: + # Remove duplicates + to_visit = list(set(to_visit)) + + # Gather connected edges + edges_to_visit = [] + for block in to_visit: + blocks_to_run.append(block) + if not reverse: + for input_socket in block.sockets_in: + for edge in input_socket.edges: + edges_to_visit.append(edge) + else: + for output_socket in block.sockets_out: + for edge in output_socket.edges: + edges_to_visit.append(edge) + to_transmit.append(edges_to_visit) + + # Gather connected blocks + to_visit = [] + for edge in edges_to_visit: + if not reverse: + to_visit.append(edge.source_socket.block) + else: + to_visit.append(edge.destination_socket.block) + to_transmit.append(to_visit) + + # Remove start node + blocks_to_run.pop(0) + + return blocks_to_run, to_transmit + + def right_traversal(self): + """ + Custom graph traversal utility + Returns blocks/edges that will potentially be run by run_right + from closest to farthest from self + + Returns: + list: each element is a list of blocks/edges to animate in order + """ + # Result + to_transmit = [[self]] + + # To check if a block has been visited + visited = [] + # We need to visit the inputs of these blocks + to_visit_input = [self] + # We need to visit the outputs of these blocks + to_visit_output = [self] + + # Next stage to put in to_transmit + next_edges = [] + next_blocks = [] + + while len(to_visit_input) != 0 or len(to_visit_output) != 0: + for block in to_visit_input.copy(): + # Check input edges and blocks + for input_socket in block.sockets_in: + for edge in input_socket.edges: + if edge not in visited: + next_edges.append(edge) + visited.append(edge) + input_block = edge.source_socket.block + to_visit_input.append(input_block) + if input_block not in visited: + next_blocks.append(input_block) + visited.append(input_block) + to_visit_input.remove(block) + for block in to_visit_output.copy(): + # Check output edges and blocks + for output_socket in block.sockets_out: + for edge in output_socket.edges: + if edge not in visited: + next_edges.append(edge) + visited.append(edge) + output_block = edge.destination_socket.block + to_visit_input.append(output_block) + to_visit_output.append(output_block) + if output_block not in visited: + next_blocks.append(output_block) + visited.append(output_block) + to_visit_output.remove(block) + + # Add the next stage to to_transmit + to_transmit.append(next_edges) + to_transmit.append(next_blocks) + + # Reset next stage + next_edges = [] + next_blocks = [] + + return to_transmit + + def run_blocks(self): + """Run a list of blocks""" + for block in self.blocks_to_run[::-1]: + if not block.has_been_run: + block.run_code() + if not self.has_been_run: + self.run_code() + + def run_left(self): + """Run all of the block's dependencies and then run the block""" + + # Reset has_been_run to make sure that the self is run again + self.has_been_run = False + + # To avoid crashing when spamming the button + if len(self.transmitting_queue) != 0: + return + + # Gather dependencies + blocks_to_run, to_transmit = self.custom_bfs(self) + self.blocks_to_run = blocks_to_run + + # Set the transmitting queue + self.transmitting_queue = to_transmit + # Set delay so that the transmitting animation has fixed total duration + self.transmitting_delay = int( + self.transmitting_duration / len(self.transmitting_queue) + ) + # Start transmitting animation + self.transmitting_animation_in() + + def run_right(self): + """Run all of the output blocks and all their dependencies""" + + # To avoid crashing when spamming the button + if len(self.transmitting_queue) != 0: + return + + # Create transmitting queue + self.transmitting_queue = self.right_traversal() + + # Gather outputs + blocks_to_run, _ = self.custom_bfs(self, reverse=True) + self.blocks_to_run = blocks_to_run + + # For each output found + for block in blocks_to_run.copy()[::-1]: + # Gather dependencies + new_blocks_to_run, _ = self.custom_bfs(block) + self.blocks_to_run += new_blocks_to_run + + # Set delay so that the transmitting animation has fixed total duration + self.transmitting_delay = int( + self.transmitting_duration / len(self.transmitting_queue) + ) + # Start transmitting animation + self.transmitting_animation_in() + + 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/markdownblock.py b/opencodeblocks/blocks/markdownblock.py index 9b3416d5..7c953b00 100644 --- a/opencodeblocks/blocks/markdownblock.py +++ b/opencodeblocks/blocks/markdownblock.py @@ -14,11 +14,11 @@ class OCBMarkdownBlock(OCBBlock): - """ A block that is able to render markdown text """ + """A block that is able to render markdown text""" def __init__(self, **kwargs): """ - Create a new OCBMarkdownBlock, a block that renders markdown + Create a new OCBMarkdownBlock, a block that renders markdown """ super().__init__(**kwargs) @@ -46,13 +46,14 @@ def __init__(self, **kwargs): self.rendered_markdown = QWebEngineView() self.rendered_markdown.page().setBackgroundColor( - QColor.fromRgba64(0, 0, 0, alpha=0)) + QColor.fromRgba64(0, 0, 0, alpha=0) + ) self.splitter.addWidget(self.rendered_markdown) self.holder.setWidget(self.root) def valueChanged(self): - """ Update markdown rendering when the content of the markdown editor changes """ + """Update markdown rendering when the content of the markdown editor changes""" t = self.editor.text() dark_theme = """ @@ -68,7 +69,7 @@ def valueChanged(self): @property def text(self) -> str: - """ The content of the markdown block """ + """The content of the markdown block""" return self.editor.text() @text.setter @@ -82,10 +83,11 @@ def serialize(self): return base_dict - def deserialize(self, data: OrderedDict, - hashmap: dict = None, restore_id: bool = True): - """ Restore a markdown block from it's serialized state """ - for dataname in ['text']: + def deserialize( + self, data: OrderedDict, hashmap: dict = None, restore_id: bool = True + ): + """Restore a markdown block from it's serialized state""" + for dataname in ["text"]: if dataname in data: setattr(self, dataname, data[dataname]) 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/core/serializable.py b/opencodeblocks/core/serializable.py index bc714a78..4ed6505d 100644 --- a/opencodeblocks/core/serializable.py +++ b/opencodeblocks/core/serializable.py @@ -13,7 +13,6 @@ class Serializable: MANDATORY_FIELDS: OrderedDict = {} DEFAULT_DATA: Set[str] = {} - def __init__(self): self.id = id(self) diff --git a/opencodeblocks/graphics/edge.py b/opencodeblocks/graphics/edge.py index 4376779e..7b178039 100644 --- a/opencodeblocks/graphics/edge.py +++ b/opencodeblocks/graphics/edge.py @@ -9,7 +9,11 @@ from PyQt5.QtCore import QPointF, Qt from PyQt5.QtGui import QColor, QPainter, QPainterPath, QPen -from PyQt5.QtWidgets import QGraphicsPathItem, QStyleOptionGraphicsItem, QWidget +from PyQt5.QtWidgets import ( + QGraphicsPathItem, + QStyleOptionGraphicsItem, + QWidget, +) from opencodeblocks.core.serializable import Serializable from opencodeblocks.graphics.socket import OCBSocket @@ -25,9 +29,11 @@ class OCBEdge(QGraphicsPathItem, Serializable): def __init__( self, edge_width: float = 4.0, - path_type = DEFAULT_DATA["path_type"], + path_type=DEFAULT_DATA["path_type"], edge_color="#001000", edge_selected_color="#00ff00", + edge_running_color="#FF0000", + edge_transmitting_color="#00ff00", source: QPointF = QPointF(0, 0), destination: QPointF = QPointF(0, 0), source_socket: OCBSocket = None, @@ -59,6 +65,17 @@ def __init__( self._pen_selected = QPen(QColor(edge_selected_color)) self._pen_selected.setWidthF(edge_width) + self._pen_running = QPen(QColor(edge_running_color)) + self._pen_running.setWidthF(edge_width) + + self._pen_transmitting = QPen(QColor(edge_transmitting_color)) + self._pen_transmitting.setWidthF(edge_width) + + self.pens = [self._pen, self._pen_running, self._pen_transmitting] + + # 0 for normal, 1 for running, 2 for transmitting + self.run_color = 0 + self.setFlag(QGraphicsPathItem.GraphicsItemFlag.ItemIsSelectable) self.setZValue(-1) @@ -104,8 +121,13 @@ def paint( ): # pylint:disable=unused-argument """Paint the edge.""" self.update_path() - pen = self._pen_dragging if self.destination_socket is None else self._pen - painter.setPen(self._pen_selected if self.isSelected() else pen) + if self.isSelected(): + pen = self._pen_selected + elif self.destination_socket is None: + pen = self._pen_dragging + else: + pen = self.pens[self.run_color] + painter.setPen(pen) painter.setBrush(Qt.BrushStyle.NoBrush) painter.drawPath(self.path()) @@ -237,3 +259,14 @@ def deserialize(self, data: OrderedDict, hashmap: dict = None, restore_id=True): self.update_path() except KeyError: self.remove() + + @property + def run_color(self) -> int: + """Run color""" + return self._run_color + + @run_color.setter + def run_color(self, value: int): + self._run_color = value + # Update to force repaint + self.update() diff --git a/opencodeblocks/graphics/function_parsing.py b/opencodeblocks/graphics/function_parsing.py index 7c18b9ac..4af206a1 100644 --- a/opencodeblocks/graphics/function_parsing.py +++ b/opencodeblocks/graphics/function_parsing.py @@ -1,4 +1,3 @@ - """ Module for code parsing and code execution """ from typing import List, Tuple @@ -119,11 +118,11 @@ def execute_function(code: str, *args, **kwargs) -> str: """ function_name = get_function_name(code) - execution_code = f'{function_name}(' + execution_code = f"{function_name}(" for arg in args: - execution_code += f'{arg},' + execution_code += f"{arg}," for name, value in kwargs.items(): - execution_code += f'{name}={value},' + execution_code += f"{name}={value}," run_cell(code) - return run_cell(execution_code + ')') + return run_cell(execution_code + ")") diff --git a/opencodeblocks/graphics/kernel.py b/opencodeblocks/graphics/kernel.py index c5a1f509..6cea732d 100644 --- a/opencodeblocks/graphics/kernel.py +++ b/opencodeblocks/graphics/kernel.py @@ -1,4 +1,3 @@ - """ Module to create and manage ipython kernels """ import queue @@ -8,7 +7,7 @@ from opencodeblocks.graphics.worker import Worker -class Kernel(): +class Kernel: """jupyter_client kernel used to execute code and return output""" @@ -28,31 +27,31 @@ def message_to_output(self, message: dict) -> Tuple[str, str]: single output found in the message in that order of priority: image > text data > text print > error > nothing """ - message_type = 'None' - if 'data' in message: - if 'image/png' in message['data']: - message_type = 'image' + message_type = "None" + if "data" in message: + if "image/png" in message["data"]: + message_type = "image" # output an image (from plt.plot or plt.imshow) - out = message['data']['image/png'] - elif 'text/html' in message['data']: - message_type = 'text' + out = message["data"]["image/png"] + elif "text/html" in message["data"]: + message_type = "text" # output some html text (like a pandas dataframe) - out = message['data']['text/html'] + out = message["data"]["text/html"] else: - message_type = 'text' + message_type = "text" # output data as str (for example if code="a=10\na") - out = message['data']['text/plain'] - elif 'name' in message and message['name'] == "stdout": - message_type = 'text' + out = message["data"]["text/plain"] + elif "name" in message and message["name"] == "stdout": + message_type = "text" # output a print (print("Hello World")) - out = message['text'] - elif 'traceback' in message: - message_type = 'error' + out = message["text"] + elif "traceback" in message: + message_type = "error" # output an error - out = '\n'.join(message['traceback']) + out = "\n".join(message["traceback"]) else: - message_type = 'text' - out = '' + message_type = "text" + out = "" return out, message_type def run_block(self, block, code: str): @@ -65,15 +64,17 @@ def run_block(self, block, code: str): code: String representing a piece of Python code to execute """ worker = Worker(self, code) + # Change color to running + block.run_color = 1 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 """ + """Runs the next code in the queue""" self.busy = True if self.execution_queue == []: self.busy = False @@ -114,8 +115,8 @@ def get_message(self) -> Tuple[str, bool]: """ done = False try: - message = self.client.get_iopub_msg(timeout=1000)['content'] - if 'execution_state' in message and message['execution_state'] == 'idle': + message = self.client.get_iopub_msg(timeout=1000)["content"] + if "execution_state" in message and message["execution_state"] == "idle": done = True except queue.Empty: message = None diff --git a/opencodeblocks/graphics/pyeditor/pyeditor.py b/opencodeblocks/graphics/pyeditor/pyeditor.py index 7d34c28d..29e62629 100644 --- a/opencodeblocks/graphics/pyeditor/pyeditor.py +++ b/opencodeblocks/graphics/pyeditor/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, @@ -19,10 +19,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 @@ -44,8 +40,6 @@ def __init__(self, block: OCBBlock): super().__init__(None) self._mode = "NOOP" self.block = block - self.kernel = kernel - self.threadpool = threadpool self.history = EditorHistory(self) self.pressingControl = False diff --git a/opencodeblocks/graphics/theme.py b/opencodeblocks/graphics/theme.py index 57171f3f..30182858 100644 --- a/opencodeblocks/graphics/theme.py +++ b/opencodeblocks/graphics/theme.py @@ -9,7 +9,7 @@ class Theme: - """ Class holding the details of a specific theme""" + """Class holding the details of a specific theme""" def __init__(self, name: str, json_str: str = "{}"): """ @@ -23,7 +23,7 @@ def __init__(self, name: str, json_str: str = "{}"): "keyword_color": "#569CD6", "classname_color": "#4EC9B0", "literal_color": "#7FB347", - "operator_color": "#D8D8D8" + "operator_color": "#D8D8D8", } for (property_name, property_value) in known_properties.items(): if property_name in json_obj: @@ -33,7 +33,7 @@ def __init__(self, name: str, json_str: str = "{}"): self.name = name def apply_to_lexer(self, lexer: QsciLexerPython): - """ Make the given lexer follow the theme """ + """Make the given lexer follow the theme""" lexer.setDefaultPaper(QColor("#1E1E1E")) lexer.setDefaultColor(QColor("#D4D4D4")) @@ -51,16 +51,10 @@ def apply_to_lexer(self, lexer: QsciLexerPython): for string_type in string_types: lexer.setColor(QColor(self.string_color), string_type) - lexer.setColor( - QColor( - self.function_color), - QsciLexerPython.FunctionMethodName) + lexer.setColor(QColor(self.function_color), QsciLexerPython.FunctionMethodName) lexer.setColor(QColor(self.keyword_color), QsciLexerPython.Keyword) lexer.setColor(QColor(self.classname_color), QsciLexerPython.ClassName) lexer.setColor(QColor(self.literal_color), QsciLexerPython.Number) lexer.setColor(QColor(self.operator_color), QsciLexerPython.Operator) - lexer.setColor( - QColor( - self.comment_color), - QsciLexerPython.CommentBlock) + lexer.setColor(QColor(self.comment_color), QsciLexerPython.CommentBlock) lexer.setColor(QColor(self.comment_color), QsciLexerPython.Comment) 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/window.py b/opencodeblocks/graphics/window.py index 3e2a6af4..e0017f7c 100644 --- a/opencodeblocks/graphics/window.py +++ b/opencodeblocks/graphics/window.py @@ -302,7 +302,8 @@ def createNewMdiChild(self, filename: str = None): ocb_widget = OCBWidget() if filename is not None: ocb_widget.scene.load(filename) - ocb_widget.savepath = filename + if filename.split(".")[-1] == "ipyg": + ocb_widget.savepath = filename return self.mdiArea.addSubWindow(ocb_widget) def onFileNew(self): @@ -374,7 +375,6 @@ def oneFileSaveAsJupyter(self) -> bool: ) if filename == "": return False - current_window.savepath = filename current_window.saveAsJupyter() self.statusbar.showMessage( f"Successfully saved ipygraph as jupter notebook at {current_window.savepath}", 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/qss/__init__.py b/opencodeblocks/qss/__init__.py index 16024f9c..208e3fc4 100644 --- a/opencodeblocks/qss/__init__.py +++ b/opencodeblocks/qss/__init__.py @@ -12,10 +12,10 @@ def loadStylesheets(filenames: List[str]): - styles = '' + styles = "" for filename in filenames: file = QFile(filename) file.open(QFile.ReadOnly | QFile.Text) stylesheet = file.readAll() - styles += "\n" + str(stylesheet, encoding='utf-8') + styles += "\n" + str(stylesheet, encoding="utf-8") QApplication.instance().setStyleSheet(styles) diff --git a/opencodeblocks/scene/clipboard.py b/opencodeblocks/scene/clipboard.py index 3d7b0320..c7fd021c 100644 --- a/opencodeblocks/scene/clipboard.py +++ b/opencodeblocks/scene/clipboard.py @@ -45,15 +45,13 @@ def _serializeSelected(self, delete=False) -> OrderedDict: selected_blocks, selected_edges = self.scene.sortedSelectedItems() selected_sockets = {} - for block in selected_blocks: # Gather selected sockets + # Gather selected sockets + for block in selected_blocks: for socket in block.sockets_in + block.sockets_out: selected_sockets[socket.id] = socket - for ( - edge - ) in ( - selected_edges - ): # Filter edges that are not fully connected to selected sockets + # Filter edges that are not fully connected to selected sockets + for edge in selected_edges: if ( edge.source_socket.id not in selected_sockets or edge.destination_socket.id not in selected_sockets diff --git a/opencodeblocks/scene/from_ipynb_conversion.py b/opencodeblocks/scene/from_ipynb_conversion.py index ab9b41c7..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,16 +37,19 @@ 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 next_block_y_pos: float = 0 + next_block_id = 0 for cell in data["cells"]: if "cell_type" not in cell or cell["cell_type"] not in ["code", "markdown"]: @@ -49,20 +58,32 @@ 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 block_data = { - "id": len(blocks_data), + "id": next_block_id, "block_type": BLOCK_TYPE_TO_NAME[block_type], "width": block_width, "height": block_height, @@ -93,6 +114,7 @@ def get_blocks_data(data: OrderedDict) -> List[OrderedDict]: next_block_y_pos += block_height + MARGIN_BETWEEN_BLOCKS_Y blocks_data.append(block_data) + next_block_id += 1 adujst_markdown_blocks_width(blocks_data) @@ -144,9 +166,13 @@ def get_edges_data(blocks_data: OrderedDict) -> OrderedDict: ] edges_data: List[OrderedDict] = [] + greatest_block_id: int = 0 + if len(blocks_data) > 0: + greatest_block_id = blocks_data[-1]["id"] + for i in range(1, len(code_blocks)): - socket_id_out = len(blocks_data) + 2 * i - socket_id_in = len(blocks_data) + 2 * i + 1 + socket_id_out = greatest_block_id + 2 * i + 2 + socket_id_in = greatest_block_id + 2 * i + 1 code_blocks[i - 1]["sockets"].append( get_output_socket_data(socket_id_out, code_blocks[i - 1]["width"]) ) diff --git a/opencodeblocks/scene/scene.py b/opencodeblocks/scene/scene.py index d00d641c..60b86814 100644 --- a/opencodeblocks/scene/scene.py +++ b/opencodeblocks/scene/scene.py @@ -5,24 +5,23 @@ 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 - -import networkx as nx +from opencodeblocks import blocks class OCBScene(QGraphicsScene, Serializable): @@ -59,6 +58,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 +178,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. """ @@ -191,6 +200,7 @@ def clear(self): return super().clear() def serialize(self) -> OrderedDict: + """Serialize the scene into a dict.""" blocks = [] edges = [] for item in self.items(): @@ -208,17 +218,6 @@ def serialize(self) -> OrderedDict: ] ) - def create_graph(self) -> nx.DiGraph: - """Create a networkx graph from the scene.""" - edges = [] - for item in self.items(): - if isinstance(item, OCBEdge): - edges.append(item) - graph = nx.DiGraph() - for edge in edges: - graph.add_edge(edge.source_socket.block, edge.destination_socket.block) - return graph - def create_block_from_file(self, filepath: str, x: float = 0, y: float = 0): """Create a new block from a .ocbb file""" with open(filepath, "r", encoding="utf-8") as file: @@ -258,8 +257,8 @@ def deserialize( ): self.clear() hashmap = hashmap if hashmap is not None else {} - if restore_id and 'id' in data: - self.id = data['id'] + if restore_id and "id" in data: + self.id = data["id"] # Create blocks for block_data in data["blocks"]: diff --git a/tests/assets/complex.ipynb b/tests/assets/complex.ipynb new file mode 100644 index 00000000..0fd86aa3 --- /dev/null +++ b/tests/assets/complex.ipynb @@ -0,0 +1,914 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Deep Neural Networks\n", + "\n", + "---\n", + "\n", + "\n", + "\n", + "\n", + "> Version: **1.0**\n", + "\n", + "\n", + "\n", + "\n", + "# 1 . Implementation of a Neural Network\n", + "\n", + "In this exercise you will learn how to implement from scratch a deep neural network.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Set-up\n", + "\n", + "Firstly you will import all the packages used through the notebook. " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\users\\efabr\\miniconda3\\lib\\site-packages\\numpy\\_distributor_init.py:32: UserWarning: loaded more than 1 DLL from .libs:\n", + "c:\\users\\efabr\\miniconda3\\lib\\site-packages\\numpy\\.libs\\libopenblas.QVLO2T66WEPI7JZ63PS3HMOHFEY472BC.gfortran-win_amd64.dll\n", + "c:\\users\\efabr\\miniconda3\\lib\\site-packages\\numpy\\.libs\\libopenblas.WCDJNK7YVMPZQ2ME2ZZHJJRJ3JIKNDB7.gfortran-win_amd64.dll\n", + " stacklevel=1)\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import h5py\n", + "\n", + "%matplotlib inline\n", + "\n", + "%load_ext autoreload\n", + "%autoreload 2\n", + "\n", + "np.random.seed(3)\n", + "\n", + "from utils import *" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Initialization\n", + "\n", + "Start by defining a function that allows to initialize the parameters of a deep neural network where the dimensions. The number of units in the different layers are passed as argument with `layer_dims`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [], + "source": [ + "def initialization(layer_dims):\n", + "\n", + " np.random.seed(5)\n", + " parameters = {}\n", + " L = len(layer_dims) \n", + " \n", + " for l in range(1, L):\n", + " parameters['W' + str(l)] = np.random.randn(layer_dims[l],layer_dims[l-1])*0.01\n", + " parameters['b' + str(l)] = np.zeros((1,layer_dims[l]))\n", + " \n", + " return parameters" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "W1 = [[ 0.00441227 -0.0033087 0.02430771 -0.00252092 0.0010961 ]\n", + " [ 0.01582481 -0.00909232 -0.00591637 0.00187603 -0.0032987 ]\n", + " [-0.01192765 -0.00204877 -0.00358829 0.00603472 -0.01664789]\n", + " [-0.00700179 0.01151391 0.01857331 -0.0151118 0.00644848]]\n", + "b1 = [[0. 0. 0. 0.]]\n", + "W2 = [[-9.80607885e-03 -8.56853155e-03 -8.71879183e-03 -4.22507929e-03]\n", + " [ 9.96439827e-03 7.12421271e-03 5.91442432e-04 -3.63310878e-03]\n", + " [ 3.28884293e-05 -1.05930442e-03 7.93053319e-03 -6.31571630e-03]]\n", + "b2 = [[0. 0. 0.]]\n" + ] + } + ], + "source": [ + "parameters = initialization([5,4,3])\n", + "print(\"W1 = \" + str(parameters[\"W1\"]))\n", + "print(\"b1 = \" + str(parameters[\"b1\"]))\n", + "print(\"W2 = \" + str(parameters[\"W2\"]))\n", + "print(\"b2 = \" + str(parameters[\"b2\"]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Forward propagation\n", + "\n", + "The forward propagation has been split in different steps. Firstly, the linear forward module computes the following equations:\n", + "\n", + "$$Z^{[l]} = W^{[l]}A^{[l-1]} +b^{[l]}\\tag{4}$$\n", + "\n", + "where $A^{[0]} = X$. \n", + "\n", + "Define a function to compute $Z^{[l]}$" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": {}, + "outputs": [], + "source": [ + "def linear_forward(A, W, b):\n", + " Z = W@A + b\n", + " cache = (A, W, b)\n", + " \n", + " return Z, cache" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Z = [[-0.67356113 0.67062057]]\n" + ] + } + ], + "source": [ + "A, W, b = linear_forward_test()\n", + "\n", + "Z, linear_cache = linear_forward(A, W, b)\n", + "print(\"Z = \" + str(Z))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Expected output**:\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
**Z** [[ -0.67356113 0.67062057]]
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Activation Functions\n", + "\n", + "In the first notebook you implemented the sigmoid function:\n", + "\n", + "- **Sigmoid**: $\\sigma(Z) = \\sigma(W A + b) = \\frac{1}{ 1 + e^{-(W A + b)}}$.\n", + "\n", + "In this notebook, you will need to implement the ReLU activation defined as:\n", + "\n", + "- **ReLU**: $A = RELU(Z) = max(0, Z)$. \n", + "\n", + "Complete the function below that computes the ReLU an activation fucntion." + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": {}, + "outputs": [], + "source": [ + "def relu(Z):\n", + "\n", + " A = np.maximum(0,Z)\n", + " cache = Z \n", + " \n", + " return A, cache" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You have implemented a function that determines the linear foward step. You will now combine the output of this function with either a sigmoid() or a relu() activation function. " + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": {}, + "outputs": [], + "source": [ + "def forward_one(A_prev, W, b, activation):\n", + " Z, linear_cache = linear_forward(A_prev, W, b)\n", + " if activation == 'relu' :\n", + " A, activation_cache = relu(Z)\n", + " \n", + " elif activation == 'sigmoid' :\n", + " A, activation_cache = sigmoid(Z)\n", + " \n", + " cache = (linear_cache, activation_cache)\n", + "\n", + " return A, cache" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "With sigmoid: A = [[0.96313579 0.22542973]]\n", + "With ReLU: A = [[3.26295337 0. ]]\n" + ] + } + ], + "source": [ + "A_prev, W, b = forward_one_test()\n", + "\n", + "A, linear_activation_cache = forward_one(A_prev, W, b, activation = \"sigmoid\")\n", + "print(\"With sigmoid: A = \" + str(A))\n", + "\n", + "A, linear_activation_cache = forward_one(A_prev, W, b, activation = \"relu\")\n", + "print(\"With ReLU: A = \" + str(A))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Forward propagation model\n", + "\n", + "The structure you will implement in this exercise consists on $L-1$ layers using a ReLU activation function and a last layer using a sigmoid.\n", + "Implement the forward propagation of the above model." + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": {}, + "outputs": [], + "source": [ + "def forward_all(X, parameters):\n", + "\n", + " caches = []\n", + " A = X\n", + " L = len(parameters) // 2 \n", + " \n", + " for l in range(1, L):\n", + " A_prev = A \n", + " # Implement L-1 layers of RELU and for each layer add \"cache\" to the \"caches\" list.\n", + " A, cache = forward_one(A,parameters[\"W\"+ str(l)],parameters[\"b\"+ str(l)],\"relu\")\n", + " caches.append(cache)\n", + " AL, cache = forward_one(A,parameters[\"W\"+ str(L)],parameters[\"b\"+ str(L)],\"sigmoid\")\n", + " caches.append(cache)\n", + " \n", + " \n", + " return AL, caches" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "AL = [[0.03921668 0.70498921 0.19734387 0.04728177]]\n", + "Length of caches list = 3\n" + ] + } + ], + "source": [ + "X, parameters = forward_all_test()\n", + "AL, caches = forward_all(X, parameters)\n", + "print(\"AL = \" + str(AL))\n", + "print(\"Length of caches list = \" + str(len(caches)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Cost function\n", + "\n", + "You will now compute the cross-entropy cost $J$, for all the training set using the following formula: $$-\\frac{1}{m} \\sum\\limits_{i = 1}^{m} (y^{(i)}\\log\\left(a^{[L] (i)}\\right) + (1-y^{(i)})\\log\\left(1- a^{[L](i)}\\right)) \\tag{7}$$\n" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": {}, + "outputs": [], + "source": [ + "def cost_function(AL, Y):\n", + " \n", + " m = Y.shape[1]\n", + "\n", + " cost = (-1/m)*np.sum((np.dot(Y,np.log(AL.T))+np.dot((1-Y),np.log(1-AL).T))) \n", + " cost = np.squeeze(cost) # Eliminates useless dimensionality for the variable cost.\n", + " \n", + " return cost" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "cost = 0.2797765635793422\n" + ] + } + ], + "source": [ + "Y, AL = compute_cost()\n", + "print(\"cost = \" + str(cost_function(AL, Y)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
**cost** 0.2797765635793422
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Backpropagation \n", + "\n", + "You will now implement the functions that will help you compute the gradient of the loss function with respect to the different parameters.\n", + "\n", + "To move backward in the computational graph you need to apply the chain rule." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Linear backward\n", + "\n", + "For each layer $l$, the linear part is: $Z^{[l]} = W^{[l]} A^{[l-1]} + b^{[l]}$ (followed by an activation).\n", + "\n", + "Suppose you have already calculated the derivative $dZ^{[l]} = \\frac{\\partial \\mathcal{L} }{\\partial Z^{[l]}}$. You want to get $(dW^{[l]}, db^{[l]}, dA^{[l-1]})$.\n", + "\n", + "\n", + "The three outputs $(dW^{[l]}, db^{[l]}, dA^{[l-1]})$ are computed using the input $dZ^{[l]}$. The formulas you saw in class are:\n", + "$$ dW^{[l]} = \\frac{\\partial \\mathcal{J} }{\\partial W^{[l]}} = \\frac{1}{m} dZ^{[l]} A^{[l-1] T} \\tag{8}$$\n", + "$$ db^{[l]} = \\frac{\\partial \\mathcal{J} }{\\partial b^{[l]}} = \\frac{1}{m} \\sum_{i = 1}^{m} dZ^{[l](i)}\\tag{9}$$\n", + "$$ dA^{[l-1]} = \\frac{\\partial \\mathcal{L} }{\\partial A^{[l-1]}} = W^{[l] T} dZ^{[l]} \\tag{10}$$\n" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": {}, + "outputs": [], + "source": [ + "def linear_backward(dZ, cache):\n", + " A_prev, W, b = cache\n", + " m = A_prev.shape[1]\n", + " \n", + " dW = 1/m * dZ@A_prev.T\n", + " db = 1/m * np.sum(dZ,1, keepdims = True)\n", + " dA_prev = W.T@dZ\n", + " \n", + " return dA_prev, dW, db\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "dA_prev = [[ 1.62477986e-01 2.08119187e+00 -1.34890293e+00 -8.08822550e-01]\n", + " [ 1.25651742e-02 -2.21287224e-01 -5.90636554e-01 4.05614891e-03]\n", + " [ 1.98659671e-01 2.39946554e+00 -1.86852905e+00 -9.65910523e-01]\n", + " [ 3.18813678e-01 -9.92645222e-01 -6.57125623e-01 -1.46564901e-01]\n", + " [ 2.48593418e-01 -1.19723579e+00 -4.44132647e-01 -6.09748046e-04]]\n", + "dW = [[-1.05705158 -0.98560069 -0.54049797 0.10982291 0.53086144]\n", + " [ 0.71089562 1.01447326 -0.10518156 0.34944625 -0.12867032]\n", + " [ 0.46569162 0.31842359 0.30629837 -0.01104559 -0.19524287]]\n", + "db = [[ 0.5722591 ]\n", + " [ 0.04780547]\n", + " [-0.38497696]]\n" + ] + } + ], + "source": [ + "# Set up some test inputs\n", + "dZ, linear_cache = linear_backward_test()\n", + "\n", + "dA_prev, dW, db = linear_backward(dZ, linear_cache)\n", + "\n", + "print (\"dA_prev = \"+ str(dA_prev))\n", + "print (\"dW = \" + str(dW))\n", + "print (\"db = \" + str(db))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "** Expected Output**:\n", + " \n", + "```\n", + "dA_prev = \n", + "[[ 1.62477986e-01 2.08119187e+00 -1.34890293e+00 -8.08822550e-01]\n", + " [ 1.25651742e-02 -2.21287224e-01 -5.90636554e-01 4.05614891e-03]\n", + " [ 1.98659671e-01 2.39946554e+00 -1.86852905e+00 -9.65910523e-01]\n", + " [ 3.18813678e-01 -9.92645222e-01 -6.57125623e-01 -1.46564901e-01]\n", + " [ 2.48593418e-01 -1.19723579e+00 -4.44132647e-01 -6.09748046e-04]]\n", + "dW = \n", + "[[-1.05705158 -0.98560069 -0.54049797 0.10982291 0.53086144]\n", + " [ 0.71089562 1.01447326 -0.10518156 0.34944625 -0.12867032]\n", + " [ 0.46569162 0.31842359 0.30629837 -0.01104559 -0.19524287]]\n", + "db = \n", + "[[ 0.5722591 ]\n", + " [ 0.04780547]\n", + " [-0.38497696]]\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Activation Functions\n", + "\n", + "Now you need to write the code that computes the derivatives for the activation functions. You have learned the derivatives for the sigmoid and the ReLU during theory class.\n", + "Complete the two function below." + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": {}, + "outputs": [], + "source": [ + "def sigmoid_backward(dA, cache): \n", + " Z = cache\n", + " \n", + " s = Z*(1-Z)\n", + " dZ = dA*s\n", + " \n", + " return dZ" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": {}, + "outputs": [], + "source": [ + "def relu_backward(dA, cache):\n", + " \n", + " Z = cache \n", + " dZ = np.array(dA, copy=True) # convert dz to an array.\n", + " dZ = dZ*np.where(Z>0,1,0)\n", + " return dZ" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### One backpropagation step\n", + "\n", + "Next, you will create a function that implements one step of backpropagation," + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": {}, + "outputs": [], + "source": [ + "def backward_one(dA, cache, activation):\n", + " linear_cache, activation_cache = cache \n", + " if activation == \"relu\":\n", + " dZ = relu_backward(dA,activation_cache)\n", + " dA_prev, dW, db = linear_backward(dZ, linear_cache)\n", + " elif activation == \"sigmoid\":\n", + " dZ = sigmoid_backward(dA,activation_cache)\n", + " dA_prev, dW, db = linear_backward(dZ, linear_cache)\n", + " \n", + " return dA_prev, dW, db" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "sigmoid:\n", + "dA_prev = [[ 0.00410547 0.03685307]\n", + " [-0.01417887 -0.12727776]\n", + " [ 0.00764463 0.06862266]]\n", + "dW = [[ 0.03231386 -0.0904648 0.02919517]]\n", + "db = [[0.06163813]]\n", + "\n", + "relu:\n", + "dA_prev = [[ 0.01679913 0.16610885]\n", + " [-0.05801838 -0.57368247]\n", + " [ 0.031281 0.30930474]]\n", + "dW = [[ 0.14820532 -0.40668077 0.13325465]]\n", + "db = [[0.27525652]]\n" + ] + } + ], + "source": [ + "dAL, linear_activation_cache = linear_activation_backward_test()\n", + "\n", + "dA_prev, dW, db = backward_one(dAL, linear_activation_cache, \"sigmoid\")\n", + "print (\"sigmoid:\")\n", + "print (\"dA_prev = \"+ str(dA_prev))\n", + "print (\"dW = \" + str(dW))\n", + "print (\"db = \" + str(db) + \"\\n\")\n", + "\n", + "dA_prev, dW, db = backward_one(dAL, linear_activation_cache, activation = \"relu\")\n", + "print (\"relu:\")\n", + "print (\"dA_prev = \"+ str(dA_prev))\n", + "print (\"dW = \" + str(dW))\n", + "print (\"db = \" + str(db))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Backpropagation model\n", + "\n", + "Now you will put all together to compute the backward function for the whole network. \n", + "In the backpropagation step, you will use the variables you stored in cache in the `forward_all` function to compute the gradients. You will iterate from the last layer backwards to layer $1$.\n", + "\n", + "You need to start by computing the derivative of the loss function with respect to $A^{[L]}$. And propagate this gradient backward thourgh all the layers in the network.\n", + "\n", + "You need to save each dA, dW and db in the grads dictionary. " + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": {}, + "outputs": [], + "source": [ + "def backward_all(AL, Y, caches):\n", + " grads = {}\n", + " L = len(caches) \n", + " m = AL.shape[1]\n", + " Y = Y.reshape(AL.shape) \n", + "\n", + " dZ = AL-Y\n", + " current_cache = caches[L-1]\n", + " grads[\"dA\" + str(L-1)], grads[\"dW\" + str(L)], grads[\"db\" + str(L)] = linear_backward(dZ, current_cache[0])\n", + " dAL = grads[\"dA\" + str(L-1)]\n", + " for l in reversed(range(L-1)):\n", + " current_cache = caches[l]\n", + " dA_prev_temp, dW_temp, db_temp = backward_one(dAL, current_cache, \"relu\")\n", + " grads[\"dA\" + str(l)] = dA_prev_temp\n", + " grads[\"dW\" + str(l + 1)] = dW_temp\n", + " grads[\"db\" + str(l + 1)] = db_temp\n", + " return grads" + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "dW1 = [[0.41642713 0.07927654 0.14011329 0.10664197]\n", + " [0. 0. 0. 0. ]\n", + " [0.05365169 0.01021384 0.01805193 0.01373955]]\n", + "db1 = [[-0.22346593]\n", + " [ 0. ]\n", + " [-0.02879093]]\n", + "dA1 = [[-0.80745758 -0.44693186]\n", + " [ 0.88640102 0.49062745]\n", + " [-0.10403132 -0.05758186]]\n" + ] + } + ], + "source": [ + "AL, Y_assess, caches = backward_all_test()\n", + "grads = backward_all(AL, Y_assess, caches)\n", + "print_grads(grads)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Expected Output**\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n", + " \n", + "
dW1 [[ 0.41010002 0.07807203 0.13798444 0.10502167]\n", + " [ 0. 0. 0. 0. ]\n", + " [ 0.05283652 0.01005865 0.01777766 0.0135308 ]]
db1 [[-0.22007063]\n", + " [ 0. ]\n", + " [-0.02835349]]
dA1 [[ 0.12913162 -0.44014127]\n", + " [-0.14175655 0.48317296]\n", + " [ 0.01663708 -0.05670698]]
\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Gradient Descent\n", + "\n", + "Finally you can update the parameters of the model according: \n", + "\n", + "$$ W^{[l]} = W^{[l]} - \\alpha \\text{ } dW^{[l]} $$\n", + "$$ b^{[l]} = b^{[l]} - \\alpha \\text{ } db^{[l]} $$\n", + "\n", + "where $\\alpha$ is the learning rate. After computing the updated parameters, store them in the parameters dictionary. " + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "metadata": {}, + "outputs": [], + "source": [ + "def gradient_descent(parameters, grads, learning_rate):\n", + " L = len(parameters) // 2 \n", + "\n", + " for l in range(L):\n", + " parameters[\"W\" + str(l+1)] = parameters[\"W\"+ str(l+1)] - learning_rate * grads[\"dW\"+ str(l+1)]\n", + " parameters[\"b\" + str(l+1)] = parameters[\"b\"+ str(l+1)] - learning_rate * grads[\"db\"+ str(l+1)]\n", + " return parameters" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "W1 = [[-0.59562069 -0.09991781 -2.14584584 1.82662008]\n", + " [-1.76569676 -0.80627147 0.51115557 -1.18258802]\n", + " [-1.0535704 -0.86128581 0.68284052 2.20374577]]\n", + "b1 = [[-0.04659241]\n", + " [-1.28888275]\n", + " [ 0.53405496]]\n", + "W2 = [[-0.55569196 0.0354055 1.32964895]]\n", + "b2 = [[-0.84610769]]\n" + ] + } + ], + "source": [ + "parameters, grads = gradient_descent_test_case()\n", + "parameters = gradient_descent(parameters, grads, 0.1)\n", + "\n", + "print (\"W1 = \"+ str(parameters[\"W1\"]))\n", + "print (\"b1 = \"+ str(parameters[\"b1\"]))\n", + "print (\"W2 = \"+ str(parameters[\"W2\"]))\n", + "print (\"b2 = \"+ str(parameters[\"b2\"]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Expected Output**:\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
W1 [[-0.59562069 -0.09991781 -2.14584584 1.82662008]\n", + " [-1.76569676 -0.80627147 0.51115557 -1.18258802]\n", + " [-1.0535704 -0.86128581 0.68284052 2.20374577]]
b1 [[-0.04659241]\n", + " [-1.28888275]\n", + " [ 0.53405496]]
W2 [[-0.55569196 0.0354055 1.32964895]]
b2 [[-0.84610769]]
\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can now create a deep neural network combining all the functions defined above." + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "metadata": {}, + "outputs": [], + "source": [ + "def dnn(X, Y, layers_dims, learning_rate = 0.009, num_iterations = 100, print_cost=True):#lr was 0.009\n", + " costs = [] \n", + " \n", + " parameters = initialization(layers_dims)\n", + " for i in range(0, num_iterations):\n", + " AL, caches = forward_all(X, parameters)\n", + " cost = cost_function(AL, Y)\n", + " costs.append(cost)\n", + " if print_cost:\n", + " print(cost)\n", + " grads = backward_all(AL, Y, caches)\n", + " parameters = gradient_descent(parameters, grads, learning_rate)\n", + " \n", + " \n", + " return parameters, costs" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 2 . Deep Neural Networks for Classification\n", + "\n", + "Consider now the dataset you used in the previous exercise. You solved the classification problem using Logistic Regression. Propose a Deep Neural Network architecture using the code you developed in the first part of this exercise that improves on the classification results of Logistic Regression." + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import h5py\n", + "import scipy\n", + "from PIL import Image\n", + "from scipy import ndimage\n", + "from lr_utils import load_dataset\n", + "\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "train_set_x_orig, train_set_y, test_set_x_orig, test_set_y, classes = load_dataset()" + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "metadata": {}, + "outputs": [], + "source": [ + "m_train = train_set_x_orig.shape[0]\n", + "m_test = test_set_x_orig.shape[0]\n", + "num_px = train_set_x_orig.shape[1] \n", + "train_set_x_flatten = train_set_x_orig.reshape(m_train,num_px * num_px * 3).T\n", + "test_set_x_flatten = test_set_x_orig.reshape(m_test,num_px * num_px * 3).T\n", + "train_set_x = train_set_x_flatten/255.\n", + "test_set_x = test_set_x_flatten/255." + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(12288, 209) 64\n" + ] + }, + { + "ename": "TypeError", + "evalue": "_swapaxes_dispatcher() missing 2 required positional arguments: 'axis1' and 'axis2'", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mTypeError\u001b[0m Traceback (most recent call last)", + "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m()\u001b[0m\n\u001b[0;32m 1\u001b[0m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mtrain_set_x_flatten\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mshape\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mnum_px\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 2\u001b[1;33m \u001b[0mnp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mswapaxes\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mtrain_set_x_flatten\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 3\u001b[0m \u001b[0mdnn\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mtrain_set_x_flatten\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrain_set_y\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m[\u001b[0m\u001b[0mtrain_set_x_flatten\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mshape\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;36m4\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;36m1\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32m<__array_function__ internals>\u001b[0m in \u001b[0;36mswapaxes\u001b[1;34m(*args, **kwargs)\u001b[0m\n", + "\u001b[1;31mTypeError\u001b[0m: _swapaxes_dispatcher() missing 2 required positional arguments: 'axis1' and 'axis2'" + ] + } + ], + "source": [ + "print(train_set_x_flatten.shape, num_px)\n", + "np.swapaxes(train_set_x_flatten)\n", + "dnn(train_set_x_flatten, train_set_y, [train_set_x_flatten.shape[0], 4, 1])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "anaconda-cloud": {}, + "coursera": { + "course_slug": "neural-networks-deep-learning", + "graded_item_id": "c4HO0", + "launcher_item_id": "lSYZM" + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} 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/empty.ipynb b/tests/assets/empty.ipynb new file mode 100644 index 00000000..c6f02f6f --- /dev/null +++ b/tests/assets/empty.ipynb @@ -0,0 +1,32 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.4" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} 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/assets/usual.ipynb b/tests/assets/usual.ipynb new file mode 100644 index 00000000..8f60756f --- /dev/null +++ b/tests/assets/usual.ipynb @@ -0,0 +1,194 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# A report" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\users\\efabr\\miniconda3\\lib\\site-packages\\numpy\\_distributor_init.py:32: UserWarning: loaded more than 1 DLL from .libs:\n", + "c:\\users\\efabr\\miniconda3\\lib\\site-packages\\numpy\\.libs\\libopenblas.QVLO2T66WEPI7JZ63PS3HMOHFEY472BC.gfortran-win_amd64.dll\n", + "c:\\users\\efabr\\miniconda3\\lib\\site-packages\\numpy\\.libs\\libopenblas.WCDJNK7YVMPZQ2ME2ZZHJJRJ3JIKNDB7.gfortran-win_amd64.dll\n", + " stacklevel=1)\n" + ] + } + ], + "source": [ + "# Imports\n", + "import numpy as np\n", + "import anotherlib as al" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First we load some data" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "def loading_data(filename):\n", + " return np.random.random((20,20))" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "data = loading_data(\"hello.csv\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then do something with it" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[3.63948558, 3.27076204, 3.48612965, 3.04451501, 3.36451054,\n", + " 3.49666763, 3.45160491, 3.41537927, 3.07652413, 3.66197315,\n", + " 3.18283939, 3.24002107, 3.55144998, 3.29783081, 3.57207105,\n", + " 3.52894424, 3.55476906, 3.8049996 , 3.97005352, 3.01381803],\n", + " [3.36975411, 3.79889692, 3.98689836, 3.19976399, 3.07865854,\n", + " 3.0357039 , 3.36727864, 3.44675425, 3.1485551 , 3.32505796,\n", + " 3.94280723, 3.93294761, 3.69969612, 3.39441561, 3.52488239,\n", + " 3.77017711, 3.31080687, 3.13851642, 3.8914675 , 3.22217344],\n", + " [3.68869882, 3.49143552, 3.94120228, 3.29677181, 3.49179313,\n", + " 3.13340065, 3.19535721, 3.19036895, 3.51647584, 3.56500995,\n", + " 3.71223623, 3.20364635, 3.74915529, 3.60763252, 3.33939492,\n", + " 3.32512116, 3.77610616, 3.4022048 , 3.58662752, 3.89863465],\n", + " [3.33233293, 3.67890573, 3.34158316, 3.20828747, 3.50817954,\n", + " 3.5434048 , 3.40567346, 3.34097188, 3.33359796, 3.54765666,\n", + " 3.78149215, 3.82061228, 3.58202782, 3.21714103, 3.78187063,\n", + " 3.14331254, 3.21238203, 3.05876572, 3.23641512, 3.07314562],\n", + " [3.96469085, 3.34664564, 3.80053502, 3.35413646, 3.2456791 ,\n", + " 3.9323963 , 3.14123278, 3.08543803, 3.32433011, 3.53758282,\n", + " 3.56606284, 3.283641 , 3.35755769, 3.57456616, 3.47084384,\n", + " 3.59188003, 3.7366276 , 3.1607076 , 3.70733558, 3.89854312],\n", + " [3.37855368, 3.37342495, 3.74749753, 3.32611455, 3.04470219,\n", + " 3.26913714, 3.55734092, 3.15093813, 3.06262451, 3.33520461,\n", + " 3.53713674, 3.73679289, 3.00179027, 3.67067575, 3.4524812 ,\n", + " 3.13490282, 3.17967332, 3.27475765, 3.63188686, 3.73635671],\n", + " [3.79496576, 3.13701034, 3.9137442 , 3.81506409, 3.45731737,\n", + " 3.71956806, 3.33353583, 3.22600394, 3.320342 , 3.50387212,\n", + " 3.52740845, 3.1051689 , 3.51774963, 3.39219758, 3.76537776,\n", + " 3.13219734, 3.2705521 , 3.51957676, 3.74631399, 3.39932089],\n", + " [3.55779719, 3.41516178, 3.07029595, 3.22289207, 3.28158917,\n", + " 3.79661708, 3.71722156, 3.36995609, 3.33689999, 3.59126513,\n", + " 3.7079066 , 3.49525298, 3.56939265, 3.96935088, 3.12164002,\n", + " 3.676332 , 3.97468624, 3.51783025, 3.21887996, 3.30523752],\n", + " [3.84033676, 3.32525466, 3.71113927, 3.9279179 , 3.002697 ,\n", + " 3.15725527, 3.03005171, 3.47318527, 3.64722962, 3.28827003,\n", + " 3.48847088, 3.19699471, 3.80627061, 3.18231442, 3.54125576,\n", + " 3.34299146, 3.73841012, 3.67166747, 3.9092137 , 3.0335553 ],\n", + " [3.7058796 , 3.57808271, 3.39623753, 3.92757397, 3.1351561 ,\n", + " 3.48092558, 3.33881418, 3.07534734, 3.4116249 , 3.83838506,\n", + " 3.48396284, 3.89211238, 3.09995526, 3.57699336, 3.04028569,\n", + " 3.70796281, 3.82625111, 3.00335284, 3.82351291, 3.45597272],\n", + " [3.97176208, 3.2377911 , 3.35122552, 3.008061 , 3.42012245,\n", + " 3.80543675, 3.05006123, 3.82998196, 3.21035844, 3.9324146 ,\n", + " 3.05317098, 3.61833738, 3.63764824, 3.52124233, 3.87902481,\n", + " 3.50641822, 3.3221031 , 3.7521446 , 3.71614348, 3.18424037],\n", + " [3.85059993, 3.70474109, 3.67106526, 3.46888781, 3.51912545,\n", + " 3.37423032, 3.03162456, 3.46525341, 3.16811541, 3.08918478,\n", + " 3.21506861, 3.49130976, 3.20194597, 3.70852144, 3.83703797,\n", + " 3.46643191, 3.56993413, 3.01070241, 3.0345923 , 3.38354211],\n", + " [3.1067765 , 3.17986932, 3.39974581, 3.81503415, 3.40999637,\n", + " 3.40962731, 3.89842993, 3.68748917, 3.46850976, 3.72833772,\n", + " 3.84667009, 3.51419885, 3.34223842, 3.75641038, 3.09752562,\n", + " 3.10430882, 3.17031172, 3.68318615, 3.26852823, 3.55773047],\n", + " [3.84328038, 3.12204164, 3.04845877, 3.06138172, 3.72354385,\n", + " 3.55180905, 3.52664241, 3.51290538, 3.41487721, 3.5808519 ,\n", + " 3.52471368, 3.1293383 , 3.97481537, 3.53333148, 3.17242121,\n", + " 3.33274109, 3.66331831, 3.48478873, 3.86629266, 3.39943898],\n", + " [3.01380158, 3.91737603, 3.92958052, 3.04459408, 3.33989635,\n", + " 3.46058188, 3.26540603, 3.86486319, 3.0671772 , 3.55414925,\n", + " 3.29465078, 3.73982667, 3.64077128, 3.79522347, 3.64958283,\n", + " 3.62710174, 3.27515289, 3.46606658, 3.65970842, 3.56858124],\n", + " [3.81765364, 3.18478292, 3.80875158, 3.08533944, 3.96073804,\n", + " 3.88650821, 3.98854391, 3.75026235, 3.99059743, 3.20337304,\n", + " 3.29633007, 3.8081186 , 3.38162993, 3.94841158, 3.14155517,\n", + " 3.63415005, 3.09000952, 3.87785134, 3.4880709 , 3.57519841],\n", + " [3.24256969, 3.50708353, 3.41433647, 3.28962266, 3.83899191,\n", + " 3.39102446, 3.6750535 , 3.65617164, 3.5215255 , 3.40090808,\n", + " 3.39341075, 3.62686904, 3.78749222, 3.68898565, 3.6632341 ,\n", + " 3.6559378 , 3.65910078, 3.47150939, 3.42101486, 3.91651094],\n", + " [3.94385503, 3.99691801, 3.93431865, 3.62193945, 3.47153953,\n", + " 3.75111669, 3.00165189, 3.66504947, 3.7384211 , 3.20962032,\n", + " 3.10356305, 3.25014366, 3.22037248, 3.36181067, 3.30191277,\n", + " 3.88477453, 3.12995043, 3.13019189, 3.31214712, 3.13234449],\n", + " [3.21909987, 3.22103605, 3.01240075, 3.27869683, 3.3607669 ,\n", + " 3.73113868, 3.30960944, 3.50728795, 3.89795141, 3.62701628,\n", + " 3.61512764, 3.62268613, 3.72038233, 3.4902457 , 3.8297139 ,\n", + " 3.55794527, 3.43277611, 3.8092867 , 3.28863261, 3.62435088],\n", + " [3.05355563, 3.74357485, 3.12245218, 3.88984309, 3.7065529 ,\n", + " 3.83742772, 3.49424284, 3.44208031, 3.10441662, 3.26142749,\n", + " 3.41649061, 3.57395604, 3.60538383, 3.19872492, 3.2726298 ,\n", + " 3.47467517, 3.72478131, 3.8025068 , 3.54180911, 3.11842077]])" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "data = data + 3\n", + "data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.4" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} 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 7ab6b67b..3c304a0a 100644 --- a/tests/integration/blocks/test_codeblock.py +++ b/tests/integration/blocks/test_codeblock.py @@ -5,35 +5,30 @@ 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() + """Setup reused variables.""" + start_app(self) - def test_run_python(self, qtbot: QtBot): - """ run source code when run button is pressed. """ + def test_run_python(self): + """run source code when run button is pressed.""" # Add a block with the source to the window EXPRESSION = "3 + 5 * 2" - SOURCE_TEST = f'''print({EXPRESSION})''' + SOURCE_TEST = f"""print({EXPRESSION})""" expected_result = str(3 + 5 * 2) test_block = OCBCodeBlock(title="CodeBlock test", source=SOURCE_TEST) @@ -56,13 +51,54 @@ 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((test_block.transmitting_duration / 1000) + 0.2) + while test_block.run_color != 0: + time.sleep(0.1) - # 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.run_color != 0: + 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 ab820dde..4bd3747e 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((block_to_run.transmitting_duration / 1000) + 0.2) + while block_to_run.run_color != 0: + time.sleep(0.1) + # 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(1) + time.sleep((block_to_run.transmitting_duration / 1000) + 0.2) + while block_to_run.run_color != 0: + time.sleep(0.1) - 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,12 @@ 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((block_to_run.transmitting_duration / 1000) + 0.2) + while block_to_run.run_color != 0: + time.sleep(0.1) msgQueue.check_equal(block_to_run.stdout.strip(), "1") msgQueue.stop() @@ -126,9 +119,14 @@ def run_block(): block_to_run.run_right() msgQueue.run_lambda(run_block) - time.sleep(0.5) + time.sleep((block_to_run.transmitting_duration / 1000) + 0.2) + while block_to_run.run_color != 0: + 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 1ab0ecee..06a084bd 100644 --- a/tests/integration/test_window.py +++ b/tests/integration/test_window.py @@ -13,20 +13,19 @@ class TestWindow: - @pytest.fixture(autouse=True) def setup(self, mocker: MockerFixture): - """ Setup reused variables. """ + """Setup reused variables.""" self.window = OCBWindow() - def test_window_close(self, qtbot): - """ closes """ - self.window.close() - - def test_open_file(self): - """ loads files """ + 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_clipboard.py b/tests/unit/scene/test_clipboard.py index 99739f20..d28b2087 100644 --- a/tests/unit/scene/test_clipboard.py +++ b/tests/unit/scene/test_clipboard.py @@ -15,7 +15,7 @@ class TestSerializeSelected: """SceneClipboard._serializeSelected""" @pytest.fixture(autouse=True) - def setup(self, mocker:MockerFixture): + def setup(self, mocker: MockerFixture): self.scene = mocker.MagicMock() self.view = mocker.MagicMock() @@ -44,23 +44,23 @@ def setup(self, mocker:MockerFixture): self.scene.sortedSelectedItems.return_value = self.blocks, self.edges self.clipboard = SceneClipboard(self.scene) - def test_serialize_selected_blocks(self, mocker:MockerFixture): - """ should allow for blocks serialization.""" + def test_serialize_selected_blocks(self, mocker: MockerFixture): + """should allow for blocks serialization.""" data = self.clipboard._serializeSelected() - check.equal(data['blocks'], [block.serialize() for block in self.blocks]) + check.equal(data["blocks"], [block.serialize() for block in self.blocks]) - def test_serialize_selected_edges(self, mocker:MockerFixture): - """ should allow for edges serialization.""" + def test_serialize_selected_edges(self, mocker: MockerFixture): + """should allow for edges serialization.""" data = self.clipboard._serializeSelected() - check.equal(data['edges'], [edge.serialize() for edge in self.edges]) + check.equal(data["edges"], [edge.serialize() for edge in self.edges]) - def test_serialize_partially_selected_edges(self, mocker:MockerFixture): - """ should not allow for partially selected edges serialization.""" + def test_serialize_partially_selected_edges(self, mocker: MockerFixture): + """should not allow for partially selected edges serialization.""" self.scene.sortedSelectedItems.return_value = self.blocks[0], self.edges data = self.clipboard._serializeSelected() - check.equal(data['edges'], [self.edges[0].serialize()]) + check.equal(data["edges"], [self.edges[0].serialize()]) - def test_serialize_delete(self, mocker:MockerFixture): - """ should allow for items deletion after serialization.""" + def test_serialize_delete(self, mocker: MockerFixture): + """should allow for items deletion after serialization.""" self.clipboard._serializeSelected(delete=True) check.is_true(self.view.deleteSelected.called) diff --git a/tests/unit/scene/test_function_parsing.py b/tests/unit/scene/test_function_parsing.py index d6e01cad..ec862ce4 100644 --- a/tests/unit/scene/test_function_parsing.py +++ b/tests/unit/scene/test_function_parsing.py @@ -4,12 +4,15 @@ from pytest_mock import MockerFixture import pytest_check as check -from opencodeblocks.graphics.function_parsing import (find_kwarg_index, run_cell, - get_function_name, - get_signature, - extract_args, - execute_function, - find_kwarg_index) +from opencodeblocks.graphics.function_parsing import ( + find_kwarg_index, + run_cell, + get_function_name, + get_signature, + extract_args, + execute_function, + find_kwarg_index, +) class TestFunctionParsing: @@ -17,72 +20,99 @@ class TestFunctionParsing: """Testing function_parsing functions""" def test_run_cell(self, mocker: MockerFixture): - """ Test run_cell """ - check.equal(run_cell("print(10)"), '10\n') + """Test run_cell""" + check.equal(run_cell("print(10)"), "10\n") def test_get_function_name(self, mocker: MockerFixture): - """ Test get_function_name """ - check.equal(get_function_name( - "def function():\n return 'Hello'"), 'function') - check.equal(get_function_name( - "#Hello\ndef function():\n return 'Hello'\na = 10"), 'function') - check.equal(get_function_name( - "#Hello\ndef function(a,b=10):\n return 'Hello'\na = 10"), 'function') + """Test get_function_name""" + check.equal(get_function_name("def function():\n return 'Hello'"), "function") + check.equal( + get_function_name("#Hello\ndef function():\n return 'Hello'\na = 10"), + "function", + ) + check.equal( + get_function_name( + "#Hello\ndef function(a,b=10):\n return 'Hello'\na = 10" + ), + "function", + ) def test_get_function_name_error(self, mocker: MockerFixture): - """ Return ValueError if get_function_name has wrong input """ + """Return ValueError if get_function_name has wrong input""" with pytest.raises(ValueError): get_function_name("") get_function_name("#Hello") get_function_name("def function") def test_get_signature(self, mocker: MockerFixture): - """ Test get_signature """ + """Test get_signature""" mocker.patch( - 'opencodeblocks.graphics.function_parsing.run_cell', return_value="(a, b, c=10)\n") - check.equal(get_signature( - "def function(a,b, c=10):\n return None"), "(a, b, c=10)\n") + "opencodeblocks.graphics.function_parsing.run_cell", + return_value="(a, b, c=10)\n", + ) + check.equal( + get_signature("def function(a,b, c=10):\n return None"), "(a, b, c=10)\n" + ) def test_find_kwarg_index(self, mocker: MockerFixture): - """ Test find_kwarg_index """ - check.equal(find_kwarg_index(['a', 'b', 'c=10']), 2) + """Test find_kwarg_index""" + check.equal(find_kwarg_index(["a", "b", "c=10"]), 2) check.equal(find_kwarg_index([]), 0) def test_extract_args(self, mocker: MockerFixture): - """ Test extract_args """ + """Test extract_args""" mocker.patch( - 'opencodeblocks.graphics.function_parsing.get_signature', return_value="()\n") + "opencodeblocks.graphics.function_parsing.get_signature", + return_value="()\n", + ) mocker.patch( - 'opencodeblocks.graphics.function_parsing.find_kwarg_index', return_value=0) - check.equal(extract_args( - "def function():\n return 'Hello'"), ([], [])) - mocker.patch('opencodeblocks.graphics.function_parsing.get_signature', - return_value="(a,b,c = 10)\n") + "opencodeblocks.graphics.function_parsing.find_kwarg_index", return_value=0 + ) + check.equal(extract_args("def function():\n return 'Hello'"), ([], [])) mocker.patch( - 'opencodeblocks.graphics.function_parsing.find_kwarg_index', return_value=2) - check.equal(extract_args( - "def function(a,b,c = 10):\n return 'Hello'"), (["a", "b"], ["c=10"])) + "opencodeblocks.graphics.function_parsing.get_signature", + return_value="(a,b,c = 10)\n", + ) + mocker.patch( + "opencodeblocks.graphics.function_parsing.find_kwarg_index", return_value=2 + ) + check.equal( + extract_args("def function(a,b,c = 10):\n return 'Hello'"), + (["a", "b"], ["c=10"]), + ) def test_extract_args_empty(self, mocker: MockerFixture): - """ Return a couple of empty lists if signature is empty """ - mocker.patch('opencodeblocks.graphics.function_parsing.get_signature', - return_value="()\n") + """Return a couple of empty lists if signature is empty""" + mocker.patch( + "opencodeblocks.graphics.function_parsing.get_signature", + return_value="()\n", + ) mocker.patch( - 'opencodeblocks.graphics.function_parsing.find_kwarg_index', return_value=None) - check.equal(extract_args( - "def function( ):\n return 'Hello'"), ([], [])) - mocker.patch('opencodeblocks.graphics.function_parsing.get_signature', - return_value="()\n") + "opencodeblocks.graphics.function_parsing.find_kwarg_index", + return_value=None, + ) + check.equal(extract_args("def function( ):\n return 'Hello'"), ([], [])) mocker.patch( - 'opencodeblocks.graphics.function_parsing.find_kwarg_index', return_value=None) - check.equal(extract_args( - "def function():\n return 'Hello'"), ([], [])) + "opencodeblocks.graphics.function_parsing.get_signature", + return_value="()\n", + ) + mocker.patch( + "opencodeblocks.graphics.function_parsing.find_kwarg_index", + return_value=None, + ) + check.equal(extract_args("def function():\n return 'Hello'"), ([], [])) def test_execute_function(self, mocker: MockerFixture): - """ Test execute_function """ - mocker.patch('opencodeblocks.graphics.function_parsing.get_function_name', - return_value="function") + """Test execute_function""" + mocker.patch( + "opencodeblocks.graphics.function_parsing.get_function_name", + return_value="function", + ) mocker.patch( - 'opencodeblocks.graphics.function_parsing.run_cell', return_value="Out[1]: 25\n") - check.equal(execute_function( - "def function(a,b,c=10):\n return a+b+c", 10, 5), "Out[1]: 25\n") + "opencodeblocks.graphics.function_parsing.run_cell", + return_value="Out[1]: 25\n", + ) + check.equal( + execute_function("def function(a,b,c=10):\n return a+b+c", 10, 5), + "Out[1]: 25\n", + ) diff --git a/tests/unit/scene/test_ipynb_conversion.py b/tests/unit/scene/test_ipynb_conversion.py new file mode 100644 index 00000000..88d1a4cf --- /dev/null +++ b/tests/unit/scene/test_ipynb_conversion.py @@ -0,0 +1,129 @@ +"""Unit tests for the conversion from and to ipynb.""" + +from typing import OrderedDict +from pytest_mock import MockerFixture +import pytest_check as check +import json + +from opencodeblocks.scene.from_ipynb_conversion import ipynb_to_ipyg, is_title +from opencodeblocks.scene.ipynb_conversion_constants import BLOCK_TYPE_TO_NAME + + +class TestIpynbConversion: + + """Conversion from .ipynb""" + + def test_empty_data(self, mocker: MockerFixture): + """should return empty ipyg graph for empty data.""" + 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.""" + file_path = "./tests/assets/empty.ipynb" + real_notebook_conversion_is_coherent(file_path) + + def test_usual_notebook_data(self, mocker: MockerFixture): + """should return expected graph for a real usual notebook data.""" + file_path = "./tests/assets/usual.ipynb" + real_notebook_conversion_is_coherent(file_path) + + def test_complex_notebook_data(self, mocker: MockerFixture): + """should return expected graph for a real complex notebook data.""" + file_path = "./tests/assets/complex.ipynb" + real_notebook_conversion_is_coherent(file_path) + + def test_is_title(self, mocker: MockerFixture): + """should return True iff the given text can be used as a title for a block.""" + check.equal(is_title(string_to_markdown_block("")), False) + check.equal(is_title(string_to_markdown_block("Data Preprocessing")), True) + check.equal(is_title(string_to_markdown_block("Étude de cas")), True) + check.equal(is_title(string_to_markdown_block("# Report")), False) + check.equal( + is_title( + string_to_markdown_block( + "This is a very very very very very very very very very very very very very very very very very very long explanation" + ) + ), + False, + ) + check.equal(is_title(string_to_markdown_block("New line \n Next line")), False) + + +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, False) + check_conversion_coherence(ipynb_data, ipyg_data) + + +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 + 1. blocks and edges + 2. the right amount of code blocks and edges + 3. blocks and sockets with unique ids + 4. edges with existing ids + 5. code blocks that always have a source + 6. markdown blocks that always have text + """ + + # blocks and edges are present + check.equal("blocks" in ipyg_data and "edges" in ipyg_data, True) + + # the amount of code blocks and edges is right + code_blocks_in_ipynb: int = 0 + for cell in ipynb_data["cells"]: + if cell["cell_type"] == "code": + code_blocks_in_ipynb += 1 + code_blocks_in_ipyg: int = 0 + for block in ipyg_data["blocks"]: + if block["block_type"] == BLOCK_TYPE_TO_NAME["code"]: + code_blocks_in_ipyg += 1 + check.equal(code_blocks_in_ipyg, code_blocks_in_ipynb) + + # blocks and sockets have unique ids + block_id_set = set([]) + socket_id_set = set([]) + for block in ipyg_data["blocks"]: + if "id" in block: + check.equal(block["id"] in block_id_set, False) + block_id_set.add(block["id"]) + if "sockets" in block: + for socket in block["sockets"]: + if "id" in socket: + check.equal(socket["id"] in socket_id_set, False) + socket_id_set.add(socket["id"]) + + # edges are between objects with existing ids + for edge in ipyg_data["edges"]: + check.equal(edge["source"]["block"] in block_id_set, True) + check.equal(edge["destination"]["block"] in block_id_set, True) + check.equal(edge["source"]["socket"] in socket_id_set, True) + check.equal(edge["destination"]["socket"] in socket_id_set, True) + + # code blocks always have a source and markdown blocks always have a text + for block in ipyg_data["blocks"]: + if block["block_type"] == BLOCK_TYPE_TO_NAME["code"]: + check.equal("source" in block and type(block["source"]) == str, True) + if block["block_type"] == BLOCK_TYPE_TO_NAME["markdown"]: + check.equal("text" in block and type(block["text"]) == str, True) + + +def load_json(file_path: str): + """Helper function that returns the ipynb data in a given file.""" + with open(file_path, "r", encoding="utf-8") as file: + data = json.loads(file.read()) + return data + + +def string_to_markdown_block(string: str): + """Helper function that returns the ipyg data necessary for the is_title function to work.""" + return { + "block_type": BLOCK_TYPE_TO_NAME["markdown"], + "text": string, + }