From 6e34c4aed26e20bc8b1af0fc0e368060cf684d20 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 9 Mar 2023 16:13:32 +0100 Subject: [PATCH 01/28] Make sure value-data is int or float --- .../modules/kitsu/utils/update_op_with_zou.py | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 4fa8cf9fdd..a2b560b75b 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -128,8 +128,8 @@ def update_op_assets( if frames_duration: frame_out = frame_in + frames_duration - 1 else: - frame_out = project_doc["data"].get("frameEnd", frame_in) - item_data["frameEnd"] = frame_out + frame_out = int(project_doc["data"].get("frameEnd", frame_in)) + item_data["frameEnd"] = int(frame_out) # Fps, fallback to project's value or default value (25.0) try: fps = float(item_data.get("fps")) @@ -147,33 +147,37 @@ def update_op_assets( item_data["resolutionWidth"] = int(match_res.group(1)) item_data["resolutionHeight"] = int(match_res.group(2)) else: - item_data["resolutionWidth"] = project_doc["data"].get( - "resolutionWidth" + item_data["resolutionWidth"] = int( + project_doc["data"].get("resolutionWidth") ) - item_data["resolutionHeight"] = project_doc["data"].get( - "resolutionHeight" + item_data["resolutionHeight"] = int( + project_doc["data"].get("resolutionHeight") ) # Properties that doesn't fully exist in Kitsu. # Guessing those property names below: # Pixel Aspect Ratio - item_data["pixelAspect"] = item_data.get( - "pixel_aspect", project_doc["data"].get("pixelAspect") + item_data["pixelAspect"] = float( + item_data.get( + "pixel_aspect", project_doc["data"].get("pixelAspect") + ) ) # Handle Start - item_data["handleStart"] = item_data.get( - "handle_start", project_doc["data"].get("handleStart") + item_data["handleStart"] = int( + item_data.get( + "handle_start", project_doc["data"].get("handleStart") + ) ) # Handle End - item_data["handleEnd"] = item_data.get( - "handle_end", project_doc["data"].get("handleEnd") + item_data["handleEnd"] = int( + item_data.get("handle_end", project_doc["data"].get("handleEnd")) ) # Clip In - item_data["clipIn"] = item_data.get( - "clip_in", project_doc["data"].get("clipIn") + item_data["clipIn"] = int( + item_data.get("clip_in", project_doc["data"].get("clipIn")) ) # Clip Out - item_data["clipOut"] = item_data.get( - "clip_out", project_doc["data"].get("clipOut") + item_data["clipOut"] = int( + item_data.get("clip_out", project_doc["data"].get("clipOut")) ) # Tasks From 10da5885cd8611354f0c903622a33124dcda4ac4 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 9 Mar 2023 21:13:42 +0100 Subject: [PATCH 02/28] Get the settings up and running --- .../defaults/project_settings/kitsu.json | 6 ++++- .../projects_schema/schema_project_kitsu.json | 26 ++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/project_settings/kitsu.json b/openpype/settings/defaults/project_settings/kitsu.json index 95b3da04ae..280895f1b9 100644 --- a/openpype/settings/defaults/project_settings/kitsu.json +++ b/openpype/settings/defaults/project_settings/kitsu.json @@ -8,6 +8,10 @@ "IntegrateKitsuNote": { "set_status_note": false, "note_status_shortname": "wfa" + }, + "CustomCommentTemplate": { + "enabled": false, + "comment_template": "{comment}\n\n| | |\n|--|--|\n| version| `{version}` |\n| family | `{family}` |\n| name | `{name}` |" } } -} +} \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json b/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json index fb47670e74..255190c396 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json @@ -54,8 +54,32 @@ "label": "Note shortname" } ] + }, + { + "type": "dict", + "collapsible": true, + "checkbox_key": "enabled", + "key": "CustomCommentTemplate", + "label": "Custom Comment Template", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "label", + "label": "Kitsu supports markdown and here you can create a custom comment template.
You can use data from your instance's anatomy." + }, + { + "key": "comment_template", + "type": "text", + "multiline": true, + "label": "Custom comment" + } + ] } ] } ] -} +} \ No newline at end of file From e2771abfb48d255288a708407b305241f4601737 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Fri, 10 Mar 2023 23:07:13 +0100 Subject: [PATCH 03/28] Move CustomCommentTemplate to within IntegrateKitsuNote --- .../defaults/project_settings/kitsu.json | 10 ++--- .../projects_schema/schema_project_kitsu.json | 44 +++++++++---------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/openpype/settings/defaults/project_settings/kitsu.json b/openpype/settings/defaults/project_settings/kitsu.json index 280895f1b9..f8a98d1a0b 100644 --- a/openpype/settings/defaults/project_settings/kitsu.json +++ b/openpype/settings/defaults/project_settings/kitsu.json @@ -7,11 +7,11 @@ "publish": { "IntegrateKitsuNote": { "set_status_note": false, - "note_status_shortname": "wfa" - }, - "CustomCommentTemplate": { - "enabled": false, - "comment_template": "{comment}\n\n| | |\n|--|--|\n| version| `{version}` |\n| family | `{family}` |\n| name | `{name}` |" + "note_status_shortname": "wfa", + "CustomCommentTemplate": { + "enabled": false, + "comment_template": "{comment}\n\n| | |\n|--|--|\n| version| `{version}` |\n| family | `{family}` |\n| name | `{name}` |" + } } } } \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json b/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json index 255190c396..25f75c8280 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json @@ -52,30 +52,30 @@ "type": "text", "key": "note_status_shortname", "label": "Note shortname" - } - ] - }, - { - "type": "dict", - "collapsible": true, - "checkbox_key": "enabled", - "key": "CustomCommentTemplate", - "label": "Custom Comment Template", - "children": [ - { - "type": "boolean", - "key": "enabled", - "label": "Enabled" }, { - "type": "label", - "label": "Kitsu supports markdown and here you can create a custom comment template.
You can use data from your instance's anatomy." - }, - { - "key": "comment_template", - "type": "text", - "multiline": true, - "label": "Custom comment" + "type": "dict", + "collapsible": true, + "checkbox_key": "enabled", + "key": "CustomCommentTemplate", + "label": "Custom Comment Template", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "label", + "label": "Kitsu supports markdown and here you can create a custom comment template.
You can use data from your instance's anatomy." + }, + { + "key": "comment_template", + "type": "text", + "multiline": true, + "label": "Custom comment" + } + ] } ] } From 7c1789faabea2380ab3001e32876d2747cf55f28 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Fri, 10 Mar 2023 23:07:41 +0100 Subject: [PATCH 04/28] Add custom message functionally --- .../plugins/publish/integrate_kitsu_note.py | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index 6702cbe7aa..b1743ca828 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import gazu import pyblish.api +import re class IntegrateKitsuNote(pyblish.api.ContextPlugin): @@ -9,17 +10,39 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): order = pyblish.api.IntegratorOrder label = "Kitsu Note and Status" families = ["render", "kitsu"] + + # status settings set_status_note = False note_status_shortname = "wfa" + # comment settings + CustomCommentTemplate = {} + CustomCommentTemplate["enabled"] = False + CustomCommentTemplate["comment_template"] = "{comment}" + + def safe_format(self, msg, **kwargs): + def replace_missing(match): + value = kwargs.get(match.group(1), None) + if value is None: + self.log.warning( + "Key `{}` was not found in instance.data " + "and will be rendered as `` in the comment".format( + match.group(1) + ) + ) + return "" + else: + return str(value) + + pattern = r"\{([^}]*)\}" + return re.sub(pattern, replace_missing, msg) + def process(self, context): # Get comment text body publish_comment = context.data.get("comment") if not publish_comment: self.log.info("Comment is not set.") - self.log.debug("Comment is `{}`".format(publish_comment)) - for instance in context: kitsu_task = instance.data.get("kitsu_task") if kitsu_task is None: @@ -42,6 +65,15 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): "changed!".format(self.note_status_shortname) ) + # If custom comment, create it + if self.CustomCommentTemplate["enabled"]: + publish_comment = self.safe_format( + self.CustomCommentTemplate["comment_template"], + **instance.data, + ) + + self.log.debug("Comment is `{}`".format(publish_comment)) + # Add comment to kitsu task task_id = kitsu_task["id"] self.log.debug("Add new note in taks id {}".format(task_id)) From a797d78d451eb953bd5c4a2018712ade3e645c2a Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Fri, 10 Mar 2023 23:19:31 +0100 Subject: [PATCH 05/28] Added a check so only renders gets published to Kitsu --- .../modules/kitsu/plugins/publish/integrate_kitsu_note.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index b1743ca828..44134dec6d 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -44,6 +44,10 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): self.log.info("Comment is not set.") for instance in context: + # Check if instance is a render by checking its family + if "render" not in instance.data["family"]: + continue + kitsu_task = instance.data.get("kitsu_task") if kitsu_task is None: continue From 1093c519add9baae3276e01aa28e1a1d46cab0c8 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Fri, 10 Mar 2023 23:27:47 +0100 Subject: [PATCH 06/28] Change the label to explain more correct what it does --- .../schemas/projects_schema/schema_project_kitsu.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json b/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json index 25f75c8280..1a7747b3dc 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json @@ -46,12 +46,12 @@ { "type": "boolean", "key": "set_status_note", - "label": "Set status on note" + "label": "Set status with note" }, { "type": "text", "key": "note_status_shortname", - "label": "Note shortname" + "label": "Status shortname" }, { "type": "dict", From 78ce629ba63ee31f104255d628dbb6067578bea9 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Tue, 14 Mar 2023 18:14:19 +0100 Subject: [PATCH 07/28] Removed double casting --- openpype/modules/kitsu/utils/update_op_with_zou.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index a2b560b75b..1f38648dfa 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -128,7 +128,7 @@ def update_op_assets( if frames_duration: frame_out = frame_in + frames_duration - 1 else: - frame_out = int(project_doc["data"].get("frameEnd", frame_in)) + frame_out = project_doc["data"].get("frameEnd", frame_in) item_data["frameEnd"] = int(frame_out) # Fps, fallback to project's value or default value (25.0) try: From 383ce7ccb6f20453ef2aa40d81b2af4ae5856eba Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Tue, 14 Mar 2023 18:32:18 +0100 Subject: [PATCH 08/28] Change variable name to snake_case --- .../kitsu/plugins/publish/integrate_kitsu_note.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index 44134dec6d..debbfdf98e 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -16,9 +16,11 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): note_status_shortname = "wfa" # comment settings - CustomCommentTemplate = {} - CustomCommentTemplate["enabled"] = False - CustomCommentTemplate["comment_template"] = "{comment}" + custom_comment_template = {} + custom_comment_template = { + "enabled": False, + "comment_template": "{comment}", + } def safe_format(self, msg, **kwargs): def replace_missing(match): @@ -70,9 +72,9 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): ) # If custom comment, create it - if self.CustomCommentTemplate["enabled"]: + if self.custom_comment_template["enabled"]: publish_comment = self.safe_format( - self.CustomCommentTemplate["comment_template"], + self.custom_comment_template["comment_template"], **instance.data, ) From c53e5b2302b9916360bf3e3a2f38e550596f64d8 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Tue, 14 Mar 2023 18:32:41 +0100 Subject: [PATCH 09/28] Added docstring --- openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index debbfdf98e..a4b229d236 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -23,6 +23,8 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): } def safe_format(self, msg, **kwargs): + """If key is not found in kwargs, set None instead""" + def replace_missing(match): value = kwargs.get(match.group(1), None) if value is None: From a789b135aed5d1dabdbee3b3ae14904a95b34428 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Tue, 14 Mar 2023 18:33:00 +0100 Subject: [PATCH 10/28] Changed the instance check from render to review --- .../modules/kitsu/plugins/publish/integrate_kitsu_note.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index a4b229d236..2e1e656cee 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -48,8 +48,8 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): self.log.info("Comment is not set.") for instance in context: - # Check if instance is a render by checking its family - if "render" not in instance.data["family"]: + # Check if instance is a review by checking its family + if "review" not in instance.data["family"]: continue kitsu_task = instance.data.get("kitsu_task") From b3816ae876ff35d6187e43357d2580e46bafe2ae Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Wed, 15 Mar 2023 17:05:00 +0100 Subject: [PATCH 11/28] Fixed key check towards kwargs --- .../kitsu/plugins/publish/integrate_kitsu_note.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index 2e1e656cee..d4282ab048 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -16,7 +16,6 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): note_status_shortname = "wfa" # comment settings - custom_comment_template = {} custom_comment_template = { "enabled": False, "comment_template": "{comment}", @@ -26,17 +25,15 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): """If key is not found in kwargs, set None instead""" def replace_missing(match): - value = kwargs.get(match.group(1), None) - if value is None: + key = match.group(1) + if key not in kwargs: self.log.warning( "Key `{}` was not found in instance.data " - "and will be rendered as `` in the comment".format( - match.group(1) - ) + "and will be rendered as `` in the comment".format(key) ) return "" else: - return str(value) + return str(kwargs[key]) pattern = r"\{([^}]*)\}" return re.sub(pattern, replace_missing, msg) From a30887bb746ebba4756329cefb36eb4fa77d0db7 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Wed, 15 Mar 2023 17:05:34 +0100 Subject: [PATCH 12/28] Change name from CamelCase to snake_case --- openpype/settings/defaults/project_settings/kitsu.json | 2 +- .../schemas/projects_schema/schema_project_kitsu.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/settings/defaults/project_settings/kitsu.json b/openpype/settings/defaults/project_settings/kitsu.json index f8a98d1a0b..738bd95e38 100644 --- a/openpype/settings/defaults/project_settings/kitsu.json +++ b/openpype/settings/defaults/project_settings/kitsu.json @@ -8,7 +8,7 @@ "IntegrateKitsuNote": { "set_status_note": false, "note_status_shortname": "wfa", - "CustomCommentTemplate": { + "custom_comment_template": { "enabled": false, "comment_template": "{comment}\n\n| | |\n|--|--|\n| version| `{version}` |\n| family | `{family}` |\n| name | `{name}` |" } diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json b/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json index 1a7747b3dc..fc421c20f5 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json @@ -57,7 +57,7 @@ "type": "dict", "collapsible": true, "checkbox_key": "enabled", - "key": "CustomCommentTemplate", + "key": "custom_comment_template", "label": "Custom Comment Template", "children": [ { @@ -67,7 +67,7 @@ }, { "type": "label", - "label": "Kitsu supports markdown and here you can create a custom comment template.
You can use data from your instance's anatomy." + "label": "Kitsu supports markdown and here you can create a custom comment template.
You can use data from your publishing instance's data." }, { "key": "comment_template", From ce22b665b4b1ce1216f84156c09074cb6436545a Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 16 Mar 2023 10:39:47 +0100 Subject: [PATCH 13/28] Fixed comments in code --- .../kitsu/plugins/publish/integrate_kitsu_note.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index d4282ab048..69b456426f 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -22,21 +22,24 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): } def safe_format(self, msg, **kwargs): - """If key is not found in kwargs, set None instead""" + """Pars the msg thourgh a custom format code. + It makes sure non existing keys gets None returned instead of error + """ - def replace_missing(match): + def replace_missing_key(match): + """If key is not found in kwargs, set None instead""" key = match.group(1) if key not in kwargs: self.log.warning( - "Key `{}` was not found in instance.data " - "and will be rendered as `` in the comment".format(key) + "Key '{}' was not found in instance.data " + "and will be rendered as '' in the comment".format(key) ) return "" else: return str(kwargs[key]) pattern = r"\{([^}]*)\}" - return re.sub(pattern, replace_missing, msg) + return re.sub(pattern, replace_missing_key, msg) def process(self, context): # Get comment text body From adc648616f2eebe7780a81de5882bf95e805ddf9 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 16 Mar 2023 10:40:03 +0100 Subject: [PATCH 14/28] Look for review in families instead of family --- openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index 69b456426f..3ad53a1f12 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -49,7 +49,7 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): for instance in context: # Check if instance is a review by checking its family - if "review" not in instance.data["family"]: + if "review" not in instance.data["families"]: continue kitsu_task = instance.data.get("kitsu_task") From f70eac116d1978a337c8adc912413ab5a12bd842 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 16 Mar 2023 17:11:16 +0100 Subject: [PATCH 15/28] Made the format function more logical --- .../plugins/publish/integrate_kitsu_note.py | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index 3ad53a1f12..b0063282d0 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -21,25 +21,27 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): "comment_template": "{comment}", } - def safe_format(self, msg, **kwargs): - """Pars the msg thourgh a custom format code. - It makes sure non existing keys gets None returned instead of error - """ + def format_publish_comment(self, instance): + """Format the instance's publish comment + Formats `instance.data` against the custom template. + """ + def replace_missing_key(match): """If key is not found in kwargs, set None instead""" key = match.group(1) - if key not in kwargs: + if key not in instance.data: self.log.warning( - "Key '{}' was not found in instance.data " - "and will be rendered as '' in the comment".format(key) + "Key "{}" was not found in instance.data " + "and will be rendered as "" in the comment".format(key) ) return "" else: - return str(kwargs[key]) + return str(instance.data[key]) + template = self.custom_comment_template["comment_template"] pattern = r"\{([^}]*)\}" - return re.sub(pattern, replace_missing_key, msg) + return re.sub(pattern, replace_missing_key, template) def process(self, context): # Get comment text body @@ -75,10 +77,7 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): # If custom comment, create it if self.custom_comment_template["enabled"]: - publish_comment = self.safe_format( - self.custom_comment_template["comment_template"], - **instance.data, - ) + publish_comment = self.format_publish_comment(instance) self.log.debug("Comment is `{}`".format(publish_comment)) From 5a7bf785f72d4dacbf9283ee362f447991db2de8 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 16 Mar 2023 17:13:17 +0100 Subject: [PATCH 16/28] Fixed hound comments --- .../modules/kitsu/plugins/publish/integrate_kitsu_note.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index b0063282d0..a5a58c8462 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -26,14 +26,15 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): Formats `instance.data` against the custom template. """ - + def replace_missing_key(match): """If key is not found in kwargs, set None instead""" key = match.group(1) if key not in instance.data: self.log.warning( - "Key "{}" was not found in instance.data " - "and will be rendered as "" in the comment".format(key) + "Key '{}' was not found in instance.data " + "and will be rendered as " + " in the comment".format(key) ) return "" else: From 316c3577c6143aab061677fbf643340ed2d6c1e6 Mon Sep 17 00:00:00 2001 From: Joseff Date: Thu, 16 Mar 2023 20:35:21 +0100 Subject: [PATCH 17/28] Fixed a bug where the QThread in the splash screen could be destroyed before finishing execution --- openpype/hosts/unreal/ue_workers.py | 42 ++++++++++++++++++++++++++--- openpype/widgets/splash_screen.py | 7 +++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/ue_workers.py b/openpype/hosts/unreal/ue_workers.py index 00f83a7d7a..f7bc0e90dc 100644 --- a/openpype/hosts/unreal/ue_workers.py +++ b/openpype/hosts/unreal/ue_workers.py @@ -28,6 +28,15 @@ def parse_prj_progress(line: str, progress_signal: QtCore.Signal(int)) -> int: progress_signal.emit(int(percent_match.group())) +def retrieve_exit_code(line: str): + match = re.search('ExitCode=\d+', line) + if match is not None: + split: list[str] = match.group().split('=') + return int(split[1]) + + return None + + class UEProjectGenerationWorker(QtCore.QObject): finished = QtCore.Signal(str) failed = QtCore.Signal(str) @@ -225,9 +234,30 @@ class UEProjectGenerationWorker(QtCore.QObject): msg = f"Unreal Python not found at {python_path}" self.failed.emit(msg, 1) raise RuntimeError(msg) - subprocess.check_call( - [python_path.as_posix(), "-m", "pip", "install", "pyside2"] - ) + pyside_cmd = [python_path.as_posix(), + "-m", + "pip", + "install", + "pyside2"] + + pyside_install = subprocess.Popen(pyside_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + for line in pyside_install.stdout: + decoded_line: str = line.decode(errors='replace') + print(decoded_line, end='') + self.log.emit(decoded_line) + + pyside_install.stdout.close() + return_code = pyside_install.wait() + + if return_code and return_code != 0: + msg = 'Failed to create the project! ' \ + f'The installation of PySide2 has failed!' + self.failed.emit(msg, return_code) + raise RuntimeError(msg) + self.progress.emit(100) self.finished.emit("Project successfully built!") @@ -274,18 +304,22 @@ class UEPluginInstallWorker(QtCore.QObject): build_proc = subprocess.Popen(build_plugin_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + return_code: int = None for line in build_proc.stdout: decoded_line: str = line.decode(errors='replace') print(decoded_line, end='') self.log.emit(decoded_line) + if return_code is None: + return_code = retrieve_exit_code(decoded_line) parse_comp_progress(decoded_line, self.progress) build_proc.stdout.close() - return_code = build_proc.wait() + build_proc.wait() if return_code and return_code != 0: msg = 'Failed to build plugin' \ f' project! Exited with return code {return_code}' + dir_util.remove_tree(temp_dir.as_posix()) self.failed.emit(msg, return_code) raise RuntimeError(msg) diff --git a/openpype/widgets/splash_screen.py b/openpype/widgets/splash_screen.py index fffe143ea5..6bb0944c46 100644 --- a/openpype/widgets/splash_screen.py +++ b/openpype/widgets/splash_screen.py @@ -80,8 +80,14 @@ class SplashScreen(QtWidgets.QDialog): """ self.thread_return_code = 0 self.q_thread.quit() + + if not self.q_thread.wait(5000): + raise RuntimeError("Failed to quit the QThread! " + "The deadline has been reached! The thread " + "has not finished it's execution!.") self.close() + @QtCore.Slot() def toggle_log(self): if self.is_log_visible: @@ -256,3 +262,4 @@ class SplashScreen(QtWidgets.QDialog): self.close_btn.show() self.thread_return_code = return_code self.q_thread.exit(return_code) + self.q_thread.wait() From c86e8e1d6ea0ae292fe414d5c499451884b230e1 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Fri, 17 Mar 2023 16:22:01 +0100 Subject: [PATCH 18/28] Added empty line in the end VSC removed it automatically before. --- openpype/settings/defaults/project_settings/kitsu.json | 2 +- .../entities/schemas/projects_schema/schema_project_kitsu.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/project_settings/kitsu.json b/openpype/settings/defaults/project_settings/kitsu.json index 738bd95e38..11c138e8e5 100644 --- a/openpype/settings/defaults/project_settings/kitsu.json +++ b/openpype/settings/defaults/project_settings/kitsu.json @@ -14,4 +14,4 @@ } } } -} \ No newline at end of file +} diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json b/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json index fc421c20f5..7ceb979d6f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json @@ -82,4 +82,4 @@ ] } ] -} \ No newline at end of file +} From 8a91e4aaa07dc10ae0d0c373749af103e284e5a7 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Fri, 17 Mar 2023 16:22:28 +0100 Subject: [PATCH 19/28] Fixed comment --- .../modules/kitsu/plugins/publish/integrate_kitsu_note.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index a5a58c8462..67702578c5 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -33,8 +33,8 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): if key not in instance.data: self.log.warning( "Key '{}' was not found in instance.data " - "and will be rendered as " - " in the comment".format(key) + "and will be rendered as an empty string " + "in the comment".format(key) ) return "" else: From 94ba8151e72b6d069d8a72fb74477a6ad0f7739c Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Fri, 17 Mar 2023 16:23:13 +0100 Subject: [PATCH 20/28] Read the comment from the instance instead from the context --- .../kitsu/plugins/publish/integrate_kitsu_note.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index 67702578c5..cf36bbc4fe 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -45,11 +45,6 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): return re.sub(pattern, replace_missing_key, template) def process(self, context): - # Get comment text body - publish_comment = context.data.get("comment") - if not publish_comment: - self.log.info("Comment is not set.") - for instance in context: # Check if instance is a review by checking its family if "review" not in instance.data["families"]: @@ -76,11 +71,15 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): "changed!".format(self.note_status_shortname) ) - # If custom comment, create it + # Get comment text body + publish_comment = instance.data.get("comment") if self.custom_comment_template["enabled"]: publish_comment = self.format_publish_comment(instance) - self.log.debug("Comment is `{}`".format(publish_comment)) + if not publish_comment: + self.log.info("Comment is not set.") + else: + self.log.debug("Comment is `{}`".format(publish_comment)) # Add comment to kitsu task task_id = kitsu_task["id"] From b8ce4b71e2fdd7786d0cb14b5b6c1bf91b5e6851 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Mar 2023 11:20:32 +0100 Subject: [PATCH 21/28] hiero: fix padding in workfile input --- openpype/hosts/hiero/api/plugin.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index 07457db1a4..5ca901caaa 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -146,6 +146,8 @@ class CreatorWidget(QtWidgets.QDialog): return " ".join([str(m.group(0)).capitalize() for m in matches]) def create_row(self, layout, type, text, **kwargs): + value_keys = ["setText", "setCheckState", "setValue", "setChecked"] + # get type attribute from qwidgets attr = getattr(QtWidgets, type) @@ -167,14 +169,27 @@ class CreatorWidget(QtWidgets.QDialog): # assign the created attribute to variable item = getattr(self, attr_name) + + # set attributes to item which are not values for func, val in kwargs.items(): + if func in value_keys: + continue + if getattr(item, func): + log.debug("Setting {} to {}".format(func, val)) func_attr = getattr(item, func) if isinstance(val, tuple): func_attr(*val) else: func_attr(val) + # set values to item + for value_item in value_keys: + if value_item not in kwargs: + continue + if getattr(item, value_item): + getattr(item, value_item)(kwargs[value_item]) + # add to layout layout.addRow(label, item) @@ -276,8 +291,11 @@ class CreatorWidget(QtWidgets.QDialog): elif v["type"] == "QSpinBox": data[k]["value"] = self.create_row( content_layout, "QSpinBox", v["label"], - setValue=v["value"], setMinimum=0, + setValue=v["value"], + setDisplayIntegerBase=10000, + setRange=(0, 99999), setMinimum=0, setMaximum=100000, setToolTip=tool_tip) + return data From 6f16f3e4f402ff37fd6c8264615d856a6cea67f4 Mon Sep 17 00:00:00 2001 From: Sharkitty <81646000+Sharkitty@users.noreply.github.com> Date: Tue, 21 Mar 2023 14:22:23 +0000 Subject: [PATCH 22/28] Enhancement kitsu note with exceptions (#4537) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Enhancement: Allowing kitsu not status exceptions * Update openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py Co-authored-by: Félix David * adding equal/not equal option * Making equal/not equal option available at for every list item * Changed into , renamed into , added documentation * Using upper cases during check, so the new settings aren't case sensitive * Linting little detail * Renaming Equality into Condition, new screenshot with both equal and not equal shown on it * Update website/docs/module_kitsu.md README adjustments Co-authored-by: Félix David * Changes needed to resolve conflict * Changing context into instance where appropriate * Minor change to avoid changing a line that doesn't need to be changed * Turning exceptions into conditions. Making checks positive instead of negative. Changing implementation based on suggestions. --------- Co-authored-by: Félix David --- .../plugins/publish/integrate_kitsu_note.py | 22 +++++++++++---- .../defaults/project_settings/kitsu.json | 1 + .../projects_schema/schema_project_kitsu.json | 26 ++++++++++++++++++ .../assets/integrate_kitsu_note_settings.png | Bin 0 -> 30524 bytes website/docs/module_kitsu.md | 12 ++++++++ 5 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 website/docs/assets/integrate_kitsu_note_settings.png diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index cf36bbc4fe..6fda32d85f 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -14,6 +14,7 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): # status settings set_status_note = False note_status_shortname = "wfa" + status_conditions = list() # comment settings custom_comment_template = { @@ -56,9 +57,19 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): # Get note status, by default uses the task status for the note # if it is not specified in the configuration - note_status = kitsu_task["task_status"]["id"] + shortname = kitsu_task["task_status"]["short_name"].upper() + note_status = kitsu_task["task_status_id"] - if self.set_status_note: + # Check if any status condition is not met + allow_status_change = True + for status_cond in self.status_conditions: + condition = status_cond["condition"] == "equal" + match = status_cond["short_name"].upper() == shortname + if match and not condition or condition and not match: + allow_status_change = False + break + + if self.set_status_note and allow_status_change: kitsu_status = gazu.task.get_task_status_by_short_name( self.note_status_shortname ) @@ -82,10 +93,11 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): self.log.debug("Comment is `{}`".format(publish_comment)) # Add comment to kitsu task - task_id = kitsu_task["id"] - self.log.debug("Add new note in taks id {}".format(task_id)) + self.log.debug( + "Add new note in tasks id {}".format(kitsu_task["id"]) + ) kitsu_comment = gazu.task.add_comment( - task_id, note_status, comment=publish_comment + kitsu_task, note_status, comment=publish_comment ) instance.data["kitsu_comment"] = kitsu_comment diff --git a/openpype/settings/defaults/project_settings/kitsu.json b/openpype/settings/defaults/project_settings/kitsu.json index 11c138e8e5..0638450595 100644 --- a/openpype/settings/defaults/project_settings/kitsu.json +++ b/openpype/settings/defaults/project_settings/kitsu.json @@ -8,6 +8,7 @@ "IntegrateKitsuNote": { "set_status_note": false, "note_status_shortname": "wfa", + "status_conditions": [], "custom_comment_template": { "enabled": false, "comment_template": "{comment}\n\n| | |\n|--|--|\n| version| `{version}` |\n| family | `{family}` |\n| name | `{name}` |" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json b/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json index 7ceb979d6f..ee309f63a7 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json @@ -51,6 +51,32 @@ { "type": "text", "key": "note_status_shortname", + "label": "Note shortname" + }, + { + "type": "list", + "key": "status_conditions", + "label": "Status conditions", + "object_type": { + "type": "dict", + "key": "conditions_dict", + "children": [ + { + "type": "enum", + "key": "condition", + "label": "Condition", + "enum_items": [ + {"equal": "Equal"}, + {"not_equal": "Not equal"} + ] + }, + { + "type": "text", + "key": "short_name", + "label": "Short name" + } + ] + }, "label": "Status shortname" }, { diff --git a/website/docs/assets/integrate_kitsu_note_settings.png b/website/docs/assets/integrate_kitsu_note_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..127e79ab8063dae0408f90a82d6bb4b5e233ed47 GIT binary patch literal 30524 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 literal 0 HcmV?d00001 diff --git a/website/docs/module_kitsu.md b/website/docs/module_kitsu.md index 7be2a42c45..23898fba2e 100644 --- a/website/docs/module_kitsu.md +++ b/website/docs/module_kitsu.md @@ -38,6 +38,18 @@ This functionality cannot deal with all cases and is not error proof, some inter openpype_console module kitsu push-to-zou -l me@domain.ext -p my_password ``` +## Integrate Kitsu Note +Task status can be automatically set during publish thanks to `Integrate Kitsu Note`. This feature can be configured in: + +`Admin -> Studio Settings -> Project Settings -> Kitsu -> Integrate Kitsu Note`. + +There are three settings available: +- `Set status on note` -> turns on and off this integrator. +- `Note shortname` -> Which status shortname should be set automatically (Case sensitive). +- `Status conditions` -> Conditions that need to be met for kitsu status to be changed. You can add as many conditions as you like. There are two fields to each conditions: `Condition` (Whether current status should be equal or not equal to the condition status) and `Short name` (Kitsu Shortname of the condition status). + +![Integrate Kitsu Note project settings](assets/integrate_kitsu_note_settings.png) + ## Q&A ### Is it safe to rename an entity from Kitsu? Absolutely! Entities are linked by their unique IDs between the two databases. From 96e184c8ca7db7d579065e3617149a38c176ebae Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 21 Mar 2023 15:29:19 +0100 Subject: [PATCH 23/28] Integrator: Enforce unique destination transfers, disallow overwrites in queued transfers (#4662) * Fix #4656: Enforce unique destination transfer in Integrator Note that this is per instance - it doesn't validate cross-instance destinations in the context * Use explicit DuplicateDestinationError and raise as KnownPublishError --- openpype/lib/file_transaction.py | 22 +++++++++++++++++++++- openpype/plugins/publish/integrate.py | 16 ++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/openpype/lib/file_transaction.py b/openpype/lib/file_transaction.py index fe70b37cb1..81332a8891 100644 --- a/openpype/lib/file_transaction.py +++ b/openpype/lib/file_transaction.py @@ -13,6 +13,16 @@ else: from shutil import copyfile +class DuplicateDestinationError(ValueError): + """Error raised when transfer destination already exists in queue. + + The error is only raised if `allow_queue_replacements` is False on the + FileTransaction instance and the added file to transfer is of a different + src file than the one already detected in the queue. + + """ + + class FileTransaction(object): """File transaction with rollback options. @@ -44,7 +54,7 @@ class FileTransaction(object): MODE_COPY = 0 MODE_HARDLINK = 1 - def __init__(self, log=None): + def __init__(self, log=None, allow_queue_replacements=False): if log is None: log = logging.getLogger("FileTransaction") @@ -60,6 +70,8 @@ class FileTransaction(object): # Backup file location mapping to original locations self._backup_to_original = {} + self._allow_queue_replacements = allow_queue_replacements + def add(self, src, dst, mode=MODE_COPY): """Add a new file to transfer queue. @@ -82,6 +94,14 @@ class FileTransaction(object): src, dst)) return else: + if not self._allow_queue_replacements: + raise DuplicateDestinationError( + "Transfer to destination is already in queue: " + "{} -> {}. It's not allowed to be replaced by " + "a new transfer from {}".format( + queued_src, dst, src + )) + self.log.warning("File transfer in queue replaced..") self.log.debug( "Removed from queue: {} -> {} replaced by {} -> {}".format( diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 6a0327ec84..760b1a6b37 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -24,7 +24,10 @@ from openpype.client import ( get_version_by_name, ) from openpype.lib import source_hash -from openpype.lib.file_transaction import FileTransaction +from openpype.lib.file_transaction import ( + FileTransaction, + DuplicateDestinationError +) from openpype.pipeline.publish import ( KnownPublishError, get_publish_template_name, @@ -170,9 +173,18 @@ class IntegrateAsset(pyblish.api.InstancePlugin): ).format(instance.data["family"])) return - file_transactions = FileTransaction(log=self.log) + file_transactions = FileTransaction(log=self.log, + # Enforce unique transfers + allow_queue_replacements=False) try: self.register(instance, file_transactions, filtered_repres) + except DuplicateDestinationError as exc: + # Raise DuplicateDestinationError as KnownPublishError + # and rollback the transactions + file_transactions.rollback() + six.reraise(KnownPublishError, + KnownPublishError(exc), + sys.exc_info()[2]) except Exception: # clean destination # todo: preferably we'd also rollback *any* changes to the database From 36ee01997672e78591e80e1d93278dc9db1a2800 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 21 Mar 2023 16:48:50 +0100 Subject: [PATCH 24/28] :rotating_light: some style fixes --- openpype/hosts/unreal/ue_workers.py | 114 +++++++++++++++------------- openpype/widgets/splash_screen.py | 4 +- 2 files changed, 62 insertions(+), 56 deletions(-) diff --git a/openpype/hosts/unreal/ue_workers.py b/openpype/hosts/unreal/ue_workers.py index f7bc0e90dc..d1740124a8 100644 --- a/openpype/hosts/unreal/ue_workers.py +++ b/openpype/hosts/unreal/ue_workers.py @@ -5,33 +5,33 @@ import re import subprocess from distutils import dir_util from pathlib import Path -from typing import List +from typing import List, Union import openpype.hosts.unreal.lib as ue_lib from qtpy import QtCore -def parse_comp_progress(line: str, progress_signal: QtCore.Signal(int)) -> int: - match = re.search('\[[1-9]+/[0-9]+\]', line) +def parse_comp_progress(line: str, progress_signal: QtCore.Signal(int)): + match = re.search(r"\[[1-9]+/[0-9]+]", line) if match is not None: - split: list[str] = match.group().split('/') + split: list[str] = match.group().split("/") curr: float = float(split[0][1:]) total: float = float(split[1][:-1]) progress_signal.emit(int((curr / total) * 100.0)) -def parse_prj_progress(line: str, progress_signal: QtCore.Signal(int)) -> int: - match = re.search('@progress', line) +def parse_prj_progress(line: str, progress_signal: QtCore.Signal(int)): + match = re.search("@progress", line) if match is not None: - percent_match = re.search('\d{1,3}', line) + percent_match = re.search(r"\d{1,3}", line) progress_signal.emit(int(percent_match.group())) def retrieve_exit_code(line: str): - match = re.search('ExitCode=\d+', line) + match = re.search(r"ExitCode=\d+", line) if match is not None: - split: list[str] = match.group().split('=') + split: list[str] = match.group().split("=") return int(split[1]) return None @@ -86,16 +86,19 @@ class UEProjectGenerationWorker(QtCore.QObject): if self.dev_mode: stage_count = 4 - self.stage_begin.emit(f'Generating a new UE project ... 1 out of ' - f'{stage_count}') + self.stage_begin.emit( + ("Generating a new UE project ... 1 out of " + f"{stage_count}")) - commandlet_cmd = [f'{ue_editor_exe.as_posix()}', - f'{cmdlet_project.as_posix()}', - f'-run=OPGenerateProject', - f'{project_file.resolve().as_posix()}'] + commandlet_cmd = [ + f"{ue_editor_exe.as_posix()}", + f"{cmdlet_project.as_posix()}", + "-run=OPGenerateProject", + f"{project_file.resolve().as_posix()}", + ] if self.dev_mode: - commandlet_cmd.append('-GenerateCode') + commandlet_cmd.append("-GenerateCode") gen_process = subprocess.Popen(commandlet_cmd, stdout=subprocess.PIPE, @@ -103,24 +106,27 @@ class UEProjectGenerationWorker(QtCore.QObject): for line in gen_process.stdout: decoded_line = line.decode(errors="replace") - print(decoded_line, end='') + print(decoded_line, end="") self.log.emit(decoded_line) gen_process.stdout.close() return_code = gen_process.wait() if return_code and return_code != 0: - msg = 'Failed to generate ' + self.project_name \ - + f' project! Exited with return code {return_code}' + msg = ( + f"Failed to generate {self.project_name} " + f"project! Exited with return code {return_code}" + ) self.failed.emit(msg, return_code) raise RuntimeError(msg) print("--- Project has been generated successfully.") - self.stage_begin.emit(f'Writing the Engine ID of the build UE ... 1' - f' out of {stage_count}') + self.stage_begin.emit( + (f"Writing the Engine ID of the build UE ... 1" + f" out of {stage_count}")) if not project_file.is_file(): - msg = "Failed to write the Engine ID into .uproject file! Can " \ - "not read!" + msg = ("Failed to write the Engine ID into .uproject file! Can " + "not read!") self.failed.emit(msg) raise RuntimeError(msg) @@ -134,13 +140,14 @@ class UEProjectGenerationWorker(QtCore.QObject): pf.seek(0) json.dump(pf_json, pf, indent=4) pf.truncate() - print(f'--- Engine ID has been written into the project file') + print("--- Engine ID has been written into the project file") self.progress.emit(90) if self.dev_mode: # 2nd stage - self.stage_begin.emit(f'Generating project files ... 2 out of ' - f'{stage_count}') + self.stage_begin.emit( + (f"Generating project files ... 2 out of " + f"{stage_count}")) self.progress.emit(0) ubt_path = ue_lib.get_path_to_ubt(self.engine_path, @@ -163,8 +170,8 @@ class UEProjectGenerationWorker(QtCore.QObject): stdout=subprocess.PIPE, stderr=subprocess.PIPE) for line in gen_proc.stdout: - decoded_line: str = line.decode(errors='replace') - print(decoded_line, end='') + decoded_line: str = line.decode(errors="replace") + print(decoded_line, end="") self.log.emit(decoded_line) parse_prj_progress(decoded_line, self.progress) @@ -172,13 +179,13 @@ class UEProjectGenerationWorker(QtCore.QObject): return_code = gen_proc.wait() if return_code and return_code != 0: - msg = 'Failed to generate project files! ' \ - f'Exited with return code {return_code}' + msg = ("Failed to generate project files! " + f"Exited with return code {return_code}") self.failed.emit(msg, return_code) raise RuntimeError(msg) - self.stage_begin.emit(f'Building the project ... 3 out of ' - f'{stage_count}') + self.stage_begin.emit( + f"Building the project ... 3 out of {stage_count}") self.progress.emit(0) # 3rd stage build_prj_cmd = [ubt_path.as_posix(), @@ -186,16 +193,16 @@ class UEProjectGenerationWorker(QtCore.QObject): arch, "Development", "-TargetType=Editor", - f'-Project={project_file}', - f'{project_file}', + f"-Project={project_file}", + f"{project_file}", "-IgnoreJunk"] build_prj_proc = subprocess.Popen(build_prj_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) for line in build_prj_proc.stdout: - decoded_line: str = line.decode(errors='replace') - print(decoded_line, end='') + decoded_line: str = line.decode(errors="replace") + print(decoded_line, end="") self.log.emit(decoded_line) parse_comp_progress(decoded_line, self.progress) @@ -203,16 +210,17 @@ class UEProjectGenerationWorker(QtCore.QObject): return_code = build_prj_proc.wait() if return_code and return_code != 0: - msg = 'Failed to build project! ' \ - f'Exited with return code {return_code}' + msg = ("Failed to build project! " + f"Exited with return code {return_code}") self.failed.emit(msg, return_code) raise RuntimeError(msg) # ensure we have PySide2 installed in engine self.progress.emit(0) - self.stage_begin.emit(f'Checking PySide2 installation... {stage_count}' - f' out of {stage_count}') + self.stage_begin.emit( + (f"Checking PySide2 installation... {stage_count} " + f" out of {stage_count}")) python_path = None if platform.system().lower() == "windows": python_path = self.engine_path / ("Engine/Binaries/ThirdParty/" @@ -245,16 +253,16 @@ class UEProjectGenerationWorker(QtCore.QObject): stderr=subprocess.PIPE) for line in pyside_install.stdout: - decoded_line: str = line.decode(errors='replace') - print(decoded_line, end='') + decoded_line: str = line.decode(errors="replace") + print(decoded_line, end="") self.log.emit(decoded_line) pyside_install.stdout.close() return_code = pyside_install.wait() if return_code and return_code != 0: - msg = 'Failed to create the project! ' \ - f'The installation of PySide2 has failed!' + msg = ("Failed to create the project! " + "The installation of PySide2 has failed!") self.failed.emit(msg, return_code) raise RuntimeError(msg) @@ -296,18 +304,18 @@ class UEPluginInstallWorker(QtCore.QObject): # in order to successfully build the plugin, # It must be built outside the Engine directory and then moved - build_plugin_cmd: List[str] = [f'{uat_path.as_posix()}', - 'BuildPlugin', - f'-Plugin={uplugin_path.as_posix()}', - f'-Package={temp_dir.as_posix()}'] + build_plugin_cmd: List[str] = [f"{uat_path.as_posix()}", + "BuildPlugin", + f"-Plugin={uplugin_path.as_posix()}", + f"-Package={temp_dir.as_posix()}"] build_proc = subprocess.Popen(build_plugin_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - return_code: int = None + return_code: Union[None, int] = None for line in build_proc.stdout: - decoded_line: str = line.decode(errors='replace') - print(decoded_line, end='') + decoded_line: str = line.decode(errors="replace") + print(decoded_line, end="") self.log.emit(decoded_line) if return_code is None: return_code = retrieve_exit_code(decoded_line) @@ -317,8 +325,8 @@ class UEPluginInstallWorker(QtCore.QObject): build_proc.wait() if return_code and return_code != 0: - msg = 'Failed to build plugin' \ - f' project! Exited with return code {return_code}' + msg = ("Failed to build plugin" + f" project! Exited with return code {return_code}") dir_util.remove_tree(temp_dir.as_posix()) self.failed.emit(msg, return_code) raise RuntimeError(msg) diff --git a/openpype/widgets/splash_screen.py b/openpype/widgets/splash_screen.py index 6bb0944c46..7c1ff72ecd 100644 --- a/openpype/widgets/splash_screen.py +++ b/openpype/widgets/splash_screen.py @@ -49,9 +49,7 @@ class SplashScreen(QtWidgets.QDialog): self.init_ui() def was_proc_successful(self) -> bool: - if self.thread_return_code == 0: - return True - return False + return self.thread_return_code == 0 def start_thread(self, q_thread: QtCore.QThread): """Saves the reference to this thread and starts it. From 25f7478955e75d852095236ce6a9b21c6a5d9a9d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 21 Mar 2023 17:02:31 +0100 Subject: [PATCH 25/28] Remove unused functions (#4671) --- .../maya/plugins/publish/extract_look.py | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index bc506b7feb..447c9a615c 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -30,36 +30,6 @@ def _has_arnold(): return False -def escape_space(path): - """Ensure path is enclosed by quotes to allow paths with spaces""" - return '"{}"'.format(path) if " " in path else path - - -def get_ocio_config_path(profile_folder): - """Path to OpenPype vendorized OCIO. - - Vendorized OCIO config file path is grabbed from the specific path - hierarchy specified below. - - "{OPENPYPE_ROOT}/vendor/OpenColorIO-Configs/{profile_folder}/config.ocio" - Args: - profile_folder (str): Name of folder to grab config file from. - - Returns: - str: Path to vendorized config file. - """ - - return os.path.join( - os.environ["OPENPYPE_ROOT"], - "vendor", - "bin", - "ocioconfig", - "OpenColorIOConfigs", - profile_folder, - "config.ocio" - ) - - def find_paths_by_hash(texture_hash): """Find the texture hash key in the dictionary. From c8c31018d656ea206c42e4a083365e21b4252861 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 21 Mar 2023 18:10:09 +0100 Subject: [PATCH 26/28] General: Filter available applications (#4667) * added project settings for applications * filter applications by new settings in launcher and ftrack * disable filtering by default --- .../event_handlers_user/action_applications.py | 9 +++++++++ .../defaults/project_settings/applications.json | 3 +++ .../schemas/projects_schema/schema_main.json | 4 ++++ .../schema_project_applications.json | 14 ++++++++++++++ openpype/tools/launcher/models.py | 6 ++++++ 5 files changed, 36 insertions(+) create mode 100644 openpype/settings/defaults/project_settings/applications.json create mode 100644 openpype/settings/entities/schemas/projects_schema/schema_project_applications.json diff --git a/openpype/modules/ftrack/event_handlers_user/action_applications.py b/openpype/modules/ftrack/event_handlers_user/action_applications.py index 102f04c956..30399b463d 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_applications.py +++ b/openpype/modules/ftrack/event_handlers_user/action_applications.py @@ -124,6 +124,11 @@ class AppplicationsAction(BaseAction): if not avalon_project_apps: return False + settings = self.get_project_settings_from_event( + event, avalon_project_doc["name"]) + + only_available = settings["applications"]["only_available"] + items = [] for app_name in avalon_project_apps: app = self.application_manager.applications.get(app_name) @@ -133,6 +138,10 @@ class AppplicationsAction(BaseAction): if app.group.name in CUSTOM_LAUNCH_APP_GROUPS: continue + # Skip applications without valid executables + if only_available and not app.find_executable(): + continue + app_icon = app.icon if app_icon and self.icon_url: try: diff --git a/openpype/settings/defaults/project_settings/applications.json b/openpype/settings/defaults/project_settings/applications.json new file mode 100644 index 0000000000..62f3cdfe1b --- /dev/null +++ b/openpype/settings/defaults/project_settings/applications.json @@ -0,0 +1,3 @@ +{ + "only_available": false +} diff --git a/openpype/settings/entities/schemas/projects_schema/schema_main.json b/openpype/settings/entities/schemas/projects_schema/schema_main.json index ebe59c7942..8c1d8ccbdd 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_main.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_main.json @@ -82,6 +82,10 @@ "type": "schema", "name": "schema_project_slack" }, + { + "type": "schema", + "name": "schema_project_applications" + }, { "type": "schema", "name": "schema_project_max" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_applications.json b/openpype/settings/entities/schemas/projects_schema/schema_project_applications.json new file mode 100644 index 0000000000..030ed3ee8a --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_applications.json @@ -0,0 +1,14 @@ +{ + "type": "dict", + "key": "applications", + "label": "Applications", + "collapsible": true, + "is_file": true, + "children": [ + { + "type": "boolean", + "key": "only_available", + "label": "Show only available applications" + } + ] +} diff --git a/openpype/tools/launcher/models.py b/openpype/tools/launcher/models.py index 6c763544a9..3aa6c5d8cb 100644 --- a/openpype/tools/launcher/models.py +++ b/openpype/tools/launcher/models.py @@ -19,6 +19,7 @@ from openpype.lib.applications import ( CUSTOM_LAUNCH_APP_GROUPS, ApplicationManager ) +from openpype.settings import get_project_settings from openpype.pipeline import discover_launcher_actions from openpype.tools.utils.lib import ( DynamicQThread, @@ -94,6 +95,8 @@ class ActionModel(QtGui.QStandardItemModel): if not project_doc: return actions + project_settings = get_project_settings(project_name) + only_available = project_settings["applications"]["only_available"] self.application_manager.refresh() for app_def in project_doc["config"]["apps"]: app_name = app_def["name"] @@ -104,6 +107,9 @@ class ActionModel(QtGui.QStandardItemModel): if app.group.name in CUSTOM_LAUNCH_APP_GROUPS: continue + if only_available and not app.find_executable(): + continue + # Get from app definition, if not there from app in project action = type( "app_{}".format(app_name), From 749bf7d2290ff256da95f634090d5a2c3bff18dd Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 22 Mar 2023 03:26:05 +0000 Subject: [PATCH 27/28] [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 339c17dc70..5b6db12b5e 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.3-nightly.2" +__version__ = "3.15.3-nightly.3" From 28b424bf2fe113857d3513948a6bdcd897fbb859 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 22 Mar 2023 12:13:21 +0100 Subject: [PATCH 28/28] Publisher: Windows reduce command window pop-ups during Publishing (#4672) * Avoid command pop-ups during publishing (tip by @iLLiCiTiT) * No need to pass creationflags because it's already done in `run_subprocess` * Hide command window for `shell=True` calls * Update openpype/lib/execute.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --------- Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/lib/execute.py | 4 ++++ openpype/lib/vendor_bin_utils.py | 12 ++++++++++-- openpype/plugins/publish/extract_burnin.py | 6 +----- openpype/scripts/otio_burnin.py | 6 ------ 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index 759a4db0cb..7a929a0ade 100644 --- a/openpype/lib/execute.py +++ b/openpype/lib/execute.py @@ -102,6 +102,10 @@ def run_subprocess(*args, **kwargs): if ( platform.system().lower() == "windows" and "creationflags" not in kwargs + # shell=True already tries to hide the console window + # and passing these creationflags then shows the window again + # so we avoid it for shell=True cases + and kwargs.get("shell") is not True ): kwargs["creationflags"] = ( subprocess.CREATE_NEW_PROCESS_GROUP diff --git a/openpype/lib/vendor_bin_utils.py b/openpype/lib/vendor_bin_utils.py index 00dd1955fe..e5deb7a6b2 100644 --- a/openpype/lib/vendor_bin_utils.py +++ b/openpype/lib/vendor_bin_utils.py @@ -224,18 +224,26 @@ def find_tool_in_custom_paths(paths, tool, validation_func=None): def _check_args_returncode(args): try: - # Python 2 compatibility where DEVNULL is not available + kwargs = {} + if platform.system().lower() == "windows": + kwargs["creationflags"] = ( + subprocess.CREATE_NEW_PROCESS_GROUP + | getattr(subprocess, "DETACHED_PROCESS", 0) + | getattr(subprocess, "CREATE_NO_WINDOW", 0) + ) + if hasattr(subprocess, "DEVNULL"): proc = subprocess.Popen( args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + **kwargs ) proc.wait() else: with open(os.devnull, "w") as devnull: proc = subprocess.Popen( - args, stdout=devnull, stderr=devnull, + args, stdout=devnull, stderr=devnull, **kwargs ) proc.wait() diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index f113e61bb0..38ec08e8d9 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -16,9 +16,7 @@ from openpype.lib import ( get_transcode_temp_directory, convert_input_paths_for_ffmpeg, - should_convert_for_ffmpeg, - - CREATE_NO_WINDOW + should_convert_for_ffmpeg ) from openpype.lib.profiles_filtering import filter_profiles @@ -338,8 +336,6 @@ class ExtractBurnin(publish.Extractor): "logger": self.log, "env": {} } - if platform.system().lower() == "windows": - process_kwargs["creationflags"] = CREATE_NO_WINDOW run_openpype_process(*args, **process_kwargs) # Remove the temporary json diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py index cb4646c099..ef449f4f74 100644 --- a/openpype/scripts/otio_burnin.py +++ b/openpype/scripts/otio_burnin.py @@ -345,12 +345,6 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins): "stderr": subprocess.PIPE, "shell": True, } - if platform.system().lower() == "windows": - kwargs["creationflags"] = ( - subprocess.CREATE_NEW_PROCESS_GROUP - | getattr(subprocess, "DETACHED_PROCESS", 0) - | getattr(subprocess, "CREATE_NO_WINDOW", 0) - ) proc = subprocess.Popen(command, **kwargs) _stdout, _stderr = proc.communicate()