From 9c1e663cbcffc85f2a85769ca33c66a9eb71e18b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Jan 2023 14:56:47 +0000 Subject: [PATCH 001/119] Bump ua-parser-js from 0.7.31 to 0.7.33 in /website Bumps [ua-parser-js](https://github.com/faisalman/ua-parser-js) from 0.7.31 to 0.7.33. - [Release notes](https://github.com/faisalman/ua-parser-js/releases) - [Changelog](https://github.com/faisalman/ua-parser-js/blob/master/changelog.md) - [Commits](https://github.com/faisalman/ua-parser-js/compare/0.7.31...0.7.33) --- updated-dependencies: - dependency-name: ua-parser-js dependency-type: indirect ... Signed-off-by: dependabot[bot] --- website/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/yarn.lock b/website/yarn.lock index 9af21c7500..ad80bf6915 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -7180,9 +7180,9 @@ typedarray-to-buffer@^3.1.5: is-typedarray "^1.0.0" ua-parser-js@^0.7.30: - version "0.7.31" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6" - integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ== + version "0.7.33" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.33.tgz#1d04acb4ccef9293df6f70f2c3d22f3030d8b532" + integrity sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw== unherit@^1.0.4: version "1.1.3" From 6246bac7742aa18f852704588ba37d228f0a1413 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 28 Jan 2023 00:29:21 +0100 Subject: [PATCH 002/119] renamed 'reset_avalon_context' to 'reset_current_context' --- openpype/pipeline/create/context.py | 6 +++--- openpype/tools/publisher/control.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 9c468ae8fc..29bc32b658 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1138,7 +1138,7 @@ class CreateContext: self.reset_preparation() - self.reset_avalon_context() + self.reset_current_context() self.reset_plugins(discover_publish_plugins) self.reset_context_data() @@ -1185,8 +1185,8 @@ class CreateContext: self._collection_shared_data = None self.refresh_thumbnails() - def reset_avalon_context(self): - """Give ability to reset avalon context. + def reset_current_context(self): + """Refresh current context. Reset is based on optional host implementation of `get_current_context` function or using `legacy_io.Session`. diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 50a814de5c..c11d7c53d3 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1756,7 +1756,7 @@ class PublisherController(BasePublisherController): self._create_context.reset_preparation() # Reset avalon context - self._create_context.reset_avalon_context() + self._create_context.reset_current_context() self._asset_docs_cache.reset() From d1b41ebac0b7bbac4a1404ca0233d2a7d92e6230 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 28 Jan 2023 00:30:24 +0100 Subject: [PATCH 003/119] AvalonMongoDB is not needed for CreateContext or Controller --- openpype/pipeline/create/context.py | 38 ++++++++++++++--------------- openpype/tools/publisher/control.py | 5 ++-- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 29bc32b658..e421a76b6e 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1003,8 +1003,6 @@ class CreateContext: Args: host(ModuleType): Host implementation which handles implementation and global metadata. - dbcon(AvalonMongoDB): Connection to mongo with context (at least - project). headless(bool): Context is created out of UI (Current not used). reset(bool): Reset context on initialization. discover_publish_plugins(bool): Discover publish plugins during reset @@ -1012,16 +1010,8 @@ class CreateContext: """ def __init__( - self, host, dbcon=None, headless=False, reset=True, - discover_publish_plugins=True + self, host, headless=False, reset=True, discover_publish_plugins=True ): - # Create conncetion if is not passed - if dbcon is None: - session = session_data_from_environment(True) - dbcon = AvalonMongoDB(session) - dbcon.install() - - self.dbcon = dbcon self.host = host # Prepare attribute for logger (Created on demand in `log` property) @@ -1045,6 +1035,10 @@ class CreateContext: " Missing methods: {}" ).format(joined_methods)) + self._current_project_name = None + self._current_asset_name = None + self._current_task_name = None + self._host_is_valid = host_is_valid # Currently unused variable self.headless = headless @@ -1119,9 +1113,16 @@ class CreateContext: def host_name(self): return os.environ["AVALON_APP"] - @property - def project_name(self): - return self.dbcon.active_project() + def get_current_project_name(self): + return self._current_project_name + + def get_current_asset_name(self): + return self._current_asset_name + + def get_current_task_name(self): + return self._current_task_name + + project_name = property(get_current_project_name) @property def log(self): @@ -1210,12 +1211,9 @@ class CreateContext: if not task_name: task_name = legacy_io.Session.get("AVALON_TASK") - if project_name: - self.dbcon.Session["AVALON_PROJECT"] = project_name - if asset_name: - self.dbcon.Session["AVALON_ASSET"] = asset_name - if task_name: - self.dbcon.Session["AVALON_TASK"] = task_name + self._current_project_name = project_name + self._current_asset_name = asset_name + self._current_task_name = task_name def reset_plugins(self, discover_publish_plugins=True): """Reload plugins. diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index c11d7c53d3..83c2dd4b1c 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1589,20 +1589,19 @@ class PublisherController(BasePublisherController): Handle both creation and publishing parts. Args: - dbcon (AvalonMongoDB): Connection to mongo with context. headless (bool): Headless publishing. ATM not implemented or used. """ _log = None - def __init__(self, dbcon=None, headless=False): + def __init__(self, headless=False): super(PublisherController, self).__init__() self._host = registered_host() self._headless = headless self._create_context = CreateContext( - self._host, dbcon, headless=headless, reset=False + self._host, headless=headless, reset=False ) self._publish_plugins_proxy = None From 0a900f8ae1e4ac3b1ba48aca017be03a7f743e89 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 28 Jan 2023 00:30:59 +0100 Subject: [PATCH 004/119] use 'name' attribute of host implementation if is available --- openpype/pipeline/create/context.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index e421a76b6e..867809a4c1 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1111,6 +1111,8 @@ class CreateContext: @property def host_name(self): + if hasattr(self.host, "name"): + return self.host.name return os.environ["AVALON_APP"] def get_current_project_name(self): From 8678f4e2fa36bab16aa8033bf518dd0231fa885c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 28 Jan 2023 00:31:27 +0100 Subject: [PATCH 005/119] 'create' returns output from creator --- openpype/pipeline/create/context.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 867809a4c1..413580526e 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1429,6 +1429,7 @@ class CreateContext: failed = False add_traceback = False exc_info = None + result = None try: # Fake CreatorError (Could be maybe specific exception?) if creator is None: @@ -1436,7 +1437,7 @@ class CreateContext: "Creator {} was not found".format(identifier) ) - creator.create(*args, **kwargs) + result = creator.create(*args, **kwargs) except CreatorError: failed = True @@ -1458,6 +1459,7 @@ class CreateContext: identifier, label, exc_info, add_traceback ) ]) + return result def creator_removed_instance(self, instance): """When creator removes instance context should be acknowledged. From d09b7812616bfaee29c4089e2ece893ff6bd3faa Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 28 Jan 2023 00:32:53 +0100 Subject: [PATCH 006/119] implemented helper function 'create_with_context' to trigger standartized creation --- openpype/pipeline/create/context.py | 64 ++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 5 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 413580526e..655af1b8ed 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -8,17 +8,13 @@ import inspect from uuid import uuid4 from contextlib import contextmanager -from openpype.client import get_assets +from openpype.client import get_assets, get_asset_by_name from openpype.settings import ( get_system_settings, get_project_settings ) from openpype.host import IPublishHost from openpype.pipeline import legacy_io -from openpype.pipeline.mongodb import ( - AvalonMongoDB, - session_data_from_environment, -) from .creator_plugins import ( Creator, @@ -1461,6 +1457,64 @@ class CreateContext: ]) return result + def create_with_context( + self, + creator_identifier, + variant=None, + asset_doc=None, + task_name=None, + pre_create_data=None + ): + """Trigger create of plugins with standartized + + Args: + creator_identifier (str): + asset_doc (Dict[str, Any]): + task_name (str): Name of task to which is context related. + variant (str): Variant used for subset name. + pre_create_data (Dict[str, Any]): Pre-create attribute values. + + Returns: + Any: Output of triggered creator's 'create' method. + + Raises: + CreatorsCreateFailed: When creation fails. + """ + + if pre_create_data is None: + pre_create_data = {} + + project_name = self.project_name + if asset_doc is None: + asset_name = self.get_current_asset_name() + asset_doc = get_asset_by_name(project_name, asset_name) + task_name = self.get_current_task_name() + + creator = self.creators.get(creator_identifier) + family = None + subset_name = None + if creator is not None: + family = creator.family + subset_name = creator.get_subset_name( + variant, + task_name, + asset_doc, + project_name, + self.host_name + ) + instance_data = { + "asset": asset_doc["name"], + "task": task_name, + "variant": variant, + "family": family + } + return self.raw_create( + creator_identifier, + subset_name, + instance_data, + pre_create_data + ) + def creator_removed_instance(self, instance): """When creator removes instance context should be acknowledged. From 430fe6aed42d8c08f57c7b91dd2eb9185a3d1fde Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 28 Jan 2023 00:33:33 +0100 Subject: [PATCH 007/119] renamed 'create'->'raw_create' and 'create_with_context'->'create' --- openpype/pipeline/create/context.py | 9 ++++++--- openpype/tools/publisher/control.py | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 655af1b8ed..a9f8ae3ce1 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1407,8 +1407,8 @@ class CreateContext: with self.bulk_instances_collection(): self._bulk_instances_to_process.append(instance) - def create(self, identifier, *args, **kwargs): - """Wrapper for creators to trigger created. + def raw_create(self, identifier, *args, **kwargs): + """Wrapper for creators to trigger 'create' method. Different types of creators may expect different arguments thus the hints for args are blind. @@ -1417,6 +1417,9 @@ class CreateContext: identifier (str): Creator's identifier. *args (Tuple[Any]): Arguments for create method. **kwargs (Dict[Any, Any]): Keyword argument for create method. + + Raises: + CreatorsCreateFailed: When creation fails. """ error_message = "Failed to run Creator with identifier \"{}\". {}" @@ -1457,7 +1460,7 @@ class CreateContext: ]) return result - def create_with_context( + def create( self, creator_identifier, variant=None, diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 83c2dd4b1c..670c22a43e 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -2017,7 +2017,7 @@ class PublisherController(BasePublisherController): success = True try: - self._create_context.create( + self._create_context.raw_create( creator_identifier, subset_name, instance_data, options ) except CreatorsOperationFailed as exc: From 498c8564f71c4d85ad88a101d6de7ae11357bb7d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 28 Jan 2023 00:40:08 +0100 Subject: [PATCH 008/119] swapped argments order in docstring --- openpype/pipeline/create/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index a9f8ae3ce1..702731f8b2 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1472,9 +1472,9 @@ class CreateContext: Args: creator_identifier (str): + variant (str): Variant used for subset name. asset_doc (Dict[str, Any]): task_name (str): Name of task to which is context related. - variant (str): Variant used for subset name. pre_create_data (Dict[str, Any]): Pre-create attribute values. Returns: From 40712089d94ce22a5d34981584dc7db8beed9554 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 30 Jan 2023 10:48:57 +0100 Subject: [PATCH 009/119] Validate creator and asset doc --- openpype/pipeline/create/context.py | 33 +++++++++++++++++------------ 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 702731f8b2..35024b5af8 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1484,27 +1484,32 @@ class CreateContext: CreatorsCreateFailed: When creation fails. """ - if pre_create_data is None: - pre_create_data = {} + creator = self.creators.get(creator_identifier) + if creator is None: + raise CreatorError( + "Creator {} was not found".format(creator_identifier) + ) project_name = self.project_name if asset_doc is None: asset_name = self.get_current_asset_name() asset_doc = get_asset_by_name(project_name, asset_name) task_name = self.get_current_task_name() + if asset_doc is None: + raise CreatorError( + "Asset with name {} was not found".format(asset_name) + ) - creator = self.creators.get(creator_identifier) - family = None - subset_name = None - if creator is not None: - family = creator.family - subset_name = creator.get_subset_name( - variant, - task_name, - asset_doc, - project_name, - self.host_name - ) + if pre_create_data is None: + pre_create_data = {} + + subset_name = creator.get_subset_name( + variant, + task_name, + asset_doc, + project_name, + self.host_name + ) instance_data = { "asset": asset_doc["name"], "task": task_name, From 75bffb4daeacc8a31dc584edf3b76b3989a0607d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 30 Jan 2023 10:49:17 +0100 Subject: [PATCH 010/119] removed unnecessary family from instance data --- openpype/pipeline/create/context.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 35024b5af8..dbbde9218f 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1513,8 +1513,7 @@ class CreateContext: instance_data = { "asset": asset_doc["name"], "task": task_name, - "variant": variant, - "family": family + "variant": variant } return self.raw_create( creator_identifier, From daa961d24976a4b9a1d5f51320017fa346bbfc84 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 30 Jan 2023 10:49:27 +0100 Subject: [PATCH 011/119] variant is required argument --- openpype/pipeline/create/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index dbbde9218f..b10bbc17de 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1463,7 +1463,7 @@ class CreateContext: def create( self, creator_identifier, - variant=None, + variant, asset_doc=None, task_name=None, pre_create_data=None From 4d990e6f87964cb4f5fb2c61cebfcaea47ac3151 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 30 Jan 2023 11:03:51 +0100 Subject: [PATCH 012/119] Updated docstrings --- openpype/pipeline/create/context.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index b10bbc17de..190d542724 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1191,7 +1191,15 @@ class CreateContext: function or using `legacy_io.Session`. Some hosts have ability to change context file without using workfiles - tool but that change is not propagated to + tool but that change is not propagated to 'legacy_io.Session' + nor 'os.environ'. + + Todos: + UI: Current context should be also checked on save - compare + initial values vs. current values. + Related to UI checks: Current workfile can be also considered + as current context information as that's where the metadata + are stored. We should store the workfile (if is available) too. """ project_name = asset_name = task_name = None @@ -1468,12 +1476,19 @@ class CreateContext: task_name=None, pre_create_data=None ): - """Trigger create of plugins with standartized + """Trigger create of plugins with standartized arguments. + + Arguments 'asset_doc' and 'task_name' use current context as default + values. If only 'task_name' is provided it will be overriden by + task name from current context. If 'task_name' is not provided + when 'asset_doc' is, it is considered that task name is not specified, + which can lead to error if subset name template requires task name. Args: - creator_identifier (str): + creator_identifier (str): Identifier of creator plugin. variant (str): Variant used for subset name. - asset_doc (Dict[str, Any]): + asset_doc (Dict[str, Any]): Asset document which define context of + creation (possible context of created instance/s). task_name (str): Name of task to which is context related. pre_create_data (Dict[str, Any]): Pre-create attribute values. @@ -1481,6 +1496,7 @@ class CreateContext: Any: Output of triggered creator's 'create' method. Raises: + CreatorError: If creator was not found or asset is empty. CreatorsCreateFailed: When creation fails. """ From c8fb00c9c81c9a60f0436dbcb43b546060cf6664 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 30 Jan 2023 12:43:40 +0100 Subject: [PATCH 013/119] global: expanding staging dir maker abstraction so it supports `OPENPYPE_TEMP_DIR` with anatomy formatting keys --- openpype/pipeline/publish/lib.py | 55 ++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index c76671fa39..5591acf57d 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -10,7 +10,11 @@ import six import pyblish.plugin import pyblish.api -from openpype.lib import Logger, filter_profiles +from openpype.lib import ( + Logger, + filter_profiles, + StringTemplate +) from openpype.settings import ( get_project_settings, get_system_settings, @@ -623,12 +627,51 @@ def get_instance_staging_dir(instance): Returns: str: Path to staging dir of instance. """ + staging_dir = instance.data.get('stagingDir', None) + openpype_temp_dir = os.getenv("OPENPYPE_TEMP_DIR") - staging_dir = instance.data.get("stagingDir") if not staging_dir: - staging_dir = os.path.normpath( - tempfile.mkdtemp(prefix="pyblish_tmp_") - ) - instance.data["stagingDir"] = staging_dir + custom_temp_dir = None + if openpype_temp_dir: + if "{" in openpype_temp_dir: + anatomy = instance.context.data["anatomy"] + # get anatomy formating data + # so template formating is supported + anatomy_data = copy.deepcopy(instance.context.data["anatomyData"]) + anatomy_data["root"] = anatomy.roots + """Template path formating is supporting: + - optional key formating + - available tokens: + - root[work | ] + - project[name | code] + - asset + - hierarchy + - task + - username + - app + """ + custom_temp_dir = StringTemplate.format_template( + openpype_temp_dir, anatomy_data) + custom_temp_dir = os.path.normpath(custom_temp_dir) + # create the dir in case it doesnt exists + os.makedirs(os.path.dirname(custom_temp_dir)) + elif os.path.exists(openpype_temp_dir): + custom_temp_dir = openpype_temp_dir + + + if custom_temp_dir: + staging_dir = os.path.normpath( + tempfile.mkdtemp( + prefix="pyblish_tmp_", + dir=custom_temp_dir + ) + ) + else: + staging_dir = os.path.normpath( + tempfile.mkdtemp(prefix="pyblish_tmp_") + ) + instance.data['stagingDir'] = staging_dir + + instance.context.data["cleanupFullPaths"].append(staging_dir) return staging_dir From ef86f1451542a1c9a85e9472da9ced35f6b92d95 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 30 Jan 2023 12:51:13 +0100 Subject: [PATCH 014/119] global: update docstrings at `get_instance_staging_dir` --- openpype/pipeline/publish/lib.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 5591acf57d..cb01d4633e 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -613,8 +613,21 @@ def context_plugin_should_run(plugin, context): def get_instance_staging_dir(instance): """Unified way how staging dir is stored and created on instances. - First check if 'stagingDir' is already set in instance data. If there is - not create new in tempdir. + First check if 'stagingDir' is already set in instance data. + In case there already is new tempdir will not be created. + + It also supports `OPENPYPE_TEMP_DIR`, so studio can define own temp shared + repository per project or even per more granular context. Template formating + is supported also with optional keys. Folder is created in case it doesnt exists. + + Available anatomy formating keys: + - root[work | ] + - project[name | code] + - asset + - hierarchy + - task + - username + - app Note: Staging dir does not have to be necessarily in tempdir so be carefull @@ -641,7 +654,7 @@ def get_instance_staging_dir(instance): anatomy_data["root"] = anatomy.roots """Template path formating is supporting: - optional key formating - - available tokens: + - available keys: - root[work | ] - project[name | code] - asset From 43399a08c82393c8522b3b4e7f59f16be354dbe6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 30 Jan 2023 12:52:11 +0100 Subject: [PATCH 015/119] flame: removing class override for staging dir creation it is already available in more expanded feature at parent class --- .../publish/extract_subset_resources.py | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py index d5294d61c2..c6148162a6 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -548,30 +548,3 @@ class ExtractSubsetResources(publish.Extractor): "Path `{}` is containing more that one clip".format(path) ) return clips[0] - - def staging_dir(self, instance): - """Provide a temporary directory in which to store extracted files - - Upon calling this method the staging directory is stored inside - the instance.data['stagingDir'] - """ - staging_dir = instance.data.get('stagingDir', None) - openpype_temp_dir = os.getenv("OPENPYPE_TEMP_DIR") - - if not staging_dir: - if openpype_temp_dir and os.path.exists(openpype_temp_dir): - staging_dir = os.path.normpath( - tempfile.mkdtemp( - prefix="pyblish_tmp_", - dir=openpype_temp_dir - ) - ) - else: - staging_dir = os.path.normpath( - tempfile.mkdtemp(prefix="pyblish_tmp_") - ) - instance.data['stagingDir'] = staging_dir - - instance.context.data["cleanupFullPaths"].append(staging_dir) - - return staging_dir From 4dc9fadc424222a3f99444aaea3df8a8fd701a62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Mon, 30 Jan 2023 13:56:56 +0100 Subject: [PATCH 016/119] Update openpype/pipeline/publish/lib.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/pipeline/publish/lib.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index cb01d4633e..33f23ddb97 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -640,7 +640,9 @@ def get_instance_staging_dir(instance): Returns: str: Path to staging dir of instance. """ - staging_dir = instance.data.get('stagingDir', None) + staging_dir = instance.data.get('stagingDir') + if staging_dir: + return staging_dir openpype_temp_dir = os.getenv("OPENPYPE_TEMP_DIR") if not staging_dir: From a9cc08120d7f6c47b65f22b64081c73d4d5e1804 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 30 Jan 2023 13:59:39 +0100 Subject: [PATCH 017/119] global: refactor code for better readibility --- openpype/pipeline/publish/lib.py | 94 +++++++++++++++++--------------- 1 file changed, 50 insertions(+), 44 deletions(-) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 33f23ddb97..cc4304cebd 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -616,11 +616,12 @@ def get_instance_staging_dir(instance): First check if 'stagingDir' is already set in instance data. In case there already is new tempdir will not be created. - It also supports `OPENPYPE_TEMP_DIR`, so studio can define own temp shared - repository per project or even per more granular context. Template formating - is supported also with optional keys. Folder is created in case it doesnt exists. + It also supports `OPENPYPE_TEMP_DIR`, so studio can define own temp + shared repository per project or even per more granular context. + Template formating is supported also with optional keys. Folder is + created in case it doesnt exists. - Available anatomy formating keys: + Available anatomy formatting keys: - root[work | ] - project[name | code] - asset @@ -643,50 +644,55 @@ def get_instance_staging_dir(instance): staging_dir = instance.data.get('stagingDir') if staging_dir: return staging_dir + openpype_temp_dir = os.getenv("OPENPYPE_TEMP_DIR") - - if not staging_dir: - custom_temp_dir = None - if openpype_temp_dir: - if "{" in openpype_temp_dir: - anatomy = instance.context.data["anatomy"] - # get anatomy formating data - # so template formating is supported - anatomy_data = copy.deepcopy(instance.context.data["anatomyData"]) - anatomy_data["root"] = anatomy.roots - """Template path formating is supporting: - - optional key formating - - available keys: - - root[work | ] - - project[name | code] - - asset - - hierarchy - - task - - username - - app - """ - custom_temp_dir = StringTemplate.format_template( - openpype_temp_dir, anatomy_data) - custom_temp_dir = os.path.normpath(custom_temp_dir) - # create the dir in case it doesnt exists - os.makedirs(os.path.dirname(custom_temp_dir)) - elif os.path.exists(openpype_temp_dir): - custom_temp_dir = openpype_temp_dir - - - if custom_temp_dir: - staging_dir = os.path.normpath( - tempfile.mkdtemp( - prefix="pyblish_tmp_", - dir=custom_temp_dir - ) + custom_temp_dir = None + if openpype_temp_dir: + if "{" in openpype_temp_dir: + custom_temp_dir = _formated_staging_dir( + instance, openpype_temp_dir ) - else: - staging_dir = os.path.normpath( - tempfile.mkdtemp(prefix="pyblish_tmp_") + elif os.path.exists(openpype_temp_dir): + custom_temp_dir = openpype_temp_dir + + + if custom_temp_dir: + staging_dir = os.path.normpath( + tempfile.mkdtemp( + prefix="pyblish_tmp_", + dir=custom_temp_dir ) - instance.data['stagingDir'] = staging_dir + ) + else: + staging_dir = os.path.normpath( + tempfile.mkdtemp(prefix="pyblish_tmp_") + ) + instance.data['stagingDir'] = staging_dir instance.context.data["cleanupFullPaths"].append(staging_dir) return staging_dir + + +def _formated_staging_dir(instance, openpype_temp_dir): + anatomy = instance.context.data["anatomy"] + # get anatomy formating data + # so template formating is supported + anatomy_data = copy.deepcopy(instance.context.data["anatomyData"]) + anatomy_data["root"] = anatomy.roots + """Template path formatting is supporting: + - optional key formating + - available keys: + - root[work | ] + - project[name | code] + - asset + - hierarchy + - task + - username + - app + """ + result = StringTemplate.format_template(openpype_temp_dir, anatomy_data) + result = os.path.normpath(result) + # create the dir in case it doesnt exists + os.makedirs(os.path.dirname(result)) + return result From e10859d322d675a6e0945e1e9958480c9ee33ca1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 1 Feb 2023 15:54:18 +0100 Subject: [PATCH 018/119] pr comments --- .../publish/extract_subset_resources.py | 3 ++ openpype/pipeline/publish/lib.py | 50 +++++++++++-------- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py index c6148162a6..5082217db0 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -143,6 +143,9 @@ class ExtractSubsetResources(publish.Extractor): # create staging dir path staging_dir = self.staging_dir(instance) + # append staging dir for later cleanup + instance.context.data["cleanupFullPaths"].append(staging_dir) + # add default preset type for thumbnail and reviewable video # update them with settings and override in case the same # are found in there diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index cc4304cebd..a32b076775 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -616,7 +616,7 @@ def get_instance_staging_dir(instance): First check if 'stagingDir' is already set in instance data. In case there already is new tempdir will not be created. - It also supports `OPENPYPE_TEMP_DIR`, so studio can define own temp + It also supports `OPENPYPE_TMPDIR`, so studio can define own temp shared repository per project or even per more granular context. Template formating is supported also with optional keys. Folder is created in case it doesnt exists. @@ -645,17 +645,16 @@ def get_instance_staging_dir(instance): if staging_dir: return staging_dir - openpype_temp_dir = os.getenv("OPENPYPE_TEMP_DIR") + openpype_temp_dir = os.getenv("OPENPYPE_TMPDIR") custom_temp_dir = None if openpype_temp_dir: if "{" in openpype_temp_dir: - custom_temp_dir = _formated_staging_dir( + custom_temp_dir = _format_staging_dir( instance, openpype_temp_dir ) elif os.path.exists(openpype_temp_dir): custom_temp_dir = openpype_temp_dir - if custom_temp_dir: staging_dir = os.path.normpath( tempfile.mkdtemp( @@ -669,30 +668,39 @@ def get_instance_staging_dir(instance): ) instance.data['stagingDir'] = staging_dir - instance.context.data["cleanupFullPaths"].append(staging_dir) - return staging_dir -def _formated_staging_dir(instance, openpype_temp_dir): +def _format_staging_dir(instance, openpype_temp_dir): + """ Formating template + + Template path formatting is supporting: + - optional key formating + - available keys: + - root[work | ] + - project[name | code] + - asset + - hierarchy + - task + - username + - app + + Args: + instance (pyblish.Instance): instance object + openpype_temp_dir (str): path string + + Returns: + str: formated path + """ anatomy = instance.context.data["anatomy"] # get anatomy formating data # so template formating is supported anatomy_data = copy.deepcopy(instance.context.data["anatomyData"]) anatomy_data["root"] = anatomy.roots - """Template path formatting is supporting: - - optional key formating - - available keys: - - root[work | ] - - project[name | code] - - asset - - hierarchy - - task - - username - - app - """ - result = StringTemplate.format_template(openpype_temp_dir, anatomy_data) - result = os.path.normpath(result) - # create the dir in case it doesnt exists + + result = StringTemplate.format_template( + openpype_temp_dir, anatomy_data).normalized() + + # create the dir in case it doesnt exists os.makedirs(os.path.dirname(result)) return result From b2ed65c17ad147e44fab334534f8fd1ad837d53c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 1 Feb 2023 16:06:01 +0100 Subject: [PATCH 019/119] pr comments --- openpype/pipeline/publish/lib.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index a32b076775..b3d273781e 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -649,12 +649,21 @@ def get_instance_staging_dir(instance): custom_temp_dir = None if openpype_temp_dir: if "{" in openpype_temp_dir: + # path is anatomy template custom_temp_dir = _format_staging_dir( instance, openpype_temp_dir ) - elif os.path.exists(openpype_temp_dir): + else: + # path is absolute custom_temp_dir = openpype_temp_dir + if not os.path.exists(custom_temp_dir): + try: + # create it if it doesnt exists + os.makedirs(custom_temp_dir) + except IOError as error: + raise IOError("Path couldn't be created: {}".format(error)) + if custom_temp_dir: staging_dir = os.path.normpath( tempfile.mkdtemp( From 76ab705e0c33aa18b009725896d1375b5a9432a5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 1 Feb 2023 16:25:52 +0100 Subject: [PATCH 020/119] added documenation --- website/docs/admin_settings_system.md | 38 ++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/website/docs/admin_settings_system.md b/website/docs/admin_settings_system.md index 8aeb281109..39b58e6f81 100644 --- a/website/docs/admin_settings_system.md +++ b/website/docs/admin_settings_system.md @@ -13,18 +13,44 @@ Settings applicable to the full studio. ![general_settings](assets/settings/settings_system_general.png) -**`Studio Name`** - Full name of the studio (can be used as variable on some places) +### Studio Name + - Full name of the studio (can be used as variable on some places) -**`Studio Code`** - Studio acronym or a short code (can be used as variable on some places) +### Studio Code + - Studio acronym or a short code (can be used as variable on some places) -**`Admin Password`** - After setting admin password, normal user won't have access to OpenPype settings +### Admin Password + - After setting admin password, normal user won't have access to OpenPype settings and Project Manager GUI. Please keep in mind that this is a studio wide password and it is meant purely as a simple barrier to prevent artists from accidental setting changes. -**`Environment`** - Globally applied environment variables that will be appended to any OpenPype process in the studio. +### Environment + - Globally applied environment variables that will be appended to any OpenPype process in the studio. + - OpenPype is using some keys to configure some tools. Here are some: -**`Disk mapping`** - Platform dependent configuration for mapping of virtual disk(s) on an artist's OpenPype machines before OP starts up. -Uses `subst` command, if configured volume character in `Destination` field already exists, no re-mapping is done for that character(volume). +#### OPENPYPE_TMPDIR: + - Custom staging dir directory + - Supports anatomy keys formating. ex `{root[work]}/{project[name]}/temp` + - supported formating keys: + - root[work] + - project[name | code] + - asset + - hierarchy + - task + - username + - app + +#### OPENPYPE_DEBUG + - setting logger to debug mode + - example value: "1" (to activate) + +#### OPENPYPE_LOG_LEVEL + - stringified numeric value of log level. [Here for more info](https://docs.python.org/3/library/logging.html#logging-levels) + - example value: "10" + +### Disk mapping +- Platform dependent configuration for mapping of virtual disk(s) on an artist's OpenPype machines before OP starts up. +- Uses `subst` command, if configured volume character in `Destination` field already exists, no re-mapping is done for that character(volume). ### FFmpeg and OpenImageIO tools We bundle FFmpeg tools for all platforms and OpenImageIO tools for Windows and Linux. By default, bundled tools are used, but it is possible to set environment variables `OPENPYPE_FFMPEG_PATHS` and `OPENPYPE_OIIO_PATHS` in system settings environments to look for them in different directory. From b00aab1eed7bf92347b64ed50e44340476b671a6 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 2 Feb 2023 23:35:22 +0000 Subject: [PATCH 021/119] Fix rounding --- openpype/hosts/maya/api/lib.py | 149 +++++++++++++----- .../plugins/publish/validate_maya_units.py | 11 +- 2 files changed, 115 insertions(+), 45 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index e5fa883c99..887c04d257 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -1969,8 +1969,6 @@ def get_id_from_sibling(node, history_only=True): return first_id - -# Project settings def set_scene_fps(fps, update=True): """Set FPS from project configuration @@ -1983,28 +1981,21 @@ def set_scene_fps(fps, update=True): """ - fps_mapping = {'15': 'game', - '24': 'film', - '25': 'pal', - '30': 'ntsc', - '48': 'show', - '50': 'palf', - '60': 'ntscf', - '23.98': '23.976fps', - '23.976': '23.976fps', - '29.97': '29.97fps', - '47.952': '47.952fps', - '47.95': '47.952fps', - '59.94': '59.94fps', - '44100': '44100fps', - '48000': '48000fps'} - - # pull from mapping - # this should convert float string to float and int to int - # so 25.0 is converted to 25, but 23.98 will be still float. - dec, ipart = math.modf(fps) - if dec == 0.0: - fps = int(ipart) + fps_mapping = { + '15': 'game', + '24': 'film', + '25': 'pal', + '30': 'ntsc', + '48': 'show', + '50': 'palf', + '60': 'ntscf', + '23.976023976023978': '23.976fps', + '29.97002997002997': '29.97fps', + '47.952047952047955': '47.952fps', + '59.94005994005994': '59.94fps', + '44100': '44100fps', + '48000': '48000fps' + } unit = fps_mapping.get(str(fps), None) if unit is None: @@ -2124,7 +2115,9 @@ def set_context_settings(): asset_data = asset_doc.get("data", {}) # Set project fps - fps = asset_data.get("fps", project_data.get("fps", 25)) + fps = convert_to_maya_fps( + asset_data.get("fps", project_data.get("fps", 25)) + ) legacy_io.Session["AVALON_FPS"] = str(fps) set_scene_fps(fps) @@ -2146,15 +2139,12 @@ def validate_fps(): """ - fps = get_current_project_asset(fields=["data.fps"])["data"]["fps"] - # TODO(antirotor): This is hack as for framerates having multiple - # decimal places. FTrack is ceiling decimal values on - # fps to two decimal places but Maya 2019+ is reporting those fps - # with much higher resolution. As we currently cannot fix Ftrack - # rounding, we have to round those numbers coming from Maya. - current_fps = float_round(mel.eval('currentTimeUnitToFPS()'), 2) + expected_fps = convert_to_maya_fps( + get_current_project_asset(fields=["data.fps"])["data"]["fps"] + ) + current_fps = mel.eval('currentTimeUnitToFPS()') - fps_match = current_fps == fps + fps_match = current_fps == expected_fps if not fps_match and not IS_HEADLESS: from openpype.widgets import popup @@ -2163,14 +2153,19 @@ def validate_fps(): dialog = popup.PopupUpdateKeys(parent=parent) dialog.setModal(True) dialog.setWindowTitle("Maya scene does not match project FPS") - dialog.setMessage("Scene %i FPS does not match project %i FPS" % - (current_fps, fps)) + dialog.setMessage( + "Scene {} FPS does not match project {} FPS".format( + current_fps, expected_fps + ) + ) dialog.setButtonText("Fix") # Set new text for button (add optional argument for the popup?) toggle = dialog.widgets["toggle"] update = toggle.isChecked() - dialog.on_clicked_state.connect(lambda: set_scene_fps(fps, update)) + dialog.on_clicked_state.connect( + lambda: set_scene_fps(expected_fps, update) + ) dialog.show() @@ -3353,3 +3348,85 @@ def iter_visible_nodes_in_range(nodes, start, end): def get_attribute_input(attr): connections = cmds.listConnections(attr, plugs=True, destination=False) return connections[0] if connections else None + + +def convert_to_maya_fps(fps): + """Convert any fps to supported Maya framerates.""" + float_framerates = [ + 23.976023976023978, + # WTF is 29.97 df vs fps? + 29.97002997002997, + 47.952047952047955, + 59.94005994005994 + ] + # 44100 fps evaluates as 41000.0. Why? Omitting for now. + int_framerates = [ + 2, + 3, + 4, + 5, + 6, + 8, + 10, + 12, + 15, + 16, + 20, + 24, + 25, + 30, + 40, + 48, + 50, + 60, + 75, + 80, + 90, + 100, + 120, + 125, + 150, + 200, + 240, + 250, + 300, + 375, + 400, + 500, + 600, + 750, + 1200, + 1500, + 2000, + 3000, + 6000, + 48000 + ] + + # If input fps is a whole number we'll return. + if float(fps).is_integer(): + # Validate fps is part of Maya's fps selection. + if fps not in int_framerates: + raise ValueError( + "Framerate \"{}\" is not supported in Maya".format(fps) + ) + return fps + else: + # Differences to supported float frame rates. + differences = [] + for i in float_framerates: + differences.append(abs(i - fps)) + + # Validate difference does not stray too far from supported framerates. + min_difference = min(differences) + min_index = differences.index(min_difference) + supported_framerate = float_framerates[min_index] + if round(min_difference) != 0: + raise ValueError( + "Framerate \"{}\" strays too far from any supported framerate" + " in Maya. Closest supported framerate is \"{}\"".format( + fps, supported_framerate + ) + ) + + return supported_framerate diff --git a/openpype/hosts/maya/plugins/publish/validate_maya_units.py b/openpype/hosts/maya/plugins/publish/validate_maya_units.py index e6fabb1712..ad256b6a72 100644 --- a/openpype/hosts/maya/plugins/publish/validate_maya_units.py +++ b/openpype/hosts/maya/plugins/publish/validate_maya_units.py @@ -33,18 +33,11 @@ class ValidateMayaUnits(pyblish.api.ContextPlugin): linearunits = context.data.get('linearUnits') angularunits = context.data.get('angularUnits') - # TODO(antirotor): This is hack as for framerates having multiple - # decimal places. FTrack is ceiling decimal values on - # fps to two decimal places but Maya 2019+ is reporting those fps - # with much higher resolution. As we currently cannot fix Ftrack - # rounding, we have to round those numbers coming from Maya. - # NOTE: this must be revisited yet again as it seems that Ftrack is - # now flooring the value? - fps = mayalib.float_round(context.data.get('fps'), 2, ceil) + fps = context.data.get('fps') # TODO repace query with using 'context.data["assetEntity"]' asset_doc = get_current_project_asset() - asset_fps = asset_doc["data"]["fps"] + asset_fps = mayalib.convert_to_maya_fps(asset_doc["data"]["fps"]) self.log.info('Units (linear): {0}'.format(linearunits)) self.log.info('Units (angular): {0}'.format(angularunits)) From 9b6bd7954d99640db5391cce39e87d02dc0e557a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 6 Feb 2023 15:44:34 +0000 Subject: [PATCH 022/119] Working AssStandinLoader --- openpype/hosts/maya/plugins/load/load_ass.py | 119 +++++++++---------- 1 file changed, 55 insertions(+), 64 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_ass.py b/openpype/hosts/maya/plugins/load/load_ass.py index 5db6fc3dfa..6317c0a7ce 100644 --- a/openpype/hosts/maya/plugins/load/load_ass.py +++ b/openpype/hosts/maya/plugins/load/load_ass.py @@ -1,6 +1,9 @@ import os import clique +import maya.cmds as cmds +import mtoa.ui.arnoldmenu + from openpype.settings import get_project_settings from openpype.pipeline import ( load, @@ -15,6 +18,15 @@ from openpype.hosts.maya.api.lib import ( from openpype.hosts.maya.api.pipeline import containerise +def is_sequence(files): + sequence = False + collections, remainder = clique.assemble(files) + if collections: + sequence = True + + return sequence + + class AssProxyLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): """Load Arnold Proxy as reference""" @@ -27,16 +39,12 @@ class AssProxyLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): color = "orange" def process_reference(self, context, name, namespace, options): - - import maya.cmds as cmds - import pymel.core as pm - version = context['version'] version_data = version.get("data", {}) self.log.info("version_data: {}\n".format(version_data)) - frameStart = version_data.get("frameStart", None) + frame_start = version_data.get("frame_start", None) try: family = context["representation"]["context"]["family"] @@ -49,7 +57,7 @@ class AssProxyLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): path = self.fname proxyPath_base = os.path.splitext(path)[0] - if frameStart is not None: + if frame_start is not None: proxyPath_base = os.path.splitext(proxyPath_base)[0] publish_folder = os.path.split(path)[0] @@ -63,11 +71,13 @@ class AssProxyLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): path = os.path.join(publish_folder, filename) - proxyPath = proxyPath_base + ".ma" + proxyPath = proxyPath_base + ".ass" project_name = context["project"]["name"] - file_url = self.prepare_root_value(proxyPath, - project_name) + file_url = self.prepare_root_value( + proxyPath, project_name + ) + self.log.info(file_url) nodes = cmds.file(file_url, namespace=namespace, @@ -80,7 +90,7 @@ class AssProxyLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): translate=True, scale=True) # Set attributes - proxyShape = pm.ls(nodes, type="mesh")[0] + proxyShape = cmds.ls(nodes, type="mesh")[0] proxyShape.aiTranslator.set('procedural') proxyShape.dso.set(path) @@ -92,10 +102,11 @@ class AssProxyLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): c = colors.get(family) if c is not None: cmds.setAttr(groupName + ".useOutlinerColor", 1) - cmds.setAttr(groupName + ".outlinerColor", - (float(c[0])/255), - (float(c[1])/255), - (float(c[2])/255) + cmds.setAttr( + groupName + ".outlinerColor", + (float(c[0]) / 255), + (float(c[1]) / 255), + (float(c[2]) / 255) ) self[:] = nodes @@ -106,18 +117,11 @@ class AssProxyLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): self.update(container, representation) def update(self, container, representation): - from maya import cmds - import pymel.core as pm - node = container["objectName"] representation["context"].pop("frame", None) path = get_representation_path(representation) - print(path) - # path = self.fname - print(self.fname) proxyPath = os.path.splitext(path)[0] + ".ma" - print(proxyPath) # Get reference node from container members members = cmds.sets(node, query=True, nodesOnly=True) @@ -186,18 +190,11 @@ class AssStandinLoader(load.LoaderPlugin): color = "orange" def load(self, context, name, namespace, options): - - import maya.cmds as cmds - import mtoa.ui.arnoldmenu - import pymel.core as pm - version = context['version'] version_data = version.get("data", {}) self.log.info("version_data: {}\n".format(version_data)) - frameStart = version_data.get("frameStart", None) - asset = context['asset']['name'] namespace = namespace or unique_namespace( asset + "_", @@ -205,36 +202,34 @@ class AssStandinLoader(load.LoaderPlugin): suffix="_", ) - # cmds.loadPlugin("gpuCache", quiet=True) - # Root group label = "{}:{}".format(namespace, name) - root = pm.group(name=label, empty=True) + root = cmds.group(name=label, empty=True) settings = get_project_settings(os.environ['AVALON_PROJECT']) colors = settings['maya']['load']['colors'] - c = colors.get('ass') - if c is not None: - cmds.setAttr(root + ".useOutlinerColor", 1) - cmds.setAttr(root + ".outlinerColor", - c[0], c[1], c[2]) + color = colors.get('ass') + if color is not None: + cmds.setAttr(root + ".useOutlinerColor", True) + cmds.setAttr( + root + ".outlinerColor", color[0], color[1], color[2] + ) # Create transform with shape transform_name = label + "_ASS" - # transform = pm.createNode("transform", name=transform_name, - # parent=root) - standinShape = pm.PyNode(mtoa.ui.arnoldmenu.createStandIn()) - standin = standinShape.getParent() - standin.rename(transform_name) + standinShape = mtoa.ui.arnoldmenu.createStandIn() + standin = cmds.listRelatives(standinShape, parent=True)[0] + standin = cmds.rename(standin, transform_name) + standinShape = cmds.listRelatives(standin, shapes=True)[0] - pm.parent(standin, root) + cmds.parent(standin, root) # Set the standin filepath - standinShape.dso.set(self.fname) - if frameStart is not None: - standinShape.useFrameExtension.set(1) + cmds.setAttr(standinShape + ".dso", self.fname, type="string") + sequence = is_sequence(os.listdir(os.path.dirname(self.fname))) + cmds.setAttr(standinShape + ".useFrameExtension", sequence) nodes = [root, standin] self[:] = nodes @@ -247,31 +242,27 @@ class AssStandinLoader(load.LoaderPlugin): loader=self.__class__.__name__) def update(self, container, representation): - - import pymel.core as pm - - path = get_representation_path(representation) - - files_in_path = os.listdir(os.path.split(path)[0]) - sequence = 0 - collections, remainder = clique.assemble(files_in_path) - if collections: - sequence = 1 - # Update the standin standins = list() - members = pm.sets(container['objectName'], query=True) + members = cmds.sets(container['objectName'], query=True) for member in members: - shape = member.getShape() - if (shape and shape.type() == "aiStandIn"): - standins.append(shape) + shapes = cmds.listRelatives(member, shapes=True) + if not shapes: + continue + if cmds.nodeType(shapes[0]) == "aiStandIn": + standins.append(shapes[0]) + path = get_representation_path(representation) + sequence = is_sequence(os.listdir(os.path.dirname(path))) for standin in standins: - standin.dso.set(path) - standin.useFrameExtension.set(sequence) + cmds.setAttr(standin + ".dso", path, type="string") + cmds.setAttr(standin + ".useFrameExtension", sequence) - container = pm.PyNode(container["objectName"]) - container.representation.set(str(representation["_id"])) + cmds.setAttr( + container["objectName"] + ".representation", + str(representation["_id"]), + type="string" + ) def switch(self, container, representation): self.update(container, representation) From 23987420a375c199b095b295282728e913bde75a Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Mon, 6 Feb 2023 19:03:06 +0100 Subject: [PATCH 023/119] update asset info of imported sets --- .../maya/api/workfile_template_builder.py | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index ef043ed0f4..56a53c070c 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -2,7 +2,7 @@ import json from maya import cmds -from openpype.pipeline import registered_host +from openpype.pipeline import registered_host, legacy_io from openpype.pipeline.workfile.workfile_template_builder import ( TemplateAlreadyImported, AbstractTemplateBuilder, @@ -41,10 +41,26 @@ class MayaTemplateBuilder(AbstractTemplateBuilder): )) cmds.sets(name=PLACEHOLDER_SET, empty=True) - cmds.file(path, i=True, returnNewNodes=True) + new_nodes = cmds.file(path, i=True, returnNewNodes=True) cmds.setAttr(PLACEHOLDER_SET + ".hiddenInOutliner", True) + imported_sets = cmds.ls(new_nodes, set=True) + if not imported_sets: + return True + + # update imported sets information + for node in imported_sets: + if not cmds.attributeQuery("id", node=node, exists=True): + continue + if cmds.getAttr("{}.id".format(node)) != "pyblish.avalon.instance": + continue + if not cmds.attributeQuery("asset", node=node, exists=True): + continue + asset = legacy_io.Session["AVALON_ASSET"] + + cmds.setAttr("{}.asset".format(node), asset, type="string") + return True From 3e25f2cddac284ed4583f751687525f1e2471e4a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 6 Feb 2023 18:08:25 +0000 Subject: [PATCH 024/119] Working AssProxyLoader --- openpype/hosts/maya/plugins/load/load_ass.py | 97 +++++++++----------- 1 file changed, 44 insertions(+), 53 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_ass.py b/openpype/hosts/maya/plugins/load/load_ass.py index 6317c0a7ce..ada65998a5 100644 --- a/openpype/hosts/maya/plugins/load/load_ass.py +++ b/openpype/hosts/maya/plugins/load/load_ass.py @@ -27,6 +27,18 @@ def is_sequence(files): return sequence +def set_color(node, context): + project_name = context["project"]["name"] + settings = get_project_settings(project_name) + colors = settings['maya']['load']['colors'] + color = colors.get('ass') + if color is not None: + cmds.setAttr(node + ".useOutlinerColor", True) + cmds.setAttr( + node + ".outlinerColor", color[0], color[1], color[2] + ) + + class AssProxyLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): """Load Arnold Proxy as reference""" @@ -46,19 +58,14 @@ class AssProxyLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): frame_start = version_data.get("frame_start", None) - try: - family = context["representation"]["context"]["family"] - except ValueError: - family = "ass" - with maintained_selection(): groupName = "{}:{}".format(namespace, name) path = self.fname - proxyPath_base = os.path.splitext(path)[0] + proxy_path_base = os.path.splitext(path)[0] if frame_start is not None: - proxyPath_base = os.path.splitext(proxyPath_base)[0] + proxy_path_base = os.path.splitext(proxy_path_base)[0] publish_folder = os.path.split(path)[0] files_in_folder = os.listdir(publish_folder) @@ -71,43 +78,33 @@ class AssProxyLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): path = os.path.join(publish_folder, filename) - proxyPath = proxyPath_base + ".ass" + proxy_path = proxy_path_base + ".ma" + msg = proxy_path + " does not exist." + assert os.path.exists(proxy_path), msg - project_name = context["project"]["name"] - file_url = self.prepare_root_value( - proxyPath, project_name + nodes = cmds.file( + proxy_path, + namespace=namespace, + reference=True, + returnNewNodes=True, + groupReference=True, + groupName=groupName ) - self.log.info(file_url) - nodes = cmds.file(file_url, - namespace=namespace, - reference=True, - returnNewNodes=True, - groupReference=True, - groupName=groupName) - - cmds.makeIdentity(groupName, apply=False, rotate=True, - translate=True, scale=True) + cmds.makeIdentity( + groupName, apply=False, rotate=True, translate=True, scale=True + ) # Set attributes - proxyShape = cmds.ls(nodes, type="mesh")[0] + proxy_shape = cmds.ls(nodes, type="mesh")[0] - proxyShape.aiTranslator.set('procedural') - proxyShape.dso.set(path) - proxyShape.aiOverrideShaders.set(0) + cmds.setAttr( + proxy_shape + ".aiTranslator", "procedural", type="string" + ) + cmds.setAttr(proxy_shape + ".dso", self.fname, type="string") + cmds.setAttr(proxy_shape + ".aiOverrideShaders", 0) - settings = get_project_settings(project_name) - colors = settings['maya']['load']['colors'] - - c = colors.get(family) - if c is not None: - cmds.setAttr(groupName + ".useOutlinerColor", 1) - cmds.setAttr( - groupName + ".outlinerColor", - (float(c[0]) / 255), - (float(c[1]) / 255), - (float(c[2]) / 255) - ) + set_color(groupName, context) self[:] = nodes @@ -121,16 +118,16 @@ class AssProxyLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): representation["context"].pop("frame", None) path = get_representation_path(representation) - proxyPath = os.path.splitext(path)[0] + ".ma" + proxy_path = os.path.splitext(path)[0] + ".ma" # Get reference node from container members members = cmds.sets(node, query=True, nodesOnly=True) reference_node = get_reference_node(members) - assert os.path.exists(proxyPath), "%s does not exist." % proxyPath + assert os.path.exists(proxy_path), "%s does not exist." % proxy_path try: - file_url = self.prepare_root_value(proxyPath, + file_url = self.prepare_root_value(proxy_path, representation["context"] ["project"] ["name"]) @@ -140,11 +137,13 @@ class AssProxyLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): returnNewNodes=True) # Set attributes - proxyShape = pm.ls(content, type="mesh")[0] + proxy_shape = cmds.ls(content, type="mesh")[0] - proxyShape.aiTranslator.set('procedural') - proxyShape.dso.set(path) - proxyShape.aiOverrideShaders.set(0) + cmds.setAttr( + proxy_shape + ".aiTranslator", "procedural", type="string" + ) + cmds.setAttr(proxy_shape + ".dso", self.fname, type="string") + cmds.setAttr(proxy_shape + ".aiOverrideShaders", 0) except RuntimeError as exc: # When changing a reference to a file that has load errors the @@ -206,15 +205,7 @@ class AssStandinLoader(load.LoaderPlugin): label = "{}:{}".format(namespace, name) root = cmds.group(name=label, empty=True) - settings = get_project_settings(os.environ['AVALON_PROJECT']) - colors = settings['maya']['load']['colors'] - - color = colors.get('ass') - if color is not None: - cmds.setAttr(root + ".useOutlinerColor", True) - cmds.setAttr( - root + ".outlinerColor", color[0], color[1], color[2] - ) + set_color(root, context) # Create transform with shape transform_name = label + "_ASS" From ecef4cbb4691ff7ea8320fda55e3ac4a8d70c4f2 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 7 Feb 2023 08:57:10 +0000 Subject: [PATCH 025/119] More information about issues with publishing. --- openpype/hosts/maya/plugins/load/load_ass.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_ass.py b/openpype/hosts/maya/plugins/load/load_ass.py index ada65998a5..e4e0b0da84 100644 --- a/openpype/hosts/maya/plugins/load/load_ass.py +++ b/openpype/hosts/maya/plugins/load/load_ass.py @@ -79,7 +79,10 @@ class AssProxyLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): path = os.path.join(publish_folder, filename) proxy_path = proxy_path_base + ".ma" - msg = proxy_path + " does not exist." + msg = ( + proxy_path + " does not exist.\nThere are most likely no " + + "proxy shapes in the \"proxy_SET\" when publishing." + ) assert os.path.exists(proxy_path), msg nodes = cmds.file( From 662d68daec5c0b786489e7035bd75be77a2cdd48 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 7 Feb 2023 10:06:56 +0000 Subject: [PATCH 026/119] Support for multiple proxy meshes. --- openpype/hosts/maya/plugins/load/load_ass.py | 39 +++++++++++++++++-- .../hosts/maya/plugins/publish/collect_ass.py | 4 -- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_ass.py b/openpype/hosts/maya/plugins/load/load_ass.py index e4e0b0da84..58abfa964e 100644 --- a/openpype/hosts/maya/plugins/load/load_ass.py +++ b/openpype/hosts/maya/plugins/load/load_ass.py @@ -60,7 +60,7 @@ class AssProxyLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): with maintained_selection(): - groupName = "{}:{}".format(namespace, name) + group_name = "{}:{}".format(namespace, name) path = self.fname proxy_path_base = os.path.splitext(path)[0] @@ -91,13 +91,36 @@ class AssProxyLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): reference=True, returnNewNodes=True, groupReference=True, - groupName=groupName + groupName=group_name ) + # Reset group to world zero. + transform_data = {} + for node in nodes: + if cmds.nodeType(node) != "transform": + continue + + transform_data[node] = {} + attrs = ["translate", "rotate", "scale"] + parameters = ["X", "Y", "Z"] + for attr in attrs: + for parameter in parameters: + transform_data[node][attr + parameter] = cmds.getAttr( + "{}.{}{}".format(node, attr, parameter) + ) + cmds.makeIdentity( - groupName, apply=False, rotate=True, translate=True, scale=True + group_name, + apply=False, + rotate=True, + translate=True, + scale=True ) + for node, data in transform_data.items(): + for attr, value in data.items(): + cmds.setAttr("{}.{}".format(node, attr), value) + # Set attributes proxy_shape = cmds.ls(nodes, type="mesh")[0] @@ -107,7 +130,15 @@ class AssProxyLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): cmds.setAttr(proxy_shape + ".dso", self.fname, type="string") cmds.setAttr(proxy_shape + ".aiOverrideShaders", 0) - set_color(groupName, context) + # Hides all other meshes at render time. + remaining_meshes = cmds.ls(nodes, type="mesh") + remaining_meshes.remove(proxy_shape) + for node in remaining_meshes: + cmds.setAttr( + node + ".aiTranslator", "procedural", type="string" + ) + + set_color(group_name, context) self[:] = nodes diff --git a/openpype/hosts/maya/plugins/publish/collect_ass.py b/openpype/hosts/maya/plugins/publish/collect_ass.py index b5e05d6665..7b5d1a00c7 100644 --- a/openpype/hosts/maya/plugins/publish/collect_ass.py +++ b/openpype/hosts/maya/plugins/publish/collect_ass.py @@ -1,5 +1,4 @@ from maya import cmds -from openpype.pipeline.publish import KnownPublishError import pyblish.api @@ -25,9 +24,6 @@ class CollectAssData(pyblish.api.InstancePlugin): instance.data['setMembers'] = members self.log.debug('content members: {}'.format(members)) elif objset.startswith("proxy_SET"): - if len(members) != 1: - msg = "You have multiple proxy meshes, please only use one" - raise KnownPublishError(msg) instance.data['proxy'] = members self.log.debug('proxy members: {}'.format(members)) From 77a6139c777d25b7bfd05eff55af94184de716ab Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 7 Feb 2023 10:13:22 +0000 Subject: [PATCH 027/119] Fix updating proxy --- openpype/hosts/maya/plugins/load/load_ass.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_ass.py b/openpype/hosts/maya/plugins/load/load_ass.py index 58abfa964e..59cfae7cdb 100644 --- a/openpype/hosts/maya/plugins/load/load_ass.py +++ b/openpype/hosts/maya/plugins/load/load_ass.py @@ -148,14 +148,14 @@ class AssProxyLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): self.update(container, representation) def update(self, container, representation): - node = container["objectName"] + container_node = container["objectName"] representation["context"].pop("frame", None) path = get_representation_path(representation) proxy_path = os.path.splitext(path)[0] + ".ma" # Get reference node from container members - members = cmds.sets(node, query=True, nodesOnly=True) + members = cmds.sets(container_node, query=True, nodesOnly=True) reference_node = get_reference_node(members) assert os.path.exists(proxy_path), "%s does not exist." % proxy_path @@ -195,18 +195,26 @@ class AssProxyLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): self.log.warning("Ignoring file read error:\n%s", exc) + # Hides all other meshes at render time. + remaining_meshes = cmds.ls(content, type="mesh") + remaining_meshes.remove(proxy_shape) + for node in remaining_meshes: + cmds.setAttr( + node + ".aiTranslator", "procedural", type="string" + ) + # Add new nodes of the reference to the container - cmds.sets(content, forceElement=node) + cmds.sets(content, forceElement=container_node) # Remove any placeHolderList attribute entries from the set that # are remaining from nodes being removed from the referenced file. - members = cmds.sets(node, query=True) + members = cmds.sets(container_node, query=True) invalid = [x for x in members if ".placeHolderList" in x] if invalid: - cmds.sets(invalid, remove=node) + cmds.sets(invalid, remove=container_node) # Update metadata - cmds.setAttr("{}.representation".format(node), + cmds.setAttr("{}.representation".format(container_node), str(representation["_id"]), type="string") From 9733b07f6dd383a63c00d51312bf700a475aa5d8 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 7 Feb 2023 15:21:58 +0000 Subject: [PATCH 028/119] Remove AssProxyLoader --- openpype/hosts/maya/plugins/load/load_ass.py | 195 +------------------ 1 file changed, 5 insertions(+), 190 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_ass.py b/openpype/hosts/maya/plugins/load/load_ass.py index 59cfae7cdb..50b72d87e8 100644 --- a/openpype/hosts/maya/plugins/load/load_ass.py +++ b/openpype/hosts/maya/plugins/load/load_ass.py @@ -9,12 +9,7 @@ from openpype.pipeline import ( load, get_representation_path ) -import openpype.hosts.maya.api.plugin -from openpype.hosts.maya.api.plugin import get_reference_node -from openpype.hosts.maya.api.lib import ( - maintained_selection, - unique_namespace -) +from openpype.hosts.maya.api.lib import unique_namespace from openpype.hosts.maya.api.pipeline import containerise @@ -39,193 +34,13 @@ def set_color(node, context): ) -class AssProxyLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): - """Load Arnold Proxy as reference""" +class ArnoldStandinLoader(load.LoaderPlugin): + """Load file as Arnold standin""" families = ["ass"] representations = ["ass"] - label = "Reference .ASS standin with Proxy" - order = -10 - icon = "code-fork" - color = "orange" - - def process_reference(self, context, name, namespace, options): - version = context['version'] - version_data = version.get("data", {}) - - self.log.info("version_data: {}\n".format(version_data)) - - frame_start = version_data.get("frame_start", None) - - with maintained_selection(): - - group_name = "{}:{}".format(namespace, name) - path = self.fname - proxy_path_base = os.path.splitext(path)[0] - - if frame_start is not None: - proxy_path_base = os.path.splitext(proxy_path_base)[0] - - publish_folder = os.path.split(path)[0] - files_in_folder = os.listdir(publish_folder) - collections, remainder = clique.assemble(files_in_folder) - - if collections: - hashes = collections[0].padding * '#' - coll = collections[0].format('{head}[index]{tail}') - filename = coll.replace('[index]', hashes) - - path = os.path.join(publish_folder, filename) - - proxy_path = proxy_path_base + ".ma" - msg = ( - proxy_path + " does not exist.\nThere are most likely no " + - "proxy shapes in the \"proxy_SET\" when publishing." - ) - assert os.path.exists(proxy_path), msg - - nodes = cmds.file( - proxy_path, - namespace=namespace, - reference=True, - returnNewNodes=True, - groupReference=True, - groupName=group_name - ) - - # Reset group to world zero. - transform_data = {} - for node in nodes: - if cmds.nodeType(node) != "transform": - continue - - transform_data[node] = {} - attrs = ["translate", "rotate", "scale"] - parameters = ["X", "Y", "Z"] - for attr in attrs: - for parameter in parameters: - transform_data[node][attr + parameter] = cmds.getAttr( - "{}.{}{}".format(node, attr, parameter) - ) - - cmds.makeIdentity( - group_name, - apply=False, - rotate=True, - translate=True, - scale=True - ) - - for node, data in transform_data.items(): - for attr, value in data.items(): - cmds.setAttr("{}.{}".format(node, attr), value) - - # Set attributes - proxy_shape = cmds.ls(nodes, type="mesh")[0] - - cmds.setAttr( - proxy_shape + ".aiTranslator", "procedural", type="string" - ) - cmds.setAttr(proxy_shape + ".dso", self.fname, type="string") - cmds.setAttr(proxy_shape + ".aiOverrideShaders", 0) - - # Hides all other meshes at render time. - remaining_meshes = cmds.ls(nodes, type="mesh") - remaining_meshes.remove(proxy_shape) - for node in remaining_meshes: - cmds.setAttr( - node + ".aiTranslator", "procedural", type="string" - ) - - set_color(group_name, context) - - self[:] = nodes - - return nodes - - def switch(self, container, representation): - self.update(container, representation) - - def update(self, container, representation): - container_node = container["objectName"] - - representation["context"].pop("frame", None) - path = get_representation_path(representation) - proxy_path = os.path.splitext(path)[0] + ".ma" - - # Get reference node from container members - members = cmds.sets(container_node, query=True, nodesOnly=True) - reference_node = get_reference_node(members) - - assert os.path.exists(proxy_path), "%s does not exist." % proxy_path - - try: - file_url = self.prepare_root_value(proxy_path, - representation["context"] - ["project"] - ["name"]) - content = cmds.file(file_url, - loadReference=reference_node, - type="mayaAscii", - returnNewNodes=True) - - # Set attributes - proxy_shape = cmds.ls(content, type="mesh")[0] - - cmds.setAttr( - proxy_shape + ".aiTranslator", "procedural", type="string" - ) - cmds.setAttr(proxy_shape + ".dso", self.fname, type="string") - cmds.setAttr(proxy_shape + ".aiOverrideShaders", 0) - - except RuntimeError as exc: - # When changing a reference to a file that has load errors the - # command will raise an error even if the file is still loaded - # correctly (e.g. when raising errors on Arnold attributes) - # When the file is loaded and has content, we consider it's fine. - if not cmds.referenceQuery(reference_node, isLoaded=True): - raise - - content = cmds.referenceQuery(reference_node, - nodes=True, - dagPath=True) - if not content: - raise - - self.log.warning("Ignoring file read error:\n%s", exc) - - # Hides all other meshes at render time. - remaining_meshes = cmds.ls(content, type="mesh") - remaining_meshes.remove(proxy_shape) - for node in remaining_meshes: - cmds.setAttr( - node + ".aiTranslator", "procedural", type="string" - ) - - # Add new nodes of the reference to the container - cmds.sets(content, forceElement=container_node) - - # Remove any placeHolderList attribute entries from the set that - # are remaining from nodes being removed from the referenced file. - members = cmds.sets(container_node, query=True) - invalid = [x for x in members if ".placeHolderList" in x] - if invalid: - cmds.sets(invalid, remove=container_node) - - # Update metadata - cmds.setAttr("{}.representation".format(container_node), - str(representation["_id"]), - type="string") - - -class AssStandinLoader(load.LoaderPlugin): - """Load .ASS file as standin""" - - families = ["ass"] - representations = ["ass"] - - label = "Load .ASS file as standin" + label = "Load file as Arnold standin" order = -5 icon = "code-fork" color = "orange" @@ -250,7 +65,7 @@ class AssStandinLoader(load.LoaderPlugin): set_color(root, context) # Create transform with shape - transform_name = label + "_ASS" + transform_name = label + "_standin" standinShape = mtoa.ui.arnoldmenu.createStandIn() standin = cmds.listRelatives(standinShape, parent=True)[0] From 01d763fe991f1ecdc83b1ce8b6ee002beead7dea Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 7 Feb 2023 16:29:02 +0000 Subject: [PATCH 029/119] Extract Ass proxy. --- .../hosts/maya/plugins/publish/extract_ass.py | 87 ++++++++++++++----- .../maya/plugins/publish/extract_assproxy.py | 81 ----------------- 2 files changed, 63 insertions(+), 105 deletions(-) delete mode 100644 openpype/hosts/maya/plugins/publish/extract_assproxy.py diff --git a/openpype/hosts/maya/plugins/publish/extract_ass.py b/openpype/hosts/maya/plugins/publish/extract_ass.py index 049f256a7a..4cff9d0183 100644 --- a/openpype/hosts/maya/plugins/publish/extract_ass.py +++ b/openpype/hosts/maya/plugins/publish/extract_ass.py @@ -1,16 +1,18 @@ import os +import copy from maya import cmds import arnold from openpype.pipeline import publish from openpype.hosts.maya.api.lib import maintained_selection, attribute_values +from openpype.lib import StringTemplate -class ExtractAssStandin(publish.Extractor): - """Extract the content of the instance to a ass file""" +class ExtractArnoldSceneSource(publish.Extractor): + """Extract the content of the instance to an Arnold Scene Source file.""" - label = "Arnold Scene Source (.ass)" + label = "Arnold Scene Source" hosts = ["maya"] families = ["ass"] asciiAss = False @@ -18,7 +20,6 @@ class ExtractAssStandin(publish.Extractor): def process(self, instance): staging_dir = self.staging_dir(instance) filename = "{}.ass".format(instance.name) - filenames = [] file_path = os.path.join(staging_dir, filename) # Mask @@ -42,7 +43,7 @@ class ExtractAssStandin(publish.Extractor): mask = mask ^ node_types[key] # Motion blur - values = { + attribute_data = { "defaultArnoldRenderOptions.motion_blur_enable": instance.data.get( "motionBlur", True ), @@ -70,13 +71,65 @@ class ExtractAssStandin(publish.Extractor): "mask": mask } - self.log.info("Writing: '%s'" % file_path) - with attribute_values(values): + filenames = self._extract( + instance.data["setMembers"], attribute_data, kwargs + ) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + "name": "ass", + "ext": "ass", + "files": filenames if len(filenames) > 1 else filenames[0], + "stagingDir": staging_dir, + "frameStart": kwargs["startFrame"] + } + + instance.data["representations"].append(representation) + + self.log.info( + "Extracted instance {} to: {}".format(instance.name, staging_dir) + ) + + # Extract proxy. + kwargs["filename"] = file_path.replace(".ass", "_proxy.ass") + filenames = self._extract( + instance.data["proxy"], attribute_data, kwargs + ) + + template_data = copy.deepcopy(instance.data["anatomyData"]) + template_data.update({"ext": "ass"}) + templates = instance.context.data["anatomy"].templates["publish"] + published_filename_without_extension = StringTemplate( + templates["file"] + ).format(template_data).replace(".ass", "_proxy") + transfers = [] + for filename in filenames: + source = os.path.join(staging_dir, filename) + destination = os.path.join( + instance.data["resourcesDir"], + filename.replace( + filename.split(".")[0], + published_filename_without_extension + ) + ) + transfers.append((source, destination)) + + for source, destination in transfers: + self.log.debug("Transfer: {} > {}".format(source, destination)) + + instance.data["transfers"] = transfers + + def _extract(self, nodes, attribute_data, kwargs): + self.log.info("Writing: " + kwargs["filename"]) + filenames = [] + with attribute_values(attribute_data): with maintained_selection(): self.log.info( - "Writing: {}".format(instance.data["setMembers"]) + "Writing: {}".format(nodes) ) - cmds.select(instance.data["setMembers"], noExpand=True) + cmds.select(nodes, noExpand=True) self.log.info( "Extracting ass sequence with: {}".format(kwargs) @@ -89,18 +142,4 @@ class ExtractAssStandin(publish.Extractor): self.log.info("Exported: {}".format(filenames)) - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { - 'name': 'ass', - 'ext': 'ass', - 'files': filenames if len(filenames) > 1 else filenames[0], - "stagingDir": staging_dir, - 'frameStart': kwargs["startFrame"] - } - - instance.data["representations"].append(representation) - - self.log.info("Extracted instance '%s' to: %s" - % (instance.name, staging_dir)) + return filenames diff --git a/openpype/hosts/maya/plugins/publish/extract_assproxy.py b/openpype/hosts/maya/plugins/publish/extract_assproxy.py deleted file mode 100644 index 4937a28a9e..0000000000 --- a/openpype/hosts/maya/plugins/publish/extract_assproxy.py +++ /dev/null @@ -1,81 +0,0 @@ -import os -import contextlib - -from maya import cmds - -from openpype.pipeline import publish -from openpype.hosts.maya.api.lib import maintained_selection - - -class ExtractAssProxy(publish.Extractor): - """Extract proxy model as Maya Ascii to use as arnold standin - - - """ - - order = publish.Extractor.order + 0.2 - label = "Ass Proxy (Maya ASCII)" - hosts = ["maya"] - families = ["ass"] - - def process(self, instance): - - @contextlib.contextmanager - def unparent(root): - """Temporarily unparent `root`""" - parent = cmds.listRelatives(root, parent=True) - if parent: - cmds.parent(root, world=True) - yield - self.log.info("{} - {}".format(root, parent)) - cmds.parent(root, parent) - else: - yield - - # Define extract output file path - stagingdir = self.staging_dir(instance) - filename = "{0}.ma".format(instance.name) - path = os.path.join(stagingdir, filename) - - # Perform extraction - self.log.info("Performing extraction..") - - # Get only the shape contents we need in such a way that we avoid - # taking along intermediateObjects - proxy = instance.data.get('proxy', None) - - if not proxy: - self.log.info("no proxy mesh") - return - - members = cmds.ls(proxy, - dag=True, - transforms=True, - noIntermediate=True) - self.log.info(members) - - with maintained_selection(): - with unparent(members[0]): - cmds.select(members, noExpand=True) - cmds.file(path, - force=True, - typ="mayaAscii", - exportSelected=True, - preserveReferences=False, - channels=False, - constraints=False, - expressions=False, - constructionHistory=False) - - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { - 'name': 'ma', - 'ext': 'ma', - 'files': filename, - "stagingDir": stagingdir - } - instance.data["representations"].append(representation) - - self.log.info("Extracted instance '%s' to: %s" % (instance.name, path)) From 1d6206d41456df1dec2f60617ba54390d1857dfd Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 7 Feb 2023 16:39:57 +0000 Subject: [PATCH 030/119] Rename plugin --- .../maya/plugins/load/{load_ass.py => load_arnold_standin.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename openpype/hosts/maya/plugins/load/{load_ass.py => load_arnold_standin.py} (100%) diff --git a/openpype/hosts/maya/plugins/load/load_ass.py b/openpype/hosts/maya/plugins/load/load_arnold_standin.py similarity index 100% rename from openpype/hosts/maya/plugins/load/load_ass.py rename to openpype/hosts/maya/plugins/load/load_arnold_standin.py From e3c58662b9cf05211d35c5282e5d25a2fb5b46f0 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 7 Feb 2023 18:13:25 +0000 Subject: [PATCH 031/119] Working loading proxy --- .../maya/plugins/load/load_arnold_standin.py | 117 ++++++++++++++---- 1 file changed, 91 insertions(+), 26 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_arnold_standin.py b/openpype/hosts/maya/plugins/load/load_arnold_standin.py index 50b72d87e8..57e1d8a6e0 100644 --- a/openpype/hosts/maya/plugins/load/load_arnold_standin.py +++ b/openpype/hosts/maya/plugins/load/load_arnold_standin.py @@ -9,7 +9,9 @@ from openpype.pipeline import ( load, get_representation_path ) -from openpype.hosts.maya.api.lib import unique_namespace +from openpype.hosts.maya.api.lib import ( + unique_namespace, get_attribute_input, maintained_selection +) from openpype.hosts.maya.api.pipeline import containerise @@ -22,18 +24,6 @@ def is_sequence(files): return sequence -def set_color(node, context): - project_name = context["project"]["name"] - settings = get_project_settings(project_name) - colors = settings['maya']['load']['colors'] - color = colors.get('ass') - if color is not None: - cmds.setAttr(node + ".useOutlinerColor", True) - cmds.setAttr( - node + ".outlinerColor", color[0], color[1], color[2] - ) - - class ArnoldStandinLoader(load.LoaderPlugin): """Load file as Arnold standin""" @@ -62,24 +52,35 @@ class ArnoldStandinLoader(load.LoaderPlugin): label = "{}:{}".format(namespace, name) root = cmds.group(name=label, empty=True) - set_color(root, context) + # Set color. + project_name = context["project"]["name"] + settings = get_project_settings(project_name) + colors = settings['maya']['load']['colors'] + color = colors.get('ass') + if color is not None: + cmds.setAttr(root + ".useOutlinerColor", True) + cmds.setAttr( + root + ".outlinerColor", color[0], color[1], color[2] + ) - # Create transform with shape - transform_name = label + "_standin" + with maintained_selection(): + # Create transform with shape + transform_name = label + "_standin" - standinShape = mtoa.ui.arnoldmenu.createStandIn() - standin = cmds.listRelatives(standinShape, parent=True)[0] - standin = cmds.rename(standin, transform_name) - standinShape = cmds.listRelatives(standin, shapes=True)[0] + standinShape = mtoa.ui.arnoldmenu.createStandIn() + standin = cmds.listRelatives(standinShape, parent=True)[0] + standin = cmds.rename(standin, transform_name) + standinShape = cmds.listRelatives(standin, shapes=True)[0] - cmds.parent(standin, root) + cmds.parent(standin, root) - # Set the standin filepath - cmds.setAttr(standinShape + ".dso", self.fname, type="string") - sequence = is_sequence(os.listdir(os.path.dirname(self.fname))) - cmds.setAttr(standinShape + ".useFrameExtension", sequence) + # Set the standin filepath + dso_path, operator = self._setup_proxy(standinShape, self.fname) + cmds.setAttr(standinShape + ".dso", dso_path, type="string") + sequence = is_sequence(os.listdir(os.path.dirname(self.fname))) + cmds.setAttr(standinShape + ".useFrameExtension", sequence) - nodes = [root, standin] + nodes = [root, standin, operator] self[:] = nodes return containerise( @@ -89,6 +90,70 @@ class ArnoldStandinLoader(load.LoaderPlugin): context=context, loader=self.__class__.__name__) + def get_next_free_multi_index(self, attr_name): + """Find the next unconnected multi index at the input attribute.""" + + start_index = 0 + # Assume a max of 10 million connections + while start_index < 10000000: + connection_info = cmds.connectionInfo( + "{}[{}]".format(attr_name, start_index), + sourceFromDestination=True + ) + if len(connection_info or []) == 0: + return start_index + start_index += 1 + + def _setup_proxy(self, shape, path): + basename_split = os.path.basename(path).split(".") + proxy_basename = ( + basename_split[0] + "_proxy." + ".".join(basename_split[1:]) + ) + proxy_path = "/".join( + [os.path.dirname(path), "resources", proxy_basename] + ) + + if not os.path.exists(proxy_path): + self.log.error("Proxy files do not exist. Skipping proxy setup.") + return path + + options_node = "defaultArnoldRenderOptions" + merge_operator = get_attribute_input(options_node + ".operator") + if merge_operator is None: + merge_operator = cmds.createNode("aiMerge") + cmds.connectAttr( + merge_operator + ".message", options_node + ".operator" + ) + + merge_operator = merge_operator.split(".")[0] + + string_replace_operator = cmds.createNode("aiStringReplace") + cmds.setAttr( + string_replace_operator + ".selection", + "*.(@node=='procedural')", + type="string" + ) + cmds.setAttr( + string_replace_operator + ".match", + "resources/" + proxy_basename, + type="string" + ) + cmds.setAttr( + string_replace_operator + ".replace", + os.path.basename(path), + type="string" + ) + + cmds.connectAttr( + string_replace_operator + ".out", + "{}.inputs[{}]".format( + merge_operator, + self.get_next_free_multi_index(merge_operator + ".inputs") + ) + ) + + return proxy_path, string_replace_operator + def update(self, container, representation): # Update the standin standins = list() From af752ae60663cd64f038ab154216a52e61d6dcb2 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 7 Feb 2023 18:23:04 +0000 Subject: [PATCH 032/119] Working proxy update --- .../maya/plugins/load/load_arnold_standin.py | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_arnold_standin.py b/openpype/hosts/maya/plugins/load/load_arnold_standin.py index 57e1d8a6e0..a90aa02d4d 100644 --- a/openpype/hosts/maya/plugins/load/load_arnold_standin.py +++ b/openpype/hosts/maya/plugins/load/load_arnold_standin.py @@ -104,7 +104,7 @@ class ArnoldStandinLoader(load.LoaderPlugin): return start_index start_index += 1 - def _setup_proxy(self, shape, path): + def _get_proxy_path(self, path): basename_split = os.path.basename(path).split(".") proxy_basename = ( basename_split[0] + "_proxy." + ".".join(basename_split[1:]) @@ -112,10 +112,14 @@ class ArnoldStandinLoader(load.LoaderPlugin): proxy_path = "/".join( [os.path.dirname(path), "resources", proxy_basename] ) + return proxy_basename, proxy_path + + def _setup_proxy(self, shape, path): + proxy_basename, proxy_path = self._get_proxy_path(path) if not os.path.exists(proxy_path): self.log.error("Proxy files do not exist. Skipping proxy setup.") - return path + return os.path.basename(path), path options_node = "defaultArnoldRenderOptions" merge_operator = get_attribute_input(options_node + ".operator") @@ -156,20 +160,33 @@ class ArnoldStandinLoader(load.LoaderPlugin): def update(self, container, representation): # Update the standin - standins = list() members = cmds.sets(container['objectName'], query=True) for member in members: + if cmds.nodeType(member) == "aiStringReplace": + string_replace_operator = member + shapes = cmds.listRelatives(member, shapes=True) if not shapes: continue if cmds.nodeType(shapes[0]) == "aiStandIn": - standins.append(shapes[0]) + standin = shapes[0] path = get_representation_path(representation) + proxy_basename, proxy_path = self._get_proxy_path(path) + cmds.setAttr( + string_replace_operator + ".match", + "resources/" + proxy_basename, + type="string" + ) + cmds.setAttr( + string_replace_operator + ".replace", + os.path.basename(path), + type="string" + ) + cmds.setAttr(standin + ".dso", proxy_path, type="string") + sequence = is_sequence(os.listdir(os.path.dirname(path))) - for standin in standins: - cmds.setAttr(standin + ".dso", path, type="string") - cmds.setAttr(standin + ".useFrameExtension", sequence) + cmds.setAttr(standin + ".useFrameExtension", sequence) cmds.setAttr( container["objectName"] + ".representation", From bb4a44fe33d0a6e1518179f19efbf309969a3d28 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 7 Feb 2023 18:30:34 +0000 Subject: [PATCH 033/119] Clean up string replace operator --- .../hosts/maya/plugins/load/load_arnold_standin.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_arnold_standin.py b/openpype/hosts/maya/plugins/load/load_arnold_standin.py index a90aa02d4d..635e86708b 100644 --- a/openpype/hosts/maya/plugins/load/load_arnold_standin.py +++ b/openpype/hosts/maya/plugins/load/load_arnold_standin.py @@ -75,7 +75,9 @@ class ArnoldStandinLoader(load.LoaderPlugin): cmds.parent(standin, root) # Set the standin filepath - dso_path, operator = self._setup_proxy(standinShape, self.fname) + dso_path, operator = self._setup_proxy( + standinShape, self.fname, namespace + ) cmds.setAttr(standinShape + ".dso", dso_path, type="string") sequence = is_sequence(os.listdir(os.path.dirname(self.fname))) cmds.setAttr(standinShape + ".useFrameExtension", sequence) @@ -114,7 +116,7 @@ class ArnoldStandinLoader(load.LoaderPlugin): ) return proxy_basename, proxy_path - def _setup_proxy(self, shape, path): + def _setup_proxy(self, shape, path, namespace): proxy_basename, proxy_path = self._get_proxy_path(path) if not os.path.exists(proxy_path): @@ -131,7 +133,9 @@ class ArnoldStandinLoader(load.LoaderPlugin): merge_operator = merge_operator.split(".")[0] - string_replace_operator = cmds.createNode("aiStringReplace") + string_replace_operator = cmds.createNode( + "aiStringReplace", name=namespace + ":string_replace_operator" + ) cmds.setAttr( string_replace_operator + ".selection", "*.(@node=='procedural')", @@ -198,7 +202,6 @@ class ArnoldStandinLoader(load.LoaderPlugin): self.update(container, representation) def remove(self, container): - import maya.cmds as cmds members = cmds.sets(container['objectName'], query=True) cmds.lockNode(members, lock=False) cmds.delete([container['objectName']] + members) From 87712716d047716800bf27a934a9648b9024bed5 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 8 Feb 2023 07:48:45 +0000 Subject: [PATCH 034/119] Combine Alembic standin --- .../maya/plugins/load/load_abc_to_standin.py | 132 ------------------ .../maya/plugins/load/load_arnold_standin.py | 18 +-- 2 files changed, 10 insertions(+), 140 deletions(-) delete mode 100644 openpype/hosts/maya/plugins/load/load_abc_to_standin.py diff --git a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py deleted file mode 100644 index 70866a3ba6..0000000000 --- a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py +++ /dev/null @@ -1,132 +0,0 @@ -import os - -from openpype.pipeline import ( - legacy_io, - load, - get_representation_path -) -from openpype.settings import get_project_settings - - -class AlembicStandinLoader(load.LoaderPlugin): - """Load Alembic as Arnold Standin""" - - families = ["animation", "model", "proxyAbc", "pointcache"] - representations = ["abc"] - - label = "Import Alembic as Arnold Standin" - order = -5 - icon = "code-fork" - color = "orange" - - def load(self, context, name, namespace, options): - - import maya.cmds as cmds - import mtoa.ui.arnoldmenu - from openpype.hosts.maya.api.pipeline import containerise - from openpype.hosts.maya.api.lib import unique_namespace - - version = context["version"] - version_data = version.get("data", {}) - family = version["data"]["families"] - self.log.info("version_data: {}\n".format(version_data)) - self.log.info("family: {}\n".format(family)) - frameStart = version_data.get("frameStart", None) - - asset = context["asset"]["name"] - namespace = namespace or unique_namespace( - asset + "_", - prefix="_" if asset[0].isdigit() else "", - suffix="_", - ) - - # Root group - label = "{}:{}".format(namespace, name) - root = cmds.group(name=label, empty=True) - - settings = get_project_settings(os.environ['AVALON_PROJECT']) - colors = settings["maya"]["load"]["colors"] - fps = legacy_io.Session["AVALON_FPS"] - c = colors.get(family[0]) - if c is not None: - r = (float(c[0]) / 255) - g = (float(c[1]) / 255) - b = (float(c[2]) / 255) - cmds.setAttr(root + ".useOutlinerColor", 1) - cmds.setAttr(root + ".outlinerColor", - r, g, b) - - transform_name = label + "_ABC" - - standinShape = cmds.ls(mtoa.ui.arnoldmenu.createStandIn())[0] - standin = cmds.listRelatives(standinShape, parent=True, - typ="transform") - standin = cmds.rename(standin, transform_name) - standinShape = cmds.listRelatives(standin, children=True)[0] - - cmds.parent(standin, root) - - # Set the standin filepath - cmds.setAttr(standinShape + ".dso", self.fname, type="string") - cmds.setAttr(standinShape + ".abcFPS", float(fps)) - - if frameStart is None: - cmds.setAttr(standinShape + ".useFrameExtension", 0) - - elif "model" in family: - cmds.setAttr(standinShape + ".useFrameExtension", 0) - - else: - cmds.setAttr(standinShape + ".useFrameExtension", 1) - - nodes = [root, standin] - self[:] = nodes - - return containerise( - name=name, - namespace=namespace, - nodes=nodes, - context=context, - loader=self.__class__.__name__) - - def update(self, container, representation): - - import pymel.core as pm - - path = get_representation_path(representation) - fps = legacy_io.Session["AVALON_FPS"] - # Update the standin - standins = list() - members = pm.sets(container['objectName'], query=True) - self.log.info("container:{}".format(container)) - for member in members: - shape = member.getShape() - if (shape and shape.type() == "aiStandIn"): - standins.append(shape) - - for standin in standins: - standin.dso.set(path) - standin.abcFPS.set(float(fps)) - if "modelMain" in container['objectName']: - standin.useFrameExtension.set(0) - else: - standin.useFrameExtension.set(1) - - container = pm.PyNode(container["objectName"]) - container.representation.set(str(representation["_id"])) - - def switch(self, container, representation): - self.update(container, representation) - - def remove(self, container): - import maya.cmds as cmds - members = cmds.sets(container['objectName'], query=True) - cmds.lockNode(members, lock=False) - cmds.delete([container['objectName']] + members) - - # Clean up the namespace - try: - cmds.namespace(removeNamespace=container['namespace'], - deleteNamespaceContent=True) - except RuntimeError: - pass diff --git a/openpype/hosts/maya/plugins/load/load_arnold_standin.py b/openpype/hosts/maya/plugins/load/load_arnold_standin.py index 635e86708b..3cfc5b71b3 100644 --- a/openpype/hosts/maya/plugins/load/load_arnold_standin.py +++ b/openpype/hosts/maya/plugins/load/load_arnold_standin.py @@ -25,12 +25,12 @@ def is_sequence(files): class ArnoldStandinLoader(load.LoaderPlugin): - """Load file as Arnold standin""" + """Load as Arnold standin""" - families = ["ass"] - representations = ["ass"] + families = ["ass", "animation", "model", "proxyAbc", "pointcache"] + representations = ["ass", "abc"] - label = "Load file as Arnold standin" + label = "Load as Arnold standin" order = -5 icon = "code-fork" color = "orange" @@ -75,14 +75,16 @@ class ArnoldStandinLoader(load.LoaderPlugin): cmds.parent(standin, root) # Set the standin filepath - dso_path, operator = self._setup_proxy( + path, operator = self._setup_proxy( standinShape, self.fname, namespace ) - cmds.setAttr(standinShape + ".dso", dso_path, type="string") + cmds.setAttr(standinShape + ".dso", path, type="string") sequence = is_sequence(os.listdir(os.path.dirname(self.fname))) cmds.setAttr(standinShape + ".useFrameExtension", sequence) - nodes = [root, standin, operator] + nodes = [root, standin] + if operator is not None: + nodes.append(operator) self[:] = nodes return containerise( @@ -121,7 +123,7 @@ class ArnoldStandinLoader(load.LoaderPlugin): if not os.path.exists(proxy_path): self.log.error("Proxy files do not exist. Skipping proxy setup.") - return os.path.basename(path), path + return path, None options_node = "defaultArnoldRenderOptions" merge_operator = get_attribute_input(options_node + ".operator") From 067a6c7c93ba3b55debf0a6deae39218871bee76 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 8 Feb 2023 08:19:11 +0000 Subject: [PATCH 035/119] Code cosmetics --- ...ect_ass.py => collect_arnold_scene_source.py} | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) rename openpype/hosts/maya/plugins/publish/{collect_ass.py => collect_arnold_scene_source.py} (72%) diff --git a/openpype/hosts/maya/plugins/publish/collect_ass.py b/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py similarity index 72% rename from openpype/hosts/maya/plugins/publish/collect_ass.py rename to openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py index 7b5d1a00c7..06d0786665 100644 --- a/openpype/hosts/maya/plugins/publish/collect_ass.py +++ b/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py @@ -3,16 +3,16 @@ from maya import cmds import pyblish.api -class CollectAssData(pyblish.api.InstancePlugin): - """Collect Ass data.""" +class CollectArnoldSceneSource(pyblish.api.InstancePlugin): + """Collect Arnold Scene Source data.""" # Offset to be after renderable camera collection. order = pyblish.api.CollectorOrder + 0.2 - label = 'Collect Ass' + label = "Collect Arnold Scene Source" families = ["ass"] def process(self, instance): - objsets = instance.data['setMembers'] + objsets = instance.data["setMembers"] for objset in objsets: objset = str(objset) @@ -21,11 +21,11 @@ class CollectAssData(pyblish.api.InstancePlugin): self.log.warning("Skipped empty instance: \"%s\" " % objset) continue if "content_SET" in objset: - instance.data['setMembers'] = members - self.log.debug('content members: {}'.format(members)) + instance.data["setMembers"] = members + self.log.debug("content members: {}".format(members)) elif objset.startswith("proxy_SET"): - instance.data['proxy'] = members - self.log.debug('proxy members: {}'.format(members)) + instance.data["proxy"] = members + self.log.debug("proxy members: {}".format(members)) # Use camera in object set if present else default to render globals # camera. From 7d5ede8ae51e5de7d22c8b6dfd3270638502dd98 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 8 Feb 2023 08:27:10 +0000 Subject: [PATCH 036/119] Fix creating multiple ass instances --- .../{create_ass.py => create_arnold_scene_source.py} | 10 +++++----- .../plugins/publish/collect_arnold_scene_source.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) rename openpype/hosts/maya/plugins/create/{create_ass.py => create_arnold_scene_source.py} (84%) diff --git a/openpype/hosts/maya/plugins/create/create_ass.py b/openpype/hosts/maya/plugins/create/create_arnold_scene_source.py similarity index 84% rename from openpype/hosts/maya/plugins/create/create_ass.py rename to openpype/hosts/maya/plugins/create/create_arnold_scene_source.py index 935a068ca5..2afb897e94 100644 --- a/openpype/hosts/maya/plugins/create/create_ass.py +++ b/openpype/hosts/maya/plugins/create/create_arnold_scene_source.py @@ -6,7 +6,7 @@ from openpype.hosts.maya.api import ( from maya import cmds -class CreateAss(plugin.Creator): +class CreateArnoldSceneSource(plugin.Creator): """Arnold Scene Source""" name = "ass" @@ -29,7 +29,7 @@ class CreateAss(plugin.Creator): maskOperator = False def __init__(self, *args, **kwargs): - super(CreateAss, self).__init__(*args, **kwargs) + super(CreateArnoldSceneSource, self).__init__(*args, **kwargs) # Add animation data self.data.update(lib.collect_animation_data()) @@ -52,7 +52,7 @@ class CreateAss(plugin.Creator): self.data["maskOperator"] = self.maskOperator def process(self): - instance = super(CreateAss, self).process() + instance = super(CreateArnoldSceneSource, self).process() nodes = [] @@ -61,6 +61,6 @@ class CreateAss(plugin.Creator): cmds.sets(nodes, rm=instance) - assContent = cmds.sets(name="content_SET") - assProxy = cmds.sets(name="proxy_SET", empty=True) + assContent = cmds.sets(name=instance + "_content_SET") + assProxy = cmds.sets(name=instance + "_proxy_SET", empty=True) cmds.sets([assContent, assProxy], forceElement=instance) diff --git a/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py index 06d0786665..c0275eef7b 100644 --- a/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py @@ -20,10 +20,10 @@ class CollectArnoldSceneSource(pyblish.api.InstancePlugin): if members is None: self.log.warning("Skipped empty instance: \"%s\" " % objset) continue - if "content_SET" in objset: + if objset.endswith("content_SET"): instance.data["setMembers"] = members self.log.debug("content members: {}".format(members)) - elif objset.startswith("proxy_SET"): + elif objset.endswith("proxy_SET"): instance.data["proxy"] = members self.log.debug("proxy members: {}".format(members)) From 33f2168e785206bd6eb3096fa52789f6cc7d737f Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 8 Feb 2023 08:27:17 +0000 Subject: [PATCH 037/119] Code cosmetics --- .../publish/{extract_ass.py => extract_arnold_scene_source.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename openpype/hosts/maya/plugins/publish/{extract_ass.py => extract_arnold_scene_source.py} (100%) diff --git a/openpype/hosts/maya/plugins/publish/extract_ass.py b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py similarity index 100% rename from openpype/hosts/maya/plugins/publish/extract_ass.py rename to openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py From f03cb52538b048167ce5a6f994953094b58d88c5 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 8 Feb 2023 09:40:25 +0000 Subject: [PATCH 038/119] Proxy workflow for pointcache --- .../maya/plugins/create/create_pointcache.py | 8 +++ .../maya/plugins/load/load_arnold_standin.py | 3 +- .../plugins/publish/collect_pointcache.py | 30 ++++++++++ .../plugins/publish/extract_pointcache.py | 55 +++++++++++++++++-- 4 files changed, 89 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_pointcache.py b/openpype/hosts/maya/plugins/create/create_pointcache.py index cdec140ea8..63c0490dc7 100644 --- a/openpype/hosts/maya/plugins/create/create_pointcache.py +++ b/openpype/hosts/maya/plugins/create/create_pointcache.py @@ -1,3 +1,5 @@ +from maya import cmds + from openpype.hosts.maya.api import ( lib, plugin @@ -37,3 +39,9 @@ class CreatePointCache(plugin.Creator): # Default to not send to farm. self.data["farm"] = False self.data["priority"] = 50 + + def process(self): + instance = super(CreatePointCache, self).process() + + assProxy = cmds.sets(name=instance + "_proxy_SET", empty=True) + cmds.sets(assProxy, forceElement=instance) diff --git a/openpype/hosts/maya/plugins/load/load_arnold_standin.py b/openpype/hosts/maya/plugins/load/load_arnold_standin.py index 3cfc5b71b3..e2bb89ed77 100644 --- a/openpype/hosts/maya/plugins/load/load_arnold_standin.py +++ b/openpype/hosts/maya/plugins/load/load_arnold_standin.py @@ -138,9 +138,10 @@ class ArnoldStandinLoader(load.LoaderPlugin): string_replace_operator = cmds.createNode( "aiStringReplace", name=namespace + ":string_replace_operator" ) + node_type = "alembic" if path.endswith(".abc") else "procedural" cmds.setAttr( string_replace_operator + ".selection", - "*.(@node=='procedural')", + "*.(@node=='{}')".format(node_type), type="string" ) cmds.setAttr( diff --git a/openpype/hosts/maya/plugins/publish/collect_pointcache.py b/openpype/hosts/maya/plugins/publish/collect_pointcache.py index a841341f72..332992ca92 100644 --- a/openpype/hosts/maya/plugins/publish/collect_pointcache.py +++ b/openpype/hosts/maya/plugins/publish/collect_pointcache.py @@ -1,3 +1,5 @@ +from maya import cmds + import pyblish.api @@ -12,3 +14,31 @@ class CollectPointcache(pyblish.api.InstancePlugin): def process(self, instance): if instance.data.get("farm"): instance.data["families"].append("publish.farm") + + proxy_set = None + for node in instance.data["setMembers"]: + if cmds.nodeType(node) != "objectSet": + continue + members = cmds.sets(node, query=True) + if members is None: + self.log.warning("Skipped empty objectset: \"%s\" " % node) + continue + if node.endswith("proxy_SET"): + proxy_set = node + instance.data["proxy"] = [] + instance.data["proxyRoots"] = [] + for member in members: + instance.data["proxy"].extend(cmds.ls(member, long=True)) + instance.data["proxyRoots"].extend( + cmds.ls(member, long=True) + ) + instance.data["proxy"].extend( + cmds.listRelatives(member, shapes=True, fullPath=True) + ) + self.log.debug( + "proxy members: {}".format(instance.data["proxy"]) + ) + + if proxy_set: + instance.remove(proxy_set) + instance.data["setMembers"].remove(proxy_set) diff --git a/openpype/hosts/maya/plugins/publish/extract_pointcache.py b/openpype/hosts/maya/plugins/publish/extract_pointcache.py index 7ed73fd5b0..0eb65e4226 100644 --- a/openpype/hosts/maya/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/maya/plugins/publish/extract_pointcache.py @@ -1,4 +1,5 @@ import os +import copy from maya import cmds @@ -9,6 +10,7 @@ from openpype.hosts.maya.api.lib import ( maintained_selection, iter_visible_nodes_in_range ) +from openpype.lib import StringTemplate class ExtractAlembic(publish.Extractor): @@ -23,9 +25,7 @@ class ExtractAlembic(publish.Extractor): label = "Extract Pointcache (Alembic)" hosts = ["maya"] - families = ["pointcache", - "model", - "vrayproxy"] + families = ["pointcache", "model", "vrayproxy"] targets = ["local", "remote"] def process(self, instance): @@ -87,6 +87,7 @@ class ExtractAlembic(publish.Extractor): end=end)) suspend = not instance.data.get("refresh", False) + self.log.info(nodes) with suspended_refresh(suspend=suspend): with maintained_selection(): cmds.select(nodes, noExpand=True) @@ -101,9 +102,9 @@ class ExtractAlembic(publish.Extractor): instance.data["representations"] = [] representation = { - 'name': 'abc', - 'ext': 'abc', - 'files': filename, + "name": "abc", + "ext": "abc", + "files": filename, "stagingDir": dirname } instance.data["representations"].append(representation) @@ -112,6 +113,48 @@ class ExtractAlembic(publish.Extractor): self.log.info("Extracted {} to {}".format(instance, dirname)) + # Extract proxy. + if not instance.data.get("proxy"): + return + + path = path.replace(".abc", "_proxy.abc") + if not instance.data.get("includeParentHierarchy", True): + # Set the root nodes if we don't want to include parents + # The roots are to be considered the ones that are the actual + # direct members of the set + options["root"] = instance.data["proxyRoots"] + + with suspended_refresh(suspend=suspend): + with maintained_selection(): + cmds.select(instance.data["proxy"]) + extract_alembic( + file=path, + startFrame=start, + endFrame=end, + **options + ) + + template_data = copy.deepcopy(instance.data["anatomyData"]) + template_data.update({"ext": "abc"}) + templates = instance.context.data["anatomy"].templates["publish"] + published_filename_without_extension = StringTemplate( + templates["file"] + ).format(template_data).replace(".abc", "_proxy") + transfers = [] + destination = os.path.join( + instance.data["resourcesDir"], + filename.replace( + filename.split(".")[0], + published_filename_without_extension + ) + ) + transfers.append((path, destination)) + + for source, destination in transfers: + self.log.debug("Transfer: {} > {}".format(source, destination)) + + instance.data["transfers"] = transfers + def get_members_and_roots(self, instance): return instance[:], instance.data.get("setMembers") From 713ede50049b3df473fb66d2ce8e556bce46df5c Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 8 Feb 2023 10:11:10 +0000 Subject: [PATCH 039/119] Documentation --- website/docs/artist_hosts_maya.md | 3 +++ website/docs/artist_hosts_maya_arnold.md | 16 ++++++++++++++++ website/docs/assets/maya-pointcache_setup.png | Bin 49860 -> 88602 bytes website/sidebars.js | 1 + 4 files changed, 20 insertions(+) create mode 100644 website/docs/artist_hosts_maya_arnold.md diff --git a/website/docs/artist_hosts_maya.md b/website/docs/artist_hosts_maya.md index 14619e52a1..9fab845e62 100644 --- a/website/docs/artist_hosts_maya.md +++ b/website/docs/artist_hosts_maya.md @@ -308,6 +308,8 @@ Select its root and Go **OpenPype β†’ Create...** and select **Point Cache**. After that, publishing will create corresponding **abc** files. +When creating the instance, a objectset child `proxy` will be created. Meshes in the `proxy` objectset will be the viewport representation where loading supports proxies. Proxy representations are stored as `resources` of the subset. + Example setup: ![Maya - Point Cache Example](assets/maya-pointcache_setup.png) @@ -315,6 +317,7 @@ Example setup: :::note Publish on farm If your studio has Deadline configured, artists could choose to offload potentially long running export of pointache and publish it to the farm. Only thing that is necessary is to toggle `Farm` property in created pointcache instance to True. +::: ### Loading Point Caches diff --git a/website/docs/artist_hosts_maya_arnold.md b/website/docs/artist_hosts_maya_arnold.md new file mode 100644 index 0000000000..b8b8da6d57 --- /dev/null +++ b/website/docs/artist_hosts_maya_arnold.md @@ -0,0 +1,16 @@ +--- +id: artist_hosts_maya_arnold +title: Arnold for Maya +sidebar_label: Arnold +--- +## Arnold Scene Source (.ass) +Arnold Scene Source can be published as a single file or a sequence of files, determined by the frame range. + +When creating the instance, two objectsets are created; `content` and `proxy`. Meshes in the `proxy` objectset will be the viewport representation when loading as `standin`. Proxy representations are stored as `resources` of the subset. + +## Standin +Arnold Scene Source `ass` and Alembic `abc` are supported to load as standins. + +If a subset has a proxy representation, this will be used as display in the viewport. At render time the standin path will be replaced using the recommended string replacement workflow; + +https://help.autodesk.com/view/ARNOL/ENU/?guid=arnold_for_maya_operators_am_Updating_procedural_file_paths_with_string_replace_html diff --git a/website/docs/assets/maya-pointcache_setup.png b/website/docs/assets/maya-pointcache_setup.png index 8904baa239f90f342f7252f5732ccf57dccf2ad7..b2dc12690199c264e6a8ceebbbff4308e08764be 100644 GIT binary patch literal 88602 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%0DJ*m$sy4TvzuBkclL5Ev|*snH)H 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 Zt#D4oNKPwk<*K!O4Q;!AW?@DqLa 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 Date: Wed, 8 Feb 2023 19:11:20 +0100 Subject: [PATCH 040/119] Abstract get_representation_path function and use it on shotgrid to fix remote errors with data instances not having 'published_path' --- .../publish/integrate_ftrack_instances.py | 52 ++----------------- .../publish/integrate_shotgrid_publish.py | 4 +- .../publish/integrate_shotgrid_version.py | 48 ++++++++++++----- .../plugins/publish/integrate_slack_api.py | 11 ++-- openpype/plugins/publish/integrate.py | 46 ++++++++++++++++ 5 files changed, 92 insertions(+), 69 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py index 2d06e2ab02..c3baecec67 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py @@ -10,6 +10,7 @@ from openpype.lib.transcoding import ( ) from openpype.lib.profiles_filtering import filter_profiles from openpype.lib.transcoding import VIDEO_EXTENSIONS +from openpype.plugins.publish.integrate import get_representation_path class IntegrateFtrackInstance(pyblish.api.InstancePlugin): @@ -153,7 +154,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): if not review_representations or has_movie_review: for repre in thumbnail_representations: - repre_path = self._get_repre_path(instance, repre, False) + repre_path = get_representation_path(instance, repre, False) if not repre_path: self.log.warning( "Published path is not set and source was removed." @@ -210,7 +211,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): "from {}".format(repre)) continue - repre_path = self._get_repre_path(instance, repre, False) + repre_path = get_representation_path(instance, repre, False) if not repre_path: self.log.warning( "Published path is not set and source was removed." @@ -324,7 +325,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): # Add others representations as component for repre in other_representations: - published_path = self._get_repre_path(instance, repre, True) + published_path = get_representation_path(instance, repre, True) if not published_path: continue # Create copy of base comp item and append it @@ -364,51 +365,6 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): def _collect_additional_metadata(self, streams): pass - def _get_repre_path(self, instance, repre, only_published): - """Get representation path that can be used for integration. - - When 'only_published' is set to true the validation of path is not - relevant. In that case we just need what is set in 'published_path' - as "reference". The reference is not used to get or upload the file but - for reference where the file was published. - - Args: - instance (pyblish.Instance): Processed instance object. Used - for source of staging dir if representation does not have - filled it. - repre (dict): Representation on instance which could be and - could not be integrated with main integrator. - only_published (bool): Care only about published paths and - ignore if filepath is not existing anymore. - - Returns: - str: Path to representation file. - None: Path is not filled or does not exists. - """ - - published_path = repre.get("published_path") - if published_path: - published_path = os.path.normpath(published_path) - if os.path.exists(published_path): - return published_path - - if only_published: - return published_path - - comp_files = repre["files"] - if isinstance(comp_files, (tuple, list, set)): - filename = comp_files[0] - else: - filename = comp_files - - staging_dir = repre.get("stagingDir") - if not staging_dir: - staging_dir = instance.data["stagingDir"] - src_path = os.path.normpath(os.path.join(staging_dir, filename)) - if os.path.exists(src_path): - return src_path - return None - def _get_asset_version_status_name(self, instance): if not self.asset_versions_status_profiles: return None diff --git a/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_publish.py b/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_publish.py index cfd2d10fd9..ee6ece2e67 100644 --- a/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_publish.py +++ b/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_publish.py @@ -1,6 +1,8 @@ import os import pyblish.api +from openpype.plugins.publish.integrate import get_representation_path + class IntegrateShotgridPublish(pyblish.api.InstancePlugin): """ @@ -22,7 +24,7 @@ class IntegrateShotgridPublish(pyblish.api.InstancePlugin): for representation in instance.data.get("representations", []): - local_path = representation.get("published_path") + local_path = get_representation_path(instance, representation, False) code = os.path.basename(local_path) if representation.get("tags", []): diff --git a/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_version.py b/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_version.py index a1b7140e22..60ad1ff91d 100644 --- a/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_version.py +++ b/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_version.py @@ -1,6 +1,7 @@ -import os import pyblish.api +from openpype.plugins.publish.integrate import get_representation_path + class IntegrateShotgridVersion(pyblish.api.InstancePlugin): """Integrate Shotgrid Version""" @@ -17,15 +18,37 @@ class IntegrateShotgridVersion(pyblish.api.InstancePlugin): # TODO: Use path template solver to build version code from settings anatomy = instance.data.get("anatomyData", {}) - code = "_".join( - [ - anatomy["project"]["code"], - anatomy["parent"], - anatomy["asset"], - anatomy["task"]["name"], - "v{:03}".format(int(anatomy["version"])), - ] - ) + ### Starts Alkemy-X Override ### + # code = "_".join( + # [ + # anatomy["project"]["code"], + # anatomy["parent"], + # anatomy["asset"], + # anatomy["task"]["name"], + # "v{:03}".format(int(anatomy["version"])), + # ] + # ) + # Initial editorial Shotgrid versions don't need task in name + if anatomy["app"] == "hiero": + code = "_".join( + [ + anatomy["project"]["code"], + anatomy["parent"], + anatomy["asset"], + "v{:03}".format(int(anatomy["version"])), + ] + ) + else: + code = "_".join( + [ + anatomy["project"]["code"], + anatomy["parent"], + anatomy["asset"], + anatomy["task"]["name"], + "v{:03}".format(int(anatomy["version"])), + ] + ) + ### Ends Alkemy-X Override ### version = self._find_existing_version(code, context) @@ -41,8 +64,9 @@ class IntegrateShotgridVersion(pyblish.api.InstancePlugin): data_to_update["sg_status_list"] = status for representation in instance.data.get("representations", []): - local_path = representation.get("published_path") - code = os.path.basename(local_path) + # Get representation path from published_path or create it from stagingDir if not existent + local_path = get_representation_path(instance, representation, False) + self.log.info("Local path: %s", local_path) if "shotgridreview" in representation.get("tags", []): diff --git a/openpype/modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py index 612031efac..ac918381c0 100644 --- a/openpype/modules/slack/plugins/publish/integrate_slack_api.py +++ b/openpype/modules/slack/plugins/publish/integrate_slack_api.py @@ -9,6 +9,7 @@ import time from openpype.client import OpenPypeMongoConnection from openpype.lib.plugin_tools import prepare_template_data +from openpype.plugins.publish.integrate import get_representation_path class IntegrateSlackAPI(pyblish.api.InstancePlugin): @@ -167,10 +168,7 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): thumbnail_path = None for repre in instance.data.get("representations", []): if repre.get('thumbnail') or "thumbnail" in repre.get('tags', []): - repre_thumbnail_path = ( - repre.get("published_path") or - os.path.join(repre["stagingDir"], repre["files"]) - ) + repre_thumbnail_path = get_representation_path(instance, repre, False) if os.path.exists(repre_thumbnail_path): thumbnail_path = repre_thumbnail_path break @@ -184,10 +182,7 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): if (repre.get("review") or "review" in tags or "burnin" in tags): - repre_review_path = ( - repre.get("published_path") or - os.path.join(repre["stagingDir"], repre["files"]) - ) + repre_review_path = get_representation_path(instance, repre, False) if os.path.exists(repre_review_path): review_path = repre_review_path if "burnin" in tags: # burnin has precedence if exists diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 7b73943c37..854cf8b9ec 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -53,6 +53,52 @@ def get_frame_padded(frame, padding): return "{frame:0{padding}d}".format(padding=padding, frame=frame) +def get_representation_path(instance, repre, only_published): + """Get representation path that can be used for integration. + + When 'only_published' is set to true the validation of path is not + relevant. In that case we just need what is set in 'published_path' + as "reference". The reference is not used to get or upload the file but + for reference where the file was published. + + Args: + instance (pyblish.Instance): Processed instance object. Used + for source of staging dir if representation does not have + filled it. + repre (dict): Representation on instance which could be and + could not be integrated with main integrator. + only_published (bool): Care only about published paths and + ignore if filepath is not existing anymore. + + Returns: + str: Path to representation file. + None: Path is not filled or does not exists. + """ + + published_path = repre.get("published_path") + if published_path: + published_path = os.path.normpath(published_path) + if os.path.exists(published_path): + return published_path + + if only_published: + return published_path + + comp_files = repre["files"] + if isinstance(comp_files, (tuple, list, set)): + filename = comp_files[0] + else: + filename = comp_files + + staging_dir = repre.get("stagingDir") + if not staging_dir: + staging_dir = instance.data["stagingDir"] + src_path = os.path.normpath(os.path.join(staging_dir, filename)) + if os.path.exists(src_path): + return src_path + return None + + class IntegrateAsset(pyblish.api.InstancePlugin): """Register publish in the database and transfer files to destinations. From c13416b68570365b9ce3b247979f79f721cc3031 Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Wed, 8 Feb 2023 19:39:47 +0100 Subject: [PATCH 041/119] Remove unintended code block and fix Hound lint warnings for <79 chars width --- .../publish/integrate_shotgrid_publish.py | 4 +- .../publish/integrate_shotgrid_version.py | 46 +++++-------------- .../plugins/publish/integrate_slack_api.py | 8 +++- 3 files changed, 21 insertions(+), 37 deletions(-) diff --git a/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_publish.py b/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_publish.py index ee6ece2e67..7789a47074 100644 --- a/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_publish.py +++ b/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_publish.py @@ -24,7 +24,9 @@ class IntegrateShotgridPublish(pyblish.api.InstancePlugin): for representation in instance.data.get("representations", []): - local_path = get_representation_path(instance, representation, False) + local_path = get_representation_path( + instance, representation, False + ) code = os.path.basename(local_path) if representation.get("tags", []): diff --git a/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_version.py b/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_version.py index 60ad1ff91d..94fc4ae9e8 100644 --- a/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_version.py +++ b/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_version.py @@ -18,37 +18,15 @@ class IntegrateShotgridVersion(pyblish.api.InstancePlugin): # TODO: Use path template solver to build version code from settings anatomy = instance.data.get("anatomyData", {}) - ### Starts Alkemy-X Override ### - # code = "_".join( - # [ - # anatomy["project"]["code"], - # anatomy["parent"], - # anatomy["asset"], - # anatomy["task"]["name"], - # "v{:03}".format(int(anatomy["version"])), - # ] - # ) - # Initial editorial Shotgrid versions don't need task in name - if anatomy["app"] == "hiero": - code = "_".join( - [ - anatomy["project"]["code"], - anatomy["parent"], - anatomy["asset"], - "v{:03}".format(int(anatomy["version"])), - ] - ) - else: - code = "_".join( - [ - anatomy["project"]["code"], - anatomy["parent"], - anatomy["asset"], - anatomy["task"]["name"], - "v{:03}".format(int(anatomy["version"])), - ] - ) - ### Ends Alkemy-X Override ### + code = "_".join( + [ + anatomy["project"]["code"], + anatomy["parent"], + anatomy["asset"], + anatomy["task"]["name"], + "v{:03}".format(int(anatomy["version"])), + ] + ) version = self._find_existing_version(code, context) @@ -64,9 +42,9 @@ class IntegrateShotgridVersion(pyblish.api.InstancePlugin): data_to_update["sg_status_list"] = status for representation in instance.data.get("representations", []): - # Get representation path from published_path or create it from stagingDir if not existent - local_path = get_representation_path(instance, representation, False) - self.log.info("Local path: %s", local_path) + local_path = get_representation_path( + instance, representation, False + ) if "shotgridreview" in representation.get("tags", []): diff --git a/openpype/modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py index ac918381c0..d486b2179a 100644 --- a/openpype/modules/slack/plugins/publish/integrate_slack_api.py +++ b/openpype/modules/slack/plugins/publish/integrate_slack_api.py @@ -168,7 +168,9 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): thumbnail_path = None for repre in instance.data.get("representations", []): if repre.get('thumbnail') or "thumbnail" in repre.get('tags', []): - repre_thumbnail_path = get_representation_path(instance, repre, False) + repre_thumbnail_path = get_representation_path( + instance, repre, False + ) if os.path.exists(repre_thumbnail_path): thumbnail_path = repre_thumbnail_path break @@ -182,7 +184,9 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): if (repre.get("review") or "review" in tags or "burnin" in tags): - repre_review_path = get_representation_path(instance, repre, False) + repre_review_path = get_representation_path( + instance, repre, False + ) if os.path.exists(repre_review_path): review_path = repre_review_path if "burnin" in tags: # burnin has precedence if exists From 27150e4abb148e7dfc4eb673ea4affa9c080f55f Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Wed, 8 Feb 2023 20:23:26 +0100 Subject: [PATCH 042/119] Address feedback on PR and move function to pipeline.publish.lib --- .../publish/integrate_ftrack_instances.py | 8 ++-- .../publish/integrate_shotgrid_publish.py | 4 +- .../publish/integrate_shotgrid_version.py | 4 +- .../plugins/publish/integrate_slack_api.py | 6 +-- openpype/pipeline/publish/__init__.py | 2 + openpype/pipeline/publish/lib.py | 46 +++++++++++++++++++ openpype/plugins/publish/integrate.py | 46 ------------------- 7 files changed, 59 insertions(+), 57 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py index c3baecec67..d6cb3daf0d 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py @@ -3,6 +3,7 @@ import json import copy import pyblish.api +from openpype.pipeline.publish import get_publish_repre_path from openpype.lib.openpype_version import get_openpype_version from openpype.lib.transcoding import ( get_ffprobe_streams, @@ -10,7 +11,6 @@ from openpype.lib.transcoding import ( ) from openpype.lib.profiles_filtering import filter_profiles from openpype.lib.transcoding import VIDEO_EXTENSIONS -from openpype.plugins.publish.integrate import get_representation_path class IntegrateFtrackInstance(pyblish.api.InstancePlugin): @@ -154,7 +154,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): if not review_representations or has_movie_review: for repre in thumbnail_representations: - repre_path = get_representation_path(instance, repre, False) + repre_path = get_publish_repre_path(instance, repre, False) if not repre_path: self.log.warning( "Published path is not set and source was removed." @@ -211,7 +211,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): "from {}".format(repre)) continue - repre_path = get_representation_path(instance, repre, False) + repre_path = get_publish_repre_path(instance, repre, False) if not repre_path: self.log.warning( "Published path is not set and source was removed." @@ -325,7 +325,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): # Add others representations as component for repre in other_representations: - published_path = get_representation_path(instance, repre, True) + published_path = get_publish_repre_path(instance, repre, True) if not published_path: continue # Create copy of base comp item and append it diff --git a/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_publish.py b/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_publish.py index 7789a47074..fc15d5515f 100644 --- a/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_publish.py +++ b/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_publish.py @@ -1,7 +1,7 @@ import os import pyblish.api -from openpype.plugins.publish.integrate import get_representation_path +from openpype.pipeline.publish import get_publish_repre_path class IntegrateShotgridPublish(pyblish.api.InstancePlugin): @@ -24,7 +24,7 @@ class IntegrateShotgridPublish(pyblish.api.InstancePlugin): for representation in instance.data.get("representations", []): - local_path = get_representation_path( + local_path = get_publish_repre_path( instance, representation, False ) code = os.path.basename(local_path) diff --git a/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_version.py b/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_version.py index 94fc4ae9e8..adfdca718c 100644 --- a/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_version.py +++ b/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_version.py @@ -1,6 +1,6 @@ import pyblish.api -from openpype.plugins.publish.integrate import get_representation_path +from openpype.pipeline.publish import get_publish_repre_path class IntegrateShotgridVersion(pyblish.api.InstancePlugin): @@ -42,7 +42,7 @@ class IntegrateShotgridVersion(pyblish.api.InstancePlugin): data_to_update["sg_status_list"] = status for representation in instance.data.get("representations", []): - local_path = get_representation_path( + local_path = get_publish_repre_path( instance, representation, False ) diff --git a/openpype/modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py index d486b2179a..4e2557ccc7 100644 --- a/openpype/modules/slack/plugins/publish/integrate_slack_api.py +++ b/openpype/modules/slack/plugins/publish/integrate_slack_api.py @@ -8,8 +8,8 @@ from abc import ABCMeta, abstractmethod import time from openpype.client import OpenPypeMongoConnection +from openpype.pipeline.publish import get_publish_repre_path from openpype.lib.plugin_tools import prepare_template_data -from openpype.plugins.publish.integrate import get_representation_path class IntegrateSlackAPI(pyblish.api.InstancePlugin): @@ -168,7 +168,7 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): thumbnail_path = None for repre in instance.data.get("representations", []): if repre.get('thumbnail') or "thumbnail" in repre.get('tags', []): - repre_thumbnail_path = get_representation_path( + repre_thumbnail_path = get_publish_repre_path( instance, repre, False ) if os.path.exists(repre_thumbnail_path): @@ -184,7 +184,7 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): if (repre.get("review") or "review" in tags or "burnin" in tags): - repre_review_path = get_representation_path( + repre_review_path = get_publish_repre_path( instance, repre, False ) if os.path.exists(repre_review_path): diff --git a/openpype/pipeline/publish/__init__.py b/openpype/pipeline/publish/__init__.py index dc6fc0f97a..5be973ad86 100644 --- a/openpype/pipeline/publish/__init__.py +++ b/openpype/pipeline/publish/__init__.py @@ -36,6 +36,7 @@ from .lib import ( filter_instances_for_context_plugin, context_plugin_should_run, get_instance_staging_dir, + get_publish_repre_path, ) from .abstract_expected_files import ExpectedFiles @@ -79,6 +80,7 @@ __all__ = ( "filter_instances_for_context_plugin", "context_plugin_should_run", "get_instance_staging_dir", + "get_publish_repre_path", "ExpectedFiles", diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index c76671fa39..e206c4552c 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -632,3 +632,49 @@ def get_instance_staging_dir(instance): instance.data["stagingDir"] = staging_dir return staging_dir + + +def get_publish_repre_path(instance, repre, only_published): + """Get representation path that can be used for integration. + + When 'only_published' is set to true the validation of path is not + relevant. In that case we just need what is set in 'published_path' + as "reference". The reference is not used to get or upload the file but + for reference where the file was published. + + Args: + instance (pyblish.Instance): Processed instance object. Used + for source of staging dir if representation does not have + filled it. + repre (dict): Representation on instance which could be and + could not be integrated with main integrator. + only_published (bool): Care only about published paths and + ignore if filepath is not existing anymore. + + Returns: + str: Path to representation file. + None: Path is not filled or does not exists. + """ + + published_path = repre.get("published_path") + if published_path: + published_path = os.path.normpath(published_path) + if os.path.exists(published_path): + return published_path + + if only_published: + return published_path + + comp_files = repre["files"] + if isinstance(comp_files, (tuple, list, set)): + filename = comp_files[0] + else: + filename = comp_files + + staging_dir = repre.get("stagingDir") + if not staging_dir: + staging_dir = get_instance_staging_dir(instance) + src_path = os.path.normpath(os.path.join(staging_dir, filename)) + if os.path.exists(src_path): + return src_path + return None diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 854cf8b9ec..7b73943c37 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -53,52 +53,6 @@ def get_frame_padded(frame, padding): return "{frame:0{padding}d}".format(padding=padding, frame=frame) -def get_representation_path(instance, repre, only_published): - """Get representation path that can be used for integration. - - When 'only_published' is set to true the validation of path is not - relevant. In that case we just need what is set in 'published_path' - as "reference". The reference is not used to get or upload the file but - for reference where the file was published. - - Args: - instance (pyblish.Instance): Processed instance object. Used - for source of staging dir if representation does not have - filled it. - repre (dict): Representation on instance which could be and - could not be integrated with main integrator. - only_published (bool): Care only about published paths and - ignore if filepath is not existing anymore. - - Returns: - str: Path to representation file. - None: Path is not filled or does not exists. - """ - - published_path = repre.get("published_path") - if published_path: - published_path = os.path.normpath(published_path) - if os.path.exists(published_path): - return published_path - - if only_published: - return published_path - - comp_files = repre["files"] - if isinstance(comp_files, (tuple, list, set)): - filename = comp_files[0] - else: - filename = comp_files - - staging_dir = repre.get("stagingDir") - if not staging_dir: - staging_dir = instance.data["stagingDir"] - src_path = os.path.normpath(os.path.join(staging_dir, filename)) - if os.path.exists(src_path): - return src_path - return None - - class IntegrateAsset(pyblish.api.InstancePlugin): """Register publish in the database and transfer files to destinations. From 15d7a7589fa119010510232e3ae243f0e6834383 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 9 Feb 2023 12:04:06 +0100 Subject: [PATCH 043/119] fix context collection from create context --- .../publish/collect_from_create_context.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/openpype/plugins/publish/collect_from_create_context.py b/openpype/plugins/publish/collect_from_create_context.py index d3398c885e..5fcf8feb56 100644 --- a/openpype/plugins/publish/collect_from_create_context.py +++ b/openpype/plugins/publish/collect_from_create_context.py @@ -32,7 +32,7 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin): thumbnail_paths_by_instance_id.get(None) ) - project_name = create_context.project_name + project_name = create_context.get_current_project_name() if project_name: context.data["projectName"] = project_name @@ -53,11 +53,15 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin): context.data.update(create_context.context_data_to_store()) context.data["newPublishing"] = True # Update context data - for key in ("AVALON_PROJECT", "AVALON_ASSET", "AVALON_TASK"): - value = create_context.dbcon.Session.get(key) - if value is not None: - legacy_io.Session[key] = value - os.environ[key] = value + asset_name = create_context.get_current_asset_name() + task_name = create_context.get_current_task_name() + for key, value in ( + ("AVALON_PROJECT", project_name), + ("AVALON_ASSET", asset_name), + ("AVALON_TASK", task_name) + ): + legacy_io.Session[key] = value + os.environ[key] = value def create_instance( self, From b148dec04843137622960b50c484d94ad9b0e82b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 9 Feb 2023 12:58:17 +0100 Subject: [PATCH 044/119] Added helper method to return unified information on create error --- openpype/pipeline/create/context.py | 70 +++++++++++++++++------------ 1 file changed, 41 insertions(+), 29 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 190d542724..dfe60d438b 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1538,6 +1538,44 @@ class CreateContext: pre_create_data ) + def _create_with_unified_error( + self, identifier, creator, *args, **kwargs + ): + error_message = "Failed to run Creator with identifier \"{}\". {}" + + label = None + add_traceback = False + result = None + fail_info = None + success = False + + try: + # Try to get creator and his label + if creator is None: + creator = self._get_creator_in_create(identifier) + label = getattr(creator, "label", label) + + # Run create + result = creator.create(*args, **kwargs) + success = True + + except CreatorError: + exc_info = sys.exc_info() + self.log.warning(error_message.format(identifier, exc_info[1])) + + except: + add_traceback = True + exc_info = sys.exc_info() + self.log.warning( + error_message.format(identifier, ""), + exc_info=True + ) + + if not success: + fail_info = prepare_failed_creator_operation_info( + identifier, label, exc_info, add_traceback + ) + return result, fail_info def creator_removed_instance(self, instance): """When creator removes instance context should be acknowledged. @@ -1663,37 +1701,11 @@ class CreateContext: Reset instances if any autocreator executed properly. """ - error_message = "Failed to run AutoCreator with identifier \"{}\". {}" failed_info = [] for identifier, creator in self.autocreators.items(): - label = creator.label - failed = False - add_traceback = False - try: - creator.create() - - except CreatorError: - failed = True - exc_info = sys.exc_info() - self.log.warning(error_message.format(identifier, exc_info[1])) - - # Use bare except because some hosts raise their exceptions that - # do not inherit from python's `BaseException` - except: - failed = True - add_traceback = True - exc_info = sys.exc_info() - self.log.warning( - error_message.format(identifier, ""), - exc_info=True - ) - - if failed: - failed_info.append( - prepare_failed_creator_operation_info( - identifier, label, exc_info, add_traceback - ) - ) + _, fail_info = self._create_with_unified_error(identifier, creator) + if fail_info is not None: + failed_info.append(fail_info) if failed_info: raise CreatorsCreateFailed(failed_info) From fac10d26337157bc253a1af90ae0a1040ff49533 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 9 Feb 2023 12:58:51 +0100 Subject: [PATCH 045/119] added public method 'create_with_unified_error' used in publisher --- openpype/pipeline/create/context.py | 25 +++++++++++++++++++++++++ openpype/tools/publisher/control.py | 3 ++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index dfe60d438b..2a92d21225 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1576,6 +1576,31 @@ class CreateContext: identifier, label, exc_info, add_traceback ) return result, fail_info + + def create_with_unified_error(self, identifier, *args, **kwargs): + """Trigger create but raise only one error if anything fails. + + Added to raise unified exception. Capture any possible issues and + reraise it with unified information. + + Args: + identifier (str): Identifier of creator. + *args (Tuple[Any]): Arguments for create method. + **kwargs (Dict[Any, Any]): Keyword argument for create method. + + Raises: + CreatorsCreateFailed: When creation fails due to any possible + reason. If anything goes wrong this is only possible exception + the method should raise. + """ + + result, fail_info = self._create_with_unified_error( + identifier, None, *args, **kwargs + ) + if fail_info is not None: + raise CreatorsCreateFailed([fail_info]) + return result + def creator_removed_instance(self, instance): """When creator removes instance context should be acknowledged. diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 670c22a43e..11215b5ff8 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -2017,9 +2017,10 @@ class PublisherController(BasePublisherController): success = True try: - self._create_context.raw_create( + self._create_context.create_with_unified_error( creator_identifier, subset_name, instance_data, options ) + except CreatorsOperationFailed as exc: success = False self._emit_event( From cb84cf769eea9d5a018ef67dd6e4cb0d6d7276b8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 9 Feb 2023 13:00:28 +0100 Subject: [PATCH 046/119] 'create' method is not triggering 'raw_create' --- openpype/pipeline/create/context.py | 34 ++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 2a92d21225..3287141970 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1415,6 +1415,30 @@ class CreateContext: with self.bulk_instances_collection(): self._bulk_instances_to_process.append(instance) + def _get_creator_in_create(self, identifier): + """Creator by identifier with unified error. + + Helper method to get creator by identifier with same error when creator + is not available. + + Args: + identifier (str): Identifier of creator plugin. + + Returns: + BaseCreator: Creator found by identifier. + + Raises: + CreatorError: When identifier is not known. + """ + + creator = self.creators.get(identifier) + # Fake CreatorError (Could be maybe specific exception?) + if creator is None: + raise CreatorError( + "Creator {} was not found".format(identifier) + ) + return creator + def raw_create(self, identifier, *args, **kwargs): """Wrapper for creators to trigger 'create' method. @@ -1497,14 +1521,9 @@ class CreateContext: Raises: CreatorError: If creator was not found or asset is empty. - CreatorsCreateFailed: When creation fails. """ - creator = self.creators.get(creator_identifier) - if creator is None: - raise CreatorError( - "Creator {} was not found".format(creator_identifier) - ) + creator = self._get_creator_in_create(creator_identifier) project_name = self.project_name if asset_doc is None: @@ -1531,8 +1550,7 @@ class CreateContext: "task": task_name, "variant": variant } - return self.raw_create( - creator_identifier, + return creator.create( subset_name, instance_data, pre_create_data From 0cb78a10e6cd7b0c7307c70be1827e2e8c1d1f2e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 9 Feb 2023 13:00:49 +0100 Subject: [PATCH 047/119] removed unused 'raw_create' method --- openpype/pipeline/create/context.py | 53 ----------------------------- 1 file changed, 53 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 3287141970..078c50acc2 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1439,59 +1439,6 @@ class CreateContext: ) return creator - def raw_create(self, identifier, *args, **kwargs): - """Wrapper for creators to trigger 'create' method. - - Different types of creators may expect different arguments thus the - hints for args are blind. - - Args: - identifier (str): Creator's identifier. - *args (Tuple[Any]): Arguments for create method. - **kwargs (Dict[Any, Any]): Keyword argument for create method. - - Raises: - CreatorsCreateFailed: When creation fails. - """ - - error_message = "Failed to run Creator with identifier \"{}\". {}" - creator = self.creators.get(identifier) - label = getattr(creator, "label", None) - failed = False - add_traceback = False - exc_info = None - result = None - try: - # Fake CreatorError (Could be maybe specific exception?) - if creator is None: - raise CreatorError( - "Creator {} was not found".format(identifier) - ) - - result = creator.create(*args, **kwargs) - - except CreatorError: - failed = True - exc_info = sys.exc_info() - self.log.warning(error_message.format(identifier, exc_info[1])) - - except: - failed = True - add_traceback = True - exc_info = sys.exc_info() - self.log.warning( - error_message.format(identifier, ""), - exc_info=True - ) - - if failed: - raise CreatorsCreateFailed([ - prepare_failed_creator_operation_info( - identifier, label, exc_info, add_traceback - ) - ]) - return result - def create( self, creator_identifier, From a3c9f792c81ac6276594f73d6dc5152bce0b12cd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 9 Feb 2023 14:29:20 +0100 Subject: [PATCH 048/119] refactor tempdir creator function wip --- openpype/pipeline/publish/lib.py | 67 ++++++-------------------------- openpype/pipeline/tempdir.py | 62 +++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 56 deletions(-) create mode 100644 openpype/pipeline/tempdir.py diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index b3d273781e..380f0df91a 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -12,13 +12,15 @@ import pyblish.api from openpype.lib import ( Logger, - filter_profiles, - StringTemplate + filter_profiles ) from openpype.settings import ( get_project_settings, get_system_settings, ) +from openpype.pipeline import ( + tempdir +) from .contants import ( DEFAULT_PUBLISH_TEMPLATE, @@ -645,24 +647,12 @@ def get_instance_staging_dir(instance): if staging_dir: return staging_dir - openpype_temp_dir = os.getenv("OPENPYPE_TMPDIR") - custom_temp_dir = None - if openpype_temp_dir: - if "{" in openpype_temp_dir: - # path is anatomy template - custom_temp_dir = _format_staging_dir( - instance, openpype_temp_dir - ) - else: - # path is absolute - custom_temp_dir = openpype_temp_dir - - if not os.path.exists(custom_temp_dir): - try: - # create it if it doesnt exists - os.makedirs(custom_temp_dir) - except IOError as error: - raise IOError("Path couldn't be created: {}".format(error)) + anatomy_data = instance.data.get("anatomy_data") + project_name = + # get customized tempdir path from `OPENPYPE_TEMPDIR` env var + custom_temp_dir = tempdir.create_custom_tempdir( + instance.data["anatomy_data"]["project"]["name"] + ) if custom_temp_dir: staging_dir = os.path.normpath( @@ -677,39 +667,4 @@ def get_instance_staging_dir(instance): ) instance.data['stagingDir'] = staging_dir - return staging_dir - - -def _format_staging_dir(instance, openpype_temp_dir): - """ Formating template - - Template path formatting is supporting: - - optional key formating - - available keys: - - root[work | ] - - project[name | code] - - asset - - hierarchy - - task - - username - - app - - Args: - instance (pyblish.Instance): instance object - openpype_temp_dir (str): path string - - Returns: - str: formated path - """ - anatomy = instance.context.data["anatomy"] - # get anatomy formating data - # so template formating is supported - anatomy_data = copy.deepcopy(instance.context.data["anatomyData"]) - anatomy_data["root"] = anatomy.roots - - result = StringTemplate.format_template( - openpype_temp_dir, anatomy_data).normalized() - - # create the dir in case it doesnt exists - os.makedirs(os.path.dirname(result)) - return result + return staging_dir \ No newline at end of file diff --git a/openpype/pipeline/tempdir.py b/openpype/pipeline/tempdir.py new file mode 100644 index 0000000000..c73fce2e9a --- /dev/null +++ b/openpype/pipeline/tempdir.py @@ -0,0 +1,62 @@ +import os + +from openpype.lib import ( + Anatomy, + StringTemplate +) + +def create_custom_tempdir(project_name, anatomy=None, formating_data=None): + """ Create custom tempdir + + Template path formatting is supporting: + - optional key formating + - available keys: + - root[work | ] + - project[name | code] + + Args: + instance (pyblish.Instance): instance object + openpype_temp_dir (str): path string + + Returns: + str: formated path + """ + openpype_tempdir = os.getenv("OPENPYPE_TMPDIR") + if not openpype_tempdir: + return + + custom_tempdir = None + if "{" in openpype_tempdir: + if anatomy is None: + anatomy = Anatomy(project_name) + # create base formate data + data = { + "root": anatomy.roots + } + if formating_data is None: + # We still don't have `project_code` on Anatomy... + project_doc = anatomy.get_project_doc_from_cache(project_name) + data["project"] = { + "name": project_name, + "code": project_doc["data"]["code"], + } + else: + data["project"] = formating_data["project"] + + # path is anatomy template + custom_tempdir = StringTemplate.format_template( + openpype_tempdir, data).normalized() + + else: + # path is absolute + custom_tempdir = openpype_tempdir + + # create he dir path if it doesnt exists + if not os.path.exists(custom_tempdir): + try: + # create it if it doesnt exists + os.makedirs(custom_tempdir) + except IOError as error: + raise IOError("Path couldn't be created: {}".format(error)) + + return custom_tempdir From 304d7584042454193be5b1c85f0dc87e06c1d4c3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 9 Feb 2023 14:40:40 +0100 Subject: [PATCH 049/119] renaming module for temporarydir, fixing docstring --- openpype/pipeline/publish/lib.py | 19 ++++++++++--------- .../pipeline/{tempdir.py => temporarydir.py} | 19 +++++++++++-------- 2 files changed, 21 insertions(+), 17 deletions(-) rename openpype/pipeline/{tempdir.py => temporarydir.py} (82%) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 380f0df91a..c6c8b71b24 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -19,7 +19,7 @@ from openpype.settings import ( get_system_settings, ) from openpype.pipeline import ( - tempdir + temporarydir ) from .contants import ( @@ -626,11 +626,6 @@ def get_instance_staging_dir(instance): Available anatomy formatting keys: - root[work | ] - project[name | code] - - asset - - hierarchy - - task - - username - - app Note: Staging dir does not have to be necessarily in tempdir so be carefull @@ -648,10 +643,16 @@ def get_instance_staging_dir(instance): return staging_dir anatomy_data = instance.data.get("anatomy_data") - project_name = + anatomy = instance.data.get("anatomy") + + if anatomy_data: + project_name = anatomy_data["project"]["name"] + else: + project_name = os.getenv("AVALON_PROJECT") + # get customized tempdir path from `OPENPYPE_TEMPDIR` env var - custom_temp_dir = tempdir.create_custom_tempdir( - instance.data["anatomy_data"]["project"]["name"] + custom_temp_dir = temporarydir.create_custom_tempdir( + project_name, anatomy=anatomy, formating_data=anatomy_data ) if custom_temp_dir: diff --git a/openpype/pipeline/tempdir.py b/openpype/pipeline/temporarydir.py similarity index 82% rename from openpype/pipeline/tempdir.py rename to openpype/pipeline/temporarydir.py index c73fce2e9a..31586d82c8 100644 --- a/openpype/pipeline/tempdir.py +++ b/openpype/pipeline/temporarydir.py @@ -1,9 +1,11 @@ -import os +""" +Temporary folder operations +""" + +import os +from openpype.lib import StringTemplate +from openpype.pipeline import Anatomy -from openpype.lib import ( - Anatomy, - StringTemplate -) def create_custom_tempdir(project_name, anatomy=None, formating_data=None): """ Create custom tempdir @@ -15,11 +17,12 @@ def create_custom_tempdir(project_name, anatomy=None, formating_data=None): - project[name | code] Args: - instance (pyblish.Instance): instance object - openpype_temp_dir (str): path string + project_name (str): name of project + anatomy (openpype.pipeline.Anatomy): Anatomy object + formating_data (dict): formating data used for filling template. Returns: - str: formated path + bool | str: formated path or None """ openpype_tempdir = os.getenv("OPENPYPE_TMPDIR") if not openpype_tempdir: From 171b9bc3dbc079d08248cc2cafa40342b9bcd762 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Thu, 9 Feb 2023 05:49:02 -0800 Subject: [PATCH 050/119] Make only_published argument optional Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/pipeline/publish/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index e206c4552c..92c43c99e8 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -634,7 +634,7 @@ def get_instance_staging_dir(instance): return staging_dir -def get_publish_repre_path(instance, repre, only_published): +def get_publish_repre_path(instance, repre, only_published=False): """Get representation path that can be used for integration. When 'only_published' is set to true the validation of path is not From 5de967b6447fb2b7f7dc84ad84704857afa35af8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 9 Feb 2023 15:07:40 +0100 Subject: [PATCH 051/119] updated documentation --- openpype/pipeline/publish/lib.py | 2 +- website/docs/admin_environment.md | 30 +++++++++++++++++++++++++++ website/docs/admin_settings_system.md | 29 ++++---------------------- website/sidebars.js | 1 + 4 files changed, 36 insertions(+), 26 deletions(-) create mode 100644 website/docs/admin_environment.md diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index c6c8b71b24..aaa2dd444a 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -668,4 +668,4 @@ def get_instance_staging_dir(instance): ) instance.data['stagingDir'] = staging_dir - return staging_dir \ No newline at end of file + return staging_dir diff --git a/website/docs/admin_environment.md b/website/docs/admin_environment.md new file mode 100644 index 0000000000..2cc558b530 --- /dev/null +++ b/website/docs/admin_environment.md @@ -0,0 +1,30 @@ +--- +id: admin_environment +title: Environment +sidebar_label: Environment +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## OPENPYPE_TMPDIR: + - Custom staging dir directory + - Supports anatomy keys formating. ex `{root[work]}/{project[name]}/temp` + - supported formating keys: + - root[work] + - project[name | code] + +## OPENPYPE_DEBUG + - setting logger to debug mode + - example value: "1" (to activate) + +## OPENPYPE_LOG_LEVEL + - stringified numeric value of log level. [Here for more info](https://docs.python.org/3/library/logging.html#logging-levels) + - example value: "10" + +## OPENPYPE_MONGO +- If set it takes precedence over the one set in keyring +- for more details on how to use it go [here](admin_use#check-for-mongodb-database-connection) + +## OPENPYPE_USERNAME +- if set it overides system created username diff --git a/website/docs/admin_settings_system.md b/website/docs/admin_settings_system.md index 39b58e6f81..6a17844755 100644 --- a/website/docs/admin_settings_system.md +++ b/website/docs/admin_settings_system.md @@ -14,39 +14,18 @@ Settings applicable to the full studio. ![general_settings](assets/settings/settings_system_general.png) ### Studio Name - - Full name of the studio (can be used as variable on some places) +Full name of the studio (can be used as variable on some places) ### Studio Code - - Studio acronym or a short code (can be used as variable on some places) +Studio acronym or a short code (can be used as variable on some places) ### Admin Password - - After setting admin password, normal user won't have access to OpenPype settings +After setting admin password, normal user won't have access to OpenPype settings and Project Manager GUI. Please keep in mind that this is a studio wide password and it is meant purely as a simple barrier to prevent artists from accidental setting changes. ### Environment - - Globally applied environment variables that will be appended to any OpenPype process in the studio. - - OpenPype is using some keys to configure some tools. Here are some: - -#### OPENPYPE_TMPDIR: - - Custom staging dir directory - - Supports anatomy keys formating. ex `{root[work]}/{project[name]}/temp` - - supported formating keys: - - root[work] - - project[name | code] - - asset - - hierarchy - - task - - username - - app - -#### OPENPYPE_DEBUG - - setting logger to debug mode - - example value: "1" (to activate) - -#### OPENPYPE_LOG_LEVEL - - stringified numeric value of log level. [Here for more info](https://docs.python.org/3/library/logging.html#logging-levels) - - example value: "10" +Globally applied environment variables that will be appended to any OpenPype process in the studio. ### Disk mapping - Platform dependent configuration for mapping of virtual disk(s) on an artist's OpenPype machines before OP starts up. diff --git a/website/sidebars.js b/website/sidebars.js index cc945a019e..ed4ff45db8 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -85,6 +85,7 @@ module.exports = { type: "category", label: "Configuration", items: [ + "admin_environment", "admin_settings", "admin_settings_system", "admin_settings_project_anatomy", From d774eab62603a8356ed55b6c74255332b6c675ee Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 9 Feb 2023 15:08:18 +0100 Subject: [PATCH 052/119] end line added --- website/docs/admin_settings_system.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/admin_settings_system.md b/website/docs/admin_settings_system.md index 6a17844755..c39cac61f5 100644 --- a/website/docs/admin_settings_system.md +++ b/website/docs/admin_settings_system.md @@ -176,4 +176,4 @@ In the image before you can see that we set most of the environment variables in In this example MTOA will automatically will the `MAYA_VERSION`(which is set by Maya Application environment) and `MTOA_VERSION` into the `MTOA` variable. We then use the `MTOA` to set all the other variables needed for it to function within Maya. ![tools](assets/settings/tools_01.png) -All of the tools defined in here can then be assigned to projects. You can also change the tools versions on any project level all the way down to individual asset or shot overrides. So if you just need to upgrade you render plugin for a single shot, while not risking the incompatibilities on the rest of the project, it is possible. \ No newline at end of file +All of the tools defined in here can then be assigned to projects. You can also change the tools versions on any project level all the way down to individual asset or shot overrides. So if you just need to upgrade you render plugin for a single shot, while not risking the incompatibilities on the rest of the project, it is possible. From d516de4ecdfcf17b2d8f91d91535d22ef7c6ad13 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 9 Feb 2023 16:28:55 +0000 Subject: [PATCH 053/119] Ensure content and proxy hierarchy is the same. --- .../publish/collect_arnold_scene_source.py | 4 +- .../publish/extract_arnold_scene_source.py | 51 ++++++--- .../publish/validate_arnold_scene_source.py | 106 ++++++++++++++++++ 3 files changed, 145 insertions(+), 16 deletions(-) create mode 100644 openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py diff --git a/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py index c0275eef7b..0415808b7a 100644 --- a/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py @@ -21,10 +21,10 @@ class CollectArnoldSceneSource(pyblish.api.InstancePlugin): self.log.warning("Skipped empty instance: \"%s\" " % objset) continue if objset.endswith("content_SET"): - instance.data["setMembers"] = members + instance.data["setMembers"] = cmds.ls(members, long=True) self.log.debug("content members: {}".format(members)) elif objset.endswith("proxy_SET"): - instance.data["proxy"] = members + instance.data["proxy"] = cmds.ls(members, long=True) self.log.debug("proxy members: {}".format(members)) # Use camera in object set if present else default to render globals diff --git a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py index 4cff9d0183..10943dd810 100644 --- a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py @@ -5,14 +5,16 @@ from maya import cmds import arnold from openpype.pipeline import publish -from openpype.hosts.maya.api.lib import maintained_selection, attribute_values +from openpype.hosts.maya.api.lib import ( + maintained_selection, attribute_values, delete_after +) from openpype.lib import StringTemplate class ExtractArnoldSceneSource(publish.Extractor): """Extract the content of the instance to an Arnold Scene Source file.""" - label = "Arnold Scene Source" + label = "Extract Arnold Scene Source" hosts = ["maya"] families = ["ass"] asciiAss = False @@ -124,22 +126,43 @@ class ExtractArnoldSceneSource(publish.Extractor): def _extract(self, nodes, attribute_data, kwargs): self.log.info("Writing: " + kwargs["filename"]) filenames = [] - with attribute_values(attribute_data): - with maintained_selection(): - self.log.info( - "Writing: {}".format(nodes) + # Duplicating nodes so they are direct children of the world. This + # makes the hierarchy of any exported ass file the same. + with delete_after() as delete_bin: + duplicate_nodes = [] + for node in nodes: + duplicate_transform = cmds.duplicate(node)[0] + delete_bin.append(duplicate_transform) + + # Discard the children. + shapes = cmds.listRelatives(duplicate_transform, shapes=True) + children = cmds.listRelatives( + duplicate_transform, children=True ) - cmds.select(nodes, noExpand=True) + cmds.delete(set(children) - set(shapes)) - self.log.info( - "Extracting ass sequence with: {}".format(kwargs) - ) + duplicate_transform = cmds.parent( + duplicate_transform, world=True + )[0] - exported_files = cmds.arnoldExportAss(**kwargs) + duplicate_nodes.append(duplicate_transform) - for file in exported_files: - filenames.append(os.path.split(file)[1]) + with attribute_values(attribute_data): + with maintained_selection(): + self.log.info( + "Writing: {}".format(duplicate_nodes) + ) + cmds.select(duplicate_nodes, noExpand=True) - self.log.info("Exported: {}".format(filenames)) + self.log.info( + "Extracting ass sequence with: {}".format(kwargs) + ) + + exported_files = cmds.arnoldExportAss(**kwargs) + + for file in exported_files: + filenames.append(os.path.split(file)[1]) + + self.log.info("Exported: {}".format(filenames)) return filenames diff --git a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py new file mode 100644 index 0000000000..ad00502d56 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py @@ -0,0 +1,106 @@ +import os +import types + +import maya.cmds as cmds +from mtoa.core import createOptions + +import pyblish.api +from openpype.pipeline.publish import ( + ValidateContentsOrder, PublishValidationError +) + + +class ValidateArnoldSceneSource(pyblish.api.InstancePlugin): + """Validate Arnold Scene Source. + + If using proxies we need the nodes to share the same names and not be + parent to the world. This ends up needing at least two groups with content + nodes and proxy nodes in another. + """ + + order = ValidateContentsOrder + hosts = ["maya"] + families = ["ass"] + label = "Validate Arnold Scene Source" + + def _get_nodes_data(self, nodes): + ungrouped_nodes = [] + nodes_by_name = {} + parents = [] + for node in nodes: + node_split = node.split("|") + if len(node_split) == 2: + ungrouped_nodes.append(node) + + parent = "|".join(node_split[:-1]) + if parent: + parents.append(parent) + + nodes_by_name[node_split[-1]] = node + for shape in cmds.listRelatives(node, shapes=True): + nodes_by_name[shape.split("|")[-1]] = shape + + return ungrouped_nodes, nodes_by_name, parents + + def process(self, instance): + if not instance.data["proxy"]: + return + + ungrouped_nodes = [] + + nodes, content_nodes_by_name, content_parents = self._get_nodes_data( + instance.data["setMembers"] + ) + ungrouped_nodes.extend(nodes) + + nodes, proxy_nodes_by_name, proxy_parents = self._get_nodes_data( + instance.data["proxy"] + ) + ungrouped_nodes.extend(nodes) + + # Validate against nodes directly parented to world. + if ungrouped_nodes: + raise PublishValidationError( + "Found nodes parented to the world: {}\n" + "All nodes need to be grouped.".format(ungrouped_nodes) + ) + + # Validate for content and proxy nodes amount being the same. + if len(instance.data["setMembers"]) != len(instance.data["proxy"]): + raise PublishValidationError( + "Amount of content nodes ({}) and proxy nodes ({}) needs to " + "be the same.".format( + len(instance.data["setMembers"]), + len(instance.data["proxy"]) + ) + ) + + # Validate against content and proxy nodes sharing same parent. + if list(set(content_parents) & set(proxy_parents)): + raise PublishValidationError( + "Content and proxy nodes cannot share the same parent." + ) + + # Validate for content and proxy nodes sharing same names. + sorted_content_names = sorted(content_nodes_by_name.keys()) + sorted_proxy_names = sorted(proxy_nodes_by_name.keys()) + odd_content_names = list( + set(sorted_content_names) - set(sorted_proxy_names) + ) + odd_content_nodes = [ + content_nodes_by_name[x] for x in odd_content_names + ] + odd_proxy_names = list( + set(sorted_proxy_names) - set(sorted_content_names) + ) + odd_proxy_nodes = [ + proxy_nodes_by_name[x] for x in odd_proxy_names + ] + if not sorted_content_names == sorted_proxy_names: + raise PublishValidationError( + "Content and proxy nodes need to share the same names.\n" + "Content nodes not matching: {}\n" + "Proxy nodes not matching: {}".format( + odd_content_nodes, odd_proxy_nodes + ) + ) From 8ee1a3a2d2cb7b952f6aee1385b31abee5d9add7 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 9 Feb 2023 17:11:31 +0000 Subject: [PATCH 054/119] Document proxy workflow --- website/docs/artist_hosts_maya_arnold.md | 14 ++++++++++++++ .../docs/assets/maya-arnold_scene_source.png | Bin 0 -> 15948 bytes website/docs/assets/maya-arnold_standin.png | Bin 0 -> 42985 bytes 3 files changed, 14 insertions(+) create mode 100644 website/docs/assets/maya-arnold_scene_source.png create mode 100644 website/docs/assets/maya-arnold_standin.png diff --git a/website/docs/artist_hosts_maya_arnold.md b/website/docs/artist_hosts_maya_arnold.md index b8b8da6d57..b3c02a0894 100644 --- a/website/docs/artist_hosts_maya_arnold.md +++ b/website/docs/artist_hosts_maya_arnold.md @@ -8,9 +8,23 @@ Arnold Scene Source can be published as a single file or a sequence of files, de When creating the instance, two objectsets are created; `content` and `proxy`. Meshes in the `proxy` objectset will be the viewport representation when loading as `standin`. Proxy representations are stored as `resources` of the subset. +### Arnold Scene Source Proxy Workflow +In order to utilize operators and proxies, the content and proxy nodes need to share the same names (including the shape names). This is done by parenting the content and proxy nodes into separate groups. For example: + +![Arnold Scene Source](assets/maya-arnold_scene_source.png) + ## Standin Arnold Scene Source `ass` and Alembic `abc` are supported to load as standins. +### Standin Proxy Workflow If a subset has a proxy representation, this will be used as display in the viewport. At render time the standin path will be replaced using the recommended string replacement workflow; https://help.autodesk.com/view/ARNOL/ENU/?guid=arnold_for_maya_operators_am_Updating_procedural_file_paths_with_string_replace_html + +Since the content and proxy nodes share the same names and hierarchy, any manually shader assignments will be shared. + + +:::note for advanced users +You can stop the proxy swapping by disabling the string replacement operator found in the container. +![Arnold Standin](assets/maya-arnold_standin.png) +::: diff --git a/website/docs/assets/maya-arnold_scene_source.png b/website/docs/assets/maya-arnold_scene_source.png new file mode 100644 index 0000000000000000000000000000000000000000..4150b78aac7bfe776d9f4b4940a56d77176a2a57 GIT binary patch literal 15948 zcmbVzcU)81*0v4`7OE({2q+c=L~5u?0_Z3LHdLArX`vfB1V)iwA{L}$KqZ2Jk=~US zr3BDWLvNuNS|F4F`A!1Pb?(gj-tWGD_*2ix-us-r)_&Hrp0!T6;Z;4h0|Ezj?b^k5 z=_2IXu3fufz@OTE%=9ZwhV{T-yW!XLw0Gs=grI zf$!RN$esRY_kfM4_pV(U!z^M<*|7pAdsy9y+fAIz9Y2YTCojDYup+wdcYf zH#?Kvg`^JIq0(hPNLnI;t)I!FxKHodv?1T<2ryr!6j%3iav33d z4B^8y!3SUZ%CIW(%9xhm{V}Kja?+rX);MsPSH{Dn%`PMn_=hR1;w;{@#C7-@IT$rT zACf5hbBHCn)OEP#&BWz*hO`X}T8H{d`Cvj?Ln>`^E{E4ji&`uCo)_9J8eqBEzOjf5 zRH%*^mZLA``qkq1;Pq1D0Nbjqnu8O9T7K>R)cUlfxs4It3DLkcDKeyKg|^l;kruEi zJE2#-7VR_r=4CU|BA`c|+9)-VSxrmv5sOh8*_a8WDw7F;G%rC1`nrKlZseAd1qbcdauWh3Kx8J_PL?#^#*O7txJ^eFt$591aDyeE6h2o7 zZDDN6Wy~9QbgTNxb-|e?&Fb2y6cq<&(6S)IR+A(Ip|W_v4Lc=P)PG$dd;7t2@OKb zuJQI_0_LYOThfG7?FPJQye$@2)$hwBdUsc>3}<+<5@Jl`fsJSFSj;l6%zAEVKEEC~ zA0%4WT62){Zp2HPclxD;98!zQ4NPC$Z?ol3<{=mBRp0M45LOnYHICZQxCe`&ongz0 zNKKe|abTKH?VzYq;KHY%iD*AcqLgSt0dQAw;9j$dWZuHoxP^N*NYo|i>m#zcy94n` zduwUKm4;Tpz)#H^_q>bnUWU8v#zlCt*TCr`_ZbHQ@JwO}5QXpJn!@F@HT! z$o61py2rWvEhLK{CS&URvAM_)+WyGY<242_mN<2sY(Th8>mXL&Glw_vkf#lz6TJGD zeezcaDs8?ZSXwfx{QFExhPfv?pvp;I1-V&+1)DPDbpsc97a;E?5=o8TYpzDd9}Rb9gMS`M_s%R@d6KGN6@d?RRm5zOgFB zeX}u0TK22uyCtsnHIHaHyk?C*OjdM4d^lqpuF2qKD;?jas=GipjnzC8yZk4V$h{^- ziAa$&rLMkm?3s-X_I$f8XL5s}vqGMEar?#^Z`HlHIW5Pnu~Fru>ORjzWt?xnJeP-v z4>WLcdl-@>5&h-{I?Z`6C010!9*P(q+)VLlp^zpXwGLQzrl>k;dab`NbtI5%oM#TK zSNs3CKG6}F-{9}R{`k&jJxj~pK&l*>Hz&-3(w#v<&JMHbc)YuW9;b|fUZnAha#u=@ zd&`&#d^n2-AA$FERriWh&-0$?JZza)Fd7lNP46aJ4XHfK#5h3&**2SiGGVzSRk%!78Rp+DZM^behWldKA!hPh=00n z!L&r--e+1vq>MEshdLr29oUGyDf31Hl9}6e=p0pKJjQd?JTdqRdWSxK!c)R#~VOnAzQ_^8u=CG$6hsU(KWgfLLlgs z{+4QlHi=wU&2jaY4ck{-u-SbFtalFoRgzj>;6shc@B(XgOO~@CIgFctM(Le@TU7gO%9TFboLL9RR6v{a$ukxyXNxio>h)PC6-n# zYv)ZZgZOfj8Ryk?YecINXL7|&b?K;auRi@Xzgt*LS*sjGV1q3?GYjf09J1{y2eC86omp{k7BhlF<}lTlVvRD7N1)71^HWfb<*f7J5`Q&9dMFfTzUu-5THz zKmfkn!n+e?tNvq@JqTvZp=i;I3ZPL34ImK3Izw)*4%5^4;sIYNF0PLAHt2q+xVQjo zNMaazf)6I1o(M&Uhrwb{lHysVB9DD5L92D#6Z@xzd|jv)@-^MA7^zU?@(~F|Imk}Y z!_`RY8k4VdX+hV`vBJ*f6)|xOq_#ATqq^BKuo>x5%*^@Bn$QBBZhi02{mXAewMluG zoh^zsF{{6Km|0^9#a_8q9JyUMZrWbXIdV6}n&d&+bUVJ$?U~;!LO3o6BUSJEmM8Da z_MqGkkuvDDd7;!|v9+dZfS(pTk#gPagt-Zo_!TjNovvmQ7bwsSzRQf~cqDq^J@;-* zExUOgH_>w@1L3$j3SCg)Z=EfvzVZ~)=$m5xq~3yWaoKVciu5pto*U_purU6e*vh7L zV%=y1&L07!VWb`Pb5MK92UgeB?jS2fB=0OghNj3#6Qbrn&AT4x zG+U~M^OL45himcD)}mJPz;|A*rWDYghKtN>&&D~O@J;(Vd)_Wv*aUC|9T=jHZx8Gw zA8>uk2TNDYz5uw=$%9~h$PGBFB9~E%93JpMEZqZz{_Uc1lc9xsyAA}BpXg|1j0zZ} z4%q7I>i*E`A6=+|ns`&I%+ zzU zWofVJ%UGVZ#R=uZiC4*md5D}R!fKw~&;t0w8Q=}#TqI@g7M%1BHg(s!*n{cf4vVs`5%oJSGl2Wrw;l+KnF+BlNL;TT_kp=q5Isa4zp1Tt=ik>R#?HT!WIf0G?yT7f>& zbxR>HDall8MYWjStuyyAmpSj+$;NXPDplO8I8Bm==a=J4FZXa6Y4Xo}jAf>q!E8yt z26%F(XY`1?d9fbk^C4Qti6fz zs(Ka{#(chrj{%Xf(#&+cw7DN?X$M9u)bnPOW94?XQl0i>cNx;ycZ@T)8rkbXg4-DmCUY)A`OajxyWt5R{t| z6lQ05|8?Q*CLZuOsd zQrpeUKX%{W?5G+l>XC2?FLK=H(AhDrD3zh)6g~o3GQinRkW2BQqnyHGgg~(g8&dxS z_@#epq4Z{-?zNCg;h|(H8(I9UdWw|ihLG10*{y4s3a5%hNtjTN6&isSe$1O+Um`bY z-4s{I6pdB${PAH1DFm{T$W4)WdIZ>wM!27%Oz72-^Lb~y>XXe+h>j*t4JV9YAoyb+ z>(45nbp2Q61_#*{YtAr{hN2sO82BD_A6R{G@AmFSRzwLo8i@=I)v%fpJGho#*=4CE zJQAS0u6W2O{|nu!xD4UY`H>9Ib6*m}J3AaH9SRUsz6)P3iiuxC`q6D-2)`=fn~P+l z+?Sjb?8VcE0egVm zbo3z#Nnd!F=)f%hvHlYsrK23>&e9=U!i1H9-~YOO)JLZ>Rx{ImXx zLtX@lbW31`Xb#01LNSQPiL*(!I1Lpur+iNCvmrN|T#uEVDLQ|a?NA~YGinwVQ$O^= zp+T;(Xe>JMgT~##gr4K7k}5BuR?K|G(I`oQTY!1y4T<|n#Lg!2L)HhW39Xp!(oFXg zbw1epX&fu1F}UNIhcpEDUht-BVbhNhm6}!GHY+)al1e!UfJrQm0}s!pI6|O-c#dn~QP7X+ zf+-RjrGErovD0Fq%HtumzthjuG_W80T#m!f^eS`Lb3Ud_qy=6%T{b|j1DQE)Lba)& z?zHX4H=Hq~M$97Jra$8_-7Rtu2p6yg_t7j`SaHBQ{ebxzg8yZ{&rhU7bj^*px!B}; ztnSg_OZRUu^^i{I_v z>YH`I!@c|#ZsFo{`oM#QQ3V+!VhLuFO;9NwijCAgL_?gF+elM8XXS6-#xU(zOw_z z)Zr6XPUF3=8wD~MMWibohM$?J5ZSaH!4LxbOhng(@8Y|rhHf)fWDdDI+>krYYwkJQ zXWwP5C$RD5NPCN%$M?sB578oYpI~>zhzJViTZkDTbwbbL*FD8g15VF3Ews;l>!t#i z(cHhV<3B*l&)dz3hUKj~yq}SyTT#|AdS(6a!7EueJ0yK>DOBs3Jga!a?qUGBIi zs^a>p9AhsymA)`jzj?L{r01SwgwwyMAO!s04W;WiE*SLfnX##5>AlgoH&~=-G0MEs z*JB8#%ATRhd-ON7&h-I8=^I2og8$CA8_I`kJV$5KsnO{=_exEJZ%872UCSX9j=Q%t z=OvNp@4=B12H$OU@#F#1h0+ri1uPKLhWKsB*K3X^RK5Q7J^W0^<7$FQ-7mn-e!8bgR zFs90D4eIWW32Cu~`UI>6eZB+QQ7d0eUm?0f5WwzW{wZKOr&#U@WQWi|6j5$!3=$bB z7G`*V^Ft%C-Md(GY1qku+T4_Pc0F1x-G91JC13-jA_Cndi zHlhvhR(|wkG~B6qd2dncDQ|>%)l!ENSBuTvctzmUelZ;7?Wj33JTtat{Jl*dogucL zG@TS?BdhgAjB%)K5!1CVHI(!rc$2%&86%{C+(GqQ`uiPzD9*S{_vf3C$d5ay9ARhF zxpNk{q#FgcG5K`5&K-J?^G7x_Je3gP~0c`TZQ+6Rle)6Hhi)T zbpuWJ2o4Gf_M?~igPp%GSAz`BlIflB^_%EXzc*K0WAtD=j7J8-we@RZCoOEWj!XB4 z&q8XR&CL8@k&KKC&;EDUGta0us45Gc1A**q!!BV}V|vcEV)!bawyc-V>fYE|Uy@7F z;waY8%-!@IQwmAkd8iN>e198mugo`-VrP9*Q0>xEK}x#AK!p#0pDERYandFgs3$7w zU`=d#Ikrfk<5R2tEU}YV-MKX520(d88jp1}K!$IkBTov#Ah-Sr%Y!6RZ6BBAw{X}E zjD95CZ9klTaUH9&utn-h_L$`nwDfXblLuq#h`iqd0a}C?+BW)s1MXwNmzuD2@*wW-At@?BDcxY>o_q^21>4JAU(bkRMUR!_P=kK7&Sf*>0Jt zds3Uy_(|LwWJ8O;jy&T0<8HvPH4&HMY3BL*GxCnxR5nR+a;r56ZC`NG;cs~F&i&SI z-}&-&kpLj#X6(2!`yN%=>BO*;+dEI^kw@D9??(g3rB*;h@zn)Y@hm3B3-a|EN_^5o zis<{-n*>>ub@Sc~3tMmEco60&cS8W?V_eQjQW4F4;zlr%ZvHk<>F3l|2Itoe}uk_ViFQ1MAzf3$~I4e8%PDs@&0lw<`U2-kae_@R~55zfLrzD>B zblZWC;a4*aoL}C5LUt78mrGK1Q3~Y{#Z@FWs3TIsJUa`);n}!4$KzJC_t_nB??L+` zQziC-MG}g&^iw5#%JK9p@A&p19Xp~Znch;BNU5F7d)C_IJYy;bo^3w)&gp)BFLNN@7Aau6-{R2}O>Z$QtZds?!CbHkSLxqCzU5hULjQu+@y2483+xyzZg-DX55QQFH5;-nXRDJX>@)v(9$> z5`@(iE5px-Gw8*v_J5`q+tC*=vj|D$aj_TtjBb0rOLNa~u=_5j_k>yw@y7$+56-

M-ZZ1puA}J8G$tO$CyZX_jwrh#*yh&^Hbc( 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

