From 27022fbe2f4960fcc0c23b90b8ec147a8c2c060d Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Thu, 2 Dec 2021 18:35:15 +0100 Subject: [PATCH 01/27] Add beginning of ipynb conversion --- opencodeblocks/scene/ipynb_conversion.py | 55 ++++++++++++++++++++++++ opencodeblocks/scene/scene.py | 15 +++++++ 2 files changed, 70 insertions(+) create mode 100644 opencodeblocks/scene/ipynb_conversion.py diff --git a/opencodeblocks/scene/ipynb_conversion.py b/opencodeblocks/scene/ipynb_conversion.py new file mode 100644 index 00000000..bbe8cfe3 --- /dev/null +++ b/opencodeblocks/scene/ipynb_conversion.py @@ -0,0 +1,55 @@ +""" Module for converting ipynb data to ipyg data """ + +from typing import OrderedDict, List + +def ipynb_to_ipyg(data: OrderedDict) -> OrderedDict: + id: int = 0 + + blocks: List[OrderedDict] = get_blocks(data) + + return { + "id": id, + "blocks": blocks, + "edges": [] + } + +def get_blocks(data: OrderedDict) -> List[OrderedDict]: + if "cells" not in data: + return [] + + blocks: List[OrderedDict] = [] + + # Markdown cells to be passed to the next code block + markdown_blocks: List[OrderedDict] = [] + for cell in data["cells"]: + if "cell_type" not in cell or cell["cell_type"] != "code": + pass # Not supported yet + else: + blocks.append({ + "id": 0, + "title": "_", + "block_type": "code", + "source": ''.join(cell["source"]), + "stdout": '', + "width": 500, + "height": 200, + "position": [ + len(blocks)*500, + 0 + ], + "splitter_pos": [ + 85, + 261 + ], + "sockets": [], + "metadata": { + "title_metadata": { + "color": "white", + "font": "Ubuntu", + "size": 12, + "padding": 4.0 + } + } + }) + + return blocks \ No newline at end of file diff --git a/opencodeblocks/scene/scene.py b/opencodeblocks/scene/scene.py index 772ba2b2..df6fb3c5 100644 --- a/opencodeblocks/scene/scene.py +++ b/opencodeblocks/scene/scene.py @@ -18,6 +18,7 @@ from opencodeblocks.graphics.edge import OCBEdge from opencodeblocks.scene.clipboard import SceneClipboard from opencodeblocks.scene.history import SceneHistory +from opencodeblocks.scene.ipynb_conversion import ipynb_to_ipyg class OCBScene(QGraphicsScene, Serializable): @@ -140,6 +141,9 @@ def load(self, filepath: str): """ if filepath.endswith('.ipyg'): data = self.load_from_ipyg(filepath) + elif filepath.endswith('.ipynb'): + ipynb_data = self.load_from_ipynb(filepath) + data = ipynb_to_ipyg(ipynb_data) else: extention_format = filepath.split('.')[-1] raise NotImplementedError(f"Unsupported format {extention_format}") @@ -158,6 +162,17 @@ def load_from_ipyg(self, filepath: str): data = json.loads(file.read()) return data + def load_from_ipynb(self, filepath: str) -> OrderedDict: + """ Load the ipynb json data. + + Args: + filepath: Path to the .ipynb file to load. + + """ + 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. """ self.has_been_modified = False From 775a220b09b8ffc066064954e4f731ac48201763 Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Thu, 2 Dec 2021 23:01:01 +0100 Subject: [PATCH 02/27] Clean the converter and add TODOs --- opencodeblocks/scene/ipynb_conversion.py | 54 +++++++++++------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/opencodeblocks/scene/ipynb_conversion.py b/opencodeblocks/scene/ipynb_conversion.py index bbe8cfe3..24e7889d 100644 --- a/opencodeblocks/scene/ipynb_conversion.py +++ b/opencodeblocks/scene/ipynb_conversion.py @@ -2,8 +2,12 @@ from typing import OrderedDict, List +import json + +MARGIN: float = 50 + def ipynb_to_ipyg(data: OrderedDict) -> OrderedDict: - id: int = 0 + id: int = 0 # TODO : give a proper id blocks: List[OrderedDict] = get_blocks(data) @@ -19,37 +23,29 @@ def get_blocks(data: OrderedDict) -> List[OrderedDict]: blocks: List[OrderedDict] = [] - # Markdown cells to be passed to the next code block - markdown_blocks: List[OrderedDict] = [] for cell in data["cells"]: if "cell_type" not in cell or cell["cell_type"] != "code": - pass # Not supported yet + pass # TODO : support markdown else: - blocks.append({ - "id": 0, - "title": "_", - "block_type": "code", - "source": ''.join(cell["source"]), - "stdout": '', - "width": 500, - "height": 200, - "position": [ - len(blocks)*500, + # Load the default empty block + # TODO : add something in case the user renames / removes the empty block / changes it too much ? + data: OrderedDict = {} + with open("blocks/empty.ocbb", 'r', encoding='utf-8') as file: + data = json.loads(file.read()) + + data["id"] = 0 # TODO : give a proper id + + data["position"] = [ + len(blocks)*(data["width"] + MARGIN), 0 - ], - "splitter_pos": [ - 85, - 261 - ], - "sockets": [], - "metadata": { - "title_metadata": { - "color": "white", - "font": "Ubuntu", - "size": 12, - "padding": 4.0 - } - } - }) + ] + + data["source"] = ''.join(cell["source"]) + + data["sockets"] = {} # TODO : add sockets + + # TODO : add support for output + + blocks.append(data) return blocks \ No newline at end of file From cb35b720b2c9f3cfed74ece961adfb229133ceb3 Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Thu, 2 Dec 2021 23:26:25 +0100 Subject: [PATCH 03/27] Correct pylint --- opencodeblocks/scene/ipynb_conversion.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/opencodeblocks/scene/ipynb_conversion.py b/opencodeblocks/scene/ipynb_conversion.py index 24e7889d..fa6e3cdb 100644 --- a/opencodeblocks/scene/ipynb_conversion.py +++ b/opencodeblocks/scene/ipynb_conversion.py @@ -7,17 +7,21 @@ MARGIN: float = 50 def ipynb_to_ipyg(data: OrderedDict) -> OrderedDict: - id: int = 0 # TODO : give a proper id + """ Convert ipynb data (ipynb file, as ordered dict) into ipyg data (ipyg, as ordered dict) """ + + dataid: int = 0 # TODO : give a proper id blocks: List[OrderedDict] = get_blocks(data) return { - "id": id, + "id": dataid, "blocks": blocks, "edges": [] } def get_blocks(data: OrderedDict) -> List[OrderedDict]: + """ Get the blocks corresponding to a ipynb file, returns them in the ipyg ordered dict format """ + if "cells" not in data: return [] @@ -48,4 +52,5 @@ def get_blocks(data: OrderedDict) -> List[OrderedDict]: blocks.append(data) - return blocks \ No newline at end of file + return blocks + \ No newline at end of file From 21de930c7849fb811b7837f19fcb7087a51be17d Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Fri, 3 Dec 2021 11:48:23 +0100 Subject: [PATCH 04/27] Add conversion for markdown blocks --- opencodeblocks/scene/ipynb_conversion.py | 89 ++++++++++++++++++------ opencodeblocks/scene/scene.py | 21 ++---- 2 files changed, 71 insertions(+), 39 deletions(-) diff --git a/opencodeblocks/scene/ipynb_conversion.py b/opencodeblocks/scene/ipynb_conversion.py index fa6e3cdb..22554c31 100644 --- a/opencodeblocks/scene/ipynb_conversion.py +++ b/opencodeblocks/scene/ipynb_conversion.py @@ -1,20 +1,30 @@ """ Module for converting ipynb data to ipyg data """ +from pickle import DICT from typing import OrderedDict, List import json -MARGIN: float = 50 +MARGIN_X: float = 50 +MARGIN_Y: float = 50 +TEXT_SIZE: float = 12 +TEXT_SIZE_TO_WIDTH_RATIO: float = 0.7 +TEXT_SIZE_TO_HEIGHT_RATIO: float = 1.42 +ipyg_id_generator = lambda: 0 +block_id_generator = lambda: 0 + +BLOCK_TYPE_TO_NAME: DICT= { + "code" : "OCBCodeBlock", + "markdown" : "OCBMarkdownBlock" +} def ipynb_to_ipyg(data: OrderedDict) -> OrderedDict: """ Convert ipynb data (ipynb file, as ordered dict) into ipyg data (ipyg, as ordered dict) """ - dataid: int = 0 # TODO : give a proper id - blocks: List[OrderedDict] = get_blocks(data) return { - "id": dataid, + "id": ipyg_id_generator(), "blocks": blocks, "edges": [] } @@ -27,30 +37,63 @@ def get_blocks(data: OrderedDict) -> List[OrderedDict]: blocks: List[OrderedDict] = [] + next_block_x_pos: float = 0 + next_block_y_pos: float = 0 + for cell in data["cells"]: - if "cell_type" not in cell or cell["cell_type"] != "code": - pass # TODO : support markdown + if "cell_type" not in cell or cell["cell_type"] not in ["code", "markdown"]: + pass else: - # Load the default empty block - # TODO : add something in case the user renames / removes the empty block / changes it too much ? - data: OrderedDict = {} - with open("blocks/empty.ocbb", 'r', encoding='utf-8') as file: - data = json.loads(file.read()) - - data["id"] = 0 # TODO : give a proper id - - data["position"] = [ - len(blocks)*(data["width"] + MARGIN), - 0 - ] - - data["source"] = ''.join(cell["source"]) + block_type = cell["cell_type"] - data["sockets"] = {} # TODO : add sockets + text = cell["source"] - # TODO : add support for output + text_width: float = TEXT_SIZE * TEXT_SIZE_TO_WIDTH_RATIO * max(len(line) for line in text) + block_width: float = text_width + MARGIN_X + text_height: float = TEXT_SIZE * TEXT_SIZE_TO_HEIGHT_RATIO * len(text) + block_height: float = text_height + MARGIN_Y + + block = { + "id": block_id_generator(), + "title": "_", + "block_type": BLOCK_TYPE_TO_NAME[block_type], + "width": block_width, + "height": block_height, + "position": [ + next_block_x_pos, + next_block_y_pos + ], + "splitter_pos": [ + 85, + 261 + ], + "sockets": [], + "metadata": { + "title_metadata": { + "color": "white", + "font": "Ubuntu", + "size": 12, + "padding": 4.0 + } + } + } + + if block_type == "code": + block.update({ + "source": ''.join(text), + "stdout": '' + }) + next_block_y_pos = 0 + next_block_x_pos += block_width + elif block_type == "markdown": + block.update({ + "text": ''.join(text) + }) + next_block_y_pos += block_height + + + blocks.append(block) - blocks.append(data) return blocks \ No newline at end of file diff --git a/opencodeblocks/scene/scene.py b/opencodeblocks/scene/scene.py index e2be38be..04ff383d 100644 --- a/opencodeblocks/scene/scene.py +++ b/opencodeblocks/scene/scene.py @@ -141,9 +141,9 @@ def load(self, filepath: str): """ if filepath.endswith('.ipyg'): - data = self.load_from_ipyg(filepath) + data = self.load_from_json(filepath) elif filepath.endswith('.ipynb'): - ipynb_data = self.load_from_ipynb(filepath) + ipynb_data = self.load_from_json(filepath) data = ipynb_to_ipyg(ipynb_data) else: extention_format = filepath.split('.')[-1] @@ -152,22 +152,11 @@ def load(self, filepath: str): 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. + def load_from_json(self, filepath: str) -> OrderedDict: + """ Load the ipynb json data into an ordered dict Args: - filepath: Path to the .ipyg file to load. - - """ - with open(filepath, 'r', encoding='utf-8') as file: - data = json.loads(file.read()) - return data - - def load_from_ipynb(self, filepath: str) -> OrderedDict: - """ Load the ipynb json data. - - Args: - filepath: Path to the .ipynb file to load. + filepath: Path to the file to load. """ with open(filepath, 'r', encoding='utf-8') as file: From 71c75a3fafd53977cf9c218589185029be58a7b9 Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Fri, 3 Dec 2021 11:54:40 +0100 Subject: [PATCH 05/27] Correct Pylint --- opencodeblocks/scene/ipynb_conversion.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/opencodeblocks/scene/ipynb_conversion.py b/opencodeblocks/scene/ipynb_conversion.py index 22554c31..c7ed5528 100644 --- a/opencodeblocks/scene/ipynb_conversion.py +++ b/opencodeblocks/scene/ipynb_conversion.py @@ -1,9 +1,6 @@ """ Module for converting ipynb data to ipyg data """ -from pickle import DICT -from typing import OrderedDict, List - -import json +from typing import OrderedDict, List, Dict MARGIN_X: float = 50 MARGIN_Y: float = 50 @@ -13,7 +10,7 @@ ipyg_id_generator = lambda: 0 block_id_generator = lambda: 0 -BLOCK_TYPE_TO_NAME: DICT= { +BLOCK_TYPE_TO_NAME: Dict[str, str] = { "code" : "OCBCodeBlock", "markdown" : "OCBMarkdownBlock" } @@ -30,7 +27,10 @@ def ipynb_to_ipyg(data: OrderedDict) -> OrderedDict: } def get_blocks(data: OrderedDict) -> List[OrderedDict]: - """ Get the blocks corresponding to a ipynb file, returns them in the ipyg ordered dict format """ + """ + Get the blocks corresponding to a ipynb file, + Returns them in the ipyg ordered dict format + """ if "cells" not in data: return [] From 576c7aa6c1c65aaad7f446065ca7ec0a99faec0d Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Fri, 3 Dec 2021 13:28:17 +0100 Subject: [PATCH 06/27] Apply black style --- opencodeblocks/scene/ipynb_conversion.py | 53 ++++++----- opencodeblocks/scene/scene.py | 110 ++++++++++++----------- 2 files changed, 88 insertions(+), 75 deletions(-) diff --git a/opencodeblocks/scene/ipynb_conversion.py b/opencodeblocks/scene/ipynb_conversion.py index c7ed5528..c077d7b1 100644 --- a/opencodeblocks/scene/ipynb_conversion.py +++ b/opencodeblocks/scene/ipynb_conversion.py @@ -11,30 +11,32 @@ block_id_generator = lambda: 0 BLOCK_TYPE_TO_NAME: Dict[str, str] = { - "code" : "OCBCodeBlock", - "markdown" : "OCBMarkdownBlock" + "code": "OCBCodeBlock", + "markdown": "OCBMarkdownBlock", } + def ipynb_to_ipyg(data: OrderedDict) -> OrderedDict: - """ Convert ipynb data (ipynb file, as ordered dict) into ipyg data (ipyg, as ordered dict) """ + """Convert ipynb data (ipynb file, as ordered dict) into ipyg data (ipyg, as ordered dict)""" blocks: List[OrderedDict] = get_blocks(data) return { "id": ipyg_id_generator(), "blocks": blocks, - "edges": [] + "edges": [], } + def get_blocks(data: OrderedDict) -> List[OrderedDict]: - """ - Get the blocks corresponding to a ipynb file, - Returns them in the ipyg ordered dict format + """ + Get the blocks corresponding to a ipynb file, + Returns them in the ipyg ordered dict format """ if "cells" not in data: return [] - + blocks: List[OrderedDict] = [] next_block_x_pos: float = 0 @@ -48,11 +50,13 @@ def get_blocks(data: OrderedDict) -> List[OrderedDict]: text = cell["source"] - text_width: float = TEXT_SIZE * TEXT_SIZE_TO_WIDTH_RATIO * max(len(line) for line in text) + text_width: float = ( + TEXT_SIZE * TEXT_SIZE_TO_WIDTH_RATIO * max(len(line) for line in text) + ) block_width: float = text_width + MARGIN_X text_height: float = TEXT_SIZE * TEXT_SIZE_TO_HEIGHT_RATIO * len(text) block_height: float = text_height + MARGIN_Y - + block = { "id": block_id_generator(), "title": "_", @@ -61,11 +65,11 @@ def get_blocks(data: OrderedDict) -> List[OrderedDict]: "height": block_height, "position": [ next_block_x_pos, - next_block_y_pos + next_block_y_pos, ], "splitter_pos": [ 85, - 261 + 261, ], "sockets": [], "metadata": { @@ -73,27 +77,28 @@ def get_blocks(data: OrderedDict) -> List[OrderedDict]: "color": "white", "font": "Ubuntu", "size": 12, - "padding": 4.0 + "padding": 4.0, } - } + }, } if block_type == "code": - block.update({ - "source": ''.join(text), - "stdout": '' - }) + block.update( + { + "source": "".join(text), + "stdout": "", + } + ) next_block_y_pos = 0 next_block_x_pos += block_width elif block_type == "markdown": - block.update({ - "text": ''.join(text) - }) + block.update( + { + "text": "".join(text), + } + ) next_block_y_pos += block_height - blocks.append(block) - return blocks - \ No newline at end of file diff --git a/opencodeblocks/scene/scene.py b/opencodeblocks/scene/scene.py index 04ff383d..63b969d1 100644 --- a/opencodeblocks/scene/scene.py +++ b/opencodeblocks/scene/scene.py @@ -24,13 +24,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) @@ -41,8 +47,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 @@ -53,7 +58,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 @@ -63,11 +68,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): @@ -77,12 +82,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())) @@ -117,54 +122,54 @@ 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_json(filepath) - elif filepath.endswith('.ipynb'): + elif filepath.endswith(".ipynb"): ipynb_data = self.load_from_json(filepath) data = ipynb_to_ipyg(ipynb_data) 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_json(self, filepath: str) -> OrderedDict: - """ Load the ipynb json data into an ordered dict + """Load the ipynb json data into an ordered dict Args: filepath: Path to the 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() @@ -178,24 +183,26 @@ 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]), - ]) - - 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: + return OrderedDict( + [ + ("id", self.id), + ("blocks", [block.serialize() for block in blocks]), + ("edges", [edge.serialize() for edge in edges]), + ] + ) + + 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 @@ -203,10 +210,10 @@ def create_block(self, data: OrderedDict, hashmap: dict = None, block_files = blocks.__dict__ for block_name in block_files: - block_module = getattr(blocks,block_name) + 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") @@ -215,23 +222,24 @@ def create_block(self, data: OrderedDict, hashmap: dict = None, 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 2b0686f789325fa37fe9df6faf4940f6fabf3b76 Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Fri, 3 Dec 2021 17:53:01 +0100 Subject: [PATCH 07/27] Fix display and empty text bug --- opencodeblocks/scene/ipynb_conversion.py | 81 ++++++++++++++++-------- 1 file changed, 56 insertions(+), 25 deletions(-) diff --git a/opencodeblocks/scene/ipynb_conversion.py b/opencodeblocks/scene/ipynb_conversion.py index c077d7b1..ee8e87fc 100644 --- a/opencodeblocks/scene/ipynb_conversion.py +++ b/opencodeblocks/scene/ipynb_conversion.py @@ -4,6 +4,7 @@ MARGIN_X: float = 50 MARGIN_Y: float = 50 +BLOCK_MIN_WIDTH = 400 TEXT_SIZE: float = 12 TEXT_SIZE_TO_WIDTH_RATIO: float = 0.7 TEXT_SIZE_TO_HEIGHT_RATIO: float = 1.42 @@ -52,35 +53,26 @@ def get_blocks(data: OrderedDict) -> List[OrderedDict]: text_width: float = ( TEXT_SIZE * TEXT_SIZE_TO_WIDTH_RATIO * max(len(line) for line in text) + if len(text) > 0 + else 0 ) - block_width: float = text_width + MARGIN_X + block_width: float = max(text_width + MARGIN_X, BLOCK_MIN_WIDTH) text_height: float = TEXT_SIZE * TEXT_SIZE_TO_HEIGHT_RATIO * len(text) block_height: float = text_height + MARGIN_Y - block = { - "id": block_id_generator(), - "title": "_", - "block_type": BLOCK_TYPE_TO_NAME[block_type], - "width": block_width, - "height": block_height, - "position": [ - next_block_x_pos, - next_block_y_pos, - ], - "splitter_pos": [ - 85, - 261, - ], - "sockets": [], - "metadata": { - "title_metadata": { - "color": "white", - "font": "Ubuntu", - "size": 12, - "padding": 4.0, - } - }, - } + block = get_default_block() + + block.update( + { + "block_type": BLOCK_TYPE_TO_NAME[block_type], + "width": block_width, + "height": block_height, + "position": [ + next_block_x_pos, + next_block_y_pos, + ], + } + ) if block_type == "code": block.update( @@ -101,4 +93,43 @@ def get_blocks(data: OrderedDict) -> List[OrderedDict]: blocks.append(block) + adujst_markdown_blocks_width(blocks) + return blocks + + +def get_default_block() -> OrderedDict: + """Return a default block with argument that vary missing""" + return { + "id": block_id_generator(), + "title": "_", + "splitter_pos": [ + 85, + 261, + ], + "sockets": [], + "metadata": { + "title_metadata": { + "color": "white", + "font": "Ubuntu", + "size": 12, + "padding": 4.0, + } + }, + } + + +def adujst_markdown_blocks_width(blocks: OrderedDict) -> None: + """Modify the markdown blocks width for them to match the width of block of code below""" + i: int = len(blocks) - 1 + + while i >= 0: + if blocks[i]["block_type"] == BLOCK_TYPE_TO_NAME["code"]: + block_width = blocks[i]["width"] + i -= 1 + + while i >= 0 and blocks[i]["block_type"] == BLOCK_TYPE_TO_NAME["markdown"]: + blocks[i]["width"] = block_width + i -= 1 + else: + i -= 1 From ecef7f362e1ef945a0a680cae1980b8a526bf4de Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Fri, 3 Dec 2021 22:36:56 +0100 Subject: [PATCH 08/27] Add a title when a small markdown is given --- opencodeblocks/scene/ipynb_conversion.py | 27 ++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/opencodeblocks/scene/ipynb_conversion.py b/opencodeblocks/scene/ipynb_conversion.py index ee8e87fc..e172d77c 100644 --- a/opencodeblocks/scene/ipynb_conversion.py +++ b/opencodeblocks/scene/ipynb_conversion.py @@ -4,7 +4,8 @@ MARGIN_X: float = 50 MARGIN_Y: float = 50 -BLOCK_MIN_WIDTH = 400 +BLOCK_MIN_WIDTH: float = 400 +TITLE_MAX_LENGTH: int = 60 TEXT_SIZE: float = 12 TEXT_SIZE_TO_WIDTH_RATIO: float = 0.7 TEXT_SIZE_TO_HEIGHT_RATIO: float = 1.42 @@ -83,6 +84,13 @@ def get_blocks(data: OrderedDict) -> List[OrderedDict]: ) next_block_y_pos = 0 next_block_x_pos += block_width + + if len(blocks) > 0 and is_title(blocks[-1]): + block_title: OrderedDict = blocks.pop() + block["title"] = block_title["text"] + + # Revert position effect of the markdown block + block["position"] = block_title["position"] elif block_type == "markdown": block.update( { @@ -119,8 +127,23 @@ def get_default_block() -> OrderedDict: } +def is_title(block: OrderedDict) -> bool: + """Checks if the block is a one-line markdown block which could correspond to a title""" + + if block["block_type"] != BLOCK_TYPE_TO_NAME["markdown"]: + return False + if "\n" in block["text"]: + return False + if len(block["text"]) == 0 or len(block["text"]) > TITLE_MAX_LENGTH: + return False + # Headings, quotes, bold or italic text are not considered to be headings + if block["text"][0] in {"#", "*", "`"}: + return False + return True + + def adujst_markdown_blocks_width(blocks: OrderedDict) -> None: - """Modify the markdown blocks width for them to match the width of block of code below""" + """Modify the markdown blocks width (in place) for them to match the width of block of code below""" i: int = len(blocks) - 1 while i >= 0: From 2097422a3d75f7e5c2cbecc67256a40db9f0248f Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Fri, 3 Dec 2021 23:08:40 +0100 Subject: [PATCH 09/27] Add sockets to converter --- opencodeblocks/scene/ipynb_conversion.py | 105 ++++++++++++++++++++--- 1 file changed, 94 insertions(+), 11 deletions(-) diff --git a/opencodeblocks/scene/ipynb_conversion.py b/opencodeblocks/scene/ipynb_conversion.py index e172d77c..dd1af912 100644 --- a/opencodeblocks/scene/ipynb_conversion.py +++ b/opencodeblocks/scene/ipynb_conversion.py @@ -1,16 +1,17 @@ """ Module for converting ipynb data to ipyg data """ -from typing import OrderedDict, List, Dict +from typing import Generator, OrderedDict, List, Dict MARGIN_X: float = 50 +MARGIN_BETWEEN_BLOCKS_X: float = 50 MARGIN_Y: float = 50 +MARGIN_BETWEEN_BLOCKS_Y: float = 5 BLOCK_MIN_WIDTH: float = 400 TITLE_MAX_LENGTH: int = 60 +SOCKET_HEIGHT: float = 44.0 TEXT_SIZE: float = 12 TEXT_SIZE_TO_WIDTH_RATIO: float = 0.7 TEXT_SIZE_TO_HEIGHT_RATIO: float = 1.42 -ipyg_id_generator = lambda: 0 -block_id_generator = lambda: 0 BLOCK_TYPE_TO_NAME: Dict[str, str] = { "code": "OCBCodeBlock", @@ -22,11 +23,12 @@ def ipynb_to_ipyg(data: OrderedDict) -> OrderedDict: """Convert ipynb data (ipynb file, as ordered dict) into ipyg data (ipyg, as ordered dict)""" blocks: List[OrderedDict] = get_blocks(data) + edges: List[OrderedDict] = add_sockets(blocks) return { - "id": ipyg_id_generator(), + "id": 0, "blocks": blocks, - "edges": [], + "edges": edges, } @@ -48,9 +50,9 @@ def get_blocks(data: OrderedDict) -> List[OrderedDict]: if "cell_type" not in cell or cell["cell_type"] not in ["code", "markdown"]: pass else: - block_type = cell["cell_type"] + block_type: str = cell["cell_type"] - text = cell["source"] + text: str = cell["source"] text_width: float = ( TEXT_SIZE * TEXT_SIZE_TO_WIDTH_RATIO * max(len(line) for line in text) @@ -65,6 +67,7 @@ def get_blocks(data: OrderedDict) -> List[OrderedDict]: block.update( { + "id": len(blocks), "block_type": BLOCK_TYPE_TO_NAME[block_type], "width": block_width, "height": block_height, @@ -83,7 +86,7 @@ def get_blocks(data: OrderedDict) -> List[OrderedDict]: } ) next_block_y_pos = 0 - next_block_x_pos += block_width + next_block_x_pos += block_width + MARGIN_BETWEEN_BLOCKS_X if len(blocks) > 0 and is_title(blocks[-1]): block_title: OrderedDict = blocks.pop() @@ -97,7 +100,7 @@ def get_blocks(data: OrderedDict) -> List[OrderedDict]: "text": "".join(text), } ) - next_block_y_pos += block_height + next_block_y_pos += block_height + MARGIN_BETWEEN_BLOCKS_Y blocks.append(block) @@ -109,7 +112,6 @@ def get_blocks(data: OrderedDict) -> List[OrderedDict]: def get_default_block() -> OrderedDict: """Return a default block with argument that vary missing""" return { - "id": block_id_generator(), "title": "_", "splitter_pos": [ 85, @@ -148,7 +150,7 @@ def adujst_markdown_blocks_width(blocks: OrderedDict) -> None: while i >= 0: if blocks[i]["block_type"] == BLOCK_TYPE_TO_NAME["code"]: - block_width = blocks[i]["width"] + block_width: float = blocks[i]["width"] i -= 1 while i >= 0 and blocks[i]["block_type"] == BLOCK_TYPE_TO_NAME["markdown"]: @@ -156,3 +158,84 @@ def adujst_markdown_blocks_width(blocks: OrderedDict) -> None: i -= 1 else: i -= 1 + + +def add_sockets(blocks: OrderedDict) -> OrderedDict: + """Add sockets to the blocks (in place) and returns the edge list""" + code_blocks: List[OrderedDict] = [ + block for block in blocks if block["block_type"] == BLOCK_TYPE_TO_NAME["code"] + ] + edges: List[OrderedDict] = [] + + for i in range(1, len(code_blocks)): + socket_id_out = len(blocks) + 2 * i + socket_id_in = len(blocks) + 2 * i + 1 + code_blocks[i - 1]["sockets"].append( + get_default_output_socket(socket_id_out, code_blocks[i - 1]["width"]) + ) + code_blocks[i]["sockets"].append(get_default_input_socket(socket_id_in)) + edges.append( + get_default_edge( + i, + code_blocks[i - 1]["id"], + socket_id_out, + code_blocks[i]["id"], + socket_id_in, + ) + ) + return edges + + +def get_default_input_socket(socket_id: int) -> OrderedDict: + """Returns the default input socket with the corresponding id""" + return { + "id": socket_id, + "type": "input", + "position": [0.0, SOCKET_HEIGHT], + "metadata": { + "color": "#FF55FFF0", + "linecolor": "#FF000000", + "linewidth": 1.0, + "radius": 10.0, + }, + } + + +def get_default_output_socket(socket_id: int, block_width: int) -> OrderedDict: + """ + Returns the default input socket with the corresponding id + and at the correct relative position with respect to the block + """ + return { + "id": socket_id, + "type": "output", + "position": [block_width, SOCKET_HEIGHT], + "metadata": { + "color": "#FF55FFF0", + "linecolor": "#FF000000", + "linewidth": 1.0, + "radius": 10.0, + }, + } + + +def get_default_edge( + edge_id: int, + edge_start_block_id: int, + edge_start_socket_id: int, + edge_end_block_id: int, + edge_end_socket_id: int, +) -> OrderedDict: + return { + "id": edge_id, + "path_type": "bezier", + "source": {"block": edge_start_block_id, "socket": edge_start_socket_id}, + "destination": {"block": edge_end_block_id, "socket": edge_end_socket_id}, + } + + +def get_integers_generator() -> Generator[int, None, None]: + n = 0 + while True: + yield n + n += 1 From 6d5e24d6fcd1fb8878270bf802b2fecdda8159d4 Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Mon, 6 Dec 2021 10:15:21 +0100 Subject: [PATCH 10/27] Change file name --- .../scene/{ipynb_conversion.py => from_ipynb_conversion.py} | 0 opencodeblocks/scene/scene.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename opencodeblocks/scene/{ipynb_conversion.py => from_ipynb_conversion.py} (100%) diff --git a/opencodeblocks/scene/ipynb_conversion.py b/opencodeblocks/scene/from_ipynb_conversion.py similarity index 100% rename from opencodeblocks/scene/ipynb_conversion.py rename to opencodeblocks/scene/from_ipynb_conversion.py diff --git a/opencodeblocks/scene/scene.py b/opencodeblocks/scene/scene.py index 63b969d1..e8ad4620 100644 --- a/opencodeblocks/scene/scene.py +++ b/opencodeblocks/scene/scene.py @@ -19,7 +19,7 @@ from opencodeblocks.graphics.edge import OCBEdge from opencodeblocks.scene.clipboard import SceneClipboard from opencodeblocks.scene.history import SceneHistory -from opencodeblocks.scene.ipynb_conversion import ipynb_to_ipyg +from opencodeblocks.scene.from_ipynb_conversion import ipynb_to_ipyg class OCBScene(QGraphicsScene, Serializable): From 3e18fd0b0b31d68bfdca925f07cb224b3bb9ec5d Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Mon, 6 Dec 2021 16:50:58 +0100 Subject: [PATCH 11/27] Add skeleton for save as ipynb --- opencodeblocks/graphics/widget.py | 3 + opencodeblocks/graphics/window.py | 307 +++++++++++++------- opencodeblocks/scene/scene.py | 17 ++ opencodeblocks/scene/to_ipynb_conversion.py | 7 + 4 files changed, 233 insertions(+), 101 deletions(-) create mode 100644 opencodeblocks/scene/to_ipynb_conversion.py diff --git a/opencodeblocks/graphics/widget.py b/opencodeblocks/graphics/widget.py index 8d79f871..beb45576 100644 --- a/opencodeblocks/graphics/widget.py +++ b/opencodeblocks/graphics/widget.py @@ -61,6 +61,9 @@ def savepath(self, value: str): def save(self): self.scene.save(self.savepath) + def saveAsJupyter(self): + self.scene.save_to_ipynb(self.savepath) + def load(self, filepath: str): self.scene.load(filepath) self.savepath = filepath diff --git a/opencodeblocks/graphics/window.py b/opencodeblocks/graphics/window.py index 6823f3fa..1005e0d3 100644 --- a/opencodeblocks/graphics/window.py +++ b/opencodeblocks/graphics/window.py @@ -8,8 +8,16 @@ from PyQt5.QtCore import QPoint, QSettings, QSize, Qt, QSignalMapper from PyQt5.QtGui import QCloseEvent, QKeySequence -from PyQt5.QtWidgets import QDockWidget, QListWidget, QWidget, QAction, QFileDialog, QMainWindow,\ - QMessageBox, QMdiArea +from PyQt5.QtWidgets import ( + QDockWidget, + QListWidget, + QWidget, + QAction, + QFileDialog, + QMainWindow, + QMessageBox, + QMdiArea, +) from opencodeblocks.graphics.widget import OCBWidget from opencodeblocks.graphics.theme_manager import theme_manager @@ -19,23 +27,24 @@ class OCBWindow(QMainWindow): - """ Main window of the OpenCodeBlocks Qt-based application. """ + """Main window of the OpenCodeBlocks Qt-based application.""" def __init__(self): super().__init__() self.stylesheet_filename = os.path.join( - os.path.dirname(__file__),'..', 'qss', 'ocb.qss') - loadStylesheets(( - os.path.join(os.path.dirname(__file__),'..', 'qss', 'ocb_dark.qss'), - self.stylesheet_filename - )) + os.path.dirname(__file__), "..", "qss", "ocb.qss" + ) + loadStylesheets( + ( + os.path.join(os.path.dirname(__file__), "..", "qss", "ocb_dark.qss"), + self.stylesheet_filename, + ) + ) self.mdiArea = QMdiArea() - self.mdiArea.setHorizontalScrollBarPolicy( - Qt.ScrollBarPolicy.ScrollBarAsNeeded) - self.mdiArea.setVerticalScrollBarPolicy( - Qt.ScrollBarPolicy.ScrollBarAsNeeded) + self.mdiArea.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) + self.mdiArea.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) self.mdiArea.setViewMode(QMdiArea.ViewMode.TabbedView) self.mdiArea.setDocumentMode(True) self.mdiArea.setTabsMovable(True) @@ -91,72 +100,145 @@ def updateMenus(self): pass def createActions(self): - """ Create all menu actions. """ + """Create all menu actions.""" # File - self._actNew = QAction('&New', statusTip='Create new ipygraph', - shortcut='Ctrl+N', triggered=self.onFileNew) - self._actOpen = QAction('&Open', statusTip='Open an ipygraph', - shortcut='Ctrl+O', triggered=self.onFileOpen) - self._actSave = QAction('&Save', statusTip='Save the ipygraph', - shortcut='Ctrl+S', triggered=self.onFileSave) - self._actSaveAs = QAction('Save &As...', statusTip='Save the ipygraph as...', - shortcut='Ctrl+Shift+S', triggered=self.onFileSaveAs) - self._actQuit = QAction('&Quit', statusTip='Save and Quit the application', - shortcut='Ctrl+Q', triggered=self.close) + self._actNew = QAction( + "&New", + statusTip="Create new ipygraph", + shortcut="Ctrl+N", + triggered=self.onFileNew, + ) + self._actOpen = QAction( + "&Open", + statusTip="Open an ipygraph", + shortcut="Ctrl+O", + triggered=self.onFileOpen, + ) + self._actSave = QAction( + "&Save", + statusTip="Save the ipygraph", + shortcut="Ctrl+S", + triggered=self.onFileSave, + ) + self._actSaveAs = QAction( + "Save &As...", + statusTip="Save the ipygraph as...", + shortcut="Ctrl+Shift+S", + triggered=self.onFileSaveAs, + ) + self._actSaveAsJupyter = QAction( + "Save &As ... .ipynb", + statusTip="Save the ipygraph as a Jupter Notebook at ...", + triggered=self.oneFileSaveAsJupyter, + ) + self._actQuit = QAction( + "&Quit", + statusTip="Save and Quit the application", + shortcut="Ctrl+Q", + triggered=self.close, + ) # Edit - self._actUndo = QAction('&Undo', statusTip='Undo last operation', - shortcut='Ctrl+Z', triggered=self.onEditUndo) - self._actRedo = QAction('&Redo', statusTip='Redo last operation', - shortcut='Ctrl+Y', triggered=self.onEditRedo) - self._actCut = QAction('Cu&t', statusTip='Cut to clipboard', - shortcut='Ctrl+X', triggered=self.onEditCut) - self._actCopy = QAction('&Copy', statusTip='Copy to clipboard', - shortcut='Ctrl+C', triggered=self.onEditCopy) - self._actPaste = QAction('&Paste', statusTip='Paste from clipboard', - shortcut='Ctrl+V', triggered=self.onEditPaste) - self._actDel = QAction('&Del', statusTip='Delete selected items', - shortcut='Del', triggered=self.onEditDelete) + self._actUndo = QAction( + "&Undo", + statusTip="Undo last operation", + shortcut="Ctrl+Z", + triggered=self.onEditUndo, + ) + self._actRedo = QAction( + "&Redo", + statusTip="Redo last operation", + shortcut="Ctrl+Y", + triggered=self.onEditRedo, + ) + self._actCut = QAction( + "Cu&t", + statusTip="Cut to clipboard", + shortcut="Ctrl+X", + triggered=self.onEditCut, + ) + self._actCopy = QAction( + "&Copy", + statusTip="Copy to clipboard", + shortcut="Ctrl+C", + triggered=self.onEditCopy, + ) + self._actPaste = QAction( + "&Paste", + statusTip="Paste from clipboard", + shortcut="Ctrl+V", + triggered=self.onEditPaste, + ) + self._actDel = QAction( + "&Del", + statusTip="Delete selected items", + shortcut="Del", + triggered=self.onEditDelete, + ) # View - self._actGlobal = QAction('Global View', statusTip='View the hole graph', - shortcut=' ', triggered=self.onViewGlobal) + self._actGlobal = QAction( + "Global View", + statusTip="View the hole graph", + shortcut=" ", + triggered=self.onViewGlobal, + ) # Window - self._actClose = QAction("Cl&ose", self, - statusTip="Close the active window", - triggered=self.mdiArea.closeActiveSubWindow) - self._actCloseAll = QAction("Close &All", self, - statusTip="Close all the windows", - triggered=self.mdiArea.closeAllSubWindows) - self._actTile = QAction("&Tile", self, statusTip="Tile the windows", - triggered=self.mdiArea.tileSubWindows) - self._actCascade = QAction("&Cascade", self, - statusTip="Cascade the windows", - triggered=self.mdiArea.cascadeSubWindows) - self._actNext = QAction("Ne&xt", self, - shortcut=QKeySequence.StandardKey.NextChild, - statusTip="Move the focus to the next window", - triggered=self.mdiArea.activateNextSubWindow) - self._actPrevious = QAction("Pre&vious", self, - shortcut=QKeySequence.StandardKey.PreviousChild, - statusTip="Move the focus to the previous window", - triggered=self.mdiArea.activatePreviousSubWindow) + self._actClose = QAction( + "Cl&ose", + self, + statusTip="Close the active window", + triggered=self.mdiArea.closeActiveSubWindow, + ) + self._actCloseAll = QAction( + "Close &All", + self, + statusTip="Close all the windows", + triggered=self.mdiArea.closeAllSubWindows, + ) + self._actTile = QAction( + "&Tile", + self, + statusTip="Tile the windows", + triggered=self.mdiArea.tileSubWindows, + ) + self._actCascade = QAction( + "&Cascade", + self, + statusTip="Cascade the windows", + triggered=self.mdiArea.cascadeSubWindows, + ) + self._actNext = QAction( + "Ne&xt", + self, + shortcut=QKeySequence.StandardKey.NextChild, + statusTip="Move the focus to the next window", + triggered=self.mdiArea.activateNextSubWindow, + ) + self._actPrevious = QAction( + "Pre&vious", + self, + shortcut=QKeySequence.StandardKey.PreviousChild, + statusTip="Move the focus to the previous window", + triggered=self.mdiArea.activatePreviousSubWindow, + ) self._actSeparator = QAction(self) self._actSeparator.setSeparator(True) def createMenus(self): - """ Create the File menu with linked shortcuts. """ - self.filemenu = self.menuBar().addMenu('&File') + """Create the File menu with linked shortcuts.""" + self.filemenu = self.menuBar().addMenu("&File") self.filemenu.addAction(self._actNew) self.filemenu.addAction(self._actOpen) self.filemenu.addSeparator() self.filemenu.addAction(self._actSave) self.filemenu.addAction(self._actSaveAs) + self.filemenu.addAction(self._actSaveAsJupyter) self.filemenu.addSeparator() self.filemenu.addAction(self._actQuit) - self.editmenu = self.menuBar().addMenu('&Edit') + self.editmenu = self.menuBar().addMenu("&Edit") self.editmenu.addAction(self._actUndo) self.editmenu.addAction(self._actRedo) self.editmenu.addSeparator() @@ -166,8 +248,8 @@ def createMenus(self): self.editmenu.addSeparator() self.editmenu.addAction(self._actDel) - self.viewmenu = self.menuBar().addMenu('&View') - self.thememenu = self.viewmenu.addMenu('Theme') + self.viewmenu = self.menuBar().addMenu("&View") + self.thememenu = self.viewmenu.addMenu("Theme") self.thememenu.aboutToShow.connect(self.updateThemeMenu) self.viewmenu.addAction(self._actGlobal) @@ -207,7 +289,7 @@ def updateWindowMenu(self): text = f"{i + 1} {child.windowTitle()}" if i < 9: - text = '&' + text + text = "&" + text action = self.windowMenu.addAction(text) action.setCheckable(True) @@ -216,7 +298,7 @@ def updateWindowMenu(self): self.windowMapper.setMapping(action, window) def createNewMdiChild(self, filename: str = None): - """ Create a new graph subwindow loading a file if a path is given. """ + """Create a new graph subwindow loading a file if a path is given.""" ocb_widget = OCBWidget() if filename is not None: ocb_widget.scene.load(filename) @@ -224,15 +306,14 @@ def createNewMdiChild(self, filename: str = None): return self.mdiArea.addSubWindow(ocb_widget) def onFileNew(self): - """ Create a new file. """ + """Create a new file.""" subwnd = self.createNewMdiChild() subwnd.show() def onFileOpen(self): - """ Open a file. """ - filename, _ = QFileDialog.getOpenFileName( - self, 'Open ipygraph from file') - if filename == '': + """Open a file.""" + filename, _ = QFileDialog.getOpenFileName(self, "Open ipygraph from file") + if filename == "": return if os.path.isfile(filename): subwnd = self.createNewMdiChild(filename) @@ -240,7 +321,7 @@ def onFileOpen(self): self.statusbar.showMessage(f"Successfully loaded {filename}", 2000) def onFileSave(self) -> bool: - """ Save file. + """Save file. Returns: True if the file was successfully saved, False otherwise. @@ -252,11 +333,12 @@ def onFileSave(self) -> bool: return self.onFileSaveAs() current_window.save() self.statusbar.showMessage( - f"Successfully saved ipygraph at {current_window.savepath}", 2000) + f"Successfully saved ipygraph at {current_window.savepath}", 2000 + ) return True def onFileSaveAs(self) -> bool: - """ Save file in a given directory, caching savepath for quick save. + """Save file in a given directory, caching savepath for quick save. Returns: True if the file was successfully saved, False otherwise. @@ -264,52 +346,74 @@ def onFileSaveAs(self) -> bool: """ current_window = self.activeMdiChild() if current_window is not None: - filename, _ = QFileDialog.getSaveFileName( - self, 'Save ipygraph to file') - if filename == '': + filename, _ = QFileDialog.getSaveFileName(self, "Save ipygraph to file") + if filename == "": return False current_window.savepath = filename self.onFileSave() return True return False + def oneFileSaveAsJupyter(self) -> bool: + """Save file in a given directory as ipynb, caching savepath for quick save. + + Returns: + True if the file was successfully saved, False otherwise. + + """ + current_window = self.activeMdiChild() + if current_window is not None: + filename, _ = QFileDialog.getSaveFileName( + self, "Save ipygraph to ipynb file" + ) + if filename == "": + return False + current_window.savepath = filename + current_window.saveAsJupyter() + self.statusbar.showMessage( + f"Successfully saved ipygraph as jupter notebook at {current_window.savepath}", + 2000, + ) + return True + return False + @staticmethod def is_not_editing(current_window: OCBWidget): - """ True if current_window exists and is not in editing mode. """ - return current_window is not None and not current_window.view.is_mode('EDITING') + """True if current_window exists and is not in editing mode.""" + return current_window is not None and not current_window.view.is_mode("EDITING") def onEditUndo(self): - """ Undo last operation if not in edit mode. """ + """Undo last operation if not in edit mode.""" current_window = self.activeMdiChild() if self.is_not_editing(current_window): current_window.scene.history.undo() def onEditRedo(self): - """ Redo last operation if not in edit mode. """ + """Redo last operation if not in edit mode.""" current_window = self.activeMdiChild() if self.is_not_editing(current_window): current_window.scene.history.redo() def onEditCut(self): - """ Cut the selected items if not in edit mode. """ + """Cut the selected items if not in edit mode.""" current_window = self.activeMdiChild() if self.is_not_editing(current_window): current_window.scene.clipboard.cut() def onEditCopy(self): - """ Copy the selected items if not in edit mode. """ + """Copy the selected items if not in edit mode.""" current_window = self.activeMdiChild() if self.is_not_editing(current_window): current_window.scene.clipboard.copy() def onEditPaste(self): - """ Paste the selected items if not in edit mode. """ + """Paste the selected items if not in edit mode.""" current_window = self.activeMdiChild() if self.is_not_editing(current_window): current_window.scene.clipboard.paste() def onEditDelete(self): - """ Delete the selected items if not in edit mode. """ + """Delete the selected items if not in edit mode.""" current_window = self.activeMdiChild() if self.is_not_editing(current_window): current_window.view.deleteSelected() @@ -322,7 +426,7 @@ def onEditDelete(self): # event.ignore() def closeEvent(self, event: QCloseEvent): - """ Save and quit the application. """ + """Save and quit the application.""" self.mdiArea.closeAllSubWindows() if self.mdiArea.currentSubWindow(): event.ignore() @@ -331,7 +435,7 @@ def closeEvent(self, event: QCloseEvent): event.accept() def maybeSave(self) -> bool: - """ Ask for save and returns if the file should be closed. + """Ask for save and returns if the file should be closed. Returns: True if the file should be closed, False otherwise. @@ -340,13 +444,14 @@ def maybeSave(self) -> bool: if not self.isModified(): return True - answer = QMessageBox.warning(self, "About to loose you work?", - "The file has been modified.\n" - "Do you want to save your changes?", - QMessageBox.StandardButton.Save | - QMessageBox.StandardButton.Discard | - QMessageBox.StandardButton.Cancel - ) + answer = QMessageBox.warning( + self, + "About to loose you work?", + "The file has been modified.\n" "Do you want to save your changes?", + QMessageBox.StandardButton.Save + | QMessageBox.StandardButton.Discard + | QMessageBox.StandardButton.Cancel, + ) if answer == QMessageBox.StandardButton.Save: return self.onFileSave() @@ -355,33 +460,33 @@ def maybeSave(self) -> bool: return False def activeMdiChild(self) -> OCBWidget: - """ Get the active OCBWidget if existing. """ + """Get the active OCBWidget if existing.""" activeSubWindow = self.mdiArea.activeSubWindow() if activeSubWindow is not None: return activeSubWindow.widget() return None def readSettings(self): - settings = QSettings('AutopIA', 'OpenCodeBlocks') - pos = settings.value('pos', QPoint(200, 200)) - size = settings.value('size', QSize(400, 400)) + settings = QSettings("AutopIA", "OpenCodeBlocks") + pos = settings.value("pos", QPoint(200, 200)) + size = settings.value("size", QSize(400, 400)) self.move(pos) self.resize(size) - if settings.value('isMaximized', False) == 'true': + if settings.value("isMaximized", False) == "true": self.showMaximized() def writeSettings(self): - settings = QSettings('AutopIA', 'OpenCodeBlocks') - settings.setValue('pos', self.pos()) - settings.setValue('size', self.size()) - settings.setValue('isMaximized', self.isMaximized()) + settings = QSettings("AutopIA", "OpenCodeBlocks") + settings.setValue("pos", self.pos()) + settings.setValue("size", self.size()) + settings.setValue("isMaximized", self.isMaximized()) def setActiveSubWindow(self, window): if window: self.mdiArea.setActiveSubWindow(window) def onViewGlobal(self): - """ Center the view to see the hole graph """ + """Center the view to see the hole graph""" current_window = self.activeMdiChild() if current_window is not None and isinstance(current_window, OCBWidget): current_window.moveToGlobalView() diff --git a/opencodeblocks/scene/scene.py b/opencodeblocks/scene/scene.py index e8ad4620..cdaf0027 100644 --- a/opencodeblocks/scene/scene.py +++ b/opencodeblocks/scene/scene.py @@ -20,6 +20,7 @@ from opencodeblocks.scene.clipboard import SceneClipboard from opencodeblocks.scene.history import SceneHistory from opencodeblocks.scene.from_ipynb_conversion import ipynb_to_ipyg +from opencodeblocks.scene.to_ipynb_conversion import ipyg_to_ipynb class OCBScene(QGraphicsScene, Serializable): @@ -138,6 +139,22 @@ def save_to_ipyg(self, filepath: str): with open(filepath, "w", encoding="utf-8") as file: file.write(json.dumps(self.serialize(), indent=4)) + def save_to_ipynb(self, filepath: str): + """Save the scene into filepath as ipynb""" + if "." not in filepath: + filepath += ".ipynb" + + extention_format: str = filepath.split(".")[-1] + if extention_format != "ipynb": + raise NotImplementedError( + f"The file should be a *.ipynb (not a .{extention_format})" + ) + + with open(filepath, "w", encoding="utf-8") as file: + json_ipyg_data: OrderedDict = self.serialize() + json_ipynb_data: OrderedDict = ipyg_to_ipynb(json_ipyg_data) + file.write(json.dumps(json_ipynb_data, indent=4)) + def load(self, filepath: str): """Load a saved scene. diff --git a/opencodeblocks/scene/to_ipynb_conversion.py b/opencodeblocks/scene/to_ipynb_conversion.py new file mode 100644 index 00000000..cf18f038 --- /dev/null +++ b/opencodeblocks/scene/to_ipynb_conversion.py @@ -0,0 +1,7 @@ +""" Module for converting ipyg data to ipynb data """ + +from typing import Generator, OrderedDict, List, Dict + + +def ipyg_to_ipynb(data: OrderedDict) -> OrderedDict: + return {} From 15d5c61b42483880899b353a30f11fcc8e22df36 Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Mon, 6 Dec 2021 17:02:20 +0100 Subject: [PATCH 12/27] Rename functions and variables for concistency --- opencodeblocks/scene/from_ipynb_conversion.py | 77 ++++++++++--------- opencodeblocks/scene/scene.py | 2 +- 2 files changed, 42 insertions(+), 37 deletions(-) diff --git a/opencodeblocks/scene/from_ipynb_conversion.py b/opencodeblocks/scene/from_ipynb_conversion.py index dd1af912..60082158 100644 --- a/opencodeblocks/scene/from_ipynb_conversion.py +++ b/opencodeblocks/scene/from_ipynb_conversion.py @@ -22,17 +22,17 @@ def ipynb_to_ipyg(data: OrderedDict) -> OrderedDict: """Convert ipynb data (ipynb file, as ordered dict) into ipyg data (ipyg, as ordered dict)""" - blocks: List[OrderedDict] = get_blocks(data) - edges: List[OrderedDict] = add_sockets(blocks) + blocks_data: List[OrderedDict] = get_blocks_data(data) + edges_data: List[OrderedDict] = get_sockets_data(blocks_data) return { "id": 0, - "blocks": blocks, - "edges": edges, + "blocks": blocks_data, + "edges": edges_data, } -def get_blocks(data: OrderedDict) -> List[OrderedDict]: +def get_blocks_data(data: OrderedDict) -> List[OrderedDict]: """ Get the blocks corresponding to a ipynb file, Returns them in the ipyg ordered dict format @@ -41,7 +41,7 @@ def get_blocks(data: OrderedDict) -> List[OrderedDict]: if "cells" not in data: return [] - blocks: List[OrderedDict] = [] + blocks_data: List[OrderedDict] = [] next_block_x_pos: float = 0 next_block_y_pos: float = 0 @@ -63,11 +63,11 @@ def get_blocks(data: OrderedDict) -> List[OrderedDict]: text_height: float = TEXT_SIZE * TEXT_SIZE_TO_HEIGHT_RATIO * len(text) block_height: float = text_height + MARGIN_Y - block = get_default_block() + block_data = get_default_block() - block.update( + block_data.update( { - "id": len(blocks), + "id": len(blocks_data), "block_type": BLOCK_TYPE_TO_NAME[block_type], "width": block_width, "height": block_height, @@ -79,7 +79,7 @@ def get_blocks(data: OrderedDict) -> List[OrderedDict]: ) if block_type == "code": - block.update( + block_data.update( { "source": "".join(text), "stdout": "", @@ -88,25 +88,25 @@ def get_blocks(data: OrderedDict) -> List[OrderedDict]: next_block_y_pos = 0 next_block_x_pos += block_width + MARGIN_BETWEEN_BLOCKS_X - if len(blocks) > 0 and is_title(blocks[-1]): - block_title: OrderedDict = blocks.pop() - block["title"] = block_title["text"] + if len(blocks_data) > 0 and is_title(blocks_data[-1]): + block_title: OrderedDict = blocks_data.pop() + block_data["title"] = block_title["text"] # Revert position effect of the markdown block - block["position"] = block_title["position"] + block_data["position"] = block_title["position"] elif block_type == "markdown": - block.update( + block_data.update( { "text": "".join(text), } ) next_block_y_pos += block_height + MARGIN_BETWEEN_BLOCKS_Y - blocks.append(block) + blocks_data.append(block_data) - adujst_markdown_blocks_width(blocks) + adujst_markdown_blocks_width(blocks_data) - return blocks + return blocks_data def get_default_block() -> OrderedDict: @@ -129,52 +129,57 @@ def get_default_block() -> OrderedDict: } -def is_title(block: OrderedDict) -> bool: +def is_title(block_data: OrderedDict) -> bool: """Checks if the block is a one-line markdown block which could correspond to a title""" - if block["block_type"] != BLOCK_TYPE_TO_NAME["markdown"]: + if block_data["block_type"] != BLOCK_TYPE_TO_NAME["markdown"]: return False - if "\n" in block["text"]: + if "\n" in block_data["text"]: return False - if len(block["text"]) == 0 or len(block["text"]) > TITLE_MAX_LENGTH: + if len(block_data["text"]) == 0 or len(block_data["text"]) > TITLE_MAX_LENGTH: return False # Headings, quotes, bold or italic text are not considered to be headings - if block["text"][0] in {"#", "*", "`"}: + if block_data["text"][0] in {"#", "*", "`"}: return False return True -def adujst_markdown_blocks_width(blocks: OrderedDict) -> None: +def adujst_markdown_blocks_width(blocks_data: OrderedDict) -> None: """Modify the markdown blocks width (in place) for them to match the width of block of code below""" - i: int = len(blocks) - 1 + i: int = len(blocks_data) - 1 while i >= 0: - if blocks[i]["block_type"] == BLOCK_TYPE_TO_NAME["code"]: - block_width: float = blocks[i]["width"] + if blocks_data[i]["block_type"] == BLOCK_TYPE_TO_NAME["code"]: + block_width: float = blocks_data[i]["width"] i -= 1 - while i >= 0 and blocks[i]["block_type"] == BLOCK_TYPE_TO_NAME["markdown"]: - blocks[i]["width"] = block_width + while ( + i >= 0 + and blocks_data[i]["block_type"] == BLOCK_TYPE_TO_NAME["markdown"] + ): + blocks_data[i]["width"] = block_width i -= 1 else: i -= 1 -def add_sockets(blocks: OrderedDict) -> OrderedDict: +def get_sockets_data(blocks_data: OrderedDict) -> OrderedDict: """Add sockets to the blocks (in place) and returns the edge list""" code_blocks: List[OrderedDict] = [ - block for block in blocks if block["block_type"] == BLOCK_TYPE_TO_NAME["code"] + block + for block in blocks_data + if block["block_type"] == BLOCK_TYPE_TO_NAME["code"] ] - edges: List[OrderedDict] = [] + edges_data: List[OrderedDict] = [] for i in range(1, len(code_blocks)): - socket_id_out = len(blocks) + 2 * i - socket_id_in = len(blocks) + 2 * i + 1 + socket_id_out = len(blocks_data) + 2 * i + socket_id_in = len(blocks_data) + 2 * i + 1 code_blocks[i - 1]["sockets"].append( get_default_output_socket(socket_id_out, code_blocks[i - 1]["width"]) ) code_blocks[i]["sockets"].append(get_default_input_socket(socket_id_in)) - edges.append( + edges_data.append( get_default_edge( i, code_blocks[i - 1]["id"], @@ -183,7 +188,7 @@ def add_sockets(blocks: OrderedDict) -> OrderedDict: socket_id_in, ) ) - return edges + return edges_data def get_default_input_socket(socket_id: int) -> OrderedDict: diff --git a/opencodeblocks/scene/scene.py b/opencodeblocks/scene/scene.py index cdaf0027..500f81f5 100644 --- a/opencodeblocks/scene/scene.py +++ b/opencodeblocks/scene/scene.py @@ -175,7 +175,7 @@ def load(self, filepath: str): self.has_been_modified = False def load_from_json(self, filepath: str) -> OrderedDict: - """Load the ipynb json data into an ordered dict + """Load the json data into an ordered dict Args: filepath: Path to the file to load. From 486a1239d00865245ec42aa388d75015501af531 Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Mon, 6 Dec 2021 17:03:00 +0100 Subject: [PATCH 13/27] Remove unused function --- opencodeblocks/scene/from_ipynb_conversion.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/opencodeblocks/scene/from_ipynb_conversion.py b/opencodeblocks/scene/from_ipynb_conversion.py index 60082158..3fde5478 100644 --- a/opencodeblocks/scene/from_ipynb_conversion.py +++ b/opencodeblocks/scene/from_ipynb_conversion.py @@ -237,10 +237,3 @@ def get_default_edge( "source": {"block": edge_start_block_id, "socket": edge_start_socket_id}, "destination": {"block": edge_end_block_id, "socket": edge_end_socket_id}, } - - -def get_integers_generator() -> Generator[int, None, None]: - n = 0 - while True: - yield n - n += 1 From 53ffa0b8ed7bd8b886252ea8e6bc073477daed98 Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Mon, 6 Dec 2021 17:37:46 +0100 Subject: [PATCH 14/27] Add default for scene, edge and socket --- opencodeblocks/graphics/edge.py | 147 ++++++++++++++++++++---------- opencodeblocks/graphics/socket.py | 28 +++++- opencodeblocks/scene/scene.py | 2 +- 3 files changed, 126 insertions(+), 51 deletions(-) diff --git a/opencodeblocks/graphics/edge.py b/opencodeblocks/graphics/edge.py index e4b33722..2fa7599e 100644 --- a/opencodeblocks/graphics/edge.py +++ b/opencodeblocks/graphics/edge.py @@ -14,17 +14,26 @@ from opencodeblocks.core.serializable import Serializable from opencodeblocks.graphics.socket import OCBSocket +DEFAULT_EDGE_DATA = {"path_type": "bezier"} +NONE_OPTIONAL_FIELDS = {"source", "destination"} -class OCBEdge(QGraphicsPathItem, Serializable): - """ Base class for directed edges in OpenCodeBlocks. """ +class OCBEdge(QGraphicsPathItem, Serializable): - 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 +71,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 +110,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 +140,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 +152,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 +167,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,33 +178,72 @@ 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'] + if restore_id and "id" in data: + self.id = data["id"] + + self.complete_with_default(data) + + 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: self.remove() + + def complete_with_default(self, data: OrderedDict) -> None: + """Add default data in place when fields are missing""" + for key in NONE_OPTIONAL_FIELDS: + if key not in data: + raise ValueError(f"{key} of the socket is missing") + + for key in DEFAULT_EDGE_DATA.keys(): + if key not in data: + data[key] = DEFAULT_EDGE_DATA[key] diff --git a/opencodeblocks/graphics/socket.py b/opencodeblocks/graphics/socket.py index d567c54f..09f7eb83 100644 --- a/opencodeblocks/graphics/socket.py +++ b/opencodeblocks/graphics/socket.py @@ -17,6 +17,17 @@ from opencodeblocks.graphics.edge import OCBEdge from opencodeblocks.blocks.block import OCBBlock +DEFAULT_SOCKET_DATA = { + "type": "input", + "metadata": { + "color": "#FF55FFF0", + "linecolor": "#FF000000", + "linewidth": 1.0, + "radius": 6.0, + }, +} +NONE_OPTIONAL_FIELDS = {"position"} + class OCBSocket(QGraphicsItem, Serializable): @@ -133,13 +144,26 @@ def serialize(self) -> OrderedDict: ) def deserialize(self, data: OrderedDict, hashmap: dict = None, restore_id=True): - if restore_id: + if restore_id and "id" in data: self.id = data["id"] + + self.complete_with_default(data) + self.socket_type = data["type"] self.setPos(QPointF(*data["position"])) self.metadata = dict(data["metadata"]) - self.radius = self.metadata["radius"] self._pen.setColor(QColor(self.metadata["linecolor"])) self._pen.setWidth(int(self.metadata["linewidth"])) self._brush.setColor(QColor(self.metadata["color"])) + + def complete_with_default(self, data: OrderedDict) -> None: + """Add default data in place when fields are missing""" + for key in NONE_OPTIONAL_FIELDS: + if key not in data: + raise ValueError(f"{key} of the socket is missing") + + for key in DEFAULT_SOCKET_DATA.keys(): + if key not in data: + data[key] = DEFAULT_SOCKET_DATA[key] + diff --git a/opencodeblocks/scene/scene.py b/opencodeblocks/scene/scene.py index e61c2d8c..34adff0b 100644 --- a/opencodeblocks/scene/scene.py +++ b/opencodeblocks/scene/scene.py @@ -218,7 +218,7 @@ 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: + if restore_id and 'id' in data: self.id = data['id'] # Create blocks From 7d57f3af59f839899937cd4ffc09252ad152f6de Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Mon, 6 Dec 2021 17:45:30 +0100 Subject: [PATCH 15/27] Remove unecessary default data --- opencodeblocks/scene/from_ipynb_conversion.py | 30 +++++-------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/opencodeblocks/scene/from_ipynb_conversion.py b/opencodeblocks/scene/from_ipynb_conversion.py index 3fde5478..5ed4e887 100644 --- a/opencodeblocks/scene/from_ipynb_conversion.py +++ b/opencodeblocks/scene/from_ipynb_conversion.py @@ -26,7 +26,6 @@ def ipynb_to_ipyg(data: OrderedDict) -> OrderedDict: edges_data: List[OrderedDict] = get_sockets_data(blocks_data) return { - "id": 0, "blocks": blocks_data, "edges": edges_data, } @@ -176,11 +175,11 @@ def get_sockets_data(blocks_data: OrderedDict) -> OrderedDict: socket_id_out = len(blocks_data) + 2 * i socket_id_in = len(blocks_data) + 2 * i + 1 code_blocks[i - 1]["sockets"].append( - get_default_output_socket(socket_id_out, code_blocks[i - 1]["width"]) + get_output_socket_data(socket_id_out, code_blocks[i - 1]["width"]) ) - code_blocks[i]["sockets"].append(get_default_input_socket(socket_id_in)) + code_blocks[i]["sockets"].append(get_input_socket_data(socket_id_in)) edges_data.append( - get_default_edge( + get_edge_data( i, code_blocks[i - 1]["id"], socket_id_out, @@ -191,40 +190,28 @@ def get_sockets_data(blocks_data: OrderedDict) -> OrderedDict: return edges_data -def get_default_input_socket(socket_id: int) -> OrderedDict: - """Returns the default input socket with the corresponding id""" +def get_input_socket_data(socket_id: int) -> OrderedDict: + """Returns the input socket's data with the corresponding id""" return { "id": socket_id, "type": "input", "position": [0.0, SOCKET_HEIGHT], - "metadata": { - "color": "#FF55FFF0", - "linecolor": "#FF000000", - "linewidth": 1.0, - "radius": 10.0, - }, } -def get_default_output_socket(socket_id: int, block_width: int) -> OrderedDict: +def get_output_socket_data(socket_id: int, block_width: int) -> OrderedDict: """ - Returns the default input socket with the corresponding id + Returns the input socket's data with the corresponding id and at the correct relative position with respect to the block """ return { "id": socket_id, "type": "output", "position": [block_width, SOCKET_HEIGHT], - "metadata": { - "color": "#FF55FFF0", - "linecolor": "#FF000000", - "linewidth": 1.0, - "radius": 10.0, - }, } -def get_default_edge( +def get_edge_data( edge_id: int, edge_start_block_id: int, edge_start_socket_id: int, @@ -233,7 +220,6 @@ def get_default_edge( ) -> OrderedDict: return { "id": edge_id, - "path_type": "bezier", "source": {"block": edge_start_block_id, "socket": edge_start_socket_id}, "destination": {"block": edge_end_block_id, "socket": edge_end_socket_id}, } From fbae7156f5d1e88847c0df9f6de82fe7f1d04146 Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Mon, 6 Dec 2021 17:55:25 +0100 Subject: [PATCH 16/27] Add default for block and code block --- opencodeblocks/blocks/block.py | 25 ++++++++++++++++++++++++- opencodeblocks/blocks/codeblock.py | 16 ++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/opencodeblocks/blocks/block.py b/opencodeblocks/blocks/block.py index 754c330d..903c4454 100644 --- a/opencodeblocks/blocks/block.py +++ b/opencodeblocks/blocks/block.py @@ -25,6 +25,16 @@ BACKGROUND_COLOR = QColor("#E3212121") +DEFAULT_BLOCK_DATA = { + "title": "_", + "splitter_pos": [88, 41], + "width": 618, + "height": 184, + "metadata": {"title_metadata": {"color": "white", "font": "Ubuntu", "size": 10}}, + "sockets": {}, +} +NONE_OPTIONAL_FIELDS = {"block_type", "position"} + class OCBBlock(QGraphicsItem, Serializable): @@ -291,8 +301,11 @@ def serialize(self) -> OrderedDict: def deserialize(self, data: dict, hashmap: dict = None, restore_id=True) -> None: """Restore the block from serialized data""" - if restore_id: + if restore_id and "id" in data: self.id = data["id"] + + self.complete_with_default(data) + for dataname in ("title", "block_type", "width", "height"): setattr(self, dataname, data[dataname]) @@ -319,3 +332,13 @@ def deserialize(self, data: dict, hashmap: dict = None, restore_id=True) -> None hashmap.update({socket_data["id"]: socket}) self.update_all() + + def complete_with_default(self, data: OrderedDict) -> None: + """Add default data in place when fields are missing""" + for key in NONE_OPTIONAL_FIELDS: + if key not in data: + raise ValueError(f"{key} of the socket is missing") + + for key in DEFAULT_BLOCK_DATA.keys(): + if key not in data: + data[key] = DEFAULT_BLOCK_DATA[key] diff --git a/opencodeblocks/blocks/codeblock.py b/opencodeblocks/blocks/codeblock.py index fec56fce..a094e6cc 100644 --- a/opencodeblocks/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -15,6 +15,9 @@ conv = Ansi2HTMLConverter() +DEFAULT_CODE_BLOCK_DATA = {"source": "", "output": ""} +NONE_OPTIONAL_FIELDS = {} + class OCBCodeBlock(OCBBlock): @@ -197,7 +200,20 @@ def deserialize( self, data: OrderedDict, hashmap: dict = None, restore_id: bool = True ): """Restore a codeblock from it's serialized state""" + + self.complete_with_default(data) + for dataname in ("source", "stdout"): if dataname in data: setattr(self, dataname, data[dataname]) super().deserialize(data, hashmap, restore_id) + + def complete_with_default(self, data: OrderedDict) -> None: + """Add default data in place when fields are missing""" + for key in NONE_OPTIONAL_FIELDS: + if key not in data: + raise ValueError(f"{key} of the socket is missing") + + for key in DEFAULT_CODE_BLOCK_DATA.keys(): + if key not in data: + data[key] = DEFAULT_CODE_BLOCK_DATA[key] From cf8107c75865b3d3f13dba7c5f15217caba62887 Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Mon, 6 Dec 2021 18:02:21 +0100 Subject: [PATCH 17/27] Removed uncessary block default --- opencodeblocks/scene/from_ipynb_conversion.py | 52 +++++-------------- 1 file changed, 12 insertions(+), 40 deletions(-) diff --git a/opencodeblocks/scene/from_ipynb_conversion.py b/opencodeblocks/scene/from_ipynb_conversion.py index 5ed4e887..d7b28989 100644 --- a/opencodeblocks/scene/from_ipynb_conversion.py +++ b/opencodeblocks/scene/from_ipynb_conversion.py @@ -62,28 +62,20 @@ def get_blocks_data(data: OrderedDict) -> List[OrderedDict]: text_height: float = TEXT_SIZE * TEXT_SIZE_TO_HEIGHT_RATIO * len(text) block_height: float = text_height + MARGIN_Y - block_data = get_default_block() - - block_data.update( - { - "id": len(blocks_data), - "block_type": BLOCK_TYPE_TO_NAME[block_type], - "width": block_width, - "height": block_height, - "position": [ - next_block_x_pos, - next_block_y_pos, - ], - } - ) + block_data = { + "id": len(blocks_data), + "block_type": BLOCK_TYPE_TO_NAME[block_type], + "width": block_width, + "height": block_height, + "position": [ + next_block_x_pos, + next_block_y_pos, + ], + "sockets": [], + } if block_type == "code": - block_data.update( - { - "source": "".join(text), - "stdout": "", - } - ) + block_data["source"] = "".join(text) next_block_y_pos = 0 next_block_x_pos += block_width + MARGIN_BETWEEN_BLOCKS_X @@ -108,26 +100,6 @@ def get_blocks_data(data: OrderedDict) -> List[OrderedDict]: return blocks_data -def get_default_block() -> OrderedDict: - """Return a default block with argument that vary missing""" - return { - "title": "_", - "splitter_pos": [ - 85, - 261, - ], - "sockets": [], - "metadata": { - "title_metadata": { - "color": "white", - "font": "Ubuntu", - "size": 12, - "padding": 4.0, - } - }, - } - - def is_title(block_data: OrderedDict) -> bool: """Checks if the block is a one-line markdown block which could correspond to a title""" From 2c679e85020187a900dd5c46e9a69ee843736ba1 Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Mon, 6 Dec 2021 18:03:20 +0100 Subject: [PATCH 18/27] Fix bad default --- opencodeblocks/blocks/block.py | 2 +- opencodeblocks/blocks/codeblock.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/opencodeblocks/blocks/block.py b/opencodeblocks/blocks/block.py index 903c4454..646f9752 100644 --- a/opencodeblocks/blocks/block.py +++ b/opencodeblocks/blocks/block.py @@ -31,7 +31,7 @@ "width": 618, "height": 184, "metadata": {"title_metadata": {"color": "white", "font": "Ubuntu", "size": 10}}, - "sockets": {}, + "sockets": [], } NONE_OPTIONAL_FIELDS = {"block_type", "position"} diff --git a/opencodeblocks/blocks/codeblock.py b/opencodeblocks/blocks/codeblock.py index a094e6cc..d0ec51c9 100644 --- a/opencodeblocks/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -217,3 +217,5 @@ def complete_with_default(self, data: OrderedDict) -> None: for key in DEFAULT_CODE_BLOCK_DATA.keys(): if key not in data: data[key] = DEFAULT_CODE_BLOCK_DATA[key] + + super().complete_with_default(data) From e6032f2699031da5770be45cad326fe51696f592 Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Mon, 6 Dec 2021 18:12:46 +0100 Subject: [PATCH 19/27] Remove unused import + Add not implemented --- opencodeblocks/scene/from_ipynb_conversion.py | 2 +- opencodeblocks/scene/to_ipynb_conversion.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/opencodeblocks/scene/from_ipynb_conversion.py b/opencodeblocks/scene/from_ipynb_conversion.py index d7b28989..61293082 100644 --- a/opencodeblocks/scene/from_ipynb_conversion.py +++ b/opencodeblocks/scene/from_ipynb_conversion.py @@ -1,6 +1,6 @@ """ Module for converting ipynb data to ipyg data """ -from typing import Generator, OrderedDict, List, Dict +from typing import OrderedDict, List, Dict MARGIN_X: float = 50 MARGIN_BETWEEN_BLOCKS_X: float = 50 diff --git a/opencodeblocks/scene/to_ipynb_conversion.py b/opencodeblocks/scene/to_ipynb_conversion.py index cf18f038..922189f3 100644 --- a/opencodeblocks/scene/to_ipynb_conversion.py +++ b/opencodeblocks/scene/to_ipynb_conversion.py @@ -1,7 +1,7 @@ """ Module for converting ipyg data to ipynb data """ -from typing import Generator, OrderedDict, List, Dict +from typing import OrderedDict def ipyg_to_ipynb(data: OrderedDict) -> OrderedDict: - return {} + raise NotImplementedError() From f223d27cd3c0cad00d01ced9054d3b515c237979 Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Mon, 6 Dec 2021 18:16:17 +0100 Subject: [PATCH 20/27] Remove uncessary .keys() --- opencodeblocks/blocks/block.py | 2 +- opencodeblocks/blocks/codeblock.py | 4 ++-- opencodeblocks/graphics/edge.py | 2 +- opencodeblocks/graphics/socket.py | 3 +-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/opencodeblocks/blocks/block.py b/opencodeblocks/blocks/block.py index 646f9752..9974bc0c 100644 --- a/opencodeblocks/blocks/block.py +++ b/opencodeblocks/blocks/block.py @@ -339,6 +339,6 @@ def complete_with_default(self, data: OrderedDict) -> None: if key not in data: raise ValueError(f"{key} of the socket is missing") - for key in DEFAULT_BLOCK_DATA.keys(): + for key in DEFAULT_BLOCK_DATA: if key not in data: data[key] = DEFAULT_BLOCK_DATA[key] diff --git a/opencodeblocks/blocks/codeblock.py b/opencodeblocks/blocks/codeblock.py index d0ec51c9..a2b1cf40 100644 --- a/opencodeblocks/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -214,8 +214,8 @@ def complete_with_default(self, data: OrderedDict) -> None: if key not in data: raise ValueError(f"{key} of the socket is missing") - for key in DEFAULT_CODE_BLOCK_DATA.keys(): + for key in DEFAULT_CODE_BLOCK_DATA: if key not in data: data[key] = DEFAULT_CODE_BLOCK_DATA[key] - + super().complete_with_default(data) diff --git a/opencodeblocks/graphics/edge.py b/opencodeblocks/graphics/edge.py index 2fa7599e..5f2de4a8 100644 --- a/opencodeblocks/graphics/edge.py +++ b/opencodeblocks/graphics/edge.py @@ -244,6 +244,6 @@ def complete_with_default(self, data: OrderedDict) -> None: if key not in data: raise ValueError(f"{key} of the socket is missing") - for key in DEFAULT_EDGE_DATA.keys(): + for key in DEFAULT_EDGE_DATA: if key not in data: data[key] = DEFAULT_EDGE_DATA[key] diff --git a/opencodeblocks/graphics/socket.py b/opencodeblocks/graphics/socket.py index 09f7eb83..6d0d2a9b 100644 --- a/opencodeblocks/graphics/socket.py +++ b/opencodeblocks/graphics/socket.py @@ -163,7 +163,6 @@ def complete_with_default(self, data: OrderedDict) -> None: if key not in data: raise ValueError(f"{key} of the socket is missing") - for key in DEFAULT_SOCKET_DATA.keys(): + for key in DEFAULT_SOCKET_DATA: if key not in data: data[key] = DEFAULT_SOCKET_DATA[key] - From faecdb51fb43a3eddf8d7bffe205dcc0f4addbfe Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Tue, 7 Dec 2021 15:29:38 +0100 Subject: [PATCH 21/27] Refactor default data code --- opencodeblocks/blocks/block.py | 38 +++++++++++--------------- opencodeblocks/blocks/codeblock.py | 21 +++++---------- opencodeblocks/core/serializable.py | 27 ++++++++++++++----- opencodeblocks/graphics/edge.py | 18 +++---------- opencodeblocks/graphics/socket.py | 42 +++++++++++------------------ 5 files changed, 62 insertions(+), 84 deletions(-) diff --git a/opencodeblocks/blocks/block.py b/opencodeblocks/blocks/block.py index 9974bc0c..e7c53adf 100644 --- a/opencodeblocks/blocks/block.py +++ b/opencodeblocks/blocks/block.py @@ -25,30 +25,32 @@ BACKGROUND_COLOR = QColor("#E3212121") -DEFAULT_BLOCK_DATA = { - "title": "_", - "splitter_pos": [88, 41], - "width": 618, - "height": 184, - "metadata": {"title_metadata": {"color": "white", "font": "Ubuntu", "size": 10}}, - "sockets": [], -} -NONE_OPTIONAL_FIELDS = {"block_type", "position"} - class OCBBlock(QGraphicsItem, Serializable): """Base class for blocks in OpenCodeBlocks.""" + DEFAULT_DATA = { + "title": "New block", + "splitter_pos": [0, 0], + "width": 300, + "height": 200, + "metadata": { + "title_metadata": {"color": "white", "font": "Ubuntu", "size": 10} + }, + "sockets": [], + } + MANDATORY_FIELDS = {"block_type", "position"} + def __init__( self, block_type: str = "base", source: str = "", position: tuple = (0, 0), - width: int = 300, - height: int = 200, + width: int = DEFAULT_DATA["width"], + height: int = DEFAULT_DATA["height"], edge_size: float = 10.0, - title: Union[OCBTitle, str] = "New block", + title: Union[OCBTitle, str] = DEFAULT_DATA["title"], parent: Optional["QGraphicsItem"] = None, ): """Base class for blocks in OpenCodeBlocks. @@ -332,13 +334,3 @@ def deserialize(self, data: dict, hashmap: dict = None, restore_id=True) -> None hashmap.update({socket_data["id"]: socket}) self.update_all() - - def complete_with_default(self, data: OrderedDict) -> None: - """Add default data in place when fields are missing""" - for key in NONE_OPTIONAL_FIELDS: - if key not in data: - raise ValueError(f"{key} of the socket is missing") - - for key in DEFAULT_BLOCK_DATA: - if key not in data: - data[key] = DEFAULT_BLOCK_DATA[key] diff --git a/opencodeblocks/blocks/codeblock.py b/opencodeblocks/blocks/codeblock.py index a2b1cf40..9fd12e1a 100644 --- a/opencodeblocks/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -15,9 +15,6 @@ conv = Ansi2HTMLConverter() -DEFAULT_CODE_BLOCK_DATA = {"source": "", "output": ""} -NONE_OPTIONAL_FIELDS = {} - class OCBCodeBlock(OCBBlock): @@ -31,6 +28,12 @@ class OCBCodeBlock(OCBBlock): """ + DEFAULT_DATA = { + **OCBBlock.DEFAULT_DATA, + "source": "", + } + MANDATORY_FIELDS = OCBBlock.MANDATORY_FIELDS + def __init__(self, **kwargs): """ Create a new OCBCodeBlock. @@ -207,15 +210,3 @@ def deserialize( if dataname in data: setattr(self, dataname, data[dataname]) super().deserialize(data, hashmap, restore_id) - - def complete_with_default(self, data: OrderedDict) -> None: - """Add default data in place when fields are missing""" - for key in NONE_OPTIONAL_FIELDS: - if key not in data: - raise ValueError(f"{key} of the socket is missing") - - for key in DEFAULT_CODE_BLOCK_DATA: - if key not in data: - data[key] = DEFAULT_CODE_BLOCK_DATA[key] - - super().complete_with_default(data) diff --git a/opencodeblocks/core/serializable.py b/opencodeblocks/core/serializable.py index 52c8675f..4ed6505d 100644 --- a/opencodeblocks/core/serializable.py +++ b/opencodeblocks/core/serializable.py @@ -3,22 +3,27 @@ """ Module for the Serializable base class """ -from typing import OrderedDict +from typing import OrderedDict, Set -class Serializable(): +class Serializable: - """ Serializable base for serializable objects. """ + """Serializable base for serializable objects.""" + + MANDATORY_FIELDS: OrderedDict = {} + DEFAULT_DATA: Set[str] = {} def __init__(self): self.id = id(self) def serialize(self) -> OrderedDict: - """ Serialize the object as an ordered dictionary. """ + """Serialize the object as an ordered dictionary.""" raise NotImplementedError() - def deserialize(self, data: OrderedDict, hashmap: dict = None, restore_id=True) -> None: - """ Deserialize the object from an ordered dictionary. + def deserialize( + self, data: OrderedDict, hashmap: dict = None, restore_id=True + ) -> None: + """Deserialize the object from an ordered dictionary. Args: data: Dictionnary containing data do deserialize from. @@ -28,3 +33,13 @@ def deserialize(self, data: OrderedDict, hashmap: dict = None, restore_id=True) """ raise NotImplementedError() + + def complete_with_default(self, data: OrderedDict) -> None: + """Add default data in place when fields are missing""" + for key in self.MANDATORY_FIELDS: + if key not in data: + raise ValueError(f"{key} of the socket is missing") + + for key in self.DEFAULT_DATA: + if key not in data: + data[key] = self.DEFAULT_DATA[key] diff --git a/opencodeblocks/graphics/edge.py b/opencodeblocks/graphics/edge.py index 5f2de4a8..4376779e 100644 --- a/opencodeblocks/graphics/edge.py +++ b/opencodeblocks/graphics/edge.py @@ -14,18 +14,18 @@ from opencodeblocks.core.serializable import Serializable from opencodeblocks.graphics.socket import OCBSocket -DEFAULT_EDGE_DATA = {"path_type": "bezier"} -NONE_OPTIONAL_FIELDS = {"source", "destination"} - class OCBEdge(QGraphicsPathItem, Serializable): """Base class for directed edges in OpenCodeBlocks.""" + DEFAULT_DATA = {"path_type": "bezier"} + MANDATORY_FIELDS = {"source", "destination"} + def __init__( self, edge_width: float = 4.0, - path_type="bezier", + path_type = DEFAULT_DATA["path_type"], edge_color="#001000", edge_selected_color="#00ff00", source: QPointF = QPointF(0, 0), @@ -237,13 +237,3 @@ def deserialize(self, data: OrderedDict, hashmap: dict = None, restore_id=True): self.update_path() except KeyError: self.remove() - - def complete_with_default(self, data: OrderedDict) -> None: - """Add default data in place when fields are missing""" - for key in NONE_OPTIONAL_FIELDS: - if key not in data: - raise ValueError(f"{key} of the socket is missing") - - for key in DEFAULT_EDGE_DATA: - if key not in data: - data[key] = DEFAULT_EDGE_DATA[key] diff --git a/opencodeblocks/graphics/socket.py b/opencodeblocks/graphics/socket.py index 6d0d2a9b..366b1f29 100644 --- a/opencodeblocks/graphics/socket.py +++ b/opencodeblocks/graphics/socket.py @@ -17,31 +17,31 @@ from opencodeblocks.graphics.edge import OCBEdge from opencodeblocks.blocks.block import OCBBlock -DEFAULT_SOCKET_DATA = { - "type": "input", - "metadata": { - "color": "#FF55FFF0", - "linecolor": "#FF000000", - "linewidth": 1.0, - "radius": 6.0, - }, -} -NONE_OPTIONAL_FIELDS = {"position"} - class OCBSocket(QGraphicsItem, Serializable): """Base class for sockets in OpenCodeBlocks.""" + DEFAULT_DATA = { + "type": "undefined", + "metadata": { + "color": "#FF55FFF0", + "linecolor": "#FF000000", + "linewidth": 1.0, + "radius": 6.0, + }, + } + MANDATORY_FIELDS = {"position"} + def __init__( self, block: "OCBBlock", - socket_type: str = "undefined", + socket_type: str = DEFAULT_DATA["type"], flow_type: str = "exe", - radius: float = 10.0, - color: str = "#FF55FFF0", - linewidth: float = 1.0, - linecolor: str = "#FF000000", + radius: float = DEFAULT_DATA["metadata"]["radius"], + color: str = DEFAULT_DATA["metadata"]["color"], + linewidth: float = DEFAULT_DATA["metadata"]["linewidth"], + linecolor: str = DEFAULT_DATA["metadata"]["linecolor"], ): """Base class for sockets in OpenCodeBlocks. @@ -156,13 +156,3 @@ def deserialize(self, data: OrderedDict, hashmap: dict = None, restore_id=True): self._pen.setColor(QColor(self.metadata["linecolor"])) self._pen.setWidth(int(self.metadata["linewidth"])) self._brush.setColor(QColor(self.metadata["color"])) - - def complete_with_default(self, data: OrderedDict) -> None: - """Add default data in place when fields are missing""" - for key in NONE_OPTIONAL_FIELDS: - if key not in data: - raise ValueError(f"{key} of the socket is missing") - - for key in DEFAULT_SOCKET_DATA: - if key not in data: - data[key] = DEFAULT_SOCKET_DATA[key] From fe5cece6fa6c0edea71492a1c147bf93e0e95b58 Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Wed, 8 Dec 2021 22:57:00 +0100 Subject: [PATCH 22/27] Add a very basic ipyg to ipynb converter --- opencodeblocks/scene/from_ipynb_conversion.py | 20 +---- .../scene/ipynb_conversion_constants.py | 75 +++++++++++++++++++ opencodeblocks/scene/to_ipynb_conversion.py | 45 ++++++++++- 3 files changed, 122 insertions(+), 18 deletions(-) create mode 100644 opencodeblocks/scene/ipynb_conversion_constants.py diff --git a/opencodeblocks/scene/from_ipynb_conversion.py b/opencodeblocks/scene/from_ipynb_conversion.py index 61293082..9a544e55 100644 --- a/opencodeblocks/scene/from_ipynb_conversion.py +++ b/opencodeblocks/scene/from_ipynb_conversion.py @@ -1,22 +1,8 @@ """ Module for converting ipynb data to ipyg data """ -from typing import OrderedDict, List, Dict - -MARGIN_X: float = 50 -MARGIN_BETWEEN_BLOCKS_X: float = 50 -MARGIN_Y: float = 50 -MARGIN_BETWEEN_BLOCKS_Y: float = 5 -BLOCK_MIN_WIDTH: float = 400 -TITLE_MAX_LENGTH: int = 60 -SOCKET_HEIGHT: float = 44.0 -TEXT_SIZE: float = 12 -TEXT_SIZE_TO_WIDTH_RATIO: float = 0.7 -TEXT_SIZE_TO_HEIGHT_RATIO: float = 1.42 - -BLOCK_TYPE_TO_NAME: Dict[str, str] = { - "code": "OCBCodeBlock", - "markdown": "OCBMarkdownBlock", -} +from typing import OrderedDict, List + +from opencodeblocks.scene.ipynb_conversion_constants import * def ipynb_to_ipyg(data: OrderedDict) -> OrderedDict: diff --git a/opencodeblocks/scene/ipynb_conversion_constants.py b/opencodeblocks/scene/ipynb_conversion_constants.py new file mode 100644 index 00000000..44b9a5ab --- /dev/null +++ b/opencodeblocks/scene/ipynb_conversion_constants.py @@ -0,0 +1,75 @@ +from typing import Dict + +MARGIN_X: float = 50 +MARGIN_BETWEEN_BLOCKS_X: float = 50 +MARGIN_Y: float = 50 +MARGIN_BETWEEN_BLOCKS_Y: float = 5 +BLOCK_MIN_WIDTH: float = 400 +TITLE_MAX_LENGTH: int = 60 +SOCKET_HEIGHT: float = 44.0 +TEXT_SIZE: float = 12 +TEXT_SIZE_TO_WIDTH_RATIO: float = 0.7 +TEXT_SIZE_TO_HEIGHT_RATIO: float = 1.42 + +BLOCK_TYPE_TO_NAME: Dict[str, str] = { + "code": "OCBCodeBlock", + "markdown": "OCBMarkdownBlock", +} + +BLOCK_TYPE_SUPPORTED_FOR_IPYG_TO_IPYNB = {"OCBCodeBlock", "OCBMarkdownBlock"} + +DEFAULT_NOTEBOOK_DATA = { + "cells": [], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3", + }, + "language_info": { + "codemirror_mode": {"name": "ipython", "version": 3}, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.4", + }, + }, + "nbformat": 4, + "nbformat_minor": 4, +} + +DEFAULT_CODE_CELL = { + "cell_type": "code", + "execution_count": None, + "metadata": { + "_cell_guid": "b1076dfc-b9ad-4769-8c92-a6c4dae69d19", + "_uuid": "8f2839f25d086af736a60e9eeb907d3b93b6e0e5", + "execution": { + "iopub.execute_input": "2021-11-23T21:43:41.246727Z", + "iopub.status.busy": "2021-11-23T21:43:41.246168Z", + "iopub.status.idle": "2021-11-23T21:43:41.260389Z", + "shell.execute_reply": "2021-11-23T21:43:41.260950Z", + "shell.execute_reply.started": "2021-11-22T18:36:28.843251Z", + }, + "tags": [], + }, + "outputs": [], + "source": [], +} + +DEFAULT_MARKDOWN_CELL = { + "cell_type": "markdown", + "metadata": { + "papermill": { + "duration": 0, + "end_time": "2021-11-23T21:43:55.202848", + "exception": False, + "start_time": "2021-11-23T21:43:55.174774", + "status": "completed", + }, + "tags": [], + }, + "source": [], +} diff --git a/opencodeblocks/scene/to_ipynb_conversion.py b/opencodeblocks/scene/to_ipynb_conversion.py index 922189f3..094bc3a8 100644 --- a/opencodeblocks/scene/to_ipynb_conversion.py +++ b/opencodeblocks/scene/to_ipynb_conversion.py @@ -2,6 +2,49 @@ from typing import OrderedDict +import copy + +from opencodeblocks.scene.ipynb_conversion_constants import * + def ipyg_to_ipynb(data: OrderedDict) -> OrderedDict: - raise NotImplementedError() + """Convert ipyg data (as ordered dict) into ipynb data (as ordered dict)""" + ordered_data: OrderedDict = get_block_in_order(data) + + ipynb_data: OrderedDict = copy.deepcopy(DEFAULT_NOTEBOOK_DATA) + + for block_data in ordered_data["blocks"]: + if block_data["block_type"] in BLOCK_TYPE_SUPPORTED_FOR_IPYG_TO_IPYNB: + ipynb_data["cells"].append(block_to_ipynb_cell(block_data)) + + return ipynb_data + + +def get_block_in_order(data: OrderedDict) -> OrderedDict: + """Changes the order of the blocks from random to the naturel flow of the text""" + + # Not implemented yet + return data + + +def block_to_ipynb_cell(block_data: OrderedDict) -> OrderedDict: + """Convert a ipyg block into its corresponding ipynb cell""" + if block_data["block_type"] == BLOCK_TYPE_TO_NAME["code"]: + cell_data = copy.deepcopy(DEFAULT_CODE_CELL) + cell_data["source"] = split_lines_and_add_newline(block_data["source"]) + return cell_data + if block_data["block_type"] == BLOCK_TYPE_TO_NAME["markdown"]: + cell_data = copy.deepcopy(DEFAULT_MARKDOWN_CELL) + cell_data["source"] = split_lines_and_add_newline(block_data["text"]) + return cell_data + + raise ValueError( + f"The block type {block_data['block_type']} is not supported but has been declared as such" + ) + + +def split_lines_and_add_newline(text: str): + """Split the text and add a \\n at the end of each line + This is the jupyter notebook default formatting for source, outputs and text""" + lines = text.split("\n") + return [line + "\n" for line in lines] From e40b2cabda929da3213b0a5b10ef1e9694de607c Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Thu, 9 Dec 2021 11:04:42 +0100 Subject: [PATCH 23/27] Fix typo in function name --- opencodeblocks/scene/from_ipynb_conversion.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opencodeblocks/scene/from_ipynb_conversion.py b/opencodeblocks/scene/from_ipynb_conversion.py index 9a544e55..48a120ce 100644 --- a/opencodeblocks/scene/from_ipynb_conversion.py +++ b/opencodeblocks/scene/from_ipynb_conversion.py @@ -9,7 +9,7 @@ def ipynb_to_ipyg(data: OrderedDict) -> OrderedDict: """Convert ipynb data (ipynb file, as ordered dict) into ipyg data (ipyg, as ordered dict)""" blocks_data: List[OrderedDict] = get_blocks_data(data) - edges_data: List[OrderedDict] = get_sockets_data(blocks_data) + edges_data: List[OrderedDict] = get_edges_data(blocks_data) return { "blocks": blocks_data, @@ -120,7 +120,7 @@ def adujst_markdown_blocks_width(blocks_data: OrderedDict) -> None: i -= 1 -def get_sockets_data(blocks_data: OrderedDict) -> OrderedDict: +def get_edges_data(blocks_data: OrderedDict) -> OrderedDict: """Add sockets to the blocks (in place) and returns the edge list""" code_blocks: List[OrderedDict] = [ block From 5949862afcaa9c283d8b51fc21eaeb86a42bf437 Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Thu, 9 Dec 2021 12:07:20 +0100 Subject: [PATCH 24/27] Dynamically determine the size of the block --- opencodeblocks/graphics/pyeditor.py | 31 ++++++++++++------- opencodeblocks/scene/from_ipynb_conversion.py | 19 ++++++++++-- .../scene/ipynb_conversion_constants.py | 7 ++--- 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/opencodeblocks/graphics/pyeditor.py b/opencodeblocks/graphics/pyeditor.py index d051448d..536e4683 100644 --- a/opencodeblocks/graphics/pyeditor.py +++ b/opencodeblocks/graphics/pyeditor.py @@ -5,7 +5,14 @@ from typing import TYPE_CHECKING, List from PyQt5.QtCore import QThreadPool, Qt -from PyQt5.QtGui import QFocusEvent, QFont, QFontMetrics, QColor, QMouseEvent, QWheelEvent +from PyQt5.QtGui import ( + QFocusEvent, + QFont, + QFontMetrics, + QColor, + QMouseEvent, + QWheelEvent, +) from PyQt5.Qsci import QsciScintilla, QsciLexerPython from opencodeblocks.graphics.theme_manager import theme_manager @@ -18,13 +25,15 @@ if TYPE_CHECKING: from opencodeblocks.graphics.view import OCBView +POINT_SIZE = 11 + class PythonEditor(QsciScintilla): - """ In-block python editor for OpenCodeBlocks. """ + """In-block python editor for OpenCodeBlocks.""" def __init__(self, block: OCBBlock): - """ In-block python editor for OpenCodeBlocks. + """In-block python editor for OpenCodeBlocks. Args: block: Block in which to add the python editor widget. @@ -64,11 +73,11 @@ def __init__(self, block: OCBBlock): self.setWindowFlags(Qt.WindowType.FramelessWindowHint) def update_theme(self): - """ Change the font and colors of the editor to match the current theme """ + """Change the font and colors of the editor to match the current theme""" font = QFont() font.setFamily(theme_manager().recommended_font_family) font.setFixedPitch(True) - font.setPointSize(11) + font.setPointSize(POINT_SIZE) self.setFont(font) # Margin 0 is used for line numbers @@ -86,19 +95,19 @@ def update_theme(self): lexer.setFont(font) self.setLexer(lexer) - def views(self) -> List['OCBView']: - """ Get the views in which the python_editor is present. """ + def views(self) -> List["OCBView"]: + """Get the views in which the python_editor is present.""" return self.block.scene().views() def wheelEvent(self, event: QWheelEvent) -> None: - """ How PythonEditor handles wheel events """ + """How PythonEditor handles wheel events""" if self.mode == "EDITING" and event.angleDelta().x() == 0: event.accept() return super().wheelEvent(event) @property def mode(self) -> int: - """ PythonEditor current mode """ + """PythonEditor current mode""" return self._mode @mode.setter @@ -108,13 +117,13 @@ def mode(self, value: str): view.set_mode(value) def mousePressEvent(self, event: QMouseEvent) -> None: - """ PythonEditor reaction to PyQt mousePressEvent events. """ + """PythonEditor reaction to PyQt mousePressEvent events.""" if event.buttons() & Qt.MouseButton.LeftButton: self.mode = "EDITING" return super().mousePressEvent(event) def focusOutEvent(self, event: QFocusEvent): - """ PythonEditor reaction to PyQt focusOut events. """ + """PythonEditor reaction to PyQt focusOut events.""" self.mode = "NOOP" self.block.source = self.text() return super().focusOutEvent(event) diff --git a/opencodeblocks/scene/from_ipynb_conversion.py b/opencodeblocks/scene/from_ipynb_conversion.py index 48a120ce..a4f50cf3 100644 --- a/opencodeblocks/scene/from_ipynb_conversion.py +++ b/opencodeblocks/scene/from_ipynb_conversion.py @@ -4,6 +4,10 @@ from opencodeblocks.scene.ipynb_conversion_constants import * +from opencodeblocks.graphics.theme_manager import theme_manager +from opencodeblocks.graphics.pyeditor import POINT_SIZE +from PyQt5.QtGui import QFontMetrics, QFont + def ipynb_to_ipyg(data: OrderedDict) -> OrderedDict: """Convert ipynb data (ipynb file, as ordered dict) into ipyg data (ipyg, as ordered dict)""" @@ -26,6 +30,13 @@ def get_blocks_data(data: OrderedDict) -> List[OrderedDict]: if "cells" not in data: return [] + # Get the font metrics to determine the size fo the blocks + font = QFont() + font.setFamily(theme_manager().recommended_font_family) + font.setFixedPitch(True) + font.setPointSize(POINT_SIZE) + fontmetrics = QFontMetrics(font) + blocks_data: List[OrderedDict] = [] next_block_x_pos: float = 0 @@ -39,13 +50,17 @@ def get_blocks_data(data: OrderedDict) -> List[OrderedDict]: text: str = cell["source"] + text_bouding_box = fontmetrics.boundingRect("".join(text)) + text_width: float = ( - TEXT_SIZE * TEXT_SIZE_TO_WIDTH_RATIO * max(len(line) for line in text) + max(fontmetrics.boundingRect(line).width() for line in text) if len(text) > 0 else 0 ) block_width: float = max(text_width + MARGIN_X, BLOCK_MIN_WIDTH) - text_height: float = TEXT_SIZE * TEXT_SIZE_TO_HEIGHT_RATIO * len(text) + text_height: float = len(text) * ( + fontmetrics.lineSpacing() + fontmetrics.lineWidth() + ) block_height: float = text_height + MARGIN_Y block_data = { diff --git a/opencodeblocks/scene/ipynb_conversion_constants.py b/opencodeblocks/scene/ipynb_conversion_constants.py index 44b9a5ab..7b57d5ee 100644 --- a/opencodeblocks/scene/ipynb_conversion_constants.py +++ b/opencodeblocks/scene/ipynb_conversion_constants.py @@ -1,15 +1,12 @@ from typing import Dict -MARGIN_X: float = 50 +MARGIN_X: float = 75 MARGIN_BETWEEN_BLOCKS_X: float = 50 -MARGIN_Y: float = 50 +MARGIN_Y: float = 60 MARGIN_BETWEEN_BLOCKS_Y: float = 5 BLOCK_MIN_WIDTH: float = 400 TITLE_MAX_LENGTH: int = 60 SOCKET_HEIGHT: float = 44.0 -TEXT_SIZE: float = 12 -TEXT_SIZE_TO_WIDTH_RATIO: float = 0.7 -TEXT_SIZE_TO_HEIGHT_RATIO: float = 1.42 BLOCK_TYPE_TO_NAME: Dict[str, str] = { "code": "OCBCodeBlock", From b54f8cb3c93769de7ba53aa307b3b30d2de4187f Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Thu, 9 Dec 2021 12:45:09 +0100 Subject: [PATCH 25/27] Fix last newline bug --- opencodeblocks/scene/to_ipynb_conversion.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/opencodeblocks/scene/to_ipynb_conversion.py b/opencodeblocks/scene/to_ipynb_conversion.py index 094bc3a8..204fa3b6 100644 --- a/opencodeblocks/scene/to_ipynb_conversion.py +++ b/opencodeblocks/scene/to_ipynb_conversion.py @@ -47,4 +47,6 @@ def split_lines_and_add_newline(text: str): """Split the text and add a \\n at the end of each line This is the jupyter notebook default formatting for source, outputs and text""" lines = text.split("\n") - return [line + "\n" for line in lines] + for i in range(len(lines) - 1): + lines[i] += "\n" + return lines From 5af131e2e563efe56fbeb3d57590eba7ade06ba9 Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Thu, 9 Dec 2021 12:47:18 +0100 Subject: [PATCH 26/27] Add some typing --- opencodeblocks/scene/to_ipynb_conversion.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/opencodeblocks/scene/to_ipynb_conversion.py b/opencodeblocks/scene/to_ipynb_conversion.py index 204fa3b6..98e78878 100644 --- a/opencodeblocks/scene/to_ipynb_conversion.py +++ b/opencodeblocks/scene/to_ipynb_conversion.py @@ -1,6 +1,6 @@ """ Module for converting ipyg data to ipynb data """ -from typing import OrderedDict +from typing import OrderedDict, List import copy @@ -30,11 +30,11 @@ def get_block_in_order(data: OrderedDict) -> OrderedDict: def block_to_ipynb_cell(block_data: OrderedDict) -> OrderedDict: """Convert a ipyg block into its corresponding ipynb cell""" if block_data["block_type"] == BLOCK_TYPE_TO_NAME["code"]: - cell_data = copy.deepcopy(DEFAULT_CODE_CELL) + cell_data: OrderedDict = copy.deepcopy(DEFAULT_CODE_CELL) cell_data["source"] = split_lines_and_add_newline(block_data["source"]) return cell_data if block_data["block_type"] == BLOCK_TYPE_TO_NAME["markdown"]: - cell_data = copy.deepcopy(DEFAULT_MARKDOWN_CELL) + cell_data: OrderedDict = copy.deepcopy(DEFAULT_MARKDOWN_CELL) cell_data["source"] = split_lines_and_add_newline(block_data["text"]) return cell_data @@ -43,7 +43,7 @@ def block_to_ipynb_cell(block_data: OrderedDict) -> OrderedDict: ) -def split_lines_and_add_newline(text: str): +def split_lines_and_add_newline(text: str) -> List[str]: """Split the text and add a \\n at the end of each line This is the jupyter notebook default formatting for source, outputs and text""" lines = text.split("\n") From 9aef64ac51c55ca5f506da80c4c2cfc7fd2549c4 Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Thu, 9 Dec 2021 12:54:12 +0100 Subject: [PATCH 27/27] Fix lint + Improve coherence --- opencodeblocks/graphics/window.py | 6 ++++-- opencodeblocks/scene/from_ipynb_conversion.py | 13 +++++++------ opencodeblocks/scene/ipynb_conversion_constants.py | 2 ++ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/opencodeblocks/graphics/window.py b/opencodeblocks/graphics/window.py index c4e925e0..3e2a6af4 100644 --- a/opencodeblocks/graphics/window.py +++ b/opencodeblocks/graphics/window.py @@ -367,8 +367,10 @@ def oneFileSaveAsJupyter(self) -> bool: """ current_window = self.activeMdiChild() if current_window is not None: - filename, _ = QFileDialog.getSaveFileName( - self, "Save ipygraph to ipynb file" + dialog = QFileDialog() + dialog.setDefaultSuffix(".ipynb") + filename, _ = dialog.getSaveFileName( + self, "Save ipygraph to file", filter="IPython Graph (*.ipynb)" ) if filename == "": return False diff --git a/opencodeblocks/scene/from_ipynb_conversion.py b/opencodeblocks/scene/from_ipynb_conversion.py index a4f50cf3..ab9b41c7 100644 --- a/opencodeblocks/scene/from_ipynb_conversion.py +++ b/opencodeblocks/scene/from_ipynb_conversion.py @@ -2,11 +2,11 @@ from typing import OrderedDict, List -from opencodeblocks.scene.ipynb_conversion_constants import * +from PyQt5.QtGui import QFontMetrics, QFont +from opencodeblocks.scene.ipynb_conversion_constants import * from opencodeblocks.graphics.theme_manager import theme_manager from opencodeblocks.graphics.pyeditor import POINT_SIZE -from PyQt5.QtGui import QFontMetrics, QFont def ipynb_to_ipyg(data: OrderedDict) -> OrderedDict: @@ -50,8 +50,6 @@ def get_blocks_data(data: OrderedDict) -> List[OrderedDict]: text: str = cell["source"] - text_bouding_box = fontmetrics.boundingRect("".join(text)) - text_width: float = ( max(fontmetrics.boundingRect(line).width() for line in text) if len(text) > 0 @@ -103,7 +101,6 @@ def get_blocks_data(data: OrderedDict) -> List[OrderedDict]: def is_title(block_data: OrderedDict) -> bool: """Checks if the block is a one-line markdown block which could correspond to a title""" - if block_data["block_type"] != BLOCK_TYPE_TO_NAME["markdown"]: return False if "\n" in block_data["text"]: @@ -117,7 +114,10 @@ def is_title(block_data: OrderedDict) -> bool: def adujst_markdown_blocks_width(blocks_data: OrderedDict) -> None: - """Modify the markdown blocks width (in place) for them to match the width of block of code below""" + """ + Modify the markdown blocks width (in place) + For them to match the width of block of code below + """ i: int = len(blocks_data) - 1 while i >= 0: @@ -191,6 +191,7 @@ def get_edge_data( edge_end_block_id: int, edge_end_socket_id: int, ) -> OrderedDict: + """Return the ordered dict corresponding to the given parameters""" return { "id": edge_id, "source": {"block": edge_start_block_id, "socket": edge_start_socket_id}, diff --git a/opencodeblocks/scene/ipynb_conversion_constants.py b/opencodeblocks/scene/ipynb_conversion_constants.py index 7b57d5ee..78ecba72 100644 --- a/opencodeblocks/scene/ipynb_conversion_constants.py +++ b/opencodeblocks/scene/ipynb_conversion_constants.py @@ -1,3 +1,5 @@ +""" Module with the constants used to converter to ipynb and from ipynb """ + from typing import Dict MARGIN_X: float = 75