From 14b5ac4c251f5cb3b9a263f7f0b03bc0567ca45f Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 18 Aug 2022 14:01:54 +0300 Subject: [PATCH 01/55] Add `extract_obj.py` and `obj.py` --- openpype/hosts/maya/api/obj.py | 0 .../hosts/maya/plugins/publish/extract_obj.py | 62 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 openpype/hosts/maya/api/obj.py create mode 100644 openpype/hosts/maya/plugins/publish/extract_obj.py diff --git a/openpype/hosts/maya/api/obj.py b/openpype/hosts/maya/api/obj.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/hosts/maya/plugins/publish/extract_obj.py b/openpype/hosts/maya/plugins/publish/extract_obj.py new file mode 100644 index 0000000000..7c915a80d8 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/extract_obj.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +import os + +from maya import cmds +import maya.mel as mel +import pyblish.api +import openpype.api +from openpype.hosts.maya.api.lib import maintained_selection + +from openpype.hosts.maya.api import obj + + +class ExtractObj(openpype.api.Extractor): + """Extract OBJ from Maya. + + This extracts reproducible OBJ exports ignoring any of the settings + set on the local machine in the OBJ export options window. + + """ + order = pyblish.api.ExtractorOrder + label = "Extract OBJ" + families = ["obj"] + + def process(self, instance): + obj_exporter = obj.OBJExtractor(log=self.log) + + # Define output path + + staging_dir = self.staging_dir(instance) + filename = "{0}.fbx".format(instance.name) + path = os.path.join(staging_dir, filename) + + # The export requires forward slashes because we need to + # format it into a string in a mel expression + path = path.replace('\\', '/') + + self.log.info("Extracting OBJ to: {0}".format(path)) + + members = instance.data["setMembners"] + self.log.info("Members: {0}".format(members)) + self.log.info("Instance: {0}".format(instance[:])) + + obj_exporter.set_options_from_instance(instance) + + # Export + with maintained_selection(): + obj_exporter.export(members, path) + cmds.select(members, r=1, noExpand=True) + mel.eval('file -force -options "{0};{1};{2};{3};{4}" -typ "OBJexport" -pr -es "{5}";'.format(grp_flag, ptgrp_flag, mats_flag, smooth_flag, normals_flag, path)) # noqa + + if "representation" not in instance.data: + instance.data["representation"] = [] + + representation = { + 'name':'obj', + 'ext':'obx', + 'files': filename, + "stagingDir": staging_dir, + } + instance.data["representations"].append(representation) + + self.log.info("Extract OBJ successful to: {0}".format(path)) From bacbff0262e8306ef16ccf3d1e826e3e1cb2728f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 17 Oct 2022 15:19:48 +0200 Subject: [PATCH 02/55] create context has callbacks for reset preparation and finalization --- openpype/pipeline/create/context.py | 15 +++++++++++++++ openpype/tools/publisher/control.py | 4 ++++ 2 files changed, 19 insertions(+) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index c1cf4dab44..1f3c32f0a7 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -860,6 +860,9 @@ class CreateContext: All changes will be lost if were not saved explicitely. """ + + self.reset_preparation() + self.reset_avalon_context() self.reset_plugins(discover_publish_plugins) self.reset_context_data() @@ -868,6 +871,18 @@ class CreateContext: self.reset_instances() self.execute_autocreators() + self.reset_finalization() + + def reset_preparation(self): + """Prepare attributes that must be prepared/cleaned before reset.""" + + pass + + def reset_finalization(self): + """Cleanup of attributes after reset.""" + + pass + def reset_avalon_context(self): """Give ability to reset avalon context. diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index b4c89f221f..19e28cca4b 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -552,6 +552,8 @@ class PublisherController: self.save_changes() + self.create_context.reset_preparation() + # Reset avalon context self.create_context.reset_avalon_context() @@ -560,6 +562,8 @@ class PublisherController: self._reset_publish() self._reset_instances() + self.create_context.reset_finalization() + self.emit_card_message("Refreshed..") def _reset_plugins(self): From e0bb8c0469d50273ad041280b786380d98de080c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 17 Oct 2022 15:20:06 +0200 Subject: [PATCH 03/55] context can handle shared data for collection phase --- openpype/pipeline/create/context.py | 68 ++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 1f3c32f0a7..02398818d9 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -27,6 +27,11 @@ from .creator_plugins import ( UpdateData = collections.namedtuple("UpdateData", ["instance", "changes"]) +class UnavailableSharedData(Exception): + """Shared data are not available at the moment when are accessed.""" + pass + + class ImmutableKeyError(TypeError): """Accessed key is immutable so does not allow changes or removements.""" @@ -809,6 +814,9 @@ class CreateContext: self._bulk_counter = 0 self._bulk_instances_to_process = [] + # Shared data across creators during collection phase + self._collection_shared_data = None + # Trigger reset if was enabled if reset: self.reset(discover_publish_plugins) @@ -877,11 +885,15 @@ class CreateContext: """Prepare attributes that must be prepared/cleaned before reset.""" pass + # Give ability to store shared data for collection phase + self._collection_shared_data = {} def reset_finalization(self): """Cleanup of attributes after reset.""" pass + # Stop access to collection shared data + self._collection_shared_data = None def reset_avalon_context(self): """Give ability to reset avalon context. @@ -991,7 +1003,8 @@ class CreateContext: and creator_class.host_name != self.host_name ): self.log.info(( - "Creator's host name is not supported for current host {}" + "Creator's host name \"{}\"" + " is not supported for current host \"{}\"" ).format(creator_class.host_name, self.host_name)) continue @@ -1266,3 +1279,56 @@ class CreateContext: if not plugin.__instanceEnabled__: plugins.append(plugin) return plugins + + def _validate_collection_shared_data(self): + if self._collection_shared_data is None: + raise UnavailableSharedData( + "Accessed Collection shared data out of collection phase" + ) + + def has_collection_shared_data(self, key): + """Check if collection shared data are set. + + Args: + key (str): Key under which are shared data stored. + + Retruns: + bool: Key is already set. + + Raises: + UnavailableSharedData: When called out of collection phase. + """ + + self._validate_collection_shared_data() + return key in self._collection_shared_data + + def get_collection_shared_data(self, key, default=None): + """Receive shared data during collection phase. + + Args: + key (str): Key under which are shared data stored. + default (Any): Default value if key is not set. + + Returns: + Any: Value stored under the key. + + Raises: + UnavailableSharedData: When called out of collection phase. + """ + + self._validate_collection_shared_data() + return self._collection_shared_data.get(key, default) + + def set_collection_shared_data(self, key, value): + """Store a value under collection shared data. + + Args: + key (str): Key under which will shared data be stored. + value (Any): Value to store. + + Raises: + UnavailableSharedData: When called out of collection phase. + """ + + self._validate_collection_shared_data() + self._collection_shared_data[key] = value From 2ed383c4768571436df3f2b3b2245d55ccdfdc6b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 17 Oct 2022 15:20:24 +0200 Subject: [PATCH 04/55] added wrappers for access to shared data in create plugins --- openpype/pipeline/create/creator_plugins.py | 36 +++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 05ba8902aa..761054fbd5 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -6,6 +6,7 @@ from abc import ( abstractmethod, abstractproperty ) + import six from openpype.settings import get_system_settings, get_project_settings @@ -323,6 +324,41 @@ class BaseCreator: return self.instance_attr_defs + def has_collection_shared_data(self, key): + """Check if collection shared data are set. + + Args: + key (str): Key under which are shared data stored. + + Retruns: + bool: Key is already set. + """ + + return self.create_context.has_collection_shared_data(key) + + def get_collection_shared_data(self, key, default=None): + """Receive shared data during collection phase. + + Args: + key (str): Key under which are shared data stored. + default (Any): Default value if key is not set. + + Returns: + Any: Value stored under the key. + """ + + return self.create_context.get_collection_shared_data(key, default) + + def set_collection_shared_data(self, key, value): + """Store a value under collection shared data. + + Args: + key (str): Key under which will shared data be stored. + value (Any): Value to store. + """ + + return self.create_context.set_collection_shared_data(key, value) + class Creator(BaseCreator): """Creator that has more information for artist to show in UI. From 3f01d008c59205136d18675068f4242def604b2f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 17 Oct 2022 15:26:38 +0200 Subject: [PATCH 05/55] added recommendation --- openpype/pipeline/create/context.py | 3 +++ openpype/pipeline/create/creator_plugins.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 02398818d9..613eaa2865 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1322,6 +1322,9 @@ class CreateContext: def set_collection_shared_data(self, key, value): """Store a value under collection shared data. + It is highly recommended to use very specific keys as creators may + clash each other if simple keys are used. + Args: key (str): Key under which will shared data be stored. value (Any): Value to store. diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 761054fbd5..343a416872 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -352,6 +352,9 @@ class BaseCreator: def set_collection_shared_data(self, key, value): """Store a value under collection shared data. + It is highly recommended to use very specific keys as creators may + clash each other if simple keys are used. + Args: key (str): Key under which will shared data be stored. value (Any): Value to store. From 0aefb39acb85b4f8dc0de3904b3613c3762d8c2c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 17 Oct 2022 15:31:07 +0200 Subject: [PATCH 06/55] cache instances in shared data in tray publisher --- openpype/hosts/traypublisher/api/plugin.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py index 89c25389cb..1e592e786d 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -17,11 +17,27 @@ from openpype.lib.transcoding import IMAGE_EXTENSIONS, VIDEO_EXTENSIONS REVIEW_EXTENSIONS = IMAGE_EXTENSIONS + VIDEO_EXTENSIONS +def _cache_and_get_instances(creator): + """Cache instances in shared data. + + Args: + creator (Creator): Plugin which would like to get instances from host. + + Returns: + List[Dict[str, Any]]: Cached instances list from host implementation. + """ + + shared_key = "openpype.traypublisher.instances" + if not creator.has_collection_shared_data(shared_key): + creator.set_collection_shared_data(shared_key, list_instances()) + return creator.get_collection_shared_data(shared_key) + + class HiddenTrayPublishCreator(HiddenCreator): host_name = "traypublisher" def collect_instances(self): - for instance_data in list_instances(): + for instance_data in _cache_and_get_instances(): creator_id = instance_data.get("creator_identifier") if creator_id == self.identifier: instance = CreatedInstance.from_existing( @@ -58,7 +74,7 @@ class TrayPublishCreator(Creator): host_name = "traypublisher" def collect_instances(self): - for instance_data in list_instances(): + for instance_data in _cache_and_get_instances(): creator_id = instance_data.get("creator_identifier") if creator_id == self.identifier: instance = CreatedInstance.from_existing( From aa6de1cfeba7338212c4043a3674fe50f6ecab90 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 17 Oct 2022 16:51:15 +0200 Subject: [PATCH 07/55] renamed 'has_collection_shared_data' to 'collection_shared_data_contains' --- openpype/hosts/traypublisher/api/plugin.py | 2 +- openpype/pipeline/create/context.py | 2 +- openpype/pipeline/create/creator_plugins.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py index 1e592e786d..0f519e3c32 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -28,7 +28,7 @@ def _cache_and_get_instances(creator): """ shared_key = "openpype.traypublisher.instances" - if not creator.has_collection_shared_data(shared_key): + if not creator.collection_shared_data_contains(shared_key): creator.set_collection_shared_data(shared_key, list_instances()) return creator.get_collection_shared_data(shared_key) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 613eaa2865..298eacecb5 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1286,7 +1286,7 @@ class CreateContext: "Accessed Collection shared data out of collection phase" ) - def has_collection_shared_data(self, key): + def collection_shared_data_contains(self, key): """Check if collection shared data are set. Args: diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 343a416872..e5018c395e 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -324,7 +324,7 @@ class BaseCreator: return self.instance_attr_defs - def has_collection_shared_data(self, key): + def collection_shared_data_contains(self, key): """Check if collection shared data are set. Args: @@ -334,7 +334,7 @@ class BaseCreator: bool: Key is already set. """ - return self.create_context.has_collection_shared_data(key) + return self.create_context.collection_shared_data_contains(key) def get_collection_shared_data(self, key, default=None): """Receive shared data during collection phase. From ba2cb2d11d7dbc0384265071a9d464b68a8813f8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 17 Oct 2022 17:00:44 +0200 Subject: [PATCH 08/55] add information about shared data to documentation --- website/docs/dev_publishing.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/website/docs/dev_publishing.md b/website/docs/dev_publishing.md index 7a6082a517..5f30f7f9c8 100644 --- a/website/docs/dev_publishing.md +++ b/website/docs/dev_publishing.md @@ -47,10 +47,14 @@ Context discovers creator and publish plugins. Trigger collections of existing i Creator plugins can call **creator_adds_instance** or **creator_removed_instance** to add/remove instances but these methods are not meant to be called directly out of the creator. The reason is that it is the creator's responsibility to remove metadata or decide if it should remove the instance. -#### Required functions in host implementation -Host implementation **must** implement **get_context_data** and **update_context_data**. These two functions are needed to store metadata that are not related to any instance but are needed for Creating and publishing process. Right now only data about enabled/disabled optional publish plugins is stored there. When data is not stored and loaded properly, reset of publishing will cause that they will be set to default value. Context data also parsed to json string similarly as instance data. +During reset are re-cached Creator plugins, re-collected instances, refreshed host context and more. Object of `CreateContext` supply shared data during the reset. They can be used by creators to share same data needed during collection phase or during creation for autocreators. -There are also few optional functions. For UI purposes it is possible to implement **get_context_title** which can return a string shown in UI as a title. Output string may contain html tags. It is recommended to return context path (it will be created function this purposes) in this order `"{project name}/{asset hierarchy}/{asset name}/{task name}"`. +#### Required functions in host implementation +It is recommended to use `HostBase` class (`from openpype.host import HostBase`) as base for host implementation with combination of `IPublishHost` interface (`from openpype.host import IPublishHost`). These abstract classes should guide you to fill missing attributes and methods. + +To sum them and in case host implementation is inheriting `HostBase` the implementation **must** implement **get_context_data** and **update_context_data**. These two functions are needed to store metadata that are not related to any instance but are needed for Creating and publishing process. Right now only data about enabled/disabled optional publish plugins is stored there. When data is not stored and loaded properly, reset of publishing will cause that they will be set to default value. Context data also parsed to json string similarly as instance data. + +There are also few optional functions. For UI purposes it is possible to implement **get_context_title** which can return a string shown in UI as a title. Output string may contain html tags. It is recommended to return context path (it will be created function this purposes) in this order `"{project name}/{asset hierarchy}/{asset name}/{task name}"` (this is default implementation in `HostBase`). Another optional function is **get_current_context**. This function is handy in hosts where it is possible to open multiple workfiles in one process so using global context variables is not relevant because artists can switch between opened workfiles without being acknowledged. When a function is not implemented or won't return the right keys the global context is used. ```json @@ -68,6 +72,12 @@ Main responsibility of create plugin is to create, update, collect and remove in #### *BaseCreator* Base implementation of creator plugin. It is not recommended to use this class as base for production plugins but rather use one of **HiddenCreator**, **AutoCreator** and **Creator** variants. +**Access to shared data** +Functions to work with "Collection shared data" can be used during reset phase of `CreateContext`. Creators can cache there data that are common for them. For example list of nodes in scene. Methods are implemented on `CreateContext` but their usage is primarily for Create plugins as nothing else should use it. +- **`collection_shared_data_contains`** - Check if shared data already has set a key. +- **`get_collection_shared_data`** - Receive value of shared data by a key. +- **`set_collection_shared_data`** - Set or update value of shared data key. + **Abstractions** - **`family`** (class attr) - Tells what kind of instance will be created. ```python From cad97d6d1dc98d9d2d46dec060801fa4645e6053 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 18 Oct 2022 15:18:24 +0200 Subject: [PATCH 09/55] simplify api by giving access to 'collection_shared_data' property --- openpype/hosts/traypublisher/api/plugin.py | 6 +- openpype/pipeline/create/context.py | 65 ++++----------------- openpype/pipeline/create/creator_plugins.py | 41 +++---------- 3 files changed, 23 insertions(+), 89 deletions(-) diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py index 0f519e3c32..2cb5a8729f 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -28,9 +28,9 @@ def _cache_and_get_instances(creator): """ shared_key = "openpype.traypublisher.instances" - if not creator.collection_shared_data_contains(shared_key): - creator.set_collection_shared_data(shared_key, list_instances()) - return creator.get_collection_shared_data(shared_key) + if shared_key not in creator.collection_shared_data: + creator.collection_shared_data[shared_key] = list_instances() + return creator.collection_shared_data[shared_key] class HiddenTrayPublishCreator(HiddenCreator): diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 298eacecb5..c5c9a14f33 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -884,14 +884,12 @@ class CreateContext: def reset_preparation(self): """Prepare attributes that must be prepared/cleaned before reset.""" - pass # Give ability to store shared data for collection phase self._collection_shared_data = {} def reset_finalization(self): """Cleanup of attributes after reset.""" - pass # Stop access to collection shared data self._collection_shared_data = None @@ -1280,58 +1278,19 @@ class CreateContext: plugins.append(plugin) return plugins - def _validate_collection_shared_data(self): + @property + def collection_shared_data(self): + """Access to shared data that can be used during creator's collection. + + Retruns: + Dict[str, Any]: Shared data. + + Raises: + UnavailableSharedData: When called out of collection phase. + """ + if self._collection_shared_data is None: raise UnavailableSharedData( "Accessed Collection shared data out of collection phase" ) - - def collection_shared_data_contains(self, key): - """Check if collection shared data are set. - - Args: - key (str): Key under which are shared data stored. - - Retruns: - bool: Key is already set. - - Raises: - UnavailableSharedData: When called out of collection phase. - """ - - self._validate_collection_shared_data() - return key in self._collection_shared_data - - def get_collection_shared_data(self, key, default=None): - """Receive shared data during collection phase. - - Args: - key (str): Key under which are shared data stored. - default (Any): Default value if key is not set. - - Returns: - Any: Value stored under the key. - - Raises: - UnavailableSharedData: When called out of collection phase. - """ - - self._validate_collection_shared_data() - return self._collection_shared_data.get(key, default) - - def set_collection_shared_data(self, key, value): - """Store a value under collection shared data. - - It is highly recommended to use very specific keys as creators may - clash each other if simple keys are used. - - Args: - key (str): Key under which will shared data be stored. - value (Any): Value to store. - - Raises: - UnavailableSharedData: When called out of collection phase. - """ - - self._validate_collection_shared_data() - self._collection_shared_data[key] = value + return self._collection_shared_data diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index e5018c395e..97ee94c449 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -324,43 +324,18 @@ class BaseCreator: return self.instance_attr_defs - def collection_shared_data_contains(self, key): - """Check if collection shared data are set. - - Args: - key (str): Key under which are shared data stored. + @property + def collection_shared_data(self): + """Access to shared data that can be used during creator's collection. Retruns: - bool: Key is already set. + Dict[str, Any]: Shared data. + + Raises: + UnavailableSharedData: When called out of collection phase. """ - return self.create_context.collection_shared_data_contains(key) - - def get_collection_shared_data(self, key, default=None): - """Receive shared data during collection phase. - - Args: - key (str): Key under which are shared data stored. - default (Any): Default value if key is not set. - - Returns: - Any: Value stored under the key. - """ - - return self.create_context.get_collection_shared_data(key, default) - - def set_collection_shared_data(self, key, value): - """Store a value under collection shared data. - - It is highly recommended to use very specific keys as creators may - clash each other if simple keys are used. - - Args: - key (str): Key under which will shared data be stored. - value (Any): Value to store. - """ - - return self.create_context.set_collection_shared_data(key, value) + return self.create_context.collection_shared_data class Creator(BaseCreator): From 7571d62709eb5c9ecd08a07062e818f32eb4ab0c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 18 Oct 2022 15:22:50 +0200 Subject: [PATCH 10/55] Updated documentation --- website/docs/dev_publishing.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/website/docs/dev_publishing.md b/website/docs/dev_publishing.md index 5f30f7f9c8..135f6cd985 100644 --- a/website/docs/dev_publishing.md +++ b/website/docs/dev_publishing.md @@ -73,10 +73,7 @@ Main responsibility of create plugin is to create, update, collect and remove in Base implementation of creator plugin. It is not recommended to use this class as base for production plugins but rather use one of **HiddenCreator**, **AutoCreator** and **Creator** variants. **Access to shared data** -Functions to work with "Collection shared data" can be used during reset phase of `CreateContext`. Creators can cache there data that are common for them. For example list of nodes in scene. Methods are implemented on `CreateContext` but their usage is primarily for Create plugins as nothing else should use it. -- **`collection_shared_data_contains`** - Check if shared data already has set a key. -- **`get_collection_shared_data`** - Receive value of shared data by a key. -- **`set_collection_shared_data`** - Set or update value of shared data key. +Functions to work with "Collection shared data" can be used during reset phase of `CreateContext`. Creators can cache there data that are common for them. For example list of nodes in scene. Methods are implemented on `CreateContext` but their usage is primarily for Create plugins as nothing else should use it. Each creator can access `collection_shared_data` attribute which is a dictionary where shared data can be stored. **Abstractions** - **`family`** (class attr) - Tells what kind of instance will be created. From c9fc2547a9b44c6b108633504e64b6dfc5b9d603 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 19 Oct 2022 11:56:55 +0200 Subject: [PATCH 11/55] replace imports from openpype.api --- openpype/hosts/hiero/api/pipeline.py | 1 - .../hiero/plugins/publish/integrate_version_up_workfile.py | 5 +++-- openpype/hosts/nuke/api/pipeline.py | 1 - openpype/hosts/nuke/plugins/publish/precollect_workfile.py | 5 +++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/hiero/api/pipeline.py b/openpype/hosts/hiero/api/pipeline.py index dacfd338bb..ea61dc4785 100644 --- a/openpype/hosts/hiero/api/pipeline.py +++ b/openpype/hosts/hiero/api/pipeline.py @@ -251,7 +251,6 @@ def reload_config(): import importlib for module in ( - "openpype.api", "openpype.hosts.hiero.lib", "openpype.hosts.hiero.menu", "openpype.hosts.hiero.tags" diff --git a/openpype/hosts/hiero/plugins/publish/integrate_version_up_workfile.py b/openpype/hosts/hiero/plugins/publish/integrate_version_up_workfile.py index 934e7112fa..6ccbe955f2 100644 --- a/openpype/hosts/hiero/plugins/publish/integrate_version_up_workfile.py +++ b/openpype/hosts/hiero/plugins/publish/integrate_version_up_workfile.py @@ -1,5 +1,6 @@ from pyblish import api -import openpype.api as pype + +from openpype.lib import version_up class IntegrateVersionUpWorkfile(api.ContextPlugin): @@ -15,7 +16,7 @@ class IntegrateVersionUpWorkfile(api.ContextPlugin): def process(self, context): project = context.data["activeProject"] path = context.data.get("currentFile") - new_path = pype.version_up(path) + new_path = version_up(path) if project: project.saveAs(new_path) diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index 7db420f6af..c343c635fa 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -66,7 +66,6 @@ def reload_config(): """ for module in ( - "openpype.api", "openpype.hosts.nuke.api.actions", "openpype.hosts.nuke.api.menu", "openpype.hosts.nuke.api.plugin", diff --git a/openpype/hosts/nuke/plugins/publish/precollect_workfile.py b/openpype/hosts/nuke/plugins/publish/precollect_workfile.py index 822f405a6f..316c651b66 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_workfile.py @@ -3,7 +3,8 @@ import os import nuke import pyblish.api -import openpype.api as pype + +from openpype.lib import get_version_from_path from openpype.hosts.nuke.api.lib import ( add_publish_knob, get_avalon_knob_data @@ -74,7 +75,7 @@ class CollectWorkfile(pyblish.api.ContextPlugin): "fps": root['fps'].value(), "currentFile": current_file, - "version": int(pype.get_version_from_path(current_file)), + "version": int(get_version_from_path(current_file)), "host": pyblish.api.current_host(), "hostVersion": nuke.NUKE_VERSION_STRING From 321512bb0115ee38d085da753a2bb6b7e4e2a2ce Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 19 Oct 2022 21:19:31 +0200 Subject: [PATCH 12/55] nuke: adding viewer and display exctractor --- openpype/hosts/nuke/api/lib.py | 44 ++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 1aea04d889..2691b7447a 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2930,3 +2930,47 @@ def get_nodes_by_names(names): nuke.toNode(name) for name in names ] + + +def get_viewer_config_from_string(input_string): + """Convert string to display and viewer string + + Args: + input_string (str): string with viewer + + Raises: + IndexError: if more then one slash in input string + IndexError: if missing closing bracket + + Returns: + tuple[str]: display, viewer + """ + display = None + viewer = input_string + # check if () or / or \ in name + if "/" in viewer: + split = viewer.split("/") + + # rise if more then one column + if len(split) > 2: + raise IndexError(( + "Viewer Input string is not correct. " + "more then two `/` slashes! {}" + ).format(input_string)) + + viewer = split[1] + display = split[0] + elif "(" in viewer: + pattern = r"([\w\d\s]+).*[(](.*)[)]" + result = re.findall(pattern, viewer) + try: + result = result.pop() + display = str(result[1]).rstrip() + viewer = str(result[0]).rstrip() + except IndexError: + raise IndexError(( + "Viewer Input string is not correct. " + "Missing bracket! {}" + ).format(input_string)) + + return (display, viewer) From babd9898d2ac5da414d5f758533e4fcd3096024c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 19 Oct 2022 21:21:05 +0200 Subject: [PATCH 13/55] nuke: implementing display and viewer assignment --- openpype/hosts/nuke/api/plugin.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 91bb90ff99..9330309f64 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -19,7 +19,8 @@ from .lib import ( add_publish_knob, get_nuke_imageio_settings, set_node_knobs_from_settings, - get_view_process_node + get_view_process_node, + get_viewer_config_from_string ) @@ -312,7 +313,8 @@ class ExporterReviewLut(ExporterReview): dag_node.setInput(0, self.previous_node) self._temp_nodes.append(dag_node) self.previous_node = dag_node - self.log.debug("OCIODisplay... `{}`".format(self._temp_nodes)) + self.log.debug( + "OCIODisplay... `{}`".format(self._temp_nodes)) # GenerateLUT gen_lut_node = nuke.createNode("GenerateLUT") @@ -491,7 +493,15 @@ class ExporterReviewMov(ExporterReview): if not self.viewer_lut_raw: # OCIODisplay dag_node = nuke.createNode("OCIODisplay") - dag_node["view"].setValue(str(baking_view_profile)) + + display, viewer = get_viewer_config_from_string( + str(baking_view_profile) + ) + if display: + dag_node["display"].setValue(display) + + # assign viewer + dag_node["view"].setValue(viewer) # connect dag_node.setInput(0, self.previous_node) From 3cdad1e9677c5320951f090c9b7674863c11ee4c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 19 Oct 2022 21:37:13 +0200 Subject: [PATCH 14/55] Nuke: add custom tags inputs to settings also implement custom tags to exctractor --- openpype/hosts/nuke/api/plugin.py | 24 ++++++++++++++++--- .../defaults/project_settings/nuke.json | 2 +- .../schemas/schema_nuke_publish.json | 4 ++-- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 9330309f64..5981a8b386 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -191,7 +191,20 @@ class ExporterReview(object): if "#" in self.fhead: self.fhead = self.fhead.replace("#", "")[:-1] - def get_representation_data(self, tags=None, range=False): + def get_representation_data( + self, tags=None, range=False, + custom_tags=None + ): + """ Add representation data to self.data + + Args: + tags (list[str], optional): list of defined tags. + Defaults to None. + range (bool, optional): flag for adding ranges. + Defaults to False. + custom_tags (list[str], optional): user inputed custom tags. + Defaults to None. + """ add_tags = tags or [] repre = { "name": self.name, @@ -201,6 +214,9 @@ class ExporterReview(object): "tags": [self.name.replace("_", "-")] + add_tags } + if custom_tags: + repre["custom_tags"] = custom_tags + if range: repre.update({ "frameStart": self.first_frame, @@ -417,6 +433,7 @@ class ExporterReviewMov(ExporterReview): return path def generate_mov(self, farm=False, **kwargs): + add_tags = [] self.publish_on_farm = farm read_raw = kwargs["read_raw"] reformat_node_add = kwargs["reformat_node_add"] @@ -435,10 +452,10 @@ class ExporterReviewMov(ExporterReview): self.log.debug(">> baking_view_profile `{}`".format( baking_view_profile)) - add_tags = kwargs.get("add_tags", []) + add_custom_tags = kwargs.get("add_custom_tags", []) self.log.info( - "__ add_tags: `{0}`".format(add_tags)) + "__ add_custom_tags: `{0}`".format(add_custom_tags)) subset = self.instance.data["subset"] self._temp_nodes[subset] = [] @@ -552,6 +569,7 @@ class ExporterReviewMov(ExporterReview): # ---------- generate representation data self.get_representation_data( tags=["review", "delete"] + add_tags, + custom_tags=add_custom_tags, range=True ) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index e5cbacbda7..57a09086ca 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -434,7 +434,7 @@ } ], "extension": "mov", - "add_tags": [] + "add_custom_tags": [] } } }, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json index e5827a92c4..c91d3c0e3d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json @@ -296,8 +296,8 @@ "label": "Write node file type" }, { - "key": "add_tags", - "label": "Add additional tags to representations", + "key": "add_custom_tags", + "label": "Add custom tags", "type": "list", "object_type": "text" } From 463f83a201519592f20fa54a45a329bdcd58b146 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 19 Oct 2022 22:17:40 +0200 Subject: [PATCH 15/55] global: adding filtering custom tags to settings --- openpype/settings/defaults/project_settings/global.json | 3 ++- .../projects_schema/schemas/schema_global_publish.json | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 1b7dc7a41a..b128564bc2 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -78,7 +78,8 @@ "review", "ftrack" ], - "subsets": [] + "subsets": [], + "custom_tags": [] }, "overscan_crop": "", "overscan_color": [ diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 773dea1229..51fc8dedf3 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -295,6 +295,15 @@ "label": "Subsets", "type": "list", "object_type": "text" + }, + { + "type": "separator" + }, + { + "key": "custom_tags", + "label": "Custom Tags", + "type": "list", + "object_type": "text" } ] }, From 9f05131c17849e076b41e96cd0e0ccba1abfa8f0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 19 Oct 2022 22:19:12 +0200 Subject: [PATCH 16/55] global: implementing filtering by custom tags --- openpype/plugins/publish/extract_review.py | 28 +++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 27117510b2..cf8d6429fa 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -1619,6 +1619,24 @@ class ExtractReview(pyblish.api.InstancePlugin): return self.profile_exclusion(matching_profiles) + def custom_tags_filter_validation( + self, repr_custom_tags, output_custom_tags_filter + ): + """Determines if entered custom tags intersect with custom tags filters. + + All cutsom tags values are lowered to avoid unexpected results. + """ + repr_custom_tags = repr_custom_tags or [] + valid = False + for tag in output_custom_tags_filter: + if tag in repr_custom_tags: + valid = True + break + + if valid: + return True + return False + def families_filter_validation(self, families, output_families_filter): """Determines if entered families intersect with families filters. @@ -1656,7 +1674,9 @@ class ExtractReview(pyblish.api.InstancePlugin): return True return False - def filter_output_defs(self, profile, subset_name, families): + def filter_output_defs( + self, profile, subset_name, families, custom_tags=None + ): """Return outputs matching input instance families. Output definitions without families filter are marked as valid. @@ -1689,6 +1709,12 @@ class ExtractReview(pyblish.api.InstancePlugin): if not self.families_filter_validation(families, families_filters): continue + custom_tags_filters = output_filters.get("custom_tags") + if custom_tags and not self.custom_tags_filter_validation( + custom_tags, custom_tags_filters + ): + continue + # Subsets name filters subset_filters = [ subset_filter From 05c87821941bf2dd51f50453fb5d2864a7419092 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 20 Oct 2022 14:32:43 +0200 Subject: [PATCH 17/55] raise exception on collect/save/remove operations of creator --- openpype/pipeline/create/context.py | 85 +++++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 5 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 4ec6d7bdad..6bc70f33ea 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -62,6 +62,22 @@ class HostMissRequiredMethod(Exception): super(HostMissRequiredMethod, self).__init__(msg) +class CreatorOperationFailed(Exception): + pass + + +class CreatorsCollectionFailed(CreatorOperationFailed): + pass + + +class CreatorsSaveFailed(CreatorOperationFailed): + pass + + +class CreatorsRemoveFailed(CreatorOperationFailed): + pass + + class InstanceMember: """Representation of instance member. @@ -1221,8 +1237,26 @@ class CreateContext: self._instances_by_id = {} # Collect instances + failed_creators = [] for creator in self.creators.values(): - creator.collect_instances() + try: + creator.collect_instances() + except: + failed_creators.append(creator) + self.log.warning( + "Collection of instances for creator {} ({}) failed".format( + creator.label, creator.identifier), + exc_info=True + ) + + if failed_creators: + joined_creators = ", ".join( + [creator.label for creator in failed_creators] + ) + + raise CreatorsCollectionFailed( + "Failed to collect instances of creators {}".format(joined_creators) + ) def execute_autocreators(self): """Execute discovered AutoCreator plugins. @@ -1315,16 +1349,35 @@ class CreateContext: identifier = instance.creator_identifier instances_by_identifier[identifier].append(instance) - for identifier, cretor_instances in instances_by_identifier.items(): + failed_creators = [] + for identifier, creator_instances in instances_by_identifier.items(): update_list = [] - for instance in cretor_instances: + for instance in creator_instances: instance_changes = instance.changes() if instance_changes: update_list.append(UpdateData(instance, instance_changes)) creator = self.creators[identifier] if update_list: - creator.update_instances(update_list) + try: + creator.update_instances(update_list) + + except: + failed_creators.append(creator) + self.log.warning( + "Instances update of creator {} ({}) failed".format( + creator.label, creator.identifier), + exc_info=True + ) + + if failed_creators: + joined_creators = ", ".join( + [creator.label for creator in failed_creators] + ) + + raise CreatorsSaveFailed( + "Failed save changes of creators {}".format(joined_creators) + ) def remove_instances(self, instances): """Remove instances from context. @@ -1333,14 +1386,36 @@ class CreateContext: instances(list): Instances that should be removed from context. """ + instances_by_identifier = collections.defaultdict(list) for instance in instances: identifier = instance.creator_identifier instances_by_identifier[identifier].append(instance) + failed_creators = [] for identifier, creator_instances in instances_by_identifier.items(): creator = self.creators.get(identifier) - creator.remove_instances(creator_instances) + try: + creator.remove_instances(creator_instances) + except: + failed_creators.append(creator) + self.log.warning( + "Instances removement of creator {} ({}) failed".format( + creator.label, creator.identifier), + exc_info=True + ) + + if failed_creators: + joined_creators = ", ".join( + [creator.label for creator in failed_creators] + ) + + raise CreatorsRemoveFailed( + "Failed to remove instances of creators {}".format( + joined_creators + ) + ) + def _get_publish_plugins_with_attr_for_family(self, family): """Publish plugin attributes for passed family. From 3f54214a1113bac1ab5b53e8a0f17078eba1ce6a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 20 Oct 2022 14:43:32 +0200 Subject: [PATCH 18/55] don't autotrigger save on controller reset --- openpype/tools/publisher/control.py | 3 --- openpype/tools/publisher/window.py | 41 ++++++++++++++++++----------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index a340f8c1d2..1a15a71040 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1662,11 +1662,8 @@ class PublisherController(BasePublisherController): def reset(self): """Reset everything related to creation and publishing.""" - # Stop publishing self.stop_publish() - self.save_changes() - self.host_is_valid = self._create_context.host_is_valid # Reset avalon context diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 39075d2489..7559b4a641 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -36,7 +36,7 @@ class PublisherWindow(QtWidgets.QDialog): footer_border = 8 publish_footer_spacer = 2 - def __init__(self, parent=None, controller=None, reset_on_show=None): + def __init__(self, parent=None, controller=None, reset_on_first_show=None): super(PublisherWindow, self).__init__(parent) self.setWindowTitle("OpenPype publisher") @@ -44,8 +44,8 @@ class PublisherWindow(QtWidgets.QDialog): icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) self.setWindowIcon(icon) - if reset_on_show is None: - reset_on_show = True + if reset_on_first_show is None: + reset_on_first_show = True if parent is None: on_top_flag = QtCore.Qt.WindowStaysOnTopHint @@ -298,7 +298,8 @@ class PublisherWindow(QtWidgets.QDialog): self._controller = controller self._first_show = True - self._reset_on_show = reset_on_show + self._reset_on_first_show = reset_on_first_show + self._reset_on_show = True self._restart_timer = None self._publish_frame_visible = None @@ -314,6 +315,18 @@ class PublisherWindow(QtWidgets.QDialog): self._first_show = False self._on_first_show() + if not self._reset_on_show: + return + + self._reset_on_show = False + # Detach showing - give OS chance to draw the window + timer = QtCore.QTimer() + timer.setSingleShot(True) + timer.setInterval(1) + timer.timeout.connect(self._on_show_restart_timer) + self._restart_timer = timer + timer.start() + def resizeEvent(self, event): super(PublisherWindow, self).resizeEvent(event) self._update_publish_frame_rect() @@ -324,16 +337,7 @@ class PublisherWindow(QtWidgets.QDialog): def _on_first_show(self): self.resize(self.default_width, self.default_height) self.setStyleSheet(style.load_stylesheet()) - if not self._reset_on_show: - return - - # Detach showing - give OS chance to draw the window - timer = QtCore.QTimer() - timer.setSingleShot(True) - timer.setInterval(1) - timer.timeout.connect(self._on_show_restart_timer) - self._restart_timer = timer - timer.start() + self._reset_on_show = self._reset_on_first_show def _on_show_restart_timer(self): """Callback for '_restart_timer' timer.""" @@ -342,9 +346,13 @@ class PublisherWindow(QtWidgets.QDialog): self.reset() def closeEvent(self, event): - self._controller.save_changes() + self.save_changes() + self._reset_on_show = True super(PublisherWindow, self).closeEvent(event) + def save_changes(self): + self._controller.save_changes() + def reset(self): self._controller.reset() @@ -436,7 +444,8 @@ class PublisherWindow(QtWidgets.QDialog): self._update_publish_frame_rect() def _on_reset_clicked(self): - self._controller.reset() + self.save_changes() + self.reset() def _on_stop_clicked(self): self._controller.stop_publish() From 2d92aed06e530e581229dc96c73ce562ba15ab70 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 20 Oct 2022 15:08:36 +0200 Subject: [PATCH 19/55] handle failed collect,update and remove instances --- openpype/tools/publisher/control.py | 41 ++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 1a15a71040..2346825734 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -31,6 +31,9 @@ from openpype.pipeline.create import ( HiddenCreator, Creator, ) +from openpype.pipeline.create.context import ( + CreatorsOperationFailed, +) # Define constant for plugin orders offset PLUGIN_ORDER_OFFSET = 0.5 @@ -1708,8 +1711,18 @@ class PublisherController(BasePublisherController): self._create_context.reset_context_data() with self._create_context.bulk_instances_collection(): - self._create_context.reset_instances() - self._create_context.execute_autocreators() + try: + self._create_context.reset_instances() + self._create_context.execute_autocreators() + + except CreatorsOperationFailed as exc: + self._emit_event( + "instances.collection.failed", + { + "title": "Instance collection failed", + "message": str(exc) + } + ) self._resetting_instances = False @@ -1845,8 +1858,19 @@ class PublisherController(BasePublisherController): def save_changes(self): """Save changes happened during creation.""" - if self._create_context.host_is_valid: + if not self._create_context.host_is_valid: + return + + try: self._create_context.save_changes() + except CreatorsOperationFailed as exc: + self._emit_event( + "instances.save.failed", + { + "title": "Save failed", + "message": str(exc) + } + ) def remove_instances(self, instance_ids): """Remove instances based on instance ids. @@ -1869,7 +1893,16 @@ class PublisherController(BasePublisherController): instances_by_id[instance_id] for instance_id in instance_ids ] - self._create_context.remove_instances(instances) + try: + self._create_context.remove_instances(instances) + except CreatorsOperationFailed as exc: + self._emit_event( + "instances.remove.failed", + { + "title": "Remove failed", + "message": str(exc) + } + ) def _on_create_instance_change(self): self._emit_event("instances.refresh.finished") From f22e3c3a99697f312d35b769a25126effab5c65d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 20 Oct 2022 15:08:44 +0200 Subject: [PATCH 20/55] show dialogs when creator fails --- openpype/tools/publisher/window.py | 65 ++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 7559b4a641..a7f2ec2ce6 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -1,3 +1,4 @@ +import collections from Qt import QtWidgets, QtCore, QtGui from openpype import ( @@ -222,6 +223,10 @@ class PublisherWindow(QtWidgets.QDialog): # Floating publish frame publish_frame = PublishFrame(controller, self.footer_border, self) + dialog_message_timer = QtCore.QTimer() + dialog_message_timer.setInterval(100) + dialog_message_timer.timeout.connect(self._on_dialog_message_timeout) + help_btn.clicked.connect(self._on_help_click) tabs_widget.tab_changed.connect(self._on_tab_change) overview_widget.active_changed.connect( @@ -259,6 +264,15 @@ class PublisherWindow(QtWidgets.QDialog): controller.event_system.add_callback( "show.card.message", self._on_overlay_message ) + controller.event_system.add_callback( + "instances.collection.failed", self._instance_collection_failed + ) + controller.event_system.add_callback( + "instances.save.failed", self._instance_save_failed + ) + controller.event_system.add_callback( + "instances.remove.failed", self._instance_remove_failed + ) # Store extra header widget for TrayPublisher # - can be used to add additional widgets to header between context @@ -303,6 +317,9 @@ class PublisherWindow(QtWidgets.QDialog): self._restart_timer = None self._publish_frame_visible = None + self._dialog_messages_to_show = collections.deque() + self._dialog_message_timer = dialog_message_timer + self._set_publish_visibility(False) @property @@ -578,3 +595,51 @@ class PublisherWindow(QtWidgets.QDialog): self._publish_frame.move( 0, window_size.height() - height ) + + def add_message_dialog(self, message, title): + self._dialog_messages_to_show.append((message, title)) + self._dialog_message_timer.start() + + def _on_dialog_message_timeout(self): + if not self._dialog_messages_to_show: + self._dialog_message_timer.stop() + return + + item = self._dialog_messages_to_show.popleft() + message, title = item + dialog = MessageDialog(message, title) + dialog.exec_() + + def _instance_collection_failed(self, event): + self.add_message_dialog(event["message"], event["title"]) + + def _instance_save_failed(self, event): + self.add_message_dialog(event["message"], event["title"]) + + def _instance_remove_failed(self, event): + self.add_message_dialog(event["message"], event["title"]) + + +class MessageDialog(QtWidgets.QDialog): + def __init__(self, message, title, parent=None): + super(MessageDialog, self).__init__(parent) + + self.setWindowTitle(title or "Something happend") + + message_widget = QtWidgets.QLabel(message, self) + + btns_widget = QtWidgets.QWidget(self) + submit_btn = QtWidgets.QPushButton("OK", btns_widget) + + btns_layout = QtWidgets.QHBoxLayout(btns_widget) + btns_layout.setContentsMargins(0, 0, 0, 0) + btns_layout.addStretch(1) + btns_layout.addWidget(submit_btn) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(message_widget, 1) + layout.addWidget(btns_widget, 0) + + def showEvent(self, event): + super(MessageDialog, self).showEvent(event) + self.resize(400, 300) From 34dcbcbf1ec987e77e7390071207b36c36aab284 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 20 Oct 2022 15:17:05 +0200 Subject: [PATCH 21/55] renamed 'CreatorOperationFailed' to 'CreatorsOperationFailed' --- openpype/pipeline/create/context.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 6bc70f33ea..fb53b95a92 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -62,19 +62,19 @@ class HostMissRequiredMethod(Exception): super(HostMissRequiredMethod, self).__init__(msg) -class CreatorOperationFailed(Exception): +class CreatorsOperationFailed(Exception): pass -class CreatorsCollectionFailed(CreatorOperationFailed): +class CreatorsCollectionFailed(CreatorsOperationFailed): pass -class CreatorsSaveFailed(CreatorOperationFailed): +class CreatorsSaveFailed(CreatorsOperationFailed): pass -class CreatorsRemoveFailed(CreatorOperationFailed): +class CreatorsRemoveFailed(CreatorsOperationFailed): pass From 65ec73a53a8589e4d3ddff7df2d579e4236cf823 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 20 Oct 2022 15:57:21 +0200 Subject: [PATCH 22/55] fix page change --- 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 a7f2ec2ce6..2199981519 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -498,7 +498,7 @@ class PublisherWindow(QtWidgets.QDialog): self._update_publish_details_widget() if ( not self._tabs_widget.is_current_tab("create") - or not self._tabs_widget.is_current_tab("publish") + and not self._tabs_widget.is_current_tab("publish") ): self._tabs_widget.set_current_tab("publish") From d04231cc6b112457b5e440b3ef6bd7e5f0bd475d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 20 Oct 2022 15:57:30 +0200 Subject: [PATCH 23/55] fix context label before publishing start --- openpype/tools/publisher/control.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 2346825734..d402ab2434 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -302,8 +302,11 @@ class PublishReport: } def _extract_context_data(self, context): + context_label = "Context" + if context is not None: + context_label = context.data.get("label") return { - "label": context.data.get("label") + "label": context_label } def _extract_instance_data(self, instance, exists): From ab40ab6201a32142b702f7ba1ad1545631c34171 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 20 Oct 2022 16:19:02 +0200 Subject: [PATCH 24/55] change init arg back --- openpype/tools/publisher/window.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 2199981519..0e514bd2f2 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -37,7 +37,7 @@ class PublisherWindow(QtWidgets.QDialog): footer_border = 8 publish_footer_spacer = 2 - def __init__(self, parent=None, controller=None, reset_on_first_show=None): + def __init__(self, parent=None, controller=None, reset_on_show=None): super(PublisherWindow, self).__init__(parent) self.setWindowTitle("OpenPype publisher") @@ -45,8 +45,8 @@ class PublisherWindow(QtWidgets.QDialog): icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) self.setWindowIcon(icon) - if reset_on_first_show is None: - reset_on_first_show = True + if reset_on_show is None: + reset_on_show = True if parent is None: on_top_flag = QtCore.Qt.WindowStaysOnTopHint @@ -312,7 +312,9 @@ class PublisherWindow(QtWidgets.QDialog): self._controller = controller self._first_show = True - self._reset_on_first_show = reset_on_first_show + # This is a little bit confusing but 'reset_on_first_show' is too long + # forin init + self._reset_on_first_show = reset_on_show self._reset_on_show = True self._restart_timer = None self._publish_frame_visible = None From 0c899e601b22c6c5c9a8f084d4a7a9ad71b8104c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 20 Oct 2022 17:10:09 +0200 Subject: [PATCH 25/55] fix attributes --- openpype/tools/publisher/control.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index bf1564597f..d2d01e7921 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1669,7 +1669,7 @@ class PublisherController(BasePublisherController): self.host_is_valid = self._create_context.host_is_valid - self.create_context.reset_preparation() + self._create_context.reset_preparation() # Reset avalon context self._create_context.reset_avalon_context() @@ -1681,7 +1681,7 @@ class PublisherController(BasePublisherController): self._reset_publish() self._reset_instances() - self.create_context.reset_finalization() + self._create_context.reset_finalization() self._emit_event("controller.reset.finished") From 2e9572aaebfac811a69a2a13b2eb2af8e094c097 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 20 Oct 2022 17:21:43 +0200 Subject: [PATCH 26/55] use float numbers for animation --- openpype/tools/publisher/widgets/overview_widget.py | 7 ++++--- openpype/tools/publisher/widgets/publish_frame.py | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/openpype/tools/publisher/widgets/overview_widget.py b/openpype/tools/publisher/widgets/overview_widget.py index 5bd3017c2a..4cf8ae0eed 100644 --- a/openpype/tools/publisher/widgets/overview_widget.py +++ b/openpype/tools/publisher/widgets/overview_widget.py @@ -93,8 +93,8 @@ class OverviewWidget(QtWidgets.QFrame): main_layout.addWidget(subset_content_widget, 1) change_anim = QtCore.QVariantAnimation() - change_anim.setStartValue(0) - change_anim.setEndValue(self.anim_end_value) + change_anim.setStartValue(float(0)) + change_anim.setEndValue(float(self.anim_end_value)) change_anim.setDuration(self.anim_duration) change_anim.setEasingCurve(QtCore.QEasingCurve.InOutQuad) @@ -264,9 +264,10 @@ class OverviewWidget(QtWidgets.QFrame): + (self._subset_content_layout.spacing() * 2) ) ) - subset_attrs_width = int(float(width) / self.anim_end_value) * value + subset_attrs_width = int((float(width) / self.anim_end_value) * value) if subset_attrs_width > width: subset_attrs_width = width + create_width = width - subset_attrs_width self._create_widget.setMinimumWidth(create_width) diff --git a/openpype/tools/publisher/widgets/publish_frame.py b/openpype/tools/publisher/widgets/publish_frame.py index e6333a104f..00597451a9 100644 --- a/openpype/tools/publisher/widgets/publish_frame.py +++ b/openpype/tools/publisher/widgets/publish_frame.py @@ -248,13 +248,13 @@ class PublishFrame(QtWidgets.QWidget): hint = self._top_content_widget.minimumSizeHint() end = hint.height() - self._shrunk_anim.setStartValue(start) - self._shrunk_anim.setEndValue(end) + self._shrunk_anim.setStartValue(float(start)) + self._shrunk_anim.setEndValue(float(end)) if not anim_is_running: self._shrunk_anim.start() def _on_shrunk_anim(self, value): - diff = self._top_content_widget.height() - value + diff = self._top_content_widget.height() - int(value) if not self._top_content_widget.isVisible(): diff -= self._content_layout.spacing() From 8049bda095ca1290443e1ebcabb1d9a9a54caeca Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 20 Oct 2022 18:38:00 +0200 Subject: [PATCH 27/55] fix message box --- openpype/tools/publisher/window.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 0e514bd2f2..4f0b81fa85 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -609,8 +609,9 @@ class PublisherWindow(QtWidgets.QDialog): item = self._dialog_messages_to_show.popleft() message, title = item - dialog = MessageDialog(message, title) + dialog = MessageDialog(message, title, self) dialog.exec_() + dialog.deleteLater() def _instance_collection_failed(self, event): self.add_message_dialog(event["message"], event["title"]) @@ -629,6 +630,7 @@ class MessageDialog(QtWidgets.QDialog): self.setWindowTitle(title or "Something happend") message_widget = QtWidgets.QLabel(message, self) + message_widget.setWordWrap(True) btns_widget = QtWidgets.QWidget(self) submit_btn = QtWidgets.QPushButton("OK", btns_widget) @@ -639,9 +641,15 @@ class MessageDialog(QtWidgets.QDialog): btns_layout.addWidget(submit_btn) layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(message_widget, 1) + layout.addWidget(message_widget, 0) + layout.addStretch(1) layout.addWidget(btns_widget, 0) + submit_btn.clicked.connect(self._on_submit_click) + + def _on_submit_click(self): + self.close() + def showEvent(self, event): super(MessageDialog, self).showEvent(event) - self.resize(400, 300) + self.resize(400, 200) From 29a50cc280c13a3be9d6c9971d91e494ac8711d7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 20 Oct 2022 21:23:46 +0200 Subject: [PATCH 28/55] global: exctract review custom tag filtering fix --- openpype/plugins/publish/extract_review.py | 95 +++++++++++----------- 1 file changed, 46 insertions(+), 49 deletions(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index cf8d6429fa..431ddcc3b4 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -128,6 +128,7 @@ class ExtractReview(pyblish.api.InstancePlugin): for repre in instance.data["representations"]: repre_name = str(repre.get("name")) tags = repre.get("tags") or [] + custom_tags = repre.get("custom_tags") if "review" not in tags: self.log.debug(( "Repre: {} - Didn't found \"review\" in tags. Skipping" @@ -158,15 +159,18 @@ class ExtractReview(pyblish.api.InstancePlugin): ) continue - # Filter output definition by representation tags (optional) - outputs = self.filter_outputs_by_tags(profile_outputs, tags) + # Filter output definition by representation's + # custom tags (optional) + outputs = self.filter_outputs_by_custom_tags( + profile_outputs, custom_tags) if not outputs: self.log.info(( "Skipped representation. All output definitions from" " selected profile does not match to representation's" - " tags. \"{}\"" + " custom tags. \"{}\"" ).format(str(tags))) continue + outputs_per_representations.append((repre, outputs)) return outputs_per_representations @@ -1619,24 +1623,6 @@ class ExtractReview(pyblish.api.InstancePlugin): return self.profile_exclusion(matching_profiles) - def custom_tags_filter_validation( - self, repr_custom_tags, output_custom_tags_filter - ): - """Determines if entered custom tags intersect with custom tags filters. - - All cutsom tags values are lowered to avoid unexpected results. - """ - repr_custom_tags = repr_custom_tags or [] - valid = False - for tag in output_custom_tags_filter: - if tag in repr_custom_tags: - valid = True - break - - if valid: - return True - return False - def families_filter_validation(self, families, output_families_filter): """Determines if entered families intersect with families filters. @@ -1675,7 +1661,7 @@ class ExtractReview(pyblish.api.InstancePlugin): return False def filter_output_defs( - self, profile, subset_name, families, custom_tags=None + self, profile, subset_name, families ): """Return outputs matching input instance families. @@ -1684,6 +1670,7 @@ class ExtractReview(pyblish.api.InstancePlugin): Args: profile (dict): Profile from presets matching current context. families (list): All families of current instance. + subset_name (str): name of subset Returns: list: Containg all output definitions matching entered families. @@ -1709,12 +1696,6 @@ class ExtractReview(pyblish.api.InstancePlugin): if not self.families_filter_validation(families, families_filters): continue - custom_tags_filters = output_filters.get("custom_tags") - if custom_tags and not self.custom_tags_filter_validation( - custom_tags, custom_tags_filters - ): - continue - # Subsets name filters subset_filters = [ subset_filter @@ -1737,39 +1718,55 @@ class ExtractReview(pyblish.api.InstancePlugin): return filtered_outputs - def filter_outputs_by_tags(self, outputs, tags): - """Filter output definitions by entered representation tags. + def filter_outputs_by_custom_tags(self, outputs, custom_tags): + """Filter output definitions by entered representation custom_tags. - Output definitions without tags filter are marked as valid. + Output definitions without custom_tags filter are marked as invalid, + only in case representation is having any custom_tags defined. Args: outputs (list): Contain list of output definitions from presets. - tags (list): Tags of processed representation. + custom_tags (list): Custom Tags of processed representation. Returns: list: Containg all output definitions matching entered tags. """ filtered_outputs = [] - repre_tags_low = [tag.lower() for tag in tags] + repre_c_tags_low = [tag.lower() for tag in (custom_tags or [])] for output_def in outputs: - valid = True - output_filters = output_def.get("filter") - if output_filters: - # Check tag filters - tag_filters = output_filters.get("tags") - if tag_filters: - tag_filters_low = [tag.lower() for tag in tag_filters] - valid = False - for tag in repre_tags_low: - if tag in tag_filters_low: - valid = True - break + valid = False + tag_filters = output_def.get("filter", {}).get("custom_tags") - if not valid: - continue + if ( + # if any of tag filter is empty, skip + custom_tags and not tag_filters + or not custom_tags and tag_filters + ): + continue + elif not custom_tags and not tag_filters: + valid = True - if valid: - filtered_outputs.append(output_def) + # lower all filter tags + tag_filters_low = [tag.lower() for tag in tag_filters] + + self.log.debug("__ tag_filters: {}".format(tag_filters)) + self.log.debug("__ repre_c_tags_low: {}".format( + repre_c_tags_low)) + + # check if any repre tag is not in filter tags + for tag in repre_c_tags_low: + if tag in tag_filters_low: + valid = True + break + + if not valid: + continue + + filtered_outputs.append(output_def) + + self.log.debug("__ filtered_outputs: {}".format( + [_o["filename_suffix"] for _o in filtered_outputs] + )) return filtered_outputs From 8e3b2ab933c11ef37678da0b784b971c861afdf6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 10:26:39 +0200 Subject: [PATCH 29/55] raise specific exceptions when creators fail to run their methods --- openpype/pipeline/create/context.py | 258 ++++++++++++++++++++++------ 1 file changed, 204 insertions(+), 54 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index fb53b95a92..dfa9049601 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1,6 +1,8 @@ import os +import sys import copy import logging +import traceback import collections import inspect from uuid import uuid4 @@ -22,6 +24,7 @@ from .creator_plugins import ( Creator, AutoCreator, discover_creator_plugins, + CreatorError, ) UpdateData = collections.namedtuple("UpdateData", ["instance", "changes"]) @@ -63,19 +66,76 @@ class HostMissRequiredMethod(Exception): class CreatorsOperationFailed(Exception): - pass + """Raised when a creator process crashes in 'CreateContext'. + + The exception contains information about the creator and error. The data + are prepared using 'prepare_failed_creator_operation_info' and can be + serialized using json. + + Usage is for UI purposes which may not have access to exceptions directly + and would not have ability to catch exceptions 'per creator'. + + Args: + msg (str): General error message. + failed_info (list[dict[str, Any]]): List of failed creators with + exception message and optionally formatted traceback. + """ + + def __init__(self, msg, failed_info): + super(CreatorsOperationFailed, self).__init__(msg) + self.failed_info = failed_info class CreatorsCollectionFailed(CreatorsOperationFailed): - pass + def __init__(self, failed_info): + msg = "Failed to collect instances" + super(CreatorsCollectionFailed, self).__init__( + msg, failed_info + ) class CreatorsSaveFailed(CreatorsOperationFailed): - pass + def __init__(self, failed_info): + msg = "Failed update instance changes" + super(CreatorsSaveFailed, self).__init__( + msg, failed_info + ) class CreatorsRemoveFailed(CreatorsOperationFailed): - pass + def __init__(self, failed_info): + msg = "Failed to remove instances" + super(CreatorsRemoveFailed, self).__init__( + msg, failed_info + ) + + +class CreatorsCreateFailed(CreatorsOperationFailed): + def __init__(self, failed_info): + msg = "Faled to create instances" + super(CreatorsCreateFailed, self).__init__( + msg, failed_info + ) + + +def prepare_failed_creator_operation_info( + identifier, label, exc_info, add_traceback=True +): + formatted_traceback = None + exc_type, exc_value, exc_traceback = exc_info + error_msg = str(exc_value) + + if add_traceback: + formatted_traceback = "".join(traceback.format_exception( + exc_type, exc_value, exc_traceback + )) + + return { + "creator_identifier": identifier, + "creator_label": label, + "message": error_msg, + "traceback": formatted_traceback + } class InstanceMember: @@ -1202,7 +1262,67 @@ 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. + + 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. + """ + + creator = self.creators.get(identifier) + label = getattr(creator, "label", None) + failed = False + add_traceback = False + try: + # Fake CreatorError (Could be maybe specific exception?) + if creator is None: + raise CreatorError( + "Creator {} was not found".format(identifier) + ) + + creator.create(*args, **kwargs) + + except CreatorError: + failed = True + exc_info = sys.exc_info() + + except: + failed = True + add_traceback = True + exc_info = sys.exc_info() + + if not failed: + return + + self.log.warning( + ( + "Failed to run Creator with identifier \"{}\"." + ).format(identifier), + exc_info=add_traceback + ) + + raise CreatorsCreateFailed([ + prepare_failed_creator_operation_info( + identifier, label, exc_info, add_traceback + ) + ]) + def creator_removed_instance(self, instance): + """When creator removes instance context should be acknowledged. + + If creator removes instance conext should know about it to avoid + possible issues in the session. + + Args: + instance (CreatedInstance): Object of instance which was removed + from scene metadata. + """ + self._instances_by_id.pop(instance.id, None) @contextmanager @@ -1237,42 +1357,72 @@ class CreateContext: self._instances_by_id = {} # Collect instances - failed_creators = [] + failed_info = [] for creator in self.creators.values(): + label = creator.label try: creator.collect_instances() except: - failed_creators.append(creator) + exc_info = sys.exc_info() + identifier = creator.identifier self.log.warning( - "Collection of instances for creator {} ({}) failed".format( - creator.label, creator.identifier), + ( + "Collection of instances for creator {} failed" + ).format(identifier), exc_info=True ) + failed_info.append( + prepare_failed_creator_operation_info( + identifier, label, exc_info + ) + ) - if failed_creators: - joined_creators = ", ".join( - [creator.label for creator in failed_creators] - ) - - raise CreatorsCollectionFailed( - "Failed to collect instances of creators {}".format(joined_creators) - ) + if failed_info: + raise CreatorsCollectionFailed(failed_info) def execute_autocreators(self): """Execute discovered AutoCreator plugins. Reset instances if any autocreator executed properly. """ + + failed_info = [] for identifier, creator in self.autocreators.items(): + label = creator.label + failed = False + add_traceback = False try: creator.create() - except Exception: - # TODO raise report exception if any crashed - msg = ( - "Failed to run AutoCreator with identifier \"{}\" ({})." - ).format(identifier, inspect.getfile(creator.__class__)) - self.log.warning(msg, exc_info=True) + except CreatorError: + failed = True + exc_info = sys.exc_info() + + # 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() + + if not failed: + continue + + failed_info.append( + prepare_failed_creator_operation_info( + identifier, label, exc_info, add_traceback + ) + ) + + self.log.warning( + ( + "Failed to run AutoCreator with identifier \"{}\"." + ).format(identifier), + exc_info=exc_info + ) + + if failed_info: + raise CreatorsCreateFailed(failed_info) def validate_instances_context(self, instances=None): """Validate 'asset' and 'task' instance context.""" @@ -1349,7 +1499,7 @@ class CreateContext: identifier = instance.creator_identifier instances_by_identifier[identifier].append(instance) - failed_creators = [] + failed_info = [] for identifier, creator_instances in instances_by_identifier.items(): update_list = [] for instance in creator_instances: @@ -1358,26 +1508,29 @@ class CreateContext: update_list.append(UpdateData(instance, instance_changes)) creator = self.creators[identifier] - if update_list: - try: - creator.update_instances(update_list) + if not update_list: + continue - except: - failed_creators.append(creator) - self.log.warning( - "Instances update of creator {} ({}) failed".format( - creator.label, creator.identifier), - exc_info=True + label = creator.label + try: + creator.update_instances(update_list) + + except: + exc_info = sys.exc_info() + self.log.warning( + "Instances update of creator \"{}\" failed".format( + identifier), + exc_info=True + ) + + failed_info.append( + prepare_failed_creator_operation_info( + identifier, label, exc_info ) + ) - if failed_creators: - joined_creators = ", ".join( - [creator.label for creator in failed_creators] - ) - - raise CreatorsSaveFailed( - "Failed save changes of creators {}".format(joined_creators) - ) + if failed_info: + raise CreatorsSaveFailed(failed_info) def remove_instances(self, instances): """Remove instances from context. @@ -1392,30 +1545,27 @@ class CreateContext: identifier = instance.creator_identifier instances_by_identifier[identifier].append(instance) - failed_creators = [] + failed_info = [] for identifier, creator_instances in instances_by_identifier.items(): creator = self.creators.get(identifier) + label = creator.label try: creator.remove_instances(creator_instances) except: - failed_creators.append(creator) + exc_info = sys.exc_info() self.log.warning( - "Instances removement of creator {} ({}) failed".format( - creator.label, creator.identifier), + "Instances removement of creator \"{}\" failed".format( + identifier), exc_info=True ) - - if failed_creators: - joined_creators = ", ".join( - [creator.label for creator in failed_creators] - ) - - raise CreatorsRemoveFailed( - "Failed to remove instances of creators {}".format( - joined_creators + failed_info.append( + prepare_failed_creator_operation_info( + identifier, label, exc_info + ) ) - ) + if failed_info: + raise CreatorsRemoveFailed(failed_info) def _get_publish_plugins_with_attr_for_family(self, family): """Publish plugin attributes for passed family. From 91fa300d997abe7f483965c1d342949d6460e61e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 10:57:45 +0200 Subject: [PATCH 30/55] implementaed separator widget --- openpype/tools/utils/widgets.py | 54 +++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py index c8133b3359..ca65182124 100644 --- a/openpype/tools/utils/widgets.py +++ b/openpype/tools/utils/widgets.py @@ -448,3 +448,57 @@ class OptionDialog(QtWidgets.QDialog): def parse(self): return self._options.copy() + + +class SeparatorWidget(QtWidgets.QFrame): + """Prepared widget that can be used as separator with predefined color. + + Args: + size (int): Size of separator (width or height). + orientation (Qt.Horizontal|Qt.Vertical): Orintation of widget. + parent (QtWidgets.QWidget): Parent widget. + """ + + def __init__(self, size=2, orientation=QtCore.Qt.Horizontal, parent=None): + super(SeparatorWidget, self).__init__(parent) + + self.setObjectName("Separator") + + maximum_width = self.maximumWidth() + maximum_height = self.maximumHeight() + + self._size = None + self._orientation = orientation + self._maximum_width = maximum_width + self._maximum_height = maximum_height + self.set_size(size) + + def set_size(self, size): + if size == self._size: + return + if self._orientation == QtCore.Qt.Vertical: + self.setMinimumWidth(size) + self.setMaximumWidth(size) + else: + self.setMinimumHeight(size) + self.setMaximumHeight(size) + + self._size = size + + def set_orientation(self, orientation): + if self._orientation == orientation: + return + + # Reset min/max sizes in opossite direction + if self._orientation == QtCore.Qt.Vertical: + self.setMinimumHeight(0) + self.setMaximumHeight(self._maximum_height) + else: + self.setMinimumWidth(0) + self.setMaximumWidth(self._maximum_width) + + self._orientation = orientation + + size = self._size + self._size = None + self.set_size(size) From 20dd53a830b6745a89b4203c681e8eea54b079cc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 10:58:08 +0200 Subject: [PATCH 31/55] use separator widget in error dialog --- openpype/tools/utils/error_dialog.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/openpype/tools/utils/error_dialog.py b/openpype/tools/utils/error_dialog.py index f7b12bb69f..30cba56416 100644 --- a/openpype/tools/utils/error_dialog.py +++ b/openpype/tools/utils/error_dialog.py @@ -1,6 +1,6 @@ from Qt import QtWidgets, QtCore -from .widgets import ClickableFrame, ExpandBtn +from .widgets import ClickableFrame, ExpandBtn, SeparatorWidget def convert_text_for_html(text): @@ -139,12 +139,10 @@ class ErrorMessageBox(QtWidgets.QDialog): mime_data ) - def _create_line(self): - line = QtWidgets.QFrame(self) - line.setObjectName("Separator") - line.setMinimumHeight(2) - line.setMaximumHeight(2) - return line + def _create_line(self, parent=None): + if parent is None: + parent = self + return SeparatorWidget(2, parent=parent) def _create_traceback_widget(self, traceback_text, parent=None): if parent is None: From 9ae99eb4badd874b1181697179fc9747963a2c58 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 10:58:34 +0200 Subject: [PATCH 32/55] change function name 'convert_text_for_html' to 'escape_text_for_html' --- openpype/tools/utils/error_dialog.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/tools/utils/error_dialog.py b/openpype/tools/utils/error_dialog.py index 30cba56416..d4ca91848c 100644 --- a/openpype/tools/utils/error_dialog.py +++ b/openpype/tools/utils/error_dialog.py @@ -3,7 +3,7 @@ from Qt import QtWidgets, QtCore from .widgets import ClickableFrame, ExpandBtn, SeparatorWidget -def convert_text_for_html(text): +def escape_text_for_html(text): return ( text .replace("<", "<") @@ -19,7 +19,7 @@ class TracebackWidget(QtWidgets.QWidget): # Modify text to match html # - add more replacements when needed - tb_text = convert_text_for_html(tb_text) + tb_text = escape_text_for_html(tb_text) expand_btn = ExpandBtn(self) clickable_frame = ClickableFrame(self) @@ -110,7 +110,7 @@ class ErrorMessageBox(QtWidgets.QDialog): @staticmethod def convert_text_for_html(text): - return convert_text_for_html(text) + return escape_text_for_html(text) def _create_top_widget(self, parent_widget): label_widget = QtWidgets.QLabel(parent_widget) From ad28ea8121dc62f7dbe708ef3f2bbbe21d111810 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 10:58:55 +0200 Subject: [PATCH 33/55] top widget is optional --- openpype/tools/utils/error_dialog.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/tools/utils/error_dialog.py b/openpype/tools/utils/error_dialog.py index d4ca91848c..9c9015c00f 100644 --- a/openpype/tools/utils/error_dialog.py +++ b/openpype/tools/utils/error_dialog.py @@ -91,11 +91,12 @@ class ErrorMessageBox(QtWidgets.QDialog): footer_layout.addWidget(ok_btn, 0) bottom_line = self._create_line() - body_layout = QtWidgets.QVBoxLayout(self) - body_layout.addWidget(top_widget, 0) - body_layout.addWidget(content_scroll, 1) - body_layout.addWidget(bottom_line, 0) - body_layout.addLayout(footer_layout, 0) + main_layout = QtWidgets.QVBoxLayout(self) + if top_widget is not None: + main_layout.addWidget(top_widget, 0) + main_layout.addWidget(content_scroll, 1) + main_layout.addWidget(bottom_line, 0) + main_layout.addWidget(footer_widget, 0) copy_report_btn.clicked.connect(self._on_copy_report) ok_btn.clicked.connect(self._on_ok_clicked) From 280e4fe0ca3d32ae506969a249034bfd720976f5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 11:00:59 +0200 Subject: [PATCH 34/55] change separator of report copy --- openpype/tools/utils/error_dialog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/tools/utils/error_dialog.py b/openpype/tools/utils/error_dialog.py index 9c9015c00f..6973fda8c4 100644 --- a/openpype/tools/utils/error_dialog.py +++ b/openpype/tools/utils/error_dialog.py @@ -132,7 +132,8 @@ class ErrorMessageBox(QtWidgets.QDialog): self.close() def _on_copy_report(self): - report_text = (10 * "*").join(self._report_data) + sep = "\n{}\n".format(10 * "*") + report_text = sep.join(self._report_data) mime_data = QtCore.QMimeData() mime_data.setText(report_text) From 239d2f90bfd729f1a6fe58883b6fd992e9242de2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 11:01:41 +0200 Subject: [PATCH 35/55] footer layout is under widget and store the widget to self --- openpype/tools/utils/error_dialog.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/tools/utils/error_dialog.py b/openpype/tools/utils/error_dialog.py index 6973fda8c4..5fe49a53af 100644 --- a/openpype/tools/utils/error_dialog.py +++ b/openpype/tools/utils/error_dialog.py @@ -85,7 +85,9 @@ class ErrorMessageBox(QtWidgets.QDialog): copy_report_btn = QtWidgets.QPushButton("Copy report", self) ok_btn = QtWidgets.QPushButton("OK", self) - footer_layout = QtWidgets.QHBoxLayout() + footer_widget = QtWidgets.QWidget(self) + footer_layout = QtWidgets.QHBoxLayout(footer_widget) + footer_layout.setContentsMargins(0, 0, 0, 0) footer_layout.addWidget(copy_report_btn, 0) footer_layout.addStretch(1) footer_layout.addWidget(ok_btn, 0) @@ -107,6 +109,8 @@ class ErrorMessageBox(QtWidgets.QDialog): if not report_data: copy_report_btn.setVisible(False) + self._content_scroll = content_scroll + self._footer_widget = footer_widget self._report_data = report_data @staticmethod From f4b123d65a5a10e871dfdd2adf1fe6c92a2a6727 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 11:01:52 +0200 Subject: [PATCH 36/55] added separator widget to public widgets --- openpype/tools/utils/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/tools/utils/__init__.py b/openpype/tools/utils/__init__.py index 5ccc1b40b3..019ea16391 100644 --- a/openpype/tools/utils/__init__.py +++ b/openpype/tools/utils/__init__.py @@ -7,6 +7,7 @@ from .widgets import ( ExpandBtn, PixmapLabel, IconButton, + SeparatorWidget, ) from .views import DeselectableTreeView from .error_dialog import ErrorMessageBox @@ -37,6 +38,7 @@ __all__ = ( "ExpandBtn", "PixmapLabel", "IconButton", + "SeparatorWidget", "DeselectableTreeView", From 36a9aa2da61d583e9ddd8209b92614f751a42bbf Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 11:02:57 +0200 Subject: [PATCH 37/55] modified error dialog to show more information by creator --- openpype/tools/publisher/window.py | 153 +++++++++++++++++++++-------- 1 file changed, 114 insertions(+), 39 deletions(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 4f0b81fa85..1bbc0d0cf4 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -6,6 +6,7 @@ from openpype import ( style ) from openpype.tools.utils import ( + ErrorMessageBox, PlaceholderLineEdit, MessageOverlayObject, PixmapLabel, @@ -223,9 +224,11 @@ class PublisherWindow(QtWidgets.QDialog): # Floating publish frame publish_frame = PublishFrame(controller, self.footer_border, self) - dialog_message_timer = QtCore.QTimer() - dialog_message_timer.setInterval(100) - dialog_message_timer.timeout.connect(self._on_dialog_message_timeout) + creators_dialog_message_timer = QtCore.QTimer() + creators_dialog_message_timer.setInterval(100) + creators_dialog_message_timer.timeout.connect( + self._on_creators_message_timeout + ) help_btn.clicked.connect(self._on_help_click) tabs_widget.tab_changed.connect(self._on_tab_change) @@ -273,6 +276,9 @@ class PublisherWindow(QtWidgets.QDialog): controller.event_system.add_callback( "instances.remove.failed", self._instance_remove_failed ) + controller.event_system.add_callback( + "instances.create.failed", self._instance_create_failed + ) # Store extra header widget for TrayPublisher # - can be used to add additional widgets to header between context @@ -319,8 +325,8 @@ class PublisherWindow(QtWidgets.QDialog): self._restart_timer = None self._publish_frame_visible = None - self._dialog_messages_to_show = collections.deque() - self._dialog_message_timer = dialog_message_timer + self._creators_messages_to_show = collections.deque() + self._creators_dialog_message_timer = creators_dialog_message_timer self._set_publish_visibility(False) @@ -598,58 +604,127 @@ class PublisherWindow(QtWidgets.QDialog): 0, window_size.height() - height ) - def add_message_dialog(self, message, title): - self._dialog_messages_to_show.append((message, title)) - self._dialog_message_timer.start() + def add_message_dialog(self, title, failed_info): + self._creators_messages_to_show.append((title, failed_info)) + self._creators_dialog_message_timer.start() - def _on_dialog_message_timeout(self): - if not self._dialog_messages_to_show: - self._dialog_message_timer.stop() + def _on_creators_message_timeout(self): + if not self._creators_messages_to_show: + self._creators_dialog_message_timer.stop() return - item = self._dialog_messages_to_show.popleft() - message, title = item - dialog = MessageDialog(message, title, self) + item = self._creators_messages_to_show.popleft() + title, failed_info = item + dialog = CreatorsErrorMessageBox(title, failed_info, self) dialog.exec_() dialog.deleteLater() def _instance_collection_failed(self, event): - self.add_message_dialog(event["message"], event["title"]) + self.add_message_dialog(event["title"], event["failed_info"]) def _instance_save_failed(self, event): - self.add_message_dialog(event["message"], event["title"]) + self.add_message_dialog(event["title"], event["failed_info"]) def _instance_remove_failed(self, event): - self.add_message_dialog(event["message"], event["title"]) + self.add_message_dialog(event["title"], event["failed_info"]) + + def _instance_create_failed(self, event): + self.add_message_dialog(event["title"], event["failed_info"]) -class MessageDialog(QtWidgets.QDialog): - def __init__(self, message, title, parent=None): - super(MessageDialog, self).__init__(parent) +class CreatorsErrorMessageBox(ErrorMessageBox): + def __init__(self, error_title, failed_info, parent): + self._failed_info = failed_info + self._info_with_id = [ + {"id": idx, "info": info} + for idx, info in enumerate(failed_info) + ] + self._widgets_by_id = {} + self._tabs_widget = None + self._stack_layout = None - self.setWindowTitle(title or "Something happend") + super(CreatorsErrorMessageBox, self).__init__(error_title, parent) - message_widget = QtWidgets.QLabel(message, self) - message_widget.setWordWrap(True) + layout = self.layout() + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) - btns_widget = QtWidgets.QWidget(self) - submit_btn = QtWidgets.QPushButton("OK", btns_widget) + footer_layout = self._footer_widget.layout() + footer_layout.setContentsMargins(5, 5, 5, 5) - btns_layout = QtWidgets.QHBoxLayout(btns_widget) - btns_layout.setContentsMargins(0, 0, 0, 0) - btns_layout.addStretch(1) - btns_layout.addWidget(submit_btn) + def _create_top_widget(self, parent_widget): + return None - layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(message_widget, 0) - layout.addStretch(1) - layout.addWidget(btns_widget, 0) + def _get_report_data(self): + output = [] + for info in self._failed_info: + creator_label = info["creator_label"] + creator_identifier = info["creator_identifier"] + report_message = "Creator:" + if creator_label: + report_message += " {} ({})".format( + creator_label, creator_identifier) + else: + report_message += " {}".format(creator_identifier) - submit_btn.clicked.connect(self._on_submit_click) + report_message += "\n\nError: {}".format(info["message"]) + formatted_traceback = info["traceback"] + if formatted_traceback: + report_message += "\n\n{}".format(formatted_traceback) + output.append(report_message) + return output - def _on_submit_click(self): - self.close() + def _create_content(self, content_layout): + tabs_widget = PublisherTabsWidget(self) - def showEvent(self, event): - super(MessageDialog, self).showEvent(event) - self.resize(400, 200) + stack_widget = QtWidgets.QFrame(self._content_widget) + stack_layout = QtWidgets.QStackedLayout(stack_widget) + + first = True + for item in self._info_with_id: + item_id = item["id"] + info = item["info"] + message = info["message"] + formatted_traceback = info["traceback"] + creator_label = info["creator_label"] + creator_identifier = info["creator_identifier"] + if not creator_label: + creator_label = creator_identifier + + msg_widget = QtWidgets.QWidget(stack_widget) + msg_layout = QtWidgets.QVBoxLayout(msg_widget) + + exc_msg_template = "{}" + message_label_widget = QtWidgets.QLabel(msg_widget) + message_label_widget.setText( + exc_msg_template.format(self.convert_text_for_html(message)) + ) + msg_layout.addWidget(message_label_widget, 0) + + if formatted_traceback: + line_widget = self._create_line(msg_widget) + tb_widget = self._create_traceback_widget(formatted_traceback) + msg_layout.addWidget(line_widget, 0) + msg_layout.addWidget(tb_widget, 0) + + msg_layout.addStretch(1) + + tabs_widget.add_tab(creator_label, item_id) + stack_layout.addWidget(msg_widget) + if first: + first = False + stack_layout.setCurrentWidget(msg_widget) + + self._widgets_by_id[item_id] = msg_widget + + content_layout.addWidget(tabs_widget, 0) + content_layout.addWidget(stack_widget, 1) + + tabs_widget.tab_changed.connect(self._on_tab_change) + + self._tabs_widget = tabs_widget + self._stack_layout = stack_layout + + def _on_tab_change(self, identifier): + widget = self._widgets_by_id[identifier] + self._stack_layout.setCurrentWidget(widget) From 098903260c79be64c58a07eb7616e24c04c41de9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 11:03:15 +0200 Subject: [PATCH 38/55] controller is triggering event on creator operation fail --- openpype/tools/publisher/control.py | 45 +++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index d402ab2434..280d2cf0a0 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1107,6 +1107,8 @@ class AbstractPublisherController(object): options (Dict[str, Any]): Data from pre-create attributes. """ + pass + def save_changes(self): """Save changes in create context.""" @@ -1716,14 +1718,24 @@ class PublisherController(BasePublisherController): with self._create_context.bulk_instances_collection(): try: self._create_context.reset_instances() - self._create_context.execute_autocreators() - except CreatorsOperationFailed as exc: self._emit_event( "instances.collection.failed", { "title": "Instance collection failed", - "message": str(exc) + "failed_info": exc.failed_info + } + ) + + try: + self._create_context.execute_autocreators() + + except CreatorsOperationFailed as exc: + self._emit_event( + "instances.create.failed", + { + "title": "AutoCreation failed", + "failed_info": exc.failed_info } ) @@ -1854,10 +1866,24 @@ class PublisherController(BasePublisherController): self, creator_identifier, subset_name, instance_data, options ): """Trigger creation and refresh of instances in UI.""" - creator = self._creators[creator_identifier] - creator.create(subset_name, instance_data, options) + + success = True + try: + self._create_context.create( + creator_identifier, subset_name, instance_data, options + ) + except CreatorsOperationFailed as exc: + success = False + self._emit_event( + "instances.create.failed", + { + "title": "Creation failed", + "failed_info": exc.failed_info + } + ) self._on_create_instance_change() + return success def save_changes(self): """Save changes happened during creation.""" @@ -1866,12 +1892,13 @@ class PublisherController(BasePublisherController): try: self._create_context.save_changes() + except CreatorsOperationFailed as exc: self._emit_event( "instances.save.failed", { - "title": "Save failed", - "message": str(exc) + "title": "Instances save failed", + "failed_info": exc.failed_info } ) @@ -1902,8 +1929,8 @@ class PublisherController(BasePublisherController): self._emit_event( "instances.remove.failed", { - "title": "Remove failed", - "message": str(exc) + "title": "Instance removement failed", + "failed_info": exc.failed_info } ) From accab0ca5f17996ff0db8e178dae1782eec31c4a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 11:03:30 +0200 Subject: [PATCH 39/55] create widget does not handle failed creation on it's own --- .../tools/publisher/widgets/create_widget.py | 118 ++---------------- 1 file changed, 7 insertions(+), 111 deletions(-) diff --git a/openpype/tools/publisher/widgets/create_widget.py b/openpype/tools/publisher/widgets/create_widget.py index 10cf39675e..910b2adfc7 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -9,7 +9,6 @@ from openpype.pipeline.create import ( SUBSET_NAME_ALLOWED_SYMBOLS, TaskNotSetError, ) -from openpype.tools.utils import ErrorMessageBox from .widgets import ( IconValuePixmapLabel, @@ -35,79 +34,6 @@ class VariantInputsWidget(QtWidgets.QWidget): self.resized.emit() -class CreateErrorMessageBox(ErrorMessageBox): - def __init__( - self, - creator_label, - subset_name, - asset_name, - exc_msg, - formatted_traceback, - parent - ): - self._creator_label = creator_label - self._subset_name = subset_name - self._asset_name = asset_name - self._exc_msg = exc_msg - self._formatted_traceback = formatted_traceback - super(CreateErrorMessageBox, self).__init__("Creation failed", parent) - - def _create_top_widget(self, parent_widget): - label_widget = QtWidgets.QLabel(parent_widget) - label_widget.setText( - "Failed to create" - ) - return label_widget - - def _get_report_data(self): - report_message = ( - "{creator}: Failed to create Subset: \"{subset}\"" - " in Asset: \"{asset}\"" - "\n\nError: {message}" - ).format( - creator=self._creator_label, - subset=self._subset_name, - asset=self._asset_name, - message=self._exc_msg, - ) - if self._formatted_traceback: - report_message += "\n\n{}".format(self._formatted_traceback) - return [report_message] - - def _create_content(self, content_layout): - item_name_template = ( - "Creator: {}
" - "Subset: {}
" - "Asset: {}
" - ) - exc_msg_template = "{}" - - line = self._create_line() - content_layout.addWidget(line) - - item_name_widget = QtWidgets.QLabel(self) - item_name_widget.setText( - item_name_template.format( - self._creator_label, self._subset_name, self._asset_name - ) - ) - content_layout.addWidget(item_name_widget) - - message_label_widget = QtWidgets.QLabel(self) - message_label_widget.setText( - exc_msg_template.format(self.convert_text_for_html(self._exc_msg)) - ) - content_layout.addWidget(message_label_widget) - - if self._formatted_traceback: - line_widget = self._create_line() - tb_widget = self._create_traceback_widget( - self._formatted_traceback - ) - content_layout.addWidget(line_widget) - content_layout.addWidget(tb_widget) - - # TODO add creator identifier/label to details class CreatorShortDescWidget(QtWidgets.QWidget): def __init__(self, parent=None): @@ -178,8 +104,6 @@ class CreateWidget(QtWidgets.QWidget): self._prereq_available = False - self._message_dialog = None - name_pattern = "^[{}]*$".format(SUBSET_NAME_ALLOWED_SYMBOLS) self._name_pattern = name_pattern self._compiled_name_pattern = re.compile(name_pattern) @@ -769,7 +693,6 @@ class CreateWidget(QtWidgets.QWidget): return index = indexes[0] - creator_label = index.data(QtCore.Qt.DisplayRole) creator_identifier = index.data(CREATOR_IDENTIFIER_ROLE) family = index.data(FAMILY_ROLE) variant = self.variant_input.text() @@ -792,40 +715,13 @@ class CreateWidget(QtWidgets.QWidget): "family": family } - error_msg = None - formatted_traceback = None - try: - self._controller.create( - creator_identifier, - subset_name, - instance_data, - pre_create_data - ) + success = self._controller.create( + creator_identifier, + subset_name, + instance_data, + pre_create_data + ) - except CreatorError as exc: - error_msg = str(exc) - - # Use bare except because some hosts raise their exceptions that - # do not inherit from python's `BaseException` - except: - exc_type, exc_value, exc_traceback = sys.exc_info() - formatted_traceback = "".join(traceback.format_exception( - exc_type, exc_value, exc_traceback - )) - error_msg = str(exc_value) - - if error_msg is None: + if success: self._set_creator(self._selected_creator) self._controller.emit_card_message("Creation finished...") - else: - box = CreateErrorMessageBox( - creator_label, - subset_name, - asset_name, - error_msg, - formatted_traceback, - parent=self - ) - box.show() - # Store dialog so is not garbage collected before is shown - self._message_dialog = box From bda1bb3f292c05004546870ddd39d4c4df8bd384 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 11:46:02 +0200 Subject: [PATCH 40/55] unify the error handling in create context --- openpype/pipeline/create/context.py | 123 +++++++++++++++++----------- 1 file changed, 76 insertions(+), 47 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index dfa9049601..2dfdfc142f 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -123,8 +123,6 @@ def prepare_failed_creator_operation_info( ): formatted_traceback = None exc_type, exc_value, exc_traceback = exc_info - error_msg = str(exc_value) - if add_traceback: formatted_traceback = "".join(traceback.format_exception( exc_type, exc_value, exc_traceback @@ -133,7 +131,7 @@ def prepare_failed_creator_operation_info( return { "creator_identifier": identifier, "creator_label": label, - "message": error_msg, + "message": str(exc_value), "traceback": formatted_traceback } @@ -1274,10 +1272,12 @@ class CreateContext: **kwargs (Dict[Any, Any]): Keyword argument for create method. """ + 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 try: # Fake CreatorError (Could be maybe specific exception?) if creator is None: @@ -1290,27 +1290,23 @@ class CreateContext: 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() - - if not failed: - return - - self.log.warning( - ( - "Failed to run Creator with identifier \"{}\"." - ).format(identifier), - exc_info=add_traceback - ) - - raise CreatorsCreateFailed([ - prepare_failed_creator_operation_info( - identifier, label, exc_info, add_traceback + 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 + ) + ]) def creator_removed_instance(self, instance): """When creator removes instance context should be acknowledged. @@ -1357,23 +1353,35 @@ class CreateContext: self._instances_by_id = {} # Collect instances + error_message = "Collection of instances for creator {} failed. {}" failed_info = [] for creator in self.creators.values(): label = creator.label + identifier = creator.identifier + failed = False + add_traceback = False + exc_info = None try: creator.collect_instances() - except: + + 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() - identifier = creator.identifier self.log.warning( - ( - "Collection of instances for creator {} failed" - ).format(identifier), + error_message.format(identifier, ""), exc_info=True ) + + if failed: failed_info.append( prepare_failed_creator_operation_info( - identifier, label, exc_info + identifier, label, exc_info, add_traceback ) ) @@ -1386,6 +1394,7 @@ 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 @@ -1397,6 +1406,7 @@ class CreateContext: 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` @@ -1404,22 +1414,17 @@ class CreateContext: failed = True add_traceback = True exc_info = sys.exc_info() - - if not failed: - continue - - failed_info.append( - prepare_failed_creator_operation_info( - identifier, label, exc_info, add_traceback + self.log.warning( + error_message.format(identifier, ""), + exc_info=True ) - ) - self.log.warning( - ( - "Failed to run AutoCreator with identifier \"{}\"." - ).format(identifier), - exc_info=exc_info - ) + if failed: + failed_info.append( + prepare_failed_creator_operation_info( + identifier, label, exc_info, add_traceback + ) + ) if failed_info: raise CreatorsCreateFailed(failed_info) @@ -1499,6 +1504,7 @@ class CreateContext: identifier = instance.creator_identifier instances_by_identifier[identifier].append(instance) + error_message = "Instances update of creator \"{}\" failed. {}" failed_info = [] for identifier, creator_instances in instances_by_identifier.items(): update_list = [] @@ -1512,20 +1518,28 @@ class CreateContext: continue label = creator.label + failed = False + add_traceback = False + exc_info = None try: creator.update_instances(update_list) + 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( - "Instances update of creator \"{}\" failed".format( - identifier), - exc_info=True - ) + error_message.format(identifier, ""), exc_info=True) + if failed: failed_info.append( prepare_failed_creator_operation_info( - identifier, label, exc_info + identifier, label, exc_info, add_traceback ) ) @@ -1545,22 +1559,37 @@ class CreateContext: identifier = instance.creator_identifier instances_by_identifier[identifier].append(instance) + error_message = "Instances removement of creator \"{}\" failed. {}" failed_info = [] for identifier, creator_instances in instances_by_identifier.items(): creator = self.creators.get(identifier) label = creator.label + failed = False + add_traceback = False + exc_info = None try: creator.remove_instances(creator_instances) - except: + + except CreatorError: + failed = True exc_info = sys.exc_info() self.log.warning( - "Instances removement of creator \"{}\" failed".format( - identifier), + 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: failed_info.append( prepare_failed_creator_operation_info( - identifier, label, exc_info + identifier, label, exc_info, add_traceback ) ) From 1f2e54c2c385af56c5d61d2976e8db434d9cb4ec Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 11:46:21 +0200 Subject: [PATCH 41/55] fix tab switch --- openpype/tools/publisher/window.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 1bbc0d0cf4..b6bd506c18 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -636,7 +636,8 @@ class CreatorsErrorMessageBox(ErrorMessageBox): def __init__(self, error_title, failed_info, parent): self._failed_info = failed_info self._info_with_id = [ - {"id": idx, "info": info} + # Id must be string when used in tab widget + {"id": str(idx), "info": info} for idx, info in enumerate(failed_info) ] self._widgets_by_id = {} @@ -725,6 +726,6 @@ class CreatorsErrorMessageBox(ErrorMessageBox): self._tabs_widget = tabs_widget self._stack_layout = stack_layout - def _on_tab_change(self, identifier): + def _on_tab_change(self, old_identifier, identifier): widget = self._widgets_by_id[identifier] self._stack_layout.setCurrentWidget(widget) From 3de3d303895cfe4bb92aaf373613d9d54871b432 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 16:47:06 +0200 Subject: [PATCH 42/55] pass creator to cache function --- openpype/hosts/traypublisher/api/plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py index 2cb5a8729f..555041d389 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -37,7 +37,7 @@ class HiddenTrayPublishCreator(HiddenCreator): host_name = "traypublisher" def collect_instances(self): - for instance_data in _cache_and_get_instances(): + for instance_data in _cache_and_get_instances(self): creator_id = instance_data.get("creator_identifier") if creator_id == self.identifier: instance = CreatedInstance.from_existing( @@ -74,7 +74,7 @@ class TrayPublishCreator(Creator): host_name = "traypublisher" def collect_instances(self): - for instance_data in _cache_and_get_instances(): + for instance_data in _cache_and_get_instances(self): creator_id = instance_data.get("creator_identifier") if creator_id == self.identifier: instance = CreatedInstance.from_existing( From ba621ee54a9f7bc318dd3701ec80b3ee18354f55 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 22 Oct 2022 04:02:46 +0000 Subject: [PATCH 43/55] [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 fd3606e9f2..cda0a98ef3 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.14.4" +__version__ = "3.14.5-nightly.1" From d0d8c8958ce81c17f72d1717822699c28a6ba04c Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 24 Oct 2022 11:29:58 +0200 Subject: [PATCH 44/55] fix obj extractor --- openpype/hosts/maya/plugins/load/actions.py | 2 +- .../hosts/maya/plugins/publish/extract_obj.py | 51 ++++++++++++------- .../defaults/project_settings/maya.json | 24 +++++---- .../schemas/schema_maya_publish.json | 19 +++++++ 4 files changed, 67 insertions(+), 29 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/actions.py b/openpype/hosts/maya/plugins/load/actions.py index 253dae1e43..eca1b27f34 100644 --- a/openpype/hosts/maya/plugins/load/actions.py +++ b/openpype/hosts/maya/plugins/load/actions.py @@ -90,7 +90,7 @@ class ImportMayaLoader(load.LoaderPlugin): so you could also use it as a new base. """ - representations = ["ma", "mb"] + representations = ["ma", "mb", "obj"] families = ["*"] label = "Import" diff --git a/openpype/hosts/maya/plugins/publish/extract_obj.py b/openpype/hosts/maya/plugins/publish/extract_obj.py index 7c915a80d8..59f11a4aa9 100644 --- a/openpype/hosts/maya/plugins/publish/extract_obj.py +++ b/openpype/hosts/maya/plugins/publish/extract_obj.py @@ -2,15 +2,12 @@ import os from maya import cmds -import maya.mel as mel +# import maya.mel as mel import pyblish.api -import openpype.api -from openpype.hosts.maya.api.lib import maintained_selection +from openpype.pipeline import publish +from openpype.hosts.maya.api import lib -from openpype.hosts.maya.api import obj - - -class ExtractObj(openpype.api.Extractor): +class ExtractObj(publish.Extractor): """Extract OBJ from Maya. This extracts reproducible OBJ exports ignoring any of the settings @@ -18,42 +15,60 @@ class ExtractObj(openpype.api.Extractor): """ order = pyblish.api.ExtractorOrder + hosts = ["maya"] label = "Extract OBJ" - families = ["obj"] + families = ["model"] def process(self, instance): - obj_exporter = obj.OBJExtractor(log=self.log) # Define output path staging_dir = self.staging_dir(instance) - filename = "{0}.fbx".format(instance.name) + filename = "{0}.obj".format(instance.name) path = os.path.join(staging_dir, filename) # The export requires forward slashes because we need to # format it into a string in a mel expression - path = path.replace('\\', '/') self.log.info("Extracting OBJ to: {0}".format(path)) - members = instance.data["setMembners"] + members = instance.data("setMembers") + members = cmds.ls(members, + dag=True, + shapes=True, + type=("mesh", "nurbsCurve"), + noIntermediate=True, + long=True) self.log.info("Members: {0}".format(members)) self.log.info("Instance: {0}".format(instance[:])) - obj_exporter.set_options_from_instance(instance) + if not cmds.pluginInfo('objExport', query=True, loaded=True): + cmds.loadPlugin('objExport') # Export - with maintained_selection(): - obj_exporter.export(members, path) - cmds.select(members, r=1, noExpand=True) - mel.eval('file -force -options "{0};{1};{2};{3};{4}" -typ "OBJexport" -pr -es "{5}";'.format(grp_flag, ptgrp_flag, mats_flag, smooth_flag, normals_flag, path)) # noqa + with lib.no_display_layers(instance): + with lib.displaySmoothness(members, + divisionsU=0, + divisionsV=0, + pointsWire=4, + pointsShaded=1, + polygonObject=1): + with lib.shader(members, + shadingEngine="initialShadingGroup"): + with lib.maintained_selection(): + cmds.select(members, noExpand=True) + cmds.file(path, + exportSelected=True, + type='OBJexport', + preserveReferences=True, + force=True) if "representation" not in instance.data: instance.data["representation"] = [] representation = { 'name':'obj', - 'ext':'obx', + 'ext':'obj', 'files': filename, "stagingDir": staging_dir, } diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 86815b8fc4..b0bef4943b 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -131,6 +131,16 @@ "Main" ] }, + "CreateModel": { + "enabled": true, + "write_color_sets": false, + "write_face_sets": false, + "defaults": [ + "Main", + "Proxy", + "Sculpt" + ] + }, "CreatePointCache": { "enabled": true, "write_color_sets": false, @@ -187,16 +197,6 @@ "Main" ] }, - "CreateModel": { - "enabled": true, - "write_color_sets": false, - "write_face_sets": false, - "defaults": [ - "Main", - "Proxy", - "Sculpt" - ] - }, "CreateRenderSetup": { "enabled": true, "defaults": [ @@ -577,6 +577,10 @@ "vrayproxy" ] }, + "ExtractObj": { + "enabled": true, + "optional": true + }, "ValidateRigContents": { "enabled": false, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index 53247f6bd4..ab8c6b885e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -657,6 +657,25 @@ "object_type": "text" } ] + }, + { + "type": "dict", + "collapsible": true, + "key": "ExtractObj", + "label": "Extract OBJ", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + } + ] } ] }, From 96af8158796dad310feae7f96f74677e4311710f Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 24 Oct 2022 11:36:43 +0200 Subject: [PATCH 45/55] turn off OBJ by default --- openpype/settings/defaults/project_settings/maya.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index b0bef4943b..988c0e777a 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -578,7 +578,7 @@ ] }, "ExtractObj": { - "enabled": true, + "enabled": false, "optional": true }, "ValidateRigContents": { From f0a394bfd9e749f3bde00fd4d43ba5921192fe7e Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 24 Oct 2022 11:37:02 +0200 Subject: [PATCH 46/55] =?UTF-8?q?=F0=9F=90=95=E2=80=8D=F0=9F=A6=BA=20shut?= =?UTF-8?q?=20up=20hound?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hosts/maya/plugins/publish/extract_obj.py | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_obj.py b/openpype/hosts/maya/plugins/publish/extract_obj.py index 59f11a4aa9..edfe0b9439 100644 --- a/openpype/hosts/maya/plugins/publish/extract_obj.py +++ b/openpype/hosts/maya/plugins/publish/extract_obj.py @@ -7,6 +7,7 @@ import pyblish.api from openpype.pipeline import publish from openpype.hosts.maya.api import lib + class ExtractObj(publish.Extractor): """Extract OBJ from Maya. @@ -34,11 +35,11 @@ class ExtractObj(publish.Extractor): members = instance.data("setMembers") members = cmds.ls(members, - dag=True, - shapes=True, - type=("mesh", "nurbsCurve"), - noIntermediate=True, - long=True) + dag=True, + shapes=True, + type=("mesh", "nurbsCurve"), + noIntermediate=True, + long=True) self.log.info("Members: {0}".format(members)) self.log.info("Instance: {0}".format(instance[:])) @@ -58,17 +59,17 @@ class ExtractObj(publish.Extractor): with lib.maintained_selection(): cmds.select(members, noExpand=True) cmds.file(path, - exportSelected=True, - type='OBJexport', - preserveReferences=True, - force=True) + exportSelected=True, + type='OBJexport', + preserveReferences=True, + force=True) if "representation" not in instance.data: instance.data["representation"] = [] representation = { - 'name':'obj', - 'ext':'obj', + 'name': 'obj', + 'ext': 'obj', 'files': filename, "stagingDir": staging_dir, } From aefb6163ee35e428facb1a8044c33a6cfdc3b372 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Mon, 24 Oct 2022 09:40:21 +0000 Subject: [PATCH 47/55] [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 cda0a98ef3..079822029e 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.14.5-nightly.1" +__version__ = "3.14.5-nightly.2" From 5f3312af04155a0d34d2a7a4ccd23a1e3c8eee1d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 24 Oct 2022 13:38:42 +0200 Subject: [PATCH 48/55] change log update --- CHANGELOG.md | 53 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4f1dcf314..5464c390ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,40 @@ # Changelog -## [3.14.4](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.14.5](https://github.com/pypeclub/OpenPype/tree/HEAD) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.3...HEAD) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.4...HEAD) + +**🚀 Enhancements** + +- Maya: add OBJ extractor to model family [\#4021](https://github.com/pypeclub/OpenPype/pull/4021) +- Publish report viewer tool [\#4010](https://github.com/pypeclub/OpenPype/pull/4010) +- Nuke | Global: adding custom tags representation filtering [\#4009](https://github.com/pypeclub/OpenPype/pull/4009) +- Publisher: Create context has shared data for collection phase [\#3995](https://github.com/pypeclub/OpenPype/pull/3995) +- Resolve: updating to v18 compatibility [\#3986](https://github.com/pypeclub/OpenPype/pull/3986) + +**🐛 Bug fixes** + +- TrayPublisher: Fix missing argument [\#4019](https://github.com/pypeclub/OpenPype/pull/4019) +- General: Fix python 2 compatibility of ffmpeg and oiio tools discovery [\#4011](https://github.com/pypeclub/OpenPype/pull/4011) + +**🔀 Refactored code** + +- Maya: Removed unused imports [\#4008](https://github.com/pypeclub/OpenPype/pull/4008) +- Unreal: Fix import of moved function [\#4007](https://github.com/pypeclub/OpenPype/pull/4007) +- Houdini: Change import of RepairAction [\#4005](https://github.com/pypeclub/OpenPype/pull/4005) +- Nuke/Hiero: Refactor openpype.api imports [\#4000](https://github.com/pypeclub/OpenPype/pull/4000) +- TVPaint: Defined with HostBase [\#3994](https://github.com/pypeclub/OpenPype/pull/3994) + +**Merged pull requests:** + +- Unreal: Remove redundant Creator stub [\#4012](https://github.com/pypeclub/OpenPype/pull/4012) +- Unreal: add `uproject` extension to Unreal project template [\#4004](https://github.com/pypeclub/OpenPype/pull/4004) +- Unreal: fix order of includes [\#4002](https://github.com/pypeclub/OpenPype/pull/4002) +- Fusion: Implement backwards compatibility \(+/- Fusion 17.2\) [\#3958](https://github.com/pypeclub/OpenPype/pull/3958) + +## [3.14.4](https://github.com/pypeclub/OpenPype/tree/3.14.4) (2022-10-19) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.3...3.14.4) **🆕 New features** @@ -27,7 +59,6 @@ - Maya: Moved plugin from global to maya [\#3939](https://github.com/pypeclub/OpenPype/pull/3939) - Publisher: Create dialog is part of main window [\#3936](https://github.com/pypeclub/OpenPype/pull/3936) - Fusion: Implement Alembic and FBX mesh loader [\#3927](https://github.com/pypeclub/OpenPype/pull/3927) -- Maya: Remove hardcoded requirement for maya/ start for image file prefix [\#3873](https://github.com/pypeclub/OpenPype/pull/3873) **🐛 Bug fixes** @@ -71,14 +102,6 @@ **🚀 Enhancements** - Publisher: Enhancement proposals [\#3897](https://github.com/pypeclub/OpenPype/pull/3897) -- Maya: better logging in Maketx [\#3886](https://github.com/pypeclub/OpenPype/pull/3886) -- Photoshop: review can be turned off [\#3885](https://github.com/pypeclub/OpenPype/pull/3885) -- TrayPublisher: added persisting of last selected project [\#3871](https://github.com/pypeclub/OpenPype/pull/3871) -- TrayPublisher: added text filter on project name to Tray Publisher [\#3867](https://github.com/pypeclub/OpenPype/pull/3867) -- Github issues adding `running version` section [\#3864](https://github.com/pypeclub/OpenPype/pull/3864) -- Publisher: Increase size of main window [\#3862](https://github.com/pypeclub/OpenPype/pull/3862) -- Flame: make migratable projects after creation [\#3860](https://github.com/pypeclub/OpenPype/pull/3860) -- Photoshop: synchronize image version with workfile [\#3854](https://github.com/pypeclub/OpenPype/pull/3854) **🐛 Bug fixes** @@ -86,12 +109,6 @@ - Flame: loading multilayer exr to batch/reel is working [\#3901](https://github.com/pypeclub/OpenPype/pull/3901) - Hiero: Fix inventory check on launch [\#3895](https://github.com/pypeclub/OpenPype/pull/3895) - WebPublisher: Fix import after refactor [\#3891](https://github.com/pypeclub/OpenPype/pull/3891) -- TVPaint: Fix renaming of rendered files [\#3882](https://github.com/pypeclub/OpenPype/pull/3882) -- Publisher: Nice checkbox visible in Python 2 [\#3877](https://github.com/pypeclub/OpenPype/pull/3877) -- Settings: Add missing default settings [\#3870](https://github.com/pypeclub/OpenPype/pull/3870) -- General: Copy of workfile does not use 'copy' function but 'copyfile' [\#3869](https://github.com/pypeclub/OpenPype/pull/3869) -- Tray Publisher: skip plugin if otioTimeline is missing [\#3856](https://github.com/pypeclub/OpenPype/pull/3856) -- Flame: retimed attributes are integrated with settings [\#3855](https://github.com/pypeclub/OpenPype/pull/3855) **🔀 Refactored code** @@ -105,8 +122,6 @@ **Merged pull requests:** - Maya: Fix Scene Inventory possibly starting off-screen due to maya preferences [\#3923](https://github.com/pypeclub/OpenPype/pull/3923) -- Maya: RenderSettings set default image format for V-Ray+Redshift to exr [\#3879](https://github.com/pypeclub/OpenPype/pull/3879) -- Remove lockfile during publish [\#3874](https://github.com/pypeclub/OpenPype/pull/3874) ## [3.14.2](https://github.com/pypeclub/OpenPype/tree/3.14.2) (2022-09-12) From 74ebf90046a442f905966a4fdd77f419b208f6e8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 24 Oct 2022 13:51:20 +0200 Subject: [PATCH 49/55] Removed submodule vendor/configs/OpenColorIO-Configs --- vendor/configs/OpenColorIO-Configs | 1 - 1 file changed, 1 deletion(-) delete mode 160000 vendor/configs/OpenColorIO-Configs diff --git a/vendor/configs/OpenColorIO-Configs b/vendor/configs/OpenColorIO-Configs deleted file mode 160000 index 0bb079c08b..0000000000 --- a/vendor/configs/OpenColorIO-Configs +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0bb079c08be410030669cbf5f19ff869b88af953 From 0ccbcbce93f789572bbdb424bdfdf02940d40abb Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 24 Oct 2022 13:54:55 +0200 Subject: [PATCH 50/55] Removed submodule vendor/configs/OpenColorIO-Configs --- vendor/configs/OpenColorIO-Configs | 1 - 1 file changed, 1 deletion(-) delete mode 160000 vendor/configs/OpenColorIO-Configs diff --git a/vendor/configs/OpenColorIO-Configs b/vendor/configs/OpenColorIO-Configs deleted file mode 160000 index 0bb079c08b..0000000000 --- a/vendor/configs/OpenColorIO-Configs +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0bb079c08be410030669cbf5f19ff869b88af953 From 883f035361cee56d4e297271d178be20b40f2557 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 14:08:24 +0200 Subject: [PATCH 51/55] extract review does not crash with old settings overrides --- openpype/plugins/publish/extract_review.py | 41 ++++++++++------------ 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 431ddcc3b4..5e8f85ab86 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -1731,38 +1731,33 @@ class ExtractReview(pyblish.api.InstancePlugin): Returns: list: Containg all output definitions matching entered tags. """ + filtered_outputs = [] repre_c_tags_low = [tag.lower() for tag in (custom_tags or [])] for output_def in outputs: - valid = False tag_filters = output_def.get("filter", {}).get("custom_tags") - if ( - # if any of tag filter is empty, skip - custom_tags and not tag_filters - or not custom_tags and tag_filters - ): - continue - elif not custom_tags and not tag_filters: + if not custom_tags and not tag_filters: + # Definition is valid if both tags are empty valid = True - # lower all filter tags - tag_filters_low = [tag.lower() for tag in tag_filters] + elif not custom_tags or not tag_filters: + # Invalid if one is empty + valid = False - self.log.debug("__ tag_filters: {}".format(tag_filters)) - self.log.debug("__ repre_c_tags_low: {}".format( - repre_c_tags_low)) + else: + # Check if output definition tags are in representation tags + valid = False + # lower all filter tags + tag_filters_low = [tag.lower() for tag in tag_filters] + # check if any repre tag is not in filter tags + for tag in repre_c_tags_low: + if tag in tag_filters_low: + valid = True + break - # check if any repre tag is not in filter tags - for tag in repre_c_tags_low: - if tag in tag_filters_low: - valid = True - break - - if not valid: - continue - - filtered_outputs.append(output_def) + if valid: + filtered_outputs.append(output_def) self.log.debug("__ filtered_outputs: {}".format( [_o["filename_suffix"] for _o in filtered_outputs] From 32d1e572d7f869b596597cdc340f787f16858b92 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Mon, 24 Oct 2022 12:52:58 +0000 Subject: [PATCH 52/55] [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 079822029e..d1ba207aa3 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.14.5-nightly.2" +__version__ = "3.14.5-nightly.3" From 2e818a44041e806bc503594c211ad04bd3b4a29d Mon Sep 17 00:00:00 2001 From: OpenPype Date: Mon, 24 Oct 2022 13:11:22 +0000 Subject: [PATCH 53/55] [Automated] Release --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index d1ba207aa3..b1e4227030 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.14.5-nightly.3" +__version__ = "3.14.5" From 72287eb3d375d6c999c961296ecb1cf3b34a6761 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 25 Oct 2022 10:23:15 +0200 Subject: [PATCH 54/55] validate source streams before otio burnins super is called --- openpype/scripts/otio_burnin.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py index 4c3a5de2ec..3520d8668c 100644 --- a/openpype/scripts/otio_burnin.py +++ b/openpype/scripts/otio_burnin.py @@ -113,11 +113,20 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins): if not ffprobe_data: ffprobe_data = _get_ffprobe_data(source) + # Validate 'streams' before calling super to raise more specific + # error + source_streams = ffprobe_data.get("streams") + if not source_streams: + raise ValueError(( + "Input file \"{}\" does not contain any streams" + " with image/video content." + ).format(source)) + self.ffprobe_data = ffprobe_data self.first_frame = first_frame self.input_args = [] - super().__init__(source, ffprobe_data["streams"]) + super().__init__(source, source_streams) if options_init: self.options_init.update(options_init) From 438922ccb0e7bd12c559e61c07f782c6d3537929 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 25 Oct 2022 10:31:53 +0200 Subject: [PATCH 55/55] ffprobe run as list of args instead of string --- openpype/scripts/otio_burnin.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py index 3520d8668c..7223e8d4de 100644 --- a/openpype/scripts/otio_burnin.py +++ b/openpype/scripts/otio_burnin.py @@ -22,10 +22,6 @@ FFMPEG = ( '"{}"%(input_args)s -i "%(input)s" %(filters)s %(args)s%(output)s' ).format(ffmpeg_path) -FFPROBE = ( - '"{}" -v quiet -print_format json -show_format -show_streams "%(source)s"' -).format(ffprobe_path) - DRAWTEXT = ( "drawtext=fontfile='%(font)s':text=\\'%(text)s\\':" "x=%(x)s:y=%(y)s:fontcolor=%(color)s@%(opacity).1f:fontsize=%(size)d" @@ -48,8 +44,15 @@ def _get_ffprobe_data(source): :param str source: source media file :rtype: [{}, ...] """ - command = FFPROBE % {'source': source} - proc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE) + command = [ + ffprobe_path, + "-v", "quiet", + "-print_format", "json", + "-show_format", + "-show_streams", + source + ] + proc = subprocess.Popen(command, stdout=subprocess.PIPE) out = proc.communicate()[0] if proc.returncode != 0: raise RuntimeError("Failed to run: %s" % command)