From a7871f54f03d1100423e1414872d7b3f613c3e98 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 24 Sep 2021 19:05:46 +0200 Subject: [PATCH 01/27] hda publishing wip --- .../houdini/plugins/create/create_hda.py | 47 ++++++++++++++++ .../hosts/houdini/plugins/load/load_hda.py | 56 +++++++++++++++++++ .../plugins/publish/collect_active_state.py | 4 +- .../plugins/publish/collect_instances.py | 6 +- .../houdini/plugins/publish/extract_hda.py | 43 ++++++++++++++ .../plugins/publish/validate_bypass.py | 2 +- openpype/plugins/publish/integrate_new.py | 3 +- 7 files changed, 157 insertions(+), 4 deletions(-) create mode 100644 openpype/hosts/houdini/plugins/create/create_hda.py create mode 100644 openpype/hosts/houdini/plugins/load/load_hda.py create mode 100644 openpype/hosts/houdini/plugins/publish/extract_hda.py diff --git a/openpype/hosts/houdini/plugins/create/create_hda.py b/openpype/hosts/houdini/plugins/create/create_hda.py new file mode 100644 index 0000000000..d58e0c5e52 --- /dev/null +++ b/openpype/hosts/houdini/plugins/create/create_hda.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +from openpype.hosts.houdini.api import plugin +from avalon.houdini import lib +import hou + + +class CreateHDA(plugin.Creator): + """Publish Houdini Digital Asset file.""" + + name = "hda" + label = "Houdini Digital Asset (Hda)" + family = "hda" + icon = "gears" + maintain_selection = False + + def __init__(self, *args, **kwargs): + super(CreateHDA, self).__init__(*args, **kwargs) + self.data.pop("active", None) + + def _process(self, instance): + + out = hou.node("/obj") + self.nodes = hou.selectedNodes() + + if (self.options or {}).get("useSelection") and self.nodes: + to_hda = self.nodes[0] + if len(self.nodes) > 1: + subnet = out.createNode( + "subnet", node_name="{}_subnet".format(self.name)) + to_hda = subnet + else: + subnet = out.createNode( + "subnet", node_name="{}_subnet".format(self.name)) + subnet.moveToGoodPosition() + to_hda = subnet + + hda_node = to_hda.createDigitalAsset( + name=self.name, + hda_file_name="$HIP/{}.hda".format(self.name) + ) + hda_node.setName(self.name) + hou.moveNodesTo(self.nodes, hda_node) + hda_node.layoutChildren() + + lib.imprint(hda_node, self.data) + + return hda_node diff --git a/openpype/hosts/houdini/plugins/load/load_hda.py b/openpype/hosts/houdini/plugins/load/load_hda.py new file mode 100644 index 0000000000..5e04d83e86 --- /dev/null +++ b/openpype/hosts/houdini/plugins/load/load_hda.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +from avalon import api + +from avalon.houdini import pipeline, lib + + +class HdaLoader(api.Loader): + """Load Houdini Digital Asset file.""" + + families = ["hda"] + label = "Load Hda" + representations = ["hda"] + order = -10 + icon = "code-fork" + color = "orange" + + def load(self, context, name=None, namespace=None, data=None): + + import os + import hou + + # Format file name, Houdini only wants forward slashes + file_path = os.path.normpath(self.fname) + file_path = file_path.replace("\\", "/") + + # Get the root node + obj = hou.node("/obj") + + # Create a unique name + counter = 1 + namespace = namespace or context["asset"]["name"] + formatted = "{}_{}".format(namespace, name) if namespace else name + node_name = "{0}_{1:03d}".format(formatted, counter) + + hou.hda.installFile(file_path) + print("installing {}".format(name)) + hda_node = obj.createNode(name, node_name) + + self[:] = [hda_node] + + return pipeline.containerise( + node_name, + namespace, + [hda_node], + context, + self.__class__.__name__, + suffix="", + ) + + def update(self, container, representation): + hda_node = container["node"] + hda_def = hda_node.type().definition() + + + def remove(self, container): + pass diff --git a/openpype/hosts/houdini/plugins/publish/collect_active_state.py b/openpype/hosts/houdini/plugins/publish/collect_active_state.py index 1193f0cd19..862d5720e1 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_active_state.py +++ b/openpype/hosts/houdini/plugins/publish/collect_active_state.py @@ -23,8 +23,10 @@ class CollectInstanceActiveState(pyblish.api.InstancePlugin): return # Check bypass state and reverse + active = True node = instance[0] - active = not node.isBypassed() + if hasattr(node, "isBypassed"): + active = not node.isBypassed() # Set instance active state instance.data.update( diff --git a/openpype/hosts/houdini/plugins/publish/collect_instances.py b/openpype/hosts/houdini/plugins/publish/collect_instances.py index 1b36526783..ac081ac297 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_instances.py +++ b/openpype/hosts/houdini/plugins/publish/collect_instances.py @@ -31,6 +31,7 @@ class CollectInstances(pyblish.api.ContextPlugin): def process(self, context): nodes = hou.node("/out").children() + nodes += hou.node("/obj").children() # Include instances in USD stage only when it exists so it # remains backwards compatible with version before houdini 18 @@ -49,9 +50,12 @@ class CollectInstances(pyblish.api.ContextPlugin): has_family = node.evalParm("family") assert has_family, "'%s' is missing 'family'" % node.name() + self.log.info("processing {}".format(node)) + data = lib.read(node) # Check bypass state and reverse - data.update({"active": not node.isBypassed()}) + if hasattr(node, "isBypassed"): + data.update({"active": not node.isBypassed()}) # temporarily translation of `active` to `publish` till issue has # been resolved, https://github.com/pyblish/pyblish-base/issues/307 diff --git a/openpype/hosts/houdini/plugins/publish/extract_hda.py b/openpype/hosts/houdini/plugins/publish/extract_hda.py new file mode 100644 index 0000000000..301dd4e297 --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/extract_hda.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +import os + +from pprint import pformat + +import pyblish.api +import openpype.api + + +class ExtractHDA(openpype.api.Extractor): + + order = pyblish.api.ExtractorOrder + label = "Extract HDA" + hosts = ["houdini"] + families = ["hda"] + + def process(self, instance): + self.log.info(pformat(instance.data)) + hda_node = instance[0] + hda_def = hda_node.type().definition() + hda_options = hda_def.options() + hda_options.setSaveInitialParmsAndContents(True) + + next_version = instance.data["anatomyData"]["version"] + self.log.info("setting version: {}".format(next_version)) + hda_def.setVersion(str(next_version)) + hda_def.setOptions(hda_options) + hda_def.save(hda_def.libraryFilePath(), hda_node, hda_options) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + file = os.path.basename(hda_def.libraryFilePath()) + staging_dir = os.path.dirname(hda_def.libraryFilePath()) + self.log.info("Using HDA from {}".format(hda_def.libraryFilePath())) + + representation = { + 'name': 'hda', + 'ext': 'hda', + 'files': file, + "stagingDir": staging_dir, + } + instance.data["representations"].append(representation) diff --git a/openpype/hosts/houdini/plugins/publish/validate_bypass.py b/openpype/hosts/houdini/plugins/publish/validate_bypass.py index 79c67c3008..fc4e18f701 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_bypass.py +++ b/openpype/hosts/houdini/plugins/publish/validate_bypass.py @@ -35,5 +35,5 @@ class ValidateBypassed(pyblish.api.InstancePlugin): def get_invalid(cls, instance): rop = instance[0] - if rop.isBypassed(): + if hasattr(rop, "isBypassed") and rop.isBypassed(): return [rop] diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 3bff3ff79c..e9f9e87d52 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -98,7 +98,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "camerarig", "redshiftproxy", "effect", - "xgen" + "xgen", + "hda" ] exclude_families = ["clip"] db_representation_context_keys = [ From 2589ce46711fbf19d4c3c9a56fe8c10cad01ef01 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 27 Sep 2021 14:09:02 +0200 Subject: [PATCH 02/27] hda updating --- .../hosts/houdini/plugins/create/create_hda.py | 3 +++ openpype/hosts/houdini/plugins/load/load_hda.py | 16 +++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_hda.py b/openpype/hosts/houdini/plugins/create/create_hda.py index d58e0c5e52..05307d4c56 100644 --- a/openpype/hosts/houdini/plugins/create/create_hda.py +++ b/openpype/hosts/houdini/plugins/create/create_hda.py @@ -41,6 +41,9 @@ class CreateHDA(plugin.Creator): hda_node.setName(self.name) hou.moveNodesTo(self.nodes, hda_node) hda_node.layoutChildren() + # delete node created by Avalon in /out + # this needs to be addressed in future Houdini workflow refactor. + hou.node("/out/{}".format(self.name)).destroy() lib.imprint(hda_node, self.data) diff --git a/openpype/hosts/houdini/plugins/load/load_hda.py b/openpype/hosts/houdini/plugins/load/load_hda.py index 5e04d83e86..f923b699d2 100644 --- a/openpype/hosts/houdini/plugins/load/load_hda.py +++ b/openpype/hosts/houdini/plugins/load/load_hda.py @@ -15,7 +15,6 @@ class HdaLoader(api.Loader): color = "orange" def load(self, context, name=None, namespace=None, data=None): - import os import hou @@ -33,7 +32,6 @@ class HdaLoader(api.Loader): node_name = "{0}_{1:03d}".format(formatted, counter) hou.hda.installFile(file_path) - print("installing {}".format(name)) hda_node = obj.createNode(name, node_name) self[:] = [hda_node] @@ -48,9 +46,17 @@ class HdaLoader(api.Loader): ) def update(self, container, representation): - hda_node = container["node"] - hda_def = hda_node.type().definition() + import hou + hda_node = container["node"] + file_path = api.get_representation_path(representation) + file_path = file_path.replace("\\", "/") + hou.hda.installFile(file_path) + defs = hda_node.type().allInstalledDefinitions() + def_paths = [d.libraryFilePath() for d in defs] + new = def_paths.index(file_path) + defs[new].setIsPreferred(True) def remove(self, container): - pass + node = container["node"] + node.destroy() From b56623ff2d45e991d2e14cc63bda4397e4821b59 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 27 Sep 2021 14:10:37 +0200 Subject: [PATCH 03/27] remove unused import --- openpype/hosts/houdini/plugins/load/load_hda.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/load/load_hda.py b/openpype/hosts/houdini/plugins/load/load_hda.py index f923b699d2..6610d5e513 100644 --- a/openpype/hosts/houdini/plugins/load/load_hda.py +++ b/openpype/hosts/houdini/plugins/load/load_hda.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from avalon import api -from avalon.houdini import pipeline, lib +from avalon.houdini import pipeline class HdaLoader(api.Loader): From 4446c535448b4d01a87dcb1cfaddd1dd36004720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 6 Oct 2021 16:26:21 +0200 Subject: [PATCH 04/27] add parent to qt windows --- .../hosts/houdini/startup/MainMenuCommon.xml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/houdini/startup/MainMenuCommon.xml b/openpype/hosts/houdini/startup/MainMenuCommon.xml index 76585085e2..cb73d2643f 100644 --- a/openpype/hosts/houdini/startup/MainMenuCommon.xml +++ b/openpype/hosts/houdini/startup/MainMenuCommon.xml @@ -7,24 +7,30 @@ @@ -45,7 +51,8 @@ publish.show(parent) From 30e431d85e83c8924f8a373c814d20b9ecc42144 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 8 Oct 2021 16:08:15 +0200 Subject: [PATCH 05/27] check existing subset wip --- .../houdini/plugins/create/create_hda.py | 58 ++++++++++++++++--- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_hda.py b/openpype/hosts/houdini/plugins/create/create_hda.py index 05307d4c56..775c51166a 100644 --- a/openpype/hosts/houdini/plugins/create/create_hda.py +++ b/openpype/hosts/houdini/plugins/create/create_hda.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from openpype.hosts.houdini.api import plugin from avalon.houdini import lib +from avalon import io import hou @@ -17,34 +18,75 @@ class CreateHDA(plugin.Creator): super(CreateHDA, self).__init__(*args, **kwargs) self.data.pop("active", None) + def _check_existing(self, subset_name): + # type: (str) -> bool + """Check if existing subset name versions already exists.""" + # Get all subsets of the current asset + subset_docs = io.find( + { + "type": "subset", + "parent": self.data["asset"] + }, + {"name": 1} + ) + existing_subset_names = set(subset_docs.distinct("name")) + existing_subset_names_low = { + _name.lower() for _name in existing_subset_names + } + return subset_name.lower() in existing_subset_names_low + def _process(self, instance): + # get selected nodes out = hou.node("/obj") self.nodes = hou.selectedNodes() if (self.options or {}).get("useSelection") and self.nodes: + # if we have `use selection` enabled and we have some + # selected nodes ... to_hda = self.nodes[0] if len(self.nodes) > 1: + # if there is more then one node, create subnet first subnet = out.createNode( "subnet", node_name="{}_subnet".format(self.name)) to_hda = subnet else: + # in case of no selection, just create subnet node subnet = out.createNode( "subnet", node_name="{}_subnet".format(self.name)) subnet.moveToGoodPosition() to_hda = subnet - hda_node = to_hda.createDigitalAsset( - name=self.name, - hda_file_name="$HIP/{}.hda".format(self.name) - ) + if not to_hda.type().definition(): + # if node type has not its definition, it is not user + # created hda. We test if hda can be created from the node. + + if not to_hda.canCreateDigitalAsset(): + raise Exception( + "cannot create hda from node {}".format(to_hda)) + + hda_node = to_hda.createDigitalAsset( + name=self.name, + hda_file_name="$HIP/{}.hda".format(self.name) + ) + hou.moveNodesTo(self.nodes, hda_node) + hda_node.layoutChildren() + else: + hda_node = to_hda + hda_node.setName(self.name) - hou.moveNodesTo(self.nodes, hda_node) - hda_node.layoutChildren() + # delete node created by Avalon in /out # this needs to be addressed in future Houdini workflow refactor. + hou.node("/out/{}".format(self.name)).destroy() - lib.imprint(hda_node, self.data) + try: + lib.imprint(hda_node, self.data) + except hou.OperationFailed as e: + raise plugin.OpenPypeCreatorError( + ("Cannot set metadata on asset. Might be that it already is " + "OpenPype asset.") + ) - return hda_node + return hda_node \ No newline at end of file From 9065204e95b964e76b4ea1beebca55933eab7549 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 15 Oct 2021 01:32:20 +0200 Subject: [PATCH 06/27] subset check and documentation --- openpype/hosts/houdini/api/plugin.py | 3 ++- .../houdini/plugins/create/create_hda.py | 24 ++++++++++-------- website/docs/artist_hosts_houdini.md | 25 +++++++++++++++++++ 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/houdini/api/plugin.py b/openpype/hosts/houdini/api/plugin.py index efdaa60084..63d9bba470 100644 --- a/openpype/hosts/houdini/api/plugin.py +++ b/openpype/hosts/houdini/api/plugin.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Houdini specific Avalon/Pyblish plugin definitions.""" import sys +from avalon.api import CreatorError from avalon import houdini import six @@ -8,7 +9,7 @@ import hou from openpype.api import PypeCreatorMixin -class OpenPypeCreatorError(Exception): +class OpenPypeCreatorError(CreatorError): pass diff --git a/openpype/hosts/houdini/plugins/create/create_hda.py b/openpype/hosts/houdini/plugins/create/create_hda.py index 775c51166a..63d235d235 100644 --- a/openpype/hosts/houdini/plugins/create/create_hda.py +++ b/openpype/hosts/houdini/plugins/create/create_hda.py @@ -22,12 +22,13 @@ class CreateHDA(plugin.Creator): # type: (str) -> bool """Check if existing subset name versions already exists.""" # Get all subsets of the current asset + asset_id = io.find_one({"name": self.data["asset"], "type": "asset"}, + projection={"_id": True})['_id'] subset_docs = io.find( { "type": "subset", - "parent": self.data["asset"] - }, - {"name": 1} + "parent": asset_id + }, {"name": 1} ) existing_subset_names = set(subset_docs.distinct("name")) existing_subset_names_low = { @@ -36,7 +37,7 @@ class CreateHDA(plugin.Creator): return subset_name.lower() in existing_subset_names_low def _process(self, instance): - + subset_name = self.data["subset"] # get selected nodes out = hou.node("/obj") self.nodes = hou.selectedNodes() @@ -60,26 +61,29 @@ class CreateHDA(plugin.Creator): if not to_hda.type().definition(): # if node type has not its definition, it is not user # created hda. We test if hda can be created from the node. - if not to_hda.canCreateDigitalAsset(): raise Exception( "cannot create hda from node {}".format(to_hda)) hda_node = to_hda.createDigitalAsset( - name=self.name, - hda_file_name="$HIP/{}.hda".format(self.name) + name=subset_name, + hda_file_name="$HIP/{}.hda".format(subset_name) ) hou.moveNodesTo(self.nodes, hda_node) hda_node.layoutChildren() else: + if self._check_existing(subset_name): + raise plugin.OpenPypeCreatorError( + ("subset {} is already published with different HDA" + "definition.").format(subset_name)) hda_node = to_hda - hda_node.setName(self.name) + hda_node.setName(subset_name) # delete node created by Avalon in /out # this needs to be addressed in future Houdini workflow refactor. - hou.node("/out/{}".format(self.name)).destroy() + hou.node("/out/{}".format(subset_name)).destroy() try: lib.imprint(hda_node, self.data) @@ -89,4 +93,4 @@ class CreateHDA(plugin.Creator): "OpenPype asset.") ) - return hda_node \ No newline at end of file + return hda_node diff --git a/website/docs/artist_hosts_houdini.md b/website/docs/artist_hosts_houdini.md index d2aadf05cb..bd422b046e 100644 --- a/website/docs/artist_hosts_houdini.md +++ b/website/docs/artist_hosts_houdini.md @@ -76,3 +76,28 @@ I've selected `vdb1` and went **OpenPype -> Create** and selected **VDB Cache**. geometry ROP in `/out` and sets its paths to output vdb files. During the publishing process whole dops are cooked. +## Publishing Houdini Digital Assets (HDA) + +You can publish most of the nodes in Houdini as hda for easy interchange of data between Houdini instances or even +other DCCs with Houdini Engine. + +## Creating HDA + +Simply select nodes you want to include in hda and go **OpenPype -> Create** and select **Houdini digital asset (hda)**. +You can even use already existing hda as a selected node, and it will be published (see below for limitation). + +:::caution HDA Workflow limitations +As long as the hda is of same type - it is created from different nodes but using the same (subset) name, everything +is ok. But once you've published version of hda subset, you cannot change its type. For example, you create hda **Foo** +from *Cube* and *Sphere* - it will create hda subset named `hdaFoo` with the same type. You publish it as version 1. +Then you create version 2 with added *Torus*. Then you create version 3 from the scratch from completely different nodes, +but still using resulting subset name `hdaFoo`. Everything still works as expected. But then you use already +existing hda as a base, for example from different artist. Its type cannot be changed from what it was and so even if +it is named `hdaFoo` it has different type. It could be published, but you would never load it and retain ability to +switch versions between different hda types. +::: + +## Loading HDA + +When you load hda, it will install its type in your hip file and add published version as its definition file. When +you switch version via Scene Manager, it will add its definition and set it as preferred. \ No newline at end of file From ecd485309e6e08cf4c090b46e4378464c5e9ce6c Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 15 Oct 2021 14:31:04 +0200 Subject: [PATCH 07/27] fix hound --- openpype/hosts/houdini/plugins/create/create_hda.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_hda.py b/openpype/hosts/houdini/plugins/create/create_hda.py index 63d235d235..2af1e4a257 100644 --- a/openpype/hosts/houdini/plugins/create/create_hda.py +++ b/openpype/hosts/houdini/plugins/create/create_hda.py @@ -74,8 +74,8 @@ class CreateHDA(plugin.Creator): else: if self._check_existing(subset_name): raise plugin.OpenPypeCreatorError( - ("subset {} is already published with different HDA" - "definition.").format(subset_name)) + ("subset {} is already published with different HDA" + "definition.").format(subset_name)) hda_node = to_hda hda_node.setName(subset_name) @@ -87,7 +87,7 @@ class CreateHDA(plugin.Creator): try: lib.imprint(hda_node, self.data) - except hou.OperationFailed as e: + except hou.OperationFailed: raise plugin.OpenPypeCreatorError( ("Cannot set metadata on asset. Might be that it already is " "OpenPype asset.") From 0f6145f0a6388ed2c83ce4b5be051e9cc6a34ea8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 26 Oct 2021 16:02:21 +0200 Subject: [PATCH 08/27] added second function get_subset_name_with_asset_doc to be able get subset name --- openpype/lib/__init__.py | 2 + openpype/lib/plugin_tools.py | 135 +++++++++++++++++++++++++++++------ 2 files changed, 115 insertions(+), 22 deletions(-) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 74004a1239..ee4821b80d 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -130,6 +130,7 @@ from .applications import ( from .plugin_tools import ( TaskNotSetError, get_subset_name, + get_subset_name_with_asset_doc, prepare_template_data, filter_pyblish_plugins, set_plugin_attributes_from_settings, @@ -249,6 +250,7 @@ __all__ = [ "TaskNotSetError", "get_subset_name", + "get_subset_name_with_asset_doc", "filter_pyblish_plugins", "set_plugin_attributes_from_settings", "source_hash", diff --git a/openpype/lib/plugin_tools.py b/openpype/lib/plugin_tools.py index 47e6641731..7d1ccf3826 100644 --- a/openpype/lib/plugin_tools.py +++ b/openpype/lib/plugin_tools.py @@ -28,17 +28,48 @@ class TaskNotSetError(KeyError): super(TaskNotSetError, self).__init__(msg) -def get_subset_name( +def _get_subset_name( family, variant, task_name, asset_id, - project_name=None, - host_name=None, - default_template=None, - dynamic_data=None, - dbcon=None + asset_doc, + project_name, + host_name, + default_template, + dynamic_data, + dbcon ): + """Calculate subset name based on passed context and OpenPype settings. + + Subst name templates are defined in `project_settings/global/tools/creator + /subset_name_profiles` where are profiles with host name, family, task name + and task type filters. If context does not match any profile then + `DEFAULT_SUBSET_TEMPLATE` is used as default template. + + That's main reason why so many arguments are required to calculate subset + name. + + Args: + family (str): Instance family. + variant (str): In most of cases it is user input during creation. + task_name (str): Task name on which context is instance created. + asset_id (ObjectId): Id of object. Is optional if `asset_doc` is + passed. + asset_doc (dict): Queried asset document with it's tasks in data. + Used to get task type. + project_name (str): Name of project on which is instance created. + Important for project settings that are loaded. + host_name (str): One of filtering criteria for template profile + filters. + default_template (str): Default template if any profile does not match + passed context. Constant 'DEFAULT_SUBSET_TEMPLATE' is used if + is not passed. + dynamic_data (dict): Dynamic data specific for a creator which creates + instance. + dbcon (AvalonMongoDB): Mongo connection to be able query asset document + if 'asset_doc' is not passed. + """ if not family: return "" @@ -53,25 +84,25 @@ def get_subset_name( project_name = avalon.api.Session["AVALON_PROJECT"] - # Function should expect asset document instead of asset id - # - that way `dbcon` is not needed - if dbcon is None: - from avalon.api import AvalonMongoDB + # Query asset document if was not passed + if asset_doc is None: + if dbcon is None: + from avalon.api import AvalonMongoDB - dbcon = AvalonMongoDB() - dbcon.Session["AVALON_PROJECT"] = project_name + dbcon = AvalonMongoDB() + dbcon.Session["AVALON_PROJECT"] = project_name - dbcon.install() + dbcon.install() - asset_doc = dbcon.find_one( - { - "type": "asset", - "_id": asset_id - }, - { - "data.tasks": True - } - ) + asset_doc = dbcon.find_one( + { + "type": "asset", + "_id": asset_id + }, + { + "data.tasks": True + } + ) or {} asset_tasks = asset_doc.get("data", {}).get("tasks") or {} task_info = asset_tasks.get(task_name) or {} task_type = task_info.get("type") @@ -113,6 +144,66 @@ def get_subset_name( return template.format(**prepare_template_data(fill_pairs)) +def get_subset_name_with_asset_doc( + family, + variant, + task_name, + asset_doc, + project_name=None, + host_name=None, + default_template=None, + dynamic_data=None, + dbcon=None +): + """Calculate subset name using OpenPype settings. + + This variant of function expects already queried asset document. + """ + return _get_subset_name( + family, variant, + task_name, + None, + asset_doc, + project_name, + host_name, + default_template, + dynamic_data, + dbcon + ) + + +def get_subset_name( + family, + variant, + task_name, + asset_id, + project_name=None, + host_name=None, + default_template=None, + dynamic_data=None, + dbcon=None +): + """Calculate subset name using OpenPype settings. + + This variant of function expects asset id as argument. + + This is legacy function should be replaced with + `get_subset_name_with_asset_doc` where asset document is expected. + """ + return _get_subset_name( + family, + variant, + task_name, + asset_id, + None, + project_name, + host_name, + default_template, + dynamic_data, + dbcon + ) + + def prepare_template_data(fill_pairs): """ Prepares formatted data for filling template. From 680792110edcd17ac5a257017c83810be24df446 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 26 Oct 2021 16:03:06 +0200 Subject: [PATCH 09/27] use 'get_subset_name_with_asset_doc' where 'get_subset_name' was used --- .../publish/collect_bulk_mov_instances.py | 20 +++++++------------ .../plugins/publish/collect_instances.py | 20 +++++++------------ .../plugins/publish/collect_workfile.py | 20 +++++++------------ 3 files changed, 21 insertions(+), 39 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_bulk_mov_instances.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_bulk_mov_instances.py index a5177335b3..9f075d66cf 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_bulk_mov_instances.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_bulk_mov_instances.py @@ -3,7 +3,7 @@ import json import pyblish.api from avalon import io -from openpype.lib import get_subset_name +from openpype.lib import get_subset_name_with_asset_doc class CollectBulkMovInstances(pyblish.api.InstancePlugin): @@ -26,16 +26,10 @@ class CollectBulkMovInstances(pyblish.api.InstancePlugin): context = instance.context asset_name = instance.data["asset"] - asset_doc = io.find_one( - { - "type": "asset", - "name": asset_name - }, - { - "_id": 1, - "data.tasks": 1 - } - ) + asset_doc = io.find_one({ + "type": "asset", + "name": asset_name + }) if not asset_doc: raise AssertionError(( "Couldn't find Asset document with name \"{}\"" @@ -53,11 +47,11 @@ class CollectBulkMovInstances(pyblish.api.InstancePlugin): task_name = available_task_names[_task_name_low] break - subset_name = get_subset_name( + subset_name = get_subset_name_with_asset_doc( self.new_instance_family, self.subset_name_variant, task_name, - asset_doc["_id"], + asset_doc, io.Session["AVALON_PROJECT"] ) instance_name = f"{asset_name}_{subset_name}" diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py index dfa8f17ee9..1d7a48e389 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py @@ -4,7 +4,7 @@ import copy import pyblish.api from avalon import io -from openpype.lib import get_subset_name +from openpype.lib import get_subset_name_with_asset_doc class CollectInstances(pyblish.api.ContextPlugin): @@ -70,16 +70,10 @@ class CollectInstances(pyblish.api.ContextPlugin): # - not sure if it's good idea to require asset id in # get_subset_name? asset_name = context.data["workfile_context"]["asset"] - asset_doc = io.find_one( - { - "type": "asset", - "name": asset_name - }, - {"_id": 1} - ) - asset_id = None - if asset_doc: - asset_id = asset_doc["_id"] + asset_doc = io.find_one({ + "type": "asset", + "name": asset_name + }) # Project name from workfile context project_name = context.data["workfile_context"]["project"] @@ -88,11 +82,11 @@ class CollectInstances(pyblish.api.ContextPlugin): # Use empty variant value variant = "" task_name = io.Session["AVALON_TASK"] - new_subset_name = get_subset_name( + new_subset_name = get_subset_name_with_asset_doc( family, variant, task_name, - asset_id, + asset_doc, project_name, host_name ) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py b/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py index 65e38ea258..68ba350a85 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py @@ -3,7 +3,7 @@ import json import pyblish.api from avalon import io -from openpype.lib import get_subset_name +from openpype.lib import get_subset_name_with_asset_doc class CollectWorkfile(pyblish.api.ContextPlugin): @@ -28,16 +28,10 @@ class CollectWorkfile(pyblish.api.ContextPlugin): # get_subset_name? family = "workfile" asset_name = context.data["workfile_context"]["asset"] - asset_doc = io.find_one( - { - "type": "asset", - "name": asset_name - }, - {"_id": 1} - ) - asset_id = None - if asset_doc: - asset_id = asset_doc["_id"] + asset_doc = io.find_one({ + "type": "asset", + "name": asset_name + }) # Project name from workfile context project_name = context.data["workfile_context"]["project"] @@ -46,11 +40,11 @@ class CollectWorkfile(pyblish.api.ContextPlugin): # Use empty variant value variant = "" task_name = io.Session["AVALON_TASK"] - subset_name = get_subset_name( + subset_name = get_subset_name_with_asset_doc( family, variant, task_name, - asset_id, + asset_doc, project_name, host_name ) From f5225b1b4e9b044132fdf98826e1ec43e22b0d02 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 27 Oct 2021 17:29:39 +0200 Subject: [PATCH 10/27] Initial commit of roots entity --- openpype/settings/entities/__init__.py | 6 +++++- .../settings/entities/dict_immutable_keys_entity.py | 12 ++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/__init__.py b/openpype/settings/entities/__init__.py index aae2d1fa89..b0fbe585ae 100644 --- a/openpype/settings/entities/__init__.py +++ b/openpype/settings/entities/__init__.py @@ -110,7 +110,10 @@ from .enum_entity import ( ) from .list_entity import ListEntity -from .dict_immutable_keys_entity import DictImmutableKeysEntity +from .dict_immutable_keys_entity import ( + DictImmutableKeysEntity, + RootsEntity +) from .dict_mutable_keys_entity import DictMutableKeysEntity from .dict_conditional import ( DictConditionalEntity, @@ -169,6 +172,7 @@ __all__ = ( "ListEntity", "DictImmutableKeysEntity", + "RootsEntity", "DictMutableKeysEntity", diff --git a/openpype/settings/entities/dict_immutable_keys_entity.py b/openpype/settings/entities/dict_immutable_keys_entity.py index 57e21ff5f3..f5946b2a86 100644 --- a/openpype/settings/entities/dict_immutable_keys_entity.py +++ b/openpype/settings/entities/dict_immutable_keys_entity.py @@ -547,3 +547,15 @@ class DictImmutableKeysEntity(ItemEntity): super(DictImmutableKeysEntity, self).reset_callbacks() for child_entity in self.children: child_entity.reset_callbacks() + + +class RootsEntity(DictImmutableKeysEntity): + """Entity that adds ability to fill value for roots of current project. + + Value schema is defined by `object_type`. + + It is not possible to change override state (Studio values will always + contain studio overrides and same for project). That is because roots can + be totally different for each project. + """ + schema_types = ["dict-roots"] From f26d16f4fe8216db920dff461b96c640e7e223ca Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 27 Oct 2021 17:34:50 +0200 Subject: [PATCH 11/27] fixed typo '_item_initalization' to '_item_initialization' --- openpype/settings/entities/base_entity.py | 4 ++-- openpype/settings/entities/color_entity.py | 2 +- openpype/settings/entities/dict_conditional.py | 2 +- .../entities/dict_immutable_keys_entity.py | 2 +- .../entities/dict_mutable_keys_entity.py | 2 +- openpype/settings/entities/enum_entity.py | 16 ++++++++-------- openpype/settings/entities/input_entities.py | 10 +++++----- openpype/settings/entities/item_entities.py | 4 ++-- openpype/settings/entities/list_entity.py | 2 +- openpype/settings/entities/root_entities.py | 4 ++-- 10 files changed, 24 insertions(+), 24 deletions(-) diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index 0e8274d374..341968bd75 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -510,7 +510,7 @@ class BaseItemEntity(BaseEntity): pass @abstractmethod - def _item_initalization(self): + def _item_initialization(self): """Entity specific initialization process.""" pass @@ -920,7 +920,7 @@ class ItemEntity(BaseItemEntity): _default_label_wrap["collapsed"] ) - self._item_initalization() + self._item_initialization() def save(self): """Call save on root item.""" diff --git a/openpype/settings/entities/color_entity.py b/openpype/settings/entities/color_entity.py index dfaa75e761..3becf2d865 100644 --- a/openpype/settings/entities/color_entity.py +++ b/openpype/settings/entities/color_entity.py @@ -9,7 +9,7 @@ from .exceptions import ( class ColorEntity(InputEntity): schema_types = ["color"] - def _item_initalization(self): + def _item_initialization(self): self.valid_value_types = (list, ) self.value_on_not_set = [0, 0, 0, 255] self.use_alpha = self.schema_data.get("use_alpha", True) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 6f27760570..0cb8827991 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -107,7 +107,7 @@ class DictConditionalEntity(ItemEntity): for _key, _value in new_value.items(): self.non_gui_children[self.current_enum][_key].set(_value) - def _item_initalization(self): + def _item_initialization(self): self._default_metadata = NOT_SET self._studio_override_metadata = NOT_SET self._project_override_metadata = NOT_SET diff --git a/openpype/settings/entities/dict_immutable_keys_entity.py b/openpype/settings/entities/dict_immutable_keys_entity.py index f5946b2a86..fe109734fe 100644 --- a/openpype/settings/entities/dict_immutable_keys_entity.py +++ b/openpype/settings/entities/dict_immutable_keys_entity.py @@ -172,7 +172,7 @@ class DictImmutableKeysEntity(ItemEntity): for child_obj in added_children: self.gui_layout.append(child_obj) - def _item_initalization(self): + def _item_initialization(self): self._default_metadata = NOT_SET self._studio_override_metadata = NOT_SET self._project_override_metadata = NOT_SET diff --git a/openpype/settings/entities/dict_mutable_keys_entity.py b/openpype/settings/entities/dict_mutable_keys_entity.py index f75fb23d82..cff346e9ea 100644 --- a/openpype/settings/entities/dict_mutable_keys_entity.py +++ b/openpype/settings/entities/dict_mutable_keys_entity.py @@ -191,7 +191,7 @@ class DictMutableKeysEntity(EndpointEntity): child_entity = self.children_by_key[key] self.set_child_label(child_entity, label) - def _item_initalization(self): + def _item_initialization(self): self._default_metadata = {} self._studio_override_metadata = {} self._project_override_metadata = {} diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index a5e734f039..81c2d5d9d5 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -8,7 +8,7 @@ from .lib import ( class BaseEnumEntity(InputEntity): - def _item_initalization(self): + def _item_initialization(self): self.multiselection = True self.value_on_not_set = None self.enum_items = None @@ -70,7 +70,7 @@ class BaseEnumEntity(InputEntity): class EnumEntity(BaseEnumEntity): schema_types = ["enum"] - def _item_initalization(self): + def _item_initialization(self): self.multiselection = self.schema_data.get("multiselection", False) self.enum_items = self.schema_data.get("enum_items") # Default is optional and non breaking attribute @@ -156,7 +156,7 @@ class HostsEnumEntity(BaseEnumEntity): "standalonepublisher" ] - def _item_initalization(self): + def _item_initialization(self): self.multiselection = self.schema_data.get("multiselection", True) use_empty_value = False if not self.multiselection: @@ -249,7 +249,7 @@ class HostsEnumEntity(BaseEnumEntity): class AppsEnumEntity(BaseEnumEntity): schema_types = ["apps-enum"] - def _item_initalization(self): + def _item_initialization(self): self.multiselection = True self.value_on_not_set = [] self.enum_items = [] @@ -316,7 +316,7 @@ class AppsEnumEntity(BaseEnumEntity): class ToolsEnumEntity(BaseEnumEntity): schema_types = ["tools-enum"] - def _item_initalization(self): + def _item_initialization(self): self.multiselection = True self.value_on_not_set = [] self.enum_items = [] @@ -375,7 +375,7 @@ class ToolsEnumEntity(BaseEnumEntity): class TaskTypeEnumEntity(BaseEnumEntity): schema_types = ["task-types-enum"] - def _item_initalization(self): + def _item_initialization(self): self.multiselection = self.schema_data.get("multiselection", True) if self.multiselection: self.valid_value_types = (list, ) @@ -451,7 +451,7 @@ class TaskTypeEnumEntity(BaseEnumEntity): class DeadlineUrlEnumEntity(BaseEnumEntity): schema_types = ["deadline_url-enum"] - def _item_initalization(self): + def _item_initialization(self): self.multiselection = self.schema_data.get("multiselection", True) self.enum_items = [] @@ -502,7 +502,7 @@ class DeadlineUrlEnumEntity(BaseEnumEntity): class AnatomyTemplatesEnumEntity(BaseEnumEntity): schema_types = ["anatomy-templates-enum"] - def _item_initalization(self): + def _item_initialization(self): self.multiselection = False self.enum_items = [] diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index 0ded3ab7e5..a0598d405e 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -362,7 +362,7 @@ class NumberEntity(InputEntity): float_number_regex = re.compile(r"^\d+\.\d+$") int_number_regex = re.compile(r"^\d+$") - def _item_initalization(self): + def _item_initialization(self): self.minimum = self.schema_data.get("minimum", -99999) self.maximum = self.schema_data.get("maximum", 99999) self.decimal = self.schema_data.get("decimal", 0) @@ -420,7 +420,7 @@ class NumberEntity(InputEntity): class BoolEntity(InputEntity): schema_types = ["boolean"] - def _item_initalization(self): + def _item_initialization(self): self.valid_value_types = (bool, ) value_on_not_set = self.convert_to_valid_type( self.schema_data.get("default", True) @@ -431,7 +431,7 @@ class BoolEntity(InputEntity): class TextEntity(InputEntity): schema_types = ["text"] - def _item_initalization(self): + def _item_initialization(self): self.valid_value_types = (STRING_TYPE, ) self.value_on_not_set = "" @@ -449,7 +449,7 @@ class TextEntity(InputEntity): class PathInput(InputEntity): schema_types = ["path-input"] - def _item_initalization(self): + def _item_initialization(self): self.valid_value_types = (STRING_TYPE, ) self.value_on_not_set = "" @@ -460,7 +460,7 @@ class PathInput(InputEntity): class RawJsonEntity(InputEntity): schema_types = ["raw-json"] - def _item_initalization(self): + def _item_initialization(self): # Schema must define if valid value is dict or list store_as_string = self.schema_data.get("store_as_string", False) is_list = self.schema_data.get("is_list", False) diff --git a/openpype/settings/entities/item_entities.py b/openpype/settings/entities/item_entities.py index c7c9c3097e..ff0a982900 100644 --- a/openpype/settings/entities/item_entities.py +++ b/openpype/settings/entities/item_entities.py @@ -48,7 +48,7 @@ class PathEntity(ItemEntity): raise AttributeError(self.attribute_error_msg.format("items")) return self.child_obj.items() - def _item_initalization(self): + def _item_initialization(self): if self.group_item is None and not self.is_group: self.is_group = True @@ -216,7 +216,7 @@ class ListStrictEntity(ItemEntity): return self.children[idx] return default - def _item_initalization(self): + def _item_initialization(self): self.valid_value_types = (list, ) self.require_key = True diff --git a/openpype/settings/entities/list_entity.py b/openpype/settings/entities/list_entity.py index b06f4d7a2e..5d89a81351 100644 --- a/openpype/settings/entities/list_entity.py +++ b/openpype/settings/entities/list_entity.py @@ -149,7 +149,7 @@ class ListEntity(EndpointEntity): return list(value) return NOT_SET - def _item_initalization(self): + def _item_initialization(self): self.valid_value_types = (list, ) self.children = [] self.value_on_not_set = [] diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index 05d20ee60b..b8baed8a93 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -65,7 +65,7 @@ class RootEntity(BaseItemEntity): super(RootEntity, self).__init__(schema_data) self._require_restart_callbacks = [] self._item_ids_require_restart = set() - self._item_initalization() + self._item_initialization() if reset: self.reset() @@ -176,7 +176,7 @@ class RootEntity(BaseItemEntity): for child_obj in added_children: self.gui_layout.append(child_obj) - def _item_initalization(self): + def _item_initialization(self): # Store `self` to `root_item` for children entities self.root_item = self From 07ccefc121f9db740b2e49c3efa58d592b9041ce Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 27 Oct 2021 17:38:00 +0200 Subject: [PATCH 12/27] added initialization of roots dict entity --- openpype/settings/entities/__init__.py | 4 +-- .../entities/dict_immutable_keys_entity.py | 30 +++++++++++++++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/openpype/settings/entities/__init__.py b/openpype/settings/entities/__init__.py index b0fbe585ae..775bf40ac4 100644 --- a/openpype/settings/entities/__init__.py +++ b/openpype/settings/entities/__init__.py @@ -112,7 +112,7 @@ from .enum_entity import ( from .list_entity import ListEntity from .dict_immutable_keys_entity import ( DictImmutableKeysEntity, - RootsEntity + RootsDictEntity ) from .dict_mutable_keys_entity import DictMutableKeysEntity from .dict_conditional import ( @@ -172,7 +172,7 @@ __all__ = ( "ListEntity", "DictImmutableKeysEntity", - "RootsEntity", + "RootsDictEntity", "DictMutableKeysEntity", diff --git a/openpype/settings/entities/dict_immutable_keys_entity.py b/openpype/settings/entities/dict_immutable_keys_entity.py index fe109734fe..d3ab86b986 100644 --- a/openpype/settings/entities/dict_immutable_keys_entity.py +++ b/openpype/settings/entities/dict_immutable_keys_entity.py @@ -4,7 +4,8 @@ import collections from .lib import ( WRAPPER_TYPES, OverrideState, - NOT_SET + NOT_SET, + STRING_TYPE ) from openpype.settings.constants import ( METADATA_KEYS, @@ -549,7 +550,7 @@ class DictImmutableKeysEntity(ItemEntity): child_entity.reset_callbacks() -class RootsEntity(DictImmutableKeysEntity): +class RootsDictEntity(DictImmutableKeysEntity): """Entity that adds ability to fill value for roots of current project. Value schema is defined by `object_type`. @@ -558,4 +559,29 @@ class RootsEntity(DictImmutableKeysEntity): contain studio overrides and same for project). That is because roots can be totally different for each project. """ + _origin_schema_data = None schema_types = ["dict-roots"] + + def _item_initialization(self): + origin_schema_data = self.schema_data + + self.separate_items = origin_schema_data.get("separate_items", True) + object_type = origin_schema_data.get("object_type") + if isinstance(object_type, STRING_TYPE): + object_type = {"type": object_type} + self.object_type = object_type + + if not self.is_group: + self.is_group = True + + schema_data = copy.deepcopy(self.schema_data) + schema_data["children"] = [] + + self.schema_data = schema_data + self._origin_schema_data = origin_schema_data + + self._default_value = NOT_SET + self._studio_value = NOT_SET + self._project_value = NOT_SET + + super(RootsDictEntity, self)._item_initialization() From afb31d1de18cfbbe2cefeecad7471ebf853c151a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 27 Oct 2021 17:39:50 +0200 Subject: [PATCH 13/27] added schema validations of roots dict entity --- .../entities/dict_immutable_keys_entity.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/openpype/settings/entities/dict_immutable_keys_entity.py b/openpype/settings/entities/dict_immutable_keys_entity.py index d3ab86b986..d7812a51d8 100644 --- a/openpype/settings/entities/dict_immutable_keys_entity.py +++ b/openpype/settings/entities/dict_immutable_keys_entity.py @@ -585,3 +585,20 @@ class RootsDictEntity(DictImmutableKeysEntity): self._project_value = NOT_SET super(RootsDictEntity, self)._item_initialization() + + def schema_validations(self): + if self.object_type is None: + reason = ( + "Missing children definitions for root values" + " ('object_type' not filled)." + ) + raise EntitySchemaError(self, reason) + + if not isinstance(self.object_type, dict): + reason = ( + "Children definitions for root values must be dictionary" + " ('object_type' is \"{}\")." + ).format(str(type(self.object_type))) + raise EntitySchemaError(self, reason) + + super(RootsDictEntity, self).schema_validations() From 61a05d5088320b080b58fc1cea5dd22d93d94861 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 27 Oct 2021 17:40:22 +0200 Subject: [PATCH 14/27] make sure that entity is always overriden for current override state --- .../settings/entities/dict_immutable_keys_entity.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openpype/settings/entities/dict_immutable_keys_entity.py b/openpype/settings/entities/dict_immutable_keys_entity.py index d7812a51d8..ccdc6fab1f 100644 --- a/openpype/settings/entities/dict_immutable_keys_entity.py +++ b/openpype/settings/entities/dict_immutable_keys_entity.py @@ -602,3 +602,14 @@ class RootsDictEntity(DictImmutableKeysEntity): raise EntitySchemaError(self, reason) super(RootsDictEntity, self).schema_validations() + + def on_child_change(self, child_obj): + if self._override_state is OverrideState.STUDIO: + if not child_obj.has_studio_override: + self.add_to_studio_default() + + elif self._override_state is OverrideState.PROJECT: + if not child_obj.has_project_override: + self.add_to_project_override() + + return super(RootsDictEntity, self).on_child_change(child_obj) From 61a7cee37872b69a04a453454e1ea7cb59160414 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 27 Oct 2021 17:43:49 +0200 Subject: [PATCH 15/27] override '_update_current_metadata' --- openpype/settings/entities/dict_immutable_keys_entity.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/settings/entities/dict_immutable_keys_entity.py b/openpype/settings/entities/dict_immutable_keys_entity.py index ccdc6fab1f..e5722f7064 100644 --- a/openpype/settings/entities/dict_immutable_keys_entity.py +++ b/openpype/settings/entities/dict_immutable_keys_entity.py @@ -613,3 +613,9 @@ class RootsDictEntity(DictImmutableKeysEntity): self.add_to_project_override() return super(RootsDictEntity, self).on_child_change(child_obj) + + + def _update_current_metadata(self): + """Override this method as this entity should not have metadata.""" + self._metadata_are_modified = False + self._current_metadata = {} From 68595d95cc88e0ee1e84bfbac224a8af159be89c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 27 Oct 2021 17:44:14 +0200 Subject: [PATCH 16/27] don't care about metadata in update methods --- .../entities/dict_immutable_keys_entity.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/openpype/settings/entities/dict_immutable_keys_entity.py b/openpype/settings/entities/dict_immutable_keys_entity.py index e5722f7064..f82f77acdc 100644 --- a/openpype/settings/entities/dict_immutable_keys_entity.py +++ b/openpype/settings/entities/dict_immutable_keys_entity.py @@ -619,3 +619,40 @@ class RootsDictEntity(DictImmutableKeysEntity): """Override this method as this entity should not have metadata.""" self._metadata_are_modified = False self._current_metadata = {} + + def update_default_value(self, value): + """Update default values. + + Not an api method, should be called by parent. + """ + value = self._check_update_value(value, "default") + value, _ = self._prepare_value(value) + + self._default_value = value + self._default_metadata = {} + self.has_default_value = value is not NOT_SET + + def update_studio_value(self, value): + """Update studio override values. + + Not an api method, should be called by parent. + """ + value = self._check_update_value(value, "studio override") + value, _ = self._prepare_value(value) + + self._studio_value = value + self._studio_override_metadata = {} + self.had_studio_override = value is not NOT_SET + + def update_project_value(self, value): + """Update project override values. + + Not an api method, should be called by parent. + """ + + value = self._check_update_value(value, "project override") + value, _metadata = self._prepare_value(value) + + self._project_value = value + self._project_override_metadata = {} + self.had_project_override = value is not NOT_SET From 94a6dbb172d7fb1d91240f528cd207d9a637dcfe Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 27 Oct 2021 17:44:37 +0200 Subject: [PATCH 17/27] implemented setting of override value --- .../entities/dict_immutable_keys_entity.py | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/openpype/settings/entities/dict_immutable_keys_entity.py b/openpype/settings/entities/dict_immutable_keys_entity.py index f82f77acdc..d0cd41d11c 100644 --- a/openpype/settings/entities/dict_immutable_keys_entity.py +++ b/openpype/settings/entities/dict_immutable_keys_entity.py @@ -19,6 +19,7 @@ from . import ( GUIEntity ) from .exceptions import ( + DefaultsNotDefined, SchemaDuplicatedKeys, EntitySchemaError, InvalidKeySymbols @@ -603,6 +604,43 @@ class RootsDictEntity(DictImmutableKeysEntity): super(RootsDictEntity, self).schema_validations() + def set_override_state(self, state, ignore_missing_defaults): + self.children = [] + self.non_gui_children = {} + self.gui_layout = [] + + roots_entity = self.get_entity_from_path( + "project_anatomy/roots" + ) + children = [] + first = True + for key in roots_entity.keys(): + if first: + first = False + elif self.separate_items: + children.append({"type": "separator"}) + child = copy.deepcopy(self.object_type) + child["key"] = key + child["label"] = key + children.append(child) + + schema_data = copy.deepcopy(self.schema_data) + schema_data["children"] = children + + self._add_children(schema_data) + + self._set_children_values(state) + + super(RootsDictEntity, self).set_override_state( + state, True + ) + + if state == OverrideState.STUDIO: + self.add_to_studio_default() + + elif state == OverrideState.PROJECT: + self.add_to_project_override() + def on_child_change(self, child_obj): if self._override_state is OverrideState.STUDIO: if not child_obj.has_studio_override: @@ -614,6 +652,36 @@ class RootsDictEntity(DictImmutableKeysEntity): return super(RootsDictEntity, self).on_child_change(child_obj) + def _set_children_values(self, state): + if state >= OverrideState.DEFAULTS: + default_value = self._default_value + if default_value is NOT_SET: + if state > OverrideState.DEFAULTS: + raise DefaultsNotDefined(self) + else: + default_value = {} + + for key, child_obj in self.non_gui_children.items(): + child_value = default_value.get(key, NOT_SET) + child_obj.update_default_value(child_value) + + if state >= OverrideState.STUDIO: + value = self._studio_value + if value is NOT_SET: + value = {} + + for key, child_obj in self.non_gui_children.items(): + child_value = value.get(key, NOT_SET) + child_obj.update_studio_value(child_value) + + if state >= OverrideState.PROJECT: + value = self._project_value + if value is NOT_SET: + value = {} + + for key, child_obj in self.non_gui_children.items(): + child_value = value.get(key, NOT_SET) + child_obj.update_project_value(child_value) def _update_current_metadata(self): """Override this method as this entity should not have metadata.""" From c1422afe2c4c13bcf4bffc4d2baadb72a4de4d9d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 27 Oct 2021 17:45:01 +0200 Subject: [PATCH 18/27] added short readme for roots dict enity --- openpype/settings/entities/schemas/README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index 5258fef9ec..4e8dcc36ce 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -208,6 +208,25 @@ } ``` +## dict-roots +- entity can be used only in Project settings +- keys of dictionary are based on current project roots +- they are not updated "live" it is required to save root changes and then + modify values on this entity + # TODO do live updates +``` +{ + "type": "dict-roots", + "key": "roots", + "label": "Roots", + "object_type": { + "type": "path", + "multiplatform": true, + "multipath": false + } +} +``` + ## dict-conditional - is similar to `dict` but has always available one enum entity - the enum entity has single selection and it's value define other children entities From 2207d3a279a7974d187a43d404861cd68f06c05c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 27 Oct 2021 18:13:17 +0200 Subject: [PATCH 19/27] use scene inventory from host tools --- openpype/hosts/maya/api/__init__.py | 3 +-- openpype/plugins/publish/validate_containers.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/api/__init__.py b/openpype/hosts/maya/api/__init__.py index d1c13b04d5..0a8370eafc 100644 --- a/openpype/hosts/maya/api/__init__.py +++ b/openpype/hosts/maya/api/__init__.py @@ -275,8 +275,7 @@ def on_open(_): # Show outdated pop-up def _on_show_inventory(): - import avalon.tools.sceneinventory as tool - tool.show(parent=parent) + host_tools.show_scene_inventory(parent=parent) dialog = popup.Popup(parent=parent) dialog.setWindowTitle("Maya scene has outdated content") diff --git a/openpype/plugins/publish/validate_containers.py b/openpype/plugins/publish/validate_containers.py index 784221c3b6..ce91bd3396 100644 --- a/openpype/plugins/publish/validate_containers.py +++ b/openpype/plugins/publish/validate_containers.py @@ -9,9 +9,9 @@ class ShowInventory(pyblish.api.Action): on = "failed" def process(self, context, plugin): - from avalon.tools import sceneinventory + from openpype.tools.utils import host_tools - sceneinventory.show() + host_tools.show_scene_inventory() class ValidateContainers(pyblish.api.ContextPlugin): From 7c1ad1883de9d8defe2adfcd65d53fd0db61eb66 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 27 Oct 2021 18:14:17 +0200 Subject: [PATCH 20/27] use AssetWidget from openpype.tool --- openpype/hosts/houdini/api/usd.py | 8 +++----- openpype/tools/launcher/window.py | 3 +-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/houdini/api/usd.py b/openpype/hosts/houdini/api/usd.py index 850ffb60e5..6f808779ea 100644 --- a/openpype/hosts/houdini/api/usd.py +++ b/openpype/hosts/houdini/api/usd.py @@ -4,8 +4,8 @@ import contextlib import logging from Qt import QtCore, QtGui -from avalon.tools.widgets import AssetWidget -from avalon import style +from openpype.tools.utils.widgets import AssetWidget +from avalon import style, io from pxr import Sdf @@ -31,7 +31,7 @@ def pick_asset(node): # Construct the AssetWidget as a frameless popup so it automatically # closes when clicked outside of it. global tool - tool = AssetWidget(silo_creatable=False) + tool = AssetWidget(io) tool.setContentsMargins(5, 5, 5, 5) tool.setWindowTitle("Pick Asset") tool.setStyleSheet(style.load_stylesheet()) @@ -41,8 +41,6 @@ def pick_asset(node): # Select the current asset if there is any name = parm.eval() if name: - from avalon import io - db_asset = io.find_one({"name": name, "type": "asset"}) if db_asset: silo = db_asset.get("silo") diff --git a/openpype/tools/launcher/window.py b/openpype/tools/launcher/window.py index 9b839fb2bc..9e4af1c89b 100644 --- a/openpype/tools/launcher/window.py +++ b/openpype/tools/launcher/window.py @@ -8,8 +8,7 @@ from avalon.api import AvalonMongoDB from openpype import style from openpype.api import resources -from avalon.tools import lib as tools_lib -from avalon.tools.widgets import AssetWidget +from openpype.tools.utils.widgets import AssetWidget from avalon.vendor import qtawesome from .models import ProjectModel from .lib import get_action_label, ProjectHandler From 0f026551fe436dba388004f8bbea70af0286664d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 27 Oct 2021 18:14:24 +0200 Subject: [PATCH 21/27] removed dummy context manager --- openpype/tools/utils/lib.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index d01dbbd169..4b91a5e6dd 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -82,18 +82,6 @@ def schedule(func, time, channel="default"): SharedObjects.jobs[channel] = timer -@contextlib.contextmanager -def dummy(): - """Dummy context manager - - Usage: - >> with some_context() if False else dummy(): - .. pass - - """ - yield - - def iter_model_rows(model, column, include_root=False): """Iterate over all row indices in a model""" indices = [QtCore.QModelIndex()] # start iteration at root From ea30fb1b7395495fded8bff9a749be2f3845b480 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 27 Oct 2021 18:35:57 +0200 Subject: [PATCH 22/27] removed unused defer function --- openpype/tools/utils/lib.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 4b91a5e6dd..05e8fc4cfb 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -35,26 +35,6 @@ def application(): yield app -def defer(delay, func): - """Append artificial delay to `func` - - This aids in keeping the GUI responsive, but complicates logic - when producing tests. To combat this, the environment variable ensures - that every operation is synchonous. - - Arguments: - delay (float): Delay multiplier; default 1, 0 means no delay - func (callable): Any callable - - """ - - delay *= float(os.getenv("PYBLISH_DELAY", 1)) - if delay > 0: - return QtCore.QTimer.singleShot(delay, func) - else: - return func() - - class SharedObjects: jobs = {} From d311e23165cfef41cbf6477a4c49e34e565dce60 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 27 Oct 2021 18:36:04 +0200 Subject: [PATCH 23/27] removed unused import --- openpype/tools/pyblish_pype/control.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/tools/pyblish_pype/control.py b/openpype/tools/pyblish_pype/control.py index 234135fd9a..1fa3ee657b 100644 --- a/openpype/tools/pyblish_pype/control.py +++ b/openpype/tools/pyblish_pype/control.py @@ -7,7 +7,6 @@ an active window manager; such as via Travis-CI. """ import os import sys -import traceback import inspect import logging From 5c851691966e5806ba9144b8e19fa3d6ffee440e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 27 Oct 2021 18:53:31 +0200 Subject: [PATCH 24/27] removed unused preserve_states --- openpype/tools/utils/lib.py | 70 ------------------------------------- 1 file changed, 70 deletions(-) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 05e8fc4cfb..aad00f886c 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -79,76 +79,6 @@ def iter_model_rows(model, column, include_root=False): yield index -@contextlib.contextmanager -def preserve_states(tree_view, - column=0, - role=None, - preserve_expanded=True, - preserve_selection=True, - expanded_role=QtCore.Qt.DisplayRole, - selection_role=QtCore.Qt.DisplayRole): - """Preserves row selection in QTreeView by column's data role. - This function is created to maintain the selection status of - the model items. When refresh is triggered the items which are expanded - will stay expanded and vise versa. - tree_view (QWidgets.QTreeView): the tree view nested in the application - column (int): the column to retrieve the data from - role (int): the role which dictates what will be returned - Returns: - None - """ - # When `role` is set then override both expanded and selection roles - if role: - expanded_role = role - selection_role = role - - model = tree_view.model() - selection_model = tree_view.selectionModel() - flags = selection_model.Select | selection_model.Rows - - expanded = set() - - if preserve_expanded: - for index in iter_model_rows( - model, column=column, include_root=False - ): - if tree_view.isExpanded(index): - value = index.data(expanded_role) - expanded.add(value) - - selected = None - - if preserve_selection: - selected_rows = selection_model.selectedRows() - if selected_rows: - selected = set(row.data(selection_role) for row in selected_rows) - - try: - yield - finally: - if expanded: - for index in iter_model_rows( - model, column=0, include_root=False - ): - value = index.data(expanded_role) - is_expanded = value in expanded - # skip if new index was created meanwhile - if is_expanded is None: - continue - tree_view.setExpanded(index, is_expanded) - - if selected: - # Go through all indices, select the ones with similar data - for index in iter_model_rows( - model, column=column, include_root=False - ): - value = index.data(selection_role) - state = value in selected - if state: - tree_view.scrollTo(index) # Ensure item is visible - selection_model.select(index, flags) - - @contextlib.contextmanager def preserve_expanded_rows(tree_view, column=0, role=None): """Preserves expanded row in QTreeView by column's data role. From 2b7115c4b7a38b13fd609312b0ab8834414523bc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 29 Oct 2021 19:45:17 +0200 Subject: [PATCH 25/27] replaced '_get_subset_name' with 'get_subset_name_with_asset_doc' --- openpype/lib/plugin_tools.py | 82 +++++++++--------------------------- 1 file changed, 21 insertions(+), 61 deletions(-) diff --git a/openpype/lib/plugin_tools.py b/openpype/lib/plugin_tools.py index 7d1ccf3826..aa9e0c9b57 100644 --- a/openpype/lib/plugin_tools.py +++ b/openpype/lib/plugin_tools.py @@ -28,17 +28,15 @@ class TaskNotSetError(KeyError): super(TaskNotSetError, self).__init__(msg) -def _get_subset_name( +def get_subset_name_with_asset_doc( family, variant, task_name, - asset_id, asset_doc, - project_name, - host_name, - default_template, - dynamic_data, - dbcon + project_name=None, + host_name=None, + default_template=None, + dynamic_data=None ): """Calculate subset name based on passed context and OpenPype settings. @@ -54,8 +52,6 @@ def _get_subset_name( family (str): Instance family. variant (str): In most of cases it is user input during creation. task_name (str): Task name on which context is instance created. - asset_id (ObjectId): Id of object. Is optional if `asset_doc` is - passed. asset_doc (dict): Queried asset document with it's tasks in data. Used to get task type. project_name (str): Name of project on which is instance created. @@ -84,25 +80,6 @@ def _get_subset_name( project_name = avalon.api.Session["AVALON_PROJECT"] - # Query asset document if was not passed - if asset_doc is None: - if dbcon is None: - from avalon.api import AvalonMongoDB - - dbcon = AvalonMongoDB() - dbcon.Session["AVALON_PROJECT"] = project_name - - dbcon.install() - - asset_doc = dbcon.find_one( - { - "type": "asset", - "_id": asset_id - }, - { - "data.tasks": True - } - ) or {} asset_tasks = asset_doc.get("data", {}).get("tasks") or {} task_info = asset_tasks.get(task_name) or {} task_type = task_info.get("type") @@ -144,34 +121,6 @@ def _get_subset_name( return template.format(**prepare_template_data(fill_pairs)) -def get_subset_name_with_asset_doc( - family, - variant, - task_name, - asset_doc, - project_name=None, - host_name=None, - default_template=None, - dynamic_data=None, - dbcon=None -): - """Calculate subset name using OpenPype settings. - - This variant of function expects already queried asset document. - """ - return _get_subset_name( - family, variant, - task_name, - None, - asset_doc, - project_name, - host_name, - default_template, - dynamic_data, - dbcon - ) - - def get_subset_name( family, variant, @@ -190,17 +139,28 @@ def get_subset_name( This is legacy function should be replaced with `get_subset_name_with_asset_doc` where asset document is expected. """ - return _get_subset_name( + if dbcon is None: + from avalon.api import AvalonMongoDB + + dbcon = AvalonMongoDB() + dbcon.Session["AVALON_PROJECT"] = project_name + + dbcon.install() + + asset_doc = dbcon.find_one( + {"_id": asset_id}, + {"data.tasks": True} + ) or {} + + return get_subset_name_with_asset_doc( family, variant, task_name, - asset_id, - None, + asset_doc, project_name, host_name, default_template, - dynamic_data, - dbcon + dynamic_data ) From 6410966ef51f3516d09b5b9a3be6f57259fa16da Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 31 Oct 2021 11:23:01 +0100 Subject: [PATCH 26/27] keep raw data as QtCore.QByteArray if already are raw --- openpype/tools/project_manager/project_manager/model.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 5b6ed78b50..b7ab9e40d0 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1456,7 +1456,11 @@ class HierarchyModel(QtCore.QAbstractItemModel): return raw_data = mime_data.data("application/copy_task") - encoded_data = QtCore.QByteArray.fromRawData(raw_data) + if isinstance(raw_data, QtCore.QByteArray): + # Raw data are already QByteArrat and we don't have to load them + encoded_data = raw_data + else: + encoded_data = QtCore.QByteArray.fromRawData(raw_data) stream = QtCore.QDataStream(encoded_data, QtCore.QIODevice.ReadOnly) text = stream.readQString() try: From 87cb2647357d48355de429a0095b0732bb4216ca Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 2 Nov 2021 11:36:23 +0100 Subject: [PATCH 27/27] added checks of templates in project anatomy --- openpype/lib/delivery.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/openpype/lib/delivery.py b/openpype/lib/delivery.py index 5735cbc99d..c89e2e7ae0 100644 --- a/openpype/lib/delivery.py +++ b/openpype/lib/delivery.py @@ -245,6 +245,27 @@ def process_sequence( report_items["Source file was not found"].append(msg) return report_items, 0 + delivery_templates = anatomy.templates.get("delivery") or {} + delivery_template = delivery_templates.get(template_name) + if delivery_template is None: + msg = ( + "Delivery template \"{}\" in anatomy of project \"{}\"" + " was not found" + ).format(template_name, anatomy.project_name) + report_items[""].append(msg) + return report_items, 0 + + # Check if 'frame' key is available in template which is required + # for sequence delivery + if "{frame" not in delivery_template: + msg = ( + "Delivery template \"{}\" in anatomy of project \"{}\"" + "does not contain '{{frame}}' key to fill. Delivery of sequence" + " can't be processed." + ).format(template_name, anatomy.project_name) + report_items[""].append(msg) + return report_items, 0 + dir_path, file_name = os.path.split(str(src_path)) context = repre["context"]