diff --git a/CHANGELOG.md b/CHANGELOG.md index d00aef4e..9ab23f46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [0.4.1] - 2022-06-18 + +### Added + +* New settings to switch che code execution engine. + ## [0.4.0] - 2022-06-17 ### Added @@ -15,6 +21,10 @@ Under the hood improvements on code execution and some refactoring. +### Changed + +* Changed default way to execute code to `executeInMainThread` function. + ## [0.2.0] - 2021-10-29 Tests, code refactoring, connection timeouts, various fixes and optimizations. diff --git a/README.html b/README.html index cf1f2db1..97516671 100644 --- a/README.html +++ b/README.html @@ -2,6 +2,7 @@ + README.md @@ -16,19 +17,57 @@ : 'default' }); -

Changelog

-

v0.2.0 2021-10-29

-
  • Connections timeouts.
  • -
  • Various fixes and optimizations.
  • +

    Changelog

    +

    v0.4.1 2022-02-18

    +
  • Switch code execution engine settings.
  • -

    v0.1.0 2021-09-23

    -
  • Execute BlinkScript code.
  • -
  • Send/Receive Nodes from another instance.
  • -
  • New About widget.
  • -
  • API now accepts also simple strings when sending a request.
  • -
  • Under the hood optimizations.
  • +

    v0.4.0 2022-02-17

    +
  • Intercept exceptions inside Nuke's thread to extension output.
  • +
  • Configurable timeout settings.
  • +
  • Display timeout timers in UI.
  • +

    v0.3.0 2022-02-06

    +
  • Various fixes and optimizations for code execution.
  • + +

    v0.2.0 2021-10-29

    +
  • Connections timeouts.
  • +
  • Various fixes and optimizations.
  • + +

    v0.1.0 2021-09-23

    +
  • Execute BlinkScript code.
  • +
  • Send/Receive Nodes from another instance.
  • +
  • New About widget.
  • +
  • API now accepts also simple strings when sending a request.
  • +
  • Under the hood optimizations.
  • +

    1. NukeServerSocket README

    +

    Main Build + Pre Release + Last commit +

    +

    issues + pull-request +

    +

    Codacy Badge + Codacy Badge + DeepSource +

    +

    license

    +

    x + x +

    A Nuke plugin that will allow code execution from the local network and more.

  • 1.5. Settings
  • -
  • 1.6. Extendibility +
  • 1.6. Extendibility
  • +
  • 1.7. Test plugin locally
  • +
  • 1.8. Known Issues
  • +
  • 1.9. Compatibility
  • +
  • 1.10. Overview
  • -
  • 1.7. Known Issues
  • -
  • 1.8. Compatibility
  • -
  • 1.9. Test plugin locally
  • -
  • 1.10. Overview
  • @@ -87,15 +126,14 @@

    1.3. Installation

    1.4. Usage

    1.4.1. Receive incoming request

    +

    Demo

    Open the NukeServerSocket panel and with the mode on Receiver, start the server by clicking Connect.

    If you receive a message: "Server did not initiate. Error: The bound address is already in use", just change the Port to a random number between 49152 and 65535 and try again. It means that probably you have a connection listening on that port already. - Also when connected, you could test the receiver with the Test Receiver button, otherwise you are - done. -

    + Also when connected, you could test the receiver with the Test Receiver button.

    When connected, NukeServerSocket will listen for incoming request on the IP Address and Port shown in the plugin.

    @@ -103,63 +141,74 @@

    1.4.1. Receive incoming request

    href="https://marketplace.visualstudio.com/items?itemName=virgilsisoe.nuke-tools">Nuke Tools or any other method you prefer.

    1.4.2. Receive/Send nodes

    +

    Demo

    1.4.2.1. Send

    When sending nodes, switch the mode from Receiver to Sender and be sure that there is another NukeServerSocket instance listening for incoming network request (Receive incoming request). Select the nodes you want to send a click - Send Selected Nodes. -

    + Send Selected Nodes.

    -

    By default, the IP Address on the sender points to the local IP address, so if you want to send nodes between the - same computer you should only specify the Port. When sending nodes to another computer you will also need to - specify the IP Address of the NukeServerSocket computer you want the nodes to be sended.

    +

    By default, the IP Address on the sender points to the local IP address, so, if you want to send nodes between + the same computer, you should only specify the Port. When sending nodes to another computer, you will also need to + specify the IP Address of the computer you want to send the nodes.

    1.4.2.2. Receive

    When receiving nodes just follow the steps for Receive incoming request - for the receiving instance and the Send steps for the sending instances.

    + for the receiving instance and the Send steps for the sending instance.

    1.5. Settings

    -

    The plugin offers some minor settings like output text to internal script editor, format text and so on.

    -

    1.6. Extendibility

    -

    At its core, the plugin is just a server socket that awaits for an incoming request, performs the operations inside - Nuke and returns the result. Nothing ties it to any application per se.

    -

    The only requirement is that the code received should be inside a string.

    -

    From the client point of view, the code can be sended either inside a stringified associative array or - inside a simple string, with the latter being valid only when sending Python code.

    -

    The associative array should have the following keys: text and an optional file.

    +

    The settings can be accessed from the plugin toolbar

    -

    Although the associative array is optional when executing Python code, it is a requirement when executing - BlinkScript. The key file must contain a valid file extension: .cpp or - .blink in order for the plugin to know where to delegate the job. -

    -
    -

    When sending a stringified array, the plugin will try to deserialize it with json.loads().

    -
    -

    1.6.1. Code Sample

    -

    Please see Github Readme #Code Samples - section for more information.

    -

    1.6.2. Port & Host address

    -

    NukeServerSocket by default will listen on any host address.

    -

    When connecting locally (same computer) you can just specify the local host address (eg.127.0.0.1) in - your socket client code. When connecting from a different computer you also specify the exact IP Address (eg - 192.168.1.10). -

    -

    The port value is written to .nuke/NukeServerSocket.ini inside the server/port field. Each - time the user changes it, it gets update automatically. If used locally, this can be used from your extension to - pick at which port to connect.

    -

    This is pretty much all you need to start your own extension for your favorite text editor or any other method you - prefer.

    -

    If you still have some problems, please feel free to reach out for any questions.

    -

    1.7. Known Issues

    +

    1.6. Extendibility

    +

    At its core, the plugin is just a server socket that awaits for an incoming request, + performs the operations inside Nuke and returns the result. Nothing ties it to any + application per se.

    +

    This makes it very easy to implement a new client, without the need to modify NukeServerSocket source code. The + client needs only to send the data at the specified address inside NukeServerSocket.

    +

    More information and examples on the wiki page.

    +

    1.7. Test plugin locally

    +

    The plugin can be launched locally outside the Nuke application. This is useful for testing code and implementing + new features more rapidly.

    +

    More information on the wiki + page.

    +

    1.8. Known Issues

    -

    1.8. Compatibility

    +

    1.9. Compatibility

    Nuke version: 11,12, 13.

    Because Nuke 11 uses an early version of PySide2, future compatibility is not a guarantee.

    @@ -180,12 +229,13 @@

    1.8. Compatibility

  • Windows 10
  • 1.10. Overview

    +

    1.10.1. Execute Code

    - - +

    1.10.2. Send Nodes

    + + \ No newline at end of file diff --git a/README.md b/README.md index 20304a0c..93348825 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,10 @@ When receiving nodes just follow the steps for [Receive incoming request](#141-r The settings can be accessed from the plugin toolbar +- **Code Execution Engine**: Change the engine that is executing the code. + - **Nuke Internal**: Nuke `executeInMainThread` function. + - **Script Editor**: Nuke Script Editor widget. + - **Mirror To Script Editor**: Allows to mirror the input/output code to the internal script editor. - **Override Output Editor**: Mirror output to the internal script editor. - **Format Text**: The script editor output window will received a formatted version of the code result. @@ -119,6 +123,7 @@ More information on the [wiki page](https://github.com/sisoe24/NukeServerSocket/ ## 1.8. Known Issues +- Creating a modal window with the Nuke internal code execution engine, will cause Nuke to freeze. A workaround is to switch to the Script Editor engine. - Settings window doesn't display the tooltip text. - When changing workspace with an active open connection, Nuke will load a new plugin instance with the default UI state. This would look as if the previous connection has been closed, where in reality is still open and listening. One way to force close all of the connections is to restart Nuke, or you can wait for the connection timeout. diff --git a/pyproject.toml b/pyproject.toml index 8415c2cb..77319b67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "nukeserversocket" -version = "0.4.0" +version = "0.4.1" description = "" authors = ["virgilsisoe "] packages = [ diff --git a/src/controllers/nuke_controllers.py b/src/controllers/nuke_controllers.py index 2a563a5c..6e1d27b7 100644 --- a/src/controllers/nuke_controllers.py +++ b/src/controllers/nuke_controllers.py @@ -76,18 +76,26 @@ def execute(self): If for some reason execution fails, method will fallback on the script editor execution mechanism. """ - try: - self._output = nuke.executeInMainThreadWithResult( - self._exec, self._input) - except Exception: # skipcq: PYL-W0703 - err = "executeInMainThread Error. Fallback on ScriptEditor for now." - self.execution_error.emit(err) - LOGGER.error(err) - - self.script_editor.set_input(self._input) - self.script_editor.execute() - self._output = self.script_editor.output() - self.script_editor.restore_state() + if AppSettings().get_bool('engine/nuke_internal', True): + try: + self._output = nuke.executeInMainThreadWithResult( + self._exec, self._input) + LOGGER.debug('execute internal') + except Exception: # skipcq: PYL-W0703 + err = "executeInMainThread Error. Fallback on ScriptEditor for now." + self.execution_error.emit(err) + LOGGER.error(err) + self._execute_script_editor() + else: + self._execute_script_editor() + + def _execute_script_editor(self): + """Execute code from the internal script editor widget.""" + LOGGER.debug('execute script editor') + self.script_editor.set_input(self._input) + self.script_editor.execute() + self._output = self.script_editor.output() + self.script_editor.restore_state() def output(self): # type: () -> str """Get output from the nuke command.""" diff --git a/src/widgets/settings_widget.py b/src/widgets/settings_widget.py index 0e95e5c8..4d9c2354 100644 --- a/src/widgets/settings_widget.py +++ b/src/widgets/settings_widget.py @@ -4,6 +4,7 @@ import logging +from PySide2.QtCore import Qt from PySide2.QtWidgets import ( QCheckBox, QFormLayout, @@ -11,7 +12,8 @@ QGroupBox, QVBoxLayout, QSpinBox, - QLabel + QLabel, + QRadioButton ) from ..utils import AppSettings @@ -19,6 +21,20 @@ LOGGER = logging.getLogger('NukeServerSocket.settings_widget') +def _format_name(name): + """Format name. + + Format a name into lowercase and no space: `This Button` to `this_button`. + + Args: + name (str): name to be formatted + + Returns: + str: the formatted name. + """ + return name.lower().replace(' ', '_') + + class CheckBox(QCheckBox): """Custom QCheckBox class. @@ -43,7 +59,7 @@ def __init__(self, title, label, default_state, tooltip, parent=None): self.setToolTip(tooltip) self._label = label - obj_name = title.lower().replace(' ', '_') + obj_name = _format_name(title) setting_name = 'options/%s' % obj_name self.setObjectName(obj_name) @@ -91,16 +107,12 @@ def __init__(self): default_state=False, title='Override Input Editor', parent=self, tooltip='Override internal input text editor', label='Input:') - _layout_checkboxes = QFormLayout() - _layout_checkboxes.setVerticalSpacing(10) + _layout = QFormLayout() + _layout.setAlignment(Qt.AlignCenter) + _layout.setVerticalSpacing(10) for checkbox in self.findChildren(CheckBox): - _layout_checkboxes.addRow(checkbox._label, checkbox) - - self.setLayout(_layout_checkboxes) - - _layout = QVBoxLayout() - _layout.addWidget(self) + _layout.addRow(checkbox._label, checkbox) self.setLayout(_layout) @@ -179,6 +191,7 @@ def __init__(self): QGroupBox.__init__(self, 'Timeout') _layout = QFormLayout() + _layout.setAlignment(Qt.AlignCenter) _layout.addRow(QLabel('Server (minutes)'), self.set_spinbox('server', 10)) _layout.addRow(QLabel('Receiver (seconds)'), @@ -211,6 +224,54 @@ def set_spinbox(label, value): # type: (str, int) -> QSpinBox return spinbox +class CodeEngineSettings(QGroupBox): + """A QGroupBox class for the code execution engine settings. + + Settings will allow the user to choose which engine to use when executing + code: + * Nuke internal: nukeExecuteInMainThread. + * Script Editor: Qt scriptEditor widget. + """ + + def __init__(self): + """Initialize the CodeEngineSettings class.""" + QGroupBox.__init__(self, 'Code Execution Engine') + + _layout = QVBoxLayout() + _layout.setAlignment(Qt.AlignCenter) + _layout.addWidget(self.setup_button('Nuke Internal', True)) + _layout.addWidget(self.setup_button('Script Editor', False)) + + self.setLayout(_layout) + + @staticmethod + def setup_button(label, default_state): # type: (str, bool) -> QRadioButton + """Set a QRadioButton widget. + + Set up a QRadioButton widget default state and connects its toggle + signal to the application settings. + + Args: + label (str): label of the radiobutton + default_state (bool): default state of the button if no settings is + found. + + Returns: + _type_: A QRadioButton object. + """ + settings = AppSettings() + setting_name = 'engine/%s' % _format_name(label) + + radiobutton = QRadioButton(label) + radiobutton.setChecked(settings.get_bool(setting_name, default_state)) + + radiobutton.toggled.connect( + lambda state: settings.setValue(setting_name, state) + ) + + return radiobutton + + class SettingsWidget(QWidget): """Settings Widget.""" @@ -220,6 +281,7 @@ def __init__(self): self.setObjectName('SettingsWidget') _layout = QVBoxLayout() + _layout.addWidget(CodeEngineSettings()) _layout.addWidget(ScriptEditorSettings()) _layout.addWidget(TimeoutSettings()) diff --git a/tests/test_options_names.py b/tests/test_options_names.py index 2cd942c2..4a58f27c 100644 --- a/tests/test_options_names.py +++ b/tests/test_options_names.py @@ -9,7 +9,10 @@ import pytest -from src.widgets import settings_widget +from PySide2.QtWidgets import QRadioButton +from src.widgets.settings_widget import ( + _format_name, SettingsWidget, CheckBox, ScriptEditorSettings +) pytestmark = pytest.mark.settings_name @@ -44,6 +47,17 @@ def _get_options_keys(file): return re.findall(r'(?<=options/)\w+', src_file.read()) +def _get_engine_keys(file): + """Get all of the options reference inside the source file. + + Returns: + list: list with all of the options reference found. + """ + # TODO: this will not find the names in the test_settings_widget.py file. + with open(file) as src_file: + return re.findall(r'(?<=engine/)\w+', src_file.read()) + + def _format_name(name): """Format name. @@ -61,18 +75,39 @@ def _format_name(name): @pytest.fixture() def _options_name(qtbot): """Get the options name from the SettingsWidget.""" - widget = settings_widget.SettingsWidget() + widget = SettingsWidget() qtbot.addWidget(widget) - checkboxes = widget.findChildren(settings_widget.CheckBox) - checkboxes_names = [_format_name(_.text()) for _ in checkboxes] + checkboxes = widget.findChildren(CheckBox) + options_names = [_format_name(_.text()) for _ in checkboxes] - group_box = widget.findChild(settings_widget.QGroupBox) - group_box_name = _format_name(group_box.title()) + # add the groupbox title name since it being used as a setting value + options_names.append(_format_name( + ScriptEditorSettings().title())) - checkboxes_names.append(group_box_name) + return options_names - return checkboxes_names + +@pytest.fixture() +def _engine_name(qtbot): + """Get the options name from the SettingsWidget.""" + widget = SettingsWidget() + qtbot.addWidget(widget) + + radio_buttons = widget.findChildren(QRadioButton) + return [_format_name(_.text()) for _ in radio_buttons] + + +def test_engine_names(_package, _engine_name): + """Test that options name are valid. + + Search for the files that are using the setting system, and confirm that + names are valid. + """ + for file in _traverse_dir(_package): + for opt in _get_engine_keys(file): + msg = 'Option name was not update in file: {}'.format(file) + assert opt in _engine_name, msg def test_options_names(_package, _options_name):