diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f52c6233..fbdd09f2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,6 +24,12 @@ Before doing your **pull request**, check using `pylint` and `pytest` that there pylint .\opencodeblocks\ ``` +Some `pylint` issues can be fixed automatically using `autopep8`, with the following command: + +```bash +autopep8 --in-place --recursive --aggressive opencodeblocks +``` + ```bash pytest --cov=opencodeblocks --cov-report=html tests/unit ``` diff --git a/main.py b/main.py index 6b11ac1d..54ac941f 100644 --- a/main.py +++ b/main.py @@ -5,10 +5,6 @@ import sys from qtpy.QtWidgets import QApplication - -from opencodeblocks.graphics.blocks.codeblock import OCBCodeBlock, OCBBlock -from opencodeblocks.graphics.edge import OCBEdge -from opencodeblocks.graphics.socket import OCBSocket from opencodeblocks.graphics.window import OCBWindow sys.path.insert(0, os.path.join( os.path.dirname(__file__), "..", ".." )) diff --git a/opencodeblocks/graphics/blocks/block.py b/opencodeblocks/graphics/blocks/block.py index 90630bf6..5475a3b9 100644 --- a/opencodeblocks/graphics/blocks/block.py +++ b/opencodeblocks/graphics/blocks/block.py @@ -8,7 +8,7 @@ from PyQt5.QtCore import QPointF, QRectF, Qt from PyQt5.QtGui import QBrush, QPen, QColor, QFont, QPainter, QPainterPath from PyQt5.QtWidgets import QGraphicsItem, QGraphicsSceneMouseEvent, QGraphicsTextItem, \ - QStyleOptionGraphicsItem, QWidget, QApplication + QStyleOptionGraphicsItem, QWidget, QApplication, QGraphicsSceneHoverEvent from opencodeblocks.core.serializable import Serializable from opencodeblocks.graphics.socket import OCBSocket @@ -72,7 +72,10 @@ def __init__(self, block_type:str='base', source:str='', position:tuple=(0, 0), self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable) self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable) + self.setAcceptHoverEvents(True) + self.resizing = False + self.resizing_hover = False # Is the mouse hovering over the resizing area ? self.moved = False self.metadata = { 'title_metadata': { @@ -130,8 +133,8 @@ def paint(self, painter: QPainter, def _is_in_resize_area(self, pos:QPointF): """ Return True if the given position is in the block resize_area. """ - return self.width - pos.x() < 2 * self.edge_size \ - and self.height - pos.y() < 2 * self.edge_size + return self.width - self.edge_size*2 < pos.x() \ + and self.height - self.edge_size*2 < pos.y() def get_socket_pos(self, socket:OCBSocket) -> Tuple[float]: """ Get a socket position to place them on the block sides. """ @@ -172,21 +175,51 @@ def remove_socket(self, socket:OCBSocket): socket.remove() self.update_sockets() + def hoverMoveEvent(self, event:QGraphicsSceneHoverEvent): + """ Triggered when hovering over a block """ + pos = event.pos() + if self._is_in_resize_area(pos): + if not self.resizing_hover: + self._start_hovering() + elif self.resizing_hover: + self._stop_hovering() + return super().hoverMoveEvent(event) + + def _start_hovering(self): + self.resizing_hover = True + QApplication.setOverrideCursor(Qt.CursorShape.SizeFDiagCursor) + + def _stop_hovering(self): + self.resizing_hover = False + QApplication.restoreOverrideCursor() + + def _start_resize(self,pos:QPointF): + self.resizing = True + self.resize_start = pos + QApplication.setOverrideCursor(Qt.CursorShape.SizeFDiagCursor) + + def _stop_resize(self): + self.resizing = False + QApplication.setOverrideCursor(Qt.CursorShape.SizeFDiagCursor) + + def hoverLeaveEvent(self, event:QGraphicsSceneHoverEvent): + """ Triggered when the mouse stops hovering over a block """ + if self.resizing_hover: + self._stop_hovering() + return super().hoverLeaveEvent(event) + def mousePressEvent(self, event:QGraphicsSceneMouseEvent): """ OCBBlock reaction to a mousePressEvent. """ pos = event.pos() - if self._is_in_resize_area(pos) and event.buttons() == Qt.MouseButton.LeftButton: - self.resize_start = pos - self.resizing = True - QApplication.setOverrideCursor(Qt.CursorShape.SizeFDiagCursor) + if self.resizing_hover and event.buttons() == Qt.MouseButton.LeftButton: + self._start_resize(pos) super().mousePressEvent(event) def mouseReleaseEvent(self, event:QGraphicsSceneMouseEvent): """ OCBBlock reaction to a mouseReleaseEvent. """ if self.resizing: self.scene().history.checkpoint("Resized block", set_modified=True) - self.resizing = False - QApplication.restoreOverrideCursor() + self._stop_resize() if self.moved: self.moved = False self.scene().history.checkpoint("Moved block", set_modified=True) diff --git a/opencodeblocks/graphics/blocks/codeblock.py b/opencodeblocks/graphics/blocks/codeblock.py index 306a2e96..ef088285 100644 --- a/opencodeblocks/graphics/blocks/codeblock.py +++ b/opencodeblocks/graphics/blocks/codeblock.py @@ -3,33 +3,61 @@ """ Module for the base OCB Code Block. """ -from PyQt5.QtWidgets import QGraphicsProxyWidget +from typing import Optional + +from PyQt5.QtCore import Qt, QByteArray, QPointF +from PyQt5.QtGui import QPainter, QPainterPath, QPixmap +from PyQt5.QtWidgets import QStyleOptionGraphicsItem, QWidget, QGraphicsProxyWidget, QLabel, \ + QGraphicsSceneMouseEvent, QApplication from opencodeblocks.graphics.blocks.block import OCBBlock from opencodeblocks.graphics.pyeditor import PythonEditor class OCBCodeBlock(OCBBlock): - """ Code Block. """ + """ + Code Block + + Features an area to edit code as well as a panel to display the output. + + The following is always true: + output_panel_height + source_panel_height + edge_size*2 + title_height == height + + """ def __init__(self, **kwargs): super().__init__(block_type='code', **kwargs) + + self.output_panel_height = self.height / 3 + self._min_output_panel_height = 20 + self._min_source_editor_height = 20 + self.source_editor = self.init_source_editor() + self.display = self.init_display() + self.stdout = "" + self.image = "" + + self.resizing_source_code = False + + self.update_all() # Set the geometry of display and source_editor def init_source_editor(self): """ Initialize the python source code editor. """ source_editor_graphics = QGraphicsProxyWidget(self) source_editor = PythonEditor(self) - source_editor.setGeometry( - int(self.edge_size), - int(self.edge_size + self.title_height), - int(self.width - 2*self.edge_size), - int(self.height - self.title_height - 2*self.edge_size) - ) source_editor_graphics.setWidget(source_editor) source_editor_graphics.setZValue(-1) return source_editor_graphics + @property + def _editor_widget_height(self): + return self.height - self.title_height - 2*self.edge_size \ + - self.output_panel_height + + @_editor_widget_height.setter + def _editor_widget_height(self, value: int): + self.output_panel_height = self.height - value - self.title_height - 2*self.edge_size + def update_all(self): """ Update the code block parts. """ if hasattr(self, 'source_editor'): @@ -38,7 +66,14 @@ def update_all(self): int(self.edge_size), int(self.edge_size + self.title_height), int(self._width - 2*self.edge_size), - int(self.height - self.title_height - 2*self.edge_size) + int(self._editor_widget_height) + ) + display_widget = self.display.widget() + display_widget.setGeometry( + int(self.edge_size), + int(self.height - self.output_panel_height - self.edge_size), + int(self.width - 2*self.edge_size), + int(self.output_panel_height) ) super().update_all() @@ -46,9 +81,128 @@ def update_all(self): def source(self) -> str: """ Source code. """ return self._source + + @source.setter + def source(self, value:str): + self._source = value + if hasattr(self, 'source_editor'): + editor_widget = self.source_editor.widget() + editor_widget.setText(self._source) + + @property + def stdout(self) -> str: + """ Code output. Be careful, this also includes stderr """ + return self._stdout + @stdout.setter + def stdout(self, value:str): + self._stdout = value + if hasattr(self, 'source_editor'): + # If there is a text output, erase the image output and display the text output + self.image = "" + editor_widget = self.display.widget() + editor_widget.setText(self._stdout) + + @property + def image(self) -> str: + """ Code output. """ + return self._image + + @image.setter + def image(self, value:str): + self._image = value + if hasattr(self, 'source_editor') and self.image != "": + # If there is an image output, erase the text output and display the image output + editor_widget = self.display.widget() + editor_widget.setText("") + qlabel = editor_widget + ba = QByteArray.fromBase64(str.encode(self.image)) + pixmap = QPixmap() + pixmap.loadFromData(ba) + qlabel.setPixmap(pixmap) + @source.setter def source(self, value:str): self._source = value if hasattr(self, 'source_editor'): editor_widget = self.source_editor.widget() editor_widget.setText(self._source) + + def paint(self, painter: QPainter, + option: QStyleOptionGraphicsItem, #pylint:disable=unused-argument + widget: Optional[QWidget]=None): #pylint:disable=unused-argument + """ Paint the code output panel """ + super().paint(painter, option, widget) + path_title = QPainterPath() + path_title.setFillRule(Qt.FillRule.WindingFill) + path_title.addRoundedRect(0, 0, self.width, self.height, + self.edge_size, self.edge_size) + painter.setPen(Qt.PenStyle.NoPen) + painter.setBrush(self._brush_background) + painter.drawPath(path_title.simplified()) + + def _is_in_resize_source_code_area(self, pos:QPointF): + """ + Return True if the given position is in the area + used to resize the source code widget + """ + source_editor_start = self.height - self.output_panel_height - self.edge_size + + return self.width - self.edge_size/2 < pos.x() and \ + source_editor_start - self.edge_size < pos.y() < source_editor_start + self.edge_size + + + def _is_in_resize_area(self, pos:QPointF): + """ Return True if the given position is in the block resize_area. """ + + # This block features 2 resizing areas with 2 different behaviors + is_in_bottom_left = super()._is_in_resize_area(pos) + return is_in_bottom_left or self._is_in_resize_source_code_area(pos) + + def _start_resize(self,pos:QPointF): + self.resizing = True + self.resize_start = pos + if self._is_in_resize_source_code_area(pos): + self.resizing_source_code = True + QApplication.setOverrideCursor(Qt.CursorShape.SizeFDiagCursor) + + def _stop_resize(self): + self.resizing = False + self.resizing_source_code = False + QApplication.restoreOverrideCursor() + + def mouseMoveEvent(self, event:QGraphicsSceneMouseEvent): + """ + We override the default resizing behavior as the code part and the display part of the block + block can be resized independently. + """ + if self.resizing: + delta = event.pos() - self.resize_start + self.width = max(self.width + delta.x(), self._min_width) + + height_delta = max(delta.y(), + # List of all the quantities that must remain negative. + # Mainly: min_height - height must be negative for all elements + self._min_output_panel_height - self.output_panel_height, + self._min_height - self.height, + self._min_source_editor_height - self._editor_widget_height + ) + + self.height += height_delta + if not self.resizing_source_code: + self.output_panel_height += height_delta + + self.resize_start = event.pos() + self.title_graphics.setTextWidth(self.width - 2 * self.edge_size) + self.update() + + self.moved = True + super().mouseMoveEvent(event) + + def init_display(self): + """ Initialize the output display widget: QLabel """ + display_graphics = QGraphicsProxyWidget(self) + display = QLabel() + display.setText("") + display_graphics.setWidget(display) + display_graphics.setZValue(-1) + return display_graphics diff --git a/opencodeblocks/graphics/edge.py b/opencodeblocks/graphics/edge.py index 5210c9b7..1daa4b90 100644 --- a/opencodeblocks/graphics/edge.py +++ b/opencodeblocks/graphics/edge.py @@ -183,8 +183,12 @@ def deserialize(self, data: OrderedDict, hashmap: dict = None, restore_id=True): if restore_id: self.id = data['id'] self.path_type = data['path_type'] - self.source_socket = hashmap[data['source']['socket']] - self.source_socket.add_edge(self) - self.destination_socket = hashmap[data['destination']['socket']] - self.destination_socket.add_edge(self) - self.update_path() + try: + self.source_socket = hashmap[data['source']['socket']] + self.source_socket.add_edge(self) + + self.destination_socket = hashmap[data['destination']['socket']] + self.destination_socket.add_edge(self) + self.update_path() + except KeyError: + self.remove() diff --git a/opencodeblocks/graphics/function_parsing.py b/opencodeblocks/graphics/function_parsing.py new file mode 100644 index 00000000..1b371ec6 --- /dev/null +++ b/opencodeblocks/graphics/function_parsing.py @@ -0,0 +1,146 @@ + +""" Module for code parsing and code execution """ + +from typing import List, Tuple +from opencodeblocks.graphics.kernel import Kernel + +kernel = Kernel() + + +def run_cell(cell: str) -> str: + """ + Executes a piece of Python code in an ipython kernel, returns its last output + + Args: + cell: String containing Python code + + Return: + output in the last message sent by the kernel + """ + return kernel.execute(cell) + + +def run_with_variable_output(cell: str) -> None: + """ + This is a proof of concept to show that it is possible + to collect a variable output from a kernel execution + + Here the kernel executes the code and prints the output repeatedly + For example: if cell="model.fit(...)", this would print the progress bar progressing + + Args: + cell: String containing Python code + """ + kernel.client.execute(cell) + done = False + while not done: + output, done = kernel.update_output() + print(output) + +def get_function_name(code: str) -> str: + """ + Parses a string of code and returns the first function name it finds + + Args: + code: String containing Python code + + Return: + Name of first defined function + """ + def_index = code.find("def") + if def_index == -1: + raise ValueError("'def' not found in source code") + start_of_name = def_index + 4 + parenthesis_index = code.find("(", start_of_name) + if parenthesis_index == -1: + raise ValueError("'(' not found in source code") + end_of_name = parenthesis_index + return code[start_of_name:end_of_name] + + +def get_signature(code: str) -> str: + """ + Returns the signature of a string of Python code defining a function + For example: the signature of def hello(a,b,c=3) is "(a,b,c=3)" + + Args: + code: String containing Python code + + Return: + Signature of first defined function + + """ + name = get_function_name(code) + run_cell(code) + run_cell("from inspect import signature") + return run_cell(f"print(signature({name}))") + + +def find_kwarg_index(signature_couple: List[str]) -> int: + """ + Returns the index delimiting the args and kwargs in a list of arguments + Examples: + find_kwwarg_index(['a','b','c=3']) -> 2 + find_kwwarg_index([]) -> None + + Args: + list of Strings representing the arguments of a function + + Return: + index delimiting the args and kwargs in a list of arguments + + """ + kwarg_index = len(signature_couple) + for i, item in enumerate(signature_couple): + if "=" in item: + kwarg_index = i + break + return kwarg_index + + +def extract_args(code: str) -> Tuple[List[str], List[str]]: + """ + Returns the args and kwargs of a string of Python code defining a function + Examples: + get_signature(def hello(a,b,c=3)...) -> "(a,b,c=3)" + + Args: + code: String containing Python code + + Return: + (args, kwargs) of first defined function + + """ + signature_string = get_signature(code) + # Remove parentheses + signature_string = signature_string[1:-2] + signature_string = signature_string.replace(" ", "") + if signature_string == "": + return ([], []) + signature_list = signature_string.split(",") + kwarg_index = find_kwarg_index(signature_list) + return signature_list[:kwarg_index], signature_list[kwarg_index:] + + +def execute_function(code: str, *args, **kwargs) -> str: + """ + Executes the function defined in code in an IPython shell and runs it fed by args and kwargs. + Other arguments than the first are passed to the function when executing it. + Keyword arguments are passed to the function when executing it. + + Args: + code: String representing the function code to execute. + + Return: + String representing the output given by the IPython shell when executing the function. + + """ + function_name = get_function_name(code) + execution_code = f'{function_name}(' + for arg in args: + execution_code += f'{arg},' + for name, value in kwargs.items(): + execution_code += f'{name}={value},' + + run_cell(code) + return run_cell(execution_code + ')') diff --git a/opencodeblocks/graphics/kernel.py b/opencodeblocks/graphics/kernel.py new file mode 100644 index 00000000..9fd3abd7 --- /dev/null +++ b/opencodeblocks/graphics/kernel.py @@ -0,0 +1,104 @@ + +""" Module to create and manage ipython kernels """ + +import queue +from typing import Tuple +from jupyter_client.manager import start_new_kernel + + +class Kernel(): + + def __init__(self): + self.kernel_manager, self.client = start_new_kernel() + + def message_to_output(self, message: dict) -> Tuple[str, str]: + """ + Converts a message sent by the kernel into a relevant output + + Args: + message: dict representing the a message sent by the kernel + + Return: + single output found in the message in that order of priority: + image > text data > text print > error > nothing + """ + message_type = 'None' + if 'data' in message: + if 'image/png' in message['data']: + message_type = 'image' + # output an image (from plt.plot or plt.imshow) + out = message['data']['image/png'] + else: + message_type = 'text' + # output data as str (for example if code="a=10\na") + out = message['data']['text/plain'] + elif 'name' in message and message['name'] == "stdout": + message_type = 'text' + # output a print (print("Hello World")) + out = message['text'] + elif 'traceback' in message: + message_type = 'text' + # output an error + out = '\n'.join(message['traceback']) + else: + message_type = 'text' + out = '' + return out, message_type + + def execute(self, code: str) -> str: + """ + Executes code in the kernel and returns the output of the last message sent by the kernel + + Args: + code: String representing a piece of Python code to execute + + Return: + output from the last message sent by the kernel + """ + _ = self.client.execute(code) + done = False + while not done: + # Check for messages, break the loop when the kernel stops sending messages + new_message, done = self.get_message() + if not done: + message = new_message + return self.message_to_output(message)[0] + + def get_message(self) -> Tuple[str, bool]: + """ + Get message in the jupyter kernel + + Args: + code: String representing a piece of Python code to execute + + Return: + Tuple of: + - output from the last message sent by the kernel + - boolean repesenting if the kernel as any other message to send. + """ + done = False + try: + message = self.client.get_iopub_msg(timeout=1000)['content'] + if 'execution_state' in message and message['execution_state'] == 'idle': + done = True + except queue.Empty: + message = None + done = True + return message, done + + def update_output(self) -> Tuple[str, str, bool]: + """ + Returns the current output of the kernel + + Return: + current output of the kernel; done: bool, True if the kernel has no message to send + """ + message, done = self.get_message() + out, output_type = self.message_to_output(message) + return out, output_type, done + + def __del__(self): + """ + Shuts down the kernel + """ + self.kernel_manager.shutdown_kernel() diff --git a/opencodeblocks/graphics/pyeditor.py b/opencodeblocks/graphics/pyeditor.py index f7127052..cddafa16 100644 --- a/opencodeblocks/graphics/pyeditor.py +++ b/opencodeblocks/graphics/pyeditor.py @@ -4,12 +4,15 @@ """ Module for OCB in block python editor. """ from typing import TYPE_CHECKING, List -from PyQt5.QtCore import Qt +from PyQt5.QtCore import Qt, QCoreApplication from PyQt5.QtGui import QFocusEvent, QFont, QFontMetrics, QColor from PyQt5.Qsci import QsciScintilla, QsciLexerPython from opencodeblocks.graphics.theme_manager import theme_manager from opencodeblocks.graphics.blocks.block import OCBBlock +from opencodeblocks.graphics.kernel import Kernel + +kernel = Kernel() if TYPE_CHECKING: @@ -98,7 +101,28 @@ def focusInEvent(self, event: QFocusEvent): def focusOutEvent(self, event: QFocusEvent): """ PythonEditor reaction to PyQt focusOut events. """ self.set_views_mode("MODE_NOOP") - if self.isModified(): - self.block.source = self.text() + + code = self.text() + if self.isModified() and code != self.block.source: + self.block.source = code self.setModified(False) - return super().focusInEvent(event) + # Execute the code + kernel.client.execute(code) + done = False + # While the kernel sends messages + while done is False: + # Keep the GUI alive + QCoreApplication.processEvents() + # Save kernel message and display it + output, output_type, done = kernel.update_output() + if done is False: + if output_type == 'text': + self.block.stdout = output + elif output_type == 'image': + self.block.image = output + return super().focusOutEvent(event) + + def gatherBlockInputs(self): + args = [2, 3] + kwargs = {"chicken": False} + return args, kwargs diff --git a/opencodeblocks/graphics/qss/dark_resources.py b/opencodeblocks/graphics/qss/dark_resources.py index 358c0113..3b7d2693 100644 --- a/opencodeblocks/graphics/qss/dark_resources.py +++ b/opencodeblocks/graphics/qss/dark_resources.py @@ -501,9 +501,11 @@ qt_resource_struct = qt_resource_struct_v2 def qInitResources(): - QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) + QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, + qt_resource_name, qt_resource_data) def qCleanupResources(): - QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) + QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, + qt_resource_name, qt_resource_data) qInitResources() diff --git a/opencodeblocks/graphics/window.py b/opencodeblocks/graphics/window.py index e8dd41c4..c33056e5 100644 --- a/opencodeblocks/graphics/window.py +++ b/opencodeblocks/graphics/window.py @@ -198,7 +198,7 @@ def updateWindowMenu(self): for i, window in enumerate(windows): child = window.widget() - text = "%d %s" % (i + 1, child.windowTitle()) + text = f"{i + 1} {child.windowTitle()}" if i < 9: text = '&' + text diff --git a/requirements-dev.txt b/requirements-dev.txt index cef44d30..7abe284a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,4 +6,5 @@ pytest-pspec pytest-qt pyautogui pylint -pylint-pytest \ No newline at end of file +pylint-pytest +autopep8 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 043f5918..18f07683 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,6 @@ pyqt5>=5.15.4 QtPy>=1.9.0 -qscintilla>=2.13.0 \ No newline at end of file +qscintilla>=2.13.0 +Ipython>=7.27.0 +jupyter_client>=7.0.6 +ipykernel>=6.5.0 \ No newline at end of file diff --git a/tests/unit/scene/test_function_parsing.py b/tests/unit/scene/test_function_parsing.py new file mode 100644 index 00000000..d6e01cad --- /dev/null +++ b/tests/unit/scene/test_function_parsing.py @@ -0,0 +1,88 @@ +""" Unit tests for the opencodeblocks function parsing module. """ + +import pytest +from pytest_mock import MockerFixture +import pytest_check as check + +from opencodeblocks.graphics.function_parsing import (find_kwarg_index, run_cell, + get_function_name, + get_signature, + extract_args, + execute_function, + find_kwarg_index) + + +class TestFunctionParsing: + + """Testing function_parsing functions""" + + def test_run_cell(self, mocker: MockerFixture): + """ Test run_cell """ + check.equal(run_cell("print(10)"), '10\n') + + def test_get_function_name(self, mocker: MockerFixture): + """ Test get_function_name """ + check.equal(get_function_name( + "def function():\n return 'Hello'"), 'function') + check.equal(get_function_name( + "#Hello\ndef function():\n return 'Hello'\na = 10"), 'function') + check.equal(get_function_name( + "#Hello\ndef function(a,b=10):\n return 'Hello'\na = 10"), 'function') + + def test_get_function_name_error(self, mocker: MockerFixture): + """ Return ValueError if get_function_name has wrong input """ + with pytest.raises(ValueError): + get_function_name("") + get_function_name("#Hello") + get_function_name("def function") + + def test_get_signature(self, mocker: MockerFixture): + """ Test get_signature """ + mocker.patch( + 'opencodeblocks.graphics.function_parsing.run_cell', return_value="(a, b, c=10)\n") + check.equal(get_signature( + "def function(a,b, c=10):\n return None"), "(a, b, c=10)\n") + + def test_find_kwarg_index(self, mocker: MockerFixture): + """ Test find_kwarg_index """ + check.equal(find_kwarg_index(['a', 'b', 'c=10']), 2) + check.equal(find_kwarg_index([]), 0) + + def test_extract_args(self, mocker: MockerFixture): + """ Test extract_args """ + mocker.patch( + 'opencodeblocks.graphics.function_parsing.get_signature', return_value="()\n") + mocker.patch( + 'opencodeblocks.graphics.function_parsing.find_kwarg_index', return_value=0) + check.equal(extract_args( + "def function():\n return 'Hello'"), ([], [])) + mocker.patch('opencodeblocks.graphics.function_parsing.get_signature', + return_value="(a,b,c = 10)\n") + mocker.patch( + 'opencodeblocks.graphics.function_parsing.find_kwarg_index', return_value=2) + check.equal(extract_args( + "def function(a,b,c = 10):\n return 'Hello'"), (["a", "b"], ["c=10"])) + + def test_extract_args_empty(self, mocker: MockerFixture): + """ Return a couple of empty lists if signature is empty """ + mocker.patch('opencodeblocks.graphics.function_parsing.get_signature', + return_value="()\n") + mocker.patch( + 'opencodeblocks.graphics.function_parsing.find_kwarg_index', return_value=None) + check.equal(extract_args( + "def function( ):\n return 'Hello'"), ([], [])) + mocker.patch('opencodeblocks.graphics.function_parsing.get_signature', + return_value="()\n") + mocker.patch( + 'opencodeblocks.graphics.function_parsing.find_kwarg_index', return_value=None) + check.equal(extract_args( + "def function():\n return 'Hello'"), ([], [])) + + def test_execute_function(self, mocker: MockerFixture): + """ Test execute_function """ + mocker.patch('opencodeblocks.graphics.function_parsing.get_function_name', + return_value="function") + mocker.patch( + 'opencodeblocks.graphics.function_parsing.run_cell', return_value="Out[1]: 25\n") + check.equal(execute_function( + "def function(a,b,c=10):\n return a+b+c", 10, 5), "Out[1]: 25\n")