diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 039b42bff3..f751a54116 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,10 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.18.7-nightly.2 + - 3.18.7-nightly.1 + - 3.18.6 + - 3.18.6-nightly.2 - 3.18.6-nightly.1 - 3.18.5 - 3.18.5-nightly.3 @@ -131,10 +135,6 @@ body: - 3.15.10-nightly.1 - 3.15.9 - 3.15.9-nightly.2 - - 3.15.9-nightly.1 - - 3.15.8 - - 3.15.8-nightly.3 - - 3.15.8-nightly.2 validations: required: true - type: dropdown diff --git a/CHANGELOG.md b/CHANGELOG.md index 14f0bc469f..009150ae7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,132 @@ # Changelog +## [3.18.6](https://github.com/ynput/OpenPype/tree/3.18.6) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.18.5...3.18.6) + +### **🚀 Enhancements** + + +
+AYON: Use `SettingsField` from ayon server #6173 + +This is preparation for new version of pydantic which will require to customize the field class for AYON purposes as raw pydantic Field could not be used. + + +___ + +
+ + +
+Nuke: Expose write knobs - OP-7592 #6137 + +This PR adds `exposed_knobs` to the creator plugins settings at `ayon+settings://nuke/create/CreateWriteRender/exposed_knobs`.When exposed knobs will be linked from the write node to the outside publish group, for users to adjust. + + +___ + +
+ + +
+AYON: Remove kitsu addon #6172 + +Removed kitsu addon from server addons because already has own repository. + + +___ + +
+ +### **🐛 Bug fixes** + + +
+Fusion: provide better logging for validate saver crash due type error #6082 + +Handles reported issue for `NoneType` error thrown in conversion `int(tool["Comments"][frame])`. It is most likely happening when saver node has no input connections.There is a validator for that, but it might be not obvious, that this error is caused by missing input connections and it has been already reported by `"Validate Saver Has Input"`. + + +___ + +
+ + +
+Workfile Template Builder: Use correct variable in create placeholder #6141 + +Use correct variable where failed instances are stored for validation. + + +___ + +
+ + +
+ExtractOIIOTranscode: Missing product_names to subsets conversion #6159 + +The `Product Names` filtering should be fixed with this. + + +___ + +
+ + +
+Blender: Fix missing animation data when updating blend assets #6165 + +Fix missing animation data when updating blend assets. + + +___ + +
+ + +
+TrayPublisher: Pre-fill of version works in AYON #6180 + +Use `folderPath` instead of `asset` in AYON mode to calculate next available version. + + +___ + +
+ +### **🔀 Refactored code** + + +
+Chore: remove Muster #6085 + +Muster isn't maintained for a long time and it wasn't working anyway. This is removing related code from the code base. If there is renewed interest in Muster, it needs to be re-implemented in modern AYON compatible way. + + +___ + +
+ +### **Merged pull requests** + + +
+Maya: change label in the render settings to be more readable #6134 + +AYON replacement for #5713. + + +___ + +
+ + + + ## [3.18.5](https://github.com/ynput/OpenPype/tree/3.18.5) diff --git a/openpype/hosts/nuke/api/__init__.py b/openpype/hosts/nuke/api/__init__.py index c6ccd0baf1..2536230637 100644 --- a/openpype/hosts/nuke/api/__init__.py +++ b/openpype/hosts/nuke/api/__init__.py @@ -43,7 +43,8 @@ from .lib import ( get_node_data, set_node_data, update_node_data, - create_write_node + create_write_node, + link_knobs ) from .utils import ( colorspace_exists_on_node, @@ -95,6 +96,7 @@ __all__ = ( "set_node_data", "update_node_data", "create_write_node", + "link_knobs", "colorspace_exists_on_node", "get_colorspace_list", diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 7ba53caead..cdefd05c11 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -3499,3 +3499,27 @@ def create_camera_node_by_version(): return nuke.createNode("Camera4") else: return nuke.createNode("Camera2") + + +def link_knobs(knobs, node, group_node): + """Link knobs from inside `group_node`""" + + missing_knobs = [] + for knob in knobs: + if knob in group_node.knobs(): + continue + + if knob not in node.knobs().keys(): + missing_knobs.append(knob) + + link = nuke.Link_Knob("") + link.makeLink(node.name(), knob) + link.setName(knob) + link.setFlag(0x1000) + group_node.addKnob(link) + + if missing_knobs: + raise ValueError( + "Write node exposed knobs missing:\n\n{}\n\nPlease review" + " project settings.".format("\n".join(missing_knobs)) + ) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 15d7bfc4b9..c8301b81fd 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -44,7 +44,8 @@ from .lib import ( get_view_process_node, get_viewer_config_from_string, deprecated, - get_filenames_without_hash + get_filenames_without_hash, + link_knobs ) from .pipeline import ( list_instances, @@ -1344,3 +1345,11 @@ def _remove_old_knobs(node): node.removeKnob(knob) except ValueError: pass + + +def exposed_write_knobs(settings, plugin_name, instance_node): + exposed_knobs = settings["nuke"]["create"][plugin_name]["exposed_knobs"] + if exposed_knobs: + instance_node.addKnob(nuke.Text_Knob('', 'Write Knobs')) + write_node = nuke.allNodes(group=instance_node, filter="Write")[0] + link_knobs(exposed_knobs, write_node, instance_node) diff --git a/openpype/hosts/nuke/plugins/create/create_write_image.py b/openpype/hosts/nuke/plugins/create/create_write_image.py index 8c18739587..f21d871c9f 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_image.py +++ b/openpype/hosts/nuke/plugins/create/create_write_image.py @@ -12,6 +12,7 @@ from openpype.lib import ( EnumDef ) from openpype.hosts.nuke import api as napi +from openpype.hosts.nuke.api.plugin import exposed_write_knobs class CreateWriteImage(napi.NukeWriteCreator): @@ -132,6 +133,10 @@ class CreateWriteImage(napi.NukeWriteCreator): instance.data_to_store() ) + exposed_write_knobs( + self.project_settings, self.__class__.__name__, instance_node + ) + return instance except Exception as er: diff --git a/openpype/hosts/nuke/plugins/create/create_write_prerender.py b/openpype/hosts/nuke/plugins/create/create_write_prerender.py index 395c3b002f..742bfb20ad 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_prerender.py +++ b/openpype/hosts/nuke/plugins/create/create_write_prerender.py @@ -9,6 +9,7 @@ from openpype.lib import ( BoolDef ) from openpype.hosts.nuke import api as napi +from openpype.hosts.nuke.api.plugin import exposed_write_knobs class CreateWritePrerender(napi.NukeWriteCreator): @@ -119,6 +120,10 @@ class CreateWritePrerender(napi.NukeWriteCreator): instance.data_to_store() ) + exposed_write_knobs( + self.project_settings, self.__class__.__name__, instance_node + ) + return instance except Exception as er: diff --git a/openpype/hosts/nuke/plugins/create/create_write_render.py b/openpype/hosts/nuke/plugins/create/create_write_render.py index 91acf4eabc..fc16876f75 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_render.py +++ b/openpype/hosts/nuke/plugins/create/create_write_render.py @@ -9,6 +9,7 @@ from openpype.lib import ( BoolDef ) from openpype.hosts.nuke import api as napi +from openpype.hosts.nuke.api.plugin import exposed_write_knobs class CreateWriteRender(napi.NukeWriteCreator): @@ -113,6 +114,10 @@ class CreateWriteRender(napi.NukeWriteCreator): instance.data_to_store() ) + exposed_write_knobs( + self.project_settings, self.__class__.__name__, instance_node + ) + return instance except Exception as er: diff --git a/openpype/hosts/nuke/plugins/publish/validate_exposed_knobs.py b/openpype/hosts/nuke/plugins/publish/validate_exposed_knobs.py new file mode 100644 index 0000000000..fe5644f0c9 --- /dev/null +++ b/openpype/hosts/nuke/plugins/publish/validate_exposed_knobs.py @@ -0,0 +1,77 @@ +import pyblish.api + +from openpype.pipeline.publish import get_errored_instances_from_context +from openpype.hosts.nuke.api.lib import link_knobs +from openpype.pipeline.publish import ( + OptionalPyblishPluginMixin, + PublishValidationError +) + + +class RepairExposedKnobs(pyblish.api.Action): + label = "Repair" + on = "failed" + icon = "wrench" + + def process(self, context, plugin): + instances = get_errored_instances_from_context(context) + + for instance in instances: + child_nodes = ( + instance.data.get("transientData", {}).get("childNodes") + or instance + ) + + write_group_node = instance.data["transientData"]["node"] + # get write node from inside of group + write_node = None + for x in child_nodes: + if x.Class() == "Write": + write_node = x + + plugin_name = plugin.families_mapping[instance.data["family"]] + nuke_settings = instance.context.data["project_settings"]["nuke"] + create_settings = nuke_settings["create"][plugin_name] + exposed_knobs = create_settings["exposed_knobs"] + link_knobs(exposed_knobs, write_node, write_group_node) + + +class ValidateExposedKnobs( + OptionalPyblishPluginMixin, + pyblish.api.InstancePlugin +): + """ Validate write node exposed knobs. + + Compare exposed linked knobs to settings. + """ + + order = pyblish.api.ValidatorOrder + optional = True + families = ["render", "prerender", "image"] + label = "Validate Exposed Knobs" + actions = [RepairExposedKnobs] + hosts = ["nuke"] + families_mapping = { + "render": "CreateWriteRender", + "prerender": "CreateWritePrerender", + "image": "CreateWriteImage" + } + + def process(self, instance): + if not self.is_active(instance.data): + return + + plugin = self.families_mapping[instance.data["family"]] + group_node = instance.data["transientData"]["node"] + nuke_settings = instance.context.data["project_settings"]["nuke"] + create_settings = nuke_settings["create"][plugin] + exposed_knobs = create_settings["exposed_knobs"] + unexposed_knobs = [] + for knob in exposed_knobs: + if knob not in group_node.knobs(): + unexposed_knobs.append(knob) + + if unexposed_knobs: + raise PublishValidationError( + "Missing exposed knobs: {}".format(unexposed_knobs) + ) diff --git a/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py b/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py index 0539c1188f..f490b580d6 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py +++ b/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py @@ -10,7 +10,7 @@ from openpype.hosts.nuke.api.lib import ( from openpype.pipeline.publish import ( PublishXmlValidationError, - OptionalPyblishPluginMixin, + OptionalPyblishPluginMixin ) diff --git a/openpype/hosts/photoshop/api/lib.py b/openpype/hosts/photoshop/api/lib.py index ff520348f0..d4d4995e6d 100644 --- a/openpype/hosts/photoshop/api/lib.py +++ b/openpype/hosts/photoshop/api/lib.py @@ -3,12 +3,11 @@ import sys import contextlib import traceback -from qtpy import QtWidgets - from openpype.lib import env_value_to_bool, Logger from openpype.modules import ModulesManager from openpype.pipeline import install_host from openpype.tools.utils import host_tools +from openpype.tools.utils import get_openpype_qt_app from openpype.tests.lib import is_in_tests from .launch_logic import ProcessLauncher, stub @@ -30,7 +29,7 @@ def main(*subprocess_args): # coloring in StdOutBroker os.environ["OPENPYPE_LOG_NO_COLORS"] = "False" - app = QtWidgets.QApplication([]) + app = get_openpype_qt_app() app.setQuitOnLastWindowClosed(False) launcher = ProcessLauncher(subprocess_args) diff --git a/openpype/hosts/standalonepublisher/addon.py b/openpype/hosts/standalonepublisher/addon.py index 67204b581b..607c4ecdae 100644 --- a/openpype/hosts/standalonepublisher/addon.py +++ b/openpype/hosts/standalonepublisher/addon.py @@ -1,10 +1,13 @@ import os -import click - from openpype.lib import get_openpype_execute_args from openpype.lib.execute import run_detached_process -from openpype.modules import OpenPypeModule, ITrayAction, IHostAddon +from openpype.modules import ( + click_wrap, + OpenPypeModule, + ITrayAction, + IHostAddon, +) STANDALONEPUBLISH_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -37,10 +40,10 @@ class StandAlonePublishAddon(OpenPypeModule, ITrayAction, IHostAddon): run_detached_process(args) def cli(self, click_group): - click_group.add_command(cli_main) + click_group.add_command(cli_main.to_click_obj()) -@click.group( +@click_wrap.group( StandAlonePublishAddon.name, help="StandalonePublisher related commands.") def cli_main(): diff --git a/openpype/hosts/traypublisher/addon.py b/openpype/hosts/traypublisher/addon.py index 3b34f9e6e8..ca60760bab 100644 --- a/openpype/hosts/traypublisher/addon.py +++ b/openpype/hosts/traypublisher/addon.py @@ -1,10 +1,13 @@ import os -import click - from openpype.lib import get_openpype_execute_args from openpype.lib.execute import run_detached_process -from openpype.modules import OpenPypeModule, ITrayAction, IHostAddon +from openpype.modules import ( + click_wrap, + OpenPypeModule, + ITrayAction, + IHostAddon, +) TRAYPUBLISH_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -38,10 +41,12 @@ class TrayPublishAddon(OpenPypeModule, IHostAddon, ITrayAction): run_detached_process(args) def cli(self, click_group): - click_group.add_command(cli_main) + click_group.add_command(cli_main.to_click_obj()) -@click.group(TrayPublishAddon.name, help="TrayPublisher related commands.") +@click_wrap.group( + TrayPublishAddon.name, + help="TrayPublisher related commands.") def cli_main(): pass diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py index 14c66fa08f..6859b85a46 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -221,9 +221,16 @@ class SettingsCreator(TrayPublishCreator): ): filtered_instance_data.append(instance) - asset_names = { - instance["asset"] - for instance in filtered_instance_data} + if AYON_SERVER_ENABLED: + asset_names = { + instance["folderPath"] + for instance in filtered_instance_data + } + else: + asset_names = { + instance["asset"] + for instance in filtered_instance_data + } subset_names = { instance["subset"] for instance in filtered_instance_data} @@ -231,7 +238,10 @@ class SettingsCreator(TrayPublishCreator): asset_names, subset_names ) for instance in filtered_instance_data: - asset_name = instance["asset"] + if AYON_SERVER_ENABLED: + asset_name = instance["folderPath"] + else: + asset_name = instance["asset"] subset_name = instance["subset"] version = subset_docs_by_asset_id[asset_name][subset_name] instance["creator_attributes"]["version_to_use"] = version diff --git a/openpype/hosts/webpublisher/addon.py b/openpype/hosts/webpublisher/addon.py index 4438775b03..810d9aa6c3 100644 --- a/openpype/hosts/webpublisher/addon.py +++ b/openpype/hosts/webpublisher/addon.py @@ -1,8 +1,6 @@ import os -import click - -from openpype.modules import OpenPypeModule, IHostAddon +from openpype.modules import click_wrap, OpenPypeModule, IHostAddon WEBPUBLISHER_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -38,10 +36,10 @@ class WebpublisherAddon(OpenPypeModule, IHostAddon): ) def cli(self, click_group): - click_group.add_command(cli_main) + click_group.add_command(cli_main.to_click_obj()) -@click.group( +@click_wrap.group( WebpublisherAddon.name, help="Webpublisher related commands.") def cli_main(): @@ -49,10 +47,10 @@ def cli_main(): @cli_main.command() -@click.argument("path") -@click.option("-u", "--user", help="User email address") -@click.option("-p", "--project", help="Project") -@click.option("-t", "--targets", help="Targets", default=None, +@click_wrap.argument("path") +@click_wrap.option("-u", "--user", help="User email address") +@click_wrap.option("-p", "--project", help="Project") +@click_wrap.option("-t", "--targets", help="Targets", default=None, multiple=True) def publish(project, path, user=None, targets=None): """Start publishing (Inner command). @@ -67,11 +65,11 @@ def publish(project, path, user=None, targets=None): @cli_main.command() -@click.argument("path") -@click.option("-p", "--project", help="Project") -@click.option("-h", "--host", help="Host") -@click.option("-u", "--user", help="User email address") -@click.option("-t", "--targets", help="Targets", default=None, +@click_wrap.argument("path") +@click_wrap.option("-p", "--project", help="Project") +@click_wrap.option("-h", "--host", help="Host") +@click_wrap.option("-u", "--user", help="User email address") +@click_wrap.option("-t", "--targets", help="Targets", default=None, multiple=True) def publishfromapp(project, path, host, user=None, targets=None): """Start publishing through application (Inner command). @@ -86,10 +84,10 @@ def publishfromapp(project, path, host, user=None, targets=None): @cli_main.command() -@click.option("-e", "--executable", help="Executable") -@click.option("-u", "--upload_dir", help="Upload dir") -@click.option("-h", "--host", help="Host", default=None) -@click.option("-p", "--port", help="Port", default=None) +@click_wrap.option("-e", "--executable", help="Executable") +@click_wrap.option("-u", "--upload_dir", help="Upload dir") +@click_wrap.option("-h", "--host", help="Host", default=None) +@click_wrap.option("-p", "--port", help="Port", default=None) def webserver(executable, upload_dir, host=None, port=None): """Start service for communication with Webpublish Front end. diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index c8ddbde061..1cfe9ac14b 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1227,12 +1227,8 @@ def get_rescaled_command_arguments( target_par = target_par or 1.0 input_par = 1.0 - # ffmpeg command - input_file_metadata = get_ffprobe_data(input_path, logger=log) - stream = input_file_metadata["streams"][0] - input_width = int(stream["width"]) - input_height = int(stream["height"]) - stream_input_par = stream.get("sample_aspect_ratio") + input_height, input_width, stream_input_par = _get_image_dimensions( + application, input_path, log) if stream_input_par: input_par = ( float(stream_input_par.split(":")[0]) @@ -1345,6 +1341,48 @@ def get_rescaled_command_arguments( return command_args +def _get_image_dimensions(application, input_path, log): + """Uses 'ffprobe' first and then 'oiiotool' if available to get dim. + + Args: + application (str): "oiiotool"|"ffmpeg" + input_path (str): path to image file + log (Optional[logging.Logger]): Logger used for logging. + Returns: + (tuple) (int, int, dict) - (height, width, sample_aspect_ratio) + Raises: + RuntimeError if image dimensions couldn't be parsed out. + """ + # ffmpeg command + input_file_metadata = get_ffprobe_data(input_path, logger=log) + input_width = input_height = 0 + stream = next( + ( + s for s in input_file_metadata["streams"] + if s.get("codec_type") == "video" + ), + {} + ) + if stream: + input_width = int(stream["width"]) + input_height = int(stream["height"]) + + # fallback for weird files with width=0, height=0 + if (input_width == 0 or input_height == 0) and application == "oiiotool": + # Load info about file from oiio tool + input_info = get_oiio_info_for_input(input_path, logger=log) + if input_info: + input_width = int(input_info["width"]) + input_height = int(input_info["height"]) + + if input_width == 0 or input_height == 0: + raise RuntimeError("Couldn't read {} either " + "with ffprobe or oiiotool".format(input_path)) + + stream_input_par = stream.get("sample_aspect_ratio") + return input_height, input_width, stream_input_par + + def convert_color_values(application, color_value): """Get color mapping for ffmpeg and oiiotool. Args: diff --git a/openpype/modules/__init__.py b/openpype/modules/__init__.py index 3097805353..87f3233afc 100644 --- a/openpype/modules/__init__.py +++ b/openpype/modules/__init__.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from . import click_wrap from .interfaces import ( ILaunchHookPaths, IPluginPaths, @@ -28,6 +29,8 @@ from .base import ( __all__ = ( + "click_wrap", + "ILaunchHookPaths", "IPluginPaths", "ITrayModule", diff --git a/openpype/modules/click_wrap.py b/openpype/modules/click_wrap.py new file mode 100644 index 0000000000..ed67035ec8 --- /dev/null +++ b/openpype/modules/click_wrap.py @@ -0,0 +1,365 @@ +"""Simplified wrapper for 'click' python module. + +Module 'click' is used as main cli handler in AYON/OpenPype. Addons can +register their own subcommands with options. This wrapper allows to define +commands and options as with 'click', but without any dependency. + +Why not to use 'click' directly? Version of 'click' used in AYON/OpenPype +is not compatible with 'click' version used in some DCCs (e.g. Houdini 20+). +And updating 'click' would break other DCCs. + +How to use it? If you already have cli commands defined in addon, just replace +'click' with 'click_wrap' and it should work and modify your addon's cli +method to convert 'click_wrap' object to 'click' object. + +Before +```python +import click +from openpype.modules import OpenPypeModule + + +class ExampleAddon(OpenPypeModule): + name = "example" + + def cli(self, click_group): + click_group.add_command(cli_main) + + +@click.group(ExampleAddon.name, help="Example addon") +def cli_main(): + pass + + +@cli_main.command(help="Example command") +@click.option("--arg1", help="Example argument 1", default="default1") +@click.option("--arg2", help="Example argument 2", is_flag=True) +def mycommand(arg1, arg2): + print(arg1, arg2) +``` + +Now +``` +from openpype import click_wrap +from openpype.modules import OpenPypeModule + + +class ExampleAddon(OpenPypeModule): + name = "example" + + def cli(self, click_group): + click_group.add_command(cli_main.to_click_obj()) + + +@click_wrap.group(ExampleAddon.name, help="Example addon") +def cli_main(): + pass + + +@cli_main.command(help="Example command") +@click_wrap.option("--arg1", help="Example argument 1", default="default1") +@click_wrap.option("--arg2", help="Example argument 2", is_flag=True) +def mycommand(arg1, arg2): + print(arg1, arg2) +``` + + +Added small enhancements: +- most of the methods can be used as chained calls +- functions/methods 'command' and 'group' can be used in a way that + first argument is callback function and the rest are arguments + for click + +Example: + ```python + from openpype import click_wrap + from openpype.modules import OpenPypeModule + + + class ExampleAddon(OpenPypeModule): + name = "example" + + def cli(self, click_group): + # Define main command (name 'example') + main = click_wrap.group( + self._cli_main, name=self.name, help="Example addon" + ) + # Add subcommand (name 'mycommand') + ( + main.command( + self._cli_command, name="mycommand", help="Example command" + ) + .option( + "--arg1", help="Example argument 1", default="default1" + ) + .option( + "--arg2", help="Example argument 2", is_flag=True, + ) + ) + # Convert main command to click object and add it to parent group + click_group.add_command(main.to_click_obj()) + + def _cli_main(self): + pass + + def _cli_command(self, arg1, arg2): + print(arg1, arg2) + ``` + + ```shell + openpype_console addon example mycommand --arg1 value1 --arg2 + ``` +""" + +import collections + +FUNC_ATTR_NAME = "__ayon_cli_options__" + + +class Command(object): + def __init__(self, func, *args, **kwargs): + # Command function + self._func = func + # Command definition arguments + self._args = args + # Command definition kwargs + self._kwargs = kwargs + # Both 'options' and 'arguments' are stored to the same variable + # - keep order of options and arguments + self._options = getattr(func, FUNC_ATTR_NAME, []) + + def to_click_obj(self): + """Converts this object to click object. + + Returns: + click.Command: Click command object. + """ + return convert_to_click(self) + + # --- Methods for 'convert_to_click' function --- + def get_args(self): + """ + Returns: + tuple: Command definition arguments. + """ + return self._args + + def get_kwargs(self): + """ + Returns: + dict[str, Any]: Command definition kwargs. + """ + return self._kwargs + + def get_func(self): + """ + Returns: + Function: Function to invoke on command trigger. + """ + return self._func + + def iter_options(self): + """ + Yields: + tuple[str, tuple, dict]: Option type name with args and kwargs. + """ + for item in self._options: + yield item + # ----------------------------------------------- + + def add_option(self, *args, **kwargs): + return self.add_option_by_type("option", *args, **kwargs) + + def add_argument(self, *args, **kwargs): + return self.add_option_by_type("argument", *args, **kwargs) + + option = add_option + argument = add_argument + + def add_option_by_type(self, option_name, *args, **kwargs): + self._options.append((option_name, args, kwargs)) + return self + + +class Group(Command): + def __init__(self, func, *args, **kwargs): + super(Group, self).__init__(func, *args, **kwargs) + # Store sub-groupd and sub-commands to the same variable + self._commands = [] + + # --- Methods for 'convert_to_click' function --- + def iter_commands(self): + for command in self._commands: + yield command + # ----------------------------------------------- + + def add_command(self, command): + """Add prepared command object as child. + + Args: + command (Command): Prepared command object. + """ + if command not in self._commands: + self._commands.append(command) + + def add_group(self, group): + """Add prepared group object as child. + + Args: + group (Group): Prepared group object. + """ + if group not in self._commands: + self._commands.append(group) + + def command(self, *args, **kwargs): + """Add child command. + + Returns: + Union[Command, Function]: New command object, or wrapper function. + """ + return self._add_new(Command, *args, **kwargs) + + def group(self, *args, **kwargs): + """Add child group. + + Returns: + Union[Group, Function]: New group object, or wrapper function. + """ + return self._add_new(Group, *args, **kwargs) + + def _add_new(self, target_cls, *args, **kwargs): + func = None + if args and callable(args[0]): + args = list(args) + func = args.pop(0) + args = tuple(args) + + def decorator(_func): + out = target_cls(_func, *args, **kwargs) + self._commands.append(out) + return out + + if func is not None: + return decorator(func) + return decorator + + +def convert_to_click(obj_to_convert): + """Convert wrapped object to click object. + + Args: + obj_to_convert (Command): Object to convert to click object. + + Returns: + click.Command: Click command object. + """ + import click + + commands_queue = collections.deque() + commands_queue.append((obj_to_convert, None)) + top_obj = None + while commands_queue: + item = commands_queue.popleft() + command_obj, parent_obj = item + if not isinstance(command_obj, Command): + raise TypeError( + "Invalid type '{}' expected 'Command'".format( + type(command_obj) + ) + ) + + if isinstance(command_obj, Group): + click_obj = ( + click.group( + *command_obj.get_args(), + **command_obj.get_kwargs() + )(command_obj.get_func()) + ) + + else: + click_obj = ( + click.command( + *command_obj.get_args(), + **command_obj.get_kwargs() + )(command_obj.get_func()) + ) + + for item in command_obj.iter_options(): + option_name, args, kwargs = item + if option_name == "option": + click.option(*args, **kwargs)(click_obj) + elif option_name == "argument": + click.argument(*args, **kwargs)(click_obj) + else: + raise ValueError( + "Invalid option name '{}'".format(option_name) + ) + + if top_obj is None: + top_obj = click_obj + + if parent_obj is not None: + parent_obj.add_command(click_obj) + + if isinstance(command_obj, Group): + for command in command_obj.iter_commands(): + commands_queue.append((command, click_obj)) + + return top_obj + + +def group(*args, **kwargs): + func = None + if args and callable(args[0]): + args = list(args) + func = args.pop(0) + args = tuple(args) + + def decorator(_func): + return Group(_func, *args, **kwargs) + + if func is not None: + return decorator(func) + return decorator + + +def command(*args, **kwargs): + func = None + if args and callable(args[0]): + args = list(args) + func = args.pop(0) + args = tuple(args) + + def decorator(_func): + return Command(_func, *args, **kwargs) + + if func is not None: + return decorator(func) + return decorator + + +def argument(*args, **kwargs): + def decorator(func): + return _add_option_to_func( + func, "argument", *args, **kwargs + ) + return decorator + + +def option(*args, **kwargs): + def decorator(func): + return _add_option_to_func( + func, "option", *args, **kwargs + ) + return decorator + + +def _add_option_to_func(func, option_name, *args, **kwargs): + if isinstance(func, Command): + func.add_option_by_type(option_name, *args, **kwargs) + return func + + if not hasattr(func, FUNC_ATTR_NAME): + setattr(func, FUNC_ATTR_NAME, []) + cli_options = getattr(func, FUNC_ATTR_NAME) + cli_options.append((option_name, args, kwargs)) + return func diff --git a/openpype/modules/example_addons/example_addon/addon.py b/openpype/modules/example_addons/example_addon/addon.py index be1d3ff920..e9de0c1bf5 100644 --- a/openpype/modules/example_addons/example_addon/addon.py +++ b/openpype/modules/example_addons/example_addon/addon.py @@ -8,9 +8,9 @@ in global space here until are required or used. """ import os -import click from openpype.modules import ( + click_wrap, JsonFilesSettingsDef, OpenPypeAddOn, ModulesManager, @@ -115,10 +115,12 @@ class ExampleAddon(OpenPypeAddOn, IPluginPaths, ITrayAction): } def cli(self, click_group): - click_group.add_command(cli_main) + click_group.add_command(cli_main.to_click_obj()) -@click.group(ExampleAddon.name, help="Example addon dynamic cli commands.") +@click_wrap.group( + ExampleAddon.name, + help="Example addon dynamic cli commands.") def cli_main(): pass diff --git a/openpype/modules/ftrack/ftrack_module.py b/openpype/modules/ftrack/ftrack_module.py index b5152ff9c4..2042367a7e 100644 --- a/openpype/modules/ftrack/ftrack_module.py +++ b/openpype/modules/ftrack/ftrack_module.py @@ -3,9 +3,8 @@ import json import collections import platform -import click - from openpype.modules import ( + click_wrap, OpenPypeModule, ITrayModule, IPluginPaths, @@ -489,7 +488,7 @@ class FtrackModule( return cred.get("username"), cred.get("api_key") def cli(self, click_group): - click_group.add_command(cli_main) + click_group.add_command(cli_main.to_click_obj()) def _check_ftrack_url(url): @@ -540,24 +539,24 @@ def resolve_ftrack_url(url, logger=None): return ftrack_url -@click.group(FtrackModule.name, help="Ftrack module related commands.") +@click_wrap.group(FtrackModule.name, help="Ftrack module related commands.") def cli_main(): pass @cli_main.command() -@click.option("-d", "--debug", is_flag=True, help="Print debug messages") -@click.option("--ftrack-url", envvar="FTRACK_SERVER", +@click_wrap.option("-d", "--debug", is_flag=True, help="Print debug messages") +@click_wrap.option("--ftrack-url", envvar="FTRACK_SERVER", help="Ftrack server url") -@click.option("--ftrack-user", envvar="FTRACK_API_USER", +@click_wrap.option("--ftrack-user", envvar="FTRACK_API_USER", help="Ftrack api user") -@click.option("--ftrack-api-key", envvar="FTRACK_API_KEY", +@click_wrap.option("--ftrack-api-key", envvar="FTRACK_API_KEY", help="Ftrack api key") -@click.option("--legacy", is_flag=True, +@click_wrap.option("--legacy", is_flag=True, help="run event server without mongo storing") -@click.option("--clockify-api-key", envvar="CLOCKIFY_API_KEY", +@click_wrap.option("--clockify-api-key", envvar="CLOCKIFY_API_KEY", help="Clockify API key.") -@click.option("--clockify-workspace", envvar="CLOCKIFY_WORKSPACE", +@click_wrap.option("--clockify-workspace", envvar="CLOCKIFY_WORKSPACE", help="Clockify workspace") def eventserver( debug, diff --git a/openpype/modules/ftrack/launch_hooks/post_ftrack_changes.py b/openpype/modules/ftrack/launch_hooks/post_ftrack_changes.py index ac4e499e41..5c780a51c4 100644 --- a/openpype/modules/ftrack/launch_hooks/post_ftrack_changes.py +++ b/openpype/modules/ftrack/launch_hooks/post_ftrack_changes.py @@ -131,6 +131,8 @@ class PostFtrackHook(PostLaunchHook): for key, value in status_mapping.items(): if key in already_tested: continue + + value = value.lower() if actual_status in value or "__any__" in value: if key != "__ignore__": next_status_name = key diff --git a/openpype/modules/job_queue/module.py b/openpype/modules/job_queue/module.py index 7075fcea14..6792cd2aca 100644 --- a/openpype/modules/job_queue/module.py +++ b/openpype/modules/job_queue/module.py @@ -41,8 +41,7 @@ import json import copy import platform -import click -from openpype.modules import OpenPypeModule +from openpype.modules import OpenPypeModule, click_wrap from openpype.settings import get_system_settings @@ -153,7 +152,7 @@ class JobQueueModule(OpenPypeModule): return requests.get(api_path).json() def cli(self, click_group): - click_group.add_command(cli_main) + click_group.add_command(cli_main.to_click_obj()) @classmethod def get_server_url_from_settings(cls): @@ -213,7 +212,7 @@ class JobQueueModule(OpenPypeModule): return main(str(executable), server_url) -@click.group( +@click_wrap.group( JobQueueModule.name, help="Application job server. Can be used as render farm." ) @@ -225,8 +224,8 @@ def cli_main(): "start_server", help="Start server handling workers and their jobs." ) -@click.option("--port", help="Server port") -@click.option("--host", help="Server host (ip address)") +@click_wrap.option("--port", help="Server port") +@click_wrap.option("--host", help="Server host (ip address)") def cli_start_server(port, host): JobQueueModule.start_server(port, host) @@ -236,7 +235,9 @@ def cli_start_server(port, host): "Start a worker for a specific application. (e.g. \"tvpaint/11.5\")" ) ) -@click.argument("app_name") -@click.option("--server_url", help="Server url which handle workers and jobs.") +@click_wrap.argument("app_name") +@click_wrap.option( + "--server_url", + help="Server url which handle workers and jobs.") def cli_start_worker(app_name, server_url): JobQueueModule.start_worker(app_name, server_url) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index 8d2d5ccd60..0ab627ba75 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -1,9 +1,9 @@ """Kitsu module.""" -import click import os from openpype.modules import ( + click_wrap, OpenPypeModule, IPluginPaths, ITrayAction, @@ -98,17 +98,17 @@ class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction): } def cli(self, click_group): - click_group.add_command(cli_main) + click_group.add_command(cli_main.to_click_obj()) -@click.group(KitsuModule.name, help="Kitsu dynamic cli commands.") +@click_wrap.group(KitsuModule.name, help="Kitsu dynamic cli commands.") def cli_main(): pass @cli_main.command() -@click.option("--login", envvar="KITSU_LOGIN", help="Kitsu login") -@click.option( +@click_wrap.option("--login", envvar="KITSU_LOGIN", help="Kitsu login") +@click_wrap.option( "--password", envvar="KITSU_PWD", help="Password for kitsu username" ) def push_to_zou(login, password): @@ -124,11 +124,11 @@ def push_to_zou(login, password): @cli_main.command() -@click.option("-l", "--login", envvar="KITSU_LOGIN", help="Kitsu login") -@click.option( +@click_wrap.option("-l", "--login", envvar="KITSU_LOGIN", help="Kitsu login") +@click_wrap.option( "-p", "--password", envvar="KITSU_PWD", help="Password for kitsu username" ) -@click.option( +@click_wrap.option( "-prj", "--project", "projects", @@ -136,7 +136,7 @@ def push_to_zou(login, password): default=[], help="Sync specific kitsu projects", ) -@click.option( +@click_wrap.option( "-lo", "--listen-only", "listen_only", diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index 8a92697920..3d6f76ad55 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -7,7 +7,6 @@ import copy import signal from collections import deque, defaultdict -import click from bson.objectid import ObjectId from openpype.client import ( @@ -15,7 +14,12 @@ from openpype.client import ( get_representations, get_representation_by_id, ) -from openpype.modules import OpenPypeModule, ITrayModule, IPluginPaths +from openpype.modules import ( + OpenPypeModule, + ITrayModule, + IPluginPaths, + click_wrap, +) from openpype.settings import ( get_project_settings, get_system_settings, @@ -2405,7 +2409,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule, IPluginPaths): return presets[project_name]['sites'][site_name]['root'] def cli(self, click_group): - click_group.add_command(cli_main) + click_group.add_command(cli_main.to_click_obj()) # Webserver module implementation def webserver_initialization(self, server_manager): @@ -2417,13 +2421,15 @@ class SyncServerModule(OpenPypeModule, ITrayModule, IPluginPaths): ) -@click.group(SyncServerModule.name, help="SyncServer module related commands.") +@click_wrap.group( + SyncServerModule.name, + help="SyncServer module related commands.") def cli_main(): pass @cli_main.command() -@click.option( +@click_wrap.option( "-a", "--active_site", required=True, diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 4948f2431c..2c851c054d 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -1458,7 +1458,7 @@ class _AyonSettingsCache: variant = "production" if is_dev_mode_enabled(): - variant = cls._get_dev_mode_settings_variant() + variant = cls._get_bundle_name() elif is_staging_enabled(): variant = "staging" @@ -1474,28 +1474,6 @@ class _AyonSettingsCache: def _get_bundle_name(cls): return os.environ["AYON_BUNDLE_NAME"] - @classmethod - def _get_dev_mode_settings_variant(cls): - """Develop mode settings variant. - - Returns: - str: Name of settings variant. - """ - - con = get_ayon_server_api_connection() - bundles = con.get_bundles() - user = con.get_user() - username = user["name"] - for bundle in bundles["bundles"]: - if ( - bundle.get("isDev") - and bundle.get("activeUser") == username - ): - return bundle["name"] - # Return fake variant - distribution logic will tell user that he - # does not have set any dev bundle - return "dev" - @classmethod def get_value_by_project(cls, project_name): cache_item = _AyonSettingsCache.cache_by_project_name[project_name] diff --git a/openpype/tools/ayon_loader/abstract.py b/openpype/tools/ayon_loader/abstract.py index bf3e81d485..1d93716e07 100644 --- a/openpype/tools/ayon_loader/abstract.py +++ b/openpype/tools/ayon_loader/abstract.py @@ -531,6 +531,9 @@ class FrontendLoaderController(_BaseLoaderController): Product types have defined if are checked for filtering or not. + Args: + project_name (Union[str, None]): Project name. + Returns: list[ProductTypeItem]: List of product type items for a project. """ diff --git a/openpype/tools/ayon_loader/models/products.py b/openpype/tools/ayon_loader/models/products.py index 135f28df97..40b6474d12 100644 --- a/openpype/tools/ayon_loader/models/products.py +++ b/openpype/tools/ayon_loader/models/products.py @@ -179,12 +179,15 @@ class ProductsModel: """Product type items for project. Args: - project_name (str): Project name. + project_name (Union[str, None]): Project name. Returns: list[ProductTypeItem]: Product type items. """ + if not project_name: + return [] + cache = self._product_type_items_cache[project_name] if not cache.is_valid: product_types = ayon_api.get_project_product_types(project_name) diff --git a/openpype/tools/ayon_loader/ui/window.py b/openpype/tools/ayon_loader/ui/window.py index a6d40d52e7..8982d92c0f 100644 --- a/openpype/tools/ayon_loader/ui/window.py +++ b/openpype/tools/ayon_loader/ui/window.py @@ -322,6 +322,7 @@ class LoaderWindow(QtWidgets.QWidget): ) def refresh(self): + self._reset_on_show = False self._controller.reset() def showEvent(self, event): @@ -332,6 +333,13 @@ class LoaderWindow(QtWidgets.QWidget): self._show_timer.start() + def closeEvent(self, event): + super(LoaderWindow, self).closeEvent(event) + # Deselect project so current context will be selected + # on next 'showEvent' + self._controller.set_selected_project(None) + self._reset_on_show = True + def keyPressEvent(self, event): modifiers = event.modifiers() ctrl_pressed = QtCore.Qt.ControlModifier & modifiers @@ -378,8 +386,7 @@ class LoaderWindow(QtWidgets.QWidget): self._show_timer.stop() if self._reset_on_show: - self._reset_on_show = False - self._controller.reset() + self.refresh() def _show_group_dialog(self): project_name = self._projects_combobox.get_selected_project_name() diff --git a/openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py b/openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py index 2ebed7f89b..1d1bd1adbc 100644 --- a/openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py +++ b/openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py @@ -1212,12 +1212,12 @@ class SwitchAssetDialog(QtWidgets.QDialog): )) version_ids = set() - version_docs_by_parent_id = {} + version_docs_by_parent_id_and_name = collections.defaultdict(dict) for version_doc in version_docs: - parent_id = version_doc["parent"] - if parent_id not in version_docs_by_parent_id: - version_ids.add(version_doc["_id"]) - version_docs_by_parent_id[parent_id] = version_doc + version_ids.add(version_doc["_id"]) + product_id = version_doc["parent"] + name = version_doc["name"] + version_docs_by_parent_id_and_name[product_id][name] = version_doc hero_version_docs_by_parent_id = {} for hero_version_doc in hero_version_docs: @@ -1242,7 +1242,7 @@ class SwitchAssetDialog(QtWidgets.QDialog): selected_product_name, selected_representation, product_docs_by_parent_and_name, - version_docs_by_parent_id, + version_docs_by_parent_id_and_name, hero_version_docs_by_parent_id, repre_docs_by_parent_id_by_name, ) @@ -1256,10 +1256,10 @@ class SwitchAssetDialog(QtWidgets.QDialog): container, loader, selected_folder_id, - product_name, + selected_product_name, selected_representation, product_docs_by_parent_and_name, - version_docs_by_parent_id, + version_docs_by_parent_id_and_name, hero_version_docs_by_parent_id, repre_docs_by_parent_id_by_name, ): @@ -1272,15 +1272,18 @@ class SwitchAssetDialog(QtWidgets.QDialog): container_product_id = container_version["parent"] container_product = self._product_docs_by_id[container_product_id] + container_product_name = container_product["name"] + + container_folder_id = container_product["parent"] if selected_folder_id: folder_id = selected_folder_id else: - folder_id = container_product["parent"] + folder_id = container_folder_id products_by_name = product_docs_by_parent_and_name[folder_id] - if product_name: - product_doc = products_by_name[product_name] + if selected_product_name: + product_doc = products_by_name[selected_product_name] else: product_doc = products_by_name[container_product["name"]] @@ -1300,7 +1303,26 @@ class SwitchAssetDialog(QtWidgets.QDialog): repre_doc = _repres.get(container_repre_name) if not repre_doc: - version_doc = version_docs_by_parent_id[product_id] + version_docs_by_name = ( + version_docs_by_parent_id_and_name[product_id] + ) + # If asset or subset are selected for switching, we use latest + # version else we try to keep the current container version. + version_name = None + if ( + selected_folder_id in (None, container_folder_id) + and selected_product_name in (None, container_product_name) + ): + version_name = container_version.get("name") + + version_doc = None + if version_name is not None: + version_doc = version_docs_by_name.get(version_name) + + if version_doc is None: + version_name = max(version_docs_by_name) + version_doc = version_docs_by_name[version_name] + version_id = version_doc["_id"] repres_by_name = repre_docs_by_parent_id_by_name[version_id] if selected_representation: diff --git a/openpype/tools/sceneinventory/switch_dialog.py b/openpype/tools/sceneinventory/switch_dialog.py index 150e369678..695f47b4d4 100644 --- a/openpype/tools/sceneinventory/switch_dialog.py +++ b/openpype/tools/sceneinventory/switch_dialog.py @@ -1299,15 +1299,21 @@ class SwitchAssetDialog(QtWidgets.QDialog): # If asset or subset are selected for switching, we use latest # version else we try to keep the current container version. + version_name = None if ( - selected_asset not in (None, container_asset_name) - or selected_subset not in (None, container_subset_name) + selected_asset in (None, container_asset_name) + and selected_subset in (None, container_subset_name) ): - version_name = max(version_docs_by_name) - else: - version_name = container_version["name"] + version_name = container_version.get("name") + + version_doc = None + if version_name is not None: + version_doc = version_docs_by_name.get(version_name) + + if version_doc is None: + version_name = max(version_docs_by_name) + version_doc = version_docs_by_name[version_name] - version_doc = version_docs_by_name[version_name] version_id = version_doc["_id"] repres_docs_by_name = repre_docs_by_parent_id_by_name[ version_id diff --git a/openpype/version.py b/openpype/version.py index 2f2fc517b8..d105b0169e 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.18.6-nightly.1" +__version__ = "3.18.7-nightly.2" diff --git a/pyproject.toml b/pyproject.toml index 24172aa77f..453833aae2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.18.5" # OpenPype +version = "3.18.6" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" diff --git a/server_addon/applications/server/applications.json b/server_addon/applications/server/applications.json index b0b12b2003..82dfd3b8d3 100644 --- a/server_addon/applications/server/applications.json +++ b/server_addon/applications/server/applications.json @@ -1175,30 +1175,6 @@ } ] }, - "djvview": { - "enabled": true, - "label": "DJV View", - "icon": "{}/app_icons/djvView.png", - "host_name": "", - "environment": "{}", - "variants": [ - { - "name": "1-1", - "label": "1.1", - "executables": { - "windows": [], - "darwin": [], - "linux": [] - }, - "arguments": { - "windows": [], - "darwin": [], - "linux": [] - }, - "environment": "{}" - } - ] - }, "wrap": { "enabled": true, "label": "Wrap", diff --git a/server_addon/fusion/server/settings.py b/server_addon/fusion/server/settings.py index b157ce9e40..a913db16da 100644 --- a/server_addon/fusion/server/settings.py +++ b/server_addon/fusion/server/settings.py @@ -57,9 +57,9 @@ class CreateSaverPluginModel(BaseSettingsModel): enum_resolver=_create_saver_instance_attributes_enum, title="Instance attributes" ) - output_formats: list[str] = SettingsField( - default_factory=list, - title="Output formats" + image_format: str = SettingsField( + enum_resolver=_image_format_enum, + title="Output Image Format" ) @@ -90,6 +90,8 @@ class CreateImageSaverModel(CreateSaverPluginModel): 0, title="Default rendered frame" ) + + class CreatPluginsModel(BaseSettingsModel): CreateSaver: CreateSaverModel = SettingsField( default_factory=CreateSaverModel, diff --git a/server_addon/fusion/server/version.py b/server_addon/fusion/server/version.py index ae7362549b..bbab0242f6 100644 --- a/server_addon/fusion/server/version.py +++ b/server_addon/fusion/server/version.py @@ -1 +1 @@ -__version__ = "0.1.3" +__version__ = "0.1.4" diff --git a/server_addon/nuke/server/settings/create_plugins.py b/server_addon/nuke/server/settings/create_plugins.py index 378d449b0b..6bdc5ee5ad 100644 --- a/server_addon/nuke/server/settings/create_plugins.py +++ b/server_addon/nuke/server/settings/create_plugins.py @@ -55,7 +55,10 @@ class CreateWriteRenderModel(BaseSettingsModel): enum_resolver=instance_attributes_enum, title="Instance attributes" ) - + exposed_knobs: list[str] = SettingsField( + title="Write Node Exposed Knobs", + default_factory=list + ) prenodes: list[PrenodeModel] = SettingsField( default_factory=list, title="Preceding nodes", @@ -81,7 +84,10 @@ class CreateWritePrerenderModel(BaseSettingsModel): enum_resolver=instance_attributes_enum, title="Instance attributes" ) - + exposed_knobs: list[str] = SettingsField( + title="Write Node Exposed Knobs", + default_factory=list + ) prenodes: list[PrenodeModel] = SettingsField( default_factory=list, title="Preceding nodes", @@ -107,7 +113,10 @@ class CreateWriteImageModel(BaseSettingsModel): enum_resolver=instance_attributes_enum, title="Instance attributes" ) - + exposed_knobs: list[str] = SettingsField( + title="Write Node Exposed Knobs", + default_factory=list + ) prenodes: list[PrenodeModel] = SettingsField( default_factory=list, title="Preceding nodes", @@ -146,6 +155,7 @@ DEFAULT_CREATE_SETTINGS = { "reviewable", "farm_rendering" ], + "exposed_knobs": [], "prenodes": [ { "name": "Reformat01", @@ -179,6 +189,7 @@ DEFAULT_CREATE_SETTINGS = { "farm_rendering", "use_range_limit" ], + "exposed_knobs": [], "prenodes": [] }, "CreateWriteImage": { @@ -191,6 +202,7 @@ DEFAULT_CREATE_SETTINGS = { "instance_attributes": [ "use_range_limit" ], + "exposed_knobs": [], "prenodes": [ { "name": "FrameHold01", diff --git a/server_addon/nuke/server/version.py b/server_addon/nuke/server/version.py index 9cb17e7976..c11f861afb 100644 --- a/server_addon/nuke/server/version.py +++ b/server_addon/nuke/server/version.py @@ -1 +1 @@ -__version__ = "0.1.8" +__version__ = "0.1.9"