From 60b1cacc76a7dfbc41fcfa8d7631be70e532e98f Mon Sep 17 00:00:00 2001 From: AlexandreSajus Date: Wed, 8 Dec 2021 11:32:25 +0100 Subject: [PATCH 01/51] :tada: highlights running blocks --- opencodeblocks/blocks/block.py | 4 ++++ opencodeblocks/blocks/codeblock.py | 14 ++++++++++---- opencodeblocks/graphics/kernel.py | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/opencodeblocks/blocks/block.py b/opencodeblocks/blocks/block.py index 3a39bd9a..a3a57d8b 100644 --- a/opencodeblocks/blocks/block.py +++ b/opencodeblocks/blocks/block.py @@ -66,6 +66,7 @@ def __init__( self._pen_outline = QPen(QColor("#7F000000")) self._pen_outline_selected = QPen(QColor("#FFFFA637")) + self._pen_outline_running = QPen(QColor("#00FF00")) self._brush_background = QBrush(BACKGROUND_COLOR) self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable) @@ -133,6 +134,9 @@ def paint( painter.setPen( self._pen_outline_selected if self.isSelected() else self._pen_outline ) + if hasattr(self, "running"): + if self.running: + painter.setPen(self._pen_outline_running) painter.setBrush(Qt.BrushStyle.NoBrush) painter.drawPath(path_outline.simplified()) diff --git a/opencodeblocks/blocks/codeblock.py b/opencodeblocks/blocks/codeblock.py index ae8c9fc1..c60ca715 100644 --- a/opencodeblocks/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -46,6 +46,7 @@ def __init__(self, **kwargs): self._splitter_size = [1, 1] self._cached_stdout = "" self.has_been_run = False + self.running = False # Add exectution flow sockets exe_sockets = ( @@ -82,14 +83,16 @@ 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.setFixedSize(int(3 * self.edge_size), + int(3 * self.edge_size)) run_button.clicked.connect(self.run_left) return run_button def init_run_all_button(self): """Initialize the run all button""" run_all_button = QPushButton(">>", self.root) - run_all_button.setFixedSize(int(3 * self.edge_size), int(3 * self.edge_size)) + run_all_button.setFixedSize( + int(3 * self.edge_size), int(3 * self.edge_size)) run_all_button.clicked.connect(self.run_right) run_all_button.raise_() @@ -97,6 +100,8 @@ def init_run_all_button(self): def run_code(self): """Run the code in the block""" + self.running = True + # Reset stdout self._cached_stdout = "" @@ -113,8 +118,9 @@ def run_code(self): kernel.run_queue() self.has_been_run = True - def reset_buttons(self): - """Reset the text of the run buttons""" + def reset_after_run(self): + """Reset buttons and color after a run""" + self.running = False self.run_button.setText(">") self.run_all_button.setText(">>") diff --git a/opencodeblocks/graphics/kernel.py b/opencodeblocks/graphics/kernel.py index 7563d9fa..041d31a0 100644 --- a/opencodeblocks/graphics/kernel.py +++ b/opencodeblocks/graphics/kernel.py @@ -64,7 +64,7 @@ def run_block(self, block, code: str): worker.signals.stdout.connect(block.handle_stdout) worker.signals.image.connect(block.handle_image) worker.signals.finished.connect(self.run_queue) - worker.signals.finished_block.connect(block.reset_buttons) + worker.signals.finished_block.connect(block.reset_after_run) block.source_editor.threadpool.start(worker) def run_queue(self): From 8b2e96bb69c9d599b200fb0ceee9a409e8a7ed01 Mon Sep 17 00:00:00 2001 From: AlexandreSajus Date: Thu, 9 Dec 2021 16:02:49 +0100 Subject: [PATCH 02/51] :beetle: adds reset_buttons --- opencodeblocks/blocks/codeblock.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/opencodeblocks/blocks/codeblock.py b/opencodeblocks/blocks/codeblock.py index 1673ac1d..924fd491 100644 --- a/opencodeblocks/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -207,6 +207,12 @@ def reset_has_been_run(self): """ Reset has_been_run, is called when the output is an error """ self.has_been_run = False + def reset_buttons(self): + """Reset the buttons""" + self.run_button.setText(">") + self.run_all_button.setText(">>") + self.running = False + def update_title(self): """Change the geometry of the title widget""" self.title_widget.setGeometry( From 61c1a72e18e59f24a48df1a4d103ef9c965febbc Mon Sep 17 00:00:00 2001 From: AlexandreSajus Date: Thu, 9 Dec 2021 17:42:07 +0100 Subject: [PATCH 03/51] :hammer: black instead of autopep8 --- opencodeblocks/blocks/block.py | 7 +- opencodeblocks/blocks/codeblock.py | 42 ++++++---- opencodeblocks/graphics/edge.py | 129 ++++++++++++++++++----------- opencodeblocks/scene/scene.py | 112 +++++++++++++------------ 4 files changed, 167 insertions(+), 123 deletions(-) diff --git a/opencodeblocks/blocks/block.py b/opencodeblocks/blocks/block.py index eff610b1..ccc8074c 100644 --- a/opencodeblocks/blocks/block.py +++ b/opencodeblocks/blocks/block.py @@ -66,7 +66,6 @@ def __init__( self._pen_outline = QPen(QColor("#7F000000")) self._pen_outline_selected = QPen(QColor("#FFFFA637")) - self._pen_outline_running = QPen(QColor("#00FF00")) self._brush_background = QBrush(BACKGROUND_COLOR) self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable) @@ -134,9 +133,6 @@ def paint( painter.setPen( self._pen_outline_selected if self.isSelected() else self._pen_outline ) - if hasattr(self, "running"): - if self.running: - painter.setPen(self._pen_outline_running) painter.setBrush(Qt.BrushStyle.NoBrush) painter.drawPath(path_outline.simplified()) @@ -154,8 +150,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 924fd491..e3bd374a 100644 --- a/opencodeblocks/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -3,8 +3,10 @@ """ Module for the base OCB Code Block. """ -from typing import List, OrderedDict -from PyQt5.QtWidgets import QPushButton, QTextEdit +from typing import List, OrderedDict, Optional +from PyQt5.QtWidgets import QPushButton, QTextEdit, QWidget, QStyleOptionGraphicsItem +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QPen, QColor, QPainter, QPainterPath from ansi2html import Ansi2HTMLConverter from networkx.algorithms.traversal.breadth_first_search import bfs_edges @@ -46,7 +48,16 @@ def __init__(self, **kwargs): self._splitter_size = [1, 1] self._cached_stdout = "" self.has_been_run = False - self.running = False + + self.run_color = 0 + self._pen_outline = QPen(QColor("#7F000000")) + self._pen_outline_running = QPen(QColor("#FF0000")) + self._pen_outline_transmitting = QPen(QColor("#FFFFA637")) + self._pen_outlines = [ + self._pen_outline, + self._pen_outline_running, + self._pen_outline_transmitting, + ] # Add exectution flow sockets exe_sockets = ( @@ -83,16 +94,14 @@ 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.setFixedSize(int(3 * self.edge_size), int(3 * self.edge_size)) run_button.clicked.connect(self.run_left) return run_button def init_run_all_button(self): """Initialize the run all button""" run_all_button = QPushButton(">>", self.root) - run_all_button.setFixedSize( - int(3 * self.edge_size), int(3 * self.edge_size)) + run_all_button.setFixedSize(int(3 * self.edge_size), int(3 * self.edge_size)) run_all_button.clicked.connect(self.run_right) run_all_button.raise_() @@ -100,7 +109,7 @@ def init_run_all_button(self): def run_code(self): """Run the code in the block""" - self.running = True + self.run_color = 1 # Reset stdout self._cached_stdout = "" @@ -120,7 +129,7 @@ def run_code(self): def reset_after_run(self): """Reset buttons and color after a run""" - self.running = False + self.run_color = 0 self.run_button.setText(">") self.run_all_button.setText(">>") @@ -139,7 +148,7 @@ def has_output(self) -> bool: return False def _interrupt_execution(self): - """ Interrupt an execution, reset the blocks in the queue """ + """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() @@ -153,7 +162,7 @@ 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 the user presses left run when run_color, cancel the execution if self.run_button.text() == "..." and not in_right_button: self._interrupt_execution() return @@ -186,7 +195,7 @@ def run_left(self, in_right_button=False): 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 the user presses right run when run_color, cancel the execution if self.run_all_button.text() == "...": self._interrupt_execution() return @@ -195,23 +204,22 @@ def run_right(self): 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 + # Same as run_left but instead of run_color 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] + 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 """ + """Reset has_been_run, is called when the output is an error""" self.has_been_run = False def reset_buttons(self): """Reset the buttons""" self.run_button.setText(">") self.run_all_button.setText(">>") - self.running = False + self.run_color = 0 def update_title(self): """Change the geometry of the title widget""" diff --git a/opencodeblocks/graphics/edge.py b/opencodeblocks/graphics/edge.py index e4b33722..0f50fd21 100644 --- a/opencodeblocks/graphics/edge.py +++ b/opencodeblocks/graphics/edge.py @@ -17,14 +17,20 @@ class OCBEdge(QGraphicsPathItem, Serializable): - """ Base class for directed edges in OpenCodeBlocks. """ - - def __init__(self, edge_width: float = 4.0, path_type='bezier', - edge_color="#001000", edge_selected_color="#00ff00", - source: QPointF = QPointF(0, 0), destination: QPointF = QPointF(0, 0), - source_socket: OCBSocket = None, destination_socket: OCBSocket = None - ): - """ Base class for edges in OpenCodeBlocks. + """Base class for directed edges in OpenCodeBlocks.""" + + def __init__( + self, + edge_width: float = 4.0, + path_type="bezier", + edge_color="#001000", + edge_selected_color="#00ff00", + source: QPointF = QPointF(0, 0), + destination: QPointF = QPointF(0, 0), + source_socket: OCBSocket = None, + destination_socket: OCBSocket = None, + ): + """Base class for edges in OpenCodeBlocks. Args: edge_width: Width of the edge. @@ -62,35 +68,38 @@ def __init__(self, edge_width: float = 4.0, path_type='bezier', self._destination = destination self.update_path() - def remove_from_socket(self, socket_type='source'): - """ Remove the edge from the sockets it is snaped to on the given socket_type. + def remove_from_socket(self, socket_type="source"): + """Remove the edge from the sockets it is snaped to on the given socket_type. Args: socket_type: One of ('source', 'destination'). """ - socket_name = f'{socket_type}_socket' + socket_name = f"{socket_type}_socket" socket = getattr(self, socket_name, OCBSocket) if socket is not None: socket.remove_edge(self) setattr(self, socket_name, None) def remove_from_sockets(self): - """ Remove the edge from all sockets it is snaped to. """ - self.remove_from_socket('source') - self.remove_from_socket('destination') + """Remove the edge from all sockets it is snaped to.""" + self.remove_from_socket("source") + self.remove_from_socket("destination") def remove(self): - """ Remove the edge from the scene in which it is drawn. """ + """Remove the edge from the scene in which it is drawn.""" scene = self.scene() if scene is not None: self.remove_from_sockets() scene.removeItem(self) - def paint(self, painter: QPainter, - option: QStyleOptionGraphicsItem, # pylint:disable=unused-argument - widget: Optional[QWidget] = None): # pylint:disable=unused-argument - """ Paint the edge. """ + def paint( + self, + painter: QPainter, + option: QStyleOptionGraphicsItem, # pylint:disable=unused-argument + widget: Optional[QWidget] = None, + ): # 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) @@ -98,22 +107,22 @@ def paint(self, painter: QPainter, painter.drawPath(self.path()) def update_path(self): - """ Update the edge path depending on the path_type. """ + """Update the edge path depending on the path_type.""" path = QPainterPath(self.source) - if self.path_type == 'direct': + if self.path_type == "direct": path.lineTo(self.destination) - elif self.path_type == 'bezier': + elif self.path_type == "bezier": sx, sy = self.source.x(), self.source.y() dx, dy = self.destination.x(), self.destination.y() mid_dist = (dx - sx) / 2 path.cubicTo(sx + mid_dist, sy, dx - mid_dist, dy, dx, dy) else: - raise NotImplementedError(f'Unknowed path type: {self.path_type}') + raise NotImplementedError(f"Unknowed path type: {self.path_type}") self.setPath(path) @property def source(self) -> QPointF: - """ Source point of the directed edge. """ + """Source point of the directed edge.""" if self.source_socket is not None: return self.source_socket.scenePos() return self._source @@ -128,7 +137,7 @@ def source(self, value: QPointF): @property def source_socket(self) -> OCBSocket: - """ Source socket of the directed edge. """ + """Source socket of the directed edge.""" return self._source_socket @source_socket.setter @@ -140,7 +149,7 @@ def source_socket(self, value: OCBSocket): @property def destination(self) -> QPointF: - """ Destination point of the directed edge. """ + """Destination point of the directed edge.""" if self.destination_socket is not None: return self.destination_socket.scenePos() return self._destination @@ -155,7 +164,7 @@ def destination(self, value: QPointF): @property def destination_socket(self) -> OCBSocket: - """ Destination socket of the directed edge. """ + """Destination socket of the directed edge.""" return self._destination_socket @destination_socket.setter @@ -166,32 +175,58 @@ def destination_socket(self, value: OCBSocket): self.destination = value.scenePos() def serialize(self) -> OrderedDict: - return OrderedDict([ - ('id', self.id), - ('path_type', self.path_type), - ('source', OrderedDict([ - ('block', - self.source_socket.block.id if self.source_socket else None), - ('socket', - self.source_socket.id if self.source_socket else None) - ])), - ('destination', OrderedDict([ - ('block', - self.destination_socket.block.id if self.destination_socket else None), - ('socket', - self.destination_socket.id if self.destination_socket else None) - ])) - ]) + return OrderedDict( + [ + ("id", self.id), + ("path_type", self.path_type), + ( + "source", + OrderedDict( + [ + ( + "block", + self.source_socket.block.id + if self.source_socket + else None, + ), + ( + "socket", + self.source_socket.id if self.source_socket else None, + ), + ] + ), + ), + ( + "destination", + OrderedDict( + [ + ( + "block", + self.destination_socket.block.id + if self.destination_socket + else None, + ), + ( + "socket", + self.destination_socket.id + if self.destination_socket + else None, + ), + ] + ), + ), + ] + ) def deserialize(self, data: OrderedDict, hashmap: dict = None, restore_id=True): if restore_id: - self.id = data['id'] - self.path_type = data['path_type'] + self.id = data["id"] + self.path_type = data["path_type"] try: - self.source_socket = hashmap[data['source']['socket']] + self.source_socket = hashmap[data["source"]["socket"]] self.source_socket.add_edge(self, is_destination=False) - self.destination_socket = hashmap[data['destination']['socket']] + self.destination_socket = hashmap[data["destination"]["socket"]] self.destination_socket.add_edge(self, is_destination=True) self.update_path() except KeyError: diff --git a/opencodeblocks/scene/scene.py b/opencodeblocks/scene/scene.py index 9069e309..ff2b85cb 100644 --- a/opencodeblocks/scene/scene.py +++ b/opencodeblocks/scene/scene.py @@ -25,13 +25,19 @@ class OCBScene(QGraphicsScene, Serializable): - """ Scene for the OCB Window. """ - - def __init__(self, parent=None, - background_color: str = "#393939", - grid_color: str = "#292929", grid_light_color: str = "#2f2f2f", - width: int = 64000, height: int = 64000, - grid_size: int = 20, grid_squares: int = 5): + """Scene for the OCB Window.""" + + def __init__( + self, + parent=None, + background_color: str = "#393939", + grid_color: str = "#292929", + grid_light_color: str = "#2f2f2f", + width: int = 64000, + height: int = 64000, + grid_size: int = 20, + grid_squares: int = 5, + ): Serializable.__init__(self) QGraphicsScene.__init__(self, parent=parent) @@ -42,8 +48,7 @@ def __init__(self, parent=None, self.grid_squares = grid_squares self.width, self.height = width, height - self.setSceneRect(-self.width // 2, -self.height // - 2, self.width, self.height) + self.setSceneRect(-self.width // 2, -self.height // 2, self.width, self.height) self.setBackgroundBrush(self._background_color) self._has_been_modified = False @@ -54,7 +59,7 @@ def __init__(self, parent=None, @property def has_been_modified(self): - """ True if the scene has been modified, False otherwise. """ + """True if the scene has been modified, False otherwise.""" return self._has_been_modified @has_been_modified.setter @@ -64,11 +69,11 @@ def has_been_modified(self, value: bool): callback() def addHasBeenModifiedListener(self, callback: FunctionType): - """ Add a callback that will trigger when the scene has been modified. """ + """Add a callback that will trigger when the scene has been modified.""" self._has_been_modified_listeners.append(callback) def sortedSelectedItems(self) -> List[Union[OCBBlock, OCBEdge]]: - """ Returns the selected blocks and selected edges in two separate lists. """ + """Returns the selected blocks and selected edges in two separate lists.""" selected_blocks, selected_edges = [], [] for item in self.selectedItems(): if isinstance(item, OCBBlock): @@ -78,12 +83,12 @@ def sortedSelectedItems(self) -> List[Union[OCBBlock, OCBEdge]]: return selected_blocks, selected_edges def drawBackground(self, painter: QPainter, rect: QRectF): - """ Draw the Scene background """ + """Draw the Scene background""" super().drawBackground(painter, rect) self.drawGrid(painter, rect) def drawGrid(self, painter: QPainter, rect: QRectF): - """ Draw the background grid """ + """Draw the background grid""" left = int(math.floor(rect.left())) top = int(math.floor(rect.top())) right = int(math.ceil(rect.right())) @@ -118,51 +123,51 @@ def drawGrid(self, painter: QPainter, rect: QRectF): painter.drawLines(*lines_light) def save(self, filepath: str): - """ Save the scene into filepath. """ + """Save the scene into filepath.""" self.save_to_ipyg(filepath) self.has_been_modified = False def save_to_ipyg(self, filepath: str): - """ Save the scene into filepath as interactive python graph (.ipyg). """ - if '.' not in filepath: - filepath += '.ipyg' + """Save the scene into filepath as interactive python graph (.ipyg).""" + if "." not in filepath: + filepath += ".ipyg" - extention_format = filepath.split('.')[-1] - if extention_format != 'ipyg': + extention_format = filepath.split(".")[-1] + if extention_format != "ipyg": raise NotImplementedError(f"Unsupported format {extention_format}") - with open(filepath, 'w', encoding='utf-8') as file: + with open(filepath, "w", encoding="utf-8") as file: file.write(json.dumps(self.serialize(), indent=4)) def load(self, filepath: str): - """ Load a saved scene. + """Load a saved scene. Args: filepath: Path to the file to load. """ - if filepath.endswith('.ipyg'): + if filepath.endswith(".ipyg"): data = self.load_from_ipyg(filepath) else: - extention_format = filepath.split('.')[-1] + extention_format = filepath.split(".")[-1] raise NotImplementedError(f"Unsupported format {extention_format}") self.deserialize(data) self.history.checkpoint("Loaded scene") self.has_been_modified = False def load_from_ipyg(self, filepath: str): - """ Load an interactive python graph (.ipyg) into the scene. + """Load an interactive python graph (.ipyg) into the scene. Args: filepath: Path to the .ipyg file to load. """ - with open(filepath, 'r', encoding='utf-8') as file: + with open(filepath, "r", encoding="utf-8") as file: data = json.loads(file.read()) return data def clear(self): - """ Clear the scene from all items. """ + """Clear the scene from all items.""" self.has_been_modified = False return super().clear() @@ -176,36 +181,37 @@ def serialize(self) -> OrderedDict: edges.append(item) blocks.sort(key=lambda x: x.id) edges.sort(key=lambda x: x.id) - return OrderedDict([ - ('id', self.id), - ('blocks', [block.serialize() for block in blocks]), - ('edges', [edge.serialize() for edge in edges]), - ]) + return OrderedDict( + [ + ("id", self.id), + ("blocks", [block.serialize() for block in blocks]), + ("edges", [edge.serialize() for edge in edges]), + ] + ) def create_graph(self) -> nx.DiGraph: - """ Create a networkx graph from the scene. """ + """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) + 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: + 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: data = json.loads(file.read()) data["position"] = [x, y] data["sockets"] = {} self.create_block(data, None, False) - def create_block(self, data: OrderedDict, hashmap: dict = None, - restore_id: bool = True) -> OCBBlock: - """ Create a new block from an OrderedDict """ + def create_block( + self, data: OrderedDict, hashmap: dict = None, restore_id: bool = True + ) -> OCBBlock: + """Create a new block from an OrderedDict""" block = None @@ -215,34 +221,34 @@ def create_block(self, data: OrderedDict, hashmap: dict = None, for block_name in block_files: block_module = getattr(blocks, block_name) if isinstance(block_module, ModuleType): - if hasattr(block_module, data['block_type']): - block_constructor = getattr(blocks, data['block_type']) + if hasattr(block_module, data["block_type"]): + block_constructor = getattr(blocks, data["block_type"]) if block_constructor is None: - raise NotImplementedError( - f"{data['block_type']} is not a known block type") + raise NotImplementedError(f"{data['block_type']} is not a known block type") block = block_constructor() block.deserialize(data, hashmap, restore_id) self.addItem(block) if hashmap is not None: - hashmap.update({data['id']: block}) + hashmap.update({data["id"]: block}) return block - def deserialize(self, data: OrderedDict, - hashmap: dict = None, restore_id: bool = True): + def deserialize( + self, data: OrderedDict, hashmap: dict = None, restore_id: bool = True + ): self.clear() hashmap = hashmap if hashmap is not None else {} if restore_id: - self.id = data['id'] + self.id = data["id"] # Create blocks - for block_data in data['blocks']: + for block_data in data["blocks"]: self.create_block(block_data, hashmap, restore_id) # Create edges - for edge_data in data['edges']: + for edge_data in data["edges"]: edge = OCBEdge() edge.deserialize(edge_data, hashmap, restore_id) self.addItem(edge) - hashmap.update({edge_data['id']: edge}) + hashmap.update({edge_data["id"]: edge}) From d96d70f80d334ef2d2a351fd4f1972f238cb88e9 Mon Sep 17 00:00:00 2001 From: AlexandreSajus Date: Thu, 9 Dec 2021 17:43:42 +0100 Subject: [PATCH 04/51] :tada: easier coloring of objects --- opencodeblocks/blocks/codeblock.py | 28 +++++++++++++++ opencodeblocks/graphics/codeedge.py | 54 +++++++++++++++++++++++++++++ opencodeblocks/scene/scene.py | 3 +- 3 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 opencodeblocks/graphics/codeedge.py diff --git a/opencodeblocks/blocks/codeblock.py b/opencodeblocks/blocks/codeblock.py index e3bd374a..89dd9d49 100644 --- a/opencodeblocks/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -251,6 +251,34 @@ def update_all(self): self.update_output_panel() self.update_run_all_button() + def paint( + self, + painter: QPainter, + option: QStyleOptionGraphicsItem, + widget: Optional[QWidget] = None, + ): + 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""" diff --git a/opencodeblocks/graphics/codeedge.py b/opencodeblocks/graphics/codeedge.py new file mode 100644 index 00000000..0b936082 --- /dev/null +++ b/opencodeblocks/graphics/codeedge.py @@ -0,0 +1,54 @@ +# OpenCodeBlock an open-source tool for modular visual programing in python +# Copyright (C) 2021 Mathïs FEDERICO + +""" Module for the OCB Edge. """ + +from __future__ import annotations + +from typing import Optional + +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QColor, QPainter, QPen +from PyQt5.QtWidgets import QStyleOptionGraphicsItem, QWidget + +from opencodeblocks.graphics.edge import OCBEdge + + +class OCBCodeEdge(OCBEdge): + + """Base class for directed edges in OpenCodeBlocks.""" + + def __init__( + self, + edge_running_color: str = "#FF0000", + edge_transmitting_color: str = "#FFFFA637", + ): + super().__init__() + self.edge_running_color = edge_running_color + self.edge_transmitting_color = edge_transmitting_color + + self.run_color = 0 + self._pen_outline = QPen(QColor("#7F000000")) + self._pen_outline_running = QPen(QColor("#FF0000")) + self._pen_outline_transmitting = QPen(QColor("#FFFFA637")) + self._pen_outlines = [ + self._pen_outline, + self._pen_outline_running, + self._pen_outline_transmitting, + ] + + def paint( + self, + painter: QPainter, + option: QStyleOptionGraphicsItem, # pylint:disable=unused-argument + widget: Optional[QWidget] = None, + ): # pylint:disable=unused-argument + """Paint the edge.""" + self.update_path() + painter.setPen( + self._pen_selected + if self.isSelected() + else self._pen_outlines[self.run_color] + ) + painter.setBrush(Qt.BrushStyle.NoBrush) + painter.drawPath(self.path()) diff --git a/opencodeblocks/scene/scene.py b/opencodeblocks/scene/scene.py index ff2b85cb..f82e535c 100644 --- a/opencodeblocks/scene/scene.py +++ b/opencodeblocks/scene/scene.py @@ -17,6 +17,7 @@ from opencodeblocks.core.serializable import Serializable from opencodeblocks.blocks.block import OCBBlock from opencodeblocks.graphics.edge import OCBEdge +from opencodeblocks.graphics.codeedge import OCBCodeEdge from opencodeblocks.scene.clipboard import SceneClipboard from opencodeblocks.scene.history import SceneHistory @@ -248,7 +249,7 @@ def deserialize( # Create edges for edge_data in data["edges"]: - edge = OCBEdge() + edge = OCBCodeEdge() edge.deserialize(edge_data, hashmap, restore_id) self.addItem(edge) hashmap.update({edge_data["id"]: edge}) From ee02258499daee88557ae4171dd076b53a7017ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Sat, 11 Dec 2021 21:35:29 +0100 Subject: [PATCH 05/51] =?UTF-8?q?=F0=9F=94=A8=20Add=20the=20concept=20of?= =?UTF-8?q?=20an=20executable=20block=20as=20not=20all=20block=20can=20be?= =?UTF-8?q?=20executed.=20This=20will=20be=20used=20to=20make=20sliders=20?= =?UTF-8?q?"executable".?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- opencodeblocks/blocks/__init__.py | 8 +- opencodeblocks/blocks/block.py | 4 - opencodeblocks/blocks/codeblock.py | 113 +------------ opencodeblocks/blocks/containerblock.py | 86 ++++++++++ opencodeblocks/blocks/executableblock.py | 201 +++++++++++++++++++++++ opencodeblocks/blocks/sliderblock.py | 21 ++- opencodeblocks/graphics/kernel.py | 15 +- opencodeblocks/graphics/pyeditor.py | 11 +- opencodeblocks/graphics/worker.py | 1 - 9 files changed, 334 insertions(+), 126 deletions(-) create mode 100644 opencodeblocks/blocks/containerblock.py create mode 100644 opencodeblocks/blocks/executableblock.py diff --git a/opencodeblocks/blocks/__init__.py b/opencodeblocks/blocks/__init__.py index 82fed7bc..93e87f76 100644 --- a/opencodeblocks/blocks/__init__.py +++ b/opencodeblocks/blocks/__init__.py @@ -3,8 +3,14 @@ """ Module for the OCB Blocks of different types. """ -from opencodeblocks.blocks.sliderblock import OCBSliderBlock +# Abstract blocks from opencodeblocks.blocks.block import OCBBlock +from opencodeblocks.blocks.executableblock import OCBExecutableBlock + +# Real blocks +from opencodeblocks.blocks.sliderblock import OCBSliderBlock from opencodeblocks.blocks.codeblock import OCBCodeBlock from opencodeblocks.blocks.markdownblock import OCBMarkdownBlock from opencodeblocks.blocks.drawingblock import OCBDrawingBlock + + diff --git a/opencodeblocks/blocks/block.py b/opencodeblocks/blocks/block.py index 569d97b1..f714b94f 100644 --- a/opencodeblocks/blocks/block.py +++ b/opencodeblocks/blocks/block.py @@ -33,7 +33,6 @@ class OCBBlock(QGraphicsItem, Serializable): def __init__( self, block_type: str = "base", - source: str = "", position: tuple = (0, 0), width: int = 300, height: int = 200, @@ -45,7 +44,6 @@ def __init__( Args: block_type: Block type. - source: Block source text. position: Block position in the scene. width: Block width. height: Block height. @@ -58,8 +56,6 @@ def __init__( Serializable.__init__(self) self.block_type = block_type - self.source = source - self.stdout = "" self.setPos(QPointF(*position)) self.sockets_in = [] self.sockets_out = [] diff --git a/opencodeblocks/blocks/codeblock.py b/opencodeblocks/blocks/codeblock.py index ae3e6c21..01824e3c 100644 --- a/opencodeblocks/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -9,14 +9,14 @@ 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 @@ -33,10 +33,10 @@ def __init__(self, **kwargs): Create a new OCBCodeBlock. Initialize all the child widgets specific to this block type """ + super().__init__(**kwargs) self.source_editor = PythonEditor(self) self._source = "" - - super().__init__(**kwargs) + self._stdout = "" self.output_panel_height = self.height / 3 self._min_output_panel_height = 20 @@ -44,16 +44,6 @@ def __init__(self, **kwargs): self.output_closed = True self._splitter_size = [1, 1] - self._cached_stdout = "" - self.has_been_run = False - - # Add exectution flow sockets - exe_sockets = ( - OCBSocket(self, socket_type="input", flow_type="exe"), - OCBSocket(self, socket_type="output", flow_type="exe"), - ) - for socket in exe_sockets: - self.add_socket(socket) # Add output pannel self.output_panel = self.init_output_panel() @@ -106,103 +96,14 @@ def run_code(self): self.run_button.setText("...") self.run_all_button.setText("...") - # Run code by adding to code to queue - code = self.source_editor.text() - self.source = code - kernel = self.source_editor.kernel - kernel.execution_queue.append((self, code)) - if kernel.busy is False: - kernel.run_queue() - self.has_been_run = True + super().run_code() # actually run the code - def reset_buttons(self): + def execution_finished(self): """Reset the text of the run buttons""" + super().execution_finished() self.run_button.setText(">") self.run_all_button.setText(">>") - def has_input(self) -> bool: - """Checks whether a block has connected input blocks""" - for input_socket in self.sockets_in: - if len(input_socket.edges) != 0: - return True - return False - - def has_output(self) -> bool: - """Checks whether a block has connected output blocks""" - for output_socket in self.sockets_out: - if len(output_socket.edges) != 0: - return True - return False - - def _interrupt_execution(self): - """ Interrupt an execution, reset the blocks in the queue """ - for block, _ in self.source_editor.kernel.execution_queue: - # Reset the blocks that have not been run - block.reset_buttons() - block.has_been_run = False - # Clear the queue - self.source_editor.kernel.execution_queue = [] - # Interrupt the kernel - self.source_editor.kernel.kernel_manager.interrupt_kernel() - - def run_left(self, in_right_button=False): - """ - Run all of the block's dependencies and then run the block - """ - # If the user presses left run when running, cancel the execution - if self.run_button.text() == "..." and not in_right_button: - self._interrupt_execution() - return - - # If no dependencies - if not self.has_input(): - return self.run_code() - - # Create the graph from the scene - graph = self.scene().create_graph() - # BFS through the input graph - edges = bfs_edges(graph, self, reverse=True) - # Run the blocks found except self - blocks_to_run: List["OCBCodeBlock"] = [v for _, v in edges] - for block in blocks_to_run[::-1]: - if not block.has_been_run: - block.run_code() - - if in_right_button: - # If run_left was called inside of run_right - # self is not necessarily the block that was clicked - # which means that self does not need to be run - if not self.has_been_run: - self.run_code() - else: - # On the contrary if run_left was called outside of run_right - # self is the block that was clicked - # so self needs to be run - self.run_code() - - def run_right(self): - """Run all of the output blocks and all their dependencies""" - # If the user presses right run when running, cancel the execution - if self.run_all_button.text() == "...": - self._interrupt_execution() - return - - # If no output, run left - if not self.has_output(): - return self.run_left(in_right_button=True) - - # Same as run_left but instead of running the blocks, we'll use run_left - graph = self.scene().create_graph() - edges = bfs_edges(graph, self) - blocks_to_run: List["OCBCodeBlock"] = [ - self] + [v for _, v in edges] - for block in blocks_to_run[::-1]: - block.run_left(in_right_button=True) - - def reset_has_been_run(self): - """ Reset has_been_run, is called when the output is an error """ - self.has_been_run = False - def update_title(self): """Change the geometry of the title widget""" self.title_widget.setGeometry( diff --git a/opencodeblocks/blocks/containerblock.py b/opencodeblocks/blocks/containerblock.py new file mode 100644 index 00000000..21505b78 --- /dev/null +++ b/opencodeblocks/blocks/containerblock.py @@ -0,0 +1,86 @@ +# OpenCodeBlock an open-source tool for modular visual programing in python + +""" +Exports OCBSliderBlock. +""" + +from typing import OrderedDict +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QHBoxLayout, QLabel, QLineEdit, QSlider, QVBoxLayout +from opencodeblocks.blocks.block import OCBBlock + + +class OCBSliderBlock(OCBBlock): + """ + Features a slider ranging from 0 to 1 and an area to choose what value to assign the slider to. + """ + + def __init__(self, **kwargs): + super().__init__(**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_text = QLineEdit("slider_value") + self.variable_value = QLabel(f"{self.slider.value()/100}") + + self.variable_text.setFixedWidth(self.root.width() / 2) + + self.variable_layout.addWidget(self.variable_text) + self.variable_layout.addWidget(self.variable_value) + + self.layout.setContentsMargins( + self.edge_size * 2, + self.title_widget.height() + self.edge_size * 2, + self.edge_size * 2, + self.edge_size * 2 + ) + self.layout.addWidget(self.slider) + self.layout.addLayout(self.variable_layout) + + 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}") + + # The code execution part will be added when the execution flow is merged. + # We print for now + print(python_code) + + @property + def value(self): + """ The value of the slider """ + return str(self.slider.value() / 100) + @value.setter + def value(self, value: str): + self.slider.setValue(int(float(value) * 100)) + + @property + def var_name(self): + """ The name of the python variable associated with the slider """ + return self.variable_text.text() + @var_name.setter + def var_name(self, value: str): + self.variable_text.setText(value) + + def serialize(self): + """ Return a serialized version of this widget """ + base_dict = super().serialize() + base_dict["value"] = self.value + base_dict["var_name"] = self.var_name + + return base_dict + + def deserialize(self, data: OrderedDict, + hashmap: dict = None, restore_id: bool = True): + """ Restore a slider block from it's serialized state """ + for dataname in ['value','var_name']: + if dataname in data: + setattr(self, dataname, data[dataname]) + + super().deserialize(data, hashmap, restore_id) diff --git a/opencodeblocks/blocks/executableblock.py b/opencodeblocks/blocks/executableblock.py new file mode 100644 index 00000000..4420abad --- /dev/null +++ b/opencodeblocks/blocks/executableblock.py @@ -0,0 +1,201 @@ +""" Module for the executable block class """ + +from typing import List, OrderedDict + +from ansi2html import Ansi2HTMLConverter +from networkx.algorithms.traversal.breadth_first_search import bfs_edges + +from opencodeblocks.blocks.block import OCBBlock +from opencodeblocks.graphics.socket import OCBSocket + +from opencodeblocks.graphics.kernel import get_main_kernel + +conv = Ansi2HTMLConverter() + + +class OCBExecutableBlock(OCBBlock): + + """ + Executable Block + + This block type is not meant to be instanciated ! + + It's an abstract class that represents blocks that can be executed like: + - OCBCodeBlock + - OCBSlider + + """ + + def __init__(self, **kwargs): + """ + Create a new executable block. + Do not call this method except when inheriting from this class. + """ + super().__init__(**kwargs) + + self.has_been_run = False + self.is_running = False + + # Add execution flow sockets + exe_sockets = ( + OCBSocket(self, socket_type="input", flow_type="exe"), + OCBSocket(self, socket_type="output", flow_type="exe"), + ) + for socket in exe_sockets: + self.add_socket(socket) + + if type(self) == OCBExecutableBlock: + raise RuntimeError("OCBExecutableBlock should not be instanciated directly") + + def has_input(self) -> bool: + """Checks whether a block has connected input blocks""" + for input_socket in self.sockets_in: + if len(input_socket.edges) != 0: + return True + return False + + def has_output(self) -> bool: + """Checks whether a block has connected output blocks""" + for output_socket in self.sockets_out: + if len(output_socket.edges) != 0: + return True + return False + + def run_code(self): + """ Run the code in the block""" + + # Queue the code to execute + code = self.source + kernel = get_main_kernel() + kernel.execution_queue.append((self, code)) + + self.is_running = True + + if kernel.busy is False: + kernel.run_queue() + self.has_been_run = True + + def execution_finished(self): + """ + Method called when the execution of the block is finished. + Implement the behavior you want here. + """ + self.is_running = False + + def _interrupt_execution(self): + """ Interrupt an execution, reset the blocks in the queue """ + kernel = get_main_kernel() + for block, _ in kernel.execution_queue: + # Reset the blocks that have not been run + block.execution_finished() + block.has_been_run = False + # Clear the queue + kernel.execution_queue = [] + # Interrupt the kernel + kernel.kernel_manager.interrupt_kernel() + + def has_input(self) -> bool: + """Checks whether a block has connected input blocks""" + for input_socket in self.sockets_in: + if len(input_socket.edges) != 0: + return True + return False + + def has_output(self) -> bool: + """Checks whether a block has connected output blocks""" + for output_socket in self.sockets_out: + if len(output_socket.edges) != 0: + return True + return False + + def run_left(self, in_run_right=False): + """ + Run all of the block's dependencies and then run the block + """ + # If the block was already running, cancel the execution + if self.is_running and not in_run_right: + 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["OCBExecutableblock"] = [v for _, v in edges] + for block in blocks_to_run[::-1]: + if not block.has_been_run: + block.run_code() + + if in_run_right: + # 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.is_running: + self._interrupt_execution() + return + + # If no output, run left + if not self.has_output(): + return self.run_left(in_run_right=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["OCBExecutableBlock"] = [ + self] + [v for _, v in edges] + for block in blocks_to_run[::-1]: + block.run_left(in_run_right=True) + + def reset_has_been_run(self): + """ Called when the output is an error """ + self.has_been_run = False + + + @property + def source(self) -> str: + """Source code""" + raise NotImplementedError("source(self) should be overriden") + + @source.setter + def source(self, value: str): + raise NotImplementedError("source(self) should be overriden") + + + def handle_stdout(self, value: str): + """Handle the stdout signal""" + pass + + def handle_image(self, image: str): + """Handle the image signal""" + pass + + def serialize(self): + """Return a serialized version of this block""" + base_dict = super().serialize() + base_dict["source"] = self.source + return base_dict + + def deserialize( + self, data: OrderedDict, hashmap: dict = None, restore_id: bool = True + ): + """Restore a codeblock from it's serialized state""" + for dataname in ("source"): + if dataname in data: + setattr(self, dataname, data[dataname]) + super().deserialize(data, hashmap, restore_id) diff --git a/opencodeblocks/blocks/sliderblock.py b/opencodeblocks/blocks/sliderblock.py index 21505b78..74833b96 100644 --- a/opencodeblocks/blocks/sliderblock.py +++ b/opencodeblocks/blocks/sliderblock.py @@ -8,7 +8,7 @@ from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QHBoxLayout, QLabel, QLineEdit, QSlider, QVBoxLayout from opencodeblocks.blocks.block import OCBBlock - +from opencodeblocks.graphics.kernel import get_main_kernel class OCBSliderBlock(OCBBlock): """ @@ -47,10 +47,21 @@ 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}") - - # The code execution part will be added when the execution flow is merged. - # We print for now - print(python_code) + + kernel = get_main_kernel() + kernel.execution_queue.append((self, python_code)) + if kernel.busy is False: + kernel.run_queue() + + + def reset_has_been_run(self): + pass + def reset_buttons(self): + pass + def handle_stdout(self): + pass + def handle_image(self): + pass @property def value(self): diff --git a/opencodeblocks/graphics/kernel.py b/opencodeblocks/graphics/kernel.py index c5a1f509..4adf9d7b 100644 --- a/opencodeblocks/graphics/kernel.py +++ b/opencodeblocks/graphics/kernel.py @@ -3,6 +3,7 @@ import queue from typing import Tuple +from PyQt5.QtCore import QThreadPool from jupyter_client.manager import start_new_kernel from opencodeblocks.graphics.worker import Worker @@ -68,9 +69,9 @@ def run_block(self, block, code: str): worker.signals.stdout.connect(block.handle_stdout) worker.signals.image.connect(block.handle_image) worker.signals.finished.connect(self.run_queue) - worker.signals.finished_block.connect(block.reset_buttons) + worker.signals.finished.connect(block.execution_finished) worker.signals.error.connect(block.reset_has_been_run) - block.source_editor.threadpool.start(worker) + get_main_threadpool().start(worker) def run_queue(self): """ Runs the next code in the queue """ @@ -138,3 +139,13 @@ def __del__(self): Shuts down the kernel """ self.kernel_manager.shutdown_kernel() + + +kernel = Kernel() +threadpool = QThreadPool() + +def get_main_kernel(): + return kernel + +def get_main_threadpool(): + return threadpool \ No newline at end of file diff --git a/opencodeblocks/graphics/pyeditor.py b/opencodeblocks/graphics/pyeditor.py index e7877257..766e5be9 100644 --- a/opencodeblocks/graphics/pyeditor.py +++ b/opencodeblocks/graphics/pyeditor.py @@ -4,16 +4,13 @@ """ 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, QFontMetrics, QColor, QMouseEvent, QWheelEvent from PyQt5.Qsci import QsciScintilla, QsciLexerPython from opencodeblocks.graphics.theme_manager import theme_manager from opencodeblocks.blocks.block import OCBBlock -from opencodeblocks.graphics.kernel import Kernel - -kernel = Kernel() -threadpool = QThreadPool() +from opencodeblocks.graphics.kernel import get_main_kernel, get_main_threadpool if TYPE_CHECKING: from opencodeblocks.graphics.view import OCBView @@ -33,8 +30,8 @@ def __init__(self, block: OCBBlock): super().__init__(None) self._mode = "NOOP" self.block = block - self.kernel = kernel - self.threadpool = threadpool + self.kernel = get_main_kernel() + self.threadpool = get_main_threadpool() self.update_theme() theme_manager().themeChanged.connect(self.update_theme) diff --git a/opencodeblocks/graphics/worker.py b/opencodeblocks/graphics/worker.py index abb6185a..126df9d3 100644 --- a/opencodeblocks/graphics/worker.py +++ b/opencodeblocks/graphics/worker.py @@ -44,7 +44,6 @@ async def run_code(self): elif output_type == 'error': self.signals.error.emit() self.signals.finished.emit() - self.signals.finished_block.emit() def run(self): """ Execute the run_code method asynchronously. """ From 25944e913a0abfa426f0cb654c3a026f9b5bc580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Sat, 11 Dec 2021 21:54:14 +0100 Subject: [PATCH 06/51] =?UTF-8?q?=E2=98=94=20Correction=20for=20test=20and?= =?UTF-8?q?=20fix=20for=20#112?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- opencodeblocks/blocks/codeblock.py | 5 ++++- opencodeblocks/blocks/executableblock.py | 23 ++++++++++------------- tests/integration/blocks/test_block.py | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/opencodeblocks/blocks/codeblock.py b/opencodeblocks/blocks/codeblock.py index 01824e3c..b96c275c 100644 --- a/opencodeblocks/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -28,16 +28,19 @@ class OCBCodeBlock(OCBExecutableBlock): """ - def __init__(self, **kwargs): + def __init__(self, source: str = "", **kwargs): """ Create a new OCBCodeBlock. Initialize all the child widgets specific to this block type """ super().__init__(**kwargs) self.source_editor = PythonEditor(self) + self._source = "" self._stdout = "" + self.source = source + self.output_panel_height = self.height / 3 self._min_output_panel_height = 20 self._min_source_editor_height = 20 diff --git a/opencodeblocks/blocks/executableblock.py b/opencodeblocks/blocks/executableblock.py index 4420abad..b40a5f75 100644 --- a/opencodeblocks/blocks/executableblock.py +++ b/opencodeblocks/blocks/executableblock.py @@ -117,19 +117,16 @@ def run_left(self, in_run_right=False): 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["OCBExecutableblock"] = [v for _, v in edges] - for block in blocks_to_run[::-1]: - if not block.has_been_run: - block.run_code() + if self.has_input(): + # Create the graph from the scene + graph = self.scene().create_graph() + # BFS through the input graph + edges = bfs_edges(graph, self, reverse=True) + # Run the blocks found except self + blocks_to_run: List["OCBExecutableblock"] = [v for _, v in edges] + for block in blocks_to_run[::-1]: + if not block.has_been_run: + block.run_code() if in_run_right: # If run_left was called inside of run_right diff --git a/tests/integration/blocks/test_block.py b/tests/integration/blocks/test_block.py index 02e6253e..c14b34d1 100644 --- a/tests/integration/blocks/test_block.py +++ b/tests/integration/blocks/test_block.py @@ -11,7 +11,7 @@ from PyQt5.QtCore import QPointF -from opencodeblocks.blocks.codeblock import OCBBlock +from opencodeblocks.blocks.block import OCBBlock from opencodeblocks.graphics.window import OCBWindow from opencodeblocks.graphics.widget import OCBWidget From a9386ad212418fa3fe66f3b926d239549f7dde6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Sat, 11 Dec 2021 22:17:22 +0100 Subject: [PATCH 07/51] =?UTF-8?q?=F0=9F=8E=89=20Slider=20are=20executable?= =?UTF-8?q?=20and=20allow=20for=20real=20time=20displays=20!=20You=20can?= =?UTF-8?q?=20for=20example=20plot=20matplotlib=20graph=20and=20see=20chan?= =?UTF-8?q?ges=20in=20live=20!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- opencodeblocks/blocks/codeblock.py | 18 ++++++++++++-- opencodeblocks/blocks/executableblock.py | 31 ++++++------------------ opencodeblocks/blocks/sliderblock.py | 30 +++++++++-------------- 3 files changed, 36 insertions(+), 43 deletions(-) diff --git a/opencodeblocks/blocks/codeblock.py b/opencodeblocks/blocks/codeblock.py index b96c275c..3a984b51 100644 --- a/opencodeblocks/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -77,7 +77,7 @@ def init_run_button(self): run_button.move(int(self.edge_size), int(self.edge_size / 2)) run_button.setFixedSize(int(3 * self.edge_size), int(3 * self.edge_size)) - run_button.clicked.connect(self.run_left) + run_button.clicked.connect(self.handle_run_left) return run_button def init_run_all_button(self): @@ -85,11 +85,25 @@ def init_run_all_button(self): run_all_button = QPushButton(">>", self.root) run_all_button.setFixedSize( int(3 * self.edge_size), int(3 * self.edge_size)) - run_all_button.clicked.connect(self.run_right) + run_all_button.clicked.connect(self.handle_run_right) run_all_button.raise_() return run_all_button + def handle_run_right(self): + """ Called when the button for "Run All" was pressed""" + if self.is_running: + self._interrupt_execution() + else: + self.run_right() + + def handle_run_left(self): + """ Called when the button for "Run Left" was pressed""" + if self.is_running: + self._interrupt_execution() + else: + self.run_left() + def run_code(self): """Run the code in the block""" # Reset stdout diff --git a/opencodeblocks/blocks/executableblock.py b/opencodeblocks/blocks/executableblock.py index b40a5f75..110226d2 100644 --- a/opencodeblocks/blocks/executableblock.py +++ b/opencodeblocks/blocks/executableblock.py @@ -82,7 +82,8 @@ def execution_finished(self): """ self.is_running = False - def _interrupt_execution(self): + @staticmethod + def _interrupt_execution(): """ Interrupt an execution, reset the blocks in the queue """ kernel = get_main_kernel() for block, _ in kernel.execution_queue: @@ -112,11 +113,7 @@ def run_left(self, in_run_right=False): """ Run all of the block's dependencies and then run the block """ - # If the block was already running, cancel the execution - if self.is_running and not in_run_right: - self._interrupt_execution() - return - + if self.has_input(): # Create the graph from the scene graph = self.scene().create_graph() @@ -127,29 +124,17 @@ def run_left(self, in_run_right=False): for block in blocks_to_run[::-1]: if not block.has_been_run: block.run_code() - - if in_run_right: - # 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() + + if self.is_running: return + 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.is_running: - self._interrupt_execution() - return # If no output, run left if not self.has_output(): - return self.run_left(in_run_right=True) + self.run_left(in_run_right=True) + return # Same as run_left but instead of running the blocks, we'll use run_left graph = self.scene().create_graph() diff --git a/opencodeblocks/blocks/sliderblock.py b/opencodeblocks/blocks/sliderblock.py index 74833b96..89953728 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 from opencodeblocks.graphics.kernel import get_main_kernel -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. """ @@ -45,23 +45,17 @@ def __init__(self, **kwargs): 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}") - - kernel = get_main_kernel() - kernel.execution_queue.append((self, python_code)) - if kernel.busy is False: - kernel.run_queue() - - - def reset_has_been_run(self): - pass - def reset_buttons(self): - pass - def handle_stdout(self): - pass - def handle_image(self): - pass + self.run_right() + + @property + def source(self): + """ Get the source code of the slider """ + python_code = f"{self.var_name} = {self.value}" + return python_code + @source.setter + def source(self, value: str): + raise RuntimeError("The source of a sliderblock is read-only.") @property def value(self): From 3cf33269f045cfb9fbb4d7e474990e8e8f09da4e Mon Sep 17 00:00:00 2001 From: AlexandreSajus Date: Sat, 11 Dec 2021 22:29:51 +0100 Subject: [PATCH 08/51] :wrench: easier coloring ob blocks/edges --- opencodeblocks/blocks/codeblock.py | 4 +-- opencodeblocks/graphics/codeedge.py | 54 ----------------------------- opencodeblocks/graphics/edge.py | 21 +++++++++-- opencodeblocks/scene/scene.py | 3 +- 4 files changed, 21 insertions(+), 61 deletions(-) delete mode 100644 opencodeblocks/graphics/codeedge.py diff --git a/opencodeblocks/blocks/codeblock.py b/opencodeblocks/blocks/codeblock.py index 89dd9d49..40d380cf 100644 --- a/opencodeblocks/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -52,7 +52,7 @@ def __init__(self, **kwargs): self.run_color = 0 self._pen_outline = QPen(QColor("#7F000000")) self._pen_outline_running = QPen(QColor("#FF0000")) - self._pen_outline_transmitting = QPen(QColor("#FFFFA637")) + self._pen_outline_transmitting = QPen(QColor("#00ff00")) self._pen_outlines = [ self._pen_outline, self._pen_outline_running, @@ -109,7 +109,6 @@ def init_run_all_button(self): def run_code(self): """Run the code in the block""" - self.run_color = 1 # Reset stdout self._cached_stdout = "" @@ -219,7 +218,6 @@ def reset_buttons(self): """Reset the buttons""" self.run_button.setText(">") self.run_all_button.setText(">>") - self.run_color = 0 def update_title(self): """Change the geometry of the title widget""" diff --git a/opencodeblocks/graphics/codeedge.py b/opencodeblocks/graphics/codeedge.py deleted file mode 100644 index 0b936082..00000000 --- a/opencodeblocks/graphics/codeedge.py +++ /dev/null @@ -1,54 +0,0 @@ -# OpenCodeBlock an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO - -""" Module for the OCB Edge. """ - -from __future__ import annotations - -from typing import Optional - -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QColor, QPainter, QPen -from PyQt5.QtWidgets import QStyleOptionGraphicsItem, QWidget - -from opencodeblocks.graphics.edge import OCBEdge - - -class OCBCodeEdge(OCBEdge): - - """Base class for directed edges in OpenCodeBlocks.""" - - def __init__( - self, - edge_running_color: str = "#FF0000", - edge_transmitting_color: str = "#FFFFA637", - ): - super().__init__() - self.edge_running_color = edge_running_color - self.edge_transmitting_color = edge_transmitting_color - - self.run_color = 0 - self._pen_outline = QPen(QColor("#7F000000")) - self._pen_outline_running = QPen(QColor("#FF0000")) - self._pen_outline_transmitting = QPen(QColor("#FFFFA637")) - self._pen_outlines = [ - self._pen_outline, - self._pen_outline_running, - self._pen_outline_transmitting, - ] - - def paint( - self, - painter: QPainter, - option: QStyleOptionGraphicsItem, # pylint:disable=unused-argument - widget: Optional[QWidget] = None, - ): # pylint:disable=unused-argument - """Paint the edge.""" - self.update_path() - painter.setPen( - self._pen_selected - if self.isSelected() - else self._pen_outlines[self.run_color] - ) - painter.setBrush(Qt.BrushStyle.NoBrush) - painter.drawPath(self.path()) diff --git a/opencodeblocks/graphics/edge.py b/opencodeblocks/graphics/edge.py index 0f50fd21..eb0b71f6 100644 --- a/opencodeblocks/graphics/edge.py +++ b/opencodeblocks/graphics/edge.py @@ -25,6 +25,8 @@ def __init__( path_type="bezier", 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, @@ -56,6 +58,16 @@ 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] + + self.run_color = 0 + self.setFlag(QGraphicsPathItem.GraphicsItemFlag.ItemIsSelectable) self.setZValue(-1) @@ -101,8 +113,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()) diff --git a/opencodeblocks/scene/scene.py b/opencodeblocks/scene/scene.py index f82e535c..ff2b85cb 100644 --- a/opencodeblocks/scene/scene.py +++ b/opencodeblocks/scene/scene.py @@ -17,7 +17,6 @@ from opencodeblocks.core.serializable import Serializable from opencodeblocks.blocks.block import OCBBlock from opencodeblocks.graphics.edge import OCBEdge -from opencodeblocks.graphics.codeedge import OCBCodeEdge from opencodeblocks.scene.clipboard import SceneClipboard from opencodeblocks.scene.history import SceneHistory @@ -249,7 +248,7 @@ def deserialize( # Create edges for edge_data in data["edges"]: - edge = OCBCodeEdge() + edge = OCBEdge() edge.deserialize(edge_data, hashmap, restore_id) self.addItem(edge) hashmap.update({edge_data["id"]: edge}) From e90d221f1b5cbd12f1184134822be613563cb001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Sat, 11 Dec 2021 23:10:09 +0100 Subject: [PATCH 09/51] =?UTF-8?q?=F0=9F=93=9D=20Add=20an=20example=20for?= =?UTF-8?q?=20sliders=20with=20a=20linear=20classifier.=20=E2=9C=A8=20Remo?= =?UTF-8?q?ve=20unused=20imports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/linear_classifier.ipyg | 609 +++++++++++++++++++++++ opencodeblocks/blocks/codeblock.py | 3 +- opencodeblocks/blocks/executableblock.py | 8 +- opencodeblocks/blocks/sliderblock.py | 3 +- opencodeblocks/graphics/worker.py | 1 + 5 files changed, 614 insertions(+), 10 deletions(-) create mode 100644 examples/linear_classifier.ipyg diff --git a/examples/linear_classifier.ipyg b/examples/linear_classifier.ipyg new file mode 100644 index 00000000..3aa765eb --- /dev/null +++ b/examples/linear_classifier.ipyg @@ -0,0 +1,609 @@ +{ + "id": 2034509196736, + "blocks": [ + { + "id": 2034509423808, + "title": "Imports", + "block_type": "OCBCodeBlock", + "splitter_pos": [ + 227, + 0 + ], + "position": [ + -837.0, + -95.0 + ], + "width": 501, + "height": 286, + "metadata": { + "title_metadata": { + "color": "white", + "font": "Ubuntu", + "size": 10 + } + }, + "sockets": [ + { + "id": 2034509424672, + "type": "input", + "position": [ + 0.0, + 53.0 + ], + "metadata": { + "color": "#FF55FFF0", + "linecolor": "#FF000000", + "linewidth": 1.0, + "radius": 10.0 + } + }, + { + "id": 2034509424816, + "type": "output", + "position": [ + 501.0, + 53.0 + ], + "metadata": { + "color": "#FF55FFF0", + "linecolor": "#FF000000", + "linewidth": 1.0, + "radius": 10.0 + } + } + ], + "source": "from matplotlib import pyplot as plt\r\nfrom sklearn import linear_model\r\nfrom random import seed, random", + "stdout": "" + }, + { + "id": 2034638878464, + "title": "", + "block_type": "OCBMarkdownBlock", + "splitter_pos": [ + 0, + 194 + ], + "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, + 403 + ], + "position": [ + 757.0, + 106.0 + ], + "width": 663, + "height": 462, + "metadata": { + "title_metadata": { + "color": "white", + "font": "Ubuntu", + "size": 10 + } + }, + "sockets": [ + { + "id": 2034686483184, + "type": "input", + "position": [ + 0.0, + 53.0 + ], + "metadata": { + "color": "#FF55FFF0", + "linecolor": "#FF000000", + "linewidth": 1.0, + "radius": 10.0 + } + }, + { + "id": 2034686483328, + "type": "output", + "position": [ + 663.0, + 53.0 + ], + "metadata": { + "color": "#FF55FFF0", + "linecolor": "#FF000000", + "linewidth": 1.0, + "radius": 10.0 + } + } + ], + "source": "um = 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": [ + 275, + 0 + ], + "position": [ + -211.0, + -101.0 + ], + "width": 641, + "height": 334, + "metadata": { + "title_metadata": { + "color": "white", + "font": "Ubuntu", + "size": 10 + } + }, + "sockets": [ + { + "id": 2034723534592, + "type": "input", + "position": [ + 0.0, + 53.0 + ], + "metadata": { + "color": "#FF55FFF0", + "linecolor": "#FF000000", + "linewidth": 1.0, + "radius": 10.0 + } + }, + { + "id": 2034723534736, + "type": "output", + "position": [ + 641.0, + 53.0 + ], + "metadata": { + "color": "#FF55FFF0", + "linecolor": "#FF000000", + "linewidth": 1.0, + "radius": 10.0 + } + } + ], + "source": "seed(132)\r\nx = [3 + random() * 10 for i in range(40)]\r\ny = [4.15 * i + random() * 2 for i in x]", + "stdout": "" + }, + { + "id": 2034723677808, + "title": "Slider", + "block_type": "OCBSliderBlock", + "splitter_pos": [], + "position": [ + -175.0, + 296.0 + ], + "width": 618, + "height": 184, + "metadata": { + "title_metadata": { + "color": "white", + "font": "Ubuntu", + "size": 10 + } + }, + "sockets": [ + { + "id": 2034723678672, + "type": "input", + "position": [ + 0.0, + 50.0 + ], + "metadata": { + "color": "#FF55FFF0", + "linecolor": "#FF000000", + "linewidth": 1.0, + "radius": 10.0 + } + }, + { + "id": 2034723678816, + "type": "output", + "position": [ + 618.0, + 50.0 + ], + "metadata": { + "color": "#FF55FFF0", + "linecolor": "#FF000000", + "linewidth": 1.0, + "radius": 10.0 + } + } + ], + "source": "b = 0.12", + "value": "0.12", + "var_name": "b" + }, + { + "id": 2034723714816, + "title": "Slider", + "block_type": "OCBSliderBlock", + "splitter_pos": [], + "position": [ + -184.0, + 550.0 + ], + "width": 618, + "height": 184, + "metadata": { + "title_metadata": { + "color": "white", + "font": "Ubuntu", + "size": 10 + } + }, + "sockets": [ + { + "id": 2034723715680, + "type": "input", + "position": [ + 0.0, + 50.0 + ], + "metadata": { + "color": "#FF55FFF0", + "linecolor": "#FF000000", + "linewidth": 1.0, + "radius": 10.0 + } + }, + { + "id": 2034723715824, + "type": "output", + "position": [ + 618.0, + 50.0 + ], + "metadata": { + "color": "#FF55FFF0", + "linecolor": "#FF000000", + "linewidth": 1.0, + "radius": 10.0 + } + } + ], + "source": "m = 0.12", + "value": "0.12", + "var_name": "m" + }, + { + "id": 2034879162976, + "title": "Regression", + "block_type": "OCBCodeBlock", + "splitter_pos": [ + 0, + 349 + ], + "position": [ + 784.0, + -676.0 + ], + "width": 615, + "height": 408, + "metadata": { + "title_metadata": { + "color": "white", + "font": "Ubuntu", + "size": 10 + } + }, + "sockets": [ + { + "id": 2034879163840, + "type": "input", + "position": [ + 0.0, + 53.0 + ], + "metadata": { + "color": "#FF55FFF0", + "linecolor": "#FF000000", + "linewidth": 1.0, + "radius": 10.0 + } + }, + { + "id": 2034879163984, + "type": "output", + "position": [ + 615.0, + 53.0 + ], + "metadata": { + "color": "#FF55FFF0", + "linecolor": "#FF000000", + "linewidth": 1.0, + "radius": 10.0 + } + } + ], + "source": "my = reg.predict([[i] for i in x])\r\nprint(my)\r\nplt.plot(x,my)\r\nplt.scatter(x,y)", + "stdout": "iVBORw0KGgoAAAANSUhEUgAAAXAAAAD4CAYAAAD1jb0+AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8/fFQqAAAACXBIWXMAAAsTAAALEwEAmpwYAAAc6ElEQVR4nO3de3jU5Z338feXECRYNCqRxVCMR2rVEuxUracqWlFgFa2r21qqrS3rbtvHWksF66qPh0qLVVy363OhVPHReihi7GorsoiPh1ZtYhCoiFrEw4ASD6EeokD4Pn/MJGZOmUkyM7/5zXxe1+WV+d3zG+Y76vXhzj33wdwdEREJn0FBFyAiIv2jABcRCSkFuIhISCnARURCSgEuIhJSg4v5ZiNGjPCGhoZivqWISOi1tLS87e51ye1FDfCGhgaam5uL+ZYiIqFnZq+ma9cQiohISOUU4Ga2zsxWmtlyM2uOt11mZtF423Izm1TYUkVEpKe+DKEc4+5vJ7Vd5+7X5LMgERHJjYZQRERCKtcAd+BhM2sxs+k92n9gZivM7DdmtlO6F5rZdDNrNrPmtra2ARcsIiIxuQb4Ee5+EHAi8H0zOwq4EdgLaAQ2AL9K90J3n+fuEXeP1NWlzIIREZF+yinA3T0a/7kRuA842N3fcvdOd98G3AQcXLgyRUQkWdYAN7PtzWx412PgeGCVmY3qcdspwKrClCgiUtqaWqMcPvsR9pj5IIfPfoSm1mj3c2vbPuCGpS+xpXNb3t83l1koI4H7zKzr/t+6+0Nm9n/NrJHY+Pg64F/yXp2ISIm7uGkldzz1Gl0nK0TbO5i1aCXuzpLVb/GHlW8CcMpB9YzeaVhe3ztrgLv7WmBcmvZpea1ERCRkmlqjCeHdpWNLJ+ff81z39XVnjMt7eIOmEYqI9NucxWtSwrunEZ/ZjmtO+wLXLH4x7fDKQBV1LxQRkXKyvr0j43O7bD+Eiyfvx6xFK+nY0gl8OrwCMHV8/YDfXz1wEZF+2q22JuNz/z7l88xZvKY7vLt0bOlkzuI1eXl/BbiISD+t35TaAzfgm4eOYer4+ow99N567n2hIRQRkT56/d2POPKXy1La62trmDFxbPfwyG61NUTThHVvPfe+UICLiPRBw8wHE67PP25fzjtun7T3zpg4NmEMHKCmuooZE8fmpRYFuIhIDlpfe49T/utPCW3rZk/u9TVdPfE5i9ewvr2D3ZJ66AOlABcRySK51339PzdycmNuITx1fH3eAjuZAlxEJIMHV2zg+799NqEtW6+7mBTgIlJxmlqjWYc1knvdC8/9MpGGnYtZZlYKcBGpKE2t0V4X1/x62csp87RLqdfdkwJcRCpKpsU1v3zoBX509/KE9kd/cjQNI7YvYnV9owAXkbLWc7ikdlg17320Je196zd9nHBdqr3unhTgIlK2kodLMoV3T89dcjw7DqsudGl5oQAXkbKVbrgkk52GVdN6yfEFrii/tBeKiJStvuw5MmxI+PqzOVVsZuuA94FOYKu7R8xsZ+BuoIHYiTynu/t7hSlTRKRvmlqjDDKj03vbsftT+dpgqpj60gM/xt0b3T0Sv54JLHX3fYCl8WsRkcB1jX3nGt6Qvw2mimkgQygnAwvijxcAUwdcjYhIHvQ29l1lqW353GCqmHINcAceNrMWM5sebxvp7hvij98kdvhxCjObbmbNZtbc1tY2wHJFRLLLNBxiwN+unszcMxqpr63BiG0Be/WpBxZsv5JCynXU/gh3j5rZrsASM3uh55Pu7maW9ncVd58HzAOIRCK5/z4jItJPmYKma5ikkBtMFVNOAe7u0fjPjWZ2H3Aw8JaZjXL3DWY2CthYwDpFRIDe9zE5+5ZneHRN+t/0wzpM0pusAW5m2wOD3P39+OPjgcuB3wNnAbPjP+8vZKEiIr3tY5K8DP60g0bz57XvFGQf7lKRSw98JHCfmXXd/1t3f8jM/gLcY2bnAK8CpxeuTBGRzPuYJId3GJbB50PWAHf3tcC4NO3vAMcWoigRkXSyzdX+4YS9ueD48hom6Y1WYopIKHQtzOnNomejRaqmNCjARaTk5bowJ4yrKQcifIv/RaSiNLVGueCe53JaVRnG1ZQDoR64iJSsptYoF967IqfwLsdpgtmoBy4iJSt5dkmyKjO2uZftNMFsFOAiUnIeWvUm597e0us9NdVVoV0Cny8KcBEput5WUyafBp9OlVnFhzdoDFxEiqxrRkm0vQPn09WU31vQnBLec89opKa6KqGtprqKX50+ruLDG9QDF5Eiy7Sacsnqt7qvzzxkDFedcmDCa8p5SXx/KcBFpKCSh0uiWeZqJy+DL5edAwtBAS4iBZNu8ykj83av3zx0TNFqKwcaAxeRgkk3XNLbjO47nnqNptbKWg4/EApwESmYvi5td2KhL7lRgItIwfRnaXul7WcyEApwESmYsw7bPaVtsEHVoMy7ClbafiYDoS8xRaQg0i3Iqa+t4aPNW3nvoy1pX1OJ+5kMRM4BbmZVQDMQdfcpZnYr8BVgU/yWs919ed4rFJFQaWqNpuxhsvryE6gZEluQs0cvKy21urJv+tIDPw9YDezQo22Guy/Mb0kiElbpet1zz2jsDm8g41zw+toahXcf5TQGbmajgcnAzYUtR0TC6Id3tmbcw2TWopUJUwNnTBybdnm8hk76LtcvMecCPwW2JbVfZWYrzOw6M9su3QvNbLqZNZtZc1tb2wBKFZFS1DDzQf77ufUZn+/Y0pkwNXDq+HquPvVA6mtrMGI9bw2d9E/WIRQzmwJsdPcWMzu6x1OzgDeBIcA84ELg8uTXu/u8+PNEIpHsu7KLSCik63FnWmWZPDVQy+PzI5ce+OHASWa2DrgLmGBmt7v7Bo/5BLgFOLiAdYpICUkX3utmT844BVBTAwsjaw/c3WcR620T74H/xN2/aWaj3H2DmRkwFVhVwDpFpARkCu4uMyaOTdj7BDS+XUgDmQd+h5nVEfutaTlwbl4qEpGSlBzep31xNNf807iEtq5hEW3/WhzmORwWmi+RSMSbm5uL9n4iMnDZet1SeGbW4u6R5HatxBSRtD78ZCv7X7o4oe2/zjyISQeOCqgiSaYAF5EU6nWHgwJcRLqt3vB3Trz+8YS2x2Ycw5hdhgVUkfRGAS4igHrdYaQAF6lwtz/1Khc3Jc4CfvHKExkyWLtNlzoFuEgFU6873BTgIhVo2vynefyltxPaFNzhowAXKTNNrdFeF9Ko110+tJBHpIw0tUZTlrID1NZU096RegqOgjscMi3k0bcUImVkzuI1KeENpIT3iM8MUXiXAQW4SBnJ5UT3+toa3vlgM4fPfiThoAUJHwW4SBnJZdvWaHsHHv+ZfFqOhIsCXKSM9HXb1uTTciRcFOAiZWJTx5aU0+Bzkcuwi5QmBbhIGWiY+SDj/vfDCW1zz2hMOHeytqY67Wt1Wk54aR64SIiteKOdk/7zyYS2Z352LLsOHwqQMP873RRDnZYTbjkHuJlVAc1A1N2nmNkexM7I3AVoAaa5++bClCkiyfq6IEen5ZSfvvTAzwNWAzvEr38BXOfud5nZ/wHOAW7Mc30iFam31ZS3PvkKl/338wn3/+3nk6gaZFn/XJ0GX15yCnAzGw1MBq4Cfhw/yHgC8I34LQuAy1CAiwxY8lBH13Q/IO2XlFqQU7ly7YHPBX4KDI9f7wK0u/vW+PUbQNq/1s1sOjAdYMyYMf0uVKRSpFtN2bGlMyW8FdySdRaKmU0BNrp7S3/ewN3nuXvE3SN1dXX9+SNEKkou0/oU3gK59cAPB04ys0nAUGJj4NcDtWY2ON4LHw1oOZdIHuxWW0M0Q4gruKWnrAHu7rOAWQBmdjTwE3c/08x+B5xGbCbKWcD9hStTpHwlf2F5zOfquP2p1xLuGWRw7emNwRQoJWsgC3kuJPaF5svExsTn56ckkcrR9YVlz/1JksO7yoxvHDJGs0ckRZ8W8rj7o8Cj8cdrgYPzX5JI5ci0/WtPne7c2xIlsvvOCnFJoKX0IgHKdR8SbTol6SjARQLy7oeb6ct5WNp0SpJpLxSRAKRbBt/FIG2wa9MpSaYeuEgRtb72Xkp4//yUAxJ2DTzz0DHUVFcl3KNNpyQd9cBFiqS3zae+ccjuCe2R3XfWplOSlQJcpICaWqNc+vu/sinpUOFXrp5EbEuh9LTplORCAS5SIE2t0bSbT809o7HX8BbJlQJcpACm3PA4q6J/T/vcnMVr1LuWvFCAi+RZbzNMQNMBJX8U4CJ5ki24u2g6oOSLphGK5EFyeE/cfyRzz2jUdEApKPXARfop05eUyVu+ajqgFIoCXKQf7m15gwt+91xCW3WVMee0cQltmg4ohaQAF+mjTGPdWzpdM0ykqDQGLpKjjX//WDNMpKRk7YGb2VDgMWC7+P0L3f1SM7sV+AqwKX7r2e6+vEB1igRKM0ykFOUyhPIJMMHdPzCzauAJM/tj/LkZ7r6wcOWJBKvl1Xf52o1/TmgbteNQjt1vV+5tiSYcxqAZJlJsuZyJ6cAH8cvq+D992cZYJJQy9bo3bPqYe1uifO2L9Sx7oU0zTCQwOX2JaWZVQAuwN/Brd3/azP4VuMrMLgGWAjPd/ZPClSpSHPOfeIUrHni+13s6tnSy7IU2npw5oUhViaTKKcDdvRNoNLNa4D4zO4DYSfVvAkOAecQOOb48+bVmNh2YDjBmzJj8VC1SIOl63ZkOWNAXlhK0Ps1Ccfd2YBlwgrtv8JhPgFvIcMCxu89z94i7R+rq6gZcsEghnH3LMynhvW72ZNbNnpzxi0l9YSlByxrgZlYX73ljZjXAV4EXzGxUvM2AqcCqwpUpUjgNMx/k0TVt3deNn61NWE05Y+JYLYmXkpTLEMooYEF8HHwQcI+7P2Bmj5hZHbHfMJcD5xauTJH86+2EnJ66vpjUkngpNRabZFIckUjEm5ubi/Z+Ipkkh/f5x+3LecftE1A1Ir0zsxZ3jyS3aym9VJRce90iYaAAl4rwydZOxl78UELbbd85mKP21RfrEl4KcCl76nVLuVKAS9la397BYbMfSWh7cuYE6jX9T8qEAlzKknrdUgkU4FJWHn+pjWnzn0loW3PlCWw3uCrDK0TCSwEuZUO9bqk0CnAJvf9Y+hLXLnkxoU3BLZVAAS6hpl63VDIFuITS1278Ey2vvpfQpuCWSqMAl9BJ7nUftW8dt30n7WaYImVNAS6hoeESkUQKcCl57s4es/6Q0Hbx5P347pF7BlSRSGlQgEtJU69bJDMFuJSkjs2d7HdJ4uZTvzv3y3ypYeeAKhIpPQpwCVRTazTloIQf3b085T71ukVSZQ1wMxsKPAZsF79/obtfamZ7AHcBuxA7sX6au28uZLFSXppao8xatJKOLZ0ARNs7UsL7mZ8dy67DhwZQnUjpy+VQ40+ACe4+DmgETjCzQ4FfANe5+97Ae8A5BatSytKcxWu6wzud+toa/vTyO0WsSCRcsgZ4/OT5D+KX1fF/HJgALIy3LyB2sLFIzta3d/T6fLS9g1mLVtLUGi1SRSLhktMYePxA4xZgb+DXwN+AdnffGr/lDSDtCa9mNh2YDjBmzJiB1ishlW6sO5fTWDu2dDJn8RodICySRi5DKLh7p7s3AqOBg4HP5foG7j7P3SPuHqmr0/FVlahrrDva3oGTfqy7N9l66iKVqk+zUNy93cyWAV8Gas1scLwXPhrQ77kVLl0ve+r4+l7Huj+7Uw3r2z+m0zP3x3fTCToiaWXtgZtZnZnVxh/XAF8FVgPLgNPit50F3F+gGiUE0vWyu8avM/WgDXj8wgls6yW8a6qrmDFxbGGKFgm5XIZQRgHLzGwF8Bdgibs/AFwI/NjMXiY2lXB+4cqUUpeul901fp0pnrt61pl62FVmXH3qgRr/Fskg6xCKu68AxqdpX0tsPFwkYy87mqG9Z896xsSxCfPBu55XeIv0LqcvMUWyyTZOfe5X9qS+tgYjNr+7ZzhPHV/P1acemPF5EUnPvJfxx3yLRCLe3NxctPeT4kleVdmTlsGLDIyZtbh7JLldPXDJi4n7/0NKeP9s0n4Kb5EC0mZWMmDa8lUkGApw6bfX3/2II3+5LKFt5WXHM3xodUAViVQWBbj0i3rdIsFTgEufPPny25x589MJbWt/PolBgyygikQqlwJccpbc6x4yeBAvXnliQNWIiAJcgMz7mADc/PharnxwdcL9Gi4RCZ4CXNKejDNr0UqAlF0Dj//8SOZ9K2U6qogEQAEuGfcxSQ5v9bpFSosCXLLut33x5P347pF7FqkaEcmVAlzYrbYm46ZT6nWLlC4tpRcuOH7flLYhVYOYe0Zj8YsRkZypB17h0i3IqU+ahSIipUkBXqE+/GQr+1+6OKHtqVnH8g87Dg2oIhHpq6wBbmafBW4DRgIOzHP3683sMuB7QFv81ovc/Q+FKlTyR8vgRcpDLj3wrcAF7v6smQ0HWsxsSfy569z9msKVJ/mUbvOpF644gaHVVQFVJCIDkcuRahuADfHH75vZakCDoyGTrtc994xGhbdIiPVpFoqZNRA7H7NrN6MfmNkKM/uNme2U4TXTzazZzJrb2trS3SIF9NTad9KGN9B9aryIhFPOR6qZ2WeA/wdc5e6LzGwk8DaxcfErgFHu/p3e/gwdqVZcmYK7p/raGp6cOaEI1YhIfw3oSDUzqwbuBe5w90UA7v6Wu3e6+zbgJnRCfcm4/alXU8I702av2VZhikjpyhrgZmbAfGC1u1/bo31Uj9tOAVblvzzpi6bWKA0zH+Tipk//U/zb0XuxbvbkjKfGZztNXkRKVy6zUA4HpgErzWx5vO0i4Otm1khsCGUd8C8FqE9ydO7tLTy06s2EtprqKvYdORyAGRPHppwaX1NdxYyJY4tap4jkTy6zUJ4g/W/gmvNdIjKNdXds6WTO4jVMHV/fvaoy057fIhI+WokZYt+7rZklz7/V6z09x7h7BrmIhJ8CPIS2bXP2vCjxF6Bdh2/Hxvc/SblXY9wi5UsBHjJfvGIJ73y4OaFt3ezJKafqgMa4RcqdAjwk0m0+tfySr1I7bAiAxrhFKpACPARy3XxKY9wilUUBXsLeeO8jjvhF4uZTL111ItVVOodDRBTggWpqjWYc8kjudX9x9524918PC6JMESlRCvCAJH/pGG3vYNailbzy9odcv/SlhHtfuXoSsQWxIiKf0u/iAZmzeE3CjBGILbzpGd7fPryBdbMnK7xFJC31wAOSbRMpnZAjItmoBx6QTAtsamuqFd4ikhMFeECO2HtESltNdRWXnbR/ANWISBhpCCUA6eZ112vhjYj0kQK8iK568HluevyVhDYNl4hIfynAi6Bzm7NX0uZTT190LCN3GBpQRSJSDhTgedDbgpxp85/m8Zfe7r53l+2H0PLvXw2qVBEpI1kD3Mw+C9wGjCR2+s48d7/ezHYG7gYaiJ3Ic7q7v1e4UktTpgU5Wzq3MWPhioR7n798IsOG6O9MEcmPXGahbAUucPfPA4cC3zezzwMzgaXuvg+wNH5dcTItyEkO7/raGh7+a++HL4iI9EXWAHf3De7+bPzx+8BqoB44GVgQv20BMLVANZa0XE917+qZN7VGC1yRiFSKPs0DN7MGYDzwNDDS3TfEn3qT2BBLutdMN7NmM2tua2sbSK0lKdOCnHSL37vOqBQRyYecA9zMPgPcC/zI3f/e8zl3d2Lj4yncfZ67R9w9UldXN6BiS9E5R+yR0lZTXZX+Xwa599hFRLLJKcDNrJpYeN/h7ovizW+Z2aj486OAjYUpsXR94bLFXP7A8wlt9bU1XH3qgdRn6JnrjEoRyZdcZqEYMB9Y7e7X9njq98BZwOz4z/sLUmEJWhXdxJQbnkhoS7cgR2dUikgh5TKn7XBgGrDSzJbH2y4iFtz3mNk5wKvA6QWpsMQkL4P/43lHst+oHVLu0xmVIlJoFhu+Lo5IJOLNzc1Fe798WrZmI9++5S/d1yN32I6nLzouwIpEpFKYWYu7R5LbtaokC3dnj1mJy+D/PGsCo3bUWLaIBEsB3ou7//IaF967svv6iL1HcPt3DwmwIhGRTynA00i3+dSKy45nh6HVAVUkIpJKAZ7k2ofX8B+PvNx9Pe3Q3bli6gEBViQikp4CPK5jcyf7XfJQQtuLV57IkME6tEhESpMCHDjvrlbuX76++/qiSZ9j+lF7BViRiEh2FR3g7364mYOuWJLQ9srVk4itXRIRKW0VG+D/eMMTrIxu6r6+4evj+cdxuwVYkYhI35RtgGc6JWfd2x9y9DWPJtyrcylFJIzKciVm8ik5ENuHZHPnNjq3ffp5755+KIfsuUvB6xERGYiKWomZ6ZScntTrFpGwK8sA723P7SXnH8U+I4cXsRoRkcIoy0nOmfbcrq+tUXiLSNkouwB3d3bZfkhKu/biFpFyU1ZDKM+93s7Jv36y+3qnYdW0f7RFe3GLSFkqiwDfts055cY/8dzr7QDsOnw7Hr/wGLYbXBVsYSIiBZTLkWq/AaYAG939gHjbZcD3gK5j5i9y9z+k/xMK6/GX2pg2/5nu61u//SWOHrtrEKWIiBRVLj3wW4H/BG5Lar/O3a/Je0U52rx1G1+Zs4wNmz4G4MD6HWn6/uFUDdIyeBGpDFkD3N0fM7OGItSSVroVlVWDjB/e2dp9z6J/O4yDxuwUVIkiIoEYyBj4D8zsW0AzcIG7v5fuJjObDkwHGDNmTJ/eIHlFZbS9gx/dvbz7+eP225WbvhXR5lMiUpH6O43wRmAvoBHYAPwq043uPs/dI+4eqaur69ObpFtR2eV/fnwUN5/1JYW3iFSsfgW4u7/l7p3uvg24CTg4v2XFZFpRacDeu2pBjohUtn4FuJmN6nF5CrAqP+UkyrSiMlO7iEglyRrgZnYn8GdgrJm9YWbnAL80s5VmtgI4Bji/EMXNmDiWmurEudxaUSkiEpPLLJSvp2meX4BaUnStnEy3r7eISKUr+ZWYU8fXK7BFRNIou82sREQqhQJcRCSkFOAiIiGlABcRCSkFuIhISCnARURCyty9eG9m1ga8WrQ37JsRwNtBFxEgff7K/fyV/NkhHJ9/d3dP2UyqqAFeysys2d0jQdcRFH3+yv38lfzZIdyfX0MoIiIhpQAXEQkpBfin5gVdQMD0+StXJX92CPHn1xi4iEhIqQcuIhJSCnARkZBSgANmVmVmrWb2QNC1FJuZ1ZrZQjN7wcxWm9mXg66pmMzsfDP7q5mtMrM7zWxo0DUVkpn9xsw2mtmqHm07m9kSM3sp/nOnIGsspAyff078//8VZnafmdUGWGKfKMBjzgNWB11EQK4HHnL3zwHjqKB/D2ZWD/wvIOLuBwBVwD8HW1XB3QqckNQ2E1jq7vsAS+PX5epWUj//EuAAd/8C8CIwq9hF9VfFB7iZjQYmAzcHXUuxmdmOwFHET1hy983u3h5oUcU3GKgxs8HAMGB9wPUUlLs/Bryb1HwysCD+eAEwtZg1FVO6z+/uD7v71vjlU8DoohfWTxUf4MBc4KfAtoDrCMIeQBtwS3wI6WYz2z7ooorF3aPANcBrwAZgk7s/HGxVgRjp7hvij98ERgZZTMC+A/wx6CJyVdEBbmZTgI3u3hJ0LQEZDBwE3Oju44EPKe9fnxPEx3pPJvYX2W7A9mb2zWCrCpbH5hVX5NxiM/sZsBW4I+haclXRAQ4cDpxkZuuAu4AJZnZ7sCUV1RvAG+7+dPx6IbFArxTHAa+4e5u7bwEWAYcFXFMQ3jKzUQDxnxsDrqfozOxsYApwpodocUxFB7i7z3L30e7eQOzLq0fcvWJ6YO7+JvC6mY2NNx0LPB9gScX2GnComQ0zMyP2+SvmS9wefg+cFX98FnB/gLUUnZmdQGwY9SR3/yjoevqi5E+ll4L7IXCHmQ0B1gLfDrieonH3p81sIfAssV+dWwnxsupcmNmdwNHACDN7A7gUmA3cY2bnENvu+fTgKiysDJ9/FrAdsCT29zhPufu5gRXZB1pKLyISUhU9hCIiEmYKcBGRkFKAi4iElAJcRCSkFOAiIiGlABcRCSkFuIhISP1/JgDCeeTfFPIAAAAASUVORK5CYII=\n" + }, + { + "id": 2034879286288, + "title": "Show user input", + "block_type": "OCBCodeBlock", + "splitter_pos": [ + 0, + 214 + ], + "position": [ + 757.0, + 619.0 + ], + "width": 685, + "height": 273, + "metadata": { + "title_metadata": { + "color": "white", + "font": "Ubuntu", + "size": 10 + } + }, + "sockets": [ + { + "id": 2034879287152, + "type": "input", + "position": [ + 0.0, + 53.0 + ], + "metadata": { + "color": "#FF55FFF0", + "linecolor": "#FF000000", + "linewidth": 1.0, + "radius": 10.0 + } + }, + { + "id": 2034879197248, + "type": "output", + "position": [ + 685.0, + 53.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 * 0.8999999999999999 + 0.24\nAccuracy:\n-6.737344734867353\n" + }, + { + "id": 2034886210608, + "title": "Create a new linear model", + "block_type": "OCBCodeBlock", + "splitter_pos": [ + 335, + 0 + ], + "position": [ + 10.0, + -562.0 + ], + "width": 656, + "height": 394, + "metadata": { + "title_metadata": { + "color": "white", + "font": "Ubuntu", + "size": 10 + } + }, + "sockets": [ + { + "id": 2034886211472, + "type": "input", + "position": [ + 0.0, + 53.0 + ], + "metadata": { + "color": "#FF55FFF0", + "linecolor": "#FF000000", + "linewidth": 1.0, + "radius": 10.0 + } + }, + { + "id": 2034886211616, + "type": "output", + "position": [ + 656.0, + 53.0 + ], + "metadata": { + "color": "#FF55FFF0", + "linecolor": "#FF000000", + "linewidth": 1.0, + "radius": 10.0 + } + } + ], + "source": "reg = 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, + 224 + ], + "position": [ + 800.0, + -219.0 + ], + "width": 670, + "height": 283, + "metadata": { + "title_metadata": { + "color": "white", + "font": "Ubuntu", + "size": 10 + } + }, + "sockets": [ + { + "id": 2136886540752, + "type": "input", + "position": [ + 0.0, + 53.0 + ], + "metadata": { + "color": "#FF55FFF0", + "linecolor": "#FF000000", + "linewidth": 1.0, + "radius": 10.0 + } + }, + { + "id": 2136886540896, + "type": "output", + "position": [ + 670.0, + 53.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": 2034686480592, + "path_type": "bezier", + "source": { + "block": 2034509423808, + "socket": 2034509424816 + }, + "destination": { + "block": 2034723533728, + "socket": 2034723534592 + } + }, + { + "id": 2034686599952, + "path_type": "bezier", + "source": { + "block": 2034723533728, + "socket": 2034723534736 + }, + "destination": { + "block": 2034686482320, + "socket": 2034686483184 + } + }, + { + "id": 2034879160672, + "path_type": "bezier", + "source": { + "block": 2034723714816, + "socket": 2034723715824 + }, + "destination": { + "block": 2034686482320, + "socket": 2034686483184 + } + }, + { + "id": 2034879161104, + "path_type": "bezier", + "source": { + "block": 2034723677808, + "socket": 2034723678816 + }, + "destination": { + "block": 2034686482320, + "socket": 2034686483184 + } + }, + { + "id": 2034882738640, + "path_type": "bezier", + "source": { + "block": 2034723533728, + "socket": 2034723534736 + }, + "destination": { + "block": 2034886210608, + "socket": 2034886211472 + } + }, + { + "id": 2034882739360, + "path_type": "bezier", + "source": { + "block": 2034886210608, + "socket": 2034886211616 + }, + "destination": { + "block": 2034879162976, + "socket": 2034879163840 + } + }, + { + "id": 2034884170944, + "path_type": "bezier", + "source": { + "block": 2034686482320, + "socket": 2034686483328 + }, + "destination": { + "block": 2034879286288, + "socket": 2034879287152 + } + }, + { + "id": 2136887093136, + "path_type": "bezier", + "source": { + "block": 2034886210608, + "socket": 2034886211616 + }, + "destination": { + "block": 2136886539168, + "socket": 2136886540752 + } + } + ] +} \ No newline at end of file diff --git a/opencodeblocks/blocks/codeblock.py b/opencodeblocks/blocks/codeblock.py index 3a984b51..2cae8fae 100644 --- a/opencodeblocks/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -3,11 +3,10 @@ """ Module for the base OCB Code Block. """ -from typing import List, OrderedDict +from typing import OrderedDict from PyQt5.QtWidgets import QPushButton, QTextEdit from ansi2html import Ansi2HTMLConverter -from networkx.algorithms.traversal.breadth_first_search import bfs_edges from opencodeblocks.blocks.executableblock import OCBExecutableBlock from opencodeblocks.graphics.socket import OCBSocket diff --git a/opencodeblocks/blocks/executableblock.py b/opencodeblocks/blocks/executableblock.py index 110226d2..b67f7535 100644 --- a/opencodeblocks/blocks/executableblock.py +++ b/opencodeblocks/blocks/executableblock.py @@ -2,7 +2,6 @@ from typing import List, OrderedDict -from ansi2html import Ansi2HTMLConverter from networkx.algorithms.traversal.breadth_first_search import bfs_edges from opencodeblocks.blocks.block import OCBBlock @@ -10,9 +9,6 @@ from opencodeblocks.graphics.kernel import get_main_kernel -conv = Ansi2HTMLConverter() - - class OCBExecutableBlock(OCBBlock): """ @@ -113,14 +109,14 @@ def run_left(self, in_run_right=False): """ Run all of the block's dependencies and then run the block """ - + if self.has_input(): # Create the graph from the scene graph = self.scene().create_graph() # BFS through the input graph edges = bfs_edges(graph, self, reverse=True) # Run the blocks found except self - blocks_to_run: List["OCBExecutableblock"] = [v for _, v in edges] + blocks_to_run: List["OCBExecutableBlock"] = [v for _, v in edges] for block in blocks_to_run[::-1]: if not block.has_been_run: block.run_code() diff --git a/opencodeblocks/blocks/sliderblock.py b/opencodeblocks/blocks/sliderblock.py index 89953728..ca17f17c 100644 --- a/opencodeblocks/blocks/sliderblock.py +++ b/opencodeblocks/blocks/sliderblock.py @@ -8,7 +8,6 @@ from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QHBoxLayout, QLabel, QLineEdit, QSlider, QVBoxLayout from opencodeblocks.blocks.executableblock import OCBExecutableBlock -from opencodeblocks.graphics.kernel import get_main_kernel class OCBSliderBlock(OCBExecutableBlock): """ @@ -23,7 +22,7 @@ def __init__(self, **kwargs): 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}") diff --git a/opencodeblocks/graphics/worker.py b/opencodeblocks/graphics/worker.py index 126df9d3..15323797 100644 --- a/opencodeblocks/graphics/worker.py +++ b/opencodeblocks/graphics/worker.py @@ -43,6 +43,7 @@ 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() def run(self): From e2d7689298405e7c84bb35662b2a991e13cfc96d Mon Sep 17 00:00:00 2001 From: AlexandreSajus Date: Sat, 11 Dec 2021 23:12:35 +0100 Subject: [PATCH 10/51] :tada: prototype of visual flow --- opencodeblocks/blocks/codeblock.py | 56 +++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/opencodeblocks/blocks/codeblock.py b/opencodeblocks/blocks/codeblock.py index 40d380cf..dddc0e66 100644 --- a/opencodeblocks/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -3,9 +3,16 @@ """ Module for the base OCB Code Block. """ +import time from typing import List, OrderedDict, Optional -from PyQt5.QtWidgets import QPushButton, QTextEdit, QWidget, QStyleOptionGraphicsItem -from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import ( + QApplication, + QPushButton, + QTextEdit, + QWidget, + QStyleOptionGraphicsItem, +) +from PyQt5.QtCore import QProcess, Qt from PyQt5.QtGui import QPen, QColor, QPainter, QPainterPath from ansi2html import Ansi2HTMLConverter @@ -49,7 +56,6 @@ def __init__(self, **kwargs): self._cached_stdout = "" self.has_been_run = False - self.run_color = 0 self._pen_outline = QPen(QColor("#7F000000")) self._pen_outline_running = QPen(QColor("#FF0000")) self._pen_outline_transmitting = QPen(QColor("#00ff00")) @@ -58,6 +64,7 @@ def __init__(self, **kwargs): self._pen_outline_running, self._pen_outline_transmitting, ] + self.run_color = 0 # Add exectution flow sockets exe_sockets = ( @@ -170,12 +177,33 @@ def run_left(self, in_right_button=False): 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] + blocks_to_run = [] + visited = [] + to_visit = [self] + + delay = 0.3 + + while len(to_visit) != 0: + edges_to_visit = [] + for block in to_visit: + block.run_color = 2 + QApplication.processEvents() + time.sleep(delay) + for block in to_visit: + block.run_color = 0 + for input_socket in block.sockets_in: + for edge in input_socket.edges: + edges_to_visit.append(edge) + for edge in edges_to_visit: + edge.run_color = 2 + QApplication.processEvents() + time.sleep(delay) + to_visit = [] + for edge in edges_to_visit: + edge.run_color = 0 + to_visit.append(edge.source_socket.block) + print(to_visit) + for block in blocks_to_run[::-1]: if not block.has_been_run: block.run_code() @@ -289,6 +317,16 @@ 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 + self.update() + @property def stdout(self) -> str: """Access the content of the output panel of the block""" From 27da4c176fbd1865c4728c9de82ee9ef314c3ca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Sat, 11 Dec 2021 23:25:37 +0100 Subject: [PATCH 11/51] =?UTF-8?q?=F0=9F=8E=89=20Make=20drawing=20block=20e?= =?UTF-8?q?xecutable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- opencodeblocks/blocks/drawingblock.py | 39 ++++++++++++++++++++------- opencodeblocks/blocks/sliderblock.py | 2 +- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/opencodeblocks/blocks/drawingblock.py b/opencodeblocks/blocks/drawingblock.py index f45cfc62..ba951c0f 100644 --- a/opencodeblocks/blocks/drawingblock.py +++ b/opencodeblocks/blocks/drawingblock.py @@ -4,18 +4,19 @@ 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 """ + on_value_changed = pyqtSignal() + def __init__(self, parent: QWidget): """ Create a new Drawable widget """ super().__init__(parent) @@ -27,14 +28,14 @@ def __init__(self, parent: QWidget): for _ in range(self.pixel_width): self.color_buffer.append([]) for _ in range(self.pixel_height): - # color hex encoded as AARRGGBB - self.color_buffer[-1].append(0xFFFFFFFF) + # 0 = white, 1 = black + self.color_buffer[-1].append(0) def clearDrawing(self): """ Clear the drawing """ for i in range(self.pixel_width): for j in range(self.pixel_height): - self.color_buffer[i][j] = 0xFFFFFFFF + self.color_buffer[i][j] = 0 def paintEvent(self, evt: QPaintEvent): """ Draw the content of the widget """ @@ -50,8 +51,9 @@ def paintEvent(self, evt: QPaintEvent): h * j, w + eps, h + eps, - QColor.fromRgb( - self.color_buffer[i][j])) + # hex color encoded as AARRGGBB + QColor.fromRgb(0xFF000000 if self.color_buffer[i][j] else 0xFFFFFFFF) + ) def mouseMoveEvent(self, evt: QMouseEvent): """ Change the drawing when dragging the mouse around""" @@ -59,8 +61,9 @@ def mouseMoveEvent(self, evt: QMouseEvent): x = floor(evt.x() / self.width() * self.pixel_width) y = floor(evt.y() / self.height() * self.pixel_height) if 0 <= x < self.pixel_width and 0 <= y < self.pixel_height: - self.color_buffer[x][y] = 0xFF000000 + self.color_buffer[x][y] = 1 self.repaint() + self.on_value_changed.emit() def mousePressEvent(self, evt: QMouseEvent): """ Signal that the drawing starts """ @@ -71,7 +74,7 @@ def mouseReleaseEvent(self, evt: QMouseEvent): self.mouse_down = False -class OCBDrawingBlock(OCBBlock): +class OCBDrawingBlock(OCBExecutableBlock): """ An OCBBlock on which you can draw, to test your CNNs for example""" def __init__(self, **kwargs): @@ -79,6 +82,8 @@ def __init__(self, **kwargs): super().__init__(**kwargs) self.draw_area = DrawableWidget(self.root) + self.draw_area.on_value_changed.connect(self.valueChanged) + self.var_name = "drawing" self.splitter.addWidget(self.draw_area) # QGraphicsView self.run_button = QPushButton("Clear", self.root) @@ -105,6 +110,20 @@ def serialize(self): return base_dict + def valueChanged(self): + """ Called when the content of the drawing block changes. """ + 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 """ diff --git a/opencodeblocks/blocks/sliderblock.py b/opencodeblocks/blocks/sliderblock.py index ca17f17c..cd4c2f01 100644 --- a/opencodeblocks/blocks/sliderblock.py +++ b/opencodeblocks/blocks/sliderblock.py @@ -49,7 +49,7 @@ def valueChanged(self): @property def source(self): - """ Get the source code of the slider """ + """ 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 @source.setter From a19cb24f3013e522fb65efa9e66caf5c9ab574b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Sat, 11 Dec 2021 23:32:19 +0100 Subject: [PATCH 12/51] =?UTF-8?q?=E2=9C=A8=20Black=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- opencodeblocks/blocks/__init__.py | 5 --- opencodeblocks/blocks/codeblock.py | 14 +++---- opencodeblocks/blocks/drawingblock.py | 53 +++++++++++++----------- opencodeblocks/blocks/executableblock.py | 50 +++++++--------------- opencodeblocks/graphics/kernel.py | 2 + 5 files changed, 53 insertions(+), 71 deletions(-) diff --git a/opencodeblocks/blocks/__init__.py b/opencodeblocks/blocks/__init__.py index 93e87f76..01b4f741 100644 --- a/opencodeblocks/blocks/__init__.py +++ b/opencodeblocks/blocks/__init__.py @@ -3,11 +3,6 @@ """ Module for the OCB Blocks of different types. """ -# Abstract blocks -from opencodeblocks.blocks.block import OCBBlock -from opencodeblocks.blocks.executableblock import OCBExecutableBlock - -# Real blocks from opencodeblocks.blocks.sliderblock import OCBSliderBlock from opencodeblocks.blocks.codeblock import OCBCodeBlock from opencodeblocks.blocks.markdownblock import OCBMarkdownBlock diff --git a/opencodeblocks/blocks/codeblock.py b/opencodeblocks/blocks/codeblock.py index 2cae8fae..3a52a5f3 100644 --- a/opencodeblocks/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -34,7 +34,7 @@ def __init__(self, source: str = "", **kwargs): """ super().__init__(**kwargs) self.source_editor = PythonEditor(self) - + self._source = "" self._stdout = "" @@ -74,30 +74,28 @@ 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.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.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""" + """Called when the button for "Run All" was pressed""" if self.is_running: self._interrupt_execution() else: self.run_right() def handle_run_left(self): - """ Called when the button for "Run Left" was pressed""" + """Called when the button for "Run Left" was pressed""" if self.is_running: self._interrupt_execution() else: @@ -112,7 +110,7 @@ def run_code(self): self.run_button.setText("...") self.run_all_button.setText("...") - super().run_code() # actually run the code + super().run_code() # actually run the code def execution_finished(self): """Reset the text of the run buttons""" diff --git a/opencodeblocks/blocks/drawingblock.py b/opencodeblocks/blocks/drawingblock.py index ba951c0f..98d1867f 100644 --- a/opencodeblocks/blocks/drawingblock.py +++ b/opencodeblocks/blocks/drawingblock.py @@ -12,13 +12,14 @@ 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 @@ -32,13 +33,13 @@ def __init__(self, parent: QWidget): 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] = 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): @@ -52,11 +53,13 @@ def paintEvent(self, evt: QPaintEvent): w + eps, h + eps, # hex color encoded as AARRGGBB - QColor.fromRgb(0xFF000000 if self.color_buffer[i][j] else 0xFFFFFFFF) + QColor.fromRgb( + 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) @@ -66,19 +69,19 @@ def mouseMoveEvent(self, evt: QMouseEvent): 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(OCBExecutableBlock): - """ An OCBBlock on which you can draw, to test your CNNs for example""" + """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) @@ -87,16 +90,17 @@ def __init__(self, **kwargs): 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 @@ -104,30 +108,31 @@ 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 valueChanged(self): - """ Called when the content of the drawing block changes. """ + """Called when the content of the drawing block changes.""" self.run_right() @property def source(self): - """ The "source code" of the drawingblock i.e an assignement to the drawing buffer """ + """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']: + 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 index b67f7535..e194f351 100644 --- a/opencodeblocks/blocks/executableblock.py +++ b/opencodeblocks/blocks/executableblock.py @@ -9,6 +9,7 @@ from opencodeblocks.graphics.kernel import get_main_kernel + class OCBExecutableBlock(OCBBlock): """ @@ -24,11 +25,11 @@ class OCBExecutableBlock(OCBBlock): def __init__(self, **kwargs): """ - Create a new executable block. - Do not call this method except when inheriting from this class. + Create a new executable block. + Do not call this method except when inheriting from this class. """ super().__init__(**kwargs) - + self.has_been_run = False self.is_running = False @@ -58,13 +59,13 @@ def has_output(self) -> bool: return False def run_code(self): - """ Run the code in the block""" + """Run the code in the block""" # Queue the code to execute code = self.source kernel = get_main_kernel() kernel.execution_queue.append((self, code)) - + self.is_running = True if kernel.busy is False: @@ -80,7 +81,7 @@ def execution_finished(self): @staticmethod def _interrupt_execution(): - """ Interrupt an execution, reset the blocks in the queue """ + """Interrupt an execution, reset the blocks in the queue""" kernel = get_main_kernel() for block, _ in kernel.execution_queue: # Reset the blocks that have not been run @@ -91,21 +92,7 @@ def _interrupt_execution(): # Interrupt the kernel kernel.kernel_manager.interrupt_kernel() - def has_input(self) -> bool: - """Checks whether a block has connected input blocks""" - for input_socket in self.sockets_in: - if len(input_socket.edges) != 0: - return True - return False - - def has_output(self) -> bool: - """Checks whether a block has connected output blocks""" - for output_socket in self.sockets_out: - if len(output_socket.edges) != 0: - return True - return False - - def run_left(self, in_run_right=False): + def run_left(self): """ Run all of the block's dependencies and then run the block """ @@ -120,8 +107,9 @@ def run_left(self, in_run_right=False): for block in blocks_to_run[::-1]: if not block.has_been_run: block.run_code() - - if self.is_running: return + + if self.is_running: + return self.run_code() def run_right(self): @@ -129,22 +117,20 @@ def run_right(self): # If no output, run left if not self.has_output(): - self.run_left(in_run_right=True) + self.run_left() return # Same as run_left but instead of running the blocks, we'll use run_left graph = self.scene().create_graph() edges = bfs_edges(graph, self) - blocks_to_run: List["OCBExecutableBlock"] = [ - self] + [v for _, v in edges] + blocks_to_run: List["OCBExecutableBlock"] = [self] + [v for _, v in edges] for block in blocks_to_run[::-1]: block.run_left(in_run_right=True) def reset_has_been_run(self): - """ Called when the output is an error """ + """Called when the output is an error""" self.has_been_run = False - @property def source(self) -> str: """Source code""" @@ -154,14 +140,11 @@ def source(self) -> str: def source(self, value: str): raise NotImplementedError("source(self) should be overriden") - def handle_stdout(self, value: str): """Handle the stdout signal""" - pass def handle_image(self, image: str): """Handle the image signal""" - pass def serialize(self): """Return a serialized version of this block""" @@ -173,7 +156,6 @@ def deserialize( self, data: OrderedDict, hashmap: dict = None, restore_id: bool = True ): """Restore a codeblock from it's serialized state""" - for dataname in ("source"): - if dataname in data: - setattr(self, dataname, data[dataname]) + if "source" in data: + setattr(self, "source", data["source"]) super().deserialize(data, hashmap, restore_id) diff --git a/opencodeblocks/graphics/kernel.py b/opencodeblocks/graphics/kernel.py index 4adf9d7b..2fbf5c30 100644 --- a/opencodeblocks/graphics/kernel.py +++ b/opencodeblocks/graphics/kernel.py @@ -145,7 +145,9 @@ def __del__(self): threadpool = QThreadPool() def get_main_kernel(): + """ Return a handle to the main kernel """ return kernel def get_main_threadpool(): + """ Return a handle to the thread pool """ return threadpool \ No newline at end of file From e9c26150c04568f65d934a9c974c04b390e9f9a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Sat, 11 Dec 2021 23:41:29 +0100 Subject: [PATCH 13/51] =?UTF-8?q?=F0=9F=AA=B2=20Make=20kernel=20an=20attri?= =?UTF-8?q?bute=20of=20scene=20so=20that=20when=20multiple=20files=20are?= =?UTF-8?q?=20opened,=20they=20do=20not=20share=20the=20same=20kernel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- opencodeblocks/blocks/containerblock.py | 78 ++---------------------- opencodeblocks/blocks/executableblock.py | 6 +- opencodeblocks/graphics/kernel.py | 15 +---- opencodeblocks/graphics/pyeditor.py | 3 - opencodeblocks/graphics/view.py | 3 +- opencodeblocks/scene/scene.py | 6 +- 6 files changed, 14 insertions(+), 97 deletions(-) diff --git a/opencodeblocks/blocks/containerblock.py b/opencodeblocks/blocks/containerblock.py index 21505b78..93cf4317 100644 --- a/opencodeblocks/blocks/containerblock.py +++ b/opencodeblocks/blocks/containerblock.py @@ -1,86 +1,16 @@ -# OpenCodeBlock an open-source tool for modular visual programing in python - """ -Exports OCBSliderBlock. +Exports OCBContainerBlock. """ -from typing import OrderedDict -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QHBoxLayout, QLabel, QLineEdit, QSlider, QVBoxLayout from opencodeblocks.blocks.block import OCBBlock -class OCBSliderBlock(OCBBlock): +class OCBContainerBlock(OCBBlock): """ - Features a slider ranging from 0 to 1 and an area to choose what value to assign the slider to. + A block that can contain other blocks. """ def __init__(self, **kwargs): super().__init__(**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_text = QLineEdit("slider_value") - self.variable_value = QLabel(f"{self.slider.value()/100}") - - self.variable_text.setFixedWidth(self.root.width() / 2) - - self.variable_layout.addWidget(self.variable_text) - self.variable_layout.addWidget(self.variable_value) - - self.layout.setContentsMargins( - self.edge_size * 2, - self.title_widget.height() + self.edge_size * 2, - self.edge_size * 2, - self.edge_size * 2 - ) - self.layout.addWidget(self.slider) - self.layout.addLayout(self.variable_layout) - - 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}") - - # The code execution part will be added when the execution flow is merged. - # We print for now - print(python_code) - - @property - def value(self): - """ The value of the slider """ - return str(self.slider.value() / 100) - @value.setter - def value(self, value: str): - self.slider.setValue(int(float(value) * 100)) - - @property - def var_name(self): - """ The name of the python variable associated with the slider """ - return self.variable_text.text() - @var_name.setter - def var_name(self, value: str): - self.variable_text.setText(value) - - def serialize(self): - """ Return a serialized version of this widget """ - base_dict = super().serialize() - base_dict["value"] = self.value - base_dict["var_name"] = self.var_name - - return base_dict - - def deserialize(self, data: OrderedDict, - hashmap: dict = None, restore_id: bool = True): - """ Restore a slider block from it's serialized state """ - for dataname in ['value','var_name']: - if dataname in data: - setattr(self, dataname, data[dataname]) - - super().deserialize(data, hashmap, restore_id) + # WIP \ No newline at end of file diff --git a/opencodeblocks/blocks/executableblock.py b/opencodeblocks/blocks/executableblock.py index e194f351..79a7325f 100644 --- a/opencodeblocks/blocks/executableblock.py +++ b/opencodeblocks/blocks/executableblock.py @@ -7,8 +7,6 @@ from opencodeblocks.blocks.block import OCBBlock from opencodeblocks.graphics.socket import OCBSocket -from opencodeblocks.graphics.kernel import get_main_kernel - class OCBExecutableBlock(OCBBlock): @@ -63,7 +61,7 @@ def run_code(self): # Queue the code to execute code = self.source - kernel = get_main_kernel() + kernel = self.scene().kernel kernel.execution_queue.append((self, code)) self.is_running = True @@ -82,7 +80,7 @@ def execution_finished(self): @staticmethod def _interrupt_execution(): """Interrupt an execution, reset the blocks in the queue""" - kernel = get_main_kernel() + kernel = self.scene().kernel for block, _ in kernel.execution_queue: # Reset the blocks that have not been run block.execution_finished() diff --git a/opencodeblocks/graphics/kernel.py b/opencodeblocks/graphics/kernel.py index 2fbf5c30..660bfc4d 100644 --- a/opencodeblocks/graphics/kernel.py +++ b/opencodeblocks/graphics/kernel.py @@ -3,7 +3,6 @@ import queue from typing import Tuple -from PyQt5.QtCore import QThreadPool from jupyter_client.manager import start_new_kernel from opencodeblocks.graphics.worker import Worker @@ -71,7 +70,7 @@ def run_block(self, block, code: str): worker.signals.finished.connect(self.run_queue) worker.signals.finished.connect(block.execution_finished) worker.signals.error.connect(block.reset_has_been_run) - get_main_threadpool().start(worker) + block.scene().threadpool.start(worker) def run_queue(self): """ Runs the next code in the queue """ @@ -139,15 +138,3 @@ def __del__(self): Shuts down the kernel """ self.kernel_manager.shutdown_kernel() - - -kernel = Kernel() -threadpool = QThreadPool() - -def get_main_kernel(): - """ Return a handle to the main kernel """ - return kernel - -def get_main_threadpool(): - """ Return a handle to the thread pool """ - return threadpool \ No newline at end of file diff --git a/opencodeblocks/graphics/pyeditor.py b/opencodeblocks/graphics/pyeditor.py index 766e5be9..26d27772 100644 --- a/opencodeblocks/graphics/pyeditor.py +++ b/opencodeblocks/graphics/pyeditor.py @@ -10,7 +10,6 @@ from opencodeblocks.graphics.theme_manager import theme_manager from opencodeblocks.blocks.block import OCBBlock -from opencodeblocks.graphics.kernel import get_main_kernel, get_main_threadpool if TYPE_CHECKING: from opencodeblocks.graphics.view import OCBView @@ -30,8 +29,6 @@ def __init__(self, block: OCBBlock): super().__init__(None) self._mode = "NOOP" self.block = block - self.kernel = get_main_kernel() - self.threadpool = get_main_threadpool() self.update_theme() theme_manager().themeChanged.connect(self.update_theme) diff --git a/opencodeblocks/graphics/view.py b/opencodeblocks/graphics/view.py index 30a927ac..485eacaa 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 import OCBCodeBlock EPS: float = 1e-10 # To check if blocks are of size 0 diff --git a/opencodeblocks/scene/scene.py b/opencodeblocks/scene/scene.py index 9069e309..3539be40 100644 --- a/opencodeblocks/scene/scene.py +++ b/opencodeblocks/scene/scene.py @@ -8,7 +8,7 @@ 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 @@ -19,6 +19,7 @@ from opencodeblocks.graphics.edge import OCBEdge from opencodeblocks.scene.clipboard import SceneClipboard from opencodeblocks.scene.history import SceneHistory +from opencodeblocks.graphics.kernel import Kernel import networkx as nx @@ -52,6 +53,9 @@ def __init__(self, parent=None, 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. """ From 48ac3e8bb9322c5196835dbc79f827393224853a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Sun, 12 Dec 2021 00:00:07 +0100 Subject: [PATCH 14/51] =?UTF-8?q?=E2=9C=A8=20Fixes=20to=20pass=20the=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- opencodeblocks/blocks/__init__.py | 2 -- opencodeblocks/blocks/executableblock.py | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/opencodeblocks/blocks/__init__.py b/opencodeblocks/blocks/__init__.py index 01b4f741..22044feb 100644 --- a/opencodeblocks/blocks/__init__.py +++ b/opencodeblocks/blocks/__init__.py @@ -7,5 +7,3 @@ from opencodeblocks.blocks.codeblock import OCBCodeBlock from opencodeblocks.blocks.markdownblock import OCBMarkdownBlock from opencodeblocks.blocks.drawingblock import OCBDrawingBlock - - diff --git a/opencodeblocks/blocks/executableblock.py b/opencodeblocks/blocks/executableblock.py index 79a7325f..985629d5 100644 --- a/opencodeblocks/blocks/executableblock.py +++ b/opencodeblocks/blocks/executableblock.py @@ -77,8 +77,7 @@ def execution_finished(self): """ self.is_running = False - @staticmethod - def _interrupt_execution(): + def _interrupt_execution(self): """Interrupt an execution, reset the blocks in the queue""" kernel = self.scene().kernel for block, _ in kernel.execution_queue: From 17fef8a44557a5e913c681f206cad858737d1f6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Sun, 12 Dec 2021 00:09:36 +0100 Subject: [PATCH 15/51] =?UTF-8?q?=E2=9C=A8=20Refactoring=20tests=20to=20pa?= =?UTF-8?q?ss=20CI=20...?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/integration/blocks/test_block.py | 9 ++------- tests/integration/blocks/test_codeblock.py | 21 ++++++--------------- tests/integration/utils.py | 7 +++++++ 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/tests/integration/blocks/test_block.py b/tests/integration/blocks/test_block.py index c14b34d1..b07cecc1 100644 --- a/tests/integration/blocks/test_block.py +++ b/tests/integration/blocks/test_block.py @@ -12,20 +12,15 @@ from PyQt5.QtCore import QPointF from opencodeblocks.blocks.block import OCBBlock -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 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): diff --git a/tests/integration/blocks/test_codeblock.py b/tests/integration/blocks/test_codeblock.py index 7ab6b67b..47a8cd28 100644 --- a/tests/integration/blocks/test_codeblock.py +++ b/tests/integration/blocks/test_codeblock.py @@ -5,30 +5,25 @@ Integration tests for the OCBCodeBlocks. """ +import time 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: +class TestCodeBlocks(): @pytest.fixture(autouse=True) def setup(self): """ Setup reused variables. """ - self.window = OCBWindow() - self.ocb_widget = OCBWidget() - self.subwindow = self.window.mdiArea.addSubWindow(self.ocb_widget) - self.subwindow.show() + start_app(self) - def test_run_python(self, qtbot: QtBot): + def test_run_python(self): """ run source code when run button is pressed. """ # Add a block with the source to the window @@ -56,12 +51,8 @@ def testing_run(msgQueue: CheckingQueue): pyautogui.mouseDown(button="left") pyautogui.mouseUp(button="left") - # qtbot.mouseMove(test_block.run_button) - # qtbot.mousePress(test_block.run_button, - # Qt.MouseButton.LeftButton, delay=1) - # qtbot.mouseRelease(test_block.run_button, Qt.MouseButton.LeftButton) + time.sleep(0.5) - # When the execution becomes non-blocking for the UI, a refactor will be needed here. msgQueue.check_equal(test_block.stdout.strip(), expected_result) msgQueue.stop() diff --git a/tests/integration/utils.py b/tests/integration/utils.py index d5cde156..20ed92ea 100644 --- a/tests/integration/utils.py +++ b/tests/integration/utils.py @@ -13,6 +13,7 @@ from queue import Queue from qtpy.QtWidgets import QApplication import pytest_check as check +from opencodeblocks.graphics.widget import OCBWidget from opencodeblocks.graphics.window import OCBWindow @@ -33,6 +34,12 @@ def stop(self): self.put([STOP_MSG]) +def start_app(obj): + 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 From 2e4b40227ec3a5e67a57548f9747a3e3ea15dcf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Sun, 12 Dec 2021 00:39:14 +0100 Subject: [PATCH 16/51] =?UTF-8?q?=F0=9F=AA=B2=20Fix=20for=20#94=20as=20wel?= =?UTF-8?q?l=20as=20*ExecutingBlock*=20serialization=20issues.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- opencodeblocks/blocks/codeblock.py | 2 +- opencodeblocks/blocks/drawingblock.py | 4 +++- opencodeblocks/blocks/executableblock.py | 8 ++------ opencodeblocks/blocks/sliderblock.py | 7 +++++-- opencodeblocks/scene/scene.py | 6 ++++++ 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/opencodeblocks/blocks/codeblock.py b/opencodeblocks/blocks/codeblock.py index 3a52a5f3..5c43f4ef 100644 --- a/opencodeblocks/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -226,7 +226,7 @@ def serialize(self): base_dict = super().serialize() base_dict["source"] = self.source base_dict["stdout"] = self.stdout - + return base_dict def deserialize( diff --git a/opencodeblocks/blocks/drawingblock.py b/opencodeblocks/blocks/drawingblock.py index 98d1867f..5d36e8f7 100644 --- a/opencodeblocks/blocks/drawingblock.py +++ b/opencodeblocks/blocks/drawingblock.py @@ -116,7 +116,9 @@ def serialize(self): def valueChanged(self): """Called when the content of the drawing block changes.""" - self.run_right() + # 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): diff --git a/opencodeblocks/blocks/executableblock.py b/opencodeblocks/blocks/executableblock.py index 985629d5..63a82a58 100644 --- a/opencodeblocks/blocks/executableblock.py +++ b/opencodeblocks/blocks/executableblock.py @@ -122,7 +122,7 @@ def run_right(self): edges = bfs_edges(graph, self) blocks_to_run: List["OCBExecutableBlock"] = [self] + [v for _, v in edges] for block in blocks_to_run[::-1]: - block.run_left(in_run_right=True) + block.run_left() def reset_has_been_run(self): """Called when the output is an error""" @@ -145,14 +145,10 @@ def handle_image(self, image: str): def serialize(self): """Return a serialized version of this block""" - base_dict = super().serialize() - base_dict["source"] = self.source - return base_dict + return super().serialize() def deserialize( self, data: OrderedDict, hashmap: dict = None, restore_id: bool = True ): """Restore a codeblock from it's serialized state""" - if "source" in data: - setattr(self, "source", data["source"]) super().deserialize(data, hashmap, restore_id) diff --git a/opencodeblocks/blocks/sliderblock.py b/opencodeblocks/blocks/sliderblock.py index cd4c2f01..4d906e98 100644 --- a/opencodeblocks/blocks/sliderblock.py +++ b/opencodeblocks/blocks/sliderblock.py @@ -20,7 +20,6 @@ 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.variable_text = QLineEdit("slider_value") @@ -40,12 +39,16 @@ 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 """ self.variable_value.setText(f"{self.value}") - self.run_right() + # 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): diff --git a/opencodeblocks/scene/scene.py b/opencodeblocks/scene/scene.py index 3539be40..019b5a1a 100644 --- a/opencodeblocks/scene/scene.py +++ b/opencodeblocks/scene/scene.py @@ -5,6 +5,7 @@ import math import json +from os import path from types import FunctionType, ModuleType from typing import List, OrderedDict, Union @@ -154,6 +155,11 @@ 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__(\"sys\").path[0] = {dir_path}" + self.kernel.execute(setup_path_code) + def load_from_ipyg(self, filepath: str): """ Load an interactive python graph (.ipyg) into the scene. From 58df0ce278415809a4e8abd4ba8222ba70cce496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Sun, 12 Dec 2021 00:55:28 +0100 Subject: [PATCH 17/51] =?UTF-8?q?=F0=9F=8E=89=20Work=20on=20implementing?= =?UTF-8?q?=20the=20container.=20A=20circular=20dependency=20needs=20to=20?= =?UTF-8?q?be=20removed.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blocks/container.ocbb | 16 ++++++++++++++++ opencodeblocks/blocks/__init__.py | 1 + opencodeblocks/blocks/containerblock.py | 13 ++++++++++++- opencodeblocks/graphics/view.py | 2 +- opencodeblocks/scene/scene.py | 3 +-- 5 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 blocks/container.ocbb 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/opencodeblocks/blocks/__init__.py b/opencodeblocks/blocks/__init__.py index 22044feb..3a2eca69 100644 --- a/opencodeblocks/blocks/__init__.py +++ b/opencodeblocks/blocks/__init__.py @@ -7,3 +7,4 @@ 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/containerblock.py b/opencodeblocks/blocks/containerblock.py index 93cf4317..c9026243 100644 --- a/opencodeblocks/blocks/containerblock.py +++ b/opencodeblocks/blocks/containerblock.py @@ -2,7 +2,11 @@ Exports OCBContainerBlock. """ +from PyQt5.QtWidgets import QVBoxLayout from opencodeblocks.blocks.block import OCBBlock +from opencodeblocks.graphics.view import OCBView +from opencodeblocks.graphics.widget import OCBWidget +from opencodeblocks.scene.scene import OCBScene class OCBContainerBlock(OCBBlock): @@ -13,4 +17,11 @@ class OCBContainerBlock(OCBBlock): def __init__(self, **kwargs): super().__init__(**kwargs) - # WIP \ No newline at end of file + self.layout = QVBoxLayout(self.root) + + self.scene = OCBScene() + self.scene.addHasBeenModifiedListener(self.updateTitle) + self.view = OCBView(self.scene) + self.layout.addWidget(self.view) + + self.holder.setWidget(self.root) diff --git a/opencodeblocks/graphics/view.py b/opencodeblocks/graphics/view.py index 485eacaa..9ace412f 100644 --- a/opencodeblocks/graphics/view.py +++ b/opencodeblocks/graphics/view.py @@ -17,7 +17,7 @@ from opencodeblocks.graphics.socket import OCBSocket from opencodeblocks.graphics.edge import OCBEdge from opencodeblocks.blocks.block import OCBBlock -from opencodeblocks.blocks import OCBCodeBlock +from opencodeblocks.blocks.codeblock import OCBCodeBlock EPS: float = 1e-10 # To check if blocks are of size 0 diff --git a/opencodeblocks/scene/scene.py b/opencodeblocks/scene/scene.py index 019b5a1a..d4f3cb8d 100644 --- a/opencodeblocks/scene/scene.py +++ b/opencodeblocks/scene/scene.py @@ -13,8 +13,6 @@ 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 @@ -218,6 +216,7 @@ def create_block(self, data: OrderedDict, hashmap: dict = None, """ Create a new block from an OrderedDict """ block = None + blocks = __import__("opencodeblocks.blocks") block_constructor = None block_files = blocks.__dict__ From 4a58079e354f37033b449a0ffccef76aa8f46749 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Sun, 12 Dec 2021 01:29:59 +0100 Subject: [PATCH 18/51] =?UTF-8?q?=F0=9F=8E=89=20The=20container=20node=20w?= =?UTF-8?q?orks=20!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- opencodeblocks/blocks/containerblock.py | 21 +++++++++++++++++---- opencodeblocks/graphics/view.py | 6 ++++++ tests/integration/blocks/test_flow.py | 2 +- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/opencodeblocks/blocks/containerblock.py b/opencodeblocks/blocks/containerblock.py index 7dfa4567..3e52fb7b 100644 --- a/opencodeblocks/blocks/containerblock.py +++ b/opencodeblocks/blocks/containerblock.py @@ -2,6 +2,7 @@ Exports OCBContainerBlock. """ +from PyQt5.QtGui import QColor from PyQt5.QtWidgets import QVBoxLayout from opencodeblocks.blocks.block import OCBBlock @@ -14,11 +15,23 @@ class OCBContainerBlock(OCBBlock): 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.scene = OCBScene() - #self.scene.addHasBeenModifiedListener(self.updateTitle) - #self.view = OCBView(self.scene) - #self.layout.addWidget(self.view) + 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/graphics/view.py b/opencodeblocks/graphics/view.py index 9ace412f..152f192c 100644 --- a/opencodeblocks/graphics/view.py +++ b/opencodeblocks/graphics/view.py @@ -316,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/tests/integration/blocks/test_flow.py b/tests/integration/blocks/test_flow.py index 22a297d7..9b2a947d 100644 --- a/tests/integration/blocks/test_flow.py +++ b/tests/integration/blocks/test_flow.py @@ -52,7 +52,7 @@ def run_block(): block_to_run.run_left() msgQueue.run_lambda(run_block) - time.sleep(0.5) + time.sleep(1) msgQueue.check_equal(block_to_run.stdout.strip(), "6") msgQueue.check_equal(block_to_not_run.stdout.strip(), "") From c3707b5ff85d5dd2a24194ce337b285c31985acc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Sun, 12 Dec 2021 01:38:13 +0100 Subject: [PATCH 19/51] =?UTF-8?q?=E2=9C=A8=20Useless=20changes=20to=20make?= =?UTF-8?q?=20the=20CI=20happy=20(that=20needlessly=20steal=20the=20time?= =?UTF-8?q?=20of=20the=20developer=20that=20might=20have=20been=20better?= =?UTF-8?q?=20spent=20implementing=20useful=20features=20or=20resolving=20?= =?UTF-8?q?bugs)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- opencodeblocks/blocks/codeblock.py | 2 +- opencodeblocks/blocks/containerblock.py | 1 - opencodeblocks/graphics/widget.py | 1 + tests/integration/blocks/test_flow.py | 5 ++--- tests/integration/utils.py | 1 + 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/opencodeblocks/blocks/codeblock.py b/opencodeblocks/blocks/codeblock.py index 77d59f69..db6ce55a 100644 --- a/opencodeblocks/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -234,7 +234,7 @@ def serialize(self): base_dict = super().serialize() base_dict["source"] = self.source base_dict["stdout"] = self.stdout - + return base_dict def deserialize( diff --git a/opencodeblocks/blocks/containerblock.py b/opencodeblocks/blocks/containerblock.py index 3e52fb7b..86ea2a13 100644 --- a/opencodeblocks/blocks/containerblock.py +++ b/opencodeblocks/blocks/containerblock.py @@ -2,7 +2,6 @@ Exports OCBContainerBlock. """ -from PyQt5.QtGui import QColor from PyQt5.QtWidgets import QVBoxLayout from opencodeblocks.blocks.block import OCBBlock diff --git a/opencodeblocks/graphics/widget.py b/opencodeblocks/graphics/widget.py index a3910319..0836b437 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/tests/integration/blocks/test_flow.py b/tests/integration/blocks/test_flow.py index 9b2a947d..70858a05 100644 --- a/tests/integration/blocks/test_flow.py +++ b/tests/integration/blocks/test_flow.py @@ -7,7 +7,6 @@ import pyautogui import pytest -from pytestqt.qtbot import QtBot import time @@ -40,7 +39,7 @@ def setup(self): if item.title in titles: self.blocks_to_run[titles.index(item.title)] = item - def test_flow_left(self, qtbot: QtBot): + def test_flow_left(self): """ Correct flow when pressing left run """ def testing_run(msgQueue: CheckingQueue): @@ -52,7 +51,7 @@ def run_block(): block_to_run.run_left() msgQueue.run_lambda(run_block) - time.sleep(1) + time.sleep(3) msgQueue.check_equal(block_to_run.stdout.strip(), "6") msgQueue.check_equal(block_to_not_run.stdout.strip(), "") diff --git a/tests/integration/utils.py b/tests/integration/utils.py index 20ed92ea..0cb37a51 100644 --- a/tests/integration/utils.py +++ b/tests/integration/utils.py @@ -35,6 +35,7 @@ def stop(self): 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) From 99ffc0c52697554ef4b3b3d67dea349d2719b781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Sun, 12 Dec 2021 01:45:55 +0100 Subject: [PATCH 20/51] =?UTF-8?q?=F0=9F=AA=B2=20Change=20CWD=20instead=20o?= =?UTF-8?q?f=20path.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- opencodeblocks/scene/scene.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opencodeblocks/scene/scene.py b/opencodeblocks/scene/scene.py index d21981a0..d4b513b4 100644 --- a/opencodeblocks/scene/scene.py +++ b/opencodeblocks/scene/scene.py @@ -183,7 +183,7 @@ def load(self, filepath: str): # Add filepath to kernel path dir_path = repr(path.abspath(path.dirname(filepath))) - setup_path_code = f"__import__(\"sys\").path[0] = {dir_path}" + setup_path_code = f"__import__(\"os\").chdir({dir_path})" self.kernel.execute(setup_path_code) def load_from_json(self, filepath: str): From 93b13c5c35514129ba1702e8506cdd1aa7abcf49 Mon Sep 17 00:00:00 2001 From: AlexandreSajus Date: Sun, 12 Dec 2021 02:37:14 +0100 Subject: [PATCH 21/51] :tada: fully functional visual flow --- opencodeblocks/blocks/codeblock.py | 98 +++++++++++++++++++++++------- opencodeblocks/graphics/edge.py | 16 ++++- opencodeblocks/graphics/kernel.py | 46 +++++++------- opencodeblocks/scene/scene.py | 13 ---- 4 files changed, 114 insertions(+), 59 deletions(-) diff --git a/opencodeblocks/blocks/codeblock.py b/opencodeblocks/blocks/codeblock.py index dddc0e66..960efcdd 100644 --- a/opencodeblocks/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -3,8 +3,7 @@ """ Module for the base OCB Code Block. """ -import time -from typing import List, OrderedDict, Optional +from typing import OrderedDict, Optional from PyQt5.QtWidgets import ( QApplication, QPushButton, @@ -12,11 +11,10 @@ QWidget, QStyleOptionGraphicsItem, ) -from PyQt5.QtCore import QProcess, Qt +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.graphics.socket import OCBSocket @@ -24,6 +22,8 @@ conv = Ansi2HTMLConverter() +transmitting_delay = 100 + class OCBCodeBlock(OCBBlock): @@ -66,6 +66,8 @@ def __init__(self, **kwargs): ] self.run_color = 0 + self.transmitting_queue = [] + # Add exectution flow sockets exe_sockets = ( OCBSocket(self, socket_type="input", flow_type="exe"), @@ -164,6 +166,20 @@ def _interrupt_execution(self): # Interrupt the kernel self.source_editor.kernel.kernel_manager.interrupt_kernel() + def transmitting_animation_in(self): + for elem in self.transmitting_queue[0]: + elem.run_color = 2 + QApplication.processEvents() + QTimer.singleShot(transmitting_delay, self.transmitting_animation_out) + + def transmitting_animation_out(self): + for elem in self.transmitting_queue[0]: + elem.run_color = 0 + QApplication.processEvents() + self.transmitting_queue.pop(0) + if len(self.transmitting_queue) != 0: + self.transmitting_animation_in() + def run_left(self, in_right_button=False): """ Run all of the block's dependencies and then run the block @@ -178,32 +194,27 @@ def run_left(self, in_right_button=False): return self.run_code() blocks_to_run = [] - visited = [] to_visit = [self] - - delay = 0.3 + to_transmit = [to_visit] while len(to_visit) != 0: + to_visit = list(set(to_visit)) edges_to_visit = [] for block in to_visit: - block.run_color = 2 - QApplication.processEvents() - time.sleep(delay) - for block in to_visit: - block.run_color = 0 + blocks_to_run.append(block) for input_socket in block.sockets_in: for edge in input_socket.edges: edges_to_visit.append(edge) - for edge in edges_to_visit: - edge.run_color = 2 - QApplication.processEvents() - time.sleep(delay) + to_transmit.append(edges_to_visit) to_visit = [] for edge in edges_to_visit: - edge.run_color = 0 to_visit.append(edge.source_socket.block) - print(to_visit) + to_transmit.append(to_visit) + + self.transmitting_queue = to_transmit + self.transmitting_animation_in() + blocks_to_run.pop(0) for block in blocks_to_run[::-1]: if not block.has_been_run: block.run_code() @@ -232,11 +243,53 @@ def run_right(self): return self.run_left(in_right_button=True) # Same as run_left but instead of run_color 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] + blocks_to_run = [] + to_visit = [self] + to_transmit = [to_visit] + + while len(to_visit) != 0: + to_visit = list(set(to_visit)) + edges_to_visit = [] + for block in to_visit: + blocks_to_run.append(block) + for output_socket in block.sockets_out: + for edge in output_socket.edges: + edges_to_visit.append(edge) + to_transmit.append(edges_to_visit) + to_visit = [] + for edge in edges_to_visit: + to_visit.append(edge.destination_socket.block) + to_transmit.append(to_visit) + + blocks_to_run.pop(0) + for block in blocks_to_run[::-1]: + new_blocks_to_run = [] + to_visit = [block] + to_transmit.append(to_visit) + + while len(to_visit) != 0: + # Remove duplicates in to_visit + to_visit = list(set(to_visit)) + edges_to_visit = [] + for block in to_visit: + new_blocks_to_run.append(block) + for input_socket in block.sockets_in: + for edge in input_socket.edges: + edges_to_visit.append(edge) + to_transmit.append(edges_to_visit) + to_visit = [] + for edge in edges_to_visit: + to_visit.append(edge.source_socket.block) + to_transmit.append(to_visit) + new_blocks_to_run.pop(0) + blocks_to_run += new_blocks_to_run + + self.transmitting_queue = to_transmit + + self.transmitting_animation_in() for block in blocks_to_run[::-1]: - block.run_left(in_right_button=True) + if not block.has_been_run: + block.run_code() def reset_has_been_run(self): """Reset has_been_run, is called when the output is an error""" @@ -244,6 +297,7 @@ def reset_has_been_run(self): def reset_buttons(self): """Reset the buttons""" + self.run_color = 0 self.run_button.setText(">") self.run_all_button.setText(">>") diff --git a/opencodeblocks/graphics/edge.py b/opencodeblocks/graphics/edge.py index eb0b71f6..44451b92 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 @@ -248,3 +252,13 @@ 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 + self.update() diff --git a/opencodeblocks/graphics/kernel.py b/opencodeblocks/graphics/kernel.py index c5a1f509..326651d1 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,6 +64,7 @@ def run_block(self, block, code: str): code: String representing a piece of Python code to execute """ worker = Worker(self, code) + 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) @@ -73,7 +73,7 @@ def run_block(self, block, code: str): block.source_editor.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 +114,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/scene/scene.py b/opencodeblocks/scene/scene.py index ff2b85cb..a9abc436 100644 --- a/opencodeblocks/scene/scene.py +++ b/opencodeblocks/scene/scene.py @@ -20,8 +20,6 @@ from opencodeblocks.scene.clipboard import SceneClipboard from opencodeblocks.scene.history import SceneHistory -import networkx as nx - class OCBScene(QGraphicsScene, Serializable): @@ -189,17 +187,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: From ee4af35f8b6d9de0ddcb7005ed3f33cbd4300470 Mon Sep 17 00:00:00 2001 From: AlexandreSajus Date: Sun, 12 Dec 2021 03:23:48 +0100 Subject: [PATCH 22/51] :hammer: Huge refactor --- opencodeblocks/blocks/codeblock.py | 193 ++++++++++++++++------------- opencodeblocks/graphics/edge.py | 4 + opencodeblocks/graphics/kernel.py | 3 +- opencodeblocks/scene/scene.py | 2 + 4 files changed, 114 insertions(+), 88 deletions(-) diff --git a/opencodeblocks/blocks/codeblock.py b/opencodeblocks/blocks/codeblock.py index 960efcdd..802a5a94 100644 --- a/opencodeblocks/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -22,8 +22,6 @@ conv = Ansi2HTMLConverter() -transmitting_delay = 100 - class OCBCodeBlock(OCBBlock): @@ -64,9 +62,15 @@ def __init__(self, **kwargs): self._pen_outline_running, self._pen_outline_transmitting, ] + # 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 + self.transmitting_delay = 200 # Add exectution flow sockets exe_sockets = ( @@ -159,7 +163,7 @@ 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.reset_after_run() block.has_been_run = False # Clear the queue self.source_editor.kernel.execution_queue = [] @@ -167,12 +171,20 @@ def _interrupt_execution(self): self.source_editor.kernel.kernel_manager.interrupt_kernel() 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]: elem.run_color = 2 QApplication.processEvents() - QTimer.singleShot(transmitting_delay, self.transmitting_animation_out) + 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]: elem.run_color = 0 QApplication.processEvents() @@ -180,127 +192,131 @@ def transmitting_animation_out(self): if len(self.transmitting_queue) != 0: self.transmitting_animation_in() - def run_left(self, in_right_button=False): + def custom_bfs(self, start_node, reverse=False): """ - Run all of the block's dependencies and then run the block - """ - # If the user presses left run when run_color, cancel the execution - if self.run_button.text() == "..." and not in_right_button: - self._interrupt_execution() - return + Graph traversal in BFS to find the blocks that are connected to the start_node - # If no dependencies - if not self.has_input(): - return self.run_code() + 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 = [] - to_visit = [self] - to_transmit = [to_visit] + # 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) - for input_socket in block.sockets_in: - for edge in input_socket.edges: - edges_to_visit.append(edge) + 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: - to_visit.append(edge.source_socket.block) + if not reverse: + to_visit.append(edge.source_socket.block) + else: + to_visit.append(edge.destination_socket.block) to_transmit.append(to_visit) - self.transmitting_queue = to_transmit - - self.transmitting_animation_in() + # Remove start node blocks_to_run.pop(0) + + return blocks_to_run, to_transmit + + def run_blocks(self, blocks_to_run): + """Run a list of blocks""" 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_left(self): + """Run all of the block's dependencies and then run the block""" + + # To avoid crashing when spamming the button + if len(self.transmitting_queue) != 0: + return + + # If the user presses left run when running, cancel the execution + if self.run_button.text() == "...": + self._interrupt_execution() + return + + # Gather dependencies + blocks_to_run, to_transmit = self.custom_bfs(self) + + # 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_delay / len(self.transmitting_queue) + ) + # Start transmitting animation + self.transmitting_animation_in() + + # Run the blocks + self.run_blocks(blocks_to_run) + + # Run self + self.run_code() def run_right(self): """Run all of the output blocks and all their dependencies""" - # If the user presses right run when run_color, cancel the execution + + # To avoid crashing when spamming the button + if len(self.transmitting_queue) != 0: + return + + # 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) + # Gather outputs + blocks_to_run, to_transmit = self.custom_bfs(self, reverse=True) - # Same as run_left but instead of run_color the blocks, we'll use run_left - blocks_to_run = [] - to_visit = [self] - to_transmit = [to_visit] - - while len(to_visit) != 0: - to_visit = list(set(to_visit)) - edges_to_visit = [] - for block in to_visit: - blocks_to_run.append(block) - for output_socket in block.sockets_out: - for edge in output_socket.edges: - edges_to_visit.append(edge) - to_transmit.append(edges_to_visit) - to_visit = [] - for edge in edges_to_visit: - to_visit.append(edge.destination_socket.block) - to_transmit.append(to_visit) + # Init transmitting queue + self.transmitting_queue = to_transmit - blocks_to_run.pop(0) + # For each output found for block in blocks_to_run[::-1]: - new_blocks_to_run = [] - to_visit = [block] - to_transmit.append(to_visit) - - while len(to_visit) != 0: - # Remove duplicates in to_visit - to_visit = list(set(to_visit)) - edges_to_visit = [] - for block in to_visit: - new_blocks_to_run.append(block) - for input_socket in block.sockets_in: - for edge in input_socket.edges: - edges_to_visit.append(edge) - to_transmit.append(edges_to_visit) - to_visit = [] - for edge in edges_to_visit: - to_visit.append(edge.source_socket.block) - to_transmit.append(to_visit) - new_blocks_to_run.pop(0) + # Gather dependencies + new_blocks_to_run, new_to_transmit = self.custom_bfs(block) blocks_to_run += new_blocks_to_run + self.transmitting_queue += new_to_transmit - self.transmitting_queue = to_transmit - + # Set delay so that the transmitting animation has fixed total duration + self.transmitting_delay = int( + self.transmitting_delay / len(self.transmitting_queue) + ) + # Start transmitting animation self.transmitting_animation_in() - for block in blocks_to_run[::-1]: - if not block.has_been_run: - block.run_code() + + # Run the blocks + self.run_blocks(blocks_to_run) def reset_has_been_run(self): """Reset has_been_run, is called when the output is an error""" self.has_been_run = False - def reset_buttons(self): - """Reset the buttons""" - self.run_color = 0 - self.run_button.setText(">") - self.run_all_button.setText(">>") - def update_title(self): """Change the geometry of the title widget""" self.title_widget.setGeometry( @@ -337,6 +353,7 @@ def paint( option: QStyleOptionGraphicsItem, widget: Optional[QWidget] = None, ): + """Paint the code block""" path_content = QPainterPath() path_content.setFillRule(Qt.FillRule.WindingFill) path_content.addRoundedRect( @@ -379,6 +396,7 @@ def run_color(self) -> int: @run_color.setter def run_color(self, value: int): self._run_color = value + # Update to force repaint self.update() @property @@ -444,6 +462,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/graphics/edge.py b/opencodeblocks/graphics/edge.py index 44451b92..51b725e2 100644 --- a/opencodeblocks/graphics/edge.py +++ b/opencodeblocks/graphics/edge.py @@ -70,6 +70,7 @@ def __init__( 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) @@ -196,6 +197,7 @@ def destination_socket(self, value: OCBSocket): self.destination = value.scenePos() def serialize(self) -> OrderedDict: + """Serialize the edge.""" return OrderedDict( [ ("id", self.id), @@ -240,6 +242,7 @@ def serialize(self) -> OrderedDict: ) def deserialize(self, data: OrderedDict, hashmap: dict = None, restore_id=True): + """Deserialize the edge.""" if restore_id: self.id = data["id"] self.path_type = data["path_type"] @@ -261,4 +264,5 @@ def run_color(self) -> int: @run_color.setter def run_color(self, value: int): self._run_color = value + # Update to force repaint self.update() diff --git a/opencodeblocks/graphics/kernel.py b/opencodeblocks/graphics/kernel.py index 326651d1..13b0ada2 100644 --- a/opencodeblocks/graphics/kernel.py +++ b/opencodeblocks/graphics/kernel.py @@ -64,11 +64,12 @@ 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_block.connect(block.reset_after_run) worker.signals.error.connect(block.reset_has_been_run) block.source_editor.threadpool.start(worker) diff --git a/opencodeblocks/scene/scene.py b/opencodeblocks/scene/scene.py index a9abc436..9fadfe2b 100644 --- a/opencodeblocks/scene/scene.py +++ b/opencodeblocks/scene/scene.py @@ -170,6 +170,7 @@ def clear(self): return super().clear() def serialize(self) -> OrderedDict: + """Serialize the scene into a dict.""" blocks = [] edges = [] for item in self.items(): @@ -224,6 +225,7 @@ def create_block( def deserialize( self, data: OrderedDict, hashmap: dict = None, restore_id: bool = True ): + """Deserialize a dict into the scene.""" self.clear() hashmap = hashmap if hashmap is not None else {} if restore_id: From d5affa2a1c6469e906a9df6dc7a35a5b8194da38 Mon Sep 17 00:00:00 2001 From: AlexandreSajus Date: Sun, 12 Dec 2021 03:37:08 +0100 Subject: [PATCH 23/51] :wrench: adjusted animation duration --- opencodeblocks/blocks/codeblock.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opencodeblocks/blocks/codeblock.py b/opencodeblocks/blocks/codeblock.py index 802a5a94..ecf3c93b 100644 --- a/opencodeblocks/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -267,7 +267,7 @@ def run_left(self): self.transmitting_queue = to_transmit # Set delay so that the transmitting animation has fixed total duration self.transmitting_delay = int( - self.transmitting_delay / len(self.transmitting_queue) + self.transmitting_duration / len(self.transmitting_queue) ) # Start transmitting animation self.transmitting_animation_in() @@ -305,7 +305,7 @@ def run_right(self): # Set delay so that the transmitting animation has fixed total duration self.transmitting_delay = int( - self.transmitting_delay / len(self.transmitting_queue) + self.transmitting_duration / len(self.transmitting_queue) ) # Start transmitting animation self.transmitting_animation_in() From e21e9d6e006a0ecf60244e069658780c69963148 Mon Sep 17 00:00:00 2001 From: AlexandreSajus Date: Sun, 12 Dec 2021 15:04:04 +0100 Subject: [PATCH 24/51] :wrench: modified animation behavior --- opencodeblocks/blocks/codeblock.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/opencodeblocks/blocks/codeblock.py b/opencodeblocks/blocks/codeblock.py index ecf3c93b..8c02e773 100644 --- a/opencodeblocks/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -176,7 +176,8 @@ def transmitting_animation_in(self): Set color to transmitting and set a timer before switching to normal """ for elem in self.transmitting_queue[0]: - elem.run_color = 2 + if elem.run_color != 1: + elem.run_color = 2 QApplication.processEvents() QTimer.singleShot(self.transmitting_delay, self.transmitting_animation_out) @@ -186,7 +187,8 @@ def transmitting_animation_out(self): After the timer, set color to normal and move on with the queue """ for elem in self.transmitting_queue[0]: - elem.run_color = 0 + if elem.run_color != 1: + elem.run_color = 0 QApplication.processEvents() self.transmitting_queue.pop(0) if len(self.transmitting_queue) != 0: @@ -291,17 +293,28 @@ def run_right(self): return # Gather outputs - blocks_to_run, to_transmit = self.custom_bfs(self, reverse=True) + blocks_to_run, to_transmit_right = self.custom_bfs(self, reverse=True) # Init transmitting queue - self.transmitting_queue = to_transmit + self.transmitting_queue = to_transmit_right + + # Gather dependencies + to_transmit_left = [] # For each output found for block in blocks_to_run[::-1]: # Gather dependencies new_blocks_to_run, new_to_transmit = self.custom_bfs(block) blocks_to_run += new_blocks_to_run - self.transmitting_queue += new_to_transmit + # Add new_to_transmit to transmit_left + # so that each left_transmit starts at the same time + for i in range(len(new_to_transmit)): + if i < len(to_transmit_left): + to_transmit_left[i] += new_to_transmit[i] + else: + to_transmit_left.append(new_to_transmit[i]) + + self.transmitting_queue += to_transmit_left # Set delay so that the transmitting animation has fixed total duration self.transmitting_delay = int( From f2f7ce3ca4421f7447e515844b7fff5525c93dd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Sun, 12 Dec 2021 17:57:11 +0100 Subject: [PATCH 25/51] =?UTF-8?q?=E2=9C=A8=20Remove=20trailing=20whitespac?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- opencodeblocks/scene/scene.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/opencodeblocks/scene/scene.py b/opencodeblocks/scene/scene.py index 0e073256..a1a7bf9c 100644 --- a/opencodeblocks/scene/scene.py +++ b/opencodeblocks/scene/scene.py @@ -183,13 +183,13 @@ def load(self, filepath: str): # Add filepath to kernel path dir_path = repr(path.abspath(path.dirname(filepath))) - setup_path_code = f"__import__(\"os\").chdir({dir_path})" + 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 - + Args: filepath: Path to the file to load. """ @@ -270,8 +270,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"]: From 3f83c586c1501bf0c5cf5762b371df0bdfd46f9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Math=C3=AFs=20F=C3=A9d=C3=A9rico?= Date: Sun, 12 Dec 2021 19:10:26 +0100 Subject: [PATCH 26/51] :memo: Update linear_classifier --- examples/linear_classifier.ipyg | 200 +++++++++++--------------------- 1 file changed, 66 insertions(+), 134 deletions(-) diff --git a/examples/linear_classifier.ipyg b/examples/linear_classifier.ipyg index 3aa765eb..a02f6f4a 100644 --- a/examples/linear_classifier.ipyg +++ b/examples/linear_classifier.ipyg @@ -1,67 +1,13 @@ { "id": 2034509196736, "blocks": [ - { - "id": 2034509423808, - "title": "Imports", - "block_type": "OCBCodeBlock", - "splitter_pos": [ - 227, - 0 - ], - "position": [ - -837.0, - -95.0 - ], - "width": 501, - "height": 286, - "metadata": { - "title_metadata": { - "color": "white", - "font": "Ubuntu", - "size": 10 - } - }, - "sockets": [ - { - "id": 2034509424672, - "type": "input", - "position": [ - 0.0, - 53.0 - ], - "metadata": { - "color": "#FF55FFF0", - "linecolor": "#FF000000", - "linewidth": 1.0, - "radius": 10.0 - } - }, - { - "id": 2034509424816, - "type": "output", - "position": [ - 501.0, - 53.0 - ], - "metadata": { - "color": "#FF55FFF0", - "linecolor": "#FF000000", - "linewidth": 1.0, - "radius": 10.0 - } - } - ], - "source": "from matplotlib import pyplot as plt\r\nfrom sklearn import linear_model\r\nfrom random import seed, random", - "stdout": "" - }, { "id": 2034638878464, "title": "", "block_type": "OCBMarkdownBlock", "splitter_pos": [ 0, - 194 + 200 ], "position": [ -940.0, @@ -85,14 +31,14 @@ "block_type": "OCBCodeBlock", "splitter_pos": [ 0, - 403 + 272 ], "position": [ - 757.0, - 106.0 + 53.875000000000085, + 212.25 ], - "width": 663, - "height": 462, + "width": 439, + "height": 325, "metadata": { "title_metadata": { "color": "white", @@ -106,7 +52,7 @@ "type": "input", "position": [ 0.0, - 53.0 + 40.0 ], "metadata": { "color": "#FF55FFF0", @@ -119,8 +65,8 @@ "id": 2034686483328, "type": "output", "position": [ - 663.0, - 53.0 + 439.0, + 40.0 ], "metadata": { "color": "#FF55FFF0", @@ -130,23 +76,23 @@ } } ], - "source": "um = 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" + "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": [ - 275, - 0 + 189, + 85 ], "position": [ - -211.0, - -101.0 + -929.7499999999999, + -121.31250000000003 ], - "width": 641, - "height": 334, + "width": 706, + "height": 327, "metadata": { "title_metadata": { "color": "white", @@ -160,7 +106,7 @@ "type": "input", "position": [ 0.0, - 53.0 + 40.0 ], "metadata": { "color": "#FF55FFF0", @@ -173,8 +119,8 @@ "id": 2034723534736, "type": "output", "position": [ - 641.0, - 53.0 + 706.0, + 40.0 ], "metadata": { "color": "#FF55FFF0", @@ -184,8 +130,8 @@ } } ], - "source": "seed(132)\r\nx = [3 + random() * 10 for i in range(40)]\r\ny = [4.15 * i + random() * 2 for i in x]", - "stdout": "" + "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, @@ -193,8 +139,8 @@ "block_type": "OCBSliderBlock", "splitter_pos": [], "position": [ - -175.0, - 296.0 + -890.625, + 258.50000000000006 ], "width": 618, "height": 184, @@ -211,7 +157,7 @@ "type": "input", "position": [ 0.0, - 50.0 + 42.0 ], "metadata": { "color": "#FF55FFF0", @@ -225,7 +171,7 @@ "type": "output", "position": [ 618.0, - 50.0 + 42.0 ], "metadata": { "color": "#FF55FFF0", @@ -235,8 +181,7 @@ } } ], - "source": "b = 0.12", - "value": "0.12", + "value": "0.82", "var_name": "b" }, { @@ -245,8 +190,8 @@ "block_type": "OCBSliderBlock", "splitter_pos": [], "position": [ - -184.0, - 550.0 + -901.1874999999999, + 471.8750000000001 ], "width": 618, "height": 184, @@ -263,7 +208,7 @@ "type": "input", "position": [ 0.0, - 50.0 + 42.0 ], "metadata": { "color": "#FF55FFF0", @@ -277,7 +222,7 @@ "type": "output", "position": [ 618.0, - 50.0 + 42.0 ], "metadata": { "color": "#FF55FFF0", @@ -287,8 +232,7 @@ } } ], - "source": "m = 0.12", - "value": "0.12", + "value": "0.41", "var_name": "m" }, { @@ -297,14 +241,14 @@ "block_type": "OCBCodeBlock", "splitter_pos": [ 0, - 349 + 276 ], "position": [ - 784.0, - -676.0 + 782.4375000000001, + -676.0000000000001 ], - "width": 615, - "height": 408, + "width": 434, + "height": 329, "metadata": { "title_metadata": { "color": "white", @@ -318,7 +262,7 @@ "type": "input", "position": [ 0.0, - 53.0 + 40.0 ], "metadata": { "color": "#FF55FFF0", @@ -331,8 +275,8 @@ "id": 2034879163984, "type": "output", "position": [ - 615.0, - 53.0 + 434.0, + 40.0 ], "metadata": { "color": "#FF55FFF0", @@ -342,8 +286,8 @@ } } ], - "source": "my = 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" + "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, @@ -351,11 +295,11 @@ "block_type": "OCBCodeBlock", "splitter_pos": [ 0, - 214 + 220 ], "position": [ - 757.0, - 619.0 + 611.6875000000001, + 251.81250000000006 ], "width": 685, "height": 273, @@ -372,7 +316,7 @@ "type": "input", "position": [ 0.0, - 53.0 + 40.0 ], "metadata": { "color": "#FF55FFF0", @@ -386,7 +330,7 @@ "type": "output", "position": [ 685.0, - 53.0 + 40.0 ], "metadata": { "color": "#FF55FFF0", @@ -397,22 +341,22 @@ } ], "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 * 0.8999999999999999 + 0.24\nAccuracy:\n-6.737344734867353\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": [ - 335, - 0 + 90, + 85 ], "position": [ - 10.0, - -562.0 + -160.3125, + -374.50000000000006 ], - "width": 656, - "height": 394, + "width": 840, + "height": 228, "metadata": { "title_metadata": { "color": "white", @@ -426,7 +370,7 @@ "type": "input", "position": [ 0.0, - 53.0 + 40.0 ], "metadata": { "color": "#FF55FFF0", @@ -439,8 +383,8 @@ "id": 2034886211616, "type": "output", "position": [ - 656.0, - 53.0 + 840.0, + 40.0 ], "metadata": { "color": "#FF55FFF0", @@ -450,7 +394,7 @@ } } ], - "source": "reg = linear_model.LinearRegression()\r\nreg.fit([[i] for i in x],y)", + "source": "from sklearn import linear_model\r\nreg = linear_model.LinearRegression()\r\nreg.fit([[i] for i in x],y)", "stdout": "LinearRegression()" }, { @@ -459,14 +403,14 @@ "block_type": "OCBCodeBlock", "splitter_pos": [ 0, - 224 + 278 ], "position": [ - 800.0, - -219.0 + 767.1875000000002, + -279.9375 ], - "width": 670, - "height": 283, + "width": 816, + "height": 331, "metadata": { "title_metadata": { "color": "white", @@ -480,7 +424,7 @@ "type": "input", "position": [ 0.0, - 53.0 + 40.0 ], "metadata": { "color": "#FF55FFF0", @@ -493,8 +437,8 @@ "id": 2136886540896, "type": "output", "position": [ - 670.0, - 53.0 + 816.0, + 40.0 ], "metadata": { "color": "#FF55FFF0", @@ -509,18 +453,6 @@ } ], "edges": [ - { - "id": 2034686480592, - "path_type": "bezier", - "source": { - "block": 2034509423808, - "socket": 2034509424816 - }, - "destination": { - "block": 2034723533728, - "socket": 2034723534592 - } - }, { "id": 2034686599952, "path_type": "bezier", From 7c40d1a4e22e244e3393ec946e897961ab1a7ab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Sun, 12 Dec 2021 20:55:21 +0100 Subject: [PATCH 27/51] =?UTF-8?q?=E2=9C=A8=20Add=20@abstract=20decorator?= =?UTF-8?q?=20to=20abstract=20method.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Note that @abstractproperty should not be used here: https://docs.python.org/3.8/library/abc.html#abc.abstractproperty --- opencodeblocks/blocks/executableblock.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/opencodeblocks/blocks/executableblock.py b/opencodeblocks/blocks/executableblock.py index 63a82a58..5f228142 100644 --- a/opencodeblocks/blocks/executableblock.py +++ b/opencodeblocks/blocks/executableblock.py @@ -1,6 +1,7 @@ """ Module for the executable block class """ from typing import List, OrderedDict +from abc import abstractmethod from networkx.algorithms.traversal.breadth_first_search import bfs_edges @@ -129,11 +130,13 @@ def reset_has_been_run(self): 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") From 9907f1135e56979e90a3b6e8b329578d5125387c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Sun, 12 Dec 2021 21:54:02 +0100 Subject: [PATCH 28/51] =?UTF-8?q?=E2=98=94=20Add=20tests=20for=20the=20CWD?= =?UTF-8?q?=20bug.=20Make=20test=20quicker=20but=20waiting=20properly=20fo?= =?UTF-8?q?r=20a=20block=20to=20complete=20instead=20of=20just=20waiting?= =?UTF-8?q?=203=20seconds.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/assets/data.txt | 7 + tests/assets/example_graph1.ipyg | 391 +-------------------- tests/integration/blocks/test_block.py | 3 + tests/integration/blocks/test_codeblock.py | 40 +++ tests/integration/blocks/test_flow.py | 9 +- tests/integration/utils.py | 1 - 6 files changed, 74 insertions(+), 377 deletions(-) create mode 100644 tests/assets/data.txt diff --git a/tests/assets/data.txt b/tests/assets/data.txt new file mode 100644 index 00000000..0bce9e3a --- /dev/null +++ b/tests/assets/data.txt @@ -0,0 +1,7 @@ +1 +2 +3 +4 +5 +6 +7 \ No newline at end of file diff --git a/tests/assets/example_graph1.ipyg b/tests/assets/example_graph1.ipyg index 7158ae33..c96688ed 100644 --- a/tests/assets/example_graph1.ipyg +++ b/tests/assets/example_graph1.ipyg @@ -2,227 +2,33 @@ "id": 2205665405400, "blocks": [ { - "id": 2443477874008, - "title": "Model Train", + "id": 1523300599264, + "title": "test1", "block_type": "OCBCodeBlock", - "source": "print(\"training \")\r\nmodel.fit(x=x_train,y=y_train, epochs=10)\r\n\r\n", - "stdout": "", - "image": "", - "position": [ - 2202.0742187499986, - -346.82031249999983 - ], - "width": 1644.8125, - "height": 481.4375, - "metadata": { - "title_metadata": { - "color": "white", - "font": "Ubuntu", - "size": 10, - "padding": 4.0 - } - }, - "sockets": [ - { - "id": 2443477875016, - "type": "input", - "position": [ - 0.0, - 42.0 - ], - "metadata": { - "color": "#e02c2c", - "linecolor": "#FF000000", - "linewidth": 1.0, - "radius": 6.0 - } - }, - { - "id": 2443477875160, - "type": "output", - "position": [ - 1644.8125, - 42.0 - ], - "metadata": { - "color": "#35bc31", - "linecolor": "#FF000000", - "linewidth": 1.0, - "radius": 6.0 - } - } - ] - }, - { - "id": 2443477924600, - "title": "Keras Model Predict", - "block_type": "OCBCodeBlock", - "source": "prediction = model.predict(x_test[9].reshape(1, 28, 28, 1))", - "stdout": "", - "image": "", - "position": [ - 4207.046874999999, - -244.57812499999991 - ], - "width": 1239.6875, - "height": 305.9374999999999, - "metadata": { - "title_metadata": { - "color": "white", - "font": "Ubuntu", - "size": 10, - "padding": 4.0 - } - }, - "sockets": [ - { - "id": 2443477925608, - "type": "input", - "position": [ - 0.0, - 42.0 - ], - "metadata": { - "color": "#FF55FFF0", - "linecolor": "#FF000000", - "linewidth": 1.0, - "radius": 6.0 - } - }, - { - "id": 2443477925752, - "type": "output", - "position": [ - 1239.6875, - 42.0 - ], - "metadata": { - "color": "#FF55FFF0", - "linecolor": "#FF000000", - "linewidth": 1.0, - "radius": 6.0 - } - } - ] - }, - { - "id": 2443477997032, - "title": "Keras Model eval", - "block_type": "OCBCodeBlock", - "source": "model.evaluate(x_test, y_test)\r\n", - "stdout": "", - "image": "", - "position": [ - 4204.085937499997, - -707.0546874999997 - ], - "width": 1628.375, - "height": 209.875, - "metadata": { - "title_metadata": { - "color": "white", - "font": "Ubuntu", - "size": 10, - "padding": 4.0 - } - }, - "sockets": [ - { - "id": 2443477997896, - "type": "input", - "position": [ - 0.0, - 42.0 - ], - "metadata": { - "color": "#FF55FFF0", - "linecolor": "#FF000000", - "linewidth": 1.0, - "radius": 6.0 - } - }, - { - "id": 2443477998184, - "type": "output", - "position": [ - 1628.375, - 42.0 - ], - "metadata": { - "color": "#FF55FFF0", - "linecolor": "#FF000000", - "linewidth": 1.0, - "radius": 6.0 - } - } - ] - }, - { - "id": 2443478874872, - "title": "Load MNIST Dataset", - "block_type": "OCBCodeBlock", - "source": "print(\"Hello, world\")\r\nfrom tensorflow.keras.datasets import mnist\r\n(x_train, y_train), (x_test, y_test) = mnist.load_data()\r\n", - "stdout": "", - "image": "", - "position": [ - -535.75, - -687.0625 + "splitter_pos": [ + 292, + 0 ], - "width": 739.5, - "height": 343.5, - "metadata": { - "title_metadata": { - "color": "white", - "font": "Ubuntu", - "size": 10, - "padding": 4.0 - } - }, - "sockets": [ - { - "id": 2443478910728, - "type": "output", - "position": [ - 739.5, - 42.0 - ], - "metadata": { - "color": "#FF55FFF0", - "linecolor": "#FF000000", - "linewidth": 1.0, - "radius": 6.0 - } - } - ] - }, - { - "id": 2443478982728, - "title": "Normalize Image Dataset", - "block_type": "OCBCodeBlock", - "source": "x_train = x_train.astype('float32') / 255.0\r\nx_test = x_test.astype('float32') / 255.0\r\n\r\n\r\nx_train = x_train.reshape(x_train.shape[0], 28, 28, 1)\r\nx_test = x_test.reshape(x_test.shape[0], 28, 28, 1)\r\n\r\nprint('train:', x_train.shape, '|test:', x_test.shape)", - "stdout": "", - "image": "", "position": [ - 281.2500000000002, - -149.74999999999977 + 1192.0, + 292.79999999999995 ], - "width": 705.7499999999998, - "height": 357.25, + "width": 707, + "height": 351, "metadata": { "title_metadata": { "color": "white", "font": "Ubuntu", - "size": 10, - "padding": 4.0 + "size": 10 } }, "sockets": [ { - "id": 2443478983592, + "id": 1523350963536, "type": "input", "position": [ 0.0, - 42.0 + 45.0 ], "metadata": { "color": "#FF55FFF0", @@ -232,11 +38,11 @@ } }, { - "id": 2443478983880, + "id": 1523350963680, "type": "output", "position": [ - 705.7499999999998, - 42.0 + 707.0, + 45.0 ], "metadata": { "color": "#FF55FFF0", @@ -245,171 +51,10 @@ "radius": 6.0 } } - ] - }, - { - "id": 2443479017656, - "title": "Build Keras CNN", - "block_type": "OCBCodeBlock", - "source": "import tensorflow as tf\r\nfrom tensorflow.keras.layers import Dense, Flatten, Conv2D, MaxPooling2D, Dropout\r\nfrom tensorflow.keras.models import Sequential\r\n\r\nmodel = Sequential()\r\nmodel.add(Conv2D(28, kernel_size=(3,3), input_shape=x_train.shape[1:]))\r\nmodel.add(MaxPooling2D(pool_size=(2, 2)))\r\nmodel.add(Flatten())\r\nmodel.add(Dense(128, activation=tf.nn.relu))\r\nmodel.add(Dropout(0.2))\r\nmodel.add(Dense(10,activation=tf.nn.softmax))\r\nprint(\"..\")\r\nmodel.compile(optimizer='adam', \r\n loss='sparse_categorical_crossentropy', \r\n metrics=['accuracy'])\r\n", - "stdout": "", - "image": "", - "position": [ - 1316.25, - -517.6249999999998 ], - "width": 680.0, - "height": 468.75, - "metadata": { - "title_metadata": { - "color": "white", - "font": "Ubuntu", - "size": 10, - "padding": 4.0 - } - }, - "sockets": [ - { - "id": 2443479018520, - "type": "input", - "position": [ - 0.0, - 42.0 - ], - "metadata": { - "color": "#FF55FFF0", - "linecolor": "#FF000000", - "linewidth": 1.0, - "radius": 6.0 - } - }, - { - "id": 2443479018808, - "type": "output", - "position": [ - 680.0, - 42.0 - ], - "metadata": { - "color": "#FF55FFF0", - "linecolor": "#FF000000", - "linewidth": 1.0, - "radius": 6.0 - } - } - ] - }, - { - "id": 2828158533848, - "title": "Plot Image Dataset Example", - "block_type": "OCBCodeBlock", - "source": "import matplotlib.pyplot as plt\r\nimport numpy as np\r\n\r\n# Display an example from the dataset\r\nrd_index = np.random.randint(len(x_train))\r\nplt.imshow(x_train[rd_index], cmap='gray')\r\nplt.title('Class '+ str(y_train[0]))\r\n", - "stdout": "", - "image": "", - "position": [ - 433.375, - -1221.75 - ], - "width": 778.9375, - "height": 763.25, - "metadata": { - "title_metadata": { - "color": "white", - "font": "Ubuntu", - "size": 10, - "padding": 4.0 - } - }, - "sockets": [ - { - "id": 2828158535432, - "type": "input", - "position": [ - 0.0, - 42.0 - ], - "metadata": { - "color": "#FF55FFF0", - "linecolor": "#FF000000", - "linewidth": 1.0, - "radius": 6.0 - } - } - ] + "source": "content = open(\"data.txt\").read()\r\nprint(content)", + "stdout": "" } ], - "edges": [ - { - "id": 1643571233840, - "path_type": "bezier", - "source": { - "block": 2443479017656, - "socket": 2443479018808 - }, - "destination": { - "block": 2443477874008, - "socket": 2443477875016 - } - }, - { - "id": 2006783605056, - "path_type": "bezier", - "source": { - "block": 2443478874872, - "socket": 2443478910728 - }, - "destination": { - "block": 2828158533848, - "socket": 2828158535432 - } - }, - { - "id": 2006783606064, - "path_type": "bezier", - "source": { - "block": 2443477874008, - "socket": 2443477875160 - }, - "destination": { - "block": 2443477924600, - "socket": 2443477925608 - } - }, - { - "id": 2111730223424, - "path_type": "bezier", - "source": { - "block": 2443478982728, - "socket": 2443478983880 - }, - "destination": { - "block": 2443479017656, - "socket": 2443479018520 - } - }, - { - "id": 2111730224144, - "path_type": "bezier", - "source": { - "block": 2443477874008, - "socket": 2443477875160 - }, - "destination": { - "block": 2443477997032, - "socket": 2443477997896 - } - }, - { - "id": 2111730844864, - "path_type": "bezier", - "source": { - "block": 2443478874872, - "socket": 2443478910728 - }, - "destination": { - "block": 2443478982728, - "socket": 2443478983592 - } - } - ] + "edges": [] } \ No newline at end of file diff --git a/tests/integration/blocks/test_block.py b/tests/integration/blocks/test_block.py index b07cecc1..1b843b11 100644 --- a/tests/integration/blocks/test_block.py +++ b/tests/integration/blocks/test_block.py @@ -73,3 +73,6 @@ def testing_drag(msgQueue: CheckingQueue): msgQueue.stop() apply_function_inapp(self.window, testing_drag) + + def test_finish(self): + self.window.close() \ No newline at end of file diff --git a/tests/integration/blocks/test_codeblock.py b/tests/integration/blocks/test_codeblock.py index 47a8cd28..e126d5be 100644 --- a/tests/integration/blocks/test_codeblock.py +++ b/tests/integration/blocks/test_codeblock.py @@ -6,6 +6,7 @@ """ import time +import os import pyautogui import pytest @@ -57,3 +58,42 @@ def testing_run(msgQueue: CheckingQueue): 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 + print(item.title) + break + msgQueue.check_equal(block_of_test is not None, True, "example_graph1 contains a block titled test1") + + def run_block(): + block_of_test.run_code() + + msgQueue.run_lambda(run_block) + time.sleep(0.1) # wait for the lambda to complete. + while block_of_test.is_running: + time.sleep(0.1) # wait for the execution to finish. + + file_content = open(asset_path).read() + + print('"'+block_of_test.stdout.strip()+'"') + print('"'+file_content+'"') + + 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() \ No newline at end of file diff --git a/tests/integration/blocks/test_flow.py b/tests/integration/blocks/test_flow.py index 70858a05..7a830310 100644 --- a/tests/integration/blocks/test_flow.py +++ b/tests/integration/blocks/test_flow.py @@ -10,8 +10,6 @@ import time -from PyQt5.QtCore import QPointF - from opencodeblocks.blocks.codeblock import OCBCodeBlock from opencodeblocks.graphics.window import OCBWindow from opencodeblocks.graphics.widget import OCBWidget @@ -51,10 +49,15 @@ def run_block(): block_to_run.run_left() msgQueue.run_lambda(run_block) - time.sleep(3) + time.sleep(0.1) + while block_to_run.is_running: + time.sleep(0.1) # wait for the execution to finish. 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) + + def test_finish(self): + self.window.close() \ No newline at end of file diff --git a/tests/integration/utils.py b/tests/integration/utils.py index 2054b4d2..9c5af1ec 100644 --- a/tests/integration/utils.py +++ b/tests/integration/utils.py @@ -62,4 +62,3 @@ def apply_function_inapp(window: OCBWindow, run_func: Callable): elif msg[0] == RUN_MSG: msg[1](*msg[2], **msg[3]) t.join() - window.close() From b84932756f4b1cbd8f76b0f92a6902b23ea1e67c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Sun, 12 Dec 2021 22:05:11 +0100 Subject: [PATCH 29/51] =?UTF-8?q?=E2=98=94=20Add=20test=20to=20prevent=20#?= =?UTF-8?q?112=20regressions.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/integration/blocks/test_flow.py | 30 ++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/tests/integration/blocks/test_flow.py b/tests/integration/blocks/test_flow.py index 7a830310..dff8311e 100644 --- a/tests/integration/blocks/test_flow.py +++ b/tests/integration/blocks/test_flow.py @@ -5,9 +5,7 @@ Integration tests for the OCBCodeBlocks. """ -import pyautogui import pytest - import time from opencodeblocks.blocks.codeblock import OCBCodeBlock @@ -37,8 +35,34 @@ def setup(self): if item.title in titles: self.blocks_to_run[titles.index(item.title)] = item + 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_no_duplicates(msgQueue: CheckingQueue): + + block_to_run: OCBCodeBlock = self.blocks_to_run[0] + + def run_block(): + block_to_run.run_right() + + msgQueue.run_lambda(run_block) + time.sleep(0.1) + while block_to_run.is_running: + time.sleep(0.1) # wait for the execution to finish. + + # 6 and not 6\n6 + msgQueue.check_equal(block_to_run.stdout.strip(), "6") + msgQueue.stop() + + apply_function_inapp(self.window, testing_no_duplicates) + 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): @@ -58,6 +82,6 @@ def run_block(): msgQueue.stop() apply_function_inapp(self.window, testing_run) - + def test_finish(self): self.window.close() \ No newline at end of file From d43a46f09bf97c71be98640fd9bcbaf2dc614a8c Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Tue, 14 Dec 2021 23:09:59 +0100 Subject: [PATCH 30/51] :umbrella: Add unit tests for ipynb conversion --- tests/assets/complex.ipynb | 914 ++++++++++++++++++++++ tests/assets/empty.ipynb | 32 + tests/assets/usual.ipynb | 194 +++++ tests/unit/scene/test_ipynb_conversion.py | 118 +++ 4 files changed, 1258 insertions(+) create mode 100644 tests/assets/complex.ipynb create mode 100644 tests/assets/empty.ipynb create mode 100644 tests/assets/usual.ipynb create mode 100644 tests/unit/scene/test_ipynb_conversion.py 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/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/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/unit/scene/test_ipynb_conversion.py b/tests/unit/scene/test_ipynb_conversion.py new file mode 100644 index 00000000..75458a3c --- /dev/null +++ b/tests/unit/scene/test_ipynb_conversion.py @@ -0,0 +1,118 @@ +"""Unit tests for the conversion from and to ipynb.""" + +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: + + """Testing function_parsing functions""" + + def test_empty_data(self, mocker: MockerFixture): + """Empty data should return empty document""" + check.equal(ipynb_to_ipyg({}), {"blocks": [], "edges": []}) + + def test_empty_notebook_data(self, mocker: MockerFixture): + """The conversion for an empty notebook should be coherent""" + file_path = "./tests/assets/empty.ipynb" + real_notebook_is_coherent(file_path) + + def test_complex_notebook_data(self, mocker: MockerFixture): + """The conversion for a usual notebook should be coherent""" + file_path = "./tests/assets/usual.ipynb" + real_notebook_is_coherent(file_path) + + def test_complex_notebook_data(self, mocker: MockerFixture): + """The conversion for a complex notebook should be coherent""" + file_path = "./tests/assets/complex.ipynb" + real_notebook_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_is_coherent(file_path: str): + """ + From ipynb conversion on a real notebooks 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 + """ + ipynb_data = load_json(file_path) + ipyg_data = ipynb_to_ipyg(ipynb_data) + + # 1. + check.equal("blocks" in ipyg_data and "edges" in ipyg_data, True) + + # 2. + 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) + + # 3. + 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"]) + + # 4. + 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) + + # 5. & 6. + 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, + } From bff689162a92eca7e0d0ad7c2cfc6c56a9b8bdf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Math=C3=AFs=20F=C3=A9d=C3=A9rico?= <60117466+MathisFederico@users.noreply.github.com> Date: Fri, 17 Dec 2021 16:05:13 +0100 Subject: [PATCH 31/51] Updated project name in README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 ! From e99affef7217ef135aac09547ea32ed3f1585d0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Math=C3=AFs=20F=C3=A9d=C3=A9rico?= <60117466+MathisFederico@users.noreply.github.com> Date: Fri, 17 Dec 2021 16:07:56 +0100 Subject: [PATCH 32/51] Update project name in CONTRIBUTING.md --- CONTRIBUTING.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d1379f8c..58f7b8f9 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). +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. @@ -71,4 +71,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. From dbbb931a9be05c00fce315163ad8710c61f3df41 Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Fri, 17 Dec 2021 19:17:52 +0100 Subject: [PATCH 33/51] :sparkles: Improve and correct docstrings --- tests/unit/scene/test_ipynb_conversion.py | 52 ++++++++++++++--------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/tests/unit/scene/test_ipynb_conversion.py b/tests/unit/scene/test_ipynb_conversion.py index 75458a3c..d73f357d 100644 --- a/tests/unit/scene/test_ipynb_conversion.py +++ b/tests/unit/scene/test_ipynb_conversion.py @@ -1,5 +1,6 @@ """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 @@ -10,29 +11,29 @@ class TestIpynbConversion: - """Testing function_parsing functions""" + """Conversion from .ipynb""" def test_empty_data(self, mocker: MockerFixture): - """Empty data should return empty document""" + """should return empty ipyg graph for empty data.""" check.equal(ipynb_to_ipyg({}), {"blocks": [], "edges": []}) def test_empty_notebook_data(self, mocker: MockerFixture): - """The conversion for an empty notebook should be coherent""" + """should return expected graph for a real empty notebook data.""" file_path = "./tests/assets/empty.ipynb" - real_notebook_is_coherent(file_path) + real_notebook_conversion_is_coherent(file_path) - def test_complex_notebook_data(self, mocker: MockerFixture): - """The conversion for a usual notebook should be coherent""" + 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_is_coherent(file_path) + real_notebook_conversion_is_coherent(file_path) def test_complex_notebook_data(self, mocker: MockerFixture): - """The conversion for a complex notebook should be coherent""" + """should return expected graph for a real complex notebook data.""" file_path = "./tests/assets/complex.ipynb" - real_notebook_is_coherent(file_path) + 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""" + """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) @@ -48,9 +49,20 @@ def test_is_title(self, mocker: MockerFixture): check.equal(is_title(string_to_markdown_block("New line \n Next line")), False) -def real_notebook_is_coherent(file_path: str): +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 """ - From ipynb conversion on a real notebooks should return + ipynb_data = load_json(file_path) + ipyg_data = ipynb_to_ipyg(ipynb_data) + 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 @@ -58,13 +70,11 @@ def real_notebook_is_coherent(file_path: str): 5. code blocks that always have a source 6. markdown blocks that always have text """ - ipynb_data = load_json(file_path) - ipyg_data = ipynb_to_ipyg(ipynb_data) - # 1. + # blocks and edges are present check.equal("blocks" in ipyg_data and "edges" in ipyg_data, True) - # 2. + # 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": @@ -75,7 +85,7 @@ def real_notebook_is_coherent(file_path: str): code_blocks_in_ipyg += 1 check.equal(code_blocks_in_ipyg, code_blocks_in_ipynb) - # 3. + # blocks and sockets have unique ids block_id_set = set([]) socket_id_set = set([]) for block in ipyg_data["blocks"]: @@ -88,14 +98,14 @@ def real_notebook_is_coherent(file_path: str): check.equal(socket["id"] in socket_id_set, False) socket_id_set.add(socket["id"]) - # 4. + # 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) - # 5. & 6. + # 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) @@ -104,14 +114,14 @@ def real_notebook_is_coherent(file_path: str): def load_json(file_path: str): - """Helper function that returns the ipynb data in a given file""" + """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""" + """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, From f6a4d973d8b49e223de92fcee3f882f68584eb69 Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Fri, 17 Dec 2021 19:24:43 +0100 Subject: [PATCH 34/51] :beetle: Fix id double attribution bug --- opencodeblocks/scene/from_ipynb_conversion.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/opencodeblocks/scene/from_ipynb_conversion.py b/opencodeblocks/scene/from_ipynb_conversion.py index ab9b41c7..7c2ba094 100644 --- a/opencodeblocks/scene/from_ipynb_conversion.py +++ b/opencodeblocks/scene/from_ipynb_conversion.py @@ -41,6 +41,7 @@ def get_blocks_data(data: OrderedDict) -> 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"]: @@ -62,7 +63,7 @@ def get_blocks_data(data: OrderedDict) -> List[OrderedDict]: 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 +94,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 +146,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"]) ) From 78395da04b86aa65d2d6cae9fabbbd29b9e8f388 Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Fri, 17 Dec 2021 19:27:50 +0100 Subject: [PATCH 35/51] :beetle: Remove wrong savepath change --- opencodeblocks/graphics/window.py | 1 - 1 file changed, 1 deletion(-) diff --git a/opencodeblocks/graphics/window.py b/opencodeblocks/graphics/window.py index 3e2a6af4..5b977894 100644 --- a/opencodeblocks/graphics/window.py +++ b/opencodeblocks/graphics/window.py @@ -374,7 +374,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}", From d320640edea024730d35e2ba077c26dbbfac5ff6 Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Sat, 18 Dec 2021 19:16:03 +0100 Subject: [PATCH 36/51] :memo: Add guidelines + Change formater to black --- CONTRIBUTING.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 58f7b8f9..3792dd13 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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,6 +36,8 @@ 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``). From b4892ada71116654dcf808f40d120f2d7472e321 Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Sat, 18 Dec 2021 19:17:21 +0100 Subject: [PATCH 37/51] :sparkles: Correct markdown lint --- CONTRIBUTING.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3792dd13..d8e8f557 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ [![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**, +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` @@ -43,7 +43,6 @@ The comments and docstrings should preferably follow [these](https://google.gith 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 From b12bc07603e77d880f56399f643db23ff05435ef Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Sat, 18 Dec 2021 19:21:26 +0100 Subject: [PATCH 38/51] :sparkles: Apply black formater to everything --- opencodeblocks/__init__.py | 6 +- opencodeblocks/blocks/block.py | 3 +- opencodeblocks/blocks/codeblock.py | 13 +- opencodeblocks/blocks/drawingblock.py | 44 +++---- opencodeblocks/blocks/markdownblock.py | 20 +-- opencodeblocks/core/serializable.py | 1 - opencodeblocks/graphics/edge.py | 2 +- opencodeblocks/graphics/function_parsing.py | 9 +- opencodeblocks/graphics/kernel.py | 45 ++++--- opencodeblocks/graphics/theme.py | 16 +-- opencodeblocks/qss/__init__.py | 4 +- opencodeblocks/scene/clipboard.py | 69 ++++++----- opencodeblocks/scene/history.py | 31 +++-- opencodeblocks/scene/scene.py | 4 +- tests/integration/blocks/test_codeblock.py | 7 +- tests/integration/test_window.py | 7 +- tests/unit/scene/test_clipboard.py | 24 ++-- tests/unit/scene/test_function_parsing.py | 128 ++++++++++++-------- tests/unit/scene/test_history.py | 110 +++++++++-------- 19 files changed, 285 insertions(+), 258 deletions(-) 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/block.py b/opencodeblocks/blocks/block.py index 29763b57..d5a3563b 100644 --- a/opencodeblocks/blocks/block.py +++ b/opencodeblocks/blocks/block.py @@ -162,8 +162,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..e6681a22 100644 --- a/opencodeblocks/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -88,16 +88,14 @@ 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.setFixedSize(int(3 * self.edge_size), int(3 * self.edge_size)) run_button.clicked.connect(self.run_left) return run_button def init_run_all_button(self): """Initialize the run all button""" run_all_button = QPushButton(">>", self.root) - run_all_button.setFixedSize( - int(3 * self.edge_size), int(3 * self.edge_size)) + run_all_button.setFixedSize(int(3 * self.edge_size), int(3 * self.edge_size)) run_all_button.clicked.connect(self.run_right) run_all_button.raise_() @@ -141,7 +139,7 @@ def has_output(self) -> bool: return False def _interrupt_execution(self): - """ Interrupt an execution, reset the blocks in the queue """ + """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() @@ -200,13 +198,12 @@ def run_right(self): # 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] + 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 """ + """Reset has_been_run, is called when the output is an error""" self.has_been_run = False def update_title(self): diff --git a/opencodeblocks/blocks/drawingblock.py b/opencodeblocks/blocks/drawingblock.py index f45cfc62..98794f5f 100644 --- a/opencodeblocks/blocks/drawingblock.py +++ b/opencodeblocks/blocks/drawingblock.py @@ -14,10 +14,10 @@ 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""" 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 @@ -31,13 +31,13 @@ def __init__(self, parent: QWidget): self.color_buffer[-1].append(0xFFFFFFFF) 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 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,11 +50,11 @@ def paintEvent(self, evt: QPaintEvent): h * j, w + eps, h + eps, - QColor.fromRgb( - self.color_buffer[i][j])) + QColor.fromRgb(self.color_buffer[i][j]), + ) 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) @@ -63,35 +63,36 @@ def mouseMoveEvent(self, evt: QMouseEvent): self.repaint() 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""" + """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.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 +100,17 @@ 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 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/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/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..7c636917 100644 --- a/opencodeblocks/graphics/edge.py +++ b/opencodeblocks/graphics/edge.py @@ -25,7 +25,7 @@ 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", source: QPointF = QPointF(0, 0), 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..3114e0a8 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): @@ -73,7 +72,7 @@ def run_block(self, block, code: str): block.source_editor.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 +113,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/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/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 eb2f6b65..3d7b0320 100644 --- a/opencodeblocks/scene/clipboard.py +++ b/opencodeblocks/scene/clipboard.py @@ -16,12 +16,12 @@ from opencodeblocks.graphics.view import OCBView -class SceneClipboard(): +class SceneClipboard: - """ Helper object to handle clipboard operations on an OCBScene. """ + """Helper object to handle clipboard operations on an OCBScene.""" - def __init__(self, scene:'OCBScene'): - """ Helper object to handle clipboard operations on an OCBScene. + def __init__(self, scene: "OCBScene"): + """Helper object to handle clipboard operations on an OCBScene. Args: scene: Scene reference. @@ -30,36 +30,44 @@ def __init__(self, scene:'OCBScene'): self.scene = scene def cut(self): - """ Cut the selected items and put them into clipboard. """ + """Cut the selected items and put them into clipboard.""" self._store(self._serializeSelected(delete=True)) def copy(self): - """ Copy the selected items into clipboard. """ + """Copy the selected items into clipboard.""" self._store(self._serializeSelected(delete=False)) def paste(self): - """ Paste the items in clipboard into the current scene. """ + """Paste the items in clipboard into the current scene.""" self._deserializeData(self._gatherData()) def _serializeSelected(self, delete=False) -> OrderedDict: selected_blocks, selected_edges = self.scene.sortedSelectedItems() selected_sockets = {} - for block in selected_blocks: # Gather selected sockets + for block in selected_blocks: # Gather selected sockets 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 - if edge.source_socket.id not in selected_sockets or \ - edge.destination_socket.id not in selected_sockets: + for ( + edge + ) in ( + selected_edges + ): # Filter edges that are not fully connected to selected sockets + if ( + edge.source_socket.id not in selected_sockets + or edge.destination_socket.id not in selected_sockets + ): selected_edges.remove(edge) - data = OrderedDict([ - ('blocks', [block.serialize() for block in selected_blocks]), - ('edges', [edge.serialize() for edge in selected_edges]) - ]) + data = OrderedDict( + [ + ("blocks", [block.serialize() for block in selected_blocks]), + ("edges", [edge.serialize() for edge in selected_edges]), + ] + ) - if delete: # Remove selected items + if delete: # Remove selected items self.scene.views()[0].deleteSelected() return data @@ -67,7 +75,7 @@ def _serializeSelected(self, delete=False) -> OrderedDict: def _find_bbox_center(self, blocks_data): xmin, xmax, ymin, ymax = 0, 0, 0, 0 for block_data in blocks_data: - x, y = block_data['position'] + x, y = block_data["position"] if x < xmin: xmin = x if x > xmax: @@ -78,8 +86,9 @@ def _find_bbox_center(self, blocks_data): ymax = y return (xmin + xmax) / 2, (ymin + ymax) / 2 - def _deserializeData(self, data:OrderedDict, set_selected=True): - if data is None: return + def _deserializeData(self, data: OrderedDict, set_selected=True): + if data is None: + return hashmap = {} @@ -89,30 +98,34 @@ def _deserializeData(self, data:OrderedDict, set_selected=True): self.scene.clearSelection() # Finding pasting bbox center - bbox_center_x, bbox_center_y = self._find_bbox_center(data['blocks']) - offset_x, offset_y = mouse_pos.x() - bbox_center_x, mouse_pos.y() - bbox_center_y + bbox_center_x, bbox_center_y = self._find_bbox_center(data["blocks"]) + offset_x, offset_y = ( + mouse_pos.x() - bbox_center_x, + mouse_pos.y() - bbox_center_y, + ) # Create blocks - for block_data in data['blocks']: - block = self.scene.create_block(block_data, hashmap, restore_id = False) + for block_data in data["blocks"]: + block = self.scene.create_block(block_data, hashmap, restore_id=False) if set_selected: block.setSelected(True) block.setPos(block.x() + offset_x, block.y() + offset_y) # Create edges - for edge_data in data['edges']: + for edge_data in data["edges"]: edge = OCBEdge() edge.deserialize(edge_data, hashmap, restore_id=False) if set_selected: edge.setSelected(True) self.scene.addItem(edge) - hashmap.update({edge_data['id']: edge}) - - self.scene.history.checkpoint('Desiralized elements into scene', set_modified=True) + hashmap.update({edge_data["id"]: edge}) + self.scene.history.checkpoint( + "Desiralized elements into scene", set_modified=True + ) - def _store(self, data:OrderedDict): + def _store(self, data: OrderedDict): str_data = json.dumps(data, indent=4) QApplication.instance().clipboard().setText(str_data) diff --git a/opencodeblocks/scene/history.py b/opencodeblocks/scene/history.py index 9d82a8a1..945079fe 100644 --- a/opencodeblocks/scene/history.py +++ b/opencodeblocks/scene/history.py @@ -9,8 +9,8 @@ from opencodeblocks.scene import OCBScene -class SceneHistory(): - """ Helper object to handle undo/redo operations on an OCBScene. +class SceneHistory: + """Helper object to handle undo/redo operations on an OCBScene. Args: scene: Scene reference. @@ -18,42 +18,39 @@ class SceneHistory(): """ - def __init__(self, scene:'OCBScene', max_stack:int = 50): + def __init__(self, scene: "OCBScene", max_stack: int = 50): self.scene = scene self.history_stack = [] self.current = -1 self.max_stack = max_stack def undo(self): - """ Undo the last action by moving the current stamp backward and restoring. """ + """Undo the last action by moving the current stamp backward and restoring.""" if len(self.history_stack) > 0 and self.current > 0: self.current -= 1 self.restore() def redo(self): - """ Redo the last undone action by moving the current stamp forward and restoring. """ + """Redo the last undone action by moving the current stamp forward and restoring.""" if len(self.history_stack) > 0 and self.current + 1 < len(self.history_stack): self.current += 1 self.restore() - def checkpoint(self, description:str, set_modified=True): - """ Store a snapshot of the scene in the history stack. + def checkpoint(self, description: str, set_modified=True): + """Store a snapshot of the scene in the history stack. Args: description: Description given to this checkpoint. set_modified: Whether the scene should be considered modified. """ - history_stamp = { - 'description': description, - 'snapshot': self.scene.serialize() - } + history_stamp = {"description": description, "snapshot": self.scene.serialize()} self.store(history_stamp) if set_modified: self.scene.has_been_modified = True - def store(self, data:Any): - """ Store new data in the history stack, updating current checkpoint. + def store(self, data: Any): + """Store new data in the history stack, updating current checkpoint. Remove data that would be forward in the history stack. Args: @@ -61,18 +58,18 @@ def store(self, data:Any): """ if self.current + 1 < len(self.history_stack): - self.history_stack = self.history_stack[0:self.current+1] + self.history_stack = self.history_stack[0 : self.current + 1] self.history_stack.append(data) if len(self.history_stack) > self.max_stack: self.history_stack.pop(0) - self.current = min(self.current + 1, len(self.history_stack)-1) + self.current = min(self.current + 1, len(self.history_stack) - 1) def restore(self): - """ Restore the scene using the snapshot pointed by current in the history stack. """ + """Restore the scene using the snapshot pointed by current in the history stack.""" if len(self.history_stack) >= 0 and self.current >= 0: stamp = self.history_stack[self.current] - snapshot = stamp['snapshot'] + snapshot = stamp["snapshot"] self.scene.deserialize(snapshot) diff --git a/opencodeblocks/scene/scene.py b/opencodeblocks/scene/scene.py index d00d641c..ba1693b6 100644 --- a/opencodeblocks/scene/scene.py +++ b/opencodeblocks/scene/scene.py @@ -258,8 +258,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/integration/blocks/test_codeblock.py b/tests/integration/blocks/test_codeblock.py index 7ab6b67b..db64f844 100644 --- a/tests/integration/blocks/test_codeblock.py +++ b/tests/integration/blocks/test_codeblock.py @@ -19,21 +19,20 @@ class TestCodeBlocks: - @pytest.fixture(autouse=True) def setup(self): - """ Setup reused variables. """ + """Setup reused variables.""" self.window = OCBWindow() self.ocb_widget = OCBWidget() self.subwindow = self.window.mdiArea.addSubWindow(self.ocb_widget) self.subwindow.show() def test_run_python(self, qtbot: QtBot): - """ run source code when run button is pressed. """ + """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) diff --git a/tests/integration/test_window.py b/tests/integration/test_window.py index 1ab0ecee..2544b81f 100644 --- a/tests/integration/test_window.py +++ b/tests/integration/test_window.py @@ -13,18 +13,17 @@ 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 """ + """closes""" self.window.close() def test_open_file(self): - """ loads files """ + """loads files""" wnd = OCBWindow() file_example_path = "./tests/assets/example_graph1.ipyg" subwnd = wnd.createNewMdiChild(os.path.abspath(file_example_path)) 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_history.py b/tests/unit/scene/test_history.py index 756e39be..30fe0964 100644 --- a/tests/unit/scene/test_history.py +++ b/tests/unit/scene/test_history.py @@ -15,28 +15,28 @@ class TestUndo: """Undo""" @pytest.fixture(autouse=True) - def setup(self, mocker:MockerFixture): + def setup(self, mocker: MockerFixture): self.scene = mocker.MagicMock() self.history = SceneHistory(self.scene, max_stack=6) - self.history.history_stack = ['A', 'B', 'C', 'D'] + self.history.history_stack = ["A", "B", "C", "D"] self.history.current = 3 - def test_undo(self, mocker:MockerFixture): - """ should allow for undo without breaking the history stack.""" - mocker.patch('opencodeblocks.scene.history.SceneHistory.restore') + def test_undo(self, mocker: MockerFixture): + """should allow for undo without breaking the history stack.""" + mocker.patch("opencodeblocks.scene.history.SceneHistory.restore") - check.equal(self.history.history_stack, ['A', 'B', 'C', 'D']) - check.equal(self.history.history_stack[self.history.current], 'D') + check.equal(self.history.history_stack, ["A", "B", "C", "D"]) + check.equal(self.history.history_stack[self.history.current], "D") self.history.undo() - check.equal(self.history.history_stack, ['A', 'B', 'C', 'D']) - check.equal(self.history.history_stack[self.history.current], 'C') + check.equal(self.history.history_stack, ["A", "B", "C", "D"]) + check.equal(self.history.history_stack[self.history.current], "C") check.is_true(self.history.restore.called) - def test_undo_nostack(self, mocker:MockerFixture): - """ should allow to undo without any change if the history stack is empty.""" - mocker.patch('opencodeblocks.scene.history.SceneHistory.restore') + def test_undo_nostack(self, mocker: MockerFixture): + """should allow to undo without any change if the history stack is empty.""" + mocker.patch("opencodeblocks.scene.history.SceneHistory.restore") self.history.history_stack = [] self.history.current = -1 @@ -47,18 +47,18 @@ def test_undo_nostack(self, mocker:MockerFixture): check.equal(self.history.current, -1) check.is_false(self.history.restore.called) - def test_undo_end_of_stack(self, mocker:MockerFixture): - """ should allow to undo without any change if at the end of the history stack.""" - mocker.patch('opencodeblocks.scene.history.SceneHistory.restore') + def test_undo_end_of_stack(self, mocker: MockerFixture): + """should allow to undo without any change if at the end of the history stack.""" + mocker.patch("opencodeblocks.scene.history.SceneHistory.restore") self.history.current = 0 - check.equal(self.history.history_stack, ['A', 'B', 'C', 'D']) - check.equal(self.history.history_stack[self.history.current], 'A') + check.equal(self.history.history_stack, ["A", "B", "C", "D"]) + check.equal(self.history.history_stack[self.history.current], "A") self.history.undo() - check.equal(self.history.history_stack, ['A', 'B', 'C', 'D']) - check.equal(self.history.history_stack[self.history.current], 'A') + check.equal(self.history.history_stack, ["A", "B", "C", "D"]) + check.equal(self.history.history_stack[self.history.current], "A") check.is_false(self.history.restore.called) @@ -67,28 +67,28 @@ class TestRedo: """Redo""" @pytest.fixture(autouse=True) - def setup(self, mocker:MockerFixture): + def setup(self, mocker: MockerFixture): self.scene = mocker.MagicMock() self.history = SceneHistory(self.scene, max_stack=6) - self.history.history_stack = ['A', 'B', 'C', 'D'] + self.history.history_stack = ["A", "B", "C", "D"] self.history.current = 1 - def test_redo(self, mocker:MockerFixture): - """ should allow for redo without changing the history stack.""" - mocker.patch('opencodeblocks.scene.history.SceneHistory.restore') + def test_redo(self, mocker: MockerFixture): + """should allow for redo without changing the history stack.""" + mocker.patch("opencodeblocks.scene.history.SceneHistory.restore") - check.equal(self.history.history_stack, ['A', 'B', 'C', 'D']) - check.equal(self.history.history_stack[self.history.current], 'B') + check.equal(self.history.history_stack, ["A", "B", "C", "D"]) + check.equal(self.history.history_stack[self.history.current], "B") self.history.redo() - check.equal(self.history.history_stack, ['A', 'B', 'C', 'D']) - check.equal(self.history.history_stack[self.history.current], 'C') + check.equal(self.history.history_stack, ["A", "B", "C", "D"]) + check.equal(self.history.history_stack[self.history.current], "C") check.is_true(self.history.restore.called) - def test_redo_nostack(self, mocker:MockerFixture): - """ should allow to redo without any change if the history stack is empty.""" - mocker.patch('opencodeblocks.scene.history.SceneHistory.restore') + def test_redo_nostack(self, mocker: MockerFixture): + """should allow to redo without any change if the history stack is empty.""" + mocker.patch("opencodeblocks.scene.history.SceneHistory.restore") self.history.history_stack = [] self.history.current = -1 @@ -99,18 +99,18 @@ def test_redo_nostack(self, mocker:MockerFixture): check.equal(self.history.current, -1) check.is_false(self.history.restore.called) - def test_redo_end_of_stack(self, mocker:MockerFixture): - """ should allow to redo without any change if at the beggining of the history stack.""" - mocker.patch('opencodeblocks.scene.history.SceneHistory.restore') + def test_redo_end_of_stack(self, mocker: MockerFixture): + """should allow to redo without any change if at the beggining of the history stack.""" + mocker.patch("opencodeblocks.scene.history.SceneHistory.restore") self.history.current = 3 - check.equal(self.history.history_stack, ['A', 'B', 'C', 'D']) - check.equal(self.history.history_stack[self.history.current], 'D') + check.equal(self.history.history_stack, ["A", "B", "C", "D"]) + check.equal(self.history.history_stack[self.history.current], "D") self.history.redo() - check.equal(self.history.history_stack, ['A', 'B', 'C', 'D']) - check.equal(self.history.history_stack[self.history.current], 'D') + check.equal(self.history.history_stack, ["A", "B", "C", "D"]) + check.equal(self.history.history_stack[self.history.current], "D") check.is_false(self.history.restore.called) @@ -119,39 +119,37 @@ class TestStore: """Store""" @pytest.fixture(autouse=True) - def setup(self, mocker:MockerFixture): + def setup(self, mocker: MockerFixture): self.scene = mocker.MagicMock() self.history = SceneHistory(self.scene, max_stack=6) - def test_store(self): - """ should update current while storing new data.""" - self.history.history_stack = ['A', 'B', 'C', 'D'] + """should update current while storing new data.""" + self.history.history_stack = ["A", "B", "C", "D"] self.history.current = 3 - self.history.store('E') + self.history.store("E") - check.equal(self.history.history_stack, ['A', 'B', 'C', 'D', 'E']) - check.equal(self.history.history_stack[self.history.current], 'E') + check.equal(self.history.history_stack, ["A", "B", "C", "D", "E"]) + check.equal(self.history.history_stack[self.history.current], "E") def test_store_cut(self): - """ should cut upper stack when storing new data.""" - self.history.history_stack = ['A', 'B', 'C', 'D'] + """should cut upper stack when storing new data.""" + self.history.history_stack = ["A", "B", "C", "D"] self.history.current = 2 - self.history.store('E') - - check.equal(self.history.history_stack, ['A', 'B', 'C', 'E']) - check.equal(self.history.history_stack[self.history.current], 'E') + self.history.store("E") + check.equal(self.history.history_stack, ["A", "B", "C", "E"]) + check.equal(self.history.history_stack[self.history.current], "E") def test_store_max_stack(self): - """ should forget oldests checkpoint when storing new data at maximum stack size.""" - self.history.history_stack = ['A', 'B', 'C', 'D'] + """should forget oldests checkpoint when storing new data at maximum stack size.""" + self.history.history_stack = ["A", "B", "C", "D"] self.history.current = 3 self.history.max_stack = 4 - self.history.store('E') + self.history.store("E") - check.equal(self.history.history_stack, ['B', 'C', 'D', 'E']) - check.equal(self.history.history_stack[self.history.current], 'E') + check.equal(self.history.history_stack, ["B", "C", "D", "E"]) + check.equal(self.history.history_stack[self.history.current], "E") From 58db48fe6e3a7987928f4ea428ee11bc86427788 Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Sat, 18 Dec 2021 19:30:10 +0100 Subject: [PATCH 39/51] :beetle: Remove wrong savepath change on load --- opencodeblocks/graphics/window.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/opencodeblocks/graphics/window.py b/opencodeblocks/graphics/window.py index 5b977894..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): From 80874fe904b965b523c06619329d576ba7e4d666 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Math=C3=AFs=20F=C3=A9d=C3=A9rico?= Date: Sun, 19 Dec 2021 13:19:32 +0100 Subject: [PATCH 40/51] :sparkles: Fix bad comment placing in clipboard.py --- opencodeblocks/scene/clipboard.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) 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 From 76c5786585b6a88b1bcae83ab31485542461cc5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Mon, 20 Dec 2021 16:25:48 +0100 Subject: [PATCH 41/51] =?UTF-8?q?=E2=98=94=F0=9F=AA=B2=20Change=20properly?= =?UTF-8?q?=20catch=20error=20in=20tests.=20Remove=20code=20that=20was=20w?= =?UTF-8?q?rongfully=20added=20by=20git=20during=20a=20merge.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- opencodeblocks/blocks/codeblock.py | 82 --------------------------- tests/integration/blocks/test_flow.py | 26 +++++---- tests/integration/test_window.py | 10 ++-- tests/integration/utils.py | 40 ++++++++++++- 4 files changed, 56 insertions(+), 102 deletions(-) diff --git a/opencodeblocks/blocks/codeblock.py b/opencodeblocks/blocks/codeblock.py index 07ae52ca..db6ce55a 100644 --- a/opencodeblocks/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -126,88 +126,6 @@ def execution_finished(self): self.run_button.setText(">") self.run_all_button.setText(">>") - def has_input(self) -> bool: - """Checks whether a block has connected input blocks""" - for input_socket in self.sockets_in: - if len(input_socket.edges) != 0: - return True - return False - - def has_output(self) -> bool: - """Checks whether a block has connected output blocks""" - for output_socket in self.sockets_out: - if len(output_socket.edges) != 0: - return True - return False - - def _interrupt_execution(self): - """Interrupt an execution, reset the blocks in the queue""" - for block, _ in self.source_editor.kernel.execution_queue: - # Reset the blocks that have not been run - block.reset_buttons() - block.has_been_run = False - # Clear the queue - self.source_editor.kernel.execution_queue = [] - # Interrupt the kernel - self.source_editor.kernel.kernel_manager.interrupt_kernel() - - def run_left(self, in_right_button=False): - """ - Run all of the block's dependencies and then run the block - """ - # If the user presses left run when running, cancel the execution - if self.run_button.text() == "..." and not in_right_button: - self._interrupt_execution() - return - - # If no dependencies - if not self.has_input(): - return self.run_code() - - # Create the graph from the scene - graph = self.scene().create_graph() - # BFS through the input graph - edges = bfs_edges(graph, self, reverse=True) - # Run the blocks found except self - blocks_to_run: List["OCBCodeBlock"] = [v for _, v in edges] - for block in blocks_to_run[::-1]: - if not block.has_been_run: - block.run_code() - - if in_right_button: - # If run_left was called inside of run_right - # self is not necessarily the block that was clicked - # which means that self does not need to be run - if not self.has_been_run: - self.run_code() - else: - # On the contrary if run_left was called outside of run_right - # self is the block that was clicked - # so self needs to be run - self.run_code() - - def run_right(self): - """Run all of the output blocks and all their dependencies""" - # If the user presses right run when running, cancel the execution - if self.run_all_button.text() == "...": - self._interrupt_execution() - return - - # If no output, run left - if not self.has_output(): - return self.run_left(in_right_button=True) - - # Same as run_left but instead of running the blocks, we'll use run_left - graph = self.scene().create_graph() - edges = bfs_edges(graph, self) - blocks_to_run: List["OCBCodeBlock"] = [self] + [v for _, v in edges] - for block in blocks_to_run[::-1]: - block.run_left(in_right_button=True) - - def reset_has_been_run(self): - """Reset has_been_run, is called when the output is an error""" - self.has_been_run = False - def update_title(self): """Change the geometry of the title widget""" self.title_widget.setGeometry( diff --git a/tests/integration/blocks/test_flow.py b/tests/integration/blocks/test_flow.py index 8d408d53..3121567c 100644 --- a/tests/integration/blocks/test_flow.py +++ b/tests/integration/blocks/test_flow.py @@ -9,10 +9,8 @@ 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 TestCodeBlocks: @@ -20,20 +18,17 @@ 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") - titles = ["Test flow 5", "Test flow 4", "Test no connection 1", + self.titles = ["Test flow 5", "Test flow 4", "Test no connection 1", "Test input only 2", "Test output only 1"] self.blocks_to_run = [None]*5 for item in self.ocb_widget.scene.items(): if isinstance(item, OCBCodeBlock): - if item.title in titles: - self.blocks_to_run[titles.index(item.title)] = item + if item.title in self.titles: + self.blocks_to_run[self.titles.index(item.title)] = item def test_duplicated_run(self): """ Don't run a block twice when the execution flows """ @@ -95,8 +90,13 @@ def testing_run(msgQueue: CheckingQueue): def run_block(): block_to_run.run_left() + print("About to run !") + msgQueue.run_lambda(run_block) - time.sleep(0.5) + time.sleep(0.1) + while block_to_run.is_running: + print("wait ...") + time.sleep(0.1) msgQueue.check_equal(block_to_run.stdout.strip(), "1") msgQueue.stop() @@ -116,7 +116,9 @@ def run_block(): block_to_run.run_right() msgQueue.run_lambda(run_block) - time.sleep(0.5) + time.sleep(0.1) + while block_to_run.is_running: + time.sleep(0.1) # Just check that it doesn't crash msgQueue.stop() diff --git a/tests/integration/test_window.py b/tests/integration/test_window.py index 2544b81f..780f041c 100644 --- a/tests/integration/test_window.py +++ b/tests/integration/test_window.py @@ -18,14 +18,14 @@ def setup(self, mocker: MockerFixture): """Setup reused variables.""" self.window = OCBWindow() - def test_window_close(self, qtbot): - """closes""" - self.window.close() - - def test_open_file(self): + def test_open_file(self, qtbot): """loads files""" wnd = OCBWindow() file_example_path = "./tests/assets/example_graph1.ipyg" subwnd = wnd.createNewMdiChild(os.path.abspath(file_example_path)) subwnd.show() wnd.close() + + def test_window_close(self, qtbot): + """closes""" + self.window.close() \ No newline at end of file diff --git a/tests/integration/utils.py b/tests/integration/utils.py index 9c5af1ec..89f41067 100644 --- a/tests/integration/utils.py +++ b/tests/integration/utils.py @@ -5,14 +5,17 @@ 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 +35,27 @@ def run_lambda(self, func: Callable, *args, **kwargs): 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 """ @@ -47,11 +71,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() @@ -61,4 +88,11 @@ 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() From df5198564f1b169df360c6c19ee657c40f37c379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Mon, 20 Dec 2021 16:43:05 +0100 Subject: [PATCH 42/51] =?UTF-8?q?=E2=9C=A8=20Black=20formatting=20?= =?UTF-8?q?=E2=98=94=20Test=20reliability=20improvements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- opencodeblocks/blocks/containerblock.py | 10 +++--- opencodeblocks/blocks/sliderblock.py | 6 ++-- opencodeblocks/graphics/widget.py | 2 +- opencodeblocks/graphics/worker.py | 2 +- tests/integration/blocks/test_block.py | 4 +-- tests/integration/blocks/test_codeblock.py | 39 ++++++++++++---------- tests/integration/blocks/test_flow.py | 26 +++++++++------ tests/integration/test_window.py | 2 +- tests/integration/utils.py | 21 +++++++----- tests/unit/scene/test_ipynb_conversion.py | 5 +-- 10 files changed, 67 insertions(+), 50 deletions(-) diff --git a/opencodeblocks/blocks/containerblock.py b/opencodeblocks/blocks/containerblock.py index 86ea2a13..df00a483 100644 --- a/opencodeblocks/blocks/containerblock.py +++ b/opencodeblocks/blocks/containerblock.py @@ -8,7 +8,7 @@ class OCBContainerBlock(OCBBlock): """ - A block that can contain other blocks. + A block that can contain other blocks. """ def __init__(self, **kwargs): @@ -18,15 +18,17 @@ def __init__(self, **kwargs): # 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 + 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.edge_size * 2, ) self.child_scene = OCBScene() diff --git a/opencodeblocks/blocks/sliderblock.py b/opencodeblocks/blocks/sliderblock.py index b04bbae4..0fa580b2 100644 --- a/opencodeblocks/blocks/sliderblock.py +++ b/opencodeblocks/blocks/sliderblock.py @@ -9,6 +9,7 @@ from PyQt5.QtWidgets import QHBoxLayout, QLabel, QLineEdit, QSlider, QVBoxLayout from opencodeblocks.blocks.executableblock import OCBExecutableBlock + class OCBSliderBlock(OCBExecutableBlock): """ Features a slider ranging from 0 to 1 and an area to choose what value to assign the slider to. @@ -44,7 +45,7 @@ def __init__(self, **kwargs): self.holder.setWidget(self.root) def valueChanged(self): - """ This is called when the value of the slider changes """ + """This is called when the value of the slider changes""" 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: @@ -52,9 +53,10 @@ def valueChanged(self): @property def source(self): - """ The "source code" of the slider i.e an assignement to the value of the slider """ + """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 + @source.setter def source(self, value: str): raise RuntimeError("The source of a sliderblock is read-only.") diff --git a/opencodeblocks/graphics/widget.py b/opencodeblocks/graphics/widget.py index 0836b437..d5025355 100644 --- a/opencodeblocks/graphics/widget.py +++ b/opencodeblocks/graphics/widget.py @@ -62,7 +62,7 @@ def save(self): self.scene.save(self.savepath) def saveAsJupyter(self): - """ Save the current graph notebook as a regular python notebook """ + """Save the current graph notebook as a regular python notebook""" self.scene.save_to_ipynb(self.savepath) def load(self, filepath: str): diff --git a/opencodeblocks/graphics/worker.py b/opencodeblocks/graphics/worker.py index 5689385c..ea339952 100644 --- a/opencodeblocks/graphics/worker.py +++ b/opencodeblocks/graphics/worker.py @@ -42,7 +42,7 @@ async def run_code(self): self.signals.stdout.emit(output) elif output_type == "image": self.signals.image.emit(output) - elif output_type == 'error': + elif output_type == "error": self.signals.error.emit() self.signals.stdout.emit(output) self.signals.finished.emit() diff --git a/tests/integration/blocks/test_block.py b/tests/integration/blocks/test_block.py index 1b843b11..93d42bce 100644 --- a/tests/integration/blocks/test_block.py +++ b/tests/integration/blocks/test_block.py @@ -73,6 +73,6 @@ def testing_drag(msgQueue: CheckingQueue): msgQueue.stop() apply_function_inapp(self.window, testing_drag) - + def test_finish(self): - self.window.close() \ No newline at end of file + self.window.close() diff --git a/tests/integration/blocks/test_codeblock.py b/tests/integration/blocks/test_codeblock.py index b902cd6d..a6f79a7d 100644 --- a/tests/integration/blocks/test_codeblock.py +++ b/tests/integration/blocks/test_codeblock.py @@ -17,15 +17,14 @@ from tests.integration.utils import apply_function_inapp, CheckingQueue, start_app -class TestCodeBlocks(): - +class TestCodeBlocks: @pytest.fixture(autouse=True) def setup(self): - """ Setup reused variables. """ + """Setup reused variables.""" start_app(self) def test_run_python(self): - """ run source code when run button is pressed. """ + """run source code when run button is pressed.""" # Add a block with the source to the window EXPRESSION = "3 + 5 * 2" @@ -58,42 +57,46 @@ def testing_run(msgQueue: CheckingQueue): 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 """ + """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": + if isinstance(item, OCBCodeBlock) and item.title == "test1": block_of_test = item - print(item.title) break - msgQueue.check_equal(block_of_test is not None, True, "example_graph1 contains a block titled test1") + 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. + time.sleep(0.1) # wait for the lambda to complete. while block_of_test.is_running: - time.sleep(0.1) # wait for the execution to finish. + time.sleep(0.1) # wait for the execution to finish. + + time.sleep(0.1) file_content = open(asset_path).read() - print('"'+block_of_test.stdout.strip()+'"') - print('"'+file_content+'"') - - msgQueue.check_equal(block_of_test.stdout.strip(), file_content, "The asset file is read properly") + 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() \ No newline at end of file + self.window.close() diff --git a/tests/integration/blocks/test_flow.py b/tests/integration/blocks/test_flow.py index 3121567c..2f041aa3 100644 --- a/tests/integration/blocks/test_flow.py +++ b/tests/integration/blocks/test_flow.py @@ -14,24 +14,28 @@ class TestCodeBlocks: - @pytest.fixture(autouse=True) def setup(self): - """ Setup reused variables. """ + """Setup reused variables.""" start_app(self) self.ocb_widget.scene.load("tests/assets/flow_test.ipyg") - self.titles = ["Test flow 5", "Test flow 4", "Test no connection 1", - "Test input only 2", "Test output only 1"] - self.blocks_to_run = [None]*5 + self.titles = [ + "Test flow 5", + "Test flow 4", + "Test no connection 1", + "Test input only 2", + "Test output only 1", + ] + 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_duplicated_run(self): - """ Don't run a block twice when the execution flows """ + """Don't run a block twice when the execution flows""" for b in self.blocks_to_run: b.stdout = "" @@ -45,7 +49,7 @@ def run_block(): msgQueue.run_lambda(run_block) time.sleep(0.1) while block_to_run.is_running: - time.sleep(0.1) # wait for the execution to finish. + time.sleep(0.1) # wait for the execution to finish. # 6 and not 6\n6 msgQueue.check_equal(block_to_run.stdout.strip(), "6") @@ -54,8 +58,8 @@ def run_block(): apply_function_inapp(self.window, testing_no_duplicates) def test_flow_left(self): - """ Correct flow when pressing left run """ - + """Correct flow when pressing left run""" + for b in self.blocks_to_run: b.stdout = "" @@ -70,7 +74,7 @@ def run_block(): msgQueue.run_lambda(run_block) time.sleep(0.1) while block_to_run.is_running: - time.sleep(0.1) # wait for the execution to finish. + time.sleep(0.1) # wait for the execution to finish. msgQueue.check_equal(block_to_run.stdout.strip(), "6") msgQueue.check_equal(block_to_not_run.stdout.strip(), "") @@ -126,4 +130,4 @@ def run_block(): apply_function_inapp(self.window, testing_run) def test_finish(self): - self.window.close() \ No newline at end of file + self.window.close() diff --git a/tests/integration/test_window.py b/tests/integration/test_window.py index 780f041c..06a084bd 100644 --- a/tests/integration/test_window.py +++ b/tests/integration/test_window.py @@ -28,4 +28,4 @@ def test_open_file(self, qtbot): def test_window_close(self, qtbot): """closes""" - self.window.close() \ No newline at end of file + self.window.close() diff --git a/tests/integration/utils.py b/tests/integration/utils.py index 89f41067..53a5ab1c 100644 --- a/tests/integration/utils.py +++ b/tests/integration/utils.py @@ -35,35 +35,38 @@ def run_lambda(self, func: Callable, *args, **kwargs): def stop(self): self.put([STOP_MSG]) + class ExceptionForwardingThread(threading.Thread): - """ A Thread class that forwards the exceptions to the calling thread """ + """A Thread class that forwards the exceptions to the calling thread""" + def __init__(self, *args, **kwargs): - """ Create an exception forwarding thread """ + """Create an exception forwarding thread""" super().__init__(*args, **kwargs) self.e = None def run(self): - """ Code ran in another thread """ + """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 """ + """Used to sync the thread with the caller""" super().join() - print("except: ",self.e) + print("except: ", self.e) if self.e != None: raise self.e def start_app(obj): - """ Create a new app for testing """ + """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 @@ -78,7 +81,7 @@ def apply_function_inapp(window: OCBWindow, run_func: Callable): deadCounter = 0 while not stop: - time.sleep(1 / 30) # 30 fps + time.sleep(1 / 30) # 30 fps QApplication.processEvents() if not msgQueue.empty(): msg = msgQueue.get() @@ -93,6 +96,8 @@ def apply_function_inapp(window: OCBWindow, run_func: Callable): 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 !") + warnings.warn( + "Warning: you need to call CheckingQueue.stop() at the end of your test !" + ) break t.join() diff --git a/tests/unit/scene/test_ipynb_conversion.py b/tests/unit/scene/test_ipynb_conversion.py index d73f357d..82c00b43 100644 --- a/tests/unit/scene/test_ipynb_conversion.py +++ b/tests/unit/scene/test_ipynb_conversion.py @@ -51,7 +51,7 @@ def test_is_title(self, mocker: MockerFixture): def real_notebook_conversion_is_coherent(file_path: str): """Checks that the conversion of the ipynb notebook gives a coherent result. - + Args: file_path: the path to a .ipynb file """ @@ -59,7 +59,8 @@ def real_notebook_conversion_is_coherent(file_path: str): ipyg_data = ipynb_to_ipyg(ipynb_data) check_conversion_coherence(ipynb_data, ipyg_data) -def check_conversion_coherence(ipynb_data: OrderedDict, ipyg_data:OrderedDict): + +def check_conversion_coherence(ipynb_data: OrderedDict, ipyg_data: OrderedDict): """Checks that the ipyg data is coherent with the ipynb data. The conversion from ipynb to ipyg should return From 28175847232d628566da40db2f0d345c6b08effe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Mon, 20 Dec 2021 16:53:20 +0100 Subject: [PATCH 43/51] =?UTF-8?q?=F0=9F=AA=B2=20Prevent=20ipynb=20conversi?= =?UTF-8?q?on=20unit=20tests=20from=20interacting=20with=20the=20font=20ma?= =?UTF-8?q?nager.=20These=20are=20not=20integration=20tests=20!=20There=20?= =?UTF-8?q?is=20no=20QApplication=20setup=20that=20allows=20you=20to=20ret?= =?UTF-8?q?rieve=20theme=20information.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- opencodeblocks/scene/from_ipynb_conversion.py | 44 ++++++++++++++----- tests/unit/scene/test_ipynb_conversion.py | 4 +- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/opencodeblocks/scene/from_ipynb_conversion.py b/opencodeblocks/scene/from_ipynb_conversion.py index 7c2ba094..3165d690 100644 --- a/opencodeblocks/scene/from_ipynb_conversion.py +++ b/opencodeblocks/scene/from_ipynb_conversion.py @@ -9,10 +9,14 @@ from opencodeblocks.graphics.pyeditor import POINT_SIZE -def ipynb_to_ipyg(data: OrderedDict) -> OrderedDict: - """Convert ipynb data (ipynb file, as ordered dict) into ipyg data (ipyg, as ordered dict)""" +def ipynb_to_ipyg(data: OrderedDict, use_theme_font: bool = True) -> OrderedDict: + """ + Convert ipynb data (ipynb file, as ordered dict) into ipyg data (ipyg, as ordered dict) + - use_theme_font: should the height of the blocks be computed based on the current + font selected. + """ - blocks_data: List[OrderedDict] = get_blocks_data(data) + blocks_data: List[OrderedDict] = get_blocks_data(data, use_theme_font) edges_data: List[OrderedDict] = get_edges_data(blocks_data) return { @@ -21,7 +25,9 @@ def ipynb_to_ipyg(data: OrderedDict) -> OrderedDict: } -def get_blocks_data(data: OrderedDict) -> List[OrderedDict]: +def get_blocks_data( + data: OrderedDict, use_theme_font: bool = True +) -> List[OrderedDict]: """ Get the blocks corresponding to a ipynb file, Returns them in the ipyg ordered dict format @@ -31,12 +37,14 @@ def get_blocks_data(data: OrderedDict) -> List[OrderedDict]: return [] # Get the font metrics to determine the size fo the blocks - font = QFont() - font.setFamily(theme_manager().recommended_font_family) - font.setFixedPitch(True) - font.setPointSize(POINT_SIZE) - fontmetrics = QFontMetrics(font) - + fontmetrics = None + if use_theme_font: + font = QFont() + font.setFamily(theme_manager().recommended_font_family) + font.setFixedPitch(True) + font.setPointSize(POINT_SIZE) + fontmetrics = QFontMetrics(font) + blocks_data: List[OrderedDict] = [] next_block_x_pos: float = 0 @@ -50,15 +58,27 @@ def get_blocks_data(data: OrderedDict) -> List[OrderedDict]: block_type: str = cell["cell_type"] text: str = cell["source"] + + boundingWidth = 10 + if use_theme_font: + boundingWidth = fontmetrics.boundingRect(line).width() text_width: float = ( - max(fontmetrics.boundingRect(line).width() for line in text) + max(boundingWidth for line in text) if len(text) > 0 else 0 ) block_width: float = max(text_width + MARGIN_X, BLOCK_MIN_WIDTH) + + lineSpacing = 2 + lineWidth = 10 + + if use_theme_font: + lineSpacing = fontmetrics.lineSpacing() + lineWidth = fontmetrics.lineWidth() + text_height: float = len(text) * ( - fontmetrics.lineSpacing() + fontmetrics.lineWidth() + lineSpacing + lineWidth ) block_height: float = text_height + MARGIN_Y diff --git a/tests/unit/scene/test_ipynb_conversion.py b/tests/unit/scene/test_ipynb_conversion.py index 82c00b43..88d1a4cf 100644 --- a/tests/unit/scene/test_ipynb_conversion.py +++ b/tests/unit/scene/test_ipynb_conversion.py @@ -15,7 +15,7 @@ class TestIpynbConversion: def test_empty_data(self, mocker: MockerFixture): """should return empty ipyg graph for empty data.""" - check.equal(ipynb_to_ipyg({}), {"blocks": [], "edges": []}) + check.equal(ipynb_to_ipyg({}, False), {"blocks": [], "edges": []}) def test_empty_notebook_data(self, mocker: MockerFixture): """should return expected graph for a real empty notebook data.""" @@ -56,7 +56,7 @@ def real_notebook_conversion_is_coherent(file_path: str): file_path: the path to a .ipynb file """ ipynb_data = load_json(file_path) - ipyg_data = ipynb_to_ipyg(ipynb_data) + ipyg_data = ipynb_to_ipyg(ipynb_data, False) check_conversion_coherence(ipynb_data, ipyg_data) From dcc12524bd3df9a3d2ee6d494579b722973cad1e Mon Sep 17 00:00:00 2001 From: Alexandre Sajus Date: Mon, 20 Dec 2021 18:31:56 +0100 Subject: [PATCH 44/51] :beetle: fixes interrupt execution --- opencodeblocks/blocks/codeblock.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/opencodeblocks/blocks/codeblock.py b/opencodeblocks/blocks/codeblock.py index 9836b91f..14aa2438 100644 --- a/opencodeblocks/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -174,14 +174,14 @@ def has_output(self) -> bool: def _interrupt_execution(self): """Interrupt an execution, reset the blocks in the queue""" - for block, _ in self.source_editor.kernel.execution_queue: + for block, _ in self.scene().kernel.execution_queue: # Reset the blocks that have not been run - block.reset_after_run() - block.has_been_run = False + block.reset_has_been_run() + block.execution_finished() # Clear the queue - self.source_editor.kernel.execution_queue = [] + self.scene().kernel.execution_queue = [] # Interrupt the kernel - self.source_editor.kernel.kernel_manager.interrupt_kernel() + self.scene().kernel.kernel_manager.interrupt_kernel() def transmitting_animation_in(self): """ From 042b491bd387ee6fd719eacd56627a5f2d6c61dd Mon Sep 17 00:00:00 2001 From: Alexandre Sajus Date: Mon, 20 Dec 2021 22:06:02 +0100 Subject: [PATCH 45/51] :hammer: modified animation behavior --- opencodeblocks/blocks/codeblock.py | 134 ++++++++++++++++++-------- tests/integration/blocks/test_flow.py | 17 +--- 2 files changed, 97 insertions(+), 54 deletions(-) diff --git a/opencodeblocks/blocks/codeblock.py b/opencodeblocks/blocks/codeblock.py index 14aa2438..6f24689b 100644 --- a/opencodeblocks/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -65,6 +65,7 @@ def __init__(self, source: str = "", **kwargs): self._splitter_size = [1, 1] self._cached_stdout = "" self.has_been_run = False + self.blocks_to_run = [] self._pen_outline = QPen(QColor("#7F000000")) self._pen_outline_running = QPen(QColor("#FF0000")) @@ -178,10 +179,12 @@ def _interrupt_execution(self): # Reset the blocks that have not been run block.reset_has_been_run() block.execution_finished() - # Clear the queue + # 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): """ @@ -189,8 +192,8 @@ def transmitting_animation_in(self): Set color to transmitting and set a timer before switching to normal """ for elem in self.transmitting_queue[0]: - if elem.run_color != 1: - elem.run_color = 2 + # Set color to transmitting + elem.run_color = 2 QApplication.processEvents() QTimer.singleShot(self.transmitting_delay, self.transmitting_animation_out) @@ -200,12 +203,21 @@ def transmitting_animation_out(self): After the timer, set color to normal and move on with the queue """ for elem in self.transmitting_queue[0]: - if elem.run_color != 1: + # 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): """ @@ -257,26 +269,89 @@ def custom_bfs(self, start_node, reverse=False): return blocks_to_run, to_transmit - def run_blocks(self, blocks_to_run): + def custom_djikstra(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 blocks_to_run[::-1]: + 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 - # If the user presses left run when running, cancel the execution - if self.run_button.text() == "...": - self._interrupt_execution() - 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 @@ -287,12 +362,6 @@ def run_left(self): # Start transmitting animation self.transmitting_animation_in() - # Run the blocks - self.run_blocks(blocks_to_run) - - # Run self - self.run_code() - def run_right(self): """Run all of the output blocks and all their dependencies""" @@ -300,34 +369,18 @@ def run_right(self): if len(self.transmitting_queue) != 0: return - # If the user presses right run when running, cancel the execution - if self.run_all_button.text() == "...": - self._interrupt_execution() - return + # Create transmitting queue + self.transmitting_queue = self.custom_djikstra() # Gather outputs - blocks_to_run, to_transmit_right = self.custom_bfs(self, reverse=True) - - # Init transmitting queue - self.transmitting_queue = to_transmit_right - - # Gather dependencies - to_transmit_left = [] + 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[::-1]: # Gather dependencies - new_blocks_to_run, new_to_transmit = self.custom_bfs(block) + new_blocks_to_run, _ = self.custom_bfs(block) blocks_to_run += new_blocks_to_run - # Add new_to_transmit to transmit_left - # so that each left_transmit starts at the same time - for i in range(len(new_to_transmit)): - if i < len(to_transmit_left): - to_transmit_left[i] += new_to_transmit[i] - else: - to_transmit_left.append(new_to_transmit[i]) - - self.transmitting_queue += to_transmit_left # Set delay so that the transmitting animation has fixed total duration self.transmitting_delay = int( @@ -336,9 +389,6 @@ def run_right(self): # Start transmitting animation self.transmitting_animation_in() - # Run the blocks - self.run_blocks(blocks_to_run) - def reset_has_been_run(self): """Reset has_been_run, is called when the output is an error""" self.has_been_run = False @@ -346,8 +396,10 @@ def reset_has_been_run(self): def execution_finished(self): """Reset the text of the run buttons""" super().execution_finished() + self.run_color = 0 self.run_button.setText(">") self.run_all_button.setText(">>") + self.blocks_to_run = [] def update_title(self): """Change the geometry of the title widget""" diff --git a/tests/integration/blocks/test_flow.py b/tests/integration/blocks/test_flow.py index 2f041aa3..6da43fe4 100644 --- a/tests/integration/blocks/test_flow.py +++ b/tests/integration/blocks/test_flow.py @@ -47,9 +47,7 @@ def run_block(): block_to_run.run_right() msgQueue.run_lambda(run_block) - time.sleep(0.1) - while block_to_run.is_running: - time.sleep(0.1) # wait for the execution to finish. + time.sleep(2) # 6 and not 6\n6 msgQueue.check_equal(block_to_run.stdout.strip(), "6") @@ -72,9 +70,7 @@ def run_block(): block_to_run.run_left() msgQueue.run_lambda(run_block) - time.sleep(0.1) - while block_to_run.is_running: - time.sleep(0.1) # wait for the execution to finish. + time.sleep(2) msgQueue.check_equal(block_to_run.stdout.strip(), "6") msgQueue.check_equal(block_to_not_run.stdout.strip(), "") @@ -97,10 +93,7 @@ def run_block(): print("About to run !") msgQueue.run_lambda(run_block) - time.sleep(0.1) - while block_to_run.is_running: - print("wait ...") - time.sleep(0.1) + time.sleep(2) msgQueue.check_equal(block_to_run.stdout.strip(), "1") msgQueue.stop() @@ -120,9 +113,7 @@ def run_block(): block_to_run.run_right() msgQueue.run_lambda(run_block) - time.sleep(0.1) - while block_to_run.is_running: - time.sleep(0.1) + time.sleep(2) # Just check that it doesn't crash msgQueue.stop() From 7dbd7930016cb098d4e973834031fe924451cca0 Mon Sep 17 00:00:00 2001 From: Alexandre Sajus Date: Tue, 21 Dec 2021 14:56:25 +0100 Subject: [PATCH 46/51] :wrench: moves from code to executableblock --- opencodeblocks/blocks/codeblock.py | 246 +---------------------- opencodeblocks/blocks/executableblock.py | 246 +++++++++++++++++++---- 2 files changed, 210 insertions(+), 282 deletions(-) diff --git a/opencodeblocks/blocks/codeblock.py b/opencodeblocks/blocks/codeblock.py index 6f24689b..b61ba9a5 100644 --- a/opencodeblocks/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -135,14 +135,14 @@ def init_run_all_button(self): def handle_run_right(self): """Called when the button for "Run All" was pressed""" - if self.is_running: + 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.is_running: + if self.run_color != 0: self._interrupt_execution() else: self.run_left() @@ -159,248 +159,6 @@ def run_code(self): super().run_code() # actually run the code - 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.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 custom_djikstra(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.custom_djikstra() - - # 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[::-1]: - # Gather dependencies - new_blocks_to_run, _ = self.custom_bfs(block) - 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): - """Reset has_been_run, is called when the output is an error""" - self.has_been_run = False - - def execution_finished(self): - """Reset the text of the run buttons""" - super().execution_finished() - self.run_color = 0 - self.run_button.setText(">") - self.run_all_button.setText(">>") - self.blocks_to_run = [] - def update_title(self): """Change the geometry of the title widget""" self.title_widget.setGeometry( diff --git a/opencodeblocks/blocks/executableblock.py b/opencodeblocks/blocks/executableblock.py index 5f228142..46629013 100644 --- a/opencodeblocks/blocks/executableblock.py +++ b/opencodeblocks/blocks/executableblock.py @@ -1,7 +1,9 @@ """ Module for the executable block class """ -from typing import List, OrderedDict +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 @@ -30,7 +32,8 @@ def __init__(self, **kwargs): super().__init__(**kwargs) self.has_been_run = False - self.is_running = False + self.run_color = 0 + self.transmitting_duration = 500 # Add execution flow sockets exe_sockets = ( @@ -65,65 +68,232 @@ def run_code(self): kernel = self.scene().kernel kernel.execution_queue.append((self, code)) - self.is_running = True - if kernel.busy is False: kernel.run_queue() self.has_been_run = True def execution_finished(self): - """ - Method called when the execution of the block is finished. - Implement the behavior you want here. - """ - self.is_running = False + """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""" - kernel = self.scene().kernel - for block, _ in kernel.execution_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() - block.has_been_run = False - # Clear the queue - kernel.execution_queue = [] + # Clear kernel execution queue + self.scene().kernel.execution_queue = [] # Interrupt the kernel - kernel.kernel_manager.interrupt_kernel() + self.scene().kernel.kernel_manager.interrupt_kernel() + # Clear local execution queue + self.blocks_to_run = [] - def run_left(self): + 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 """ - Run all of the block's dependencies and then run the block + # 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 - if self.has_input(): - # Create the graph from the scene - graph = self.scene().create_graph() - # BFS through the input graph - edges = bfs_edges(graph, self, reverse=True) - # Run the blocks found except self - blocks_to_run: List["OCBExecutableBlock"] = [v for _, v in edges] - for block in blocks_to_run[::-1]: - if not block.has_been_run: - block.run_code() - - if self.is_running: + 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 - self.run_code() + + # 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""" - # If no output, run left - if not self.has_output(): - self.run_left() + # To avoid crashing when spamming the button + if len(self.transmitting_queue) != 0: return - # Same as run_left but instead of running the blocks, we'll use run_left - graph = self.scene().create_graph() - edges = bfs_edges(graph, self) - blocks_to_run: List["OCBExecutableBlock"] = [self] + [v for _, v in edges] + # 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[::-1]: - block.run_left() + # Gather dependencies + new_blocks_to_run, _ = self.custom_bfs(block) + 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""" From a1a1f46fb13da22c8ce95180c0590abb2d2756ae Mon Sep 17 00:00:00 2001 From: Alexandre Sajus Date: Tue, 21 Dec 2021 15:08:12 +0100 Subject: [PATCH 47/51] :wrench: reduced testing time --- tests/integration/blocks/test_flow.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/integration/blocks/test_flow.py b/tests/integration/blocks/test_flow.py index 6da43fe4..4bd3747e 100644 --- a/tests/integration/blocks/test_flow.py +++ b/tests/integration/blocks/test_flow.py @@ -47,7 +47,9 @@ def run_block(): block_to_run.run_right() msgQueue.run_lambda(run_block) - time.sleep(2) + 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") @@ -70,7 +72,9 @@ def run_block(): block_to_run.run_left() msgQueue.run_lambda(run_block) - time.sleep(2) + 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(), "6") msgQueue.check_equal(block_to_not_run.stdout.strip(), "") @@ -93,7 +97,9 @@ def run_block(): print("About to run !") msgQueue.run_lambda(run_block) - time.sleep(2) + 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() @@ -113,7 +119,9 @@ def run_block(): block_to_run.run_right() msgQueue.run_lambda(run_block) - time.sleep(2) + 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() From 3732f3d1a4df7f562cc444fa0b277ff6f0a88da7 Mon Sep 17 00:00:00 2001 From: Alexandre Sajus Date: Tue, 21 Dec 2021 15:15:10 +0100 Subject: [PATCH 48/51] :beetle: changes is_running attribute --- tests/integration/blocks/test_codeblock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/blocks/test_codeblock.py b/tests/integration/blocks/test_codeblock.py index a6f79a7d..54baa553 100644 --- a/tests/integration/blocks/test_codeblock.py +++ b/tests/integration/blocks/test_codeblock.py @@ -81,7 +81,7 @@ def run_block(): msgQueue.run_lambda(run_block) time.sleep(0.1) # wait for the lambda to complete. - while block_of_test.is_running: + while block_of_test.run_color != 0: time.sleep(0.1) # wait for the execution to finish. time.sleep(0.1) From 2542aa6c88557e0b51fba4ef6d5b53eaab8a79c7 Mon Sep 17 00:00:00 2001 From: AlexandreSajus Date: Tue, 21 Dec 2021 15:44:48 +0100 Subject: [PATCH 49/51] :beetle: fixes run_code test --- tests/integration/blocks/test_codeblock.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/integration/blocks/test_codeblock.py b/tests/integration/blocks/test_codeblock.py index 54baa553..3c304a0a 100644 --- a/tests/integration/blocks/test_codeblock.py +++ b/tests/integration/blocks/test_codeblock.py @@ -51,7 +51,9 @@ def testing_run(msgQueue: CheckingQueue): pyautogui.mouseDown(button="left") pyautogui.mouseUp(button="left") - time.sleep(0.5) + time.sleep((test_block.transmitting_duration / 1000) + 0.2) + while test_block.run_color != 0: + time.sleep(0.1) msgQueue.check_equal(test_block.stdout.strip(), expected_result) msgQueue.stop() From d6150cd1e19298609ba432888ab77ae1cbf15d6b Mon Sep 17 00:00:00 2001 From: Alexandre Sajus Date: Tue, 21 Dec 2021 15:56:21 +0100 Subject: [PATCH 50/51] :wrench: moves from code to executable --- opencodeblocks/blocks/codeblock.py | 17 ----------------- opencodeblocks/blocks/executableblock.py | 7 +++++++ 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/opencodeblocks/blocks/codeblock.py b/opencodeblocks/blocks/codeblock.py index b61ba9a5..229f5681 100644 --- a/opencodeblocks/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -75,23 +75,6 @@ def __init__(self, source: str = "", **kwargs): self._pen_outline_running, self._pen_outline_transmitting, ] - # 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 - self.transmitting_delay = 200 - - # Add exectution flow sockets - exe_sockets = ( - OCBSocket(self, socket_type="input", flow_type="exe"), - OCBSocket(self, socket_type="output", flow_type="exe"), - ) - for socket in exe_sockets: - self.add_socket(socket) # Add output pannel self.output_panel = self.init_output_panel() diff --git a/opencodeblocks/blocks/executableblock.py b/opencodeblocks/blocks/executableblock.py index 46629013..e2d232d5 100644 --- a/opencodeblocks/blocks/executableblock.py +++ b/opencodeblocks/blocks/executableblock.py @@ -32,7 +32,14 @@ def __init__(self, **kwargs): 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 From 868c209e0ed1d83396cce3e62d0b1f398f06e90e Mon Sep 17 00:00:00 2001 From: Alexandre Sajus Date: Tue, 21 Dec 2021 16:05:17 +0100 Subject: [PATCH 51/51] :wrench: refactor for clarity --- opencodeblocks/blocks/executableblock.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opencodeblocks/blocks/executableblock.py b/opencodeblocks/blocks/executableblock.py index e2d232d5..a6adb77e 100644 --- a/opencodeblocks/blocks/executableblock.py +++ b/opencodeblocks/blocks/executableblock.py @@ -290,10 +290,10 @@ def run_right(self): self.blocks_to_run = blocks_to_run # For each output found - for block in blocks_to_run[::-1]: + for block in blocks_to_run.copy()[::-1]: # Gather dependencies new_blocks_to_run, _ = self.custom_bfs(block) - blocks_to_run += new_blocks_to_run + self.blocks_to_run += new_blocks_to_run # Set delay so that the transmitting animation has fixed total duration self.transmitting_delay = int(