Merge branch 'develop' into enhancement/OP-3622_Delivery-renamed-frame-numbers

This commit is contained in:
Kayla Man 2023-07-07 20:23:03 +08:00
commit 511c2a3b7c
17 changed files with 161 additions and 107 deletions

View file

@ -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)

View file

@ -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)):

View file

@ -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)):

View file

@ -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.")

View file

@ -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]

View file

@ -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

View file

@ -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)):

View file

@ -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:

View file

@ -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 <Scene>")
file_prefix = "<Scene>"
log.warning("Image prefix not set, using <Scene>")
prefix = "<Scene>"
return prefix

View file

@ -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:

View file

@ -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:

View file

@ -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)):

View file

@ -1,3 +1,4 @@
import os
import copy
import collections

View file

@ -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):

View file

@ -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"])

View file

@ -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)
)

View file

@ -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()