From 9b0e2c3e8beccc86fdbfa317080008cfa8e2d9b3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 21 Dec 2021 13:21:31 +0100 Subject: [PATCH 1/8] added new exception PublishXmlValidationError --- openpype/pipeline/publish/__init__.py | 8 ++- openpype/pipeline/publish/lib.py | 57 ++++++++++++++++++++ openpype/pipeline/publish/publish_plugins.py | 25 ++++++++- 3 files changed, 87 insertions(+), 3 deletions(-) diff --git a/openpype/pipeline/publish/__init__.py b/openpype/pipeline/publish/__init__.py index ca958816fe..d106f28617 100644 --- a/openpype/pipeline/publish/__init__.py +++ b/openpype/pipeline/publish/__init__.py @@ -6,7 +6,9 @@ from .publish_plugins import ( from .lib import ( DiscoverResult, - publish_plugins_discover + publish_plugins_discover, + load_help_content_from_plugin, + load_help_content_from_filepath ) @@ -16,5 +18,7 @@ __all__ = ( "OpenPypePyblishPluginMixin", "DiscoverResult", - "publish_plugins_discover" + "publish_plugins_discover", + "load_help_content_from_plugin", + "load_help_content_from_filepath" ) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 0fa712a301..aa30ac22c9 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -1,6 +1,8 @@ import os import sys import types +import inspect +import xml.etree.ElementTree import six import pyblish.plugin @@ -28,6 +30,61 @@ class DiscoverResult: self.plugins[item] = value +class HelpContent: + def __init__(self, title, description, detail=None): + self.title = title + self.description = description + self.detail = detail + + +def load_help_content_from_filepath(filepath): + """Load help content from xml file. + + Xml file may containt errors and warnings. + """ + errors = {} + warnings = {} + output = { + "errors": errors, + "warnings": warnings + } + if not os.path.exists(filepath): + return output + tree = xml.etree.ElementTree.parse(filepath) + root = tree.getroot() + for child in root: + child_id = child.attrib.get("id") + if child_id is None: + continue + + # Make sure ID is string + child_id = str(child_id) + + title = child.find("title").text + description = child.find("description").text + detail_node = child.find("detail") + detail = None + if detail_node: + detail = detail_node.text + if child.tag == "error": + errors[child_id] = HelpContent(title, description, detail) + elif child.tag == "warning": + warnings[child_id] = HelpContent(title, description, detail) + return output + + +def load_help_content_from_plugin(plugin): + cls = plugin + if not inspect.isclass(plugin): + cls = plugin.__class__ + plugin_filepath = inspect.getfile(cls) + plugin_dir = os.path.dirname(plugin_filepath) + basename = os.path.splitext(os.path.basename(plugin_filepath))[0] + filename = basename + ".xml" + filepath = os.path.join(plugin_dir, "help", filename) + return load_help_content_from_filepath(filepath) + + def publish_plugins_discover(paths=None): """Find and return available pyblish plug-ins diff --git a/openpype/pipeline/publish/publish_plugins.py b/openpype/pipeline/publish/publish_plugins.py index b60b9f43a7..9a73d1acc6 100644 --- a/openpype/pipeline/publish/publish_plugins.py +++ b/openpype/pipeline/publish/publish_plugins.py @@ -1,3 +1,6 @@ +from .lib import load_help_content_from_plugin + + class PublishValidationError(Exception): """Validation error happened during publishing. @@ -12,13 +15,33 @@ class PublishValidationError(Exception): description(str): Detailed description of an error. It is possible to use Markdown syntax. """ - def __init__(self, message, title=None, description=None): + def __init__(self, message, title=None, description=None, detail=None): self.message = message self.title = title or "< Missing title >" self.description = description or message + self.detail = detail super(PublishValidationError, self).__init__(message) +class PublishXmlValidationError(PublishValidationError): + def __init__( + self, message, plugin, key=None, *formattings_arg, **formatting_kwargs + ): + if key is None: + key = "main" + result = load_help_content_from_plugin(plugin) + content_obj = result["errors"][key] + description = content_obj.description.format( + *formattings_arg, **formatting_kwargs + ) + detail = content_obj.detail.format( + *formattings_arg, **formatting_kwargs + ) + super(PublishXmlValidationError, self).__init__( + message, content_obj.title, description, detail + ) + + class KnownPublishError(Exception): """Publishing crashed because of known error. From c5b9a0bd05ecf8764b2418cdf1fbba0746f02cc2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 21 Dec 2021 13:22:11 +0100 Subject: [PATCH 2/8] added PublishXmlValidationError to 'pipeline.publish' --- openpype/pipeline/publish/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/pipeline/publish/__init__.py b/openpype/pipeline/publish/__init__.py index d106f28617..228c4d8dcb 100644 --- a/openpype/pipeline/publish/__init__.py +++ b/openpype/pipeline/publish/__init__.py @@ -1,5 +1,6 @@ from .publish_plugins import ( PublishValidationError, + PublishXmlValidationError, KnownPublishError, OpenPypePyblishPluginMixin ) @@ -14,6 +15,7 @@ from .lib import ( __all__ = ( "PublishValidationError", + "PublishXmlValidationError", "KnownPublishError", "OpenPypePyblishPluginMixin", From d5b24781bb53234bf9942ab50d1760113d4f8fbd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 21 Dec 2021 13:25:13 +0100 Subject: [PATCH 3/8] reduced formatting possibilities --- openpype/pipeline/publish/publish_plugins.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/openpype/pipeline/publish/publish_plugins.py b/openpype/pipeline/publish/publish_plugins.py index 9a73d1acc6..78dbaf2ddc 100644 --- a/openpype/pipeline/publish/publish_plugins.py +++ b/openpype/pipeline/publish/publish_plugins.py @@ -25,18 +25,17 @@ class PublishValidationError(Exception): class PublishXmlValidationError(PublishValidationError): def __init__( - self, message, plugin, key=None, *formattings_arg, **formatting_kwargs + self, message, plugin, key=None, formatting_data=None ): if key is None: key = "main" + + if not formatting_data: + formatting_data = {} result = load_help_content_from_plugin(plugin) content_obj = result["errors"][key] - description = content_obj.description.format( - *formattings_arg, **formatting_kwargs - ) - detail = content_obj.detail.format( - *formattings_arg, **formatting_kwargs - ) + description = content_obj.description.format(**formatting_data) + detail = content_obj.detail.format(**formatting_data) super(PublishXmlValidationError, self).__init__( message, content_obj.title, description, detail ) From a05a6785584e7efa5e6f93e9939c5e91b80038b2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 21 Dec 2021 13:50:05 +0100 Subject: [PATCH 4/8] imported PublishXmlValidationError to 'openpype.pipeline' level --- openpype/pipeline/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/pipeline/__init__.py b/openpype/pipeline/__init__.py index e968df4011..79d6ce4d54 100644 --- a/openpype/pipeline/__init__.py +++ b/openpype/pipeline/__init__.py @@ -9,6 +9,7 @@ from .create import ( from .publish import ( PublishValidationError, + PublishXmlValidationError, KnownPublishError, OpenPypePyblishPluginMixin ) @@ -23,6 +24,7 @@ __all__ = ( "CreatedInstance", "PublishValidationError", + "PublishXmlValidationError", "KnownPublishError", "OpenPypePyblishPluginMixin" ) From fe3af18101de12804bc9361c5c121e4c4238aa74 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 21 Dec 2021 13:53:17 +0100 Subject: [PATCH 5/8] swapped plugin and message args order --- openpype/pipeline/publish/publish_plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/publish/publish_plugins.py b/openpype/pipeline/publish/publish_plugins.py index 78dbaf2ddc..48fa2499b8 100644 --- a/openpype/pipeline/publish/publish_plugins.py +++ b/openpype/pipeline/publish/publish_plugins.py @@ -25,7 +25,7 @@ class PublishValidationError(Exception): class PublishXmlValidationError(PublishValidationError): def __init__( - self, message, plugin, key=None, formatting_data=None + self, plugin, message, key=None, formatting_data=None ): if key is None: key = "main" From 5683dda65d2f17ec17efc2d4f749d95324295236 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 21 Dec 2021 14:05:12 +0100 Subject: [PATCH 6/8] do not format empty detail --- openpype/pipeline/publish/publish_plugins.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/publish/publish_plugins.py b/openpype/pipeline/publish/publish_plugins.py index 48fa2499b8..bce64ec709 100644 --- a/openpype/pipeline/publish/publish_plugins.py +++ b/openpype/pipeline/publish/publish_plugins.py @@ -35,7 +35,9 @@ class PublishXmlValidationError(PublishValidationError): result = load_help_content_from_plugin(plugin) content_obj = result["errors"][key] description = content_obj.description.format(**formatting_data) - detail = content_obj.detail.format(**formatting_data) + detail = content_obj.detail + if detail: + detail = detail.format(**formatting_data) super(PublishXmlValidationError, self).__init__( message, content_obj.title, description, detail ) From d6b386583633c0c510f6b54a6b0999f7312f8e75 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 21 Dec 2021 14:06:27 +0100 Subject: [PATCH 7/8] fix detail node bool --- openpype/pipeline/publish/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index aa30ac22c9..f38e73afe2 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -64,7 +64,7 @@ def load_help_content_from_filepath(filepath): description = child.find("description").text detail_node = child.find("detail") detail = None - if detail_node: + if detail_node is not None: detail = detail_node.text if child.tag == "error": errors[child_id] = HelpContent(title, description, detail) From 0c7a12c1f22938704c3c70bdd9ee375a8c5366d5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Dec 2021 15:59:00 +0100 Subject: [PATCH 8/8] Implemented validators for New publisher for Photoshop --- .../publish/help/validate_instance_asset.xml | 25 +++++++++++++++++++ .../plugins/publish/help/validate_naming.xml | 21 ++++++++++++++++ .../publish/help/validate_unique_subsets.xml | 23 +++++++++++++++++ .../publish/validate_instance_asset.py | 12 +++++++-- .../plugins/publish/validate_naming.py | 22 ++++++++++------ .../publish/validate_unique_subsets.py | 23 +++++++++++++---- 6 files changed, 111 insertions(+), 15 deletions(-) create mode 100644 openpype/hosts/photoshop/plugins/publish/help/validate_instance_asset.xml create mode 100644 openpype/hosts/photoshop/plugins/publish/help/validate_naming.xml create mode 100644 openpype/hosts/photoshop/plugins/publish/help/validate_unique_subsets.xml diff --git a/openpype/hosts/photoshop/plugins/publish/help/validate_instance_asset.xml b/openpype/hosts/photoshop/plugins/publish/help/validate_instance_asset.xml new file mode 100644 index 0000000000..3b040e8ea8 --- /dev/null +++ b/openpype/hosts/photoshop/plugins/publish/help/validate_instance_asset.xml @@ -0,0 +1,25 @@ + + + +Subset context + +## Invalid subset context + +Asset name found '{found}' in subsets, expected '{expected}'. + +### How to repair? + +You can fix this with `Repair` button on the right. This will use '{expected}' asset name and overwrite '{found}' asset name in scene metadata. + +After that restart `Publish` with a `Reload button`. + +If this is unwanted, close workfile and open again, that way different asset value would be used for context information. + + +### __Detailed Info__ (optional) + +This might happen if you are reuse old workfile and open it in different context. +(Eg. you created subset "renderCompositingDefault" from asset "Robot' in "your_project_Robot_compositing.aep", now you opened this workfile in a context "Sloth" but existing subset for "Robot" asset stayed in the workfile.) + + + \ No newline at end of file diff --git a/openpype/hosts/photoshop/plugins/publish/help/validate_naming.xml b/openpype/hosts/photoshop/plugins/publish/help/validate_naming.xml new file mode 100644 index 0000000000..21a7370340 --- /dev/null +++ b/openpype/hosts/photoshop/plugins/publish/help/validate_naming.xml @@ -0,0 +1,21 @@ + + + +Invalid name + +## Invalid name of subset + +Name of subset is created from a layer name. Some characters (whitespace, '/' etc.) are not allowed because of publishing (files couldn't be saved on some OSes). + +### How to repair? + +You can fix this with `Repair` button on the right. This will remove invalid characters with safe character ('_' by default) in both subset names and matching group names. + +After that restart `Publish` with a `Reload button`. + +Or you use `Subset Manager` to delete existing subsets, remove created groups, rename layers that are used for their creation and use `Create` option in the Openpype menu to create them again. + +Invalid characters and 'safe character' could be configured in Settings. Ask your OpenPype admin to modify them if necessary. + + + \ No newline at end of file diff --git a/openpype/hosts/photoshop/plugins/publish/help/validate_unique_subsets.xml b/openpype/hosts/photoshop/plugins/publish/help/validate_unique_subsets.xml new file mode 100644 index 0000000000..fa7c76a2dd --- /dev/null +++ b/openpype/hosts/photoshop/plugins/publish/help/validate_unique_subsets.xml @@ -0,0 +1,23 @@ + + + +Subsets duplicated + +## Some subsets are duplicated + +Created subsets must be unique. + +Subsets '{duplicates_str}' are duplicated. + +### How to repair? + +Use `Subset Manager` to delete duplicated subset to have only unique subset names and restart `Publish` with a `Reload button`. + + +### __Detailed Info__ (optional) + +Subset names are created from layer names. Layer names are filtered for characters that would break publishing process when files are created. +This replacement process might result in duplicate names of subsets. + + + \ No newline at end of file diff --git a/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py b/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py index 4dc1972074..8f13cc6b33 100644 --- a/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py +++ b/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py @@ -1,8 +1,10 @@ from avalon import api import pyblish.api -import openpype.api from avalon import photoshop +import openpype.api +from openpype.pipeline import PublishXmlValidationError + class ValidateInstanceAssetRepair(pyblish.api.Action): """Repair the instance asset.""" @@ -56,4 +58,10 @@ class ValidateInstanceAsset(pyblish.api.InstancePlugin): f"If that's not correct value, close workfile and " f"reopen via Workfiles!" ) - assert instance_asset == current_asset, msg + formatting_data = { + "found": instance_asset, + "expected": current_asset + } + if instance_asset != current_asset: + raise PublishXmlValidationError(self, msg, + formatting_data=formatting_data) diff --git a/openpype/hosts/photoshop/plugins/publish/validate_naming.py b/openpype/hosts/photoshop/plugins/publish/validate_naming.py index 1635096f4b..d548992f09 100644 --- a/openpype/hosts/photoshop/plugins/publish/validate_naming.py +++ b/openpype/hosts/photoshop/plugins/publish/validate_naming.py @@ -1,9 +1,11 @@ import re import pyblish.api -import openpype.api from avalon import photoshop +import openpype.api +from openpype.pipeline import PublishXmlValidationError + class ValidateNamingRepair(pyblish.api.Action): """Repair the instance asset.""" @@ -69,14 +71,18 @@ class ValidateNaming(pyblish.api.InstancePlugin): replace_char = '' def process(self, instance): - help_msg = ' Use Repair action (A) in Pyblish to fix it.' - msg = "Name \"{}\" is not allowed.{}".format(instance.data["name"], - help_msg) - assert not re.search(self.invalid_chars, instance.data["name"]), msg + msg = "Name \"{}\" is not allowed.".format(instance.data["name"]) - msg = "Subset \"{}\" is not allowed.{}".format(instance.data["subset"], - help_msg) - assert not re.search(self.invalid_chars, instance.data["subset"]), msg + formatting_data = {"error_msg": msg} + if re.search(self.invalid_chars, instance.data["name"]): + raise PublishXmlValidationError(self, msg, + formatting_data=formatting_data) + + msg = "Subset \"{}\" is not allowed.".format(instance.data["subset"]) + formatting_data = {"error_msg": msg} + if re.search(self.invalid_chars, instance.data["subset"]): + raise PublishXmlValidationError(self, msg, + formatting_data=formatting_data) @classmethod def get_replace_chars(cls): diff --git a/openpype/hosts/photoshop/plugins/publish/validate_unique_subsets.py b/openpype/hosts/photoshop/plugins/publish/validate_unique_subsets.py index 15ae5fbcea..d41fefa971 100644 --- a/openpype/hosts/photoshop/plugins/publish/validate_unique_subsets.py +++ b/openpype/hosts/photoshop/plugins/publish/validate_unique_subsets.py @@ -1,5 +1,8 @@ +import collections + import pyblish.api import openpype.api +from openpype.pipeline import PublishXmlValidationError class ValidateSubsetUniqueness(pyblish.api.ContextPlugin): @@ -19,8 +22,18 @@ class ValidateSubsetUniqueness(pyblish.api.ContextPlugin): if instance.data.get('publish'): subset_names.append(instance.data.get('subset')) - msg = ( - "Instance subset names are not unique. " + - "Remove duplicates via SubsetManager." - ) - assert len(subset_names) == len(set(subset_names)), msg + duplicates = [item + for item, count in + collections.Counter(subset_names).items() + if count > 1] + + if duplicates: + duplicates_str = ",".join(duplicates) + formatting_data = {"duplicates_str": duplicates_str} + msg = ( + "Instance subset names {} are not unique.".format( + duplicates_str) + + " Remove duplicates via SubsetManager." + ) + raise PublishXmlValidationError(self, msg, + formatting_data=formatting_data)