From 60b1cacc76a7dfbc41fcfa8d7631be70e532e98f Mon Sep 17 00:00:00 2001 From: AlexandreSajus Date: Wed, 8 Dec 2021 11:32:25 +0100 Subject: [PATCH 01/18] :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/18] :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/18] :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/18] :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 3cf33269f045cfb9fbb4d7e474990e8e8f09da4e Mon Sep 17 00:00:00 2001 From: AlexandreSajus Date: Sat, 11 Dec 2021 22:29:51 +0100 Subject: [PATCH 05/18] :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 e2d7689298405e7c84bb35662b2a991e13cfc96d Mon Sep 17 00:00:00 2001 From: AlexandreSajus Date: Sat, 11 Dec 2021 23:12:35 +0100 Subject: [PATCH 06/18] :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 93b13c5c35514129ba1702e8506cdd1aa7abcf49 Mon Sep 17 00:00:00 2001 From: AlexandreSajus Date: Sun, 12 Dec 2021 02:37:14 +0100 Subject: [PATCH 07/18] :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 08/18] :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 09/18] :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 10/18] :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 dcc12524bd3df9a3d2ee6d494579b722973cad1e Mon Sep 17 00:00:00 2001 From: Alexandre Sajus Date: Mon, 20 Dec 2021 18:31:56 +0100 Subject: [PATCH 11/18] :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 12/18] :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 13/18] :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 14/18] :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 15/18] :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 16/18] :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 17/18] :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 18/18] :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(