From 7baa3bdd6a12732dcc62bc957c815c23c96b511a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 23 Jul 2020 14:55:07 +0200 Subject: [PATCH 01/15] support for single frame renders from maya --- .../global/publish/submit_publish_job.py | 38 +++++++++++++------ .../maya/publish/submit_maya_deadline.py | 35 ++++++++++++----- 2 files changed, 52 insertions(+), 21 deletions(-) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index 4f32e37c17..cec5f61011 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -380,15 +380,22 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # go through aovs in expected files for aov, files in exp_files[0].items(): cols, rem = clique.assemble(files) - # we shouldn't have any reminders - if rem: - self.log.warning( - "skipping unexpected files found " - "in sequence: {}".format(rem)) - - # but we really expect only one collection, nothing else make sense - assert len(cols) == 1, "only one image sequence type is expected" + # we shouldn't have any reminders. And if we do, it should + # be just one item for single frame renders. + if not cols and rem: + assert len(rem) == 1, ("Found multiple non related files " + "to render, don't know what to do " + "with them.") + col = rem[0] + _, ext = os.path.splitext(col) + else: + # but we really expect only one collection. + # Nothing else make sense. + assert len(cols) == 1, "only one image sequence type is expected" # noqa: E501 + _, ext = os.path.splitext(cols[0].tail) + col = list(cols[0]) + self.log.debug(col) # create subset name `familyTaskSubset_AOV` group_name = 'render{}{}{}{}'.format( task[0].upper(), task[1:], @@ -396,7 +403,11 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): subset_name = '{}_{}'.format(group_name, aov) - staging = os.path.dirname(list(cols[0])[0]) + if isinstance(col, (list, tuple)): + staging = os.path.dirname(col[0]) + else: + staging = os.path.dirname(col) + success, rootless_staging_dir = ( self.anatomy.find_root_template_from_path(staging) ) @@ -421,13 +432,16 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): new_instance["subset"] = subset_name new_instance["subsetGroup"] = group_name - ext = cols[0].tail.lstrip(".") - # create represenation + if isinstance(col, (list, tuple)): + files = [os.path.basename(f) for f in col] + else: + files = os.path.basename(col) + rep = { "name": ext, "ext": ext, - "files": [os.path.basename(f) for f in list(cols[0])], + "files": files, "frameStart": int(instance_data.get("frameStartHandle")), "frameEnd": int(instance_data.get("frameEndHandle")), # If expectedFile are absolute, we need only filenames diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index d81d43749c..d5500f7aa8 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -20,6 +20,7 @@ import os import json import getpass import copy +import re import clique import requests @@ -108,8 +109,8 @@ def get_renderer_variables(renderlayer, root): # does not work for vray. scene = cmds.file(query=True, sceneName=True) scene, _ = os.path.splitext(os.path.basename(scene)) - filename_0 = filename_prefix.replace('', scene) - filename_0 = filename_0.replace('', renderlayer) + filename_0 = re.sub('', scene, filename_prefix, flags=re.IGNORECASE) # noqa: E501 + filename_0 = re.sub('', renderlayer, filename_0, flags=re.IGNORECASE) # noqa: E501 filename_0 = "{}.{}.{}".format( filename_0, "#" * int(padding), extension) filename_0 = os.path.normpath(os.path.join(root, filename_0)) @@ -375,16 +376,32 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): if isinstance(exp[0], dict): # we have aovs and we need to iterate over them for _aov, files in exp[0].items(): - col = clique.assemble(files)[0][0] - output_file = col.format('{head}{padding}{tail}') - payload['JobInfo']['OutputFilename' + str(exp_index)] = output_file # noqa: E501 + col, rem = clique.assemble(files) + if not col and rem: + # we couldn't find any collections but have + # individual files. + assert len(rem) == 1, ("Found multiple non related files " + "to render, don't know what to do " + "with them.") + payload['JobInfo']['OutputFilename' + str(exp_index)] = rem[0] # noqa: E501 + output_file = rem[0] + else: + output_file = col.format('{head}{padding}{tail}') + payload['JobInfo']['OutputFilename' + str(exp_index)] = output_file # noqa: E501 output_filenames[exp_index] = output_file exp_index += 1 else: - col = clique.assemble(files)[0][0] - output_file = col.format('{head}{padding}{tail}') - payload['JobInfo']['OutputFilename' + str(exp_index)] = output_file - # OutputFilenames[exp_index] = output_file + col, rem = clique.assemble(files) + if not col and rem: + # we couldn't find any collections but have + # individual files. + assert len(rem) == 1, ("Found multiple non related files " + "to render, don't know what to do " + "with them.") + payload['JobInfo']['OutputFilename' + str(exp_index)] = rem[0] # noqa: E501 + else: + output_file = col.format('{head}{padding}{tail}') + payload['JobInfo']['OutputFilename' + str(exp_index)] = output_file # noqa: E501 plugin = payload["JobInfo"]["Plugin"] self.log.info("using render plugin : {}".format(plugin)) From bd7876fe20acd526c56cdca408dd89805e7ce588 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 24 Jul 2020 19:52:20 +0200 Subject: [PATCH 02/15] bake custom attributes to camera during export --- .../maya/publish/extract_camera_alembic.py | 9 ++++ .../maya/publish/extract_camera_mayaAscii.py | 50 +++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/pype/plugins/maya/publish/extract_camera_alembic.py b/pype/plugins/maya/publish/extract_camera_alembic.py index cc090760ff..c61ec5e19e 100644 --- a/pype/plugins/maya/publish/extract_camera_alembic.py +++ b/pype/plugins/maya/publish/extract_camera_alembic.py @@ -19,6 +19,7 @@ class ExtractCameraAlembic(pype.api.Extractor): label = "Camera (Alembic)" hosts = ["maya"] families = ["camera"] + bake_attributes = [] def process(self, instance): @@ -66,6 +67,14 @@ class ExtractCameraAlembic(pype.api.Extractor): job_str += ' -file "{0}"'.format(path) + # bake specified attributes in preset + assert isinstance(self.bake_attributes, (list, tuple)), ( + "Attributes to bake must be specified as a list" + ) + for attr in self.bake_attributes: + self.log.info("Adding {} attribute".format(attr)) + job_str += " -attr {0}".format(attr) + with lib.evaluation("off"): with avalon.maya.suspended_refresh(): cmds.AbcExport(j=job_str, verbose=False) diff --git a/pype/plugins/maya/publish/extract_camera_mayaAscii.py b/pype/plugins/maya/publish/extract_camera_mayaAscii.py index 973d8d452a..eb3b1671de 100644 --- a/pype/plugins/maya/publish/extract_camera_mayaAscii.py +++ b/pype/plugins/maya/publish/extract_camera_mayaAscii.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +"""Extract camera to Maya file.""" import os from maya import cmds @@ -65,6 +67,45 @@ def unlock(plug): cmds.disconnectAttr(source, destination) +def bake_attribute(camera, + attributes, + step=1.0, simulation=True, frame_range=None): + """Bake specified attributes on camera. + + Args: + camera (str): Camera name. + attributes (list): List of attributes to bake. + step (float): Animation step used for baking. + simulation (bool): Perform simulation instead of just evaluating + each attribute separately over the range of time. + frame_rage (list, tuple): start and end frame to define range. + + .. See also: + http://download.autodesk.com/us/maya/2011help/Commandspython/bakeResults.html + + """ + + if frame_range is None: + frame_range = [cmds.playbackOptions(query=True, minTime=True), + cmds.playbackOptions(query=True, maxTime=True)] + + # If frame range is single frame bake one frame more, + # otherwise maya.cmds.bakeResults gets confused + if frame_range[1] == frame_range[0]: + frame_range[1] += 1 + + assert isinstance(attributes, (list, tuple)), ( + "Attributes to bake must be specified as a list" + ) + + with lib.keytangent_default(in_tangent_type='auto', + out_tangent_type='auto'): + cmds.bakeResults(camera, attribute=attributes, + simulation=simulation, + time=(frame_range[0], frame_range[1]), + sampleBy=step) + + class ExtractCameraMayaAscii(pype.api.Extractor): """Extract a Camera as Maya Ascii. @@ -84,6 +125,7 @@ class ExtractCameraMayaAscii(pype.api.Extractor): label = "Camera (Maya Ascii)" hosts = ["maya"] families = ["camera"] + bake_attributes = [] def process(self, instance): @@ -148,6 +190,14 @@ class ExtractCameraMayaAscii(pype.api.Extractor): unlock(plug) cmds.setAttr(plug, value) + if self.bake_attributes: + self.log.info( + "Baking attributes: {}".format( + self.bake_attributes)) + bake_attribute( + cam, self.bake_attributes, + frame_range=range_with_handles, step=step) + self.log.info("Performing extraction..") cmds.select(baked_shapes, noExpand=True) cmds.file(path, From a47ffde54203adcff6b87cf2d379ab402fea4fb1 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 27 Jul 2020 11:05:32 +0200 Subject: [PATCH 03/15] sanitize camera names --- pype/hosts/maya/expected_files.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/pype/hosts/maya/expected_files.py b/pype/hosts/maya/expected_files.py index a7204cba93..77d55eb1c1 100644 --- a/pype/hosts/maya/expected_files.py +++ b/pype/hosts/maya/expected_files.py @@ -158,6 +158,25 @@ class AExpectedFiles: """To be implemented by renderer class.""" pass + def sanitize_camera_name(self, camera): + """Sanitize camera name. + + Remove Maya illegal characters from camera name. + + Args: + camera (str): Maya camera name. + + Returns: + (str): sanitized camera name + + Example: + >>> sanizite_camera_name('test:camera_01') + test_camera_01 + + """ + sanitized = re.sub('[^0-9a-zA-Z_]+', '_', camera) + return sanitized + def get_renderer_prefix(self): """Return prefix for specific renderer. @@ -252,7 +271,7 @@ class AExpectedFiles: mappings = ( (R_SUBSTITUTE_SCENE_TOKEN, layer_data["sceneName"]), (R_SUBSTITUTE_LAYER_TOKEN, layer_data["layerName"]), - (R_SUBSTITUTE_CAMERA_TOKEN, cam), + (R_SUBSTITUTE_CAMERA_TOKEN, self.sanitize_camera_name(cam)), # this is required to remove unfilled aov token, for example # in Redshift (R_REMOVE_AOV_TOKEN, ""), @@ -287,7 +306,8 @@ class AExpectedFiles: mappings = ( (R_SUBSTITUTE_SCENE_TOKEN, layer_data["sceneName"]), (R_SUBSTITUTE_LAYER_TOKEN, layer_data["layerName"]), - (R_SUBSTITUTE_CAMERA_TOKEN, cam), + (R_SUBSTITUTE_CAMERA_TOKEN, + self.sanitize_camera_name(cam)), (R_SUBSTITUTE_AOV_TOKEN, aov[0]), (R_CLEAN_FRAME_TOKEN, ""), (R_CLEAN_EXT_TOKEN, ""), @@ -314,7 +334,8 @@ class AExpectedFiles: # camera name to AOV to allow per camera AOVs. aov_name = aov[0] if len(layer_data["cameras"]) > 1: - aov_name = "{}_{}".format(aov[0], cam) + aov_name = "{}_{}".format(aov[0], + self.sanitize_camera_name(cam)) aov_file_list[aov_name] = aov_files file_prefix = layer_data["filePrefix"] From 86119ea823c7436461e6aa4ca3681ea3febcd9d3 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 27 Jul 2020 13:37:16 +0200 Subject: [PATCH 04/15] support for updating renderSetup settings --- pype/plugins/maya/load/load_rendersetup.py | 63 +++++++++++++++++++--- 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/pype/plugins/maya/load/load_rendersetup.py b/pype/plugins/maya/load/load_rendersetup.py index b38e2988b1..fae79548a5 100644 --- a/pype/plugins/maya/load/load_rendersetup.py +++ b/pype/plugins/maya/load/load_rendersetup.py @@ -1,14 +1,23 @@ -from avalon import api -import maya.app.renderSetup.model.renderSetup as renderSetup -from avalon.maya import lib -from maya import cmds +# -*- coding: utf-8 -*- +"""Load and update RenderSetup settings. + +Working with RenderSetup setting is Maya is done utilizing json files. +When this json is loaded, it will overwrite all settings on RenderSetup +instance. +""" + import json +from avalon import api +from avalon.maya import lib +from pype.hosts.maya import lib as pypelib + +from maya import cmds +import maya.app.renderSetup.model.renderSetup as renderSetup + class RenderSetupLoader(api.Loader): - """ - This will load json preset for RenderSetup, overwriting current one. - """ + """Load json preset for RenderSetup overwriting current one.""" families = ["rendersetup"] representations = ["json"] @@ -19,7 +28,7 @@ class RenderSetupLoader(api.Loader): color = "orange" def load(self, context, name, namespace, data): - + """Load RenderSetup settings.""" from avalon.maya.pipeline import containerise # from pype.hosts.maya.lib import namespaced @@ -48,3 +57,41 @@ class RenderSetupLoader(api.Loader): nodes=nodes, context=context, loader=self.__class__.__name__) + + def remove(self, container): + """Remove RenderSetup settings instance.""" + from maya import cmds + + namespace = container["namespace"] + container_name = container["objectName"] + + self.log.info("Removing '%s' from Maya.." % container["name"]) + + container_content = cmds.sets(container_name, query=True) + nodes = cmds.ls(container_content, long=True) + + nodes.append(container_name) + + try: + cmds.delete(nodes) + except ValueError: + # Already implicitly deleted by Maya upon removing reference + pass + + cmds.namespace(removeNamespace=namespace, deleteNamespaceContent=True) + + def update(self, container, representation): + """Update RenderSetup setting by overwriting existing settings.""" + pypelib.show_message( + "Render setup update", + "Render setup setting will be overwritten by new version. All " + "setting specified by user not included in loaded version " + "will be lost.") + path = api.get_representation_path(representation) + with open(path, "r") as file: + renderSetup.instance().decode( + json.load(file), renderSetup.DECODE_AND_OVERWRITE, None) + + def switch(self, container, representation): + """Switch representations.""" + self.update(container, representation) From 7ce31ba1ecf45a5a03b5c4d3e3d19c1b780c9e4d Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 28 Jul 2020 13:14:20 +0200 Subject: [PATCH 05/15] small code fixes --- pype/plugins/maya/load/load_rendersetup.py | 23 ++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/pype/plugins/maya/load/load_rendersetup.py b/pype/plugins/maya/load/load_rendersetup.py index fae79548a5..45a314a9d1 100644 --- a/pype/plugins/maya/load/load_rendersetup.py +++ b/pype/plugins/maya/load/load_rendersetup.py @@ -7,6 +7,8 @@ instance. """ import json +import six +import sys from avalon import api from avalon.maya import lib @@ -38,7 +40,7 @@ class RenderSetupLoader(api.Loader): prefix="_" if asset[0].isdigit() else "", suffix="_", ) - + self.log.info(">>> loading json [ {} ]".format(self.fname)) with open(self.fname, "r") as file: renderSetup.instance().decode( json.load(file), renderSetup.DECODE_AND_OVERWRITE, None) @@ -51,6 +53,7 @@ class RenderSetupLoader(api.Loader): if not nodes: return + self.log.info(">>> containerising [ {} ]".format(name)) return containerise( name=name, namespace=namespace, @@ -62,7 +65,6 @@ class RenderSetupLoader(api.Loader): """Remove RenderSetup settings instance.""" from maya import cmds - namespace = container["namespace"] container_name = container["objectName"] self.log.info("Removing '%s' from Maya.." % container["name"]) @@ -78,8 +80,6 @@ class RenderSetupLoader(api.Loader): # Already implicitly deleted by Maya upon removing reference pass - cmds.namespace(removeNamespace=namespace, deleteNamespaceContent=True) - def update(self, container, representation): """Update RenderSetup setting by overwriting existing settings.""" pypelib.show_message( @@ -89,8 +89,19 @@ class RenderSetupLoader(api.Loader): "will be lost.") path = api.get_representation_path(representation) with open(path, "r") as file: - renderSetup.instance().decode( - json.load(file), renderSetup.DECODE_AND_OVERWRITE, None) + try: + renderSetup.instance().decode( + json.load(file), renderSetup.DECODE_AND_OVERWRITE, None) + except Exception: + self.log.error("There were errors during loading") + six.reraise(*sys.exc_info()) + + # Update metadata + node = container["objectName"] + cmds.setAttr("{}.representation".format(node), + str(representation["_id"]), + type="string") + self.log.info("... updated") def switch(self, container, representation): """Switch representations.""" From edf61b7c28781b710dd3ce1f5495ec21e2002ed6 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 29 Jul 2020 10:51:23 +0200 Subject: [PATCH 06/15] bump version to 2.11.1 --- pype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/version.py b/pype/version.py index 7f6646a762..200c236308 100644 --- a/pype/version.py +++ b/pype/version.py @@ -1 +1 @@ -__version__ = "2.11.0" +__version__ = "2.11.1" From 832fd9fa66b67c5a6dacb7817745f84cfbea4e4b Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 29 Jul 2020 16:17:15 +0100 Subject: [PATCH 07/15] Fix multiple attributes on the same node overwriting. --- pype/plugins/maya/publish/validate_attributes.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pype/plugins/maya/publish/validate_attributes.py b/pype/plugins/maya/publish/validate_attributes.py index 6ecebfa107..a77fbe5e93 100644 --- a/pype/plugins/maya/publish/validate_attributes.py +++ b/pype/plugins/maya/publish/validate_attributes.py @@ -62,9 +62,16 @@ class ValidateAttributes(pyblish.api.ContextPlugin): for family in families: for preset in presets[family]: [node_name, attribute_name] = preset.split(".") - attributes.update( - {node_name: {attribute_name: presets[family][preset]}} - ) + try: + attributes[node_name].update( + {attribute_name: presets[family][preset]} + ) + except KeyError: + attributes.update({ + node_name: { + attribute_name: presets[family][preset] + } + }) # Get invalid attributes. nodes = pm.ls() From 3925ddc5aeabe392230224247bd909c73031fed2 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 29 Jul 2020 17:34:54 +0100 Subject: [PATCH 08/15] Instance in Same Context for Nuke Moved the equivalent Maya plugin into global and merged with Nuke. Fixed Nuke instances collection not using node data for asset. --- .../publish/validate_instance_in_context.py | 133 ++++++++++++++++++ .../publish/validate_instance_in_context.py | 108 -------------- .../plugins/nuke/publish/collect_instances.py | 2 +- 3 files changed, 134 insertions(+), 109 deletions(-) create mode 100644 pype/plugins/global/publish/validate_instance_in_context.py delete mode 100644 pype/plugins/maya/publish/validate_instance_in_context.py diff --git a/pype/plugins/global/publish/validate_instance_in_context.py b/pype/plugins/global/publish/validate_instance_in_context.py new file mode 100644 index 0000000000..a4fc555161 --- /dev/null +++ b/pype/plugins/global/publish/validate_instance_in_context.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +"""Validate if instance asset is the same as context asset.""" +from __future__ import absolute_import + +import pyblish.api +import pype.api + + +class SelectInvalidInstances(pyblish.api.Action): + """Select invalid instances in Outliner.""" + + label = "Select Instances" + icon = "briefcase" + on = "failed" + + def process(self, context, plugin): + """Process invalid validators and select invalid instances.""" + # Get the errored instances + failed = [] + for result in context.data["results"]: + if result["error"] is None: + continue + if result["instance"] is None: + continue + if result["instance"] in failed: + continue + if result["plugin"] != plugin: + continue + + failed.append(result["instance"]) + + # Apply pyblish.logic to get the instances for the plug-in + instances = pyblish.api.instances_by_plugin(failed, plugin) + + if instances: + self.log.info( + "Selecting invalid nodes: %s" % ", ".join( + [str(x) for x in instances] + ) + ) + self.select(instances) + else: + self.log.info("No invalid nodes found.") + self.deselect() + + def select(self, instances): + if "nuke" in pyblish.api.registered_hosts(): + import avalon.nuke.lib + import nuke + avalon.nuke.lib.select_nodes( + [nuke.toNode(str(x)) for x in instances] + ) + + if "maya" in pyblish.api.registered_hosts(): + from maya import cmds + cmds.select(instances, replace=True, noExpand=True) + + def deselect(self): + if "nuke" in pyblish.api.registered_hosts(): + import avalon.nuke.lib + avalon.nuke.lib.reset_selection() + + if "maya" in pyblish.api.registered_hosts(): + from maya import cmds + cmds.select(deselect=True) + + +class RepairSelectInvalidInstances(pyblish.api.Action): + """Repair the instance asset.""" + + label = "Repair" + icon = "wrench" + on = "failed" + + def process(self, context, plugin): + # Get the errored instances + failed = [] + for result in context.data["results"]: + if result["error"] is None: + continue + if result["instance"] is None: + continue + if result["instance"] in failed: + continue + if result["plugin"] != plugin: + continue + + failed.append(result["instance"]) + + # Apply pyblish.logic to get the instances for the plug-in + instances = pyblish.api.instances_by_plugin(failed, plugin) + + context_asset = context.data["assetEntity"]["name"] + for instance in instances: + self.set_attribute(instance, context_asset) + + def set_attribute(self, instance, context_asset): + if "nuke" in pyblish.api.registered_hosts(): + import nuke + nuke.toNode( + instance.data.get("name") + )["avalon:asset"].setValue(context_asset) + + if "maya" in pyblish.api.registered_hosts(): + from maya import cmds + cmds.setAttr( + instance.data.get("name") + ".asset", + context_asset, + type="string" + ) + + +class ValidateInstanceInContext(pyblish.api.InstancePlugin): + """Validator to check if instance asset match context asset. + + When working in per-shot style you always publish data in context of + current asset (shot). This validator checks if this is so. It is optional + so it can be disabled when needed. + + Action on this validator will select invalid instances in Outliner. + """ + + order = pype.api.ValidateContentsOrder + label = "Instance in same Context" + optional = True + hosts = ["maya", "nuke"] + actions = [SelectInvalidInstances, RepairSelectInvalidInstances] + + def process(self, instance): + asset = instance.data.get("asset") + context_asset = instance.context.data["assetEntity"]["name"] + msg = "{} has asset {}".format(instance.name, asset) + assert asset == context_asset, msg diff --git a/pype/plugins/maya/publish/validate_instance_in_context.py b/pype/plugins/maya/publish/validate_instance_in_context.py deleted file mode 100644 index 542249bb2d..0000000000 --- a/pype/plugins/maya/publish/validate_instance_in_context.py +++ /dev/null @@ -1,108 +0,0 @@ -# -*- coding: utf-8 -*- -"""Validate if instance asset is the same as context asset.""" -from __future__ import absolute_import -import pyblish.api -from pype.action import get_errored_instances_from_context -import pype.api - - -class SelectInvalidInstances(pyblish.api.Action): - """Select invalid instances in Outliner.""" - - label = "Show Instances" - icon = "briefcase" - on = "failed" - - def process(self, context, plugin): - """Process invalid validators and select invalid instances.""" - try: - from maya import cmds - 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) - - # Get the invalid nodes for the plug-ins - self.log.info("Finding invalid nodes..") - invalid = list() - for _instance in instances: - invalid_instances = plugin.get_invalid(context) - if invalid_instances: - if isinstance(invalid_instances, (list, tuple)): - invalid.extend(invalid_instances) - else: - self.log.warning("Plug-in returned to be invalid, " - "but has no selectable nodes.") - - # Ensure unique (process each node only once) - invalid = list(set(invalid)) - - if invalid: - self.log.info("Selecting invalid nodes: %s" % ", ".join(invalid)) - cmds.select(invalid, replace=True, noExpand=True) - else: - self.log.info("No invalid nodes found.") - cmds.select(deselect=True) - - -class RepairSelectInvalidInstances(pyblish.api.Action): - """Repair the instance asset.""" - - label = "Repair" - icon = "wrench" - on = "failed" - - def process(self, context, plugin): - from maya import cmds - # 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 - instances = pyblish.api.instances_by_plugin(failed, plugin) - context_asset = context.data["assetEntity"]["name"] - for instance in instances: - cmds.setAttr(instance.data.get("name") + ".asset", - context_asset, type="string") - - -class ValidateInstanceInContext(pyblish.api.ContextPlugin): - """Validator to check if instance asset match context asset. - - When working in per-shot style you always publish data in context of - current asset (shot). This validator checks if this is so. It is optional - so it can be disabled when needed. - - Action on this validator will select invalid instances in Outliner. - """ - - order = pype.api.ValidateContentsOrder - label = "Instance in same Context" - optional = True - actions = [SelectInvalidInstances, RepairSelectInvalidInstances] - - @classmethod - def get_invalid(cls, context): - """Get invalid instances.""" - invalid = [] - context_asset = context.data["assetEntity"]["name"] - cls.log.info("we are in {}".format(context_asset)) - for instance in context: - asset = instance.data.get("asset") - if asset != context_asset: - cls.log.warning("{} has asset {}".format(instance.name, asset)) - invalid.append(instance.name) - - return invalid - - def process(self, context): - """Check instances.""" - invalid = self.get_invalid(context) - if invalid: - raise AssertionError("Some instances doesn't share same context") diff --git a/pype/plugins/nuke/publish/collect_instances.py b/pype/plugins/nuke/publish/collect_instances.py index 0bbede11c0..9085e12bd8 100644 --- a/pype/plugins/nuke/publish/collect_instances.py +++ b/pype/plugins/nuke/publish/collect_instances.py @@ -106,7 +106,7 @@ class CollectNukeInstances(pyblish.api.ContextPlugin): instance.data.update({ "subset": subset, - "asset": os.environ["AVALON_ASSET"], + "asset": avalon_knob_data["asset"], "label": node.name(), "name": node.name(), "subset": subset, From 907ebe17604142a2487879a9004247bc81cf2c54 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 30 Jul 2020 08:46:53 +0100 Subject: [PATCH 09/15] Fix viewer input process node return as Viewer node --- pype/hosts/nuke/lib.py | 2 +- pype/plugins/nuke/publish/extract_thumbnail.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/hosts/nuke/lib.py b/pype/hosts/nuke/lib.py index 72a8836a03..8c0e37b15d 100644 --- a/pype/hosts/nuke/lib.py +++ b/pype/hosts/nuke/lib.py @@ -1445,7 +1445,7 @@ class ExporterReview: anlib.reset_selection() ipn_orig = None for v in [n for n in nuke.allNodes() - if "Viewer" in n.Class()]: + if "Viewer" == n.Class()]: ip = v['input_process'].getValue() ipn = v['input_process_node'].getValue() if "VIEWER_INPUT" not in ipn and ip: diff --git a/pype/plugins/nuke/publish/extract_thumbnail.py b/pype/plugins/nuke/publish/extract_thumbnail.py index 5e9302a01a..a3ef09bc9f 100644 --- a/pype/plugins/nuke/publish/extract_thumbnail.py +++ b/pype/plugins/nuke/publish/extract_thumbnail.py @@ -152,7 +152,7 @@ class ExtractThumbnail(pype.api.Extractor): ipn_orig = None for v in [n for n in nuke.allNodes() - if "Viewer" in n.Class()]: + if "Viewer" == n.Class()]: ip = v['input_process'].getValue() ipn = v['input_process_node'].getValue() if "VIEWER_INPUT" not in ipn and ip: From 8934fba38883963349d5773de9dd3c7535bd35fc Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 30 Jul 2020 17:45:05 +0200 Subject: [PATCH 10/15] make png and jpeg configurable in config --- pype/plugins/photoshop/publish/extract_image.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pype/plugins/photoshop/publish/extract_image.py b/pype/plugins/photoshop/publish/extract_image.py index 1bb13bce6b..6dfccdc4f2 100644 --- a/pype/plugins/photoshop/publish/extract_image.py +++ b/pype/plugins/photoshop/publish/extract_image.py @@ -13,6 +13,7 @@ class ExtractImage(pype.api.Extractor): label = "Extract Image" hosts = ["photoshop"] families = ["image"] + formats = ["png", "jpg"] def process(self, instance): @@ -32,10 +33,12 @@ class ExtractImage(pype.api.Extractor): if layer.id not in extract_ids: layer.Visible = False - save_options = { - "png": photoshop.com_objects.PNGSaveOptions(), - "jpg": photoshop.com_objects.JPEGSaveOptions() - } + save_options = {} + if "png" in self.formats: + save_options["png"] = photoshop.com_objects.PNGSaveOptions() + if "jpg" in self.formats: + save_options["jpg"] = photoshop.com_objects.JPEGSaveOptions() + file_basename = os.path.splitext( photoshop.app().ActiveDocument.Name )[0] From c59eeab6e60aae9f01bcd399d387259e41646a2d Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 31 Jul 2020 15:35:06 +0200 Subject: [PATCH 11/15] some code cleanup --- pype/plugins/global/publish/cleanup.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/pype/plugins/global/publish/cleanup.py b/pype/plugins/global/publish/cleanup.py index 3ab41f90ca..bca540078f 100644 --- a/pype/plugins/global/publish/cleanup.py +++ b/pype/plugins/global/publish/cleanup.py @@ -1,11 +1,18 @@ +# -*- coding: utf-8 -*- +"""Cleanup leftover files from publish.""" import os import shutil import pyblish.api def clean_renders(instance): - transfers = instance.data.get("transfers", list()) + """Delete renders after publishing. + Args: + instance (pyblish.api.Instace): Instance to work on. + + """ + transfers = instance.data.get("transfers", list()) current_families = instance.data.get("families", list()) instance_family = instance.data.get("family", None) dirnames = [] @@ -40,6 +47,7 @@ class CleanUp(pyblish.api.InstancePlugin): active = True def process(self, instance): + """Plugin entry point.""" # Get the errored instances failed = [] for result in instance.context.data["results"]: @@ -52,7 +60,7 @@ class CleanUp(pyblish.api.InstancePlugin): ) ) - self.log.info("Cleaning renders ...") + self.log.info("Performing cleanup on {}".format(instance)) clean_renders(instance) if [ef for ef in self.exclude_families @@ -60,16 +68,17 @@ class CleanUp(pyblish.api.InstancePlugin): return import tempfile - staging_dir = instance.data.get("stagingDir", None) - if not staging_dir or not os.path.exists(staging_dir): - self.log.info("No staging directory found: %s" % staging_dir) - return - temp_root = tempfile.gettempdir() + staging_dir = instance.data.get("stagingDir", None) + if not os.path.normpath(staging_dir).startswith(temp_root): self.log.info("Skipping cleanup. Staging directory is not in the " "temp folder: %s" % staging_dir) return - self.log.info("Removing staging directory ...") + if not staging_dir or not os.path.exists(staging_dir): + self.log.info("No staging directory found: %s" % staging_dir) + return + + self.log.info("Removing staging directory {}".format(staging_dir)) shutil.rmtree(staging_dir) From 7394e7284a7ccef06e9c2321088bc0cb7a314b25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Sun, 2 Aug 2020 23:08:55 +0200 Subject: [PATCH 12/15] disable undo/redo during extraction, fix frame num --- pype/hosts/harmony/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pype/hosts/harmony/__init__.py b/pype/hosts/harmony/__init__.py index 3cae695852..d1a9c3ae17 100644 --- a/pype/hosts/harmony/__init__.py +++ b/pype/hosts/harmony/__init__.py @@ -151,6 +151,7 @@ def application_launch(): def export_template(backdrops, nodes, filepath): func = """function func(args) { + scene.beginUndoRedoAccum("Publish: export template"); // Add an extra node just so a new group can be created. var temp_node = node.add("Top", "temp_note", "NOTE", 0, 0, 0); var template_group = node.createGroup(temp_node, "temp_group"); @@ -168,7 +169,7 @@ def export_template(backdrops, nodes, filepath): }; // Copy-paste the selected nodes into the new group. - var drag_object = copyPaste.copy(args[1], 1, frame.numberOf, ""); + var drag_object = copyPaste.copy(args[1], 1, frame.numberOf(), ""); copyPaste.pasteNewNodes(drag_object, template_group, ""); // Select all nodes within group and export as template. @@ -179,6 +180,7 @@ def export_template(backdrops, nodes, filepath): // created during the process. Action.perform("onActionUpToParent()", "Node View"); node.deleteNode(template_group, true, true); + scene.cancelUndoRedoAccum(); } func """ From 0f32a6d056a48c5102d07c023ea335f81224b63a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 4 Aug 2020 13:57:41 +0200 Subject: [PATCH 13/15] use Action.perform copy() to copy nodes --- pype/hosts/harmony/__init__.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pype/hosts/harmony/__init__.py b/pype/hosts/harmony/__init__.py index d1a9c3ae17..d4b7d91fdb 100644 --- a/pype/hosts/harmony/__init__.py +++ b/pype/hosts/harmony/__init__.py @@ -151,28 +151,31 @@ def application_launch(): def export_template(backdrops, nodes, filepath): func = """function func(args) { - scene.beginUndoRedoAccum("Publish: export template"); - // Add an extra node just so a new group can be created. + var temp_node = node.add("Top", "temp_note", "NOTE", 0, 0, 0); var template_group = node.createGroup(temp_node, "temp_group"); node.deleteNode( template_group + "/temp_note" ); - // This will make Node View to focus on the new group. + selection.clearSelection(); + for (var f = 0; f < args[1].length; f++) + { + selection.addNodeToSelection(args[1][f]); + } + + Action.perform("copy()", "Node View"); + selection.clearSelection(); selection.addNodeToSelection(template_group); Action.perform("onActionEnterGroup()", "Node View"); + Action.perform("paste()", "Node View"); // Recreate backdrops in group. for (var i = 0 ; i < args[0].length; i++) { + MessageLog.trace(args[0][i]); Backdrop.addBackdrop(template_group, args[0][i]); }; - // Copy-paste the selected nodes into the new group. - var drag_object = copyPaste.copy(args[1], 1, frame.numberOf(), ""); - copyPaste.pasteNewNodes(drag_object, template_group, ""); - - // Select all nodes within group and export as template. Action.perform( "selectAll()", "Node View" ); copyPaste.createTemplateFromSelection(args[2], args[3]); @@ -180,7 +183,6 @@ def export_template(backdrops, nodes, filepath): // created during the process. Action.perform("onActionUpToParent()", "Node View"); node.deleteNode(template_group, true, true); - scene.cancelUndoRedoAccum(); } func """ From 9a4c7673e9244f8486655f2c7795f6f5b67553ae Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 4 Aug 2020 21:37:35 +0200 Subject: [PATCH 14/15] temporary change to extract render logic collection should eventually be phased out in favour of explicit render collection --- .../plugins/harmony/publish/extract_render.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/pype/plugins/harmony/publish/extract_render.py b/pype/plugins/harmony/publish/extract_render.py index fe1352f9f9..45b52e0307 100644 --- a/pype/plugins/harmony/publish/extract_render.py +++ b/pype/plugins/harmony/publish/extract_render.py @@ -72,19 +72,27 @@ class ExtractRender(pyblish.api.InstancePlugin): self.log.info(output.decode("utf-8")) # Collect rendered files. + self.log.debug(path) files = os.listdir(path) + self.log.debug(files) collections, remainder = clique.assemble(files, minimum_items=1) assert not remainder, ( "There should not be a remainder for {0}: {1}".format( instance[0], remainder ) ) - assert len(collections) == 1, ( - "There should only be one image sequence in {}. Found: {}".format( - path, len(collections) - ) - ) - collection = collections[0] + self.log.debug(collections) + if len(collections) > 1: + for col in collections: + if len(list(col)) > 1: + collection = col + else: + # assert len(collections) == 1, ( + # "There should only be one image sequence in {}. Found: {}".format( + # path, len(collections) + # ) + # ) + collection = collections[0] # Generate thumbnail. thumbnail_path = os.path.join(path, "thumbnail.png") From 0c883a9f5b300874be3ba52849936e17ac09673a Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 4 Aug 2020 21:38:29 +0200 Subject: [PATCH 15/15] temp frame range filter implementation temporary implementation for a client. must be cleaned up and logic should be changed to work based on task type --- .../harmony/publish/validate_scene_settings.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pype/plugins/harmony/publish/validate_scene_settings.py b/pype/plugins/harmony/publish/validate_scene_settings.py index aa9a70bd85..3602f1ca22 100644 --- a/pype/plugins/harmony/publish/validate_scene_settings.py +++ b/pype/plugins/harmony/publish/validate_scene_settings.py @@ -28,8 +28,11 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin): hosts = ["harmony"] actions = [ValidateSceneSettingsRepair] + frame_check_filter = ["_ch_", "_pr_", "_intd_", "_extd_"] + def process(self, instance): expected_settings = pype.hosts.harmony.get_asset_settings() + self.log.info(expected_settings) # Harmony is expected to start at 1. frame_start = expected_settings["frameStart"] @@ -37,6 +40,14 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin): expected_settings["frameEnd"] = frame_end - frame_start + 1 expected_settings["frameStart"] = 1 + + + self.log.info(instance.context.data['anatomyData']['asset']) + + if any(string in instance.context.data['anatomyData']['asset'] + for string in frame_check_filter): + expected_settings.pop("frameEnd") + func = """function func() { return {