From baa373339bf6db3c861228838c1d038ae5c3e4c7 Mon Sep 17 00:00:00 2001
From: Virgil <28490646+sisoe24@users.noreply.github.com>
Date: Wed, 1 Jan 2025 15:28:08 -0500
Subject: [PATCH] 1.2.0 (#34)
* Refactor controllers by adding a base class
Because we now have a new case for houdini that does not use an editor, we need a base controller class so we can keep everything as it is.
* Refactor tests to new base class
* Improve logging message
* Add example in docs
* Add houdini controller
* Rename module
* Cleanup vscode tasks
* Add py37 specific testenv case
* Remove user from about info
* Renamed and move modules
* Add missing type annotations for parent
* Update changelog
* Update local script name
* Check if button is none
* Fix path
* Update houdini package.json
* Update readme
* Bump to 1.2.0
* Bump version
* Rename scripts to tools
houdini includes a scripts folder which shoud not be on the git
attributes exclude
* Fix comment
* Fix houdini exec with exception
* Refactor local editor
* Improve logging message
* Refactor exec code with stdout capture
* Add tests for exec_code
* Fix format
---
.gitattributes | 2 +-
.pre-commit-config.yaml | 4 +-
.vscode/tasks.json | 20 +--
CHANGELOG.md | 12 ++
README.md | 151 +++++++++++-------
houdini_package.json | 10 ++
nukeserversocket/__init__.py | 1 -
nukeserversocket/console.py | 7 +-
.../base.py} | 34 ++--
.../python_panels/nukeserversocket.pypanel | 17 ++
.../houdini/scripts/python/app/main.py | 21 +++
.../controllers/{local_app.py => local.py} | 55 ++++---
nukeserversocket/controllers/nuke.py | 28 ++--
nukeserversocket/main.py | 18 +--
nukeserversocket/received_data.py | 2 +-
nukeserversocket/server.py | 9 +-
nukeserversocket/settings_ui.py | 4 +-
nukeserversocket/toolbar.py | 7 +-
nukeserversocket/utils/__init__.py | 2 +-
nukeserversocket/utils/exec_code.py | 55 +++++++
nukeserversocket/utils/stdout.py | 21 ---
nukeserversocket/version.py | 2 +-
pyproject.toml | 4 +-
tests/test_editor_controller.py | 22 +--
tests/test_exec_code.py | 49 ++++++
tests/test_main.py | 10 +-
tests/test_nuke.py | 29 ++--
tests/test_server.py | 15 +-
{scripts => tools}/release_manager.py | 0
tox.ini | 9 ++
30 files changed, 407 insertions(+), 213 deletions(-)
create mode 100644 houdini_package.json
rename nukeserversocket/{editor_controller.py => controllers/base.py} (86%)
create mode 100644 nukeserversocket/controllers/houdini/python_panels/nukeserversocket.pypanel
create mode 100644 nukeserversocket/controllers/houdini/scripts/python/app/main.py
rename nukeserversocket/controllers/{local_app.py => local.py} (53%)
create mode 100644 nukeserversocket/utils/exec_code.py
delete mode 100644 nukeserversocket/utils/stdout.py
create mode 100644 tests/test_exec_code.py
rename {scripts => tools}/release_manager.py (100%)
diff --git a/.gitattributes b/.gitattributes
index 703f5e21..80fa83e0 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -27,7 +27,7 @@ CHANGELOG.md export-ignore
.githooks export-ignore
.vscode export-ignore
images export-ignore
-scripts export-ignore
+tools export-ignore
tests export-ignore
# Source files
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index a8c9eb0b..1b0f99e1 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -2,14 +2,14 @@
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.5.0
+ rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: double-quote-string-fixer
- id: end-of-file-fixer
- id: check-ast
- repo: https://github.com/hhatto/autopep8
- rev: v2.0.4
+ rev: v2.3.1
hooks:
- id: autopep8
args: [--global-config=pyproject.toml, --in-place]
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 62cec614..cb566ea2 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -11,21 +11,7 @@
{
"label": "RunApp",
"type": "shell",
- "options": {
- "env": {
- "PYTHONDONTWRITEBYTECODE": "1"
- }
- },
- "osx": {
- "command": ".venv/bin/python",
- },
- "windows": {
- "command": ".venv\\Scripts\\python.exe",
- },
- "args": [
- "-m",
- "nukeserversocket.controllers.local_app"
- ],
+ "command": "poetry run nukeserversocket",
"problemMatcher": []
},
{
@@ -36,8 +22,8 @@
"run",
"pytest",
"-xsl",
- // "--lf",
- "-vvv",
+ "--count",
+ "2",
// "-m quick"
],
"group": "test",
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8e29a957..468d75e9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
# Changelog
+## [1.2.0] - 2024-11-17
+
+### Added
+
+- Houdini support through new `HoudiniController` and `HoudiniEditor` in `controllers.houdini` module.
+
+### Changed
+
+- Refactor the core controller components to allow multiple controllers types (houdini does not uses a script editor for example).
+- Moved and rename some files for better organizzation.
+- Updated logging and documentation
+
## [1.1.0] - 2024-08-18
### Added
diff --git a/README.md b/README.md
index a162d545..f60a0ead 100644
--- a/README.md
+++ b/README.md
@@ -10,47 +10,36 @@
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/5b59bd7f80c646a8b2b16ad4b8cba599)](https://www.codacy.com/gh/sisoe24/nukeserversocket/dashboard?utm_source=github.com&utm_medium=referral&utm_content=sisoe24/nukeserversocket&utm_campaign=Badge_Grade)
[![Codacy Badge](https://app.codacy.com/project/badge/Coverage/5b59bd7f80c646a8b2b16ad4b8cba599)](https://www.codacy.com/gh/sisoe24/nukeserversocket/dashboard?utm_source=github.com&utm_medium=referral&utm_content=sisoe24/nukeserversocket&utm_campaign=Badge_Coverage)
-
-[![NukeTools](https://img.shields.io/github/v/release/sisoe24/Nuke-Tools?label=NukeTools)](https://marketplace.visualstudio.com/items?itemName=virgilsisoe.nuke-tools)
+![x](https://img.shields.io/badge/Nuke-✅-success)
+![x](https://img.shields.io/badge/Houdini-✅-success)
![x](https://img.shields.io/badge/Python-3.*-success)
-![x](https://img.shields.io/badge/Nuke-_13_|_14_|_15-yellow)
-A Nuke plugin to run code from external applications.
+[![NukeTools](https://img.shields.io/github/v/release/sisoe24/Nuke-Tools?label=NukeTools)](https://marketplace.visualstudio.com/items?itemName=virgilsisoe.nuke-tools)
- [1. nukeserversocket README](#1-nukeserversocket-readme)
- - [1.1. 1.0.0 Release](#11-100-release)
- - [1.2. Features](#12-features)
- - [1.3. Client applications](#13-client-applications)
- - [1.3.1. Create a custom client](#131-create-a-custom-client)
- - [1.4. Installation](#14-installation)
- - [1.5. Usage](#15-usage)
- - [1.6. Settings](#16-settings)
- - [1.7. Known Issues](#17-known-issues)
- - [1.8. Compatibility](#18-compatibility)
- - [1.9. Contributing](#19-contributing)
-
-## 1.1. 1.0.0 Release
-
-This is the initial stable version of nukeserversocket. It's a total rewrite of the earlier version with the primary goal to enhance stability and simplify maintenance. Now, the plugin is more flexible and straightforward to use in different applications.
-
-For a full list of changes, see the [CHANGELOG](https://github.com/sisoe24/nukeserversocket/blob/main/CHANGELOG.md)
+ - [1.1. Client applications](#11-client-applications)
+ - [1.1.1. Create a custom client](#111-create-a-custom-client)
+ - [1.2. Installation](#12-installation)
+ - [1.2.1. Nuke](#121-nuke)
+ - [1.2.1.1. Using NukeTools (Recommended)](#1211-using-nuketools-recommended)
+ - [1.2.1.2. Manual Installation](#1212-manual-installation)
+ - [1.2.2. Houdini Installation](#122-houdini-installation)
+ - [1.2.2.1. HOUDINI\_PACKAGE\_DIR](#1221-houdini_package_dir)
+ - [1.2.2.2. Using Houdini Preferences](#1222-using-houdini-preferences)
+ - [1.2.3. Houdini Notes](#123-houdini-notes)
+ - [1.3. Usage](#13-usage)
+ - [1.4. Settings](#14-settings)
+ - [1.5. Known Issues](#15-known-issues)
+ - [1.6. Compatibility](#16-compatibility)
+ - [1.7. Python2.7](#17-python27)
+ - [1.8. Contributing](#18-contributing)
>[!IMPORTANT]
-> The repository name has changed from `NukeServerSocket` to `nukeserversocket`. Although GitHub url seems to be case insensitive, if you have cloned the repository before, you might need to update the remote url.
-> ```bash
-> git remote set-url origin https://github.com/sisoe24/nukeserversocket.git
-> ```
-
->[!NOTE]
->If you are using Nuke 12 or Python 2.7, you can still use the previous version of the plugin `<=0.6.2` from the [releases page](https://github.com/sisoe24/nukeserversocket/releases/tag/v0.6.2)
----
-
-## 1.2. Features
+> You can now execute code for Houdini! See installation notes
-- Receive Python or BlinkScript code from any client in your local network.
-- Connect more than one client to the same Nuke instance.
+A PySide2 plugin for executing Python/BlinkScript remotely in Nuke from any network client, supporting multiple connections. Compatible with both Nuke and Houdini.
-## 1.3. Client applications
+## 1.1. Client applications
Client applications that use nukeserversocket:
@@ -59,7 +48,7 @@ Client applications that use nukeserversocket:
- [Nuke Tools ST](https://packagecontrol.io/packages/NukeToolsST) - Sublime Text package.
- [DCC WebSocket](https://marketplace.visualstudio.com/items?itemName=virgilsisoe.dcc-websocket) - Visual Studio Code Web extension (deprecated at the moment).
-### 1.3.1. Create a custom client
+### 1.1.1. Create a custom client
You can create a custom client in any programming language that supports socket communication. The client sends the code to the server, which then executes it in Nuke and sends back the result. For more information, see the [wiki page](https://github.com/sisoe24/nukeserversocket/wiki/Client-Applications-for-NukeServerSocket)
@@ -79,20 +68,70 @@ for node in nodes:
print(node)
```
-## 1.4. Installation
+## 1.2. Installation
+
+### 1.2.1. Nuke
+
+#### 1.2.1.1. Using NukeTools (Recommended)
+
+If you use [Nuke Tools](https://marketplace.visualstudio.com/items?itemName=virgilsisoe.nuke-tools), simply run the command `Nuke: Add Packages` to install.
+
+#### 1.2.1.2. Manual Installation
+
+1. Download from [releases page](https://github.com/sisoe24/nukeserversocket/releases) or clone from GitHub
+2. Place in *~/.nuke* or your preferred directory
+3. Add to your *menu.py*:
+```python
+from nukeserversocket import nukeserversocket
+nukeserversocket.install_nuke()
+```
+
+### 1.2.2. Houdini Installation
+
+> **Note:** These instructions assume NukeServerSocket was installed via NukeTools in `$HOME/.nuke/NukeTools`. If installed manually, adjust paths accordingly. Also, you dont need to have Nuke installed to make this work.
+
+#### 1.2.2.1. HOUDINI_PACKAGE_DIR
+
+Add to your shell configuration:
+
+**Mac/Linux** (.bashrc or .zshrc):
+```bash
+export HOUDINI_PACKAGE_DIR=$HOME/.nuke/NukeTools/nukeserversocket
+```
-1. Download the repository via the [releases page](https://github.com/sisoe24/nukeserversocket/releases) or by cloning it from GitHub.
-2. Place the folder inside the _~/.nuke_ directory or into a custom one.
-3. Then, in your _menu.py_, write
- ```python
- from nukeserversocket import nukeserversocket
- nukeserversocket.install_nuke()
- ```
+**Windows:**
+- Add `HOUDINI_PACKAGE_DIR` to Environment Variables (start menu)
+- Set value to `%USERPROFILE%\.nuke\NukeTools\nukeserversocket`
>[!NOTE]
-> If you use [Nuke Tools](https://marketplace.visualstudio.com/items?itemName=virgilsisoe.nuke-tools), use the command `Nuke: Add Packages` then select nukeServerSocket.
+>For CMD/PowerShell users, refer to Microsoft's documentation on environment variables for alternative setup methods.
+
+#### 1.2.2.2. Using Houdini Preferences
+
+1. Navigate to your Houdini packages directory:
+ - Windows: `C:/Users/YourName/Documents/houdiniXX.X/packages`
+ - Mac: `~/Library/Preferences/houdini/XX.X/packages`
+ - Linux: `~/houdiniXX.X/packages`
+
+2. Create `nukeserversocket.json`:
+```json
+{
+ "hpath": [
+ "$HOME/.nuke/NukeTools/nukeserversocket/nukeserversocket/controllers/houdini"
+ ],
+ "env": [
+ {
+ "PYTHONPATH": "$HOME/.nuke/NukeTools/nukeserversocket"
+ }
+ ]
+}
+```
+
+### 1.2.3. Houdini Notes
+
+The Houdini execution method differs from Nuke's. Nuke relies on its internal script editor, while Houdini uses Python's `exec`. The Nuke controller also used to rely on `exec`, but I removed that functionality because it didn't work in all scenarios. If you have better suggestions, let me know!
-## 1.5. Usage
+## 1.3. Usage
![Execute Code](images/run_code.gif)
@@ -102,7 +141,10 @@ for node in nodes:
>[!NOTE]
> If you receive a message: "_Server did not initiate. Error: The bound address is already in use_", change the **port** to a random number between `49152` and `65535` and try again.
-## 1.6. Settings
+## 1.4. Settings
+
+>[!NOTE]
+> Only the server timeout setting applies to Houdini.
You can access the settings from the plugin toolbar.
@@ -120,27 +162,20 @@ You can access the settings from the plugin toolbar.
- **Clear Output**: The script editor output window will clear the code after each execution.
- **Server Timeout**: Set the Timeout when clicking the **Connect** button. The default value is `10` minutes.
-## 1.7. Known Issues
+## 1.5. Known Issues
- Changing workspace with an active open connection makes Nuke load a new plugin instance with the default UI state. So it would look as if the previous connection has been closed, whereas in reality is still open and listening. To force close all of the listening connections, you can:
- Restart the Nuke instance.
- Wait for the connection timeout.
-## 1.8. Compatibility
-
-Nuke version: 13, 14, 15
+## 1.6. Compatibility
-While it should work the same on all platforms, I have tested the plugin only on:
+Should work everywhere PySide2 and Python 3 work.
-- Linux:
- - CentOS 8
-- macOS:
- - Mojave 10.14.06
- - Catalina 10.15.07
- - Monterey 12.6.3
-- Windows 10
+## 1.7. Python2.7
+If you are using Python 2.7, you can still use the previous version of the plugin `<=0.6.2` from the [releases page](https://github.com/sisoe24/nukeserversocket/releases/tag/v0.6.2)
-## 1.9. Contributing
+## 1.8. Contributing
If you have any suggestions, bug reports, or questions, feel free to open an issue or a pull request. I am always open to new ideas and improvements. Occasionally, I pick something from the [Projects](https://github.com/users/sisoe24/projects/4) tab, so feel free to check it out.
diff --git a/houdini_package.json b/houdini_package.json
new file mode 100644
index 00000000..519d6493
--- /dev/null
+++ b/houdini_package.json
@@ -0,0 +1,10 @@
+{
+ "hpath": [
+ "$HOUDINI_PACKAGE_PATH/nukeserversocket/controllers/houdini"
+ ],
+ "env": [
+ {
+ "PYTHONPATH": "$HOUDINI_PACKAGE_PATH"
+ }
+ ]
+}
diff --git a/nukeserversocket/__init__.py b/nukeserversocket/__init__.py
index e5d3319e..158b2998 100644
--- a/nukeserversocket/__init__.py
+++ b/nukeserversocket/__init__.py
@@ -1,4 +1,3 @@
-"""Module will initialize the logging system and import Nuke."""
from __future__ import annotations
from .main import NukeServerSocket
diff --git a/nukeserversocket/console.py b/nukeserversocket/console.py
index e4d9c8ff..09fa6cba 100644
--- a/nukeserversocket/console.py
+++ b/nukeserversocket/console.py
@@ -2,10 +2,11 @@
import sys
import logging
+from typing import Optional
from PySide2.QtCore import Slot
-from PySide2.QtWidgets import (QCheckBox, QGroupBox, QHBoxLayout, QPushButton,
- QVBoxLayout, QPlainTextEdit)
+from PySide2.QtWidgets import (QWidget, QCheckBox, QGroupBox, QHBoxLayout,
+ QPushButton, QVBoxLayout, QPlainTextEdit)
from .logger import get_logger
@@ -21,7 +22,7 @@
class NssConsole(QGroupBox):
- def __init__(self, parent=None):
+ def __init__(self, parent: Optional[QWidget] = None):
super().__init__(parent, title='Logs')
self._console = QPlainTextEdit()
diff --git a/nukeserversocket/editor_controller.py b/nukeserversocket/controllers/base.py
similarity index 86%
rename from nukeserversocket/editor_controller.py
rename to nukeserversocket/controllers/base.py
index e0dc7d05..0a70ce5a 100644
--- a/nukeserversocket/editor_controller.py
+++ b/nukeserversocket/controllers/base.py
@@ -7,9 +7,9 @@
from PySide2.QtWidgets import QTextEdit, QPlainTextEdit
-from .logger import get_logger
-from .settings import _NssSettings
-from .received_data import ReceivedData
+from ..logger import get_logger
+from ..settings import _NssSettings
+from ..received_data import ReceivedData
LOGGER = get_logger()
@@ -43,9 +43,7 @@ def format_output(file: str, text: str, string_format: str) -> str:
return string_format
-class EditorController(ABC):
- history: List[str] = []
-
+class BaseController(ABC):
def __init__(self):
self._settings = None
@@ -59,6 +57,19 @@ def settings(self) -> _NssSettings:
def settings(self, settings: _NssSettings) -> None:
self._settings = settings
+ @abstractmethod
+ def execute(self, data: ReceivedData) -> str: ...
+
+
+class EditorController(BaseController):
+ history: List[str] = []
+
+ def __init__(self):
+ super().__init__()
+
+ @abstractmethod
+ def execute_code(self) -> None: ...
+
@property
@abstractmethod
def input_editor(self) -> QPlainTextEdit: ...
@@ -67,9 +78,6 @@ def input_editor(self) -> QPlainTextEdit: ...
@abstractmethod
def output_editor(self) -> QTextEdit: ...
- @abstractmethod
- def execute(self) -> None: ...
-
@classmethod
def _add_to_history(cls, text: str) -> None:
cls.history.append(text)
@@ -84,7 +92,7 @@ def _process_output(self, data: ReceivedData, result: str) -> str:
output = result
if self.settings.get('clear_output'):
- LOGGER.debug('Clearing output.')
+ LOGGER.debug('Clean up output.')
self.history.clear()
self.output_editor.setPlainText(output)
else:
@@ -103,15 +111,15 @@ def get_output(self) -> str:
def set_input(self, data: ReceivedData) -> None:
self.input_editor.setPlainText(data.text)
- def run(self, data: ReceivedData) -> str:
+ def execute(self, data: ReceivedData) -> str:
- LOGGER.debug('Running script: %s', data.file)
+ LOGGER.debug('Executing data')
initial_input = self.input_editor.toPlainText()
initial_output = self.output_editor.toPlainText()
self.set_input(data)
- self.execute()
+ self.execute_code()
result = self.get_output()
diff --git a/nukeserversocket/controllers/houdini/python_panels/nukeserversocket.pypanel b/nukeserversocket/controllers/houdini/python_panels/nukeserversocket.pypanel
new file mode 100644
index 00000000..52961002
--- /dev/null
+++ b/nukeserversocket/controllers/houdini/python_panels/nukeserversocket.pypanel
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
diff --git a/nukeserversocket/controllers/houdini/scripts/python/app/main.py b/nukeserversocket/controllers/houdini/scripts/python/app/main.py
new file mode 100644
index 00000000..c0006a98
--- /dev/null
+++ b/nukeserversocket/controllers/houdini/scripts/python/app/main.py
@@ -0,0 +1,21 @@
+"""Nuke-specific plugin for the NukeServerSocket."""
+from __future__ import annotations
+
+from typing import Optional
+
+from PySide2.QtWidgets import QWidget
+
+from nukeserversocket.main import NukeServerSocket
+from nukeserversocket.utils import exec_code
+from nukeserversocket.received_data import ReceivedData
+from nukeserversocket.controllers.base import BaseController
+
+
+class HoudiniController(BaseController):
+ def execute(self, data: ReceivedData) -> str:
+ return exec_code(data.text, data.file)
+
+
+class HoudiniEditor(NukeServerSocket):
+ def __init__(self, parent: Optional[QWidget] = None):
+ super().__init__(HoudiniController(), parent)
diff --git a/nukeserversocket/controllers/local_app.py b/nukeserversocket/controllers/local.py
similarity index 53%
rename from nukeserversocket/controllers/local_app.py
rename to nukeserversocket/controllers/local.py
index 6e9a8d59..f5949daa 100644
--- a/nukeserversocket/controllers/local_app.py
+++ b/nukeserversocket/controllers/local.py
@@ -6,15 +6,16 @@
from __future__ import annotations
import sys
-import traceback
+import json
from PySide2.QtWidgets import (QLabel, QWidget, QTextEdit, QHBoxLayout,
- QPushButton, QSizePolicy, QApplication,
- QPlainTextEdit)
+ QPushButton, QSizePolicy, QVBoxLayout,
+ QApplication, QPlainTextEdit)
+from .base import EditorController
from ..main import NukeServerSocket
-from ..utils import stdoutIO
-from ..editor_controller import EditorController
+from ..utils import exec_code
+from ..received_data import ReceivedData
class LocalController(EditorController):
@@ -23,6 +24,7 @@ def __init__(self):
self._input_editor = QPlainTextEdit()
self._input_editor.setPlaceholderText('Enter your code here...')
self._input_editor.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Maximum)
+ self._input_editor.setPlainText('print(hello world".upper())')
self._output_editor = QTextEdit()
self._output_editor.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Maximum)
@@ -37,37 +39,46 @@ def input_editor(self) -> QPlainTextEdit:
def output_editor(self) -> QTextEdit:
return self._output_editor
- def execute(self) -> None:
- with stdoutIO() as s:
- try:
- exec(self.input_editor.toPlainText())
- except Exception:
- result = traceback.format_exc()
- else:
- result = s.getvalue()
-
- self.output_editor.setPlainText(result)
+ def execute_code(self) -> None:
+ self.output_editor.setPlainText(
+ exec_code(self.input_editor.toPlainText(), '')
+ )
class LocalEditor(NukeServerSocket):
def __init__(self):
super().__init__(LocalController())
- run_button = QPushButton('Run')
- run_button.clicked.connect(self.editor.execute)
- run_button.setShortcut('Ctrl+R')
+ execute_button = QPushButton('Execute')
+ execute_button.clicked.connect(self._execute)
+ execute_button.setShortcut('Ctrl+R')
+
+ input_layout = QVBoxLayout()
+ input_layout.addWidget(QLabel('Input Editor'))
+ input_layout.addWidget(self.editor.input_editor)
+
+ output_layout = QVBoxLayout()
+ output_layout.addWidget(QLabel('Output Editor'))
+ output_layout.addWidget(self.editor.output_editor)
lower_layout = QHBoxLayout()
- lower_layout.addWidget(self.editor.input_editor)
- lower_layout.addWidget(self.editor.output_editor)
+ lower_layout.addLayout(input_layout)
+ lower_layout.addLayout(output_layout)
lower_widget = QWidget()
lower_widget.setLayout(lower_layout)
main_layout = self.view.layout()
- main_layout.addWidget(QLabel('Local Editor
'))
+ main_layout.addWidget(QLabel('Mock Editor
'))
+ main_layout.addWidget(execute_button)
main_layout.addWidget(lower_widget)
- main_layout.addWidget(run_button)
+
+ execute_button.click()
+
+ def _execute(self):
+ self.editor.execute(ReceivedData(
+ json.dumps({'text': self.editor.input_editor.toPlainText()})
+ ))
def main():
diff --git a/nukeserversocket/controllers/nuke.py b/nukeserversocket/controllers/nuke.py
index 852c65ba..d7059943 100644
--- a/nukeserversocket/controllers/nuke.py
+++ b/nukeserversocket/controllers/nuke.py
@@ -4,26 +4,21 @@
import os
import json
import logging
+from typing import Optional
from textwrap import dedent
-from dataclasses import dataclass
from PySide2.QtWidgets import (QWidget, QSplitter, QTextEdit, QPushButton,
QApplication, QPlainTextEdit)
+from .base import EditorController
from ..main import NukeServerSocket
from ..utils import cache
from ..received_data import ReceivedData
-from ..editor_controller import EditorController
LOGGER = logging.getLogger('nukeserversocket')
-@dataclass(init=False)
-class Editor:
-
- input_editor: QPlainTextEdit
- output_editor: QTextEdit
- run_button: QPushButton
+class NukeScriptEditor:
def __init__(self):
self.input_editor = self.get_input_editor()
@@ -51,7 +46,7 @@ def get_output_editor(self) -> QTextEdit:
@cache('nuke')
def get_run_button(self) -> QPushButton:
- return next(
+ btn = next(
(
button
for button in self.get_script_editor().findChildren(QPushButton)
@@ -59,13 +54,20 @@ def get_run_button(self) -> QPushButton:
),
None,
)
+ if not btn:
+ # likely will never going to happen
+ raise RuntimeError(
+ 'NukeServerSocket error: Could not find Nuke Script Editor execute button'
+ )
+
+ return btn
class NukeController(EditorController):
- def __init__(self, editor: Editor):
+ def __init__(self, editor: NukeScriptEditor):
self.editor = editor
- def execute(self):
+ def execute_code(self):
self.editor.run_button.click()
@property
@@ -104,8 +106,8 @@ def set_input(self, data: ReceivedData) -> None:
class NukeEditor(NukeServerSocket):
- def __init__(self, parent=None):
- super().__init__(NukeController(Editor()), parent)
+ def __init__(self, parent: Optional[QWidget] = None):
+ super().__init__(NukeController(NukeScriptEditor()), parent)
def install_nuke():
diff --git a/nukeserversocket/main.py b/nukeserversocket/main.py
index c0e771bf..3e74417f 100644
--- a/nukeserversocket/main.py
+++ b/nukeserversocket/main.py
@@ -2,7 +2,7 @@
from __future__ import annotations
import socket
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Optional
from PySide2.QtCore import Qt, Slot, QTimer
from PySide2.QtWidgets import (QLabel, QWidget, QSpinBox, QFormLayout,
@@ -18,7 +18,7 @@
if TYPE_CHECKING:
from .settings import _NssSettings
- from .editor_controller import EditorController
+ from .controllers.base import BaseController
LOGGER = get_logger()
@@ -50,7 +50,7 @@ def get_server_timeout(self):
class MainView(QWidget):
- def __init__(self, parent=None):
+ def __init__(self, parent: Optional[QWidget] = None):
"""Init method for MainWindowWidget."""
super().__init__(parent)
@@ -150,19 +150,13 @@ def _close_connection(self):
class NukeServerSocket(QMainWindow):
-
- def __init__(self, editor: EditorController, parent=None):
- """Init method for NukeServerSocket.
-
- NOTE: Most variables are bound to the class only to avoid garbage collection.
-
- """
-
+ def __init__(self, editor: BaseController, parent: Optional[QWidget] = None):
super().__init__(parent)
self.setWindowTitle('NukeServerSocket')
-
print(f'\nLoading NukeServerSocket: {__version__}')
+ # NOTE: Most variables are bound to the class only to avoid garbage collection.
+
self.settings = get_settings()
self.editor = editor
diff --git a/nukeserversocket/received_data.py b/nukeserversocket/received_data.py
index 1fb20478..2f3341cd 100644
--- a/nukeserversocket/received_data.py
+++ b/nukeserversocket/received_data.py
@@ -44,7 +44,7 @@ def __post_init__(self):
self.text = self.data.get('text', '')
if not self.text:
- LOGGER.critical('Data does not contain a text field.')
+ LOGGER.critical('Data has invalid text.')
self.file = self.data['file']
diff --git a/nukeserversocket/server.py b/nukeserversocket/server.py
index 88abb600..203abd48 100644
--- a/nukeserversocket/server.py
+++ b/nukeserversocket/server.py
@@ -1,15 +1,16 @@
from __future__ import annotations
-from typing import TYPE_CHECKING, Optional
+from typing import TYPE_CHECKING, Union, Optional
from PySide2.QtCore import Slot, Signal
from PySide2.QtNetwork import QTcpServer, QTcpSocket, QHostAddress
+from PySide2.QtWidgets import QWidget
from .logger import get_logger
from .received_data import ReceivedData
if TYPE_CHECKING:
- from .editor_controller import EditorController
+ from .controllers.base import BaseController
LOGGER = get_logger()
@@ -25,7 +26,7 @@ class NssServer(QTcpServer):
"""
on_data_received = Signal()
- def __init__(self, editor: EditorController, parent=None):
+ def __init__(self, editor: BaseController, parent: Optional[QWidget] = None):
super().__init__(parent)
self._editor = editor
@@ -46,7 +47,7 @@ def _on_socket_ready(self):
# parse the incoming data
data = ReceivedData(self._socket.readAll().data())
- output = self._editor.run(data)
+ output = self._editor.execute(data)
self.on_data_received.emit()
diff --git a/nukeserversocket/settings_ui.py b/nukeserversocket/settings_ui.py
index f90cfe09..02672f7c 100644
--- a/nukeserversocket/settings_ui.py
+++ b/nukeserversocket/settings_ui.py
@@ -6,7 +6,7 @@
from __future__ import annotations
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, Optional
from textwrap import dedent
from PySide2.QtCore import Slot
@@ -29,7 +29,7 @@ def set(self, key: str, value: Any):
class NssSettingsView(QWidget):
- def __init__(self, parent=None):
+ def __init__(self, parent: Optional[QWidget] = None):
super().__init__(parent)
self.setMinimumSize(400, 200)
diff --git a/nukeserversocket/toolbar.py b/nukeserversocket/toolbar.py
index 49af9f72..fd2f1ef7 100644
--- a/nukeserversocket/toolbar.py
+++ b/nukeserversocket/toolbar.py
@@ -3,7 +3,7 @@
import os
import sysconfig
import webbrowser
-from typing import Dict
+from typing import Dict, Optional
from platform import python_version
from PySide2 import __version__ as PySide2_version
@@ -21,12 +21,11 @@ def about() -> Dict[str, str]:
'python': python_version(),
'pyside': PySide2_version,
'machine': sysconfig.get_platform(),
- 'user': os.getenv('USER', os.getenv('USERNAME', 'unknown')),
}
class HelpWidget(QDialog):
- def __init__(self, parent=None):
+ def __init__(self, parent: Optional[QWidget] = None):
super().__init__(parent)
self.setWindowTitle('NukeServerSocket Help')
@@ -70,7 +69,7 @@ def _show_window(widget: QWidget) -> None:
class ToolBar(QToolBar):
"""Custom QToolBar class."""
- def __init__(self, parent=None):
+ def __init__(self, parent: Optional[QWidget] = None):
"""Init method for the ToolBar class."""
super().__init__(parent)
self.setMovable(False)
diff --git a/nukeserversocket/utils/__init__.py b/nukeserversocket/utils/__init__.py
index 6af66b2e..922cffa5 100644
--- a/nukeserversocket/utils/__init__.py
+++ b/nukeserversocket/utils/__init__.py
@@ -1,2 +1,2 @@
from .cache import cache, clear_cache
-from .stdout import stdoutIO
+from .exec_code import stdoutIO, exec_code
diff --git a/nukeserversocket/utils/exec_code.py b/nukeserversocket/utils/exec_code.py
new file mode 100644
index 00000000..6e5024c9
--- /dev/null
+++ b/nukeserversocket/utils/exec_code.py
@@ -0,0 +1,55 @@
+from __future__ import annotations
+
+import io
+import sys
+import traceback
+import contextlib
+from typing import Any, List, Generator
+
+
+@contextlib.contextmanager
+def stdoutIO(stdout: Any = None) -> Generator[Any, Any, None]:
+ """Get output from sys.stdout after executing code from `exec`.
+
+ ```
+ with stdoutIO() as s:
+ exec(text, globals())
+ return s.getvalue()
+ ```
+
+ ref: https://stackoverflow.com/a/3906390/9392852
+
+ """
+ old = sys.stdout
+ if stdout is None:
+ stdout = io.StringIO()
+ sys.stdout = stdout
+ yield stdout
+ sys.stdout = old
+
+
+def exec_code(input_text: str, filename: str = '') -> str:
+ """Execute code with exec and returns its output.
+
+ Accepts an optional filename argument for the source file is executing if
+ there is an exception.
+
+ ```
+ result = exec_code("print('hello'.upper())")
+ ```
+
+ """
+ with stdoutIO() as s:
+ try:
+ code_object = compile(input_text, filename, 'exec')
+ exec(code_object, globals())
+ except Exception:
+ exc_type, exc_value, exc_traceback = sys.exc_info()
+ tb_lines = traceback.format_exception(exc_type, exc_value, exc_traceback)
+
+ # remove the codebase line exec
+ lines: List[str] = [
+ line for line in tb_lines if f'File "{__file__}"' not in line
+ ]
+ return ''.join(lines)
+ return s.getvalue()
diff --git a/nukeserversocket/utils/stdout.py b/nukeserversocket/utils/stdout.py
deleted file mode 100644
index a9d7b2db..00000000
--- a/nukeserversocket/utils/stdout.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from __future__ import annotations
-
-import io
-import sys
-import contextlib
-from typing import Any, Generator
-
-
-@contextlib.contextmanager
-def stdoutIO(stdout: Any = None) -> Generator[Any, Any, None]:
- """Get output from sys.stdout after executing code from `exec`.
-
- https://stackoverflow.com/a/3906390/9392852
-
- """
- old = sys.stdout
- if stdout is None:
- stdout = io.StringIO()
- sys.stdout = stdout
- yield stdout
- sys.stdout = old
diff --git a/nukeserversocket/version.py b/nukeserversocket/version.py
index 1a72d32e..58d478ab 100644
--- a/nukeserversocket/version.py
+++ b/nukeserversocket/version.py
@@ -1 +1 @@
-__version__ = '1.1.0'
+__version__ = '1.2.0'
diff --git a/pyproject.toml b/pyproject.toml
index ab447a36..9748b1f6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,11 +1,11 @@
[tool.poetry]
name = "nukeserversocket"
-version = "1.1.0"
+version = "1.2.0"
description = "A Nuke plugin that will allow code execution from the local network via TCP/WebSocket connections and more."
authors = ["virgilsisoe "]
[tool.poetry.scripts]
-nukeserversocket = "nukeserversocket.controllers.local_app:main"
+nukeserversocket = "nukeserversocket.controllers.local:main"
build = "scripts.release_manager:main"
[tool.isort]
diff --git a/tests/test_editor_controller.py b/tests/test_editor_controller.py
index 97724613..fcb9df5a 100644
--- a/tests/test_editor_controller.py
+++ b/tests/test_editor_controller.py
@@ -10,7 +10,7 @@
from nukeserversocket.settings import _NssSettings
from nukeserversocket.received_data import ReceivedData
-from nukeserversocket.editor_controller import EditorController, format_output
+from nukeserversocket.controllers.base import EditorController, format_output
class MockEditorController(EditorController):
@@ -31,7 +31,7 @@ def input_editor(self):
def output_editor(self):
return self._output_editor
- def execute(self):
+ def execute_code(self):
self._output_editor.setPlainText('hello world\n')
@@ -54,7 +54,7 @@ def data() -> ReceivedData:
def test_execute_no_mirror(editor: MockEditorController, data: ReceivedData):
- result = editor.run(data)
+ result = editor.execute(data)
assert result == 'hello world\n'
assert editor.output_editor.toPlainText() == 'initial output'
assert editor.input_editor.toPlainText() == 'initial input'
@@ -63,10 +63,10 @@ def test_execute_no_mirror(editor: MockEditorController, data: ReceivedData):
def test_execute_mirror(editor: MockEditorController, data: ReceivedData):
editor.settings.set('mirror_script_editor', True)
- with patch('nukeserversocket.editor_controller.datetime') as mock_datetime:
+ with patch('nukeserversocket.controllers.base.datetime') as mock_datetime:
mock_datetime.now.return_value = datetime(2000, 1, 1, 0, 0, 0)
- result = editor.run(data)
+ result = editor.execute(data)
output = '[00:00:00 NukeTools] test.py\nhello world\n'
assert result == output
@@ -78,7 +78,7 @@ def test_execute_mirror_no_output_format(editor: MockEditorController, data: Rec
editor.settings.set('mirror_script_editor', True)
editor.settings.set('format_output', '')
- result = editor.run(data)
+ result = editor.execute(data)
assert result == 'hello world\n'
assert editor.output_editor.toPlainText() == 'hello world\n'
assert editor.input_editor.toPlainText() == 'print("hello world")'
@@ -89,8 +89,8 @@ def test_execute_no_clear_output(editor: MockEditorController, data: ReceivedDat
editor.settings.set('format_output', '')
editor.settings.set('clear_output', False)
- editor.run(data)
- editor.run(data)
+ editor.execute(data)
+ editor.execute(data)
assert editor.output_editor.toPlainText() == 'hello world\n\nhello world\n\n'
assert editor.history == ['hello world\n', 'hello world\n']
@@ -101,8 +101,8 @@ def test_execute_clear_output(editor: MockEditorController, data: ReceivedData):
editor.settings.set('format_output', '')
editor.settings.set('clear_output', True)
- editor.run(data)
- editor.run(data)
+ editor.execute(data)
+ editor.execute(data)
assert editor.output_editor.toPlainText() == 'hello world\n'
assert editor.history == []
@@ -117,7 +117,7 @@ def test_execute_clear_output(editor: MockEditorController, data: ReceivedData):
('path/test.py', 'hello world', '%d %f %F %t %n', '00:00:00 path/test.py test.py hello world \n'),
])
def test_formatting_placeholders(file: str, text: str, format: str, expected: str):
- with patch('nukeserversocket.editor_controller.datetime') as mock_datetime:
+ with patch('nukeserversocket.controllers.base.datetime') as mock_datetime:
mock_datetime.now.return_value = datetime(2000, 1, 1, 0, 0, 0)
output = format_output(file, text, format)
assert output == expected
diff --git a/tests/test_exec_code.py b/tests/test_exec_code.py
new file mode 100644
index 00000000..19d4b876
--- /dev/null
+++ b/tests/test_exec_code.py
@@ -0,0 +1,49 @@
+from __future__ import annotations
+
+import io
+
+from nukeserversocket.utils import stdoutIO, exec_code
+
+
+def test_stdoutIO_captures_output():
+ with stdoutIO() as s:
+ print('Hello, World!')
+ output = s.getvalue()
+ assert output == 'Hello, World!\n'
+
+
+def test_stdoutIO_custom_stdout():
+ custom_stdout = io.StringIO()
+ with stdoutIO(custom_stdout) as s:
+ print('Custom stdout test')
+ output = s.getvalue()
+ assert output == 'Custom stdout test\n'
+ assert custom_stdout is s
+
+
+def test_exec_code_print_statement():
+ result = exec_code("print('Hello from exec_code')")
+ assert result == 'Hello from exec_code\n'
+
+
+def test_exec_code_variable_assignment():
+ result = exec_code('a = 5\nprint(a)')
+ assert result == '5\n'
+
+
+def test_exec_code_runtime_error():
+ result = exec_code('1/0')
+ assert 'ZeroDivisionError' in result
+ assert 'division by zero' in result
+
+
+def test_exec_code_exception_traceback_excludes_current_file():
+ result = exec_code("raise ValueError('Test error')")
+ assert 'ValueError: Test error' in result
+ assert f'File "{__file__}"' not in result
+
+
+def test_exec_code_custom_filename_in_exception():
+ result = exec_code("raise IndexError('Index error')", filename='custom_script.py')
+ assert 'IndexError: Index error' in result
+ assert 'File "custom_script.py"' in result
diff --git a/tests/test_main.py b/tests/test_main.py
index fc8f286a..8dcc2eeb 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -1,14 +1,16 @@
from __future__ import annotations
+from typing import Optional
+
import pytest
from PySide2.QtCore import Signal, QObject
from pytestqt.qtbot import QtBot
-from PySide2.QtWidgets import QTextEdit, QPlainTextEdit
+from PySide2.QtWidgets import QWidget, QTextEdit, QPlainTextEdit
from nukeserversocket.main import MainView, MainModel, MainController
from nukeserversocket.server import NssServer
from nukeserversocket.settings import _NssSettings
-from nukeserversocket.editor_controller import EditorController
+from nukeserversocket.controllers.base import EditorController
Controller = MainController
View = MainView
@@ -32,14 +34,14 @@ def input_editor(self):
def output_editor(self):
return self._output_editor
- def execute(self):
+ def execute_code(self):
pass
class MockServer(QObject):
on_data_received = Signal()
- def __init__(self, editor: MockEditorController, parent=None):
+ def __init__(self, editor: MockEditorController, parent: Optional[QWidget] = None):
super().__init__(parent)
def try_connect(self, port: int) -> bool:
diff --git a/tests/test_nuke.py b/tests/test_nuke.py
index 955db78c..e1634707 100644
--- a/tests/test_nuke.py
+++ b/tests/test_nuke.py
@@ -4,46 +4,53 @@
from textwrap import dedent
import pytest
+from pytestqt.qtbot import QtBot
from PySide2.QtWidgets import QTextEdit, QPushButton, QPlainTextEdit
from nukeserversocket.settings import _NssSettings
from nukeserversocket.received_data import ReceivedData
-from nukeserversocket.controllers.nuke import Editor, NukeController
+from nukeserversocket.controllers.nuke import NukeController, NukeScriptEditor
-# pytestmark = pytest.mark.quick
-
-class NukeEditor(Editor):
+class MockNukeEditor(NukeScriptEditor):
def __init__(self):
self.input_editor = QPlainTextEdit()
self.output_editor = QTextEdit()
self.run_button = QPushButton()
+ self.run_button.clicked.connect(self._eval_input)
+
+ def _eval_input(self):
+ # we dont eval any code since we dont really care
+ r = self.input_editor.toPlainText()
+ self.output_editor.setPlainText(f"# Result: {r}")
-def test_nuke_python(qtbot, mock_settings):
- data = ReceivedData('{"file": "test.py", "text": "print(\\"hello world\\")"}')
- editor = NukeController(NukeEditor())
+def test_nuke_python(qtbot: QtBot, mock_settings: _NssSettings):
+ d = json.dumps({'file': 'test.py', 'text': 'print("hello world")'})
+ data = ReceivedData(d)
+ editor = NukeController(MockNukeEditor())
editor.settings = mock_settings
editor.settings.set('mirror_script_editor', True)
- editor.run(data)
+ out = editor.execute(data)
assert editor.editor.input_editor.toPlainText() == 'print("hello world")'
+ assert 'hello world' in out
@pytest.mark.parametrize('file, text', (
('test.cpp', 'blinkscript'),
('test.blink', 'blinkscript'),
))
-def test_nuke_blinkscript(qtbot, mock_settings: _NssSettings, file: str, text: str):
+def test_nuke_blinkscript(qtbot: QtBot, mock_settings: _NssSettings, file: str, text: str):
d = json.dumps({'file': file, 'text': text})
data = ReceivedData(d)
- editor = NukeController(NukeEditor())
+ editor = NukeController(MockNukeEditor())
editor.settings = mock_settings
editor.settings.set('mirror_script_editor', True)
- editor.run(data)
+ editor.execute(data)
assert editor.editor.input_editor.toPlainText() == dedent(f"""
node = nuke.toNode("test") or nuke.createNode('BlinkScript', 'name test')
diff --git a/tests/test_server.py b/tests/test_server.py
index da8874ce..ba98e527 100644
--- a/tests/test_server.py
+++ b/tests/test_server.py
@@ -8,15 +8,13 @@
from pytestqt.qtbot import QtBot
from PySide2.QtWidgets import QTextEdit, QPlainTextEdit
-from nukeserversocket.utils import stdoutIO
+from nukeserversocket.utils import exec_code
from nukeserversocket.server import NssServer
from nukeserversocket.settings import _NssSettings
-from nukeserversocket.editor_controller import EditorController
+from nukeserversocket.controllers.base import EditorController
PORT = 55559
-pytestmark = pytest.mark.quick
-
class MockEditorController(EditorController):
def __init__(self):
@@ -36,11 +34,10 @@ def input_editor(self):
def output_editor(self):
return self._output_editor
- def execute(self) -> None:
- with stdoutIO() as s:
- exec(self.input_editor.toPlainText())
- result = s.getvalue()
- self.output_editor.setPlainText(result)
+ def execute_code(self) -> None:
+ self.output_editor.setPlainText(
+ exec_code(self.input_editor.toPlainText())
+ )
@pytest.fixture()
diff --git a/scripts/release_manager.py b/tools/release_manager.py
similarity index 100%
rename from scripts/release_manager.py
rename to tools/release_manager.py
diff --git a/tox.ini b/tox.ini
index 6dc80712..bb588742 100644
--- a/tox.ini
+++ b/tox.ini
@@ -2,6 +2,15 @@
isolated_build = True
envlist = py{37,39,310}
+[testenv:py37]
+deps =
+ # latest 3.7 compatible
+ typing_extensions==4.7.1
+ poetry
+commands =
+ poetry install
+ poetry run pytest -q --count=10
+
[testenv]
description = run unit tests
deps =