H1$S7zu1R^u(X=+a-N2qaE)ReLSRVk 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 Date: Thu, 9 Feb 2023 17:49:25 +0000 Subject: [PATCH 055/119] Hound --- .../maya/plugins/publish/validate_arnold_scene_source.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py index ad00502d56..2d6c6e8e14 100644 --- a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py @@ -1,8 +1,4 @@ -import os -import types - import maya.cmds as cmds -from mtoa.core import createOptions import pyblish.api from openpype.pipeline.publish import ( From 514ba7e79b55e1aedcfe32c1fe4b949c32af0c13 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 10 Feb 2023 07:25:38 +0000 Subject: [PATCH 056/119] Fix reset_frame_range --- openpype/hosts/maya/api/commands.py | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/maya/api/commands.py b/openpype/hosts/maya/api/commands.py index 4a36406632..19ad18d824 100644 --- a/openpype/hosts/maya/api/commands.py +++ b/openpype/hosts/maya/api/commands.py @@ -4,6 +4,7 @@ from maya import cmds from openpype.client import get_asset_by_name, get_project from openpype.pipeline import legacy_io +from . import lib class ToolWindows: @@ -59,25 +60,11 @@ def edit_shader_definitions(): def reset_frame_range(): """Set frame range to current asset""" - # Set FPS first - fps = {15: 'game', - 24: 'film', - 25: 'pal', - 30: 'ntsc', - 48: 'show', - 50: 'palf', - 60: 'ntscf', - 23.98: '23.976fps', - 23.976: '23.976fps', - 29.97: '29.97fps', - 47.952: '47.952fps', - 47.95: '47.952fps', - 59.94: '59.94fps', - 44100: '44100fps', - 48000: '48000fps' - }.get(float(legacy_io.Session.get("AVALON_FPS", 25)), "pal") - cmds.currentUnit(time=fps) + fps = lib.convert_to_maya_fps( + float(legacy_io.Session.get("AVALON_FPS", 25)) + ) + lib.set_scene_fps(fps) # Set frame start/end project_name = legacy_io.active_project() From ff63a91864af8d9dbfdfe940863468a03c0233af Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 10 Feb 2023 10:00:49 +0000 Subject: [PATCH 057/119] Support switching between proxy and non-proxy --- .../maya/plugins/load/load_arnold_standin.py | 25 ++++++++++++------- .../publish/extract_arnold_scene_source.py | 8 +++++- .../publish/validate_arnold_scene_source.py | 12 ++++++--- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_arnold_standin.py b/openpype/hosts/maya/plugins/load/load_arnold_standin.py index e2bb89ed77..bebe40f9a6 100644 --- a/openpype/hosts/maya/plugins/load/load_arnold_standin.py +++ b/openpype/hosts/maya/plugins/load/load_arnold_standin.py @@ -53,10 +53,8 @@ class ArnoldStandinLoader(load.LoaderPlugin): root = cmds.group(name=label, empty=True) # Set color. - project_name = context["project"]["name"] - settings = get_project_settings(project_name) - colors = settings['maya']['load']['colors'] - color = colors.get('ass') + settings = get_project_settings(context["project"]["name"]) + color = settings['maya']['load']['colors'].get('ass') if color is not None: cmds.setAttr(root + ".useOutlinerColor", True) cmds.setAttr( @@ -121,10 +119,6 @@ class ArnoldStandinLoader(load.LoaderPlugin): def _setup_proxy(self, shape, path, namespace): proxy_basename, proxy_path = self._get_proxy_path(path) - if not os.path.exists(proxy_path): - self.log.error("Proxy files do not exist. Skipping proxy setup.") - return path, None - options_node = "defaultArnoldRenderOptions" merge_operator = get_attribute_input(options_node + ".operator") if merge_operator is None: @@ -163,6 +157,12 @@ class ArnoldStandinLoader(load.LoaderPlugin): ) ) + # We setup the string operator no matter whether there is a proxy or + # not. This makes it easier to update since the string operator will + # always be created. Return original path to use for standin. + if not os.path.exists(proxy_path): + return path, string_replace_operator + return proxy_path, string_replace_operator def update(self, container, representation): @@ -180,6 +180,9 @@ class ArnoldStandinLoader(load.LoaderPlugin): path = get_representation_path(representation) proxy_basename, proxy_path = self._get_proxy_path(path) + + # Whether there is proxy or so, we still update the string operator. + # If no proxy exists, the string operator wont replace anything. cmds.setAttr( string_replace_operator + ".match", "resources/" + proxy_basename, @@ -190,7 +193,11 @@ class ArnoldStandinLoader(load.LoaderPlugin): os.path.basename(path), type="string" ) - cmds.setAttr(standin + ".dso", proxy_path, type="string") + + dso_path = path + if os.path.exists(proxy_path): + dso_path = proxy_path + cmds.setAttr(standin + ".dso", dso_path, type="string") sequence = is_sequence(os.listdir(os.path.dirname(path))) cmds.setAttr(standin + ".useFrameExtension", sequence) diff --git a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py index 10943dd810..153a1a513e 100644 --- a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py @@ -95,6 +95,9 @@ class ExtractArnoldSceneSource(publish.Extractor): ) # Extract proxy. + if not instance.data.get("proxy", []): + return + kwargs["filename"] = file_path.replace(".ass", "_proxy.ass") filenames = self._extract( instance.data["proxy"], attribute_data, kwargs @@ -132,7 +135,6 @@ class ExtractArnoldSceneSource(publish.Extractor): duplicate_nodes = [] for node in nodes: duplicate_transform = cmds.duplicate(node)[0] - delete_bin.append(duplicate_transform) # Discard the children. shapes = cmds.listRelatives(duplicate_transform, shapes=True) @@ -145,7 +147,11 @@ class ExtractArnoldSceneSource(publish.Extractor): duplicate_transform, world=True )[0] + cmds.rename(duplicate_transform, node.split("|")[-1]) + duplicate_transform = "|" + node.split("|")[-1] + duplicate_nodes.append(duplicate_transform) + delete_bin.append(duplicate_transform) with attribute_values(attribute_data): with maintained_selection(): diff --git a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py index 2d6c6e8e14..3b0ffd52d7 100644 --- a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py @@ -9,6 +9,9 @@ from openpype.pipeline.publish import ( class ValidateArnoldSceneSource(pyblish.api.InstancePlugin): """Validate Arnold Scene Source. + We require at least 1 root node/parent for the meshes. This is to ensure we + can duplicate the nodes and preserve the names. + If using proxies we need the nodes to share the same names and not be parent to the world. This ends up needing at least two groups with content nodes and proxy nodes in another. @@ -39,9 +42,6 @@ class ValidateArnoldSceneSource(pyblish.api.InstancePlugin): return ungrouped_nodes, nodes_by_name, parents def process(self, instance): - if not instance.data["proxy"]: - return - ungrouped_nodes = [] nodes, content_nodes_by_name, content_parents = self._get_nodes_data( @@ -50,7 +50,7 @@ class ValidateArnoldSceneSource(pyblish.api.InstancePlugin): ungrouped_nodes.extend(nodes) nodes, proxy_nodes_by_name, proxy_parents = self._get_nodes_data( - instance.data["proxy"] + instance.data.get("proxy", []) ) ungrouped_nodes.extend(nodes) @@ -61,6 +61,10 @@ class ValidateArnoldSceneSource(pyblish.api.InstancePlugin): "All nodes need to be grouped.".format(ungrouped_nodes) ) + # Proxy validation. + if not instance.data.get("proxy", []): + return + # Validate for content and proxy nodes amount being the same. if len(instance.data["setMembers"]) != len(instance.data["proxy"]): raise PublishValidationError( From 165689463dde7be46804a18166434a5ef8f6ee8b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 10 Feb 2023 11:20:20 +0100 Subject: [PATCH 058/119] typo --- openpype/pipeline/publish/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index aaa2dd444a..c15eadb22f 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -650,7 +650,7 @@ def get_instance_staging_dir(instance): else: project_name = os.getenv("AVALON_PROJECT") - # get customized tempdir path from `OPENPYPE_TEMPDIR` env var + # get customized tempdir path from `OPENPYPE_TMPDIR` env var custom_temp_dir = temporarydir.create_custom_tempdir( project_name, anatomy=anatomy, formating_data=anatomy_data ) From 87f9cf09d77cc8ccec04c2c8dd31905f425ba212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 10 Feb 2023 11:23:53 +0100 Subject: [PATCH 059/119] Update openpype/pipeline/temporarydir.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/pipeline/temporarydir.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/pipeline/temporarydir.py b/openpype/pipeline/temporarydir.py index 31586d82c8..c5805b2dc1 100644 --- a/openpype/pipeline/temporarydir.py +++ b/openpype/pipeline/temporarydir.py @@ -38,10 +38,9 @@ def create_custom_tempdir(project_name, anatomy=None, formating_data=None): } if formating_data is None: # We still don't have `project_code` on Anatomy... - project_doc = anatomy.get_project_doc_from_cache(project_name) data["project"] = { "name": project_name, - "code": project_doc["data"]["code"], + "code": anatomy.project_code, } else: data["project"] = formating_data["project"] From bbd634bcd428b630324b7fbe57324c6eac8bf4eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 10 Feb 2023 11:24:06 +0100 Subject: [PATCH 060/119] Update openpype/pipeline/publish/lib.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/pipeline/publish/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index c15eadb22f..423661880c 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -643,7 +643,7 @@ def get_instance_staging_dir(instance): return staging_dir anatomy_data = instance.data.get("anatomy_data") - anatomy = instance.data.get("anatomy") + anatomy = instance.context.data.get("anatomy") if anatomy_data: project_name = anatomy_data["project"]["name"] From af3c0cb951bcd4227ab07cfe174734cd43645b1d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 10 Feb 2023 11:26:09 +0100 Subject: [PATCH 061/119] pr comments --- openpype/pipeline/publish/lib.py | 4 ++-- openpype/pipeline/{temporarydir.py => tempdir.py} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename openpype/pipeline/{temporarydir.py => tempdir.py} (100%) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 423661880c..d6e8097690 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -19,7 +19,7 @@ from openpype.settings import ( get_system_settings, ) from openpype.pipeline import ( - temporarydir + tempdir ) from .contants import ( @@ -651,7 +651,7 @@ def get_instance_staging_dir(instance): project_name = os.getenv("AVALON_PROJECT") # get customized tempdir path from `OPENPYPE_TMPDIR` env var - custom_temp_dir = temporarydir.create_custom_tempdir( + custom_temp_dir = tempdir.create_custom_tempdir( project_name, anatomy=anatomy, formating_data=anatomy_data ) diff --git a/openpype/pipeline/temporarydir.py b/openpype/pipeline/tempdir.py similarity index 100% rename from openpype/pipeline/temporarydir.py rename to openpype/pipeline/tempdir.py From 69937c62858a69d9d42beaeeaa6d23e5073a9446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 10 Feb 2023 11:27:30 +0100 Subject: [PATCH 062/119] Update openpype/pipeline/publish/lib.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/pipeline/publish/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index d6e8097690..7d3c367c7a 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -648,7 +648,7 @@ def get_instance_staging_dir(instance): if anatomy_data: project_name = anatomy_data["project"]["name"] else: - project_name = os.getenv("AVALON_PROJECT") + project_name = instance.context.data["projectName"] # get customized tempdir path from `OPENPYPE_TMPDIR` env var custom_temp_dir = tempdir.create_custom_tempdir( From be0209e4135bea83ffbda230aa23f33651e9cbd0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 10 Feb 2023 11:34:05 +0100 Subject: [PATCH 063/119] refactor in favour of code changes from #4445 https://github.com/ynput/OpenPype/pull/4445 --- openpype/pipeline/publish/lib.py | 10 +--------- openpype/pipeline/tempdir.py | 19 ++++++------------- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 7d3c367c7a..2884dd495f 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -642,18 +642,10 @@ def get_instance_staging_dir(instance): if staging_dir: return staging_dir - anatomy_data = instance.data.get("anatomy_data") anatomy = instance.context.data.get("anatomy") - if anatomy_data: - project_name = anatomy_data["project"]["name"] - else: - project_name = instance.context.data["projectName"] - # get customized tempdir path from `OPENPYPE_TMPDIR` env var - custom_temp_dir = tempdir.create_custom_tempdir( - project_name, anatomy=anatomy, formating_data=anatomy_data - ) + custom_temp_dir = tempdir.create_custom_tempdir(anatomy) if custom_temp_dir: staging_dir = os.path.normpath( diff --git a/openpype/pipeline/tempdir.py b/openpype/pipeline/tempdir.py index c5805b2dc1..ff5c58bbc5 100644 --- a/openpype/pipeline/tempdir.py +++ b/openpype/pipeline/tempdir.py @@ -7,7 +7,7 @@ from openpype.lib import StringTemplate from openpype.pipeline import Anatomy -def create_custom_tempdir(project_name, anatomy=None, formating_data=None): +def create_custom_tempdir(anatomy=None): """ Create custom tempdir Template path formatting is supporting: @@ -17,9 +17,7 @@ def create_custom_tempdir(project_name, anatomy=None, formating_data=None): - project[name | code] Args: - project_name (str): name of project anatomy (openpype.pipeline.Anatomy): Anatomy object - formating_data (dict): formating data used for filling template. Returns: bool | str: formated path or None @@ -31,20 +29,15 @@ def create_custom_tempdir(project_name, anatomy=None, formating_data=None): custom_tempdir = None if "{" in openpype_tempdir: if anatomy is None: - anatomy = Anatomy(project_name) + anatomy = Anatomy() # create base formate data data = { - "root": anatomy.roots - } - if formating_data is None: - # We still don't have `project_code` on Anatomy... - data["project"] = { - "name": project_name, + "root": anatomy.roots, + "project": { + "name": anatomy.project_name, "code": anatomy.project_code, } - else: - data["project"] = formating_data["project"] - + } # path is anatomy template custom_tempdir = StringTemplate.format_template( openpype_tempdir, data).normalized() From 9f4153fbe64ee0f5918354a2723358a760fb571d Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 10 Feb 2023 12:27:10 +0000 Subject: [PATCH 064/119] Code cosmetics --- .../hosts/maya/plugins/load/load_arnold_standin.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_arnold_standin.py b/openpype/hosts/maya/plugins/load/load_arnold_standin.py index bebe40f9a6..6e5fe16bcd 100644 --- a/openpype/hosts/maya/plugins/load/load_arnold_standin.py +++ b/openpype/hosts/maya/plugins/load/load_arnold_standin.py @@ -94,17 +94,13 @@ class ArnoldStandinLoader(load.LoaderPlugin): def get_next_free_multi_index(self, attr_name): """Find the next unconnected multi index at the input attribute.""" - - start_index = 0 - # Assume a max of 10 million connections - while start_index < 10000000: + for index in range(10000000): connection_info = cmds.connectionInfo( - "{}[{}]".format(attr_name, start_index), + "{}[{}]".format(attr_name, index), sourceFromDestination=True ) if len(connection_info or []) == 0: - return start_index - start_index += 1 + return index def _get_proxy_path(self, path): basename_split = os.path.basename(path).split(".") From 3927dc13af80888375dd1151ff9eef0929a71f9e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 10 Feb 2023 13:42:46 +0100 Subject: [PATCH 065/119] adding back project name --- openpype/pipeline/publish/lib.py | 3 ++- openpype/pipeline/tempdir.py | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 2884dd495f..cc7f5678f5 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -645,7 +645,8 @@ def get_instance_staging_dir(instance): anatomy = instance.context.data.get("anatomy") # get customized tempdir path from `OPENPYPE_TMPDIR` env var - custom_temp_dir = tempdir.create_custom_tempdir(anatomy) + custom_temp_dir = tempdir.create_custom_tempdir( + anatomy.project_name, anatomy) if custom_temp_dir: staging_dir = os.path.normpath( diff --git a/openpype/pipeline/tempdir.py b/openpype/pipeline/tempdir.py index ff5c58bbc5..ab3cc216ef 100644 --- a/openpype/pipeline/tempdir.py +++ b/openpype/pipeline/tempdir.py @@ -7,7 +7,7 @@ from openpype.lib import StringTemplate from openpype.pipeline import Anatomy -def create_custom_tempdir(anatomy=None): +def create_custom_tempdir(project_name, anatomy=None): """ Create custom tempdir Template path formatting is supporting: @@ -17,7 +17,8 @@ def create_custom_tempdir(anatomy=None): - project[name | code] Args: - anatomy (openpype.pipeline.Anatomy): Anatomy object + project_name (str): project name + anatomy (openpype.pipeline.Anatomy)[optional]: Anatomy object Returns: bool | str: formated path or None @@ -29,7 +30,7 @@ def create_custom_tempdir(anatomy=None): custom_tempdir = None if "{" in openpype_tempdir: if anatomy is None: - anatomy = Anatomy() + anatomy = Anatomy(project_name) # create base formate data data = { "root": anatomy.roots, From f458fbc9258e97e0e9f340d3e9e59c3eb5b2b820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 10 Feb 2023 13:44:01 +0100 Subject: [PATCH 066/119] Update openpype/pipeline/tempdir.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/pipeline/tempdir.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/tempdir.py b/openpype/pipeline/tempdir.py index ab3cc216ef..6a346f3342 100644 --- a/openpype/pipeline/tempdir.py +++ b/openpype/pipeline/tempdir.py @@ -21,7 +21,7 @@ def create_custom_tempdir(project_name, anatomy=None): anatomy (openpype.pipeline.Anatomy)[optional]: Anatomy object Returns: - bool | str: formated path or None + str | None: formated path or None """ openpype_tempdir = os.getenv("OPENPYPE_TMPDIR") if not openpype_tempdir: From 25e4a4b5a3b23fde16f5908b07d81103be99fd03 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 10 Feb 2023 16:15:55 +0100 Subject: [PATCH 067/119] Added support for multiple install dirs in Deadline SearchDirectoryList returns FIRST existing so if you would have multiple OP install dirs, it won't search for appropriate version in later ones. --- .../custom/plugins/GlobalJobPreLoad.py | 28 ++++++++++--------- .../custom/plugins/OpenPype/OpenPype.py | 23 +++++++-------- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index 984590ddba..65a3782dfe 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -196,19 +196,21 @@ def get_openpype_versions(dir_list): print(">>> Getting OpenPype executable ...") openpype_versions = [] - install_dir = DirectoryUtils.SearchDirectoryList(dir_list) - if install_dir: - print("--- Looking for OpenPype at: {}".format(install_dir)) - sub_dirs = [ - f.path for f in os.scandir(install_dir) - if f.is_dir() - ] - for subdir in sub_dirs: - version = get_openpype_version_from_path(subdir) - if not version: - continue - print(" - found: {} - {}".format(version, subdir)) - openpype_versions.append((version, subdir)) + # special case of multiple install dirs + for dir_list in dir_list.split(","): + install_dir = DirectoryUtils.SearchDirectoryList(dir_list) + if install_dir: + print("--- Looking for OpenPype at: {}".format(install_dir)) + sub_dirs = [ + f.path for f in os.scandir(install_dir) + if f.is_dir() + ] + for subdir in sub_dirs: + version = get_openpype_version_from_path(subdir) + if not version: + continue + print(" - found: {} - {}".format(version, subdir)) + openpype_versions.append((version, subdir)) return openpype_versions diff --git a/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py b/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py index 6b0f69d98f..ae31f2e35f 100644 --- a/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py +++ b/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py @@ -107,17 +107,18 @@ class OpenPypeDeadlinePlugin(DeadlinePlugin): "Scanning for compatible requested " f"version {requested_version}")) dir_list = self.GetConfigEntry("OpenPypeInstallationDirs") - install_dir = DirectoryUtils.SearchDirectoryList(dir_list) - if dir: - sub_dirs = [ - f.path for f in os.scandir(install_dir) - if f.is_dir() - ] - for subdir in sub_dirs: - version = self.get_openpype_version_from_path(subdir) - if not version: - continue - openpype_versions.append((version, subdir)) + for dir_list in dir_list.split(","): + install_dir = DirectoryUtils.SearchDirectoryList(dir_list) + if install_dir: + sub_dirs = [ + f.path for f in os.scandir(install_dir) + if f.is_dir() + ] + for subdir in sub_dirs: + version = self.get_openpype_version_from_path(subdir) + if not version: + continue + openpype_versions.append((version, subdir)) exe_list = self.GetConfigEntry("OpenPypeExecutable") exe = FileUtils.SearchFileList(exe_list) From 75dfd6c3f6b2e37cf9013a692df5385f9af7bddc Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 10 Feb 2023 15:19:56 +0000 Subject: [PATCH 068/119] Publish proxy representation. --- .../maya/plugins/load/load_arnold_standin.py | 6 ++-- .../publish/extract_arnold_scene_source.py | 32 ++++++------------- 2 files changed, 11 insertions(+), 27 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_arnold_standin.py b/openpype/hosts/maya/plugins/load/load_arnold_standin.py index 6e5fe16bcd..66e8b69639 100644 --- a/openpype/hosts/maya/plugins/load/load_arnold_standin.py +++ b/openpype/hosts/maya/plugins/load/load_arnold_standin.py @@ -107,9 +107,7 @@ class ArnoldStandinLoader(load.LoaderPlugin): proxy_basename = ( basename_split[0] + "_proxy." + ".".join(basename_split[1:]) ) - proxy_path = "/".join( - [os.path.dirname(path), "resources", proxy_basename] - ) + proxy_path = "/".join([os.path.dirname(path), proxy_basename]) return proxy_basename, proxy_path def _setup_proxy(self, shape, path, namespace): @@ -136,7 +134,7 @@ class ArnoldStandinLoader(load.LoaderPlugin): ) cmds.setAttr( string_replace_operator + ".match", - "resources/" + proxy_basename, + proxy_basename, type="string" ) cmds.setAttr( diff --git a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py index 153a1a513e..924ac58c40 100644 --- a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py @@ -1,5 +1,4 @@ import os -import copy from maya import cmds import arnold @@ -8,7 +7,6 @@ from openpype.pipeline import publish from openpype.hosts.maya.api.lib import ( maintained_selection, attribute_values, delete_after ) -from openpype.lib import StringTemplate class ExtractArnoldSceneSource(publish.Extractor): @@ -103,28 +101,16 @@ class ExtractArnoldSceneSource(publish.Extractor): instance.data["proxy"], attribute_data, kwargs ) - template_data = copy.deepcopy(instance.data["anatomyData"]) - template_data.update({"ext": "ass"}) - templates = instance.context.data["anatomy"].templates["publish"] - published_filename_without_extension = StringTemplate( - templates["file"] - ).format(template_data).replace(".ass", "_proxy") - transfers = [] - for filename in filenames: - source = os.path.join(staging_dir, filename) - destination = os.path.join( - instance.data["resourcesDir"], - filename.replace( - filename.split(".")[0], - published_filename_without_extension - ) - ) - transfers.append((source, destination)) + representation = { + "name": "proxy", + "ext": "ass", + "files": filenames if len(filenames) > 1 else filenames[0], + "stagingDir": staging_dir, + "frameStart": kwargs["startFrame"], + "outputName": "proxy" + } - for source, destination in transfers: - self.log.debug("Transfer: {} > {}".format(source, destination)) - - instance.data["transfers"] = transfers + instance.data["representations"].append(representation) def _extract(self, nodes, attribute_data, kwargs): self.log.info("Writing: " + kwargs["filename"]) From f56e7bcbf879072c88fcf74944d41cf4366c4e79 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 13 Feb 2023 10:51:21 +0100 Subject: [PATCH 069/119] removed deprecated functions from openpype lib --- openpype/lib/__init__.py | 43 -- openpype/lib/anatomy.py | 38 -- openpype/lib/avalon_context.py | 431 +----------------- openpype/lib/plugin_tools.py | 119 ----- .../tests/test_lib_restructuralization.py | 6 - openpype/tests/test_pyblish_filter.py | 6 +- 6 files changed, 7 insertions(+), 636 deletions(-) delete mode 100644 openpype/lib/anatomy.py diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index b5fb955a84..9eb7724a60 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -82,9 +82,6 @@ from .mongo import ( validate_mongo_connection, OpenPypeMongoConnection ) -from .anatomy import ( - Anatomy -) from .dateutils import ( get_datetime_data, @@ -119,36 +116,19 @@ from .transcoding import ( ) from .avalon_context import ( CURRENT_DOC_SCHEMAS, - PROJECT_NAME_ALLOWED_SYMBOLS, - PROJECT_NAME_REGEX, create_project, - is_latest, - any_outdated, - get_asset, - get_linked_assets, - get_latest_version, - get_system_general_anatomy_data, get_workfile_template_key, get_workfile_template_key_from_context, - get_workdir_data, - get_workdir, - get_workdir_with_workdir_data, get_last_workfile_with_version, get_last_workfile, - create_workfile_doc, - save_workfile_data_to_doc, - get_workfile_doc, - BuildWorkfile, get_creator_by_name, get_custom_workfile_template, - change_timer_to_current_context, - get_custom_workfile_template_by_context, get_custom_workfile_template_by_string_context, get_custom_workfile_template @@ -186,8 +166,6 @@ from .plugin_tools import ( get_subset_name, get_subset_name_with_asset_doc, prepare_template_data, - filter_pyblish_plugins, - set_plugin_attributes_from_settings, source_hash, ) @@ -278,34 +256,17 @@ __all__ = [ "convert_ffprobe_fps_to_float", "CURRENT_DOC_SCHEMAS", - "PROJECT_NAME_ALLOWED_SYMBOLS", - "PROJECT_NAME_REGEX", "create_project", - "is_latest", - "any_outdated", - "get_asset", - "get_linked_assets", - "get_latest_version", - "get_system_general_anatomy_data", "get_workfile_template_key", "get_workfile_template_key_from_context", - "get_workdir_data", - "get_workdir", - "get_workdir_with_workdir_data", "get_last_workfile_with_version", "get_last_workfile", - "create_workfile_doc", - "save_workfile_data_to_doc", - "get_workfile_doc", - "BuildWorkfile", "get_creator_by_name", - "change_timer_to_current_context", - "get_custom_workfile_template_by_context", "get_custom_workfile_template_by_string_context", "get_custom_workfile_template", @@ -338,8 +299,6 @@ __all__ = [ "TaskNotSetError", "get_subset_name", "get_subset_name_with_asset_doc", - "filter_pyblish_plugins", - "set_plugin_attributes_from_settings", "source_hash", "format_file_size", @@ -358,8 +317,6 @@ __all__ = [ "terminal", - "Anatomy", - "get_datetime_data", "get_formatted_current_time", diff --git a/openpype/lib/anatomy.py b/openpype/lib/anatomy.py deleted file mode 100644 index 6d339f058f..0000000000 --- a/openpype/lib/anatomy.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Code related to project Anatomy was moved -to 'openpype.pipeline.anatomy' please change your imports as soon as -possible. File will be probably removed in OpenPype 3.14.* -""" - -import warnings -import functools - - -class AnatomyDeprecatedWarning(DeprecationWarning): - pass - - -def anatomy_deprecated(func): - """Mark functions as deprecated. - - It will result in a warning being emitted when the function is used. - """ - - @functools.wraps(func) - def new_func(*args, **kwargs): - warnings.simplefilter("always", AnatomyDeprecatedWarning) - warnings.warn( - ( - "Deprecated import of 'Anatomy'." - " Class was moved to 'openpype.pipeline.anatomy'." - " Please change your imports of Anatomy in codebase." - ), - category=AnatomyDeprecatedWarning - ) - return func(*args, **kwargs) - return new_func - - -@anatomy_deprecated -def Anatomy(*args, **kwargs): - from openpype.pipeline.anatomy import Anatomy - return Anatomy(*args, **kwargs) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 12f4a5198b..a9ae27cb79 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -1,6 +1,5 @@ """Should be used only inside of hosts.""" -import os -import copy + import platform import logging import functools @@ -10,17 +9,12 @@ import six from openpype.client import ( get_project, - get_assets, get_asset_by_name, - get_last_version_by_subset_name, - get_workfile_info, ) from openpype.client.operations import ( CURRENT_ASSET_DOC_SCHEMA, CURRENT_PROJECT_SCHEMA, CURRENT_PROJECT_CONFIG_SCHEMA, - PROJECT_NAME_ALLOWED_SYMBOLS, - PROJECT_NAME_REGEX, ) from .profiles_filtering import filter_profiles from .path_templates import StringTemplate @@ -128,70 +122,6 @@ def with_pipeline_io(func): return wrapped -@deprecated("openpype.pipeline.context_tools.is_representation_from_latest") -def is_latest(representation): - """Return whether the representation is from latest version - - Args: - representation (dict): The representation document from the database. - - Returns: - bool: Whether the representation is of latest version. - - Deprecated: - Function will be removed after release version 3.15.* - """ - - from openpype.pipeline.context_tools import is_representation_from_latest - - return is_representation_from_latest(representation) - - -@deprecated("openpype.pipeline.load.any_outdated_containers") -def any_outdated(): - """Return whether the current scene has any outdated content. - - Deprecated: - Function will be removed after release version 3.15.* - """ - - from openpype.pipeline.load import any_outdated_containers - - return any_outdated_containers() - - -@deprecated("openpype.pipeline.context_tools.get_current_project_asset") -def get_asset(asset_name=None): - """ Returning asset document from database by its name. - - Doesn't count with duplicities on asset names! - - Args: - asset_name (str) - - Returns: - (MongoDB document) - - Deprecated: - Function will be removed after release version 3.15.* - """ - - from openpype.pipeline.context_tools import get_current_project_asset - - return get_current_project_asset(asset_name=asset_name) - - -@deprecated("openpype.pipeline.template_data.get_general_template_data") -def get_system_general_anatomy_data(system_settings=None): - """ - Deprecated: - Function will be removed after release version 3.15.* - """ - from openpype.pipeline.template_data import get_general_template_data - - return get_general_template_data(system_settings) - - @deprecated("openpype.client.get_linked_asset_ids") def get_linked_asset_ids(asset_doc): """Return linked asset ids for `asset_doc` from DB @@ -214,66 +144,6 @@ def get_linked_asset_ids(asset_doc): return get_linked_asset_ids(project_name, asset_doc=asset_doc) -@deprecated("openpype.client.get_linked_assets") -def get_linked_assets(asset_doc): - """Return linked assets for `asset_doc` from DB - - Args: - asset_doc (dict): Asset document from DB - - Returns: - (list) Asset documents of input links for passed asset doc. - - Deprecated: - Function will be removed after release version 3.15.* - """ - - from openpype.pipeline import legacy_io - from openpype.client import get_linked_assets - - project_name = legacy_io.active_project() - - return get_linked_assets(project_name, asset_doc=asset_doc) - - -@deprecated("openpype.client.get_last_version_by_subset_name") -def get_latest_version(asset_name, subset_name, dbcon=None, project_name=None): - """Retrieve latest version from `asset_name`, and `subset_name`. - - Do not use if you want to query more than 5 latest versions as this method - query 3 times to mongo for each call. For those cases is better to use - more efficient way, e.g. with help of aggregations. - - Args: - asset_name (str): Name of asset. - subset_name (str): Name of subset. - dbcon (AvalonMongoDB, optional): Avalon Mongo connection with Session. - project_name (str, optional): Find latest version in specific project. - - Returns: - None: If asset, subset or version were not found. - dict: Last version document for entered. - - Deprecated: - Function will be removed after release version 3.15.* - """ - - if not project_name: - if not dbcon: - from openpype.pipeline import legacy_io - - log.debug("Using `legacy_io` for query.") - dbcon = legacy_io - # Make sure is installed - dbcon.install() - - project_name = dbcon.active_project() - - return get_last_version_by_subset_name( - project_name, subset_name, asset_name=asset_name - ) - - @deprecated( "openpype.pipeline.workfile.get_workfile_template_key_from_context") def get_workfile_template_key_from_context( @@ -361,142 +231,6 @@ def get_workfile_template_key( ) -@deprecated("openpype.pipeline.template_data.get_template_data") -def get_workdir_data(project_doc, asset_doc, task_name, host_name): - """Prepare data for workdir template filling from entered information. - - Args: - project_doc (dict): Mongo document of project from MongoDB. - asset_doc (dict): Mongo document of asset from MongoDB. - task_name (str): Task name for which are workdir data preapred. - host_name (str): Host which is used to workdir. This is required - because workdir template may contain `{app}` key. - - Returns: - dict: Data prepared for filling workdir template. - - Deprecated: - Function will be removed after release version 3.15.* - """ - - from openpype.pipeline.template_data import get_template_data - - return get_template_data( - project_doc, asset_doc, task_name, host_name - ) - - -@deprecated("openpype.pipeline.workfile.get_workdir_with_workdir_data") -def get_workdir_with_workdir_data( - workdir_data, anatomy=None, project_name=None, template_key=None -): - """Fill workdir path from entered data and project's anatomy. - - It is possible to pass only project's name instead of project's anatomy but - one of them **must** be entered. It is preferred to enter anatomy if is - available as initialization of a new Anatomy object may be time consuming. - - Args: - workdir_data (dict): Data to fill workdir template. - anatomy (Anatomy): Anatomy object for specific project. Optional if - `project_name` is entered. - project_name (str): Project's name. Optional if `anatomy` is entered - otherwise Anatomy object is created with using the project name. - template_key (str): Key of work templates in anatomy templates. If not - passed `get_workfile_template_key_from_context` is used to get it. - dbcon(AvalonMongoDB): Mongo connection. Required only if 'template_key' - and 'project_name' are not passed. - - Returns: - TemplateResult: Workdir path. - - Raises: - ValueError: When both `anatomy` and `project_name` are set to None. - - Deprecated: - Function will be removed after release version 3.15.* - """ - - if not anatomy and not project_name: - raise ValueError(( - "Missing required arguments one of `project_name` or `anatomy`" - " must be entered." - )) - - if not project_name: - project_name = anatomy.project_name - - from openpype.pipeline.workfile import get_workdir_with_workdir_data - - return get_workdir_with_workdir_data( - workdir_data, project_name, anatomy, template_key - ) - - -@deprecated("openpype.pipeline.workfile.get_workdir_with_workdir_data") -def get_workdir( - project_doc, - asset_doc, - task_name, - host_name, - anatomy=None, - template_key=None -): - """Fill workdir path from entered data and project's anatomy. - - Args: - project_doc (dict): Mongo document of project from MongoDB. - asset_doc (dict): Mongo document of asset from MongoDB. - task_name (str): Task name for which are workdir data preapred. - host_name (str): Host which is used to workdir. This is required - because workdir template may contain `{app}` key. In `Session` - is stored under `AVALON_APP` key. - anatomy (Anatomy): Optional argument. Anatomy object is created using - project name from `project_doc`. It is preferred to pass this - argument as initialization of a new Anatomy object may be time - consuming. - template_key (str): Key of work templates in anatomy templates. Default - value is defined in `get_workdir_with_workdir_data`. - - Returns: - TemplateResult: Workdir path. - - Deprecated: - Function will be removed after release version 3.15.* - """ - - from openpype.pipeline.workfile import get_workdir - # Output is TemplateResult object which contain useful data - return get_workdir( - project_doc, - asset_doc, - task_name, - host_name, - anatomy, - template_key - ) - - -@deprecated("openpype.pipeline.context_tools.get_template_data_from_session") -def template_data_from_session(session=None): - """ Return dictionary with template from session keys. - - Args: - session (dict, Optional): The Session to use. If not provided use the - currently active global Session. - - Returns: - dict: All available data from session. - - Deprecated: - Function will be removed after release version 3.15.* - """ - - from openpype.pipeline.context_tools import get_template_data_from_session - - return get_template_data_from_session(session) - - @deprecated("openpype.pipeline.context_tools.compute_session_changes") def compute_session_changes( session, task=None, asset=None, app=None, template_key=None @@ -588,133 +322,6 @@ def update_current_task(task=None, asset=None, app=None, template_key=None): return change_current_context(asset, task, template_key) -@deprecated("openpype.client.get_workfile_info") -def get_workfile_doc(asset_id, task_name, filename, dbcon=None): - """Return workfile document for entered context. - - Do not use this method to get more than one document. In that cases use - custom query as this will return documents from database one by one. - - Args: - asset_id (ObjectId): Mongo ID of an asset under which workfile belongs. - task_name (str): Name of task under which the workfile belongs. - filename (str): Name of a workfile. - dbcon (AvalonMongoDB): Optionally enter avalon AvalonMongoDB object and - `legacy_io` is used if not entered. - - Returns: - dict: Workfile document or None. - - Deprecated: - Function will be removed after release version 3.15.* - """ - - # Use legacy_io if dbcon is not entered - if not dbcon: - from openpype.pipeline import legacy_io - dbcon = legacy_io - - project_name = dbcon.active_project() - return get_workfile_info(project_name, asset_id, task_name, filename) - - -@deprecated -def create_workfile_doc(asset_doc, task_name, filename, workdir, dbcon=None): - """Creates or replace workfile document in mongo. - - Do not use this method to update data. This method will remove all - additional data from existing document. - - Args: - asset_doc (dict): Document of asset under which workfile belongs. - task_name (str): Name of task for which is workfile related to. - filename (str): Filename of workfile. - workdir (str): Path to directory where `filename` is located. - dbcon (AvalonMongoDB): Optionally enter avalon AvalonMongoDB object and - `legacy_io` is used if not entered. - """ - - from openpype.pipeline import Anatomy - from openpype.pipeline.template_data import get_template_data - - # Use legacy_io if dbcon is not entered - if not dbcon: - from openpype.pipeline import legacy_io - dbcon = legacy_io - - # Filter of workfile document - doc_filter = { - "type": "workfile", - "parent": asset_doc["_id"], - "task_name": task_name, - "filename": filename - } - # Document data are copy of filter - doc_data = copy.deepcopy(doc_filter) - - # Prepare project for workdir data - project_name = dbcon.active_project() - project_doc = get_project(project_name) - workdir_data = get_template_data( - project_doc, asset_doc, task_name, dbcon.Session["AVALON_APP"] - ) - # Prepare anatomy - anatomy = Anatomy(project_name) - # Get workdir path (result is anatomy.TemplateResult) - template_workdir = get_workdir_with_workdir_data( - workdir_data, anatomy - ) - template_workdir_path = str(template_workdir).replace("\\", "/") - - # Replace slashses in workdir path where workfile is located - mod_workdir = workdir.replace("\\", "/") - - # Replace workdir from templates with rootless workdir - rootles_workdir = mod_workdir.replace( - template_workdir_path, - template_workdir.rootless.replace("\\", "/") - ) - - doc_data["schema"] = "pype:workfile-1.0" - doc_data["files"] = ["/".join([rootles_workdir, filename])] - doc_data["data"] = {} - - dbcon.replace_one( - doc_filter, - doc_data, - upsert=True - ) - - -@deprecated -def save_workfile_data_to_doc(workfile_doc, data, dbcon=None): - if not workfile_doc: - # TODO add log message - return - - if not data: - return - - # Use legacy_io if dbcon is not entered - if not dbcon: - from openpype.pipeline import legacy_io - dbcon = legacy_io - - # Convert data to mongo modification keys/values - # - this is naive implementation which does not expect nested - # dictionaries - set_data = {} - for key, value in data.items(): - new_key = "data.{}".format(key) - set_data[new_key] = value - - # Update workfile document with data - dbcon.update_one( - {"_id": workfile_doc["_id"]}, - {"$set": set_data} - ) - - @deprecated("openpype.pipeline.workfile.BuildWorkfile") def BuildWorkfile(): """Build workfile class was moved to workfile pipeline. @@ -747,38 +354,6 @@ def get_creator_by_name(creator_name, case_sensitive=False): return get_legacy_creator_by_name(creator_name, case_sensitive) -@deprecated -def change_timer_to_current_context(): - """Called after context change to change timers. - - Deprecated: - This method is specific for TimersManager module so please use the - functionality from there. Function will be removed after release - version 3.15.* - """ - - from openpype.pipeline import legacy_io - - webserver_url = os.environ.get("OPENPYPE_WEBSERVER_URL") - if not webserver_url: - log.warning("Couldn't find webserver url") - return - - rest_api_url = "{}/timers_manager/start_timer".format(webserver_url) - try: - import requests - except Exception: - log.warning("Couldn't start timer") - return - data = { - "project_name": legacy_io.Session["AVALON_PROJECT"], - "asset_name": legacy_io.Session["AVALON_ASSET"], - "task_name": legacy_io.Session["AVALON_TASK"] - } - - requests.post(rest_api_url, json=data) - - def _get_task_context_data_for_anatomy( project_doc, asset_doc, task_name, anatomy=None ): @@ -800,6 +375,8 @@ def _get_task_context_data_for_anatomy( dict: With Anatomy context data. """ + from openpype.pipeline.template_data import get_general_template_data + if anatomy is None: from openpype.pipeline import Anatomy anatomy = Anatomy(project_doc["name"]) @@ -840,7 +417,7 @@ def _get_task_context_data_for_anatomy( } } - system_general_data = get_system_general_anatomy_data() + system_general_data = get_general_template_data() data.update(system_general_data) return data diff --git a/openpype/lib/plugin_tools.py b/openpype/lib/plugin_tools.py index 1e157dfbfd..10fd3940b8 100644 --- a/openpype/lib/plugin_tools.py +++ b/openpype/lib/plugin_tools.py @@ -8,7 +8,6 @@ import warnings import functools from openpype.client import get_asset_by_id -from openpype.settings import get_project_settings log = logging.getLogger(__name__) @@ -101,8 +100,6 @@ def get_subset_name_with_asset_doc( 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. """ from openpype.pipeline.create import get_subset_name @@ -202,122 +199,6 @@ def prepare_template_data(fill_pairs): return fill_data -@deprecated("openpype.pipeline.publish.lib.filter_pyblish_plugins") -def filter_pyblish_plugins(plugins): - """Filter pyblish plugins by presets. - - This servers as plugin filter / modifier for pyblish. It will load plugin - definitions from presets and filter those needed to be excluded. - - Args: - plugins (dict): Dictionary of plugins produced by :mod:`pyblish-base` - `discover()` method. - - Deprecated: - Function will be removed after release version 3.15.* - """ - - from openpype.pipeline.publish.lib import filter_pyblish_plugins - - filter_pyblish_plugins(plugins) - - -@deprecated -def set_plugin_attributes_from_settings( - plugins, superclass, host_name=None, project_name=None -): - """Change attribute values on Avalon plugins by project settings. - - This function should be used only in host context. Modify - behavior of plugins. - - Args: - plugins (list): Plugins discovered by origin avalon discover method. - superclass (object): Superclass of plugin type (e.g. Cretor, Loader). - host_name (str): Name of host for which plugins are loaded and from. - Value from environment `AVALON_APP` is used if not entered. - project_name (str): Name of project for which settings will be loaded. - Value from environment `AVALON_PROJECT` is used if not entered. - - Deprecated: - Function will be removed after release version 3.15.* - """ - - # Function is not used anymore - from openpype.pipeline import LegacyCreator, LoaderPlugin - - # determine host application to use for finding presets - if host_name is None: - host_name = os.environ.get("AVALON_APP") - - if project_name is None: - project_name = os.environ.get("AVALON_PROJECT") - - # map plugin superclass to preset json. Currently supported is load and - # create (LoaderPlugin and LegacyCreator) - plugin_type = None - if superclass is LoaderPlugin or issubclass(superclass, LoaderPlugin): - plugin_type = "load" - elif superclass is LegacyCreator or issubclass(superclass, LegacyCreator): - plugin_type = "create" - - if not host_name or not project_name or plugin_type is None: - msg = "Skipped attributes override from settings." - if not host_name: - msg += " Host name is not defined." - - if not project_name: - msg += " Project name is not defined." - - if plugin_type is None: - msg += " Plugin type is unsupported for class {}.".format( - superclass.__name__ - ) - - print(msg) - return - - print(">>> Finding presets for {}:{} ...".format(host_name, plugin_type)) - - project_settings = get_project_settings(project_name) - plugin_type_settings = ( - project_settings - .get(host_name, {}) - .get(plugin_type, {}) - ) - global_type_settings = ( - project_settings - .get("global", {}) - .get(plugin_type, {}) - ) - if not global_type_settings and not plugin_type_settings: - return - - for plugin in plugins: - plugin_name = plugin.__name__ - - plugin_settings = None - # Look for plugin settings in host specific settings - if plugin_name in plugin_type_settings: - plugin_settings = plugin_type_settings[plugin_name] - - # Look for plugin settings in global settings - elif plugin_name in global_type_settings: - plugin_settings = global_type_settings[plugin_name] - - if not plugin_settings: - continue - - print(">>> We have preset for {}".format(plugin_name)) - for option, value in plugin_settings.items(): - if option == "enabled" and value is False: - setattr(plugin, "active", False) - print(" - is disabled by preset") - else: - setattr(plugin, option, value) - print(" - setting `{}`: `{}`".format(option, value)) - - def source_hash(filepath, *args): """Generate simple identifier for a source file. This is used to identify whether a source file has previously been diff --git a/openpype/tests/test_lib_restructuralization.py b/openpype/tests/test_lib_restructuralization.py index c8952e5a1c..669706d470 100644 --- a/openpype/tests/test_lib_restructuralization.py +++ b/openpype/tests/test_lib_restructuralization.py @@ -5,11 +5,9 @@ def test_backward_compatibility(printer): printer("Test if imports still work") try: - from openpype.lib import filter_pyblish_plugins from openpype.lib import execute_hook from openpype.lib import PypeHook - from openpype.lib import get_latest_version from openpype.lib import ApplicationLaunchFailed from openpype.lib import get_ffmpeg_tool_path @@ -18,10 +16,6 @@ def test_backward_compatibility(printer): from openpype.lib import get_version_from_path from openpype.lib import version_up - from openpype.lib import is_latest - from openpype.lib import any_outdated - from openpype.lib import get_asset - from openpype.lib import get_linked_assets from openpype.lib import get_ffprobe_streams from openpype.hosts.fusion.lib import switch_item diff --git a/openpype/tests/test_pyblish_filter.py b/openpype/tests/test_pyblish_filter.py index ea23da26e4..b74784145f 100644 --- a/openpype/tests/test_pyblish_filter.py +++ b/openpype/tests/test_pyblish_filter.py @@ -1,9 +1,9 @@ -from . import lib +import os import pyblish.api import pyblish.util import pyblish.plugin -from openpype.lib import filter_pyblish_plugins -import os +from openpype.pipeline.publish.lib import filter_pyblish_plugins +from . import lib def test_pyblish_plugin_filter_modifier(printer, monkeypatch): From 09dff1629d734e4172cebc4ec9d1134ff36d0f48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Mon, 13 Feb 2023 12:42:36 +0100 Subject: [PATCH 070/119] Update openpype/pipeline/tempdir.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: OndΕ™ej Samohel <33513211+antirotor@users.noreply.github.com> --- openpype/pipeline/tempdir.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/tempdir.py b/openpype/pipeline/tempdir.py index 6a346f3342..7e1778539c 100644 --- a/openpype/pipeline/tempdir.py +++ b/openpype/pipeline/tempdir.py @@ -11,7 +11,7 @@ def create_custom_tempdir(project_name, anatomy=None): """ Create custom tempdir Template path formatting is supporting: - - optional key formating + - optional key formatting - available keys: - root[work | ] - project[name | code] From 8daa8059ccede7693a01a869810acfe0c0fd0cd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Mon, 13 Feb 2023 12:42:45 +0100 Subject: [PATCH 071/119] Update openpype/pipeline/tempdir.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: OndΕ™ej Samohel <33513211+antirotor@users.noreply.github.com> --- openpype/pipeline/tempdir.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/tempdir.py b/openpype/pipeline/tempdir.py index 7e1778539c..f26f988557 100644 --- a/openpype/pipeline/tempdir.py +++ b/openpype/pipeline/tempdir.py @@ -21,7 +21,7 @@ def create_custom_tempdir(project_name, anatomy=None): anatomy (openpype.pipeline.Anatomy)[optional]: Anatomy object Returns: - str | None: formated path or None + str | None: formatted path or None """ openpype_tempdir = os.getenv("OPENPYPE_TMPDIR") if not openpype_tempdir: From 198050959a4835b436d3fc7e7529f341ed870560 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Mon, 13 Feb 2023 12:42:54 +0100 Subject: [PATCH 072/119] Update openpype/pipeline/tempdir.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: OndΕ™ej Samohel <33513211+antirotor@users.noreply.github.com> --- openpype/pipeline/tempdir.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/tempdir.py b/openpype/pipeline/tempdir.py index f26f988557..4bb62f0afa 100644 --- a/openpype/pipeline/tempdir.py +++ b/openpype/pipeline/tempdir.py @@ -47,7 +47,7 @@ def create_custom_tempdir(project_name, anatomy=None): # path is absolute custom_tempdir = openpype_tempdir - # create he dir path if it doesnt exists + # create the dir path if it doesn't exists if not os.path.exists(custom_tempdir): try: # create it if it doesnt exists From 053a903662b026837f43308e26df18d049589df1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Mon, 13 Feb 2023 12:43:21 +0100 Subject: [PATCH 073/119] Update openpype/pipeline/tempdir.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: OndΕ™ej Samohel <33513211+antirotor@users.noreply.github.com> --- openpype/pipeline/tempdir.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/tempdir.py b/openpype/pipeline/tempdir.py index 4bb62f0afa..88f8296dcf 100644 --- a/openpype/pipeline/tempdir.py +++ b/openpype/pipeline/tempdir.py @@ -53,6 +53,6 @@ def create_custom_tempdir(project_name, anatomy=None): # create it if it doesnt exists os.makedirs(custom_tempdir) except IOError as error: - raise IOError("Path couldn't be created: {}".format(error)) + raise IOError("Path couldn't be created: {}".format(error)) from error return custom_tempdir From 7ea78fee7b0a7b59fdfbe830ea83d66f399350b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Mon, 13 Feb 2023 12:43:30 +0100 Subject: [PATCH 074/119] Update openpype/pipeline/tempdir.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: OndΕ™ej Samohel <33513211+antirotor@users.noreply.github.com> --- openpype/pipeline/tempdir.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/tempdir.py b/openpype/pipeline/tempdir.py index 88f8296dcf..3f9384a7fd 100644 --- a/openpype/pipeline/tempdir.py +++ b/openpype/pipeline/tempdir.py @@ -50,7 +50,7 @@ def create_custom_tempdir(project_name, anatomy=None): # create the dir path if it doesn't exists if not os.path.exists(custom_tempdir): try: - # create it if it doesnt exists + # create it if it doesn't exists os.makedirs(custom_tempdir) except IOError as error: raise IOError("Path couldn't be created: {}".format(error)) from error From 859863129a033bcde335f4b3af441aecd991716b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Mon, 13 Feb 2023 12:43:39 +0100 Subject: [PATCH 075/119] Update openpype/pipeline/publish/lib.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: OndΕ™ej Samohel <33513211+antirotor@users.noreply.github.com> --- openpype/pipeline/publish/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index cc7f5678f5..2b0d111412 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -620,7 +620,7 @@ def get_instance_staging_dir(instance): It also supports `OPENPYPE_TMPDIR`, so studio can define own temp shared repository per project or even per more granular context. - Template formating is supported also with optional keys. Folder is + Template formatting is supported also with optional keys. Folder is created in case it doesnt exists. Available anatomy formatting keys: From 1735d6cc74d75ae732fcd1b0d7832ccd8d89fb53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Mon, 13 Feb 2023 12:45:00 +0100 Subject: [PATCH 076/119] Update openpype/pipeline/publish/lib.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: OndΕ™ej Samohel <33513211+antirotor@users.noreply.github.com> --- openpype/pipeline/publish/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 2b0d111412..27ab523352 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -628,7 +628,7 @@ def get_instance_staging_dir(instance): - project[name | code] Note: - Staging dir does not have to be necessarily in tempdir so be carefull + Staging dir does not have to be necessarily in tempdir so be careful about it's usage. Args: From abe803234ea73ebad6fa12b22932689daa527a64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Mon, 13 Feb 2023 12:45:12 +0100 Subject: [PATCH 077/119] Update website/docs/admin_environment.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: OndΕ™ej Samohel <33513211+antirotor@users.noreply.github.com> --- website/docs/admin_environment.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/admin_environment.md b/website/docs/admin_environment.md index 2cc558b530..1eb755b90b 100644 --- a/website/docs/admin_environment.md +++ b/website/docs/admin_environment.md @@ -9,8 +9,8 @@ import TabItem from '@theme/TabItem'; ## OPENPYPE_TMPDIR: - Custom staging dir directory - - Supports anatomy keys formating. ex `{root[work]}/{project[name]}/temp` - - supported formating keys: + - Supports anatomy keys formatting. ex `{root[work]}/{project[name]}/temp` + - supported formatting keys: - root[work] - project[name | code] From 3885f3cd7c502d04d0ec801cf62e4c047e2a2d23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Mon, 13 Feb 2023 12:45:29 +0100 Subject: [PATCH 078/119] Update website/docs/admin_settings_system.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: OndΕ™ej Samohel <33513211+antirotor@users.noreply.github.com> --- website/docs/admin_settings_system.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/admin_settings_system.md b/website/docs/admin_settings_system.md index c39cac61f5..d61713ccd5 100644 --- a/website/docs/admin_settings_system.md +++ b/website/docs/admin_settings_system.md @@ -176,4 +176,4 @@ In the image before you can see that we set most of the environment variables in In this example MTOA will automatically will the `MAYA_VERSION`(which is set by Maya Application environment) and `MTOA_VERSION` into the `MTOA` variable. We then use the `MTOA` to set all the other variables needed for it to function within Maya. ![tools](assets/settings/tools_01.png) -All of the tools defined in here can then be assigned to projects. You can also change the tools versions on any project level all the way down to individual asset or shot overrides. So if you just need to upgrade you render plugin for a single shot, while not risking the incompatibilities on the rest of the project, it is possible. +All the tools defined in here can then be assigned to projects. You can also change the tools versions on any project level all the way down to individual asset or shot overrides. So if you just need to upgrade you render plugin for a single shot, while not risking the incompatibilities on the rest of the project, it is possible. From 9591d42b84aed7c3321cff5b01b718053593f58d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 13 Feb 2023 12:51:51 +0100 Subject: [PATCH 079/119] spell errors --- openpype/pipeline/publish/lib.py | 6 +++--- openpype/pipeline/tempdir.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 27ab523352..d0a9396a42 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -601,7 +601,7 @@ def context_plugin_should_run(plugin, context): Args: plugin (pyblish.api.Plugin): Plugin with filters. - context (pyblish.api.Context): Pyblish context with insances. + context (pyblish.api.Context): Pyblish context with instances. Returns: bool: Context plugin should run based on valid instances. @@ -621,7 +621,7 @@ def get_instance_staging_dir(instance): It also supports `OPENPYPE_TMPDIR`, so studio can define own temp shared repository per project or even per more granular context. Template formatting is supported also with optional keys. Folder is - created in case it doesnt exists. + created in case it doesn't exists. Available anatomy formatting keys: - root[work | ] @@ -629,7 +629,7 @@ def get_instance_staging_dir(instance): Note: Staging dir does not have to be necessarily in tempdir so be careful - about it's usage. + about its usage. Args: instance (pyblish.lib.Instance): Instance for which we want to get diff --git a/openpype/pipeline/tempdir.py b/openpype/pipeline/tempdir.py index 3f9384a7fd..3216c596da 100644 --- a/openpype/pipeline/tempdir.py +++ b/openpype/pipeline/tempdir.py @@ -53,6 +53,7 @@ def create_custom_tempdir(project_name, anatomy=None): # create it if it doesn't exists os.makedirs(custom_tempdir) except IOError as error: - raise IOError("Path couldn't be created: {}".format(error)) from error + raise IOError( + "Path couldn't be created: {}".format(error)) from error return custom_tempdir From 75637cc1a46eca825fee99346424828fe41a83fc Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 14 Feb 2023 07:00:21 +0000 Subject: [PATCH 080/119] Strict Error Checking Default Provide default of strict error checking for instances created prior to PR. --- openpype/hosts/maya/plugins/publish/collect_render.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index fc297ef612..5bc295a56f 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -320,7 +320,8 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "renderSetupIncludeLights" ), "strict_error_checking": render_instance.data.get( - "strict_error_checking") + "strict_error_checking", False + ) } # Collect Deadline url if Deadline module is enabled From d3cc8b59c5add4269908947f5fdad108ec2ade30 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 14 Feb 2023 10:42:54 +0100 Subject: [PATCH 081/119] replaced call to mongo 'dbcon.parenthood' with 'get_representation_parents' function --- openpype/pipeline/load/utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index e30923f922..fefdb8537b 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -28,7 +28,6 @@ from openpype.lib import ( TemplateUnsolved, ) from openpype.pipeline import ( - schema, legacy_io, Anatomy, ) @@ -643,7 +642,10 @@ def get_representation_path(representation, root=None, dbcon=None): def path_from_config(): try: - version_, subset, asset, project = dbcon.parenthood(representation) + project_name = dbcon.active_project() + version_, subset, asset, project = get_representation_parents( + project_name, representation + ) except ValueError: log.debug( "Representation %s wasn't found in database, " From 33a7ecd19eacfe237cc7ea68513ff17d692e6a77 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 14 Feb 2023 10:03:01 +0000 Subject: [PATCH 082/119] Code cosmetics --- openpype/hosts/maya/plugins/publish/collect_render.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index 5bc295a56f..aa35f687ca 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -42,7 +42,6 @@ Provides: import re import os import platform -import json from maya import cmds import maya.app.renderSetup.model.renderSetup as renderSetup From abe9a2b8951414327c2df4718085dec5ccd20485 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 14 Feb 2023 10:03:12 +0000 Subject: [PATCH 083/119] Default should be True --- openpype/hosts/maya/plugins/publish/collect_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index aa35f687ca..f2b5262187 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -319,7 +319,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "renderSetupIncludeLights" ), "strict_error_checking": render_instance.data.get( - "strict_error_checking", False + "strict_error_checking", True ) } From 6bdbdd4337d7d268667cf08f3ef784a8e306184f Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Tue, 14 Feb 2023 15:32:54 +0000 Subject: [PATCH 084/119] Update openpype/hosts/maya/plugins/load/load_arnold_standin.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: OndΕ™ej Samohel <33513211+antirotor@users.noreply.github.com> --- .../hosts/maya/plugins/load/load_arnold_standin.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_arnold_standin.py b/openpype/hosts/maya/plugins/load/load_arnold_standin.py index 66e8b69639..ab69d62ef5 100644 --- a/openpype/hosts/maya/plugins/load/load_arnold_standin.py +++ b/openpype/hosts/maya/plugins/load/load_arnold_standin.py @@ -65,20 +65,20 @@ class ArnoldStandinLoader(load.LoaderPlugin): # Create transform with shape transform_name = label + "_standin" - standinShape = mtoa.ui.arnoldmenu.createStandIn() - standin = cmds.listRelatives(standinShape, parent=True)[0] + standin_shape = mtoa.ui.arnoldmenu.createStandIn() + standin = cmds.listRelatives(standin_shape, parent=True)[0] standin = cmds.rename(standin, transform_name) - standinShape = cmds.listRelatives(standin, shapes=True)[0] + standin_shape = cmds.listRelatives(standin, shapes=True)[0] cmds.parent(standin, root) # Set the standin filepath path, operator = self._setup_proxy( - standinShape, self.fname, namespace + standin_shape, self.fname, namespace ) - cmds.setAttr(standinShape + ".dso", path, type="string") + cmds.setAttr(standin_shape + ".dso", path, type="string") sequence = is_sequence(os.listdir(os.path.dirname(self.fname))) - cmds.setAttr(standinShape + ".useFrameExtension", sequence) + cmds.setAttr(standin_shape + ".useFrameExtension", sequence) nodes = [root, standin] if operator is not None: From 5903dbce9e176d26d45681a86efe92f607a512e2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 14 Feb 2023 17:03:05 +0100 Subject: [PATCH 085/119] autofill precreate attributes if are not passed --- openpype/pipeline/create/context.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index ba566f93d4..1567acdb79 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -17,6 +17,7 @@ from openpype.lib.attribute_definitions import ( UnknownDef, serialize_attr_defs, deserialize_attr_defs, + get_default_values, ) from openpype.host import IPublishHost from openpype.pipeline import legacy_io @@ -1866,6 +1867,13 @@ class CreateContext: if pre_create_data is None: pre_create_data = {} + precreate_attr_defs = creator.get_pre_create_attr_defs() or [] + # Create default values of precreate data + _pre_create_data = get_default_values(precreate_attr_defs) + # Update passed precreate data to default values + # TODO validate types + _pre_create_data.update(pre_create_data) + subset_name = creator.get_subset_name( variant, task_name, @@ -1881,7 +1889,7 @@ class CreateContext: return creator.create( subset_name, instance_data, - pre_create_data + _pre_create_data ) def _create_with_unified_error( From 8ddcc9c151aea67ae893ee1518b70731fa776deb Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 15 Feb 2023 03:29:31 +0000 Subject: [PATCH 086/119] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 8dfd638414..6d060656cb 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.1-nightly.5" +__version__ = "3.15.1-nightly.6" From ac4078259200edbdf88d58954b1e96e09468e5b9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 10:14:19 +0100 Subject: [PATCH 087/119] fix used constant 'ActiveWindow' -> 'WindowActive' --- openpype/tools/publisher/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 097e289f32..a82f60d5a5 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -366,7 +366,7 @@ class PublisherWindow(QtWidgets.QDialog): def make_sure_is_visible(self): if self._window_is_visible: - self.setWindowState(QtCore.Qt.ActiveWindow) + self.setWindowState(QtCore.Qt.WindowActive) else: self.show() From 3e6a120eaa808bf69e0bdbe893b0dd8c21c6939a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 11:46:20 +0100 Subject: [PATCH 088/119] fix default settings of nuke --- openpype/settings/defaults/project_settings/nuke.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index cd8ea02272..2ec2028219 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -1,7 +1,6 @@ { "general": { "menu": { - "create": "ctrl+alt+c", "publish": "ctrl+alt+p", "load": "ctrl+alt+l", "manage": "ctrl+alt+m", @@ -246,6 +245,7 @@ "sourcetype": "python", "title": "Gizmo Note", "command": "nuke.nodes.StickyNote(label='You can create your own toolbar menu in the Nuke GizmoMenu of OpenPype')", + "icon": "", "shortcut": "" } ] From df532268a2e05b6b48074336453ab3e18b86e08f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 12:00:15 +0100 Subject: [PATCH 089/119] add family to instance data --- openpype/pipeline/create/context.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 1567acdb79..79c9805604 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1884,6 +1884,7 @@ class CreateContext: instance_data = { "asset": asset_doc["name"], "task": task_name, + "family": self.family, "variant": variant } return creator.create( From fb93780640ed5de588cb11499ccd68d1f6a91d75 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 12:08:45 +0100 Subject: [PATCH 090/119] use family form creator --- openpype/pipeline/create/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 79c9805604..89eec52676 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1884,7 +1884,7 @@ class CreateContext: instance_data = { "asset": asset_doc["name"], "task": task_name, - "family": self.family, + "family": creator.family, "variant": variant } return creator.create( From 222b39f024e5fcf8890145f08eba8298282ddb39 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 15 Feb 2023 12:22:13 +0100 Subject: [PATCH 091/119] nuke: adding back Create shortcut it was removed accidentally --- openpype/settings/defaults/project_settings/nuke.json | 3 ++- .../schemas/projects_schema/schema_project_nuke.json | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 2ec2028219..d475c337d9 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -1,6 +1,7 @@ { "general": { "menu": { + "create": "ctrl+alt+c", "publish": "ctrl+alt+p", "load": "ctrl+alt+l", "manage": "ctrl+alt+m", @@ -532,4 +533,4 @@ "profiles": [] }, "filters": {} -} \ No newline at end of file +} diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json index b1a8cc1812..26c64e6219 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json @@ -17,6 +17,11 @@ "key": "menu", "label": "OpenPype Menu shortcuts", "children": [ + { + "type": "text", + "key": "create", + "label": "Create..." + }, { "type": "text", "key": "publish", @@ -288,4 +293,4 @@ "name": "schema_publish_gui_filter" } ] -} \ No newline at end of file +} From e93c5d0d4055db7e2ff8cd7067fda24ccc243250 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 15 Feb 2023 12:26:27 +0100 Subject: [PATCH 092/119] OP-4928 - fix wrong usage of legacy_io Import was removed, but usage stayed. Now it should be replaced from context --- openpype/hosts/photoshop/plugins/create/create_image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index cdea82cb05..3d82d6b6f0 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -193,7 +193,7 @@ class ImageCreator(Creator): instance_data.pop("uuid") if not instance_data.get("task"): - instance_data["task"] = legacy_io.Session.get("AVALON_TASK") + instance_data["task"] = self.create_context.get_current_task_name() if not instance_data.get("variant"): instance_data["variant"] = '' From 423f2bcbdadc723d6499f8f92a9ed391533a75e8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 15 Feb 2023 12:26:50 +0100 Subject: [PATCH 093/119] removing python3 only code --- openpype/pipeline/tempdir.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/tempdir.py b/openpype/pipeline/tempdir.py index 3216c596da..55a1346b08 100644 --- a/openpype/pipeline/tempdir.py +++ b/openpype/pipeline/tempdir.py @@ -54,6 +54,6 @@ def create_custom_tempdir(project_name, anatomy=None): os.makedirs(custom_tempdir) except IOError as error: raise IOError( - "Path couldn't be created: {}".format(error)) from error + "Path couldn't be created: {}".format(error)) return custom_tempdir From 410ea87e18a582628fbd456549207e2dac2ef164 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 15 Feb 2023 12:27:44 +0100 Subject: [PATCH 094/119] OP-4928 - fix wrong usage of legacy_io Import should be removed. Now it should be replaced from context. --- openpype/hosts/aftereffects/plugins/create/create_render.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index 10ded8b912..02f045b0ec 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -6,8 +6,7 @@ from openpype.hosts.aftereffects import api from openpype.pipeline import ( Creator, CreatedInstance, - CreatorError, - legacy_io, + CreatorError ) from openpype.hosts.aftereffects.api.pipeline import cache_and_get_instances from openpype.lib import prepare_template_data @@ -195,7 +194,7 @@ class RenderCreator(Creator): instance_data.pop("uuid") if not instance_data.get("task"): - instance_data["task"] = legacy_io.Session.get("AVALON_TASK") + instance_data["task"] = self.create_context.get_current_task_name() if not instance_data.get("creator_attributes"): is_old_farm = instance_data["family"] != "renderLocal" From eb5d1e3816b07760c6ffdc8c71999fe1167dfdf9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 12:29:52 +0100 Subject: [PATCH 095/119] resave to remove empty line --- openpype/settings/defaults/project_settings/nuke.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index d475c337d9..2999d1427d 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -533,4 +533,4 @@ "profiles": [] }, "filters": {} -} +} \ No newline at end of file From 66c42dde73174c8a3b288419a616f8c23b98064a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 15 Feb 2023 12:32:41 +0100 Subject: [PATCH 096/119] OP-4928 - removed legacy_io in workfile creator in PS Legacy_io should be eradicated, replaced by abstracted methods --- .../photoshop/plugins/create/workfile_creator.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/create/workfile_creator.py b/openpype/hosts/photoshop/plugins/create/workfile_creator.py index 8ee9a0d832..f5d56adcbc 100644 --- a/openpype/hosts/photoshop/plugins/create/workfile_creator.py +++ b/openpype/hosts/photoshop/plugins/create/workfile_creator.py @@ -2,8 +2,7 @@ import openpype.hosts.photoshop.api as api from openpype.client import get_asset_by_name from openpype.pipeline import ( AutoCreator, - CreatedInstance, - legacy_io + CreatedInstance ) from openpype.hosts.photoshop.api.pipeline import cache_and_get_instances @@ -38,10 +37,11 @@ class PSWorkfileCreator(AutoCreator): existing_instance = instance break - project_name = legacy_io.Session["AVALON_PROJECT"] - asset_name = legacy_io.Session["AVALON_ASSET"] - task_name = legacy_io.Session["AVALON_TASK"] - host_name = legacy_io.Session["AVALON_APP"] + context = self.create_context + project_name = context.get_current_project_name() + asset_name = context.get_current_asset_name() + task_name = context.get_current_task_name() + host_name = context.host_name if existing_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( From 03013095023cdce494142740a70efdbce60cb03c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 15 Feb 2023 12:33:35 +0100 Subject: [PATCH 097/119] OP-4928 - removed legacy_io in workfile creator in AE Legacy_io should be eradicated, replaced by abstracted methods --- .../aftereffects/plugins/create/workfile_creator.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/create/workfile_creator.py b/openpype/hosts/aftereffects/plugins/create/workfile_creator.py index c698af896b..2e7b9d4a7e 100644 --- a/openpype/hosts/aftereffects/plugins/create/workfile_creator.py +++ b/openpype/hosts/aftereffects/plugins/create/workfile_creator.py @@ -2,8 +2,7 @@ import openpype.hosts.aftereffects.api as api from openpype.client import get_asset_by_name from openpype.pipeline import ( AutoCreator, - CreatedInstance, - legacy_io, + CreatedInstance ) from openpype.hosts.aftereffects.api.pipeline import cache_and_get_instances @@ -38,10 +37,11 @@ class AEWorkfileCreator(AutoCreator): existing_instance = instance break - project_name = legacy_io.Session["AVALON_PROJECT"] - asset_name = legacy_io.Session["AVALON_ASSET"] - task_name = legacy_io.Session["AVALON_TASK"] - host_name = legacy_io.Session["AVALON_APP"] + context = self.create_context + project_name = context.get_current_project_name() + asset_name = context.get_current_asset_name() + task_name = context.get_current_task_name() + host_name = context.host_name if existing_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) From 6ab581df7da24302158d32c9a68a9baca33b1cb3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 15:45:22 +0100 Subject: [PATCH 098/119] on first reset always go to create tab --- openpype/tools/publisher/window.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 097e289f32..5ef25c9f8c 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -647,10 +647,7 @@ class PublisherWindow(QtWidgets.QDialog): # otherwise 'create' is used # - this happens only on first show if first_reset: - if self._overview_widget.has_items(): - self._go_to_publish_tab() - else: - self._go_to_create_tab() + self._go_to_create_tab() elif ( not self._is_on_create_tab() From 1cc9a7a90fd6deef343b45f0944bcefed6497521 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 15:45:45 +0100 Subject: [PATCH 099/119] change tab on reset only if is on report tab (Details for user) --- openpype/tools/publisher/window.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 5ef25c9f8c..ef9c99d998 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -649,11 +649,8 @@ class PublisherWindow(QtWidgets.QDialog): if first_reset: self._go_to_create_tab() - elif ( - not self._is_on_create_tab() - and not self._is_on_publish_tab() - ): - # If current tab is not 'Create' or 'Publish' go to 'Publish' + elif self._is_on_report_tab(): + # Go to 'Publish' tab if is on 'Details' tab # - this can happen when publishing started and was reset # at that moment it doesn't make sense to stay at publish # specific tabs. From 37a7841db8024341cbc4fa0c7881c6925ab7a188 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 15:46:02 +0100 Subject: [PATCH 100/119] reordered methods to match order of tabs in UI --- openpype/tools/publisher/window.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index ef9c99d998..86eed31afd 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -566,24 +566,24 @@ class PublisherWindow(QtWidgets.QDialog): def _go_to_publish_tab(self): self._set_current_tab("publish") - def _go_to_details_tab(self): - self._set_current_tab("details") - def _go_to_report_tab(self): self._set_current_tab("report") + def _go_to_details_tab(self): + self._set_current_tab("details") + def _is_on_create_tab(self): return self._is_current_tab("create") def _is_on_publish_tab(self): return self._is_current_tab("publish") - def _is_on_details_tab(self): - return self._is_current_tab("details") - def _is_on_report_tab(self): return self._is_current_tab("report") + def _is_on_details_tab(self): + return self._is_current_tab("details") + def _set_publish_overlay_visibility(self, visible): if visible: widget = self._publish_overlay From b70c6e4bfd433c1470efa1fde319834ec7068264 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 15 Feb 2023 18:32:56 +0100 Subject: [PATCH 101/119] OP-4938 - fix obsolete access to instance change --- openpype/hosts/aftereffects/plugins/create/create_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index 02f045b0ec..c20b0ec51b 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -126,7 +126,7 @@ class RenderCreator(Creator): subset_change = _changes.get("subset") if subset_change: api.get_stub().rename_item(created_inst.data["members"][0], - subset_change[1]) + subset_change.new_value) def remove_instances(self, instances): for instance in instances: From eef8990101eef7e79ad34b7b8429c164b93e14c0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 16 Feb 2023 09:52:39 +0100 Subject: [PATCH 102/119] public 'discover' function can expect all possible arguments --- openpype/pipeline/plugin_discover.py | 36 ++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/openpype/pipeline/plugin_discover.py b/openpype/pipeline/plugin_discover.py index 7edd9ac290..e5257b801a 100644 --- a/openpype/pipeline/plugin_discover.py +++ b/openpype/pipeline/plugin_discover.py @@ -135,11 +135,12 @@ class PluginDiscoverContext(object): allow_duplicates (bool): Validate class name duplications. ignore_classes (list): List of classes that will be ignored and not added to result. + return_report (bool): Output will be full report if set to 'True'. Returns: - DiscoverResult: Object holding succesfully discovered plugins, - ignored plugins, plugins with missing abstract implementation - and duplicated plugin. + Union[DiscoverResult, list[Any]]: Object holding successfully + discovered plugins, ignored plugins, plugins with missing + abstract implementation and duplicated plugin. """ if not ignore_classes: @@ -268,9 +269,34 @@ class _GlobalDiscover: return cls._context -def discover(superclass, allow_duplicates=True): +def discover( + superclass, + allow_duplicates=True, + ignore_classes=None, + return_report=False +): + """Find and return subclasses of `superclass` + + Args: + superclass (type): Class which determines discovered subclasses. + allow_duplicates (bool): Validate class name duplications. + ignore_classes (list): List of classes that will be ignored + and not added to result. + return_report (bool): Output will be full report if set to 'True'. + + Returns: + Union[DiscoverResult, list[Any]]: Object holding successfully + discovered plugins, ignored plugins, plugins with missing + abstract implementation and duplicated plugin. + """ + context = _GlobalDiscover.get_context() - return context.discover(superclass, allow_duplicates) + return context.discover( + superclass, + allow_duplicates, + ignore_classes, + return_report + ) def get_last_discovered_plugins(superclass): From 542405775a36c08075dc118dc0801be312d0e5e1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 16 Feb 2023 09:53:15 +0100 Subject: [PATCH 103/119] discover creators and convertors can pass other arguments to 'discover' function --- openpype/pipeline/create/creator_plugins.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 53acb618ed..74e6cb289a 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -605,12 +605,12 @@ class AutoCreator(BaseCreator): pass -def discover_creator_plugins(): - return discover(BaseCreator) +def discover_creator_plugins(*args, **kwargs): + return discover(BaseCreator, *args, **kwargs) -def discover_convertor_plugins(): - return discover(SubsetConvertorPlugin) +def discover_convertor_plugins(*args, **kwargs): + return discover(SubsetConvertorPlugin, *args, **kwargs) def discover_legacy_creator_plugins(): From 86a9c77c1e970a78d08888af5326c19c0fd51aa3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 16 Feb 2023 09:53:47 +0100 Subject: [PATCH 104/119] reuse 'DiscoverResult' from plugin discover --- openpype/pipeline/create/context.py | 4 ++-- openpype/pipeline/publish/__init__.py | 2 -- openpype/pipeline/publish/lib.py | 23 +---------------------- 3 files changed, 3 insertions(+), 26 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 89eec52676..8b5da74bc7 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -21,6 +21,7 @@ from openpype.lib.attribute_definitions import ( ) from openpype.host import IPublishHost from openpype.pipeline import legacy_io +from openpype.pipeline.plugin_discover import DiscoverResult from .creator_plugins import ( Creator, @@ -1620,8 +1621,7 @@ class CreateContext: from openpype.pipeline import OpenPypePyblishPluginMixin from openpype.pipeline.publish import ( - publish_plugins_discover, - DiscoverResult + publish_plugins_discover ) # Reset publish plugins diff --git a/openpype/pipeline/publish/__init__.py b/openpype/pipeline/publish/__init__.py index dc6fc0f97a..86f3bde0dc 100644 --- a/openpype/pipeline/publish/__init__.py +++ b/openpype/pipeline/publish/__init__.py @@ -25,7 +25,6 @@ from .publish_plugins import ( from .lib import ( get_publish_template_name, - DiscoverResult, publish_plugins_discover, load_help_content_from_plugin, load_help_content_from_filepath, @@ -68,7 +67,6 @@ __all__ = ( "get_publish_template_name", - "DiscoverResult", "publish_plugins_discover", "load_help_content_from_plugin", "load_help_content_from_filepath", diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index d0a9396a42..50623e5110 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -21,6 +21,7 @@ from openpype.settings import ( from openpype.pipeline import ( tempdir ) +from openpype.pipeline.plugin_discover import DiscoverResult from .contants import ( DEFAULT_PUBLISH_TEMPLATE, @@ -202,28 +203,6 @@ def get_publish_template_name( return template or default_template -class DiscoverResult: - """Hold result of publish plugins discovery. - - Stores discovered plugins duplicated plugins and file paths which - crashed on execution of file. - """ - def __init__(self): - self.plugins = [] - self.crashed_file_paths = {} - self.duplicated_plugins = [] - - def __iter__(self): - for plugin in self.plugins: - yield plugin - - def __getitem__(self, item): - return self.plugins[item] - - def __setitem__(self, item, value): - self.plugins[item] = value - - class HelpContent: def __init__(self, title, description, detail=None): self.title = title From b3a86bdbf540c8e8f0df6c52cb0873f2b84b513b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 16 Feb 2023 09:54:18 +0100 Subject: [PATCH 105/119] store reports of discovered plugins --- openpype/pipeline/create/context.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 8b5da74bc7..5f1371befa 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1379,6 +1379,8 @@ class CreateContext: # Instances by their ID self._instances_by_id = {} + self.creator_discover_result = None + self.convertor_discover_result = None # Discovered creators self.creators = {} # Prepare categories of creators @@ -1666,7 +1668,9 @@ class CreateContext: creators = {} autocreators = {} manual_creators = {} - for creator_class in discover_creator_plugins(): + report = discover_creator_plugins(return_report=True) + self.creator_discover_result = report + for creator_class in report.plugins: if inspect.isabstract(creator_class): self.log.info( "Skipping abstract Creator {}".format(str(creator_class)) @@ -1711,7 +1715,9 @@ class CreateContext: def _reset_convertor_plugins(self): convertors_plugins = {} - for convertor_class in discover_convertor_plugins(): + report = discover_convertor_plugins(return_report=True) + self.convertor_discover_result = report + for convertor_class in report.plugins: if inspect.isabstract(convertor_class): self.log.info( "Skipping abstract Creator {}".format(str(convertor_class)) From 0dc73617a65dfc947c3c2715a6948928403c9f54 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 16 Feb 2023 09:54:44 +0100 Subject: [PATCH 106/119] use reports to store crashed files to publish report --- openpype/tools/publisher/control.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 9ab37f2a3e..023a20ca5e 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -169,6 +169,8 @@ class PublishReport: def __init__(self, controller): self.controller = controller + self._create_discover_result = None + self._convert_discover_result = None self._publish_discover_result = None self._plugin_data = [] self._plugin_data_with_plugin = [] @@ -181,6 +183,10 @@ class PublishReport: def reset(self, context, create_context): """Reset report and clear all data.""" + self._create_discover_result = create_context.creator_discover_result + self._convert_discover_result = ( + create_context.convertor_discover_result + ) self._publish_discover_result = create_context.publish_discover_result self._plugin_data = [] self._plugin_data_with_plugin = [] @@ -293,9 +299,19 @@ class PublishReport: if plugin not in self._stored_plugins: plugins_data.append(self._create_plugin_data_item(plugin)) - crashed_file_paths = {} + reports = [] + if self._create_discover_result is not None: + reports.append(self._create_discover_result) + + if self._convert_discover_result is not None: + reports.append(self._convert_discover_result) + if self._publish_discover_result is not None: - items = self._publish_discover_result.crashed_file_paths.items() + reports.append(self._publish_discover_result) + + crashed_file_paths = {} + for report in reports: + items = report.crashed_file_paths.items() for filepath, exc_info in items: crashed_file_paths[filepath] = "".join( traceback.format_exception(*exc_info) From 56470c47e23bcb337b48731252439c22e0300130 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 16 Feb 2023 09:55:32 +0100 Subject: [PATCH 107/119] added Args to documentation --- openpype/pipeline/create/creator_plugins.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 74e6cb289a..628245faf2 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -153,6 +153,12 @@ class BaseCreator: Single object should be used for multiple instances instead of single instance per one creator object. Do not store temp data or mid-process data to `self` if it's not Plugin specific. + + Args: + project_settings (Dict[str, Any]): Project settings. + system_settings (Dict[str, Any]): System settings. + create_context (CreateContext): Context which initialized creator. + headless (bool): Running in headless mode. """ # Label shown in UI From 7739c3a54f0b12e106d05f8742dfa63266bbde7f Mon Sep 17 00:00:00 2001 From: mre7a <68907585+mre7a@users.noreply.github.com> Date: Thu, 16 Feb 2023 10:43:13 +0100 Subject: [PATCH 108/119] Update openpype/hosts/maya/api/workfile_template_builder.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/maya/api/workfile_template_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index 56a53c070c..1e6094e996 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -2,7 +2,7 @@ import json from maya import cmds -from openpype.pipeline import registered_host, legacy_io +from openpype.pipeline import registered_host, get_current_asset_name from openpype.pipeline.workfile.workfile_template_builder import ( TemplateAlreadyImported, AbstractTemplateBuilder, From 035c888d9be86d546c49569b370dbe1264844fa7 Mon Sep 17 00:00:00 2001 From: mre7a <68907585+mre7a@users.noreply.github.com> Date: Thu, 16 Feb 2023 10:43:55 +0100 Subject: [PATCH 109/119] Update openpype/hosts/maya/api/workfile_template_builder.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/maya/api/workfile_template_builder.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index 1e6094e996..094f45221c 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -50,6 +50,7 @@ class MayaTemplateBuilder(AbstractTemplateBuilder): return True # update imported sets information + asset_name = get_current_asset_name() for node in imported_sets: if not cmds.attributeQuery("id", node=node, exists=True): continue @@ -57,9 +58,9 @@ class MayaTemplateBuilder(AbstractTemplateBuilder): continue if not cmds.attributeQuery("asset", node=node, exists=True): continue - asset = legacy_io.Session["AVALON_ASSET"] - cmds.setAttr("{}.asset".format(node), asset, type="string") + cmds.setAttr( + "{}.asset".format(node), asset_name, type="string") return True From c2b2dbb3f32aec8041b6cf6b1da08b3968d330ca Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 16 Feb 2023 10:47:24 +0100 Subject: [PATCH 110/119] fix compatibility of QAction in Publisher --- openpype/tools/publisher/widgets/widgets.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 587bcb059d..8da3886419 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -250,21 +250,25 @@ class PublishReportBtn(PublishIconBtn): self._actions = [] def add_action(self, label, identifier): - action = QtWidgets.QAction(label) - action.setData(identifier) - action.triggered.connect( - functools.partial(self._on_action_trigger, action) + self._actions.append( + (label, identifier) ) - self._actions.append(action) - def _on_action_trigger(self, action): - identifier = action.data() + def _on_action_trigger(self, identifier): self.triggered.emit(identifier) def mouseReleaseEvent(self, event): super(PublishReportBtn, self).mouseReleaseEvent(event) menu = QtWidgets.QMenu(self) - menu.addActions(self._actions) + actions = [] + for item in self._actions: + label, identifier = item + action = QtWidgets.QAction(label, menu) + action.triggered.connect( + functools.partial(self._on_action_trigger, identifier) + ) + actions.append(action) + menu.addActions(actions) menu.exec_(event.globalPos()) From 8eef66c3df8583a8503caefd69e6bd7255fd1498 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 16 Feb 2023 12:29:43 +0100 Subject: [PATCH 111/119] Fix creation of DiscoverResult for pyblish plugins --- openpype/pipeline/create/context.py | 7 ++++--- openpype/pipeline/publish/lib.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 5f1371befa..7672c49eb3 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -8,6 +8,9 @@ import inspect from uuid import uuid4 from contextlib import contextmanager +import pyblish.logic +import pyblish.api + from openpype.client import get_assets, get_asset_by_name from openpype.settings import ( get_system_settings, @@ -1619,8 +1622,6 @@ class CreateContext: self._reset_convertor_plugins() def _reset_publish_plugins(self, discover_publish_plugins): - import pyblish.logic - from openpype.pipeline import OpenPypePyblishPluginMixin from openpype.pipeline.publish import ( publish_plugins_discover @@ -1629,7 +1630,7 @@ class CreateContext: # Reset publish plugins self._attr_plugins_by_family = {} - discover_result = DiscoverResult() + discover_result = DiscoverResult(pyblish.api.Plugin) plugins_with_defs = [] plugins_by_targets = [] plugins_mismatch_targets = [] diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 50623e5110..5f95c6695e 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -270,7 +270,7 @@ def publish_plugins_discover(paths=None): """ # The only difference with `pyblish.api.discover` - result = DiscoverResult() + result = DiscoverResult(pyblish.api.Plugin) plugins = dict() plugin_names = [] From 70ab3cd2ab75d29e6ceb60e793a9ffc85ecb3f5c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 16 Feb 2023 14:14:45 +0100 Subject: [PATCH 112/119] fix newly added creators on refresh --- openpype/tools/publisher/widgets/create_widget.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/create_widget.py b/openpype/tools/publisher/widgets/create_widget.py index dbf075c216..ef9c5b98fe 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -457,13 +457,14 @@ class CreateWidget(QtWidgets.QWidget): # TODO add details about creator new_creators.add(identifier) if identifier in existing_items: + is_new = False item = existing_items[identifier] else: + is_new = True item = QtGui.QStandardItem() item.setFlags( QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable ) - self._creators_model.appendRow(item) item.setData(creator_item.label, QtCore.Qt.DisplayRole) item.setData(creator_item.show_order, CREATOR_SORT_ROLE) @@ -473,6 +474,8 @@ class CreateWidget(QtWidgets.QWidget): CREATOR_THUMBNAIL_ENABLED_ROLE ) item.setData(creator_item.family, FAMILY_ROLE) + if is_new: + self._creators_model.appendRow(item) # Remove families that are no more available for identifier in (old_creators - new_creators): From a305de03f0116e06e490266d8f34c967f12d32f6 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 16 Feb 2023 15:31:33 +0000 Subject: [PATCH 113/119] Lower tolerance for framerate difference. --- openpype/hosts/maya/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 4c8b11ecd3..b920428b20 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3422,7 +3422,7 @@ def convert_to_maya_fps(fps): min_difference = min(differences) min_index = differences.index(min_difference) supported_framerate = float_framerates[min_index] - if round(min_difference) != 0: + if min_difference > 0.1: raise ValueError( "Framerate \"{}\" strays too far from any supported framerate" " in Maya. Closest supported framerate is \"{}\"".format( From 8a5831c6fe6e4bf434d45af457da013df4def88a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 16 Feb 2023 17:11:00 +0100 Subject: [PATCH 114/119] check for 'pyside6' when filling kwargs for file dialog --- openpype/tools/workfiles/files_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/workfiles/files_widget.py b/openpype/tools/workfiles/files_widget.py index 765d32b3d5..18be746d49 100644 --- a/openpype/tools/workfiles/files_widget.py +++ b/openpype/tools/workfiles/files_widget.py @@ -621,7 +621,7 @@ class FilesWidget(QtWidgets.QWidget): "caption": "Work Files", "filter": ext_filter } - if qtpy.API in ("pyside", "pyside2"): + if qtpy.API in ("pyside", "pyside2", "pyside6"): kwargs["dir"] = self._workfiles_root else: kwargs["directory"] = self._workfiles_root From e6bf6add2f0b25d93dfd0d822b3d1d5a4393f1a2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 17 Feb 2023 10:48:15 +0100 Subject: [PATCH 115/119] modify integrate ftrack instances to be able upload origin filename --- .../ftrack/plugins/publish/integrate_ftrack_instances.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py index d6cb3daf0d..75f43cb22f 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py @@ -56,6 +56,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): "reference": "reference" } keep_first_subset_name_for_review = True + upload_reviewable_with_origin_name = False asset_versions_status_profiles = [] additional_metadata_keys = [] @@ -294,6 +295,13 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): ) # Add item to component list component_list.append(review_item) + if self.upload_reviewable_with_origin_name: + origin_name_component = copy.deepcopy(review_item) + filename = os.path.basename(repre_path) + origin_name_component["component_data"]["name"] = ( + os.path.splitext(filename)[0] + ) + component_list.append(origin_name_component) # Duplicate thumbnail component for all not first reviews if first_thumbnail_component is not None: From f42831ecb04d163e647f68db8b008530926de81e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 17 Feb 2023 10:48:48 +0100 Subject: [PATCH 116/119] added settings for original basename upload --- .../defaults/project_settings/ftrack.json | 3 ++- .../projects_schema/schema_project_ftrack.json | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index cdf861df4a..f3f2345a0f 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -488,7 +488,8 @@ }, "keep_first_subset_name_for_review": true, "asset_versions_status_profiles": [], - "additional_metadata_keys": [] + "additional_metadata_keys": [], + "upload_reviewable_with_origin_name": false }, "IntegrateFtrackFarmStatus": { "farm_status_profiles": [] diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json index da414cc961..7050721742 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -1037,6 +1037,21 @@ {"fps": "FPS"}, {"code": "Codec"} ] + }, + { + "type": "separator" + }, + { + "type": "boolean", + "key": "upload_reviewable_with_origin_name", + "label": "Upload reviewable with origin name" + }, + { + "type": "label", + "label": "Note: Reviewable will be uploaded twice into ftrack when enabled. One with original name and second with required 'ftrackreview-mp4'. That may cause dramatic increase of ftrack storage usage." + }, + { + "type": "separator" } ] }, From e246afe55b2ecee3f726b54c27b85a5a45f3dcfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Fri, 17 Feb 2023 15:07:24 +0100 Subject: [PATCH 117/119] removing typo --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 485ae7f4ee..514ffb62c0 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,6 @@ OpenPype [![documentation](https://github.com/pypeclub/pype/actions/workflows/documentation.yml/badge.svg)](https://github.com/pypeclub/pype/actions/workflows/documentation.yml) ![GitHub VFX Platform](https://img.shields.io/badge/vfx%20platform-2022-lightgrey?labelColor=303846) -this Introduction ------------ From 8afd618a0d2b2abbe6441d6d9c3d5c57170424ac Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 17 Feb 2023 15:38:43 +0100 Subject: [PATCH 118/119] updating workflows --- .../workflows/miletone_release_trigger.yml | 47 ++++++++++++ .github/workflows/nightly_merge.yml | 29 ------- .github/workflows/prerelease.yml | 67 ---------------- .github/workflows/release.yml | 76 ------------------- 4 files changed, 47 insertions(+), 172 deletions(-) create mode 100644 .github/workflows/miletone_release_trigger.yml delete mode 100644 .github/workflows/nightly_merge.yml delete mode 100644 .github/workflows/prerelease.yml delete mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/miletone_release_trigger.yml b/.github/workflows/miletone_release_trigger.yml new file mode 100644 index 0000000000..b5b8aab1dc --- /dev/null +++ b/.github/workflows/miletone_release_trigger.yml @@ -0,0 +1,47 @@ +name: Milestone Release [trigger] + +on: + workflow_dispatch: + inputs: + milestone: + required: true + release-type: + type: choice + description: What release should be created + options: + - release + - pre-release + milestone: + types: closed + + +jobs: + milestone-title: + runs-on: ubuntu-latest + outputs: + milestone: ${{ steps.milestoneTitle.outputs.value }} + steps: + - name: Switch input milestone + uses: haya14busa/action-cond@v1 + id: milestoneTitle + with: + cond: ${{ inputs.milestone == '' }} + if_true: ${{ github.event.milestone.title }} + if_false: ${{ inputs.milestone }} + - name: Print resulted milestone + run: | + echo "${{ steps.milestoneTitle.outputs.value }}" + + call-ci-tools-milestone-release: + needs: milestone-title + uses: ynput/ci-tools/.github/workflows/milestone_release_ref.yml@main + with: + milestone: ${{ needs.milestone-title.outputs.milestone }} + repo-owner: ${{ github.event.repository.owner.login }} + repo-name: ${{ github.event.repository.name }} + version-py-path: "./openpype/version.py" + pyproject-path: "./pyproject.toml" + secrets: + token: ${{ secrets.YNPUT_BOT_TOKEN }} + user_email: ${{ secrets.CI_EMAIL }} + user_name: ${{ secrets.CI_USER }} diff --git a/.github/workflows/nightly_merge.yml b/.github/workflows/nightly_merge.yml deleted file mode 100644 index 1776d7a464..0000000000 --- a/.github/workflows/nightly_merge.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Dev -> Main - -on: - schedule: - - cron: '21 3 * * 3,6' - workflow_dispatch: - -jobs: - develop-to-main: - - runs-on: ubuntu-latest - - steps: - - name: πŸš› Checkout Code - uses: actions/checkout@v2 - - - name: πŸ”¨ Merge develop to main - uses: everlytic/branch-merge@1.1.0 - with: - github_token: ${{ secrets.YNPUT_BOT_TOKEN }} - source_ref: 'develop' - target_branch: 'main' - commit_message_template: '[Automated] Merged {source_ref} into {target_branch}' - - - name: Invoke pre-release workflow - uses: benc-uk/workflow-dispatch@v1 - with: - workflow: Nightly Prerelease - token: ${{ secrets.YNPUT_BOT_TOKEN }} diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml deleted file mode 100644 index 571b0339e1..0000000000 --- a/.github/workflows/prerelease.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: Nightly Prerelease - -on: - workflow_dispatch: - - -jobs: - create_nightly: - runs-on: ubuntu-latest - - steps: - - name: πŸš› Checkout Code - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.9 - - - name: Install Python requirements - run: pip install gitpython semver PyGithub - - - name: πŸ”Ž Determine next version type - id: version_type - run: | - TYPE=$(python ./tools/ci_tools.py --bump --github_token ${{ secrets.YNPUT_BOT_TOKEN }}) - echo "type=${TYPE}" >> $GITHUB_OUTPUT - - - name: πŸ’‰ Inject new version into files - id: version - if: steps.version_type.outputs.type != 'skip' - run: | - NEW_VERSION_TAG=$(python ./tools/ci_tools.py --nightly --github_token ${{ secrets.YNPUT_BOT_TOKEN }}) - echo "next_tag=${NEW_VERSION_TAG}" >> $GITHUB_OUTPUT - - - name: πŸ’Ύ Commit and Tag - id: git_commit - if: steps.version_type.outputs.type != 'skip' - run: | - git config user.email ${{ secrets.CI_EMAIL }} - git config user.name ${{ secrets.CI_USER }} - git checkout main - git pull - git add . - git commit -m "[Automated] Bump version" - tag_name="CI/${{ steps.version.outputs.next_tag }}" - echo $tag_name - git tag -a $tag_name -m "nightly build" - - - name: Push to protected main branch - uses: CasperWA/push-protected@v2.10.0 - with: - token: ${{ secrets.YNPUT_BOT_TOKEN }} - branch: main - tags: true - unprotect_reviews: true - - - name: πŸ”¨ Merge main back to develop - uses: everlytic/branch-merge@1.1.0 - if: steps.version_type.outputs.type != 'skip' - with: - github_token: ${{ secrets.YNPUT_BOT_TOKEN }} - source_ref: 'main' - target_branch: 'develop' - commit_message_template: '[Automated] Merged {source_ref} into {target_branch}' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 0b4c8af2c7..0000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,76 +0,0 @@ -name: Stable Release - -on: - release: - types: - - prereleased - -jobs: - create_release: - runs-on: ubuntu-latest - if: github.actor != 'pypebot' - - steps: - - name: πŸš› Checkout Code - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.9 - - name: Install Python requirements - run: pip install gitpython semver PyGithub - - - name: πŸ’‰ Inject new version into files - id: version - run: | - NEW_VERSION=$(python ./tools/ci_tools.py --finalize ${GITHUB_REF#refs/*/}) - LAST_VERSION=$(python ./tools/ci_tools.py --lastversion release) - - echo "current_version=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT - echo "last_release=${LAST_VERSION}" >> $GITHUB_OUTPUT - echo "release_tag=${NEW_VERSION}" >> $GITHUB_OUTPUT - - - name: πŸ’Ύ Commit and Tag - id: git_commit - if: steps.version.outputs.release_tag != 'skip' - run: | - git config user.email ${{ secrets.CI_EMAIL }} - git config user.name ${{ secrets.CI_USER }} - git add . - git commit -m "[Automated] Release" - tag_name="${{ steps.version.outputs.release_tag }}" - git tag -a $tag_name -m "stable release" - - - name: πŸ” Push to protected main branch - if: steps.version.outputs.release_tag != 'skip' - uses: CasperWA/push-protected@v2.10.0 - with: - token: ${{ secrets.YNPUT_BOT_TOKEN }} - branch: main - tags: true - unprotect_reviews: true - - - name: πŸš€ Github Release - if: steps.version.outputs.release_tag != 'skip' - uses: ncipollo/release-action@v1 - with: - tag: ${{ steps.version.outputs.release_tag }} - token: ${{ secrets.YNPUT_BOT_TOKEN }} - - - name: ☠ Delete Pre-release - if: steps.version.outputs.release_tag != 'skip' - uses: cb80/delrel@latest - with: - tag: "${{ steps.version.outputs.current_version }}" - - - name: πŸ” Merge main back to develop - if: steps.version.outputs.release_tag != 'skip' - uses: everlytic/branch-merge@1.1.0 - with: - github_token: ${{ secrets.YNPUT_BOT_TOKEN }} - source_ref: 'main' - target_branch: 'develop' - commit_message_template: '[Automated] Merged release {source_ref} into {target_branch}' From 3af80e97e81204ee461acc58ac73cb402f505a6f Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 17 Feb 2023 14:54:45 +0000 Subject: [PATCH 119/119] [Automated] Release --- CHANGELOG.md | 76 +++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 8 ++--- 3 files changed, 81 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0da167763b..8a37886deb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,81 @@ # Changelog + +## [3.15.1](https://github.com/ynput/OpenPype/tree/3.15.1) + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.1...3.15.0) + +### **πŸ†• New features** + + +

+Maya: Xgen (3d / maya ) - #4256 + + +___ + + +## Brief description +Initial Xgen implementation. + +## Description +Client request of Xgen pipeline. + + + + +___ + + +
+ +### **πŸš€ Enhancements** + + +
+Adding path validator for non-maya nodes (3d / maya ) - #4271 + + +___ + + +## Brief description +Adding a path validator for filepaths from non-maya nodes, which are created by plugins such as Renderman, Yeti and abcImport. + +## Description +As File Path Editor cannot catch the wrong filenpaths from non-maya nodes such as AlembicNodes, It is neccessary to have a new validator to ensure the existence of the filepaths from the nodes. + + + + +___ + + +
+ +### **πŸ› Bug fixes** + + +
+Fix features for gizmo menu (2d / nuke ) - #4280 + + +___ + + +## Brief description +Fix features for the Gizmo Menu project settings (shortcut for python type of usage and file type of usage functionality) + + + + +___ + + +
+ + + ## [3.15.0](https://github.com/ynput/OpenPype/tree/HEAD) [Full Changelog](https://github.com/ynput/OpenPype/compare/3.14.10...HEAD) diff --git a/openpype/version.py b/openpype/version.py index 6d060656cb..72d6b64c60 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.1-nightly.6" +__version__ = "3.15.1" diff --git a/pyproject.toml b/pyproject.toml index a872ed3609..d1d5c8e2d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.15.0" # OpenPype +version = "3.15.1" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" @@ -114,15 +114,15 @@ build-backend = "poetry.core.masonry.api" # https://pip.pypa.io/en/stable/cli/pip_install/#requirement-specifiers [openpype.qtbinding.windows] package = "PySide2" -version = "5.15.2" +version = "3.15.1" [openpype.qtbinding.darwin] package = "PySide6" -version = "6.4.1" +version = "3.15.1" [openpype.qtbinding.linux] package = "PySide2" -version = "5.15.2" +version = "3.15.1" # TODO: we will need to handle different linux flavours here and # also different macos versions too.