From a3a9ffd581d4a67696221c92353d0bd3bc605045 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 13 May 2020 12:54:35 +0100 Subject: [PATCH 01/19] Add Harmony icon --- res/app_icons/harmony.png | Bin 0 -> 30038 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 res/app_icons/harmony.png diff --git a/res/app_icons/harmony.png b/res/app_icons/harmony.png new file mode 100644 index 0000000000000000000000000000000000000000..f0f6c82c6edfb6b3a3d43b9d1bf495c5928eabef GIT binary patch literal 30038 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 T!6jG!exN9)CR_f}^uzxFMZ*H^ literal 0 HcmV?d00001 From 23e6e05d79722c61ade0e1c27ecf768a0ecb0288 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 13 May 2020 18:41:32 +0200 Subject: [PATCH 02/19] removed unused collect project root plugin from nukestudio --- .../nukestudio/publish/collect_project_root.py | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 pype/plugins/nukestudio/publish/collect_project_root.py diff --git a/pype/plugins/nukestudio/publish/collect_project_root.py b/pype/plugins/nukestudio/publish/collect_project_root.py deleted file mode 100644 index 1b21a6b641..0000000000 --- a/pype/plugins/nukestudio/publish/collect_project_root.py +++ /dev/null @@ -1,15 +0,0 @@ -import pyblish.api -import avalon.api as avalon -import os - -class CollectActiveProjectRoot(pyblish.api.ContextPlugin): - """Inject the active project into context""" - - label = "Collect Project Root" - order = pyblish.api.CollectorOrder - 0.1 - - def process(self, context): - S = avalon.Session - context.data["projectroot"] = os.path.normpath( - os.path.join(S['AVALON_PROJECTS'], S['AVALON_PROJECT']) - ) From c2efa69e209148e97798d494b1264b0811e546a2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 13 May 2020 18:41:56 +0200 Subject: [PATCH 03/19] removed AVALON_PROJECTS from required in session schema --- schema/session-2.0.json | 1 - 1 file changed, 1 deletion(-) diff --git a/schema/session-2.0.json b/schema/session-2.0.json index 006a9e2dbf..d37f2ac822 100644 --- a/schema/session-2.0.json +++ b/schema/session-2.0.json @@ -9,7 +9,6 @@ "additionalProperties": true, "required": [ - "AVALON_PROJECTS", "AVALON_PROJECT", "AVALON_ASSET", "AVALON_CONFIG" From b44b8683a0d1f2fb5d3f99171b4b74b73f0136ce Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 13 May 2020 18:42:43 +0200 Subject: [PATCH 04/19] skip AVALON PROJECTS in adaobe communicator context collector --- pype/plugins/adobecommunicator/publish/collect_context.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pype/plugins/adobecommunicator/publish/collect_context.py b/pype/plugins/adobecommunicator/publish/collect_context.py index 139dd86480..6d05825844 100644 --- a/pype/plugins/adobecommunicator/publish/collect_context.py +++ b/pype/plugins/adobecommunicator/publish/collect_context.py @@ -39,10 +39,8 @@ class CollectContextDataFromAport(pyblish.api.ContextPlugin): # get avalon session data and convert \ to / _S = avalon.session - projects = Path(_S["AVALON_PROJECTS"]).resolve() asset = _S["AVALON_ASSET"] workdir = Path(_S["AVALON_WORKDIR"]).resolve() - _S["AVALON_PROJECTS"] = str(projects) _S["AVALON_WORKDIR"] = str(workdir) context.data["avalonSession"] = _S From a9fc6614a97eb7961374310ad80e9d24022aae0d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 13 May 2020 18:43:00 +0200 Subject: [PATCH 05/19] changed docstring with mentioned AVAON_PROJECTS --- pype/ftrack/actions/action_delivery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/ftrack/actions/action_delivery.py b/pype/ftrack/actions/action_delivery.py index 9d686929de..23da81d383 100644 --- a/pype/ftrack/actions/action_delivery.py +++ b/pype/ftrack/actions/action_delivery.py @@ -340,7 +340,7 @@ class Delivery(BaseAction): repre_path = self.path_from_represenation(repre, anatomy) # TODO add backup solution where root of path from component - # is repalced with AVALON_PROJECTS root + # is repalced with root if not frame: self.process_single_file( repre_path, anatomy, anatomy_name, anatomy_data From 8402e616a170122583103ac7922c523443780e47 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 13 May 2020 18:43:37 +0200 Subject: [PATCH 06/19] delete old version does not do root validation --- .../actions/action_delete_old_versions.py | 30 +------------------ 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/pype/ftrack/actions/action_delete_old_versions.py b/pype/ftrack/actions/action_delete_old_versions.py index c13845f58c..30f786e93f 100644 --- a/pype/ftrack/actions/action_delete_old_versions.py +++ b/pype/ftrack/actions/action_delete_old_versions.py @@ -42,36 +42,8 @@ class DeleteOldVersions(BaseAction): return False def interface(self, session, entities, event): + # TODO Add roots existence validation items = [] - root = os.environ.get("AVALON_PROJECTS") - if not root: - msg = "Root path to projects is not set." - items.append({ - "type": "label", - "value": "ERROR: {}".format(msg) - }) - self.show_interface( - items=items, title=self.inteface_title, event=event - ) - return { - "success": False, - "message": msg - } - - if not os.path.exists(root): - msg = "Root path does not exists \"{}\".".format(str(root)) - items.append({ - "type": "label", - "value": "ERROR: {}".format(msg) - }) - self.show_interface( - items=items, title=self.inteface_title, event=event - ) - return { - "success": False, - "message": msg - } - values = event["data"].get("values") if values: versions_count = int(values["last_versions_count"]) From b778163c86fb8196e1365508d245a8d227af7362 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 13 May 2020 19:02:16 +0200 Subject: [PATCH 07/19] set root environments during install of pype --- pype/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pype/__init__.py b/pype/__init__.py index 505db4c57f..775b75e2b0 100644 --- a/pype/__init__.py +++ b/pype/__init__.py @@ -3,7 +3,7 @@ import os from pyblish import api as pyblish from avalon import api as avalon from .lib import filter_pyblish_plugins -from pypeapp import config, Roots +from pypeapp import config, Roots, Anatomy import logging @@ -100,8 +100,9 @@ def install(): avalon.register_plugin_path(avalon.InventoryAction, path) if project_name: - root_obj = Roots(project_name) - avalon.register_root(root_obj.roots) + anatomy = Anatomy(project_name) + anatomy.set_root_environments() + avalon.register_root(anatomy.roots) # apply monkey patched discover to original one avalon.discover = patched_discover From 5fff3e7bdc17b6b38e0593c35f0db181b6ecfcce Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 13 May 2020 19:21:19 +0200 Subject: [PATCH 08/19] temporarily disable avalon-launcher --- pype/avalon_apps/avalon_app.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pype/avalon_apps/avalon_app.py b/pype/avalon_apps/avalon_app.py index 35ab4c1eb7..30e7a7412b 100644 --- a/pype/avalon_apps/avalon_app.py +++ b/pype/avalon_apps/avalon_app.py @@ -33,23 +33,23 @@ class AvalonApps: return icon = QtGui.QIcon(launcher_lib.resource("icon", "main.png")) - aShowLauncher = QtWidgets.QAction(icon, "&Launcher", parent_menu) + # aShowLauncher = QtWidgets.QAction(icon, "&Launcher", parent_menu) aLibraryLoader = QtWidgets.QAction("Library", parent_menu) - aShowLauncher.triggered.connect(self.show_launcher) + # aShowLauncher.triggered.connect(self.show_launcher) aLibraryLoader.triggered.connect(self.show_library_loader) - parent_menu.addAction(aShowLauncher) + # parent_menu.addAction(aShowLauncher) parent_menu.addAction(aLibraryLoader) - def show_launcher(self): - # if app_launcher don't exist create it/otherwise only show main window - if self.app_launcher is None: - root = os.path.realpath(os.environ["AVALON_PROJECTS"]) - io.install() - APP_PATH = launcher_lib.resource("qml", "main.qml") - self.app_launcher = launcher_widget.Launcher(root, APP_PATH) - self.app_launcher.window.show() + # def show_launcher(self): + # # if app_launcher don't exist create it/otherwise only show main window + # if self.app_launcher is None: + # root = os.path.realpath(os.environ["AVALON_PROJECTS"]) + # io.install() + # APP_PATH = launcher_lib.resource("qml", "main.qml") + # self.app_launcher = launcher_widget.Launcher(root, APP_PATH) + # self.app_launcher.window.show() def show_library_loader(self): libraryloader.show( From 22fff1f0bbcc859361ad254c7771db62cad68585 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 13 May 2020 19:41:14 +0200 Subject: [PATCH 09/19] fixed nuke and nukestudio AVALON_PROJECTS --- pype/nuke/lib.py | 4 +++- pype/nukestudio/lib.py | 25 ++++++++++++++++--------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/pype/nuke/lib.py b/pype/nuke/lib.py index 163fcd27b7..bd39666ab9 100644 --- a/pype/nuke/lib.py +++ b/pype/nuke/lib.py @@ -973,7 +973,9 @@ class WorkfileSettings(object): self.set_colorspace() def set_favorites(self): - projects_root = os.getenv("AVALON_PROJECTS") + anatomy = get_anatomy() + work_template = anatomy.templates["work"]["path"] + projects_root = anatomy.root_value_for_template(work_template) work_dir = os.getenv("AVALON_WORKDIR") asset = os.getenv("AVALON_ASSET") project = os.getenv("AVALON_PROJECT") diff --git a/pype/nukestudio/lib.py b/pype/nukestudio/lib.py index 774a9d45bf..e0e6d8750c 100644 --- a/pype/nukestudio/lib.py +++ b/pype/nukestudio/lib.py @@ -6,7 +6,7 @@ import pyblish.api import avalon.api as avalon from avalon.vendor.Qt import (QtWidgets, QtGui) import pype.api as pype -from pypeapp import Logger +from pypeapp import Logger, Anatomy log = Logger().get_logger(__name__, "nukestudio") @@ -30,11 +30,16 @@ def set_workfiles(): # show workfile gui workfiles.show(workdir) + def sync_avalon_data_to_workfile(): # import session to get project dir - S = avalon.Session + project_name = avalon.Session["AVALON_PROJECT"] + + anatomy = Anatomy(project_name) + work_template = anatomy.templates["work"]["path"] + work_root = anatomy.root_value_for_template(work_template) active_project_root = os.path.normpath( - os.path.join(S['AVALON_PROJECTS'], S['AVALON_PROJECT']) + os.path.join(work_root, project_name) ) # getting project project = hiero.core.projects()[-1] @@ -350,17 +355,19 @@ def CreateNukeWorkfile(nodes=None, # create root node and save all metadata root_node = hiero.core.nuke.RootNode() - root_path = os.environ["AVALON_PROJECTS"] + anatomy = Anatomy(os.environ["AVALON_PROJECT"]) + work_template = anatomy.templates["work"]["path"] + root_path = anatomy.root_value_for_template(work_template) nuke_script.addNode(root_node) # here to call pype.nuke.lib.BuildWorkfile script_builder = nklib.BuildWorkfile( - root_node=root_node, - root_path=root_path, - nodes=nuke_script.getNodes(), - **kwargs - ) + root_node=root_node, + root_path=root_path, + nodes=nuke_script.getNodes(), + **kwargs + ) class ClipLoader: From b6929fc41be8f6fc62f7f117eafe648c0cd90833 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 13 May 2020 19:47:55 +0200 Subject: [PATCH 10/19] using find_root_template_from_path method directly on anatomy object --- pype/plugins/global/publish/integrate_new.py | 2 +- pype/plugins/global/publish/submit_publish_job.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index f7ea4eebf9..fc785f6065 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -651,7 +651,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): source = context.data["currentFile"] anatomy = instance.context.data["anatomy"] success, rootless_path = ( - anatomy.roots_obj.find_root_template_from_path(source) + anatomy.find_root_template_from_path(source) ) if success: source = rootless_path diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index 9741a7135a..eb8264dc23 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -195,7 +195,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): output_dir = instance.data["outputDir"] # Convert output dir to `{root}/rest/of/path/...` with Anatomy success, rootless_path = ( - self.anatomy.roots_obj.find_root_template_from_path(output_dir) + self.anatomy.find_root_template_from_path(output_dir) ) if not success: # `rootless_path` is not set to `output_dir` if none of roots match @@ -379,7 +379,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): staging = os.path.dirname(list(cols[0])[0]) success, rootless_staging_dir = ( - self.anatomy.roots_obj.find_root_template_from_path(staging) + self.anatomy.find_root_template_from_path(staging) ) if success: staging = rootless_staging_dir @@ -471,7 +471,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): staging = os.path.dirname(list(collection)[0]) success, rootless_staging_dir = ( - self.anatomy.roots_obj.find_root_template_from_path(staging) + self.anatomy.find_root_template_from_path(staging) ) if success: staging = rootless_staging_dir @@ -506,7 +506,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): staging = os.path.dirname(remainder) success, rootless_staging_dir = ( - self.anatomy.roots_obj.find_root_template_from_path(staging) + self.anatomy.find_root_template_from_path(staging) ) if success: staging = rootless_staging_dir @@ -619,7 +619,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): source = context.data["currentFile"] success, rootless_path = ( - self.anatomy.roots_obj.find_root_template_from_path(source) + self.anatomy.find_root_template_from_path(source) ) if success: source = rootless_path @@ -684,7 +684,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): staging_dir = repre.get("stagingDir") if staging_dir: success, rootless_staging_dir = ( - self.anatomy.roots_obj.find_root_template_from_path( + self.anatomy.find_root_template_from_path( staging_dir ) ) From 8f1ffbffcd93a4f9a5ba3a7117b07242f00e0b0a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 13 May 2020 19:48:18 +0200 Subject: [PATCH 11/19] tried to fix validate ass relative paths --- .../publish/validate_ass_relative_paths.py | 90 +++++++++++++------ 1 file changed, 63 insertions(+), 27 deletions(-) diff --git a/pype/plugins/maya/publish/validate_ass_relative_paths.py b/pype/plugins/maya/publish/validate_ass_relative_paths.py index b0fd12a550..0373f1bbdf 100644 --- a/pype/plugins/maya/publish/validate_ass_relative_paths.py +++ b/pype/plugins/maya/publish/validate_ass_relative_paths.py @@ -37,50 +37,86 @@ class ValidateAssRelativePaths(pyblish.api.InstancePlugin): scene_dir, scene_basename = os.path.split(cmds.file(q=True, loc=True)) scene_name, _ = os.path.splitext(scene_basename) - project_root = "{}{}{}".format( - os.environ.get("AVALON_PROJECTS"), - os.path.sep, - os.environ.get("AVALON_PROJECT") - ) assert self.maya_is_true(relative_texture) is not True, \ ("Texture path is set to be absolute") assert self.maya_is_true(relative_procedural) is not True, \ ("Procedural path is set to be absolute") + anatomy = instance.context.data["anatomy"] + texture_search_path = texture_search_path.replace("\\", "/") procedural_search_path = procedural_search_path.replace("\\", "/") - project_root = project_root.replace("\\", "/") - assert project_root in texture_search_path, \ + texture_success, texture_search_rootless_path = ( + anatomy.find_root_template_from_path( + texture_search_path + ) + ) + procedural_success, procedural_search_rootless_path = ( + anatomy.find_root_template_from_path( + texture_search_path + ) + ) + + assert not texture_success, \ ("Project root is not in texture_search_path") - assert project_root in procedural_search_path, \ + assert not procedural_success, \ ("Project root is not in procedural_search_path") @classmethod def repair(cls, instance): - texture_search_path = cmds.getAttr( - "defaultArnoldRenderOptions.tspath" + texture_path = cmds.getAttr("defaultArnoldRenderOptions.tspath") + procedural_path = cmds.getAttr("defaultArnoldRenderOptions.pspath") + + anatomy = instance.context.data["anatomy"] + texture_success, texture_rootless_path = ( + anatomy.find_root_template_from_path(texture_path) ) - procedural_search_path = cmds.getAttr( - "defaultArnoldRenderOptions.pspath" + procedural_success, procedural_rootless_path = ( + anatomy.find_root_template_from_path(procedural_path) ) - project_root = "{}{}{}".format( - os.environ.get("AVALON_PROJECTS"), - os.path.sep, - os.environ.get("AVALON_PROJECT"), - ).replace("\\", "/") + all_root_paths = anatomy.all_root_paths() - cmds.setAttr("defaultArnoldRenderOptions.tspath", - project_root + os.pathsep + texture_search_path, - type="string") - cmds.setAttr("defaultArnoldRenderOptions.pspath", - project_root + os.pathsep + procedural_search_path, - type="string") - cmds.setAttr("defaultArnoldRenderOptions.absolute_procedural_paths", - False) - cmds.setAttr("defaultArnoldRenderOptions.absolute_texture_paths", - False) + if not texture_success: + final_path = cls.find_absolute_path( + texture_rootless_path, all_root_paths + ) + if final_path is None: + raise AssertionError("Ass is loaded out of defined roots.") + + cmds.setAttr( + "defaultArnoldRenderOptions.tspath", + final_path, + type="string" + ) + cmds.setAttr( + "defaultArnoldRenderOptions.absolute_texture_paths", + False + ) + + if not procedural_success: + final_path = cls.find_absolute_path( + texture_rootless_path, all_root_paths + ) + if final_path is None: + raise AssertionError("Ass is loaded out of defined roots.") + cmds.setAttr( + "defaultArnoldRenderOptions.pspath", + final_path, + type="string" + ) + cmds.setAttr( + "defaultArnoldRenderOptions.absolute_procedural_paths", + False + ) + + @staticmethod + def find_absolute_path(relative_path, all_root_paths): + for root_path in all_root_paths: + possible_path = os.path.join(root_path, relative_path) + if os.path.exists(possible_path): + return possible_path def maya_is_true(self, attr_val): """ From a1d35fb5bdf5485950aa6bef43665ad78ce343ed Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 13 May 2020 23:27:49 +0200 Subject: [PATCH 12/19] avalon-launcher can be launched again but without setting root --- pype/avalon_apps/avalon_app.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/pype/avalon_apps/avalon_app.py b/pype/avalon_apps/avalon_app.py index 30e7a7412b..d3190a9d53 100644 --- a/pype/avalon_apps/avalon_app.py +++ b/pype/avalon_apps/avalon_app.py @@ -33,23 +33,22 @@ class AvalonApps: return icon = QtGui.QIcon(launcher_lib.resource("icon", "main.png")) - # aShowLauncher = QtWidgets.QAction(icon, "&Launcher", parent_menu) + aShowLauncher = QtWidgets.QAction(icon, "&Launcher", parent_menu) aLibraryLoader = QtWidgets.QAction("Library", parent_menu) - # aShowLauncher.triggered.connect(self.show_launcher) + aShowLauncher.triggered.connect(self.show_launcher) aLibraryLoader.triggered.connect(self.show_library_loader) - # parent_menu.addAction(aShowLauncher) + parent_menu.addAction(aShowLauncher) parent_menu.addAction(aLibraryLoader) - # def show_launcher(self): - # # if app_launcher don't exist create it/otherwise only show main window - # if self.app_launcher is None: - # root = os.path.realpath(os.environ["AVALON_PROJECTS"]) - # io.install() - # APP_PATH = launcher_lib.resource("qml", "main.qml") - # self.app_launcher = launcher_widget.Launcher(root, APP_PATH) - # self.app_launcher.window.show() + def show_launcher(self): + # if app_launcher don't exist create it/otherwise only show main window + if self.app_launcher is None: + io.install() + APP_PATH = launcher_lib.resource("qml", "main.qml") + self.app_launcher = launcher_widget.Launcher(APP_PATH) + self.app_launcher.window.show() def show_library_loader(self): libraryloader.show( From 0d311d63c822e721337a63a147617874981017af Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 14 May 2020 10:15:28 +0200 Subject: [PATCH 13/19] removed unsused import --- pype/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/__init__.py b/pype/__init__.py index 775b75e2b0..9ca0380bf3 100644 --- a/pype/__init__.py +++ b/pype/__init__.py @@ -3,7 +3,7 @@ import os from pyblish import api as pyblish from avalon import api as avalon from .lib import filter_pyblish_plugins -from pypeapp import config, Roots, Anatomy +from pypeapp import config, Anatomy import logging From ef0f346195bccddc716e13b29ee84e80c5564b18 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 14 May 2020 09:59:05 +0100 Subject: [PATCH 14/19] fixing the default host import to be from pype --- pype/services/adobe_communicator/lib/publish.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/services/adobe_communicator/lib/publish.py b/pype/services/adobe_communicator/lib/publish.py index 2e7d993a60..a6fe991025 100644 --- a/pype/services/adobe_communicator/lib/publish.py +++ b/pype/services/adobe_communicator/lib/publish.py @@ -18,7 +18,7 @@ def main(env): # Register Host (and it's pyblish plugins) host_name = env["AVALON_APP"] # TODO not sure if use "pype." or "avalon." for host import - host_import_str = f"avalon.{host_name}" + host_import_str = f"pype.{host_name}" try: host_module = importlib.import_module(host_import_str) From f30f0099fd65416ffcf4d282d6accbd4e885e9c9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 14 May 2020 10:59:40 +0200 Subject: [PATCH 15/19] for padding is used frame_padding from anatomy templates but kept backwards compatible "frame" --- pype/nuke/lib.py | 9 ++++++++- pype/plugins/global/publish/integrate_master_version.py | 7 +++++-- pype/plugins/global/publish/integrate_new.py | 7 +++++-- pype/plugins/nukestudio/publish/collect_plates.py | 7 ++++++- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/pype/nuke/lib.py b/pype/nuke/lib.py index bd39666ab9..a706753755 100644 --- a/pype/nuke/lib.py +++ b/pype/nuke/lib.py @@ -177,9 +177,16 @@ def format_anatomy(data): log.debug("__ anatomy.templates: {}".format(anatomy.templates)) try: - padding = int(anatomy.templates['render']['padding']) + # TODO: bck compatibility with old anatomy template + padding = int( + anatomy.templates["render"].get( + "frame_padding", + anatomy.templates["render"].get("padding") + ) + ) except KeyError as e: msg = ("`padding` key is not in `render` " + "or `frame_padding` on is not available in " "Anatomy template. Please, add it there and restart " "the pipeline (padding: \"4\"): `{}`").format(e) diff --git a/pype/plugins/global/publish/integrate_master_version.py b/pype/plugins/global/publish/integrate_master_version.py index e6e4247dd8..d82c3be075 100644 --- a/pype/plugins/global/publish/integrate_master_version.py +++ b/pype/plugins/global/publish/integrate_master_version.py @@ -356,8 +356,11 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): _anatomy_filled = anatomy.format(anatomy_data) _template_filled = _anatomy_filled["master"]["path"] head, tail = _template_filled.split(frame_splitter) - padding = ( - anatomy.templates["render"]["padding"] + padding = int( + anatomy.templates["render"].get( + "frame_padding", + anatomy.templates["render"].get("padding") + ) ) dst_col = clique.Collection( diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index fc785f6065..08c390d040 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -341,8 +341,11 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): index_frame_start = None if repre.get("frameStart"): - frame_start_padding = ( - anatomy.templates["render"]["padding"] + frame_start_padding = int( + anatomy.templates["render"].get( + "frame_padding", + anatomy.templates["render"].get("padding") + ) ) index_frame_start = int(repre.get("frameStart")) diff --git a/pype/plugins/nukestudio/publish/collect_plates.py b/pype/plugins/nukestudio/publish/collect_plates.py index 8a79354bbf..3e5ba51b60 100644 --- a/pype/plugins/nukestudio/publish/collect_plates.py +++ b/pype/plugins/nukestudio/publish/collect_plates.py @@ -104,7 +104,12 @@ class CollectPlatesData(api.InstancePlugin): version_data = dict() context = instance.context anatomy = context.data.get("anatomy", None) - padding = int(anatomy.templates['render']['padding']) + padding = int( + anatomy.templates["render"].get( + "frame_padding", + anatomy.templates["render"].get("padding") + ) + ) name = instance.data["subset"] source_path = instance.data["sourcePath"] From 2c854faadf7142cb6e5f689d601f9f0335e40fb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Thu, 14 May 2020 12:05:43 +0200 Subject: [PATCH 16/19] fixed ass path validator --- .../publish/validate_ass_relative_paths.py | 91 ++++++++----------- 1 file changed, 38 insertions(+), 53 deletions(-) diff --git a/pype/plugins/maya/publish/validate_ass_relative_paths.py b/pype/plugins/maya/publish/validate_ass_relative_paths.py index 0373f1bbdf..b64e23e92c 100644 --- a/pype/plugins/maya/publish/validate_ass_relative_paths.py +++ b/pype/plugins/maya/publish/validate_ass_relative_paths.py @@ -44,72 +44,57 @@ class ValidateAssRelativePaths(pyblish.api.InstancePlugin): anatomy = instance.context.data["anatomy"] - texture_search_path = texture_search_path.replace("\\", "/") - procedural_search_path = procedural_search_path.replace("\\", "/") + # Use project root variables for multiplatform support, see: + # https://docs.arnoldrenderer.com/display/A5AFMUG/Search+Path + # ':' as path separator is supported by Arnold for all platforms. + keys = anatomy.root_environments().keys() + paths = [] + for k in keys: + paths.append("[{}]".format(k)) - texture_success, texture_search_rootless_path = ( - anatomy.find_root_template_from_path( - texture_search_path - ) - ) - procedural_success, procedural_search_rootless_path = ( - anatomy.find_root_template_from_path( - texture_search_path - ) + self.log.info("discovered roots: {}".format(":".join(paths))) + + assert ":".join(paths) in texture_search_path, ( + "Project roots are not in texture_search_path" ) - assert not texture_success, \ - ("Project root is not in texture_search_path") - assert not procedural_success, \ - ("Project root is not in procedural_search_path") + assert ":".join(paths) in procedural_search_path, ( + "Project roots are not in procedural_search_path" + ) @classmethod def repair(cls, instance): texture_path = cmds.getAttr("defaultArnoldRenderOptions.tspath") procedural_path = cmds.getAttr("defaultArnoldRenderOptions.pspath") + # Use project root variables for multiplatform support, see: + # https://docs.arnoldrenderer.com/display/A5AFMUG/Search+Path + # ':' as path separator is supported by Arnold for all platforms. anatomy = instance.context.data["anatomy"] - texture_success, texture_rootless_path = ( - anatomy.find_root_template_from_path(texture_path) + keys = anatomy.root_environments().keys() + paths = [] + for k in keys: + paths.append("[{}]".format(k)) + + cmds.setAttr( + "defaultArnoldRenderOptions.tspath", + ":".join([p for p in paths + [texture_path] if p]), + type="string" ) - procedural_success, procedural_rootless_path = ( - anatomy.find_root_template_from_path(procedural_path) + cmds.setAttr( + "defaultArnoldRenderOptions.absolute_texture_paths", + False ) - all_root_paths = anatomy.all_root_paths() - - if not texture_success: - final_path = cls.find_absolute_path( - texture_rootless_path, all_root_paths - ) - if final_path is None: - raise AssertionError("Ass is loaded out of defined roots.") - - cmds.setAttr( - "defaultArnoldRenderOptions.tspath", - final_path, - type="string" - ) - cmds.setAttr( - "defaultArnoldRenderOptions.absolute_texture_paths", - False - ) - - if not procedural_success: - final_path = cls.find_absolute_path( - texture_rootless_path, all_root_paths - ) - if final_path is None: - raise AssertionError("Ass is loaded out of defined roots.") - cmds.setAttr( - "defaultArnoldRenderOptions.pspath", - final_path, - type="string" - ) - cmds.setAttr( - "defaultArnoldRenderOptions.absolute_procedural_paths", - False - ) + cmds.setAttr( + "defaultArnoldRenderOptions.pspath", + ":".join([p for p in paths + [procedural_path] if p]), + type="string" + ) + cmds.setAttr( + "defaultArnoldRenderOptions.absolute_procedural_paths", + False + ) @staticmethod def find_absolute_path(relative_path, all_root_paths): From c34664a370e6a4f6f1cc12daf8ade1a60fa3e98c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 14 May 2020 15:45:19 +0200 Subject: [PATCH 17/19] replace backslashes with forwardslashed in nukestudio --- pype/nukestudio/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/nukestudio/lib.py b/pype/nukestudio/lib.py index e0e6d8750c..3a8e35f100 100644 --- a/pype/nukestudio/lib.py +++ b/pype/nukestudio/lib.py @@ -38,9 +38,9 @@ def sync_avalon_data_to_workfile(): anatomy = Anatomy(project_name) work_template = anatomy.templates["work"]["path"] work_root = anatomy.root_value_for_template(work_template) - active_project_root = os.path.normpath( + active_project_root = ( os.path.join(work_root, project_name) - ) + ).replace("\\", "/") # getting project project = hiero.core.projects()[-1] From 031f9ae9030c6ac3f09067a79bef37f605042b81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Thu, 14 May 2020 16:30:23 +0200 Subject: [PATCH 18/19] added 10sec timeout for connection to deadline --- pype/plugins/global/publish/submit_publish_job.py | 2 +- pype/plugins/maya/publish/submit_maya_deadline.py | 4 ++++ pype/plugins/nuke/publish/submit_nuke_deadline.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index 9741a7135a..f6ad9f76af 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -262,7 +262,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # self.log.info(json.dumps(payload, indent=4, sort_keys=True)) url = "{}/api/jobs".format(self.DEADLINE_REST_URL) - response = requests.post(url, json=payload) + response = requests.post(url, json=payload, timeout=10) if not response.ok: raise Exception(response.text) diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index b5e4cfe98c..c65f13c653 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -352,6 +352,8 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): """ if 'verify' not in kwargs: kwargs['verify'] = False if os.getenv("PYPE_DONT_VERIFY_SSL", True) else True # noqa + # add 10sec timeout before bailing out + kwargs['timeout'] = 10 return requests.post(*args, **kwargs) def _requests_get(self, *args, **kwargs): @@ -366,4 +368,6 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): """ if 'verify' not in kwargs: kwargs['verify'] = False if os.getenv("PYPE_DONT_VERIFY_SSL", True) else True # noqa + # add 10sec timeout before bailing out + kwargs['timeout'] = 10 return requests.get(*args, **kwargs) diff --git a/pype/plugins/nuke/publish/submit_nuke_deadline.py b/pype/plugins/nuke/publish/submit_nuke_deadline.py index 4b68056e09..6a1654f77e 100644 --- a/pype/plugins/nuke/publish/submit_nuke_deadline.py +++ b/pype/plugins/nuke/publish/submit_nuke_deadline.py @@ -251,7 +251,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): self.expected_files(instance, render_path) self.log.debug("__ expectedFiles: `{}`".format( instance.data["expectedFiles"])) - response = requests.post(self.deadline_url, json=payload) + response = requests.post(self.deadline_url, json=payload, timeout=10) if not response.ok: raise Exception(response.text) From 844ea5be7f92a05326e148599d6d49cd025abd03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Thu, 14 May 2020 17:17:10 +0200 Subject: [PATCH 19/19] removing unused collect deadline user plugin --- .../global/publish/collect_deadline_user.py | 64 ------------------- 1 file changed, 64 deletions(-) delete mode 100644 pype/plugins/global/publish/collect_deadline_user.py diff --git a/pype/plugins/global/publish/collect_deadline_user.py b/pype/plugins/global/publish/collect_deadline_user.py deleted file mode 100644 index 125f9d0d26..0000000000 --- a/pype/plugins/global/publish/collect_deadline_user.py +++ /dev/null @@ -1,64 +0,0 @@ -""" -Requires: - environment -> DEADLINE_PATH - -Provides: - context -> deadlineUser (str) -""" - -import os -import subprocess - -import pyblish.api -from pype.plugin import contextplugin_should_run - -CREATE_NO_WINDOW = 0x08000000 - - -def deadline_command(cmd): - # Find Deadline - path = os.environ.get("DEADLINE_PATH", None) - assert path is not None, "Variable 'DEADLINE_PATH' must be set" - - executable = os.path.join(path, "deadlinecommand") - if os.name == "nt": - executable += ".exe" - assert os.path.exists( - executable), "Deadline executable not found at %s" % executable - assert cmd, "Must have a command" - - query = (executable, cmd) - - process = subprocess.Popen(query, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True, - creationflags=CREATE_NO_WINDOW) - out, err = process.communicate() - - return out - - -class CollectDeadlineUser(pyblish.api.ContextPlugin): - """Retrieve the local active Deadline user""" - - order = pyblish.api.CollectorOrder + 0.499 - label = "Deadline User" - hosts = ['maya', 'fusion'] - families = ["renderlayer", "saver.deadline"] - - def process(self, context): - """Inject the current working file""" - - # Workaround bug pyblish-base#250 - if not contextplugin_should_run(self, context): - return - - user = deadline_command("GetCurrentUserName").strip() - - if not user: - self.log.warning("No Deadline user found. " - "Do you have Deadline installed?") - return - - self.log.info("Found Deadline user: {}".format(user)) - context.data['deadlineUser'] = user