diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9eaf1ffb..8e29a957 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
# Changelog
+## [1.1.0] - 2024-08-18
+
+### Added
+
+* Added color to logs
+
+### Fixed
+
+* [[#30](https://github.com/sisoe24/nukeserversocket/issues/30)]
+* [#31](https://github.com/sisoe24/nukeserversocket/issues/31)
+* [#32](https://github.com/sisoe24/nukeserversocket/issues/32)
+* Logs font now use proper monospace font
## [1.0.0] - 2023-11-19
diff --git a/README.md b/README.md
index 01552d7f..a162d545 100644
--- a/README.md
+++ b/README.md
@@ -55,6 +55,7 @@ For a full list of changes, see the [CHANGELOG](https://github.com/sisoe24/nukes
Client applications that use nukeserversocket:
- [Nuke Tools](https://marketplace.visualstudio.com/items?itemName=virgilsisoe.nuke-tools) - Visual Studio Code extension.
+- [nuketools.nvim](https://github.com/sisoe24/nuketools.nvim) - Neovim plugin.
- [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).
@@ -62,6 +63,22 @@ Client applications that use nukeserversocket:
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)
+```py
+# ... your socket code
+data = {
+ "text": "print([n.name() for n in nuke.allNodes()])",
+ "file" : "path/to/file.py",
+ "formatText": "0"
+}
+s.sendall(bytearray(json.dumps(data), 'utf-8'))
+data = s.recv(1024)
+s.close()
+
+nodes = json.loads(data.decode('utf-8').replace("'", '"'))
+for node in nodes:
+ print(node)
+```
+
## 1.4. Installation
1. Download the repository via the [releases page](https://github.com/sisoe24/nukeserversocket/releases) or by cloning it from GitHub.
diff --git a/nukeserversocket/console.py b/nukeserversocket/console.py
index bf9192e7..e4d9c8ff 100644
--- a/nukeserversocket/console.py
+++ b/nukeserversocket/console.py
@@ -1,23 +1,39 @@
from __future__ import annotations
+import sys
import logging
from PySide2.QtCore import Slot
from PySide2.QtWidgets import (QCheckBox, QGroupBox, QHBoxLayout, QPushButton,
QVBoxLayout, QPlainTextEdit)
-from .utils import cache
from .logger import get_logger
LOGGER = get_logger()
+LOG_COLORS = {
+ 'DEBUG': 'aqua',
+ 'INFO': 'white',
+ 'WARNING': 'orange',
+ 'ERROR': 'red',
+ 'CRITICAL': 'magenta',
+}
+
class NssConsole(QGroupBox):
def __init__(self, parent=None):
super().__init__(parent, title='Logs')
self._console = QPlainTextEdit()
- self._console.setStyleSheet('font-family: menlo;')
+
+ if sys.platform == 'darwin':
+ font = 'menlo'
+ elif sys.platform == 'win32':
+ font = 'consolas'
+ else:
+ font = 'monospace'
+
+ self._console.setStyleSheet(f'font-family: {font};')
self._console.setReadOnly(True)
self._console.setLineWrapMode(QPlainTextEdit.NoWrap)
@@ -50,8 +66,10 @@ def __init__(self, parent=None):
def _on_enable_debug(self, state: int) -> None:
LOGGER.console.setLevel(logging.DEBUG if state == 2 else logging.INFO)
- def write(self, text: str) -> None:
- self._console.insertPlainText(text)
+ def write(self, text: str, level_name: str = 'INFO') -> None:
+ color = LOG_COLORS.get(level_name, 'white')
+ text = text.replace(' ', ' ')
+ self._console.appendHtml(f'{text}')
self._console.verticalScrollBar().setValue(
self._console.verticalScrollBar().maximum()
)
diff --git a/nukeserversocket/controllers/local_app.py b/nukeserversocket/controllers/local_app.py
index 5ad16cca..6e9a8d59 100644
--- a/nukeserversocket/controllers/local_app.py
+++ b/nukeserversocket/controllers/local_app.py
@@ -6,8 +6,10 @@
from __future__ import annotations
import sys
+import traceback
-from PySide2.QtWidgets import (QLabel, QTextEdit, QPushButton, QApplication,
+from PySide2.QtWidgets import (QLabel, QWidget, QTextEdit, QHBoxLayout,
+ QPushButton, QSizePolicy, QApplication,
QPlainTextEdit)
from ..main import NukeServerSocket
@@ -19,8 +21,12 @@ class LocalController(EditorController):
def __init__(self):
super().__init__()
self._input_editor = QPlainTextEdit()
+ self._input_editor.setPlaceholderText('Enter your code here...')
+ self._input_editor.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Maximum)
self._output_editor = QTextEdit()
+ self._output_editor.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Maximum)
+ self._output_editor.setPlaceholderText('Output will be shown here...')
self._output_editor.setReadOnly(True)
@property
@@ -33,8 +39,13 @@ def output_editor(self) -> QTextEdit:
def execute(self) -> None:
with stdoutIO() as s:
- exec(self.input_editor.toPlainText())
- result = s.getvalue()
+ try:
+ exec(self.input_editor.toPlainText())
+ except Exception:
+ result = traceback.format_exc()
+ else:
+ result = s.getvalue()
+
self.output_editor.setPlainText(result)
@@ -46,10 +57,16 @@ def __init__(self):
run_button.clicked.connect(self.editor.execute)
run_button.setShortcut('Ctrl+R')
+ lower_layout = QHBoxLayout()
+ lower_layout.addWidget(self.editor.input_editor)
+ lower_layout.addWidget(self.editor.output_editor)
+
+ lower_widget = QWidget()
+ lower_widget.setLayout(lower_layout)
+
main_layout = self.view.layout()
main_layout.addWidget(QLabel('
Local Editor
'))
- main_layout.addWidget(self.editor.output_editor)
- main_layout.addWidget(self.editor.input_editor)
+ main_layout.addWidget(lower_widget)
main_layout.addWidget(run_button)
diff --git a/nukeserversocket/logger.py b/nukeserversocket/logger.py
index 028f11f1..20fa8f42 100644
--- a/nukeserversocket/logger.py
+++ b/nukeserversocket/logger.py
@@ -27,7 +27,7 @@ def __init__(self, console: NssConsole) -> None:
self._console = console
def emit(self, record: logging.LogRecord) -> None:
- self._console.write(self.format(record) + '\n')
+ self._console.write(self.format(record) + '\n', record.levelname)
def _file_handler() -> TimedRotatingFileHandler:
diff --git a/nukeserversocket/received_data.py b/nukeserversocket/received_data.py
index 6bc7d8dc..1fb20478 100644
--- a/nukeserversocket/received_data.py
+++ b/nukeserversocket/received_data.py
@@ -37,9 +37,7 @@ def __post_init__(self):
self.data.setdefault('file', '')
self.data.setdefault('formatText', '1')
except Exception as e:
- LOGGER.error(
- f'Nukeserversocket: An exception occurred while decoding the data. {e}'
- )
+ LOGGER.error(f'An exception occurred while decoding the data. {e}')
self.data = {'text': '', 'file': '', 'formatText': '1'}
LOGGER.debug('Received data: %s', self.data)
@@ -49,4 +47,12 @@ def __post_init__(self):
LOGGER.critical('Data does not contain a text field.')
self.file = self.data['file']
- self.format_text = bool(int(self.data['formatText']))
+
+ try:
+ self.format_text = bool(int(self.data['formatText']))
+ except ValueError:
+ LOGGER.error(
+ 'formatText must be either "0" or "1". Got "%s". Fallback to "1".',
+ self.data['formatText']
+ )
+ self.format_text = True
diff --git a/nukeserversocket/version.py b/nukeserversocket/version.py
index 3285f2dc..1a72d32e 100644
--- a/nukeserversocket/version.py
+++ b/nukeserversocket/version.py
@@ -1,3 +1 @@
-from __future__ import annotations
-
-__version__ = '1.0.0'
+__version__ = '1.1.0'
diff --git a/pyproject.toml b/pyproject.toml
index 5a06343f..ab447a36 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,15 +1,15 @@
[tool.poetry]
name = "nukeserversocket"
-version = "1.0.0"
+version = "1.1.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"
-build = "release_manager:build"
+build = "scripts.release_manager:main"
[tool.isort]
-skip = ["__init__.py"]
+skip = ["__init__.py" , "version.py"]
length_sort = true
add_imports = "from __future__ import annotations"
@@ -38,9 +38,9 @@ addopts = [
[tool.poetry.dependencies]
python = ">=3.7.7,<3.11"
+pyside2 = "5.15.2.1"
[tool.poetry.group.dev.dependencies]
-pyside2 = "5.15.2.1"
pytest = "^7.4.3"
tox = "4.8.0"
pytest-qt = "^4.2.0"
diff --git a/release_manager.py b/release_manager.py
deleted file mode 100644
index 986f0338..00000000
--- a/release_manager.py
+++ /dev/null
@@ -1,61 +0,0 @@
-from __future__ import annotations
-
-import pathlib
-import argparse
-import subprocess
-
-CWD = pathlib.Path(__file__).parent
-DIST = CWD / 'dist'
-DIST.mkdir(exist_ok=True)
-
-
-def build() -> None:
- subprocess.run(
- ['git', 'archive', '-o', f'{DIST}/nukeserversocket.zip', 'HEAD'],
- cwd=CWD
- )
-
-
-def bump_version(version: str) -> None:
- version = subprocess.run(
- ['poetry', 'version', '-s', version], cwd=CWD, capture_output=True
- ).stdout.decode().strip()
-
- with open(CWD / 'nukeserversocket' / 'version.py', 'w') as f:
- f.write(f"__version__ = '{version}'")
-
- build()
-
-
-def get_parser():
-
- parser = argparse.ArgumentParser(
- description='NukeServerSocket - Build Manager',
- )
-
- group = parser.add_mutually_exclusive_group()
- group.add_argument('--build', action='store_true', help='Build the package.')
-
- group.add_argument(
- '--bump',
- type=str,
- metavar='VERSION',
- help='''
- Bump the version of the package. A valid version string must be provided.
- Valid versions are the same as the ones accepted by poetry version command.
- Bumping the version will also build the package and create a new release.
- '''
- )
-
- return parser
-
-
-if __name__ == '__main__':
- parser = get_parser()
- args = parser.parse_args()
- if args.bump:
- bump_version(args.bump)
- elif args.build:
- build()
- else:
- parser.print_help()
diff --git a/scripts/release_manager.py b/scripts/release_manager.py
new file mode 100644
index 00000000..43f2e302
--- /dev/null
+++ b/scripts/release_manager.py
@@ -0,0 +1,127 @@
+from __future__ import annotations
+
+import sys
+import argparse
+import subprocess
+from typing import Any, List
+from pathlib import Path
+from datetime import datetime
+from textwrap import dedent
+
+ROOT = Path(__file__).parent.parent
+DIST = ROOT / 'dist'
+DIST.mkdir(exist_ok=True)
+
+PACKAGE = ROOT.name
+
+
+def bump_version(version: str) -> None:
+
+ version = subprocess.run(
+ ['poetry', 'version', '-s', version], cwd=ROOT, capture_output=True
+ ).stdout.decode().strip()
+
+ with open(ROOT / PACKAGE / 'version.py', 'w') as f:
+ f.write(f"__version__ = '{version}'\n")
+
+ print(f'Version bumped to {version}')
+
+
+def make_release():
+ def format_output(files: list[Path]):
+ max_length = max(len(f.name) for f in files)
+ output: List[str] = []
+ for i, f in enumerate(files, 1):
+ date = datetime.fromtimestamp(
+ f.lstat().st_mtime).strftime('%Y-%m-%d %H:%M:%S')
+ output.append(f'[{i}]: {f.name.ljust(max_length)} - {date}')
+ return '\n'.join(output)
+
+ try:
+ subprocess.run(['gh', '--version'],
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ except FileNotFoundError:
+ print('gh command not found. Please install gh CLI.', file=sys.stderr)
+ sys.exit(1)
+
+ version = subprocess.run(
+ ['poetry', 'version', '-s'], cwd=ROOT, capture_output=True
+ ).stdout.decode().strip()
+
+ print(f"Select the asset to release for version {version}:")
+ files = sorted([f for f in DIST.iterdir() if not f.name.startswith('.')])
+ index = input('[0]: Skip asset\n' + format_output(files) + '\n> ')
+
+ version = f'v{version}'
+ cmd = ['gh', 'release', 'create', version, '--notes', f'Release: {version}']
+ if index != '0':
+ cmd.append(str(files[int(index)-1]))
+
+ subprocess.run(cmd, cwd=ROOT)
+
+
+def build_parser(subparser: Any) -> None:
+
+ def build(args: argparse.Namespace):
+
+ if args.version:
+ bump_version(args.version)
+
+ if args.format == 'git':
+ subprocess.run(
+ ['git', 'archive', '-o', f'{DIST}/{args.name}.zip', 'HEAD'],
+ cwd=ROOT
+ )
+
+ elif args.format == 'poetry':
+ subprocess.run(['poetry', 'build'], cwd=ROOT)
+
+ if args.release:
+ make_release()
+
+ parser = subparser.add_parser(
+ 'build', help='Build the package.',
+ usage=dedent('''
+ %(prog)s [options]
+
+ Example:
+ %(prog)s --name mypackage --version 0.1.0 --format git
+ %(prog)s --format poetry
+ '''),
+ )
+ parser.add_argument('--name', default=PACKAGE,
+ help='The name of the package.')
+ parser.add_argument('--version', type=str, metavar='VERSION',
+ help='The version of the package.')
+ parser.add_argument('--format', choices=['git', 'poetry'], default='git',
+ help='The format of the package. Default is git (zip).')
+ parser.add_argument('--release', action='store_true',
+ help='Create a new release (requires `gh` command).')
+ parser.set_defaults(func=build)
+
+
+def main():
+
+ parser = argparse.ArgumentParser(description=f'{PACKAGE} - Build Manager')
+
+ subparsers = parser.add_subparsers(title='Commands')
+ build_parser(subparsers)
+
+ group = parser.add_mutually_exclusive_group()
+ group.add_argument('--bump', type=str, metavar='VERSION',
+ help='Bump the version of the package.')
+
+ args = parser.parse_args()
+ if args.bump:
+ bump_version(args.bump)
+ sys.exit(0)
+
+ if not hasattr(args, 'func'):
+ parser.print_help()
+ sys.exit(1)
+
+ args.func(args)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tests/conftest.py b/tests/conftest.py
index d656798c..b9a87098 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,7 +1,6 @@
from __future__ import annotations
import os
-import logging
import pathlib
import pytest
@@ -12,22 +11,11 @@
SETTINGS_FILE.parent.mkdir(parents=True, exist_ok=True)
-@pytest.fixture(scope='session', autouse=True)
-def patch_logger():
- logger = logging.getLogger('nukeserversocket')
- logger.handlers.clear()
-
-
@pytest.fixture()
def mock_settings():
SETTINGS_FILE.write_text('{}')
os.environ['NSS_SETTINGS'] = str(SETTINGS_FILE)
- yield SETTINGS_FILE
+ yield _NssSettings(SETTINGS_FILE)
SETTINGS_FILE.write_text('{}')
-
-
-@pytest.fixture()
-def settings(mock_settings: pathlib.Path):
- return _NssSettings(mock_settings)
diff --git a/tests/test_editor_controller.py b/tests/test_editor_controller.py
index c08875c7..97724613 100644
--- a/tests/test_editor_controller.py
+++ b/tests/test_editor_controller.py
@@ -36,9 +36,9 @@ def execute(self):
@pytest.fixture()
-def editor(qtbot: QtBot, settings: _NssSettings) -> MockEditorController:
+def editor(qtbot: QtBot, mock_settings: _NssSettings) -> MockEditorController:
editor = MockEditorController()
- editor.settings = settings
+ editor.settings = mock_settings
qtbot.addWidget(editor.input_editor)
yield editor
editor.history.clear()
diff --git a/tests/test_main.py b/tests/test_main.py
index b1b65ef1..fc8f286a 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -55,8 +55,8 @@ def errorString(self) -> str:
@pytest.fixture()
-def model(settings: _NssSettings):
- return Model(settings)
+def model(mock_settings: _NssSettings):
+ return Model(mock_settings)
@pytest.fixture()
diff --git a/tests/test_nuke.py b/tests/test_nuke.py
index 40a198f6..955db78c 100644
--- a/tests/test_nuke.py
+++ b/tests/test_nuke.py
@@ -20,10 +20,10 @@ def __init__(self):
self.run_button = QPushButton()
-def test_nuke_python(qtbot, settings):
+def test_nuke_python(qtbot, mock_settings):
data = ReceivedData('{"file": "test.py", "text": "print(\\"hello world\\")"}')
editor = NukeController(NukeEditor())
- editor.settings = settings
+ editor.settings = mock_settings
editor.settings.set('mirror_script_editor', True)
editor.run(data)
@@ -35,12 +35,12 @@ def test_nuke_python(qtbot, settings):
('test.cpp', 'blinkscript'),
('test.blink', 'blinkscript'),
))
-def test_nuke_blinkscript(qtbot, settings: _NssSettings, file: str, text: str):
+def test_nuke_blinkscript(qtbot, mock_settings: _NssSettings, file: str, text: str):
d = json.dumps({'file': file, 'text': text})
data = ReceivedData(d)
editor = NukeController(NukeEditor())
- editor.settings = settings
+ editor.settings = mock_settings
editor.settings.set('mirror_script_editor', True)
editor.run(data)
diff --git a/tests/test_received_data.py b/tests/test_received_data.py
index 925b96c3..d61a3f6a 100644
--- a/tests/test_received_data.py
+++ b/tests/test_received_data.py
@@ -10,6 +10,7 @@
@dataclass
class ReceivedTestData:
+ name: str
raw: str
data: Dict[str, str]
text: str
@@ -19,42 +20,54 @@ class ReceivedTestData:
@pytest.mark.parametrize('data', [
ReceivedTestData(
- '{"text": "Hello World", "file": "test.py", "formatText": "1"}',
- {'text': 'Hello World', 'file': 'test.py', 'formatText': '1'},
- 'Hello World',
- 'test.py',
- True
+ name='Valid data with formatText as "1"',
+ raw='{"text": "Hello World", "file": "test.py", "formatText": "1"}',
+ data={'text': 'Hello World', 'file': 'test.py', 'formatText': '1'},
+ text='Hello World',
+ file='test.py',
+ format_text=True
),
ReceivedTestData(
- '{"text": "Hello World", "file": "test.py", "formatText": "0"}',
- {'text': 'Hello World', 'file': 'test.py', 'formatText': '0'},
- 'Hello World',
- 'test.py',
- False
+ name='Valid data with formatText as "0"',
+ raw='{"text": "Hello World", "file": "test.py", "formatText": "0"}',
+ data={'text': 'Hello World', 'file': 'test.py', 'formatText': '0'},
+ text='Hello World',
+ file='test.py',
+ format_text=False
),
ReceivedTestData(
- '{"text": "Hello World", "file": ""}',
- {'text': 'Hello World', 'file': '', 'formatText': '1'},
- 'Hello World',
- '',
- True
+ name='Valid data with wrong formatText',
+ raw='{"text": "Hello World", "formatText": "None"}',
+ data={'text': 'Hello World', 'file': '', 'formatText': 'None'},
+ text='Hello World',
+ file='',
+ format_text=True
),
ReceivedTestData(
- '{"text": "Hello World"}',
- {'text': 'Hello World', 'file': '', 'formatText': '1'},
- 'Hello World',
- '',
- True
+ name='Validata data with missing file',
+ raw='{"text": "Hello World", "file": ""}',
+ data={'text': 'Hello World', 'file': '', 'formatText': '1'},
+ text='Hello World',
+ file='',
+ format_text=True
),
ReceivedTestData(
- '{"text": "",',
- {'text': '', 'file': '', 'formatText': '1'},
- '',
- '',
- True
+ name='Validata data only text',
+ raw='{"text": "Hello World"}',
+ data={'text': 'Hello World', 'file': '', 'formatText': '1'},
+ text='Hello World',
+ file='',
+ format_text=True
+ ),
+ ReceivedTestData(
+ name='Invalid data with missing text',
+ raw='{"text": "",',
+ data={'text': '', 'file': '', 'formatText': '1'},
+ text='',
+ file='',
+ format_text=True
)
-
-])
+], ids=lambda data: data.name)
def test_received_data(data: ReceivedTestData):
received = ReceivedData(data.raw)
diff --git a/tests/test_server.py b/tests/test_server.py
index 5547f799..da8874ce 100644
--- a/tests/test_server.py
+++ b/tests/test_server.py
@@ -44,9 +44,9 @@ def execute(self) -> None:
@pytest.fixture()
-def editor(settings: _NssSettings):
+def editor(mock_settings: _NssSettings):
editor = MockEditorController()
- editor.settings = settings
+ editor.settings = mock_settings
yield editor
diff --git a/tests/test_settings.py b/tests/test_settings.py
index 926d7f18..e15fec10 100644
--- a/tests/test_settings.py
+++ b/tests/test_settings.py
@@ -3,11 +3,11 @@
from nukeserversocket.settings import _NssSettings
-def test_get_settings_default(settings: _NssSettings):
- assert isinstance(settings, _NssSettings)
- assert settings.data == _NssSettings.defaults
-
- settings.set('temp_value', True)
- assert settings.get('temp_value') is True
- assert settings.data['temp_value'] is True
- assert '"temp_value": true' in settings.path.read_text()
+def test_get_settings_default(mock_settings: _NssSettings):
+ assert isinstance(mock_settings, _NssSettings)
+ assert mock_settings.data == _NssSettings.defaults
+
+ mock_settings.set('temp_value', True)
+ assert mock_settings.get('temp_value') is True
+ assert mock_settings.data['temp_value'] is True
+ assert '"temp_value": true' in mock_settings.path.read_text()
diff --git a/tests/test_settings_ui.py b/tests/test_settings_ui.py
index 27324cd7..7d55bebf 100644
--- a/tests/test_settings_ui.py
+++ b/tests/test_settings_ui.py
@@ -13,8 +13,8 @@
@pytest.fixture()
-def model(settings: _NssSettings):
- return Model(settings)
+def model(mock_settings: _NssSettings):
+ return Model(mock_settings)
@pytest.fixture()