diff --git a/openpype/action.py b/openpype/action.py index 15c96404b6..6114c65fd4 100644 --- a/openpype/action.py +++ b/openpype/action.py @@ -49,7 +49,7 @@ def deprecated(new_destination): @deprecated("openpype.pipeline.publish.get_errored_instances_from_context") -def get_errored_instances_from_context(context): +def get_errored_instances_from_context(context, plugin=None): """ Deprecated: Since 3.14.* will be removed in 3.16.* or later. @@ -57,7 +57,7 @@ def get_errored_instances_from_context(context): from openpype.pipeline.publish import get_errored_instances_from_context - return get_errored_instances_from_context(context) + return get_errored_instances_from_context(context, plugin=plugin) @deprecated("openpype.pipeline.publish.get_errored_plugins_from_context") @@ -97,11 +97,9 @@ class RepairAction(pyblish.api.Action): # Get the errored instances self.log.info("Finding failed instances..") - errored_instances = get_errored_instances_from_context(context) - - # Apply pyblish.logic to get the instances for the plug-in - instances = pyblish.api.instances_by_plugin(errored_instances, plugin) - for instance in instances: + errored_instances = get_errored_instances_from_context(context, + plugin=plugin) + for instance in errored_instances: plugin.repair(instance) diff --git a/openpype/hosts/blender/api/action.py b/openpype/hosts/blender/api/action.py index fe0833e39f..dc49d6d9ae 100644 --- a/openpype/hosts/blender/api/action.py +++ b/openpype/hosts/blender/api/action.py @@ -12,13 +12,13 @@ class SelectInvalidAction(pyblish.api.Action): icon = "search" def process(self, context, plugin): - errored_instances = get_errored_instances_from_context(context) - instances = pyblish.api.instances_by_plugin(errored_instances, plugin) + errored_instances = get_errored_instances_from_context(context, + plugin=plugin) # Get the invalid nodes for the plug-ins self.log.info("Finding invalid nodes...") invalid = list() - for instance in instances: + for instance in errored_instances: invalid_nodes = plugin.get_invalid(instance) if invalid_nodes: if isinstance(invalid_nodes, (list, tuple)): diff --git a/openpype/hosts/fusion/api/action.py b/openpype/hosts/fusion/api/action.py index ff5dd14caa..347d552108 100644 --- a/openpype/hosts/fusion/api/action.py +++ b/openpype/hosts/fusion/api/action.py @@ -18,15 +18,13 @@ class SelectInvalidAction(pyblish.api.Action): icon = "search" # Icon from Awesome Icon def process(self, context, plugin): - errored_instances = get_errored_instances_from_context(context) - - # Apply pyblish.logic to get the instances for the plug-in - instances = pyblish.api.instances_by_plugin(errored_instances, plugin) + errored_instances = get_errored_instances_from_context(context, + plugin=plugin) # Get the invalid nodes for the plug-ins self.log.info("Finding invalid nodes..") invalid = list() - for instance in instances: + for instance in errored_instances: invalid_nodes = plugin.get_invalid(instance) if invalid_nodes: if isinstance(invalid_nodes, (list, tuple)): diff --git a/openpype/hosts/houdini/api/action.py b/openpype/hosts/houdini/api/action.py index 27e8ce55bb..77966d6d5c 100644 --- a/openpype/hosts/houdini/api/action.py +++ b/openpype/hosts/houdini/api/action.py @@ -17,15 +17,13 @@ class SelectInvalidAction(pyblish.api.Action): def process(self, context, plugin): - errored_instances = get_errored_instances_from_context(context) - - # Apply pyblish.logic to get the instances for the plug-in - instances = pyblish.api.instances_by_plugin(errored_instances, plugin) + errored_instances = get_errored_instances_from_context(context, + plugin=plugin) # Get the invalid nodes for the plug-ins self.log.info("Finding invalid nodes..") invalid = list() - for instance in instances: + for instance in errored_instances: invalid_nodes = plugin.get_invalid(instance) if invalid_nodes: if isinstance(invalid_nodes, (list, tuple)): @@ -44,3 +42,42 @@ class SelectInvalidAction(pyblish.api.Action): node.setCurrent(True) else: self.log.info("No invalid nodes found.") + + +class SelectROPAction(pyblish.api.Action): + """Select ROP. + + It's used to select the associated ROPs with the errored instances. + """ + + label = "Select ROP" + on = "failed" # This action is only available on a failed plug-in + icon = "mdi.cursor-default-click" + + def process(self, context, plugin): + errored_instances = get_errored_instances_from_context(context, plugin) + + # Get the invalid nodes for the plug-ins + self.log.info("Finding ROP nodes..") + rop_nodes = list() + for instance in errored_instances: + node_path = instance.data.get("instance_node") + if not node_path: + continue + + node = hou.node(node_path) + if not node: + continue + + rop_nodes.append(node) + + hou.clearAllSelected() + if rop_nodes: + self.log.info("Selecting ROP nodes: {}".format( + ", ".join(node.path() for node in rop_nodes) + )) + for node in rop_nodes: + node.setSelected(True) + node.setCurrent(True) + else: + self.log.info("No ROP nodes found.") diff --git a/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py index ed7f438729..d9dee38680 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py @@ -1,6 +1,12 @@ # -*- coding: utf-8 -*- import pyblish.api from openpype.pipeline import PublishValidationError +from openpype.hosts.houdini.api.action import ( + SelectInvalidAction, + SelectROPAction, +) + +import hou class ValidateSopOutputNode(pyblish.api.InstancePlugin): @@ -19,6 +25,7 @@ class ValidateSopOutputNode(pyblish.api.InstancePlugin): families = ["pointcache", "vdbcache"] hosts = ["houdini"] label = "Validate Output Node" + actions = [SelectROPAction, SelectInvalidAction] def process(self, instance): @@ -31,9 +38,6 @@ class ValidateSopOutputNode(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - - import hou - output_node = instance.data.get("output_node") if output_node is None: @@ -43,7 +47,7 @@ class ValidateSopOutputNode(pyblish.api.InstancePlugin): "Ensure a valid SOP output path is set." % node.path() ) - return [node.path()] + return [node] # Output node must be a Sop node. if not isinstance(output_node, hou.SopNode): @@ -53,7 +57,7 @@ class ValidateSopOutputNode(pyblish.api.InstancePlugin): "instead found category type: %s" % (output_node.path(), output_node.type().category().name()) ) - return [output_node.path()] + return [output_node] # For the sake of completeness also assert the category type # is Sop to avoid potential edge case scenarios even though @@ -73,11 +77,11 @@ class ValidateSopOutputNode(pyblish.api.InstancePlugin): except hou.Error as exc: cls.log.error("Cook failed: %s" % exc) cls.log.error(output_node.errors()[0]) - return [output_node.path()] + return [output_node] # Ensure the output node has at least Geometry data if not output_node.geometry(): cls.log.error( "Output node `%s` has no geometry data." % output_node.path() ) - return [output_node.path()] + return [output_node] diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 995c35792a..ccd4cd67e1 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -78,6 +78,14 @@ def read(container) -> dict: value.startswith(JSON_PREFIX): with contextlib.suppress(json.JSONDecodeError): value = json.loads(value[len(JSON_PREFIX):]) + + # default value behavior + # convert maxscript boolean values + if value == "true": + value = True + elif value == "false": + value = False + data[key.strip()] = value data["instance_node"] = container.Name diff --git a/openpype/hosts/maya/api/action.py b/openpype/hosts/maya/api/action.py index 065fdf3691..3b8e2c1848 100644 --- a/openpype/hosts/maya/api/action.py +++ b/openpype/hosts/maya/api/action.py @@ -111,15 +111,13 @@ class SelectInvalidAction(pyblish.api.Action): except ImportError: raise ImportError("Current host is not Maya") - errored_instances = get_errored_instances_from_context(context) - - # Apply pyblish.logic to get the instances for the plug-in - instances = pyblish.api.instances_by_plugin(errored_instances, plugin) + errored_instances = get_errored_instances_from_context(context, + plugin=plugin) # Get the invalid nodes for the plug-ins self.log.info("Finding invalid nodes..") invalid = list() - for instance in instances: + for instance in errored_instances: invalid_nodes = plugin.get_invalid(instance) if invalid_nodes: if isinstance(invalid_nodes, (list, tuple)): diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 8569bbd38f..fca4410ede 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -1522,7 +1522,15 @@ def set_attribute(attribute, value, node): cmds.addAttr(node, longName=attribute, **kwargs) node_attr = "{}.{}".format(node, attribute) - if "dataType" in kwargs: + enum_type = cmds.attributeQuery(attribute, node=node, enum=True) + if enum_type and value_type == "str": + enum_string_values = cmds.attributeQuery( + attribute, node=node, listEnum=True + )[0].split(":") + cmds.setAttr( + "{}.{}".format(node, attribute), enum_string_values.index(value) + ) + elif "dataType" in kwargs: attr_type = kwargs["dataType"] cmds.setAttr(node_attr, value, type=attr_type) else: diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index a6bcd003a5..7bfb53d500 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -274,12 +274,14 @@ class ARenderProducts: "Unsupported renderer {}".format(self.renderer) ) + # Note: When this attribute is never set (e.g. on maya launch) then + # this can return None even though it is a string attribute prefix = self._get_attr(prefix_attr) if not prefix: # Fall back to scene name by default - log.debug("Image prefix not set, using ") - file_prefix = "" + log.warning("Image prefix not set, using ") + prefix = "" return prefix diff --git a/openpype/hosts/nuke/api/actions.py b/openpype/hosts/nuke/api/actions.py index 92b83560da..c955a85acc 100644 --- a/openpype/hosts/nuke/api/actions.py +++ b/openpype/hosts/nuke/api/actions.py @@ -25,15 +25,13 @@ class SelectInvalidAction(pyblish.api.Action): except ImportError: raise ImportError("Current host is not Nuke") - errored_instances = get_errored_instances_from_context(context) - - # Apply pyblish.logic to get the instances for the plug-in - instances = pyblish.api.instances_by_plugin(errored_instances, plugin) + errored_instances = get_errored_instances_from_context(context, + plugin=plugin) # Get the invalid nodes for the plug-ins self.log.info("Finding invalid nodes..") invalid = list() - for instance in instances: + for instance in errored_instances: invalid_nodes = plugin.get_invalid(instance) if invalid_nodes: diff --git a/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py b/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py index 1c22c5b9d0..45c20412c8 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py +++ b/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py @@ -2,6 +2,7 @@ import os import pyblish.api import clique from openpype.pipeline import PublishXmlValidationError +from openpype.pipeline.publish import get_errored_instances_from_context class RepairActionBase(pyblish.api.Action): @@ -11,14 +12,7 @@ class RepairActionBase(pyblish.api.Action): @staticmethod def get_instance(context, plugin): # Get the errored instances - failed = [] - for result in context.data["results"]: - if (result["error"] is not None and result["instance"] is not None - and result["instance"] not in failed): - failed.append(result["instance"]) - - # Apply pyblish.logic to get the instances for the plug-in - return pyblish.api.instances_by_plugin(failed, plugin) + return get_errored_instances_from_context(context, plugin=plugin) def repair_knob(self, instances, state): for instance in instances: diff --git a/openpype/hosts/resolve/api/action.py b/openpype/hosts/resolve/api/action.py index ceedc2cc54..d1dffca7cc 100644 --- a/openpype/hosts/resolve/api/action.py +++ b/openpype/hosts/resolve/api/action.py @@ -27,15 +27,13 @@ class SelectInvalidAction(pyblish.api.Action): except ImportError: raise ImportError("Current host is not Resolve") - errored_instances = get_errored_instances_from_context(context) - - # Apply pyblish.logic to get the instances for the plug-in - instances = pyblish.api.instances_by_plugin(errored_instances, plugin) + errored_instances = get_errored_instances_from_context(context, + plugin=plugin) # Get the invalid nodes for the plug-ins self.log.info("Finding invalid clips..") invalid = list() - for instance in instances: + for instance in errored_instances: invalid_nodes = plugin.get_invalid(instance) if invalid_nodes: if isinstance(invalid_nodes, (list, tuple)): diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index fbb459ab12..947a90ef08 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -1,3 +1,4 @@ +import os import copy import collections diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index e0c3ced626..fe22f7401c 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -159,6 +159,8 @@ def deliver_single_file( delivery_path = delivery_path.replace("..", ".") # Make sure path is valid for all platforms delivery_path = os.path.normpath(delivery_path.replace("\\", "/")) + # Remove newlines from the end of the string to avoid OSError during copy + delivery_path = delivery_path.rstrip() delivery_folder = os.path.dirname(delivery_path) if not os.path.exists(delivery_folder): diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 471be5ddb8..0961d79234 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -577,12 +577,14 @@ def remote_publish(log, close_plugin_name=None, raise_error=False): raise RuntimeError(error_message) -def get_errored_instances_from_context(context): +def get_errored_instances_from_context(context, plugin=None): """Collect failed instances from pyblish context. Args: context (pyblish.api.Context): Publish context where we're looking for failed instances. + plugin (pyblish.api.Plugin): If provided then only consider errors + related to that plug-in. Returns: List[pyblish.lib.Instance]: Instances which failed during processing. @@ -594,6 +596,9 @@ def get_errored_instances_from_context(context): # When instance is None we are on the "context" result continue + if plugin is not None and result.get("plugin") != plugin: + continue + if result["error"]: instances.append(result["instance"]) diff --git a/openpype/pipeline/publish/publish_plugins.py b/openpype/pipeline/publish/publish_plugins.py index 1eec0760a1..ba3be6397e 100644 --- a/openpype/pipeline/publish/publish_plugins.py +++ b/openpype/pipeline/publish/publish_plugins.py @@ -234,11 +234,9 @@ class RepairAction(pyblish.api.Action): # Get the errored instances self.log.debug("Finding failed instances..") - errored_instances = get_errored_instances_from_context(context) - - # Apply pyblish.logic to get the instances for the plug-in - instances = pyblish.api.instances_by_plugin(errored_instances, plugin) - for instance in instances: + errored_instances = get_errored_instances_from_context(context, + plugin=plugin) + for instance in errored_instances: self.log.debug( "Attempting repair for instance: {} ...".format(instance) ) diff --git a/openpype/tools/sceneinventory/view.py b/openpype/tools/sceneinventory/view.py index 73d33392b9..57e6e24411 100644 --- a/openpype/tools/sceneinventory/view.py +++ b/openpype/tools/sceneinventory/view.py @@ -1,5 +1,6 @@ import collections import logging +import itertools from functools import partial from qtpy import QtWidgets, QtCore @@ -195,20 +196,17 @@ class SceneInventoryView(QtWidgets.QTreeView): version_name_by_id[version_doc["_id"]] = \ version_doc["name"] + # Specify version per item to update to + update_items = [] + update_versions = [] for item in items: repre_id = item["representation"] version_id = version_id_by_repre_id.get(repre_id) version_name = version_name_by_id.get(version_id) if version_name is not None: - try: - update_container(item, version_name) - except AssertionError: - self._show_version_error_dialog( - version_name, [item] - ) - log.warning("Update failed", exc_info=True) - - self.data_changed.emit() + update_items.append(item) + update_versions.append(version_name) + self._update_containers(update_items, update_versions) update_icon = qtawesome.icon( "fa.asterisk", @@ -225,16 +223,6 @@ class SceneInventoryView(QtWidgets.QTreeView): update_to_latest_action = None if has_outdated or has_loaded_hero_versions: - # update to latest version - def _on_update_to_latest(items): - for item in items: - try: - update_container(item, -1) - except AssertionError: - self._show_version_error_dialog(None, [item]) - log.warning("Update failed", exc_info=True) - self.data_changed.emit() - update_icon = qtawesome.icon( "fa.angle-double-up", color=DEFAULT_COLOR @@ -245,21 +233,11 @@ class SceneInventoryView(QtWidgets.QTreeView): menu ) update_to_latest_action.triggered.connect( - lambda: _on_update_to_latest(items) + lambda: self._update_containers(items, version=-1) ) change_to_hero = None if has_available_hero_version: - # change to hero version - def _on_update_to_hero(items): - for item in items: - try: - update_container(item, HeroVersionType(-1)) - except AssertionError: - self._show_version_error_dialog('hero', [item]) - log.warning("Update failed", exc_info=True) - self.data_changed.emit() - # TODO change icon change_icon = qtawesome.icon( "fa.asterisk", @@ -271,7 +249,8 @@ class SceneInventoryView(QtWidgets.QTreeView): menu ) change_to_hero.triggered.connect( - lambda: _on_update_to_hero(items) + lambda: self._update_containers(items, + version=HeroVersionType(-1)) ) # set version @@ -740,14 +719,7 @@ class SceneInventoryView(QtWidgets.QTreeView): if label: version = versions_by_label[label] - for item in items: - try: - update_container(item, version) - except AssertionError: - self._show_version_error_dialog(version, [item]) - log.warning("Update failed", exc_info=True) - # refresh model when done - self.data_changed.emit() + self._update_containers(items, version) def _show_switch_dialog(self, items): """Display Switch dialog""" @@ -782,9 +754,9 @@ class SceneInventoryView(QtWidgets.QTreeView): Args: version: str or int or None """ - if not version: + if version == -1: version_str = "latest" - elif version == "hero": + elif isinstance(version, HeroVersionType): version_str = "hero" elif isinstance(version, int): version_str = "v{:03d}".format(version) @@ -841,10 +813,43 @@ class SceneInventoryView(QtWidgets.QTreeView): return # Trigger update to latest - for item in outdated_items: - try: - update_container(item, -1) - except AssertionError: - self._show_version_error_dialog(None, [item]) - log.warning("Update failed", exc_info=True) - self.data_changed.emit() + self._update_containers(outdated_items, version=-1) + + def _update_containers(self, items, version): + """Helper to update items to given version (or version per item) + + If at least one item is specified this will always try to refresh + the inventory even if errors occurred on any of the items. + + Arguments: + items (list): Items to update + version (int or list): Version to set to. + This can be a list specifying a version for each item. + Like `update_container` version -1 sets the latest version + and HeroTypeVersion instances set the hero version. + + """ + + if isinstance(version, (list, tuple)): + # We allow a unique version to be specified per item. In that case + # the length must match with the items + assert len(items) == len(version), ( + "Number of items mismatches number of versions: " + "{} items - {} versions".format(len(items), len(version)) + ) + versions = version + else: + # Repeat the same version infinitely + versions = itertools.repeat(version) + + # Trigger update to latest + try: + for item, item_version in zip(items, versions): + try: + update_container(item, item_version) + except AssertionError: + self._show_version_error_dialog(item_version, [item]) + log.warning("Update failed", exc_info=True) + finally: + # Always update the scene inventory view, even if errors occurred + self.data_changed.emit()