From 6d3d52c05c630b9f559ff9a86f0e8cc574007fc7 Mon Sep 17 00:00:00 2001 From: Kaa Maurice Date: Thu, 11 Aug 2022 14:46:43 +0200 Subject: [PATCH 01/39] Blender Validators settings schemas and defaults --- .../defaults/project_settings/blender.json | 62 +++++++++- .../schema_project_blender.json | 4 + .../schemas/schema_blender_publish.json | 114 ++++++++++++++++++ 3 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index a7262dcb5d..a596d13865 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -2,5 +2,65 @@ "workfile_builder": { "create_first_version": false, "custom_templates": [] + }, + "publish": { + "ValidateCameraZeroKeyframe": { + "enabled": true, + "optional": true, + "active": true + }, + "ValidateMeshHasUvs": { + "enabled": true, + "optional": true, + "active": true + }, + "ValidateTransformZero": { + "enabled": true, + "optional": false, + "active": true + }, + "ExtractBlend": { + "enabled": true, + "optional": true, + "active": true, + "pack_images": true, + "families": [ + "model", + "camera", + "rig", + "action", + "layout" + ] + }, + "ExtractBlendAnimation": { + "enabled": true, + "optional": true, + "active": true + }, + "ExtractCamera": { + "enabled": true, + "optional": true, + "active": true + }, + "ExtractFBX": { + "enabled": true, + "optional": true, + "active": false + }, + "ExtractAnimationFBX": { + "enabled": true, + "optional": true, + "active": false + }, + "ExtractABC": { + "enabled": true, + "optional": true, + "active": false + }, + "ExtractLayout": { + "enabled": true, + "optional": true, + "active": false + } } -} \ No newline at end of file +} diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index af09329a03..4c72ebda2f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -12,6 +12,10 @@ "workfile_builder/builder_on_start", "workfile_builder/profiles" ] + }, + { + "type": "schema", + "name": "schema_blender_publish" } ] } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json new file mode 100644 index 0000000000..6111ae4a74 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json @@ -0,0 +1,114 @@ +{ + "type": "dict", + "collapsible": true, + "key": "publish", + "label": "Publish plugins", + "children": [ + { + "type": "label", + "label": "Validators" + }, + { + "type": "schema_template", + "name": "template_publish_plugin", + "template_data": [ + { + "key": "ValidateCameraZeroKeyframe", + "label": "Validate Camera Zero Keyframe" + } + ] + }, + + { + "type": "collapsible-wrap", + "label": "Model", + "children": [ + { + "type": "label", + "label": "Validators" + }, + { + "type": "schema_template", + "name": "template_publish_plugin", + "template_data": [ + { + "key": "ValidateMeshHasUvs", + "label": "Validate Mesh Has UVs" + }, + { + "key": "ValidateTransformZero", + "label": "Validate Transform Zero" + } + ] + } + ] + }, + { + "type": "splitter" + }, + { + "type": "label", + "label": "Extractors" + }, + { + "type": "dict", + "collapsible": true, + "key": "ExtractBlend", + "label": "Extract Blend", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" + }, + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "task-types-enum" + } + ] + }, + { + "type": "schema_template", + "name": "template_publish_plugin", + "template_data": [ + { + "key": "ExtractFBX", + "label": "Extract FBX (model and rig)", + }, + { + "key": "ExtractABC", + "label": "Extract ABC (model and pointcache)" + }, + { + "key": "ExtractBlendAnimation", + "label": "Extract Animation as Blend" + }, + { + "key": "ExtractAnimationFBX", + "label": "Extract Animation as FBX" + }, + { + "key": "ExtractCamera", + "label": "Extract FBX Camera as FBX" + }, + { + "key": "ExtractLayout", + "label": "Extract Layout as JSON" + } + ] + } + ] +} From 6cba799c460dc3c9745bf68fc6edcd3c6ab345e0 Mon Sep 17 00:00:00 2001 From: Kaa Maurice Date: Thu, 11 Aug 2022 15:39:03 +0200 Subject: [PATCH 02/39] refactor blender Validators --- .../publish/validate_camera_zero_keyframe.py | 19 ++++++++++-------- .../plugins/publish/validate_mesh_has_uv.py | 17 ++++++++-------- .../validate_mesh_no_negative_scale.py | 19 ++++++++---------- .../publish/validate_no_colons_in_name.py | 15 ++++++++------ .../plugins/publish/validate_object_mode.py | 20 +++++++++---------- .../publish/validate_transform_zero.py | 19 ++++++++++++------ .../defaults/project_settings/blender.json | 6 +++++- .../schemas/schema_blender_publish.json | 9 ++------- 8 files changed, 66 insertions(+), 58 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py b/openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py index 39b9b67511..bfd7224b80 100644 --- a/openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py +++ b/openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py @@ -1,9 +1,11 @@ from typing import List import mathutils +import bpy import pyblish.api -import openpype.hosts.blender.api.action +from openpype.api import ValidateContentsOrder +from openpype.hosts.blender.api.action import SelectInvalidAction class ValidateCameraZeroKeyframe(pyblish.api.InstancePlugin): @@ -14,21 +16,21 @@ class ValidateCameraZeroKeyframe(pyblish.api.InstancePlugin): in Unreal and Blender. """ - order = openpype.api.ValidateContentsOrder + order = ValidateContentsOrder hosts = ["blender"] families = ["camera"] category = "geometry" version = (0, 1, 0) label = "Zero Keyframe" - actions = [openpype.hosts.blender.api.action.SelectInvalidAction] + actions = [SelectInvalidAction] _identity = mathutils.Matrix() - @classmethod - def get_invalid(cls, instance) -> List: + @staticmethod + def get_invalid(instance) -> List: invalid = [] - for obj in [obj for obj in instance]: - if obj.type == "CAMERA": + for obj in set(instance): + if isinstance(obj, bpy.types.Object) and obj.type == "CAMERA": if obj.animation_data and obj.animation_data.action: action = obj.animation_data.action frames_set = set() @@ -45,4 +47,5 @@ class ValidateCameraZeroKeyframe(pyblish.api.InstancePlugin): invalid = self.get_invalid(instance) if invalid: raise RuntimeError( - f"Object found in instance is not in Object Mode: {invalid}") + f"Camera must have a keyframe at frame 0: {invalid}" + ) diff --git a/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py b/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py index 1c73476fc8..d83ead78cc 100644 --- a/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py +++ b/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py @@ -3,18 +3,19 @@ from typing import List import bpy import pyblish.api -import openpype.hosts.blender.api.action +from openpype.api import ValidateContentsOrder +from openpype.hosts.blender.api.action import SelectInvalidAction class ValidateMeshHasUvs(pyblish.api.InstancePlugin): """Validate that the current mesh has UV's.""" - order = pyblish.api.ValidatorOrder + order = ValidateContentsOrder hosts = ["blender"] families = ["model"] category = "geometry" label = "Mesh Has UV's" - actions = [openpype.hosts.blender.api.action.SelectInvalidAction] + actions = [SelectInvalidAction] optional = True @staticmethod @@ -33,20 +34,20 @@ class ValidateMeshHasUvs(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance) -> List: invalid = [] - # TODO (jasper): only check objects in the collection that will be published? - for obj in [ - obj for obj in instance]: + for obj in set(instance): try: if obj.type == 'MESH': # Make sure we are in object mode. bpy.ops.object.mode_set(mode='OBJECT') if not cls.has_uvs(obj): invalid.append(obj) - except: + except RuntimeError: continue return invalid def process(self, instance): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError(f"Meshes found in instance without valid UV's: {invalid}") + raise RuntimeError( + f"Meshes found in instance without valid UV's: {invalid}" + ) diff --git a/openpype/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py b/openpype/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py index 00159a2d36..b7687009d7 100644 --- a/openpype/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py +++ b/openpype/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py @@ -3,29 +3,26 @@ from typing import List import bpy import pyblish.api -import openpype.hosts.blender.api.action +from openpype.api import ValidateContentsOrder +from openpype.hosts.blender.api.action import SelectInvalidAction class ValidateMeshNoNegativeScale(pyblish.api.Validator): """Ensure that meshes don't have a negative scale.""" - order = pyblish.api.ValidatorOrder + order = ValidateContentsOrder hosts = ["blender"] families = ["model"] label = "Mesh No Negative Scale" - actions = [openpype.hosts.blender.api.action.SelectInvalidAction] + actions = [SelectInvalidAction] @staticmethod def get_invalid(instance) -> List: invalid = [] - # TODO (jasper): only check objects in the collection that will be published? - for obj in [ - obj for obj in bpy.data.objects if obj.type == 'MESH' - ]: - if any(v < 0 for v in obj.scale): - invalid.append(obj) - - return invalid + for obj in set(instance): + if isinstance(obj, bpy.types.Object) and obj.type == 'MESH': + if any(v < 0 for v in obj.scale): + invalid.append(obj) def process(self, instance): invalid = self.get_invalid(instance) diff --git a/openpype/hosts/blender/plugins/publish/validate_no_colons_in_name.py b/openpype/hosts/blender/plugins/publish/validate_no_colons_in_name.py index 261ff864d5..cb8fa0f34a 100644 --- a/openpype/hosts/blender/plugins/publish/validate_no_colons_in_name.py +++ b/openpype/hosts/blender/plugins/publish/validate_no_colons_in_name.py @@ -1,7 +1,10 @@ from typing import List +import bpy + import pyblish.api -import openpype.hosts.blender.api.action +from openpype.api import ValidateContentsOrder +from openpype.hosts.blender.api.action import SelectInvalidAction class ValidateNoColonsInName(pyblish.api.InstancePlugin): @@ -12,20 +15,20 @@ class ValidateNoColonsInName(pyblish.api.InstancePlugin): """ - order = openpype.api.ValidateContentsOrder + order = ValidateContentsOrder hosts = ["blender"] families = ["model", "rig"] version = (0, 1, 0) label = "No Colons in names" - actions = [openpype.hosts.blender.api.action.SelectInvalidAction] + actions = [SelectInvalidAction] - @classmethod + @staticmethod def get_invalid(cls, instance) -> List: invalid = [] - for obj in [obj for obj in instance]: + for obj in set(instance): if ':' in obj.name: invalid.append(obj) - if obj.type == 'ARMATURE': + if isinstance(obj, bpy.types.Object) and obj.type == 'ARMATURE': for bone in obj.data.bones: if ':' in bone.name: invalid.append(obj) diff --git a/openpype/hosts/blender/plugins/publish/validate_object_mode.py b/openpype/hosts/blender/plugins/publish/validate_object_mode.py index 90ef0b7c41..36b7a59eb2 100644 --- a/openpype/hosts/blender/plugins/publish/validate_object_mode.py +++ b/openpype/hosts/blender/plugins/publish/validate_object_mode.py @@ -1,7 +1,9 @@ from typing import List +import bpy + import pyblish.api -import openpype.hosts.blender.api.action +from openpype.hosts.blender.api.action import SelectInvalidAction class ValidateObjectIsInObjectMode(pyblish.api.InstancePlugin): @@ -12,20 +14,16 @@ class ValidateObjectIsInObjectMode(pyblish.api.InstancePlugin): families = ["model", "rig", "layout"] category = "geometry" label = "Validate Object Mode" - actions = [openpype.hosts.blender.api.action.SelectInvalidAction] + actions = [SelectInvalidAction] optional = False - @classmethod + @staticmethod def get_invalid(cls, instance) -> List: invalid = [] - for obj in [obj for obj in instance]: - try: - if obj.type == 'MESH' or obj.type == 'ARMATURE': - # Check if the object is in object mode. - if not obj.mode == 'OBJECT': - invalid.append(obj) - except Exception: - continue + for obj in set(instance): + if isinstance(obj, bpy.types.Object): + if not obj.mode == 'OBJECT': + invalid.append(obj) return invalid def process(self, instance): diff --git a/openpype/hosts/blender/plugins/publish/validate_transform_zero.py b/openpype/hosts/blender/plugins/publish/validate_transform_zero.py index 7456dbc423..737c43cc3f 100644 --- a/openpype/hosts/blender/plugins/publish/validate_transform_zero.py +++ b/openpype/hosts/blender/plugins/publish/validate_transform_zero.py @@ -1,9 +1,11 @@ from typing import List import mathutils +import bpy import pyblish.api -import openpype.hosts.blender.api.action +from openpype.api import ValidateContentsOrder +from openpype.hosts.blender.api.action import SelectInvalidAction class ValidateTransformZero(pyblish.api.InstancePlugin): @@ -15,21 +17,24 @@ class ValidateTransformZero(pyblish.api.InstancePlugin): """ - order = openpype.api.ValidateContentsOrder + order = ValidateContentsOrder hosts = ["blender"] families = ["model"] category = "geometry" version = (0, 1, 0) label = "Transform Zero" - actions = [openpype.hosts.blender.api.action.SelectInvalidAction] + actions = [SelectInvalidAction] _identity = mathutils.Matrix() @classmethod def get_invalid(cls, instance) -> List: invalid = [] - for obj in [obj for obj in instance]: - if obj.matrix_basis != cls._identity: + for obj in set(instance): + if ( + isinstance(obj, bpy.types.Object) + and obj.matrix_basis != cls._identity + ): invalid.append(obj) return invalid @@ -37,4 +42,6 @@ class ValidateTransformZero(pyblish.api.InstancePlugin): invalid = self.get_invalid(instance) if invalid: raise RuntimeError( - f"Object found in instance is not in Object Mode: {invalid}") + "Object found in instance has not" + f" transform to zero: {invalid}" + ) diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index a596d13865..2720e0286d 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -14,6 +14,11 @@ "optional": true, "active": true }, + "ValidateMeshNoNegativeScale": { + "enabled": true, + "optional": false, + "active": true + }, "ValidateTransformZero": { "enabled": true, "optional": false, @@ -23,7 +28,6 @@ "enabled": true, "optional": true, "active": true, - "pack_images": true, "families": [ "model", "camera", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json index 6111ae4a74..4dab373efd 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json @@ -18,15 +18,10 @@ } ] }, - { "type": "collapsible-wrap", "label": "Model", "children": [ - { - "type": "label", - "label": "Validators" - }, { "type": "schema_template", "name": "template_publish_plugin", @@ -76,7 +71,7 @@ "key": "families", "label": "Families", "type": "list", - "object_type": "task-types-enum" + "object_type": "text" } ] }, @@ -86,7 +81,7 @@ "template_data": [ { "key": "ExtractFBX", - "label": "Extract FBX (model and rig)", + "label": "Extract FBX (model and rig)" }, { "key": "ExtractABC", From b8376b4a42a4ff333e6305b88ee94b3b13e6fb0c Mon Sep 17 00:00:00 2001 From: Kaa Maurice Date: Thu, 11 Aug 2022 15:44:54 +0200 Subject: [PATCH 03/39] added validator no negative scale to the schema --- .../projects_schema/schemas/schema_blender_publish.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json index 4dab373efd..58428ad60a 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json @@ -30,6 +30,10 @@ "key": "ValidateMeshHasUvs", "label": "Validate Mesh Has UVs" }, + { + "key": "ValidateMeshNoNegativeScale", + "label": "Validate Mesh No Negative Scale" + }, { "key": "ValidateTransformZero", "label": "Validate Transform Zero" From 0f90ca4a7a8a856da60e345ee86a3d7f3758c23a Mon Sep 17 00:00:00 2001 From: Kaa Maurice Date: Thu, 11 Aug 2022 16:09:16 +0200 Subject: [PATCH 04/39] fix and clean Blender validators attrs --- .../blender/plugins/publish/validate_camera_zero_keyframe.py | 2 -- openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py | 2 +- .../blender/plugins/publish/validate_mesh_no_negative_scale.py | 1 + .../hosts/blender/plugins/publish/validate_no_colons_in_name.py | 1 + openpype/hosts/blender/plugins/publish/validate_object_mode.py | 2 +- .../hosts/blender/plugins/publish/validate_transform_zero.py | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py b/openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py index bfd7224b80..ea45318219 100644 --- a/openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py +++ b/openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py @@ -24,8 +24,6 @@ class ValidateCameraZeroKeyframe(pyblish.api.InstancePlugin): label = "Zero Keyframe" actions = [SelectInvalidAction] - _identity = mathutils.Matrix() - @staticmethod def get_invalid(instance) -> List: invalid = [] diff --git a/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py b/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py index d83ead78cc..4995eedad4 100644 --- a/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py +++ b/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py @@ -13,7 +13,7 @@ class ValidateMeshHasUvs(pyblish.api.InstancePlugin): order = ValidateContentsOrder hosts = ["blender"] families = ["model"] - category = "geometry" + category = "uv" label = "Mesh Has UV's" actions = [SelectInvalidAction] optional = True diff --git a/openpype/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py b/openpype/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py index b7687009d7..449e711663 100644 --- a/openpype/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py +++ b/openpype/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py @@ -13,6 +13,7 @@ class ValidateMeshNoNegativeScale(pyblish.api.Validator): order = ValidateContentsOrder hosts = ["blender"] families = ["model"] + category = "geometry" label = "Mesh No Negative Scale" actions = [SelectInvalidAction] diff --git a/openpype/hosts/blender/plugins/publish/validate_no_colons_in_name.py b/openpype/hosts/blender/plugins/publish/validate_no_colons_in_name.py index cb8fa0f34a..f1889e5837 100644 --- a/openpype/hosts/blender/plugins/publish/validate_no_colons_in_name.py +++ b/openpype/hosts/blender/plugins/publish/validate_no_colons_in_name.py @@ -18,6 +18,7 @@ class ValidateNoColonsInName(pyblish.api.InstancePlugin): order = ValidateContentsOrder hosts = ["blender"] families = ["model", "rig"] + category = "cleanup" version = (0, 1, 0) label = "No Colons in names" actions = [SelectInvalidAction] diff --git a/openpype/hosts/blender/plugins/publish/validate_object_mode.py b/openpype/hosts/blender/plugins/publish/validate_object_mode.py index 36b7a59eb2..65b0bf7655 100644 --- a/openpype/hosts/blender/plugins/publish/validate_object_mode.py +++ b/openpype/hosts/blender/plugins/publish/validate_object_mode.py @@ -12,7 +12,7 @@ class ValidateObjectIsInObjectMode(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder - 0.01 hosts = ["blender"] families = ["model", "rig", "layout"] - category = "geometry" + category = "cleanup" label = "Validate Object Mode" actions = [SelectInvalidAction] optional = False diff --git a/openpype/hosts/blender/plugins/publish/validate_transform_zero.py b/openpype/hosts/blender/plugins/publish/validate_transform_zero.py index 737c43cc3f..7443e3c64e 100644 --- a/openpype/hosts/blender/plugins/publish/validate_transform_zero.py +++ b/openpype/hosts/blender/plugins/publish/validate_transform_zero.py @@ -20,7 +20,7 @@ class ValidateTransformZero(pyblish.api.InstancePlugin): order = ValidateContentsOrder hosts = ["blender"] families = ["model"] - category = "geometry" + category = "cleanup" version = (0, 1, 0) label = "Transform Zero" actions = [SelectInvalidAction] From b1f29676227726f4367c4f6aa4de9defd305d41e Mon Sep 17 00:00:00 2001 From: Kaa Maurice Date: Mon, 15 Aug 2022 11:00:10 +0200 Subject: [PATCH 05/39] validate mesh has UV safe code --- .../hosts/blender/plugins/publish/validate_mesh_has_uv.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py b/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py index 4995eedad4..d87b4ff1ef 100644 --- a/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py +++ b/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py @@ -36,9 +36,10 @@ class ValidateMeshHasUvs(pyblish.api.InstancePlugin): invalid = [] for obj in set(instance): try: - if obj.type == 'MESH': - # Make sure we are in object mode. - bpy.ops.object.mode_set(mode='OBJECT') + if isinstance(obj, bpy.types.Object) and obj.type == 'MESH': + if obj.mode != 'OBJECT': + # Make sure we are in object mode. + bpy.ops.object.mode_set(mode='OBJECT') if not cls.has_uvs(obj): invalid.append(obj) except RuntimeError: From 9cfa2e12e388be7f6910d97a54c12be5aa452e07 Mon Sep 17 00:00:00 2001 From: Kaa Maurice Date: Mon, 15 Aug 2022 14:20:11 +0200 Subject: [PATCH 06/39] reviews fix and clean - bugfix with staticmethod --- .../publish/validate_camera_zero_keyframe.py | 12 ++++------ .../plugins/publish/validate_mesh_has_uv.py | 24 +++++++------------ .../validate_mesh_no_negative_scale.py | 10 ++++---- .../publish/validate_no_colons_in_name.py | 16 ++++++------- .../plugins/publish/validate_object_mode.py | 17 +++++++------ .../publish/validate_transform_zero.py | 11 ++++----- 6 files changed, 40 insertions(+), 50 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py b/openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py index ea45318219..5ba4808875 100644 --- a/openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py +++ b/openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py @@ -1,11 +1,10 @@ from typing import List -import mathutils import bpy import pyblish.api -from openpype.api import ValidateContentsOrder -from openpype.hosts.blender.api.action import SelectInvalidAction +import openpype.api +import openpype.hosts.blender.api.action class ValidateCameraZeroKeyframe(pyblish.api.InstancePlugin): @@ -16,18 +15,17 @@ class ValidateCameraZeroKeyframe(pyblish.api.InstancePlugin): in Unreal and Blender. """ - order = ValidateContentsOrder + order = openpype.api.ValidateContentsOrder hosts = ["blender"] families = ["camera"] - category = "geometry" version = (0, 1, 0) label = "Zero Keyframe" - actions = [SelectInvalidAction] + actions = [openpype.hosts.blender.api.action.SelectInvalidAction] @staticmethod def get_invalid(instance) -> List: invalid = [] - for obj in set(instance): + for obj in instance: if isinstance(obj, bpy.types.Object) and obj.type == "CAMERA": if obj.animation_data and obj.animation_data.action: action = obj.animation_data.action diff --git a/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py b/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py index d87b4ff1ef..1a52b3f851 100644 --- a/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py +++ b/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py @@ -3,19 +3,19 @@ from typing import List import bpy import pyblish.api -from openpype.api import ValidateContentsOrder -from openpype.hosts.blender.api.action import SelectInvalidAction +import openpype.api +import openpype.hosts.blender.api.action class ValidateMeshHasUvs(pyblish.api.InstancePlugin): """Validate that the current mesh has UV's.""" - order = ValidateContentsOrder + order = openpype.api.ValidateContentsOrder hosts = ["blender"] families = ["model"] - category = "uv" + category = "geometry" label = "Mesh Has UV's" - actions = [SelectInvalidAction] + actions = [openpype.hosts.blender.api.action.SelectInvalidAction] optional = True @staticmethod @@ -34,16 +34,10 @@ class ValidateMeshHasUvs(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance) -> List: invalid = [] - for obj in set(instance): - try: - if isinstance(obj, bpy.types.Object) and obj.type == 'MESH': - if obj.mode != 'OBJECT': - # Make sure we are in object mode. - bpy.ops.object.mode_set(mode='OBJECT') - if not cls.has_uvs(obj): - invalid.append(obj) - except RuntimeError: - continue + for obj in instance: + if isinstance(obj, bpy.types.Object) and obj.type == 'MESH': + if not cls.has_uvs(obj): + invalid.append(obj) return invalid def process(self, instance): diff --git a/openpype/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py b/openpype/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py index 449e711663..3c5c7c11eb 100644 --- a/openpype/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py +++ b/openpype/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py @@ -3,24 +3,24 @@ from typing import List import bpy import pyblish.api -from openpype.api import ValidateContentsOrder -from openpype.hosts.blender.api.action import SelectInvalidAction +import openpype.api +import openpype.hosts.blender.api.action class ValidateMeshNoNegativeScale(pyblish.api.Validator): """Ensure that meshes don't have a negative scale.""" - order = ValidateContentsOrder + order = openpype.api.ValidateContentsOrder hosts = ["blender"] families = ["model"] category = "geometry" label = "Mesh No Negative Scale" - actions = [SelectInvalidAction] + actions = [openpype.hosts.blender.api.action.SelectInvalidAction] @staticmethod def get_invalid(instance) -> List: invalid = [] - for obj in set(instance): + for obj in instance: if isinstance(obj, bpy.types.Object) and obj.type == 'MESH': if any(v < 0 for v in obj.scale): invalid.append(obj) diff --git a/openpype/hosts/blender/plugins/publish/validate_no_colons_in_name.py b/openpype/hosts/blender/plugins/publish/validate_no_colons_in_name.py index f1889e5837..daf35c61ac 100644 --- a/openpype/hosts/blender/plugins/publish/validate_no_colons_in_name.py +++ b/openpype/hosts/blender/plugins/publish/validate_no_colons_in_name.py @@ -3,8 +3,8 @@ from typing import List import bpy import pyblish.api -from openpype.api import ValidateContentsOrder -from openpype.hosts.blender.api.action import SelectInvalidAction +import openpype.api +import openpype.hosts.blender.api.action class ValidateNoColonsInName(pyblish.api.InstancePlugin): @@ -15,18 +15,17 @@ class ValidateNoColonsInName(pyblish.api.InstancePlugin): """ - order = ValidateContentsOrder + order = openpype.api.ValidateContentsOrder hosts = ["blender"] families = ["model", "rig"] - category = "cleanup" version = (0, 1, 0) label = "No Colons in names" - actions = [SelectInvalidAction] + actions = [openpype.hosts.blender.api.action.SelectInvalidAction] @staticmethod - def get_invalid(cls, instance) -> List: + def get_invalid(instance) -> List: invalid = [] - for obj in set(instance): + for obj in instance: if ':' in obj.name: invalid.append(obj) if isinstance(obj, bpy.types.Object) and obj.type == 'ARMATURE': @@ -40,4 +39,5 @@ class ValidateNoColonsInName(pyblish.api.InstancePlugin): invalid = self.get_invalid(instance) if invalid: raise RuntimeError( - f"Objects found with colon in name: {invalid}") + f"Objects found with colon in name: {invalid}" + ) diff --git a/openpype/hosts/blender/plugins/publish/validate_object_mode.py b/openpype/hosts/blender/plugins/publish/validate_object_mode.py index 65b0bf7655..ac60e00f89 100644 --- a/openpype/hosts/blender/plugins/publish/validate_object_mode.py +++ b/openpype/hosts/blender/plugins/publish/validate_object_mode.py @@ -3,7 +3,7 @@ from typing import List import bpy import pyblish.api -from openpype.hosts.blender.api.action import SelectInvalidAction +import openpype.hosts.blender.api.action class ValidateObjectIsInObjectMode(pyblish.api.InstancePlugin): @@ -12,22 +12,21 @@ class ValidateObjectIsInObjectMode(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder - 0.01 hosts = ["blender"] families = ["model", "rig", "layout"] - category = "cleanup" label = "Validate Object Mode" - actions = [SelectInvalidAction] + actions = [openpype.hosts.blender.api.action.SelectInvalidAction] optional = False @staticmethod - def get_invalid(cls, instance) -> List: + def get_invalid(instance) -> List: invalid = [] - for obj in set(instance): - if isinstance(obj, bpy.types.Object): - if not obj.mode == 'OBJECT': - invalid.append(obj) + for obj in instance: + if isinstance(obj, bpy.types.Object) and obj.mode != "OBJECT": + invalid.append(obj) return invalid def process(self, instance): invalid = self.get_invalid(instance) if invalid: raise RuntimeError( - f"Object found in instance is not in Object Mode: {invalid}") + f"Object found in instance is not in Object Mode: {invalid}" + ) diff --git a/openpype/hosts/blender/plugins/publish/validate_transform_zero.py b/openpype/hosts/blender/plugins/publish/validate_transform_zero.py index 7443e3c64e..6e03094794 100644 --- a/openpype/hosts/blender/plugins/publish/validate_transform_zero.py +++ b/openpype/hosts/blender/plugins/publish/validate_transform_zero.py @@ -4,8 +4,8 @@ import mathutils import bpy import pyblish.api -from openpype.api import ValidateContentsOrder -from openpype.hosts.blender.api.action import SelectInvalidAction +import openpype.api +import openpype.hosts.blender.api.action class ValidateTransformZero(pyblish.api.InstancePlugin): @@ -17,20 +17,19 @@ class ValidateTransformZero(pyblish.api.InstancePlugin): """ - order = ValidateContentsOrder + order = openpype.api.ValidateContentsOrder hosts = ["blender"] families = ["model"] - category = "cleanup" version = (0, 1, 0) label = "Transform Zero" - actions = [SelectInvalidAction] + actions = [openpype.hosts.blender.api.action.SelectInvalidAction] _identity = mathutils.Matrix() @classmethod def get_invalid(cls, instance) -> List: invalid = [] - for obj in set(instance): + for obj in instance: if ( isinstance(obj, bpy.types.Object) and obj.matrix_basis != cls._identity From 403f5ddfc9cc754a13a3419b06260a34a8f682c6 Mon Sep 17 00:00:00 2001 From: Kaa Maurice Date: Mon, 15 Aug 2022 15:11:25 +0200 Subject: [PATCH 07/39] fix mesh uv validator with editmode --- .../blender/plugins/publish/validate_mesh_has_uv.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py b/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py index 1a52b3f851..83146c641e 100644 --- a/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py +++ b/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py @@ -26,7 +26,10 @@ class ValidateMeshHasUvs(pyblish.api.InstancePlugin): for uv_layer in obj.data.uv_layers: for polygon in obj.data.polygons: for loop_index in polygon.loop_indices: - if not uv_layer.data[loop_index].uv: + if ( + loop_index >= len(uv_layer.data) + or not uv_layer.data[loop_index].uv + ): return False return True @@ -36,6 +39,11 @@ class ValidateMeshHasUvs(pyblish.api.InstancePlugin): invalid = [] for obj in instance: if isinstance(obj, bpy.types.Object) and obj.type == 'MESH': + if obj.mode != "OBJECT": + cls.log.warning( + f"Mesh object {obj.name} should be in 'OBJECT' mode" + " to be properly checked." + ) if not cls.has_uvs(obj): invalid.append(obj) return invalid From d623dfa857be9b6650a7c4cd285f73b02be32808 Mon Sep 17 00:00:00 2001 From: Kaa Maurice Date: Thu, 25 Aug 2022 11:54:09 +0200 Subject: [PATCH 08/39] fix validator invalid return --- .../blender/plugins/publish/validate_mesh_no_negative_scale.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py b/openpype/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py index 3c5c7c11eb..329a8d80c3 100644 --- a/openpype/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py +++ b/openpype/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py @@ -24,6 +24,7 @@ class ValidateMeshNoNegativeScale(pyblish.api.Validator): if isinstance(obj, bpy.types.Object) and obj.type == 'MESH': if any(v < 0 for v in obj.scale): invalid.append(obj) + return invalid def process(self, instance): invalid = self.get_invalid(instance) From 9b7b217faafefb5bc32873337b41a3cce415c124 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 25 Aug 2022 16:02:56 +0200 Subject: [PATCH 09/39] Nuke: adding sumbitted job ids to instance attribute for downstream --- .../deadline/plugins/publish/submit_nuke_deadline.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py index 336a56ec45..b09d2935ab 100644 --- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -114,6 +114,13 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): instance.data["deadlineSubmissionJob"] = resp.json() instance.data["publishJobState"] = "Suspended" + # add to list of job Id + if not instance.data.get("bakingSubmissionJobs"): + instance.data["bakingSubmissionJobs"] = [] + + instance.data["bakingSubmissionJobs"].append( + resp.json()["_id"]) + # redefinition of families if "render.farm" in families: instance.data['family'] = 'write' From 33661b665cd60b6c4bc0fef13788f40cd906f0c9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 25 Aug 2022 16:10:17 +0200 Subject: [PATCH 10/39] global: submitting job is creating multiple job dependencies if multiple baking streams are submitted --- .../modules/deadline/plugins/publish/submit_publish_job.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 379953c9e4..2647dcf0cb 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -296,6 +296,12 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): for assembly_id in instance.data.get("assemblySubmissionJobs"): payload["JobInfo"]["JobDependency{}".format(job_index)] = assembly_id # noqa: E501 job_index += 1 + elif instance.data.get("bakingSubmissionJobs"): + self.log.info("Adding baking submission jobs as dependencies...") + job_index = 0 + for assembly_id in instance.data["bakingSubmissionJobs"]: + payload["JobInfo"]["JobDependency{}".format(job_index)] = assembly_id # noqa: E501 + job_index += 1 else: payload["JobInfo"]["JobDependency0"] = job["_id"] From 3ad9533fa82955301383c53e096d8fde2067c778 Mon Sep 17 00:00:00 2001 From: maxpareschi Date: Thu, 25 Aug 2022 20:10:27 +0200 Subject: [PATCH 11/39] workfile template also matches against os.environ --- openpype/pipeline/workfile/path_resolving.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/pipeline/workfile/path_resolving.py b/openpype/pipeline/workfile/path_resolving.py index ed1d1d793e..4cd225a515 100644 --- a/openpype/pipeline/workfile/path_resolving.py +++ b/openpype/pipeline/workfile/path_resolving.py @@ -408,6 +408,9 @@ def get_custom_workfile_template( # add root dict anatomy_context_data["root"] = anatomy.roots + # extend anatomy context with os.environ + anatomy_context_data.update(os.environ) + # get task type for the task in context current_task_type = anatomy_context_data["task"]["type"] From 380965927ad4aa58672008588940c455f02d08cc Mon Sep 17 00:00:00 2001 From: maxpareschi Date: Fri, 26 Aug 2022 12:13:29 +0200 Subject: [PATCH 12/39] reversed dict merging, anatomy has precedence. --- openpype/pipeline/workfile/path_resolving.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/pipeline/workfile/path_resolving.py b/openpype/pipeline/workfile/path_resolving.py index 4cd225a515..97e00d807c 100644 --- a/openpype/pipeline/workfile/path_resolving.py +++ b/openpype/pipeline/workfile/path_resolving.py @@ -409,10 +409,11 @@ def get_custom_workfile_template( anatomy_context_data["root"] = anatomy.roots # extend anatomy context with os.environ - anatomy_context_data.update(os.environ) + full_context_data = os.environ + full_context_data.update(anatomy_context_data) # get task type for the task in context - current_task_type = anatomy_context_data["task"]["type"] + current_task_type = full_context_data["task"]["type"] # get path from matching profile matching_item = filter_profiles( @@ -424,7 +425,7 @@ def get_custom_workfile_template( if matching_item: template = matching_item["path"][platform.system().lower()] return StringTemplate.format_strict_template( - template, anatomy_context_data + template, full_context_data ).normalized() return None From 2d9f2a6e767f340589c0f1955904a2b6762e178a Mon Sep 17 00:00:00 2001 From: maxpareschi Date: Fri, 26 Aug 2022 14:46:03 +0200 Subject: [PATCH 13/39] os.environ is now a copy not an instance --- openpype/pipeline/workfile/path_resolving.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/workfile/path_resolving.py b/openpype/pipeline/workfile/path_resolving.py index 97e00d807c..4ab4a4936c 100644 --- a/openpype/pipeline/workfile/path_resolving.py +++ b/openpype/pipeline/workfile/path_resolving.py @@ -409,7 +409,7 @@ def get_custom_workfile_template( anatomy_context_data["root"] = anatomy.roots # extend anatomy context with os.environ - full_context_data = os.environ + full_context_data = os.environ.copy() full_context_data.update(anatomy_context_data) # get task type for the task in context From 2c81bb5788db784073eec6a61755c288f4dd41d6 Mon Sep 17 00:00:00 2001 From: maxpareschi Date: Fri, 26 Aug 2022 15:09:47 +0200 Subject: [PATCH 14/39] moved env logic inside matching check --- openpype/pipeline/workfile/path_resolving.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/pipeline/workfile/path_resolving.py b/openpype/pipeline/workfile/path_resolving.py index 4ab4a4936c..6d9e72dbd2 100644 --- a/openpype/pipeline/workfile/path_resolving.py +++ b/openpype/pipeline/workfile/path_resolving.py @@ -408,12 +408,8 @@ def get_custom_workfile_template( # add root dict anatomy_context_data["root"] = anatomy.roots - # extend anatomy context with os.environ - full_context_data = os.environ.copy() - full_context_data.update(anatomy_context_data) - # get task type for the task in context - current_task_type = full_context_data["task"]["type"] + current_task_type = anatomy_context_data["task"]["type"] # get path from matching profile matching_item = filter_profiles( @@ -423,6 +419,11 @@ def get_custom_workfile_template( # when path is available try to format it in case # there are some anatomy template strings if matching_item: + # extend anatomy context with os.environ to + # also allow formatting against env + full_context_data = os.environ.copy() + full_context_data.update(anatomy_context_data) + template = matching_item["path"][platform.system().lower()] return StringTemplate.format_strict_template( template, full_context_data From fcb047770ad41364bbb9aa50ab40765fd43132cf Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 29 Aug 2022 12:07:51 +0200 Subject: [PATCH 15/39] fix import in collect ftrack api --- .../modules/ftrack/plugins/publish/collect_ftrack_family.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py index 5758068f86..576a7d36c4 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py @@ -8,7 +8,7 @@ Provides: import pyblish.api from openpype.pipeline import legacy_io -from openpype.lib.plugin_tools import filter_profiles +from openpype.lib import filter_profiles class CollectFtrackFamily(pyblish.api.InstancePlugin): From 5ad2de372a3507ed38321afe881e7414e6738051 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 29 Aug 2022 12:11:30 +0200 Subject: [PATCH 16/39] use new 'get_subset_name' in creator plugins --- openpype/pipeline/create/creator_plugins.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 9e1530a6a7..bf2fdd2c5f 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -9,7 +9,7 @@ from abc import ( import six from openpype.settings import get_system_settings, get_project_settings -from openpype.lib import get_subset_name_with_asset_doc +from .subset_name import get_subset_name from openpype.pipeline.plugin_discover import ( discover, register_plugin, @@ -75,6 +75,7 @@ class BaseCreator: ): # Reference to CreateContext self.create_context = create_context + self.project_settings = project_settings # Creator is running in headless mode (without UI elemets) # - we may use UI inside processing this attribute should be checked @@ -276,14 +277,15 @@ class BaseCreator: variant, task_name, asset_doc, project_name, host_name ) - return get_subset_name_with_asset_doc( + return get_subset_name( self.family, variant, task_name, asset_doc, project_name, host_name, - dynamic_data=dynamic_data + dynamic_data=dynamic_data, + project_settings=self.project_settings ) def get_instance_attr_defs(self): From 6f4f87418eabdf1248dbd4db29cff77ff018b0ab Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 29 Aug 2022 13:31:28 +0200 Subject: [PATCH 17/39] integrate thumbnail does not require 'AVALON_THUMBNAIL_ROOT' to be set if template does not use it --- .../plugins/publish/integrate_thumbnail.py | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/openpype/plugins/publish/integrate_thumbnail.py b/openpype/plugins/publish/integrate_thumbnail.py index 8ae0dd2d60..445c563d27 100644 --- a/openpype/plugins/publish/integrate_thumbnail.py +++ b/openpype/plugins/publish/integrate_thumbnail.py @@ -6,10 +6,9 @@ import copy import six import pyblish.api -from bson.objectid import ObjectId from openpype.client import get_version_by_id -from openpype.pipeline import legacy_io +from openpype.client.operations import OperationsSession, new_thumbnail_doc class IntegrateThumbnails(pyblish.api.InstancePlugin): @@ -24,13 +23,9 @@ class IntegrateThumbnails(pyblish.api.InstancePlugin): ] def process(self, instance): - - if not os.environ.get("AVALON_THUMBNAIL_ROOT"): - self.log.warning( - "AVALON_THUMBNAIL_ROOT is not set." - " Skipping thumbnail integration." - ) - return + env_key = "AVALON_THUMBNAIL_ROOT" + thumbnail_root_format_key = "{thumbnail_root}" + thumbnail_root = os.environ.get(env_key) or "" published_repres = instance.data.get("published_representations") if not published_repres: @@ -51,6 +46,16 @@ class IntegrateThumbnails(pyblish.api.InstancePlugin): ).format(project_name)) return + thumbnail_template = anatomy.templates["publish"]["thumbnail"] + if ( + not thumbnail_root + and thumbnail_root_format_key in thumbnail_template + ): + self.log.warning(( + "{} is not set. Skipping thumbnail integration." + ).format(env_key)) + return + thumb_repre = None thumb_repre_anatomy_data = None for repre_info in published_repres.values(): From 503d64ec11be7c9af86992df3ffbe6a14534d97f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 29 Aug 2022 13:31:57 +0200 Subject: [PATCH 18/39] thumbnail resolver does not need to have 'AVALON_THUMBNAIL_ROOT' set if thumbnail template does not need it --- openpype/pipeline/thumbnail.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/openpype/pipeline/thumbnail.py b/openpype/pipeline/thumbnail.py index eb383b16d9..5530d29614 100644 --- a/openpype/pipeline/thumbnail.py +++ b/openpype/pipeline/thumbnail.py @@ -73,19 +73,20 @@ class ThumbnailResolver(object): class TemplateResolver(ThumbnailResolver): - priority = 90 def process(self, thumbnail_entity, thumbnail_type): - - if not os.environ.get("AVALON_THUMBNAIL_ROOT"): - return - template = thumbnail_entity["data"].get("template") if not template: self.log.debug("Thumbnail entity does not have set template") return + thumbnail_root_format_key = "{thumbnail_root}" + thumbnail_root = os.environ.get("AVALON_THUMBNAIL_ROOT") or "" + # Check if template require thumbnail root and if is avaiable + if thumbnail_root_format_key in template and not thumbnail_root: + return + project_name = self.dbcon.active_project() project = get_project(project_name, fields=["name", "data.code"]) @@ -95,7 +96,7 @@ class TemplateResolver(ThumbnailResolver): template_data.update({ "_id": str(thumbnail_entity["_id"]), "thumbnail_type": thumbnail_type, - "thumbnail_root": os.environ.get("AVALON_THUMBNAIL_ROOT"), + "thumbnail_root": thumbnail_root, "project": { "name": project["name"], "code": project["data"].get("code") From 88d914811647427e52d86b5b99a0eb1afd8f1b6c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 29 Aug 2022 13:32:12 +0200 Subject: [PATCH 19/39] added creation of new thumbnail document into operations --- openpype/client/operations.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/openpype/client/operations.py b/openpype/client/operations.py index c0716ee109..9daaa3e116 100644 --- a/openpype/client/operations.py +++ b/openpype/client/operations.py @@ -24,6 +24,7 @@ CURRENT_SUBSET_SCHEMA = "openpype:subset-3.0" CURRENT_VERSION_SCHEMA = "openpype:version-3.0" CURRENT_REPRESENTATION_SCHEMA = "openpype:representation-2.0" CURRENT_WORKFILE_INFO_SCHEMA = "openpype:workfile-1.0" +CURRENT_THUMBNAIL_SCHEMA = "openpype:thumbnail-1.0" def _create_or_convert_to_mongo_id(mongo_id): @@ -195,6 +196,29 @@ def new_representation_doc( } +def new_thumbnail_doc(data=None, entity_id=None): + """Create skeleton data of thumbnail document. + + Args: + data (Dict[str, Any]): Thumbnail document data. + entity_id (Union[str, ObjectId]): Predefined id of document. New id is + created if not passed. + + Returns: + Dict[str, Any]: Skeleton of thumbnail document. + """ + + if data is None: + data = {} + + return { + "_id": _create_or_convert_to_mongo_id(entity_id), + "type": "thumbnail", + "schema": CURRENT_THUMBNAIL_SCHEMA, + "data": data + } + + def new_workfile_info_doc( filename, asset_id, task_name, files, data=None, entity_id=None ): From 46553deec9cdb85937b298ba7aa6e1482b5aa673 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 29 Aug 2022 13:32:23 +0200 Subject: [PATCH 20/39] use perations in integrate thumbnail --- .../plugins/publish/integrate_thumbnail.py | 55 ++++++++++--------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/openpype/plugins/publish/integrate_thumbnail.py b/openpype/plugins/publish/integrate_thumbnail.py index 445c563d27..d86cec10ad 100644 --- a/openpype/plugins/publish/integrate_thumbnail.py +++ b/openpype/plugins/publish/integrate_thumbnail.py @@ -71,10 +71,6 @@ class IntegrateThumbnails(pyblish.api.InstancePlugin): ) return - legacy_io.install() - - thumbnail_template = anatomy.templates["publish"]["thumbnail"] - version = get_version_by_id(project_name, thumb_repre["parent"]) if not version: raise AssertionError( @@ -93,14 +89,15 @@ class IntegrateThumbnails(pyblish.api.InstancePlugin): filename, file_extension = os.path.splitext(src_full_path) # Create id for mongo entity now to fill anatomy template - thumbnail_id = ObjectId() + thumbnail_doc = new_thumbnail_doc() + thumbnail_id = thumbnail_doc["_id"] # Prepare anatomy template fill data template_data = copy.deepcopy(thumb_repre_anatomy_data) template_data.update({ "_id": str(thumbnail_id), - "thumbnail_root": os.environ.get("AVALON_THUMBNAIL_ROOT"), "ext": file_extension[1:], + "thumbnail_root": thumbnail_root, "thumbnail_type": "thumbnail" }) @@ -122,8 +119,8 @@ class IntegrateThumbnails(pyblish.api.InstancePlugin): shutil.copy(src_full_path, dst_full_path) # Clean template data from keys that are dynamic - template_data.pop("_id") - template_data.pop("thumbnail_root") + for key in ("_id", "thumbnail_root"): + template_data.pop(key, None) repre_context = template_filled.used_values for key in self.required_context_keys: @@ -132,34 +129,40 @@ class IntegrateThumbnails(pyblish.api.InstancePlugin): continue repre_context[key] = template_data[key] - thumbnail_entity = { - "_id": thumbnail_id, - "type": "thumbnail", - "schema": "openpype:thumbnail-1.0", - "data": { - "template": thumbnail_template, - "template_data": repre_context - } + op_session = OperationsSession() + + thumbnail_doc["data"] = { + "template": thumbnail_template, + "template_data": repre_context } - # Create thumbnail entity - legacy_io.insert_one(thumbnail_entity) - self.log.debug( - "Creating entity in database {}".format(str(thumbnail_entity)) + op_session.create_entity( + project_name, thumbnail_doc["type"], thumbnail_doc ) + # Create thumbnail entity + self.log.debug( + "Creating entity in database {}".format(str(thumbnail_doc)) + ) + # Set thumbnail id for version - legacy_io.update_many( - {"_id": version["_id"]}, - {"$set": {"data.thumbnail_id": thumbnail_id}} + op_session.update_entity( + project_name, + version["type"], + version["_id"], + {"data.thumbnail_id": thumbnail_id} ) self.log.debug("Setting thumbnail for version \"{}\" <{}>".format( version["name"], str(version["_id"]) )) asset_entity = instance.data["assetEntity"] - legacy_io.update_many( - {"_id": asset_entity["_id"]}, - {"$set": {"data.thumbnail_id": thumbnail_id}} + op_session.update_entity( + project_name, + asset_entity["type"], + asset_entity["_id"], + {"data.thumbnail_id": thumbnail_id} ) self.log.debug("Setting thumbnail for asset \"{}\" <{}>".format( asset_entity["name"], str(version["_id"]) )) + + op_session.commit() From f56658737a9abc52ede2f971a136d44e749c4771 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 29 Aug 2022 13:35:50 +0200 Subject: [PATCH 21/39] use also anatomy roots --- openpype/pipeline/thumbnail.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/thumbnail.py b/openpype/pipeline/thumbnail.py index 5530d29614..d95f5e35c9 100644 --- a/openpype/pipeline/thumbnail.py +++ b/openpype/pipeline/thumbnail.py @@ -4,6 +4,7 @@ import logging from openpype.client import get_project from . import legacy_io +from .anatomy import Anatomy from .plugin_discover import ( discover, register_plugin, @@ -89,6 +90,7 @@ class TemplateResolver(ThumbnailResolver): project_name = self.dbcon.active_project() project = get_project(project_name, fields=["name", "data.code"]) + anatomy = Anatomy(project_name) template_data = copy.deepcopy( thumbnail_entity["data"].get("template_data") or {} @@ -100,7 +102,8 @@ class TemplateResolver(ThumbnailResolver): "project": { "name": project["name"], "code": project["data"].get("code") - } + }, + "root": anatomy.roots }) try: From f99d9d3d77149b084b75ebf3a5621bf49c4eb9b6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 29 Aug 2022 13:36:58 +0200 Subject: [PATCH 22/39] use project anatomy if needed --- openpype/pipeline/thumbnail.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/thumbnail.py b/openpype/pipeline/thumbnail.py index d95f5e35c9..39f3e17893 100644 --- a/openpype/pipeline/thumbnail.py +++ b/openpype/pipeline/thumbnail.py @@ -90,7 +90,6 @@ class TemplateResolver(ThumbnailResolver): project_name = self.dbcon.active_project() project = get_project(project_name, fields=["name", "data.code"]) - anatomy = Anatomy(project_name) template_data = copy.deepcopy( thumbnail_entity["data"].get("template_data") or {} @@ -103,8 +102,11 @@ class TemplateResolver(ThumbnailResolver): "name": project["name"], "code": project["data"].get("code") }, - "root": anatomy.roots }) + # Add anatomy roots if is in template + if "{root" in template: + anatomy = Anatomy(project_name) + template_data["root"] = anatomy.roots try: filepath = os.path.normpath(template.format(**template_data)) From 3618e8f856859106714bc9c550af7ac8aac9f8c6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 29 Aug 2022 14:10:23 +0200 Subject: [PATCH 23/39] create formatting function for file sizes 'format_file_size' --- openpype/lib/path_tools.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/openpype/lib/path_tools.py b/openpype/lib/path_tools.py index 4f28be3302..f807917f5b 100644 --- a/openpype/lib/path_tools.py +++ b/openpype/lib/path_tools.py @@ -14,6 +14,27 @@ from .profiles_filtering import filter_profiles log = logging.getLogger(__name__) +def format_file_size(file_size, suffix=None): + """Returns formatted string with size in appropriate unit. + + Args: + file_size (int): Size of file in bytes. + suffix (str): Suffix for formatted size. Default is 'B' (as bytes). + + Returns: + str: Formatted size using proper unit and passed suffix (e.g. 7 MiB). + """ + + if suffix is None: + suffix = "B" + + for unit in ["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]: + if abs(file_size) < 1024.0: + return "%3.1f%s%s" % (file_size, unit, suffix) + file_size /= 1024.0 + return "%.1f%s%s" % (file_size, "Yi", suffix) + + def create_hard_link(src_path, dst_path): """Create hardlink of file. From 6398f021092d2de440218acec0d8a024aa55d75d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 29 Aug 2022 14:10:48 +0200 Subject: [PATCH 24/39] copied function to collect frames 'collect_frames' --- openpype/lib/path_tools.py | 39 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/openpype/lib/path_tools.py b/openpype/lib/path_tools.py index f807917f5b..45aa54d6cb 100644 --- a/openpype/lib/path_tools.py +++ b/openpype/lib/path_tools.py @@ -6,6 +6,8 @@ import logging import six import platform +import clique + from openpype.client import get_project from openpype.settings import get_project_settings @@ -71,6 +73,43 @@ def create_hard_link(src_path, dst_path): ) +def collect_frames(files): + """Returns dict of source path and its frame, if from sequence + + Uses clique as most precise solution, used when anatomy template that + created files is not known. + + Assumption is that frames are separated by '.', negative frames are not + allowed. + + Args: + files(list) or (set with single value): list of source paths + + Returns: + (dict): {'/asset/subset_v001.0001.png': '0001', ....} + """ + + patterns = [clique.PATTERNS["frames"]] + collections, remainder = clique.assemble( + files, minimum_items=1, patterns=patterns) + + sources_and_frames = {} + if collections: + for collection in collections: + src_head = collection.head + src_tail = collection.tail + + for index in collection.indexes: + src_frame = collection.format("{padding}") % index + src_file_name = "{}{}{}".format( + src_head, src_frame, src_tail) + sources_and_frames[src_file_name] = src_frame + else: + sources_and_frames[remainder.pop()] = None + + return sources_and_frames + + def _rreplace(s, a, b, n=1): """Replace a with b in string s from right side n times.""" return b.join(s.rsplit(a, n)) From c26119cc9f6a9fe4c330842f9dddbf7865a63425 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 29 Aug 2022 14:13:08 +0200 Subject: [PATCH 25/39] use new functions in code --- openpype/lib/__init__.py | 4 ++++ .../publish/submit_aftereffects_deadline.py | 6 ++++-- .../validate_expected_and_rendered_files.py | 2 +- .../action_delete_old_versions.py | 17 +++++++---------- openpype/plugins/load/delete_old_versions.py | 11 ++--------- openpype/plugins/load/delivery.py | 11 +++++++---- 6 files changed, 25 insertions(+), 26 deletions(-) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index adb857a056..17aafc3e8b 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -192,6 +192,8 @@ from .plugin_tools import ( ) from .path_tools import ( + format_file_size, + collect_frames, create_hard_link, version_up, get_version_from_path, @@ -353,6 +355,8 @@ __all__ = [ "set_plugin_attributes_from_settings", "source_hash", + "format_file_size", + "collect_frames", "create_hard_link", "version_up", "get_version_from_path", diff --git a/openpype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py b/openpype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py index c55f85c8da..1d68793d53 100644 --- a/openpype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py @@ -3,8 +3,10 @@ import attr import getpass import pyblish.api -from openpype.lib import env_value_to_bool -from openpype.lib.delivery import collect_frames +from openpype.lib import ( + env_value_to_bool, + collect_frames, +) from openpype.pipeline import legacy_io from openpype_modules.deadline import abstract_submit_deadline from openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo diff --git a/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py b/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py index c2426e0d78..f0a3ddd246 100644 --- a/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py +++ b/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py @@ -3,7 +3,7 @@ import requests import pyblish.api -from openpype.lib.delivery import collect_frames +from openpype.lib import collect_frames from openpype_modules.deadline.abstract_submit_deadline import requests_get diff --git a/openpype/modules/ftrack/event_handlers_user/action_delete_old_versions.py b/openpype/modules/ftrack/event_handlers_user/action_delete_old_versions.py index 79d04a7854..c543dc8834 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_delete_old_versions.py +++ b/openpype/modules/ftrack/event_handlers_user/action_delete_old_versions.py @@ -11,7 +11,11 @@ from openpype.client import ( get_versions, get_representations ) -from openpype.lib import StringTemplate, TemplateUnsolved +from openpype.lib import ( + StringTemplate, + TemplateUnsolved, + format_file_size, +) from openpype.pipeline import AvalonMongoDB, Anatomy from openpype_modules.ftrack.lib import BaseAction, statics_icon @@ -134,13 +138,6 @@ class DeleteOldVersions(BaseAction): "title": self.inteface_title } - def sizeof_fmt(self, num, suffix='B'): - for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']: - if abs(num) < 1024.0: - return "%3.1f%s%s" % (num, unit, suffix) - num /= 1024.0 - return "%.1f%s%s" % (num, 'Yi', suffix) - def launch(self, session, entities, event): values = event["data"].get("values") if not values: @@ -359,7 +356,7 @@ class DeleteOldVersions(BaseAction): dir_paths, file_paths_by_dir, delete=False ) - msg = "Total size of files: " + self.sizeof_fmt(size) + msg = "Total size of files: {}".format(format_file_size(size)) self.log.warning(msg) @@ -430,7 +427,7 @@ class DeleteOldVersions(BaseAction): "message": msg } - msg = "Total size of files deleted: " + self.sizeof_fmt(size) + msg = "Total size of files deleted: {}".format(format_file_size(size)) self.log.warning(msg) diff --git a/openpype/plugins/load/delete_old_versions.py b/openpype/plugins/load/delete_old_versions.py index 6e0b464cc1..ce6f204c64 100644 --- a/openpype/plugins/load/delete_old_versions.py +++ b/openpype/plugins/load/delete_old_versions.py @@ -10,7 +10,7 @@ from Qt import QtWidgets, QtCore from openpype.client import get_versions, get_representations from openpype import style from openpype.pipeline import load, AvalonMongoDB, Anatomy -from openpype.lib import StringTemplate +from openpype.lib import StringTemplate, format_file_size from openpype.modules import ModulesManager @@ -38,13 +38,6 @@ class DeleteOldVersions(load.SubsetLoaderPlugin): ) ] - def sizeof_fmt(self, num, suffix='B'): - for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']: - if abs(num) < 1024.0: - return "%3.1f%s%s" % (num, unit, suffix) - num /= 1024.0 - return "%.1f%s%s" % (num, 'Yi', suffix) - def delete_whole_dir_paths(self, dir_paths, delete=True): size = 0 @@ -456,7 +449,7 @@ class DeleteOldVersions(load.SubsetLoaderPlugin): size += self.main(project_name, data, remove_publish_folder) print("Progressing {}/{}".format(count + 1, len(contexts))) - msg = "Total size of files: " + self.sizeof_fmt(size) + msg = "Total size of files: {}".format(format_file_size(size)) self.log.info(msg) self.message(msg) diff --git a/openpype/plugins/load/delivery.py b/openpype/plugins/load/delivery.py index f6e1d4f06b..2a9f25e0fb 100644 --- a/openpype/plugins/load/delivery.py +++ b/openpype/plugins/load/delivery.py @@ -7,15 +7,17 @@ from openpype.client import get_representations from openpype.pipeline import load, Anatomy from openpype import resources, style +from openpype.lib import ( + format_file_size, + collect_frames, +) from openpype.lib.dateutils import get_datetime_data from openpype.lib.delivery import ( - sizeof_fmt, path_from_representation, get_format_dict, check_destination_path, process_single_file, process_sequence, - collect_frames ) @@ -263,8 +265,9 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): def _prepare_label(self): """Provides text with no of selected files and their size.""" - label = "{} files, size {}".format(self.files_selected, - sizeof_fmt(self.size_selected)) + label = "{} files, size {}".format( + self.files_selected, + format_file_size(self.size_selected)) return label def _get_selected_repres(self): From aeb30b3101c31f8965e80cf40287f5e0d4e4dfe9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 29 Aug 2022 14:13:21 +0200 Subject: [PATCH 26/39] marked functions in delivery as deprecated --- openpype/lib/delivery.py | 78 ++++++++++++++++++++++++++++------------ 1 file changed, 55 insertions(+), 23 deletions(-) diff --git a/openpype/lib/delivery.py b/openpype/lib/delivery.py index ffcfe9fa4d..5244187354 100644 --- a/openpype/lib/delivery.py +++ b/openpype/lib/delivery.py @@ -4,6 +4,8 @@ import shutil import glob import clique import collections +import functools +import warnings from .path_templates import ( StringTemplate, @@ -11,6 +13,52 @@ from .path_templates import ( ) +class DeliveryDeprecatedWarning(DeprecationWarning): + pass + + +def deprecated(new_destination): + """Mark functions as deprecated. + + It will result in a warning being emitted when the function is used. + """ + + func = None + if callable(new_destination): + func = new_destination + new_destination = None + + def _decorator(decorated_func): + if new_destination is None: + warning_message = ( + " Please check content of deprecated function to figure out" + " possible replacement." + ) + else: + warning_message = " Please replace your usage with '{}'.".format( + new_destination + ) + + @functools.wraps(decorated_func) + def wrapper(*args, **kwargs): + warnings.simplefilter("always", DeliveryDeprecatedWarning) + warnings.warn( + ( + "Call to deprecated function '{}'" + "\nFunction was moved or removed.{}" + ).format(decorated_func.__name__, warning_message), + category=DeliveryDeprecatedWarning, + stacklevel=4 + ) + return decorated_func(*args, **kwargs) + return wrapper + + if func is None: + return _decorator + return _decorator(func) + + +@deprecated("openpype.lib.path_tools.collect_frames") def collect_frames(files): """ Returns dict of source path and its frame, if from sequence @@ -26,34 +74,18 @@ def collect_frames(files): Returns: (dict): {'/asset/subset_v001.0001.png': '0001', ....} """ - patterns = [clique.PATTERNS["frames"]] - collections, remainder = clique.assemble(files, minimum_items=1, - patterns=patterns) - sources_and_frames = {} - if collections: - for collection in collections: - src_head = collection.head - src_tail = collection.tail + from .path_tools import collect_frames - for index in collection.indexes: - src_frame = collection.format("{padding}") % index - src_file_name = "{}{}{}".format(src_head, src_frame, - src_tail) - sources_and_frames[src_file_name] = src_frame - else: - sources_and_frames[remainder.pop()] = None - - return sources_and_frames + return collect_frames(files) -def sizeof_fmt(num, suffix='B'): +@deprecated("openpype.lib.path_tools.format_file_size") +def sizeof_fmt(num, suffix=None): """Returns formatted string with size in appropriate unit""" - for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']: - if abs(num) < 1024.0: - return "%3.1f%s%s" % (num, unit, suffix) - num /= 1024.0 - return "%.1f%s%s" % (num, 'Yi', suffix) + + from .path_tools import format_file_size + return format_file_size(num, suffix) def path_from_representation(representation, anatomy): From d58ea894159cb1190fb5bcad5cdf4e949adf39f7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 29 Aug 2022 14:26:23 +0200 Subject: [PATCH 27/39] implemented 'get_representation_path_with_anatomy'. --- openpype/pipeline/load/__init__.py | 5 +++ openpype/pipeline/load/utils.py | 62 +++++++++++++++++++++++++++--- 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/openpype/pipeline/load/__init__.py b/openpype/pipeline/load/__init__.py index b6bdd13d50..4fc8ad1d16 100644 --- a/openpype/pipeline/load/__init__.py +++ b/openpype/pipeline/load/__init__.py @@ -1,6 +1,8 @@ from .utils import ( HeroVersionType, + IncompatibleLoaderError, + InvalidRepresentationContext, get_repres_contexts, get_subset_contexts, @@ -20,6 +22,7 @@ from .utils import ( get_representation_path_from_context, get_representation_path, + get_representation_path_with_anatomy, is_compatible_loader, @@ -46,7 +49,9 @@ from .plugins import ( __all__ = ( # utils.py "HeroVersionType", + "IncompatibleLoaderError", + "InvalidRepresentationContext", "get_repres_contexts", "get_subset_contexts", diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index 99d6876d4b..d4a5c2be5a 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -23,10 +23,16 @@ from openpype.client import ( get_representation_by_name, get_representation_parents ) +from openpype.lib import ( + StringTemplate, + TemplateUnsolved, +) from openpype.pipeline import ( schema, legacy_io, Anatomy, + registered_root, + registered_host, ) log = logging.getLogger(__name__) @@ -61,6 +67,11 @@ class IncompatibleLoaderError(ValueError): pass +class InvalidRepresentationContext(ValueError): + """Representation path can't be received using representation document.""" + pass + + def get_repres_contexts(representation_ids, dbcon=None): """Return parenthood context for representation. @@ -515,6 +526,52 @@ def get_representation_path_from_context(context): return get_representation_path(representation, root) +def get_representation_path_with_anatomy(repre_doc, anatomy): + """Receive representation path using representation document and anatomy. + + Anatomy is used to replace 'root' key in representation file. Ideally + should be used instead of 'get_representation_path' which is based on + "current context". + + Future notes: + We want also be able store resources into representation and I can + imagine the result should also contain paths to possible resources. + + Args: + repre_doc (Dict[str, Any]): Representation document. + anatomy (Anatomy): Project anatomy object. + + Returns: + Union[None, TemplateResult]: None if path can't be received + + Raises: + InvalidRepresentationContext: When representation data are probably + invalid or not available. + """ + + try: + template = repre_doc["data"]["template"] + + except KeyError: + raise InvalidRepresentationContext(( + "Representation document does not" + " contain template in data ('data.template')" + )) + + try: + context = repre_doc["context"] + context["root"] = anatomy.roots + path = StringTemplate.format_strict_template(template, context) + + except TemplateUnsolved as exc: + raise InvalidRepresentationContext(( + "Couldn't resolve representation template with available data." + " Reason: {}".format(str(exc)) + )) + + return path.normalized() + + def get_representation_path(representation, root=None, dbcon=None): """Get filename from representation document @@ -533,14 +590,10 @@ def get_representation_path(representation, root=None, dbcon=None): """ - from openpype.lib import StringTemplate, TemplateUnsolved - if dbcon is None: dbcon = legacy_io if root is None: - from openpype.pipeline import registered_root - root = registered_root() def path_from_represenation(): @@ -736,7 +789,6 @@ def get_outdated_containers(host=None, project_name=None): """ if host is None: - from openpype.pipeline import registered_host host = registered_host() if project_name is None: From 315cf40d8baa47bb3a4f6864e49551bdfc6d196b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 29 Aug 2022 14:29:43 +0200 Subject: [PATCH 28/39] fixed import in load utils --- openpype/pipeline/load/utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index d4a5c2be5a..83b904e4a7 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -31,8 +31,6 @@ from openpype.pipeline import ( schema, legacy_io, Anatomy, - registered_root, - registered_host, ) log = logging.getLogger(__name__) @@ -594,6 +592,8 @@ def get_representation_path(representation, root=None, dbcon=None): dbcon = legacy_io if root is None: + from openpype.pipeline import registered_root + root = registered_root() def path_from_represenation(): @@ -789,6 +789,8 @@ def get_outdated_containers(host=None, project_name=None): """ if host is None: + from openpype.pipeline import registered_host + host = registered_host() if project_name is None: From f2a191861b9264383da3c0b63ed8f4feac629a1c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 29 Aug 2022 14:41:28 +0200 Subject: [PATCH 29/39] marked 'path_from_representation' as deprecated and replaced it's usage with 'get_representation_path_with_anatomy' --- openpype/lib/delivery.py | 29 +++++-------- .../event_handlers_user/action_delivery.py | 6 +-- openpype/pipeline/load/__init__.py | 1 + openpype/plugins/load/delete_old_versions.py | 41 +++++++++++-------- openpype/plugins/load/delivery.py | 8 ++-- 5 files changed, 43 insertions(+), 42 deletions(-) diff --git a/openpype/lib/delivery.py b/openpype/lib/delivery.py index 5244187354..ea757932c9 100644 --- a/openpype/lib/delivery.py +++ b/openpype/lib/delivery.py @@ -7,11 +7,6 @@ import collections import functools import warnings -from .path_templates import ( - StringTemplate, - TemplateUnsolved, -) - class DeliveryDeprecatedWarning(DeprecationWarning): pass @@ -88,24 +83,22 @@ def sizeof_fmt(num, suffix=None): return format_file_size(num, suffix) +@deprecated("openpype.pipeline.load.get_representation_path_with_anatomy") def path_from_representation(representation, anatomy): - try: - template = representation["data"]["template"] + """Get representation path using representation document and anatomy. - except KeyError: - return None + Args: + representation (Dict[str, Any]): Representation document. + anatomy (Anatomy): Project anatomy. - try: - context = representation["context"] - context["root"] = anatomy.roots - path = StringTemplate.format_strict_template(template, context) - return os.path.normpath(path) + Deprecated: + Function was moved to different location and will be removed + after 3.16.* release. + """ - except TemplateUnsolved: - # Template references unavailable data - return None + from openpype.pipeline.load import get_representation_path_with_anatomy - return path + return get_representation_path_with_anatomy(representation, anatomy) def copy_file(src_path, dst_path): diff --git a/openpype/modules/ftrack/event_handlers_user/action_delivery.py b/openpype/modules/ftrack/event_handlers_user/action_delivery.py index eec245070c..59a34b3f85 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_delivery.py +++ b/openpype/modules/ftrack/event_handlers_user/action_delivery.py @@ -10,15 +10,15 @@ from openpype.client import ( get_versions, get_representations ) -from openpype.pipeline import Anatomy from openpype_modules.ftrack.lib import BaseAction, statics_icon from openpype_modules.ftrack.lib.avalon_sync import CUST_ATTR_ID_KEY from openpype_modules.ftrack.lib.custom_attributes import ( query_custom_attributes ) from openpype.lib.dateutils import get_datetime_data +from openpype.pipeline import Anatomy +from openpype.pipeline.load import get_representation_path_with_anatomy from openpype.lib.delivery import ( - path_from_representation, get_format_dict, check_destination_path, process_single_file, @@ -580,7 +580,7 @@ class Delivery(BaseAction): if frame: repre["context"]["frame"] = len(str(frame)) * "#" - repre_path = path_from_representation(repre, anatomy) + repre_path = get_representation_path_with_anatomy(repre, anatomy) # TODO add backup solution where root of path from component # is replaced with root args = ( diff --git a/openpype/pipeline/load/__init__.py b/openpype/pipeline/load/__init__.py index 4fc8ad1d16..bf38a0b3c8 100644 --- a/openpype/pipeline/load/__init__.py +++ b/openpype/pipeline/load/__init__.py @@ -71,6 +71,7 @@ __all__ = ( "get_representation_path_from_context", "get_representation_path", + "get_representation_path_with_anatomy", "is_compatible_loader", diff --git a/openpype/plugins/load/delete_old_versions.py b/openpype/plugins/load/delete_old_versions.py index ce6f204c64..8c8546d9c8 100644 --- a/openpype/plugins/load/delete_old_versions.py +++ b/openpype/plugins/load/delete_old_versions.py @@ -7,11 +7,15 @@ from pymongo import UpdateOne import qargparse from Qt import QtWidgets, QtCore -from openpype.client import get_versions, get_representations from openpype import style -from openpype.pipeline import load, AvalonMongoDB, Anatomy -from openpype.lib import StringTemplate, format_file_size +from openpype.client import get_versions, get_representations from openpype.modules import ModulesManager +from openpype.lib import StringTemplate, format_file_size +from openpype.pipeline import load, AvalonMongoDB, Anatomy +from openpype.pipeline.load import ( + get_representation_path_with_anatomy, + InvalidRepresentationContext, +) class DeleteOldVersions(load.SubsetLoaderPlugin): @@ -73,27 +77,28 @@ class DeleteOldVersions(load.SubsetLoaderPlugin): def path_from_representation(self, representation, anatomy): try: - template = representation["data"]["template"] - + context = representation["context"] except KeyError: return (None, None) + try: + path = get_representation_path_with_anatomy( + representation, anatomy + ) + except InvalidRepresentationContext: + return (None, None) + sequence_path = None - try: - context = representation["context"] - context["root"] = anatomy.roots - path = str(StringTemplate.format_template(template, context)) - if "frame" in context: - context["frame"] = self.sequence_splitter - sequence_path = os.path.normpath(str( - StringTemplate.format_template(template, context) - )) + if "frame" in context: + context["frame"] = self.sequence_splitter + sequence_path = get_representation_path_with_anatomy( + representation, anatomy + ) - except KeyError: - # Template references unavailable data - return (None, None) + if sequence_path: + sequence_path = sequence_path.normalized() - return (os.path.normpath(path), sequence_path) + return (path.normalized(), sequence_path) def delete_only_repre_files(self, dir_paths, file_paths, delete=True): size = 0 diff --git a/openpype/plugins/load/delivery.py b/openpype/plugins/load/delivery.py index 2a9f25e0fb..4651efd4a3 100644 --- a/openpype/plugins/load/delivery.py +++ b/openpype/plugins/load/delivery.py @@ -10,10 +10,10 @@ from openpype import resources, style from openpype.lib import ( format_file_size, collect_frames, + get_datetime_data, ) -from openpype.lib.dateutils import get_datetime_data +from openpype.pipeline.load import get_representation_path_with_anatomy from openpype.lib.delivery import ( - path_from_representation, get_format_dict, check_destination_path, process_single_file, @@ -169,7 +169,9 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): if repre["name"] not in selected_repres: continue - repre_path = path_from_representation(repre, self.anatomy) + repre_path = get_representation_path_with_anatomy( + repre, self.anatomy + ) anatomy_data = copy.deepcopy(repre["context"]) new_report_items = check_destination_path(str(repre["_id"]), From ea241ca807837896ac1f8299ec0c6c6bbb1020ad Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 29 Aug 2022 14:41:44 +0200 Subject: [PATCH 30/39] added some docstrings to deprecated functions --- openpype/lib/delivery.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/openpype/lib/delivery.py b/openpype/lib/delivery.py index ea757932c9..e09188d3bb 100644 --- a/openpype/lib/delivery.py +++ b/openpype/lib/delivery.py @@ -55,19 +55,23 @@ def deprecated(new_destination): @deprecated("openpype.lib.path_tools.collect_frames") def collect_frames(files): - """ - Returns dict of source path and its frame, if from sequence + """Returns dict of source path and its frame, if from sequence - Uses clique as most precise solution, used when anatomy template that - created files is not known. + Uses clique as most precise solution, used when anatomy template that + created files is not known. - Assumption is that frames are separated by '.', negative frames are not - allowed. + Assumption is that frames are separated by '.', negative frames are not + allowed. - Args: - files(list) or (set with single value): list of source paths - Returns: - (dict): {'/asset/subset_v001.0001.png': '0001', ....} + Args: + files(list) or (set with single value): list of source paths + + Returns: + (dict): {'/asset/subset_v001.0001.png': '0001', ....} + + Deprecated: + Function was moved to different location and will be removed + after 3.16.* release. """ from .path_tools import collect_frames @@ -77,7 +81,12 @@ def collect_frames(files): @deprecated("openpype.lib.path_tools.format_file_size") def sizeof_fmt(num, suffix=None): - """Returns formatted string with size in appropriate unit""" + """Returns formatted string with size in appropriate unit + + Deprecated: + Function was moved to different location and will be removed + after 3.16.* release. + """ from .path_tools import format_file_size return format_file_size(num, suffix) From 14dc209ab0a42d799cfa37eebd08d090666b537f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 29 Aug 2022 14:53:52 +0200 Subject: [PATCH 31/39] 'get_project_template_data' can access project name --- openpype/pipeline/template_data.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/template_data.py b/openpype/pipeline/template_data.py index 824a25127c..bab46a627d 100644 --- a/openpype/pipeline/template_data.py +++ b/openpype/pipeline/template_data.py @@ -28,27 +28,37 @@ def get_general_template_data(system_settings=None): } -def get_project_template_data(project_doc): +def get_project_template_data(project_doc=None, project_name=None): """Extract data from project document that are used in templates. Project document must have 'name' and (at this moment) optional key 'data.code'. + One of 'project_name' or 'project_doc' must be passed. With prepared + project document is function much faster because don't have to query. + Output contains formatting keys: - 'project[name]' - Project name - 'project[code]' - Project code Args: project_doc (Dict[str, Any]): Queried project document. + project_name (str): Name of project. Returns: Dict[str, Dict[str, str]]: Template data based on project document. """ + if not project_name: + project_name = project_doc["name"] + + if not project_doc: + project_code = get_project(project_name, fields=["data.code"]) + project_code = project_doc.get("data", {}).get("code") return { "project": { - "name": project_doc["name"], + "name": project_name, "code": project_code } } From e2060b9d65e4b7224fff916b4f492de0d015e9bf Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 29 Aug 2022 15:00:08 +0200 Subject: [PATCH 32/39] marked 'get_format_dict' as deprecated and moved it to pipeline delivery --- openpype/lib/delivery.py | 33 +++++++++---------- .../event_handlers_user/action_delivery.py | 4 ++- openpype/pipeline/delivery.py | 26 +++++++++++++++ openpype/plugins/load/delivery.py | 4 ++- 4 files changed, 47 insertions(+), 20 deletions(-) create mode 100644 openpype/pipeline/delivery.py diff --git a/openpype/lib/delivery.py b/openpype/lib/delivery.py index e09188d3bb..1e364c45d7 100644 --- a/openpype/lib/delivery.py +++ b/openpype/lib/delivery.py @@ -125,28 +125,25 @@ def copy_file(src_path, dst_path): shutil.copyfile(src_path, dst_path) +@deprecated("openpype.pipeline.delivery.get_format_dict") def get_format_dict(anatomy, location_path): """Returns replaced root values from user provider value. - Args: - anatomy (Anatomy) - location_path (str): user provided value - Returns: - (dict): prepared for formatting of a template + Args: + anatomy (Anatomy) + location_path (str): user provided value + + Returns: + (dict): prepared for formatting of a template + + Deprecated: + Function was moved to different location and will be removed + after 3.16.* release. """ - format_dict = {} - if location_path: - location_path = location_path.replace("\\", "/") - root_names = anatomy.root_names_from_templates( - anatomy.templates["delivery"] - ) - if root_names is None: - format_dict["root"] = location_path - else: - format_dict["root"] = {} - for name in root_names: - format_dict["root"][name] = location_path - return format_dict + + from openpype.pipeline.delivery import get_format_dict + + return get_format_dict(anatomy, location_path) def check_destination_path(repre_id, diff --git a/openpype/modules/ftrack/event_handlers_user/action_delivery.py b/openpype/modules/ftrack/event_handlers_user/action_delivery.py index 59a34b3f85..08d6e53078 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_delivery.py +++ b/openpype/modules/ftrack/event_handlers_user/action_delivery.py @@ -18,8 +18,10 @@ from openpype_modules.ftrack.lib.custom_attributes import ( from openpype.lib.dateutils import get_datetime_data from openpype.pipeline import Anatomy from openpype.pipeline.load import get_representation_path_with_anatomy -from openpype.lib.delivery import ( +from openpype.pipeline.delivery import ( get_format_dict, +) +from openpype.lib.delivery import ( check_destination_path, process_single_file, process_sequence diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py new file mode 100644 index 0000000000..03319f7ddc --- /dev/null +++ b/openpype/pipeline/delivery.py @@ -0,0 +1,26 @@ +"""Functions useful for delivery of published representations.""" + + +def get_format_dict(anatomy, location_path): + """Returns replaced root values from user provider value. + + Args: + anatomy (Anatomy): Project anatomy. + location_path (str): User provided value. + + Returns: + (dict): Prepared data for formatting of a template. + """ + + format_dict = {} + if not location_path: + return format_dict + + location_path = location_path.replace("\\", "/") + root_names = anatomy.root_names_from_templates( + anatomy.templates["delivery"] + ) + format_dict["root"] = {} + for name in root_names: + format_dict["root"][name] = location_path + return format_dict diff --git a/openpype/plugins/load/delivery.py b/openpype/plugins/load/delivery.py index 4651efd4a3..0ea62510a4 100644 --- a/openpype/plugins/load/delivery.py +++ b/openpype/plugins/load/delivery.py @@ -13,8 +13,10 @@ from openpype.lib import ( get_datetime_data, ) from openpype.pipeline.load import get_representation_path_with_anatomy -from openpype.lib.delivery import ( +from openpype.pipeline.delivery import ( get_format_dict, +) +from openpype.lib.delivery import ( check_destination_path, process_single_file, process_sequence, From fe566f4a4b1f5695e73731927601a814a330d8ae Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 29 Aug 2022 15:00:57 +0200 Subject: [PATCH 33/39] copied 'copy_file' to 'pipeline.delivery' and renamed to '_copy_file' --- openpype/pipeline/delivery.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index 03319f7ddc..5906892d59 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -1,4 +1,26 @@ """Functions useful for delivery of published representations.""" +import os +import shutil + +from openpype.lib import create_hard_link + + +def _copy_file(src_path, dst_path): + """Hardlink file if possible(to save space), copy if not. + + Because of using hardlinks should not be function used in other parts + of pipeline. + """ + + if os.path.exists(dst_path): + return + try: + create_hard_link( + src_path, + dst_path + ) + except OSError: + shutil.copyfile(src_path, dst_path) def get_format_dict(anatomy, location_path): From dc77d4a60908729f3b7ce343216b9af9852f1912 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 29 Aug 2022 15:04:03 +0200 Subject: [PATCH 34/39] marked 'check_destination_path' as deprecated and moved to pipeline.delivery --- openpype/lib/delivery.py | 65 +++++++------------ .../event_handlers_user/action_delivery.py | 2 +- openpype/pipeline/delivery.py | 61 +++++++++++++++++ openpype/plugins/load/delivery.py | 2 +- 4 files changed, 86 insertions(+), 44 deletions(-) diff --git a/openpype/lib/delivery.py b/openpype/lib/delivery.py index 1e364c45d7..543c3d12e5 100644 --- a/openpype/lib/delivery.py +++ b/openpype/lib/delivery.py @@ -3,7 +3,6 @@ import os import shutil import glob import clique -import collections import functools import warnings @@ -146,56 +145,38 @@ def get_format_dict(anatomy, location_path): return get_format_dict(anatomy, location_path) +@deprecated("openpype.pipeline.delivery.check_destination_path") def check_destination_path(repre_id, anatomy, anatomy_data, datetime_data, template_name): """ Try to create destination path based on 'template_name'. - In the case that path cannot be filled, template contains unmatched - keys, provide error message to filter out repre later. + In the case that path cannot be filled, template contains unmatched + keys, provide error message to filter out repre later. - Args: - anatomy (Anatomy) - anatomy_data (dict): context to fill anatomy - datetime_data (dict): values with actual date - template_name (str): to pick correct delivery template - Returns: - (collections.defauldict): {"TYPE_OF_ERROR":"ERROR_DETAIL"} + Args: + anatomy (Anatomy) + anatomy_data (dict): context to fill anatomy + datetime_data (dict): values with actual date + template_name (str): to pick correct delivery template + + Returns: + (collections.defauldict): {"TYPE_OF_ERROR":"ERROR_DETAIL"} + + Deprecated: + Function was moved to different location and will be removed + after 3.16.* release. """ - anatomy_data.update(datetime_data) - anatomy_filled = anatomy.format_all(anatomy_data) - dest_path = anatomy_filled["delivery"][template_name] - report_items = collections.defaultdict(list) - if not dest_path.solved: - msg = ( - "Missing keys in Representation's context" - " for anatomy template \"{}\"." - ).format(template_name) + from openpype.pipeline.delivery import check_destination_path - sub_msg = ( - "Representation: {}
" - ).format(repre_id) - - if dest_path.missing_keys: - keys = ", ".join(dest_path.missing_keys) - sub_msg += ( - "- Missing keys: \"{}\"
" - ).format(keys) - - if dest_path.invalid_types: - items = [] - for key, value in dest_path.invalid_types.items(): - items.append("\"{}\" {}".format(key, str(value))) - - keys = ", ".join(items) - sub_msg += ( - "- Invalid value DataType: \"{}\"
" - ).format(keys) - - report_items[msg].append(sub_msg) - - return report_items + return check_destination_path( + repre_id, + anatomy, + anatomy_data, + datetime_data, + template_name + ) def process_single_file( diff --git a/openpype/modules/ftrack/event_handlers_user/action_delivery.py b/openpype/modules/ftrack/event_handlers_user/action_delivery.py index 08d6e53078..8b314d8f1d 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_delivery.py +++ b/openpype/modules/ftrack/event_handlers_user/action_delivery.py @@ -20,9 +20,9 @@ from openpype.pipeline import Anatomy from openpype.pipeline.load import get_representation_path_with_anatomy from openpype.pipeline.delivery import ( get_format_dict, + check_destination_path, ) from openpype.lib.delivery import ( - check_destination_path, process_single_file, process_sequence ) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index 5906892d59..79667161a6 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -1,6 +1,7 @@ """Functions useful for delivery of published representations.""" import os import shutil +import collections from openpype.lib import create_hard_link @@ -46,3 +47,63 @@ def get_format_dict(anatomy, location_path): for name in root_names: format_dict["root"][name] = location_path return format_dict + + +def check_destination_path( + repre_id, + anatomy, + anatomy_data, + datetime_data, + template_name +): + """ Try to create destination path based on 'template_name'. + + In the case that path cannot be filled, template contains unmatched + keys, provide error message to filter out repre later. + + Args: + repre_id (str): Representation id. + anatomy (Anatomy): Project anatomy. + anatomy_data (dict): Template data to fill anatomy templates. + datetime_data (dict): Values with actual date. + template_name (str): Name of template which should be used from anatomy + templates. + Returns: + Dict[str, List[str]]: Report of happened errors. Key is message title + value is detailed information. + """ + + anatomy_data.update(datetime_data) + anatomy_filled = anatomy.format_all(anatomy_data) + dest_path = anatomy_filled["delivery"][template_name] + report_items = collections.defaultdict(list) + + if not dest_path.solved: + msg = ( + "Missing keys in Representation's context" + " for anatomy template \"{}\"." + ).format(template_name) + + sub_msg = ( + "Representation: {}
" + ).format(repre_id) + + if dest_path.missing_keys: + keys = ", ".join(dest_path.missing_keys) + sub_msg += ( + "- Missing keys: \"{}\"
" + ).format(keys) + + if dest_path.invalid_types: + items = [] + for key, value in dest_path.invalid_types.items(): + items.append("\"{}\" {}".format(key, str(value))) + + keys = ", ".join(items) + sub_msg += ( + "- Invalid value DataType: \"{}\"
" + ).format(keys) + + report_items[msg].append(sub_msg) + + return report_items diff --git a/openpype/plugins/load/delivery.py b/openpype/plugins/load/delivery.py index 0ea62510a4..1161636cb7 100644 --- a/openpype/plugins/load/delivery.py +++ b/openpype/plugins/load/delivery.py @@ -15,9 +15,9 @@ from openpype.lib import ( from openpype.pipeline.load import get_representation_path_with_anatomy from openpype.pipeline.delivery import ( get_format_dict, + check_destination_path, ) from openpype.lib.delivery import ( - check_destination_path, process_single_file, process_sequence, ) From eaff50b23e29dcb142557e3068c3031e1f1e268a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 29 Aug 2022 15:07:45 +0200 Subject: [PATCH 35/39] Marked 'process_single_file' as deprecated and moved to pipeline delivery as 'deliver_single_file' --- openpype/lib/delivery.py | 61 +++++++------------ .../event_handlers_user/action_delivery.py | 4 +- openpype/pipeline/delivery.py | 59 ++++++++++++++++++ openpype/plugins/load/delivery.py | 6 +- 4 files changed, 87 insertions(+), 43 deletions(-) diff --git a/openpype/lib/delivery.py b/openpype/lib/delivery.py index 543c3d12e5..455401d0fd 100644 --- a/openpype/lib/delivery.py +++ b/openpype/lib/delivery.py @@ -179,53 +179,38 @@ def check_destination_path(repre_id, ) +@deprecated("openpype.pipeline.delivery.deliver_single_file") def process_single_file( src_path, repre, anatomy, template_name, anatomy_data, format_dict, report_items, log ): """Copy single file to calculated path based on template - Args: - src_path(str): path of source representation file - _repre (dict): full repre, used only in process_sequence, here only - as to share same signature - anatomy (Anatomy) - template_name (string): user selected delivery template name - anatomy_data (dict): data from repre to fill anatomy with - format_dict (dict): root dictionary with names and values - report_items (collections.defaultdict): to return error messages - log (Logger): for log printing - Returns: - (collections.defaultdict , int) + Args: + src_path(str): path of source representation file + _repre (dict): full repre, used only in process_sequence, here only + as to share same signature + anatomy (Anatomy) + template_name (string): user selected delivery template name + anatomy_data (dict): data from repre to fill anatomy with + format_dict (dict): root dictionary with names and values + report_items (collections.defaultdict): to return error messages + log (Logger): for log printing + + Returns: + (collections.defaultdict , int) + + Deprecated: + Function was moved to different location and will be removed + after 3.16.* release. """ - # Make sure path is valid for all platforms - src_path = os.path.normpath(src_path.replace("\\", "/")) - if not os.path.exists(src_path): - msg = "{} doesn't exist for {}".format(src_path, repre["_id"]) - report_items["Source file was not found"].append(msg) - return report_items, 0 + from openpype.pipeline.delivery import deliver_single_file - anatomy_filled = anatomy.format(anatomy_data) - if format_dict: - template_result = anatomy_filled["delivery"][template_name] - delivery_path = template_result.rootless.format(**format_dict) - else: - delivery_path = anatomy_filled["delivery"][template_name] - - # Backwards compatibility when extension contained `.` - delivery_path = delivery_path.replace("..", ".") - # Make sure path is valid for all platforms - delivery_path = os.path.normpath(delivery_path.replace("\\", "/")) - - delivery_folder = os.path.dirname(delivery_path) - if not os.path.exists(delivery_folder): - os.makedirs(delivery_folder) - - log.debug("Copying single: {} -> {}".format(src_path, delivery_path)) - copy_file(src_path, delivery_path) - - return report_items, 1 + return deliver_single_file( + src_path, repre, anatomy, template_name, anatomy_data, format_dict, + report_items, log + ) def process_sequence( diff --git a/openpype/modules/ftrack/event_handlers_user/action_delivery.py b/openpype/modules/ftrack/event_handlers_user/action_delivery.py index 8b314d8f1d..fe91670c3d 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_delivery.py +++ b/openpype/modules/ftrack/event_handlers_user/action_delivery.py @@ -21,9 +21,9 @@ from openpype.pipeline.load import get_representation_path_with_anatomy from openpype.pipeline.delivery import ( get_format_dict, check_destination_path, + deliver_single_file, ) from openpype.lib.delivery import ( - process_single_file, process_sequence ) @@ -596,7 +596,7 @@ class Delivery(BaseAction): self.log ) if not frame: - process_single_file(*args) + deliver_single_file(*args) else: process_sequence(*args) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index 79667161a6..7c5121aa53 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -1,6 +1,8 @@ """Functions useful for delivery of published representations.""" import os import shutil +import glob +import clique import collections from openpype.lib import create_hard_link @@ -107,3 +109,60 @@ def check_destination_path( report_items[msg].append(sub_msg) return report_items + + +def deliver_single_file( + src_path, + repre, + anatomy, + template_name, + anatomy_data, + format_dict, + report_items, + log +): + """Copy single file to calculated path based on template + + Args: + src_path(str): path of source representation file + repre (dict): full repre, used only in process_sequence, here only + as to share same signature + anatomy (Anatomy) + template_name (string): user selected delivery template name + anatomy_data (dict): data from repre to fill anatomy with + format_dict (dict): root dictionary with names and values + report_items (collections.defaultdict): to return error messages + log (logging.Logger): for log printing + + Returns: + (collections.defaultdict, int) + """ + + # Make sure path is valid for all platforms + src_path = os.path.normpath(src_path.replace("\\", "/")) + + if not os.path.exists(src_path): + msg = "{} doesn't exist for {}".format(src_path, repre["_id"]) + report_items["Source file was not found"].append(msg) + return report_items, 0 + + anatomy_filled = anatomy.format(anatomy_data) + if format_dict: + template_result = anatomy_filled["delivery"][template_name] + delivery_path = template_result.rootless.format(**format_dict) + else: + delivery_path = anatomy_filled["delivery"][template_name] + + # Backwards compatibility when extension contained `.` + delivery_path = delivery_path.replace("..", ".") + # Make sure path is valid for all platforms + delivery_path = os.path.normpath(delivery_path.replace("\\", "/")) + + delivery_folder = os.path.dirname(delivery_path) + if not os.path.exists(delivery_folder): + os.makedirs(delivery_folder) + + log.debug("Copying single: {} -> {}".format(src_path, delivery_path)) + _copy_file(src_path, delivery_path) + + return report_items, 1 diff --git a/openpype/plugins/load/delivery.py b/openpype/plugins/load/delivery.py index 1161636cb7..a028ac0a87 100644 --- a/openpype/plugins/load/delivery.py +++ b/openpype/plugins/load/delivery.py @@ -16,9 +16,9 @@ from openpype.pipeline.load import get_representation_path_with_anatomy from openpype.pipeline.delivery import ( get_format_dict, check_destination_path, + deliver_single_file, ) from openpype.lib.delivery import ( - process_single_file, process_sequence, ) @@ -208,7 +208,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): args[0] = src_path if frame: anatomy_data["frame"] = frame - new_report_items, uploaded = process_single_file(*args) + new_report_items, uploaded = deliver_single_file(*args) report_items.update(new_report_items) self._update_progress(uploaded) else: # fallback for Pype2 and representations without files @@ -217,7 +217,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): repre["context"]["frame"] = len(str(frame)) * "#" if not frame: - new_report_items, uploaded = process_single_file(*args) + new_report_items, uploaded = deliver_single_file(*args) else: new_report_items, uploaded = process_sequence(*args) report_items.update(new_report_items) From d3a7637d1561a411a40a7b74494d692e292f5c4b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 29 Aug 2022 15:10:44 +0200 Subject: [PATCH 36/39] Marked 'process_sequence' as deprecated and moved to pipeline delivery as 'deliver_sequence' --- openpype/lib/delivery.py | 144 +++--------------- .../event_handlers_user/action_delivery.py | 6 +- openpype/pipeline/delivery.py | 144 +++++++++++++++++- openpype/plugins/load/delivery.py | 6 +- 4 files changed, 172 insertions(+), 128 deletions(-) diff --git a/openpype/lib/delivery.py b/openpype/lib/delivery.py index 455401d0fd..d44a4edb3f 100644 --- a/openpype/lib/delivery.py +++ b/openpype/lib/delivery.py @@ -213,6 +213,7 @@ def process_single_file( ) +@deprecated("openpype.pipeline.delivery.deliver_sequence") def process_sequence( src_path, repre, anatomy, template_name, anatomy_data, format_dict, report_items, log @@ -220,128 +221,33 @@ def process_sequence( """ For Pype2(mainly - works in 3 too) where representation might not contain files. - Uses listing physical files (not 'files' on repre as a)might not be - present, b)might not be reliable for representation and copying them. + Uses listing physical files (not 'files' on repre as a)might not be + present, b)might not be reliable for representation and copying them. - TODO Should be refactored when files are sufficient to drive all - representations. + TODO Should be refactored when files are sufficient to drive all + representations. - Args: - src_path(str): path of source representation file - repre (dict): full representation - anatomy (Anatomy) - template_name (string): user selected delivery template name - anatomy_data (dict): data from repre to fill anatomy with - format_dict (dict): root dictionary with names and values - report_items (collections.defaultdict): to return error messages - log (Logger): for log printing - Returns: - (collections.defaultdict , int) + Args: + src_path(str): path of source representation file + repre (dict): full representation + anatomy (Anatomy) + template_name (string): user selected delivery template name + anatomy_data (dict): data from repre to fill anatomy with + format_dict (dict): root dictionary with names and values + report_items (collections.defaultdict): to return error messages + log (Logger): for log printing + + Returns: + (collections.defaultdict , int) + + Deprecated: + Function was moved to different location and will be removed + after 3.16.* release. """ - src_path = os.path.normpath(src_path.replace("\\", "/")) - def hash_path_exist(myPath): - res = myPath.replace('#', '*') - glob_search_results = glob.glob(res) - if len(glob_search_results) > 0: - return True - return False + from openpype.pipeline.delivery import deliver_sequence - if not hash_path_exist(src_path): - msg = "{} doesn't exist for {}".format(src_path, - repre["_id"]) - report_items["Source file was not found"].append(msg) - return report_items, 0 - - delivery_templates = anatomy.templates.get("delivery") or {} - delivery_template = delivery_templates.get(template_name) - if delivery_template is None: - msg = ( - "Delivery template \"{}\" in anatomy of project \"{}\"" - " was not found" - ).format(template_name, anatomy.project_name) - report_items[""].append(msg) - return report_items, 0 - - # Check if 'frame' key is available in template which is required - # for sequence delivery - if "{frame" not in delivery_template: - msg = ( - "Delivery template \"{}\" in anatomy of project \"{}\"" - "does not contain '{{frame}}' key to fill. Delivery of sequence" - " can't be processed." - ).format(template_name, anatomy.project_name) - report_items[""].append(msg) - return report_items, 0 - - dir_path, file_name = os.path.split(str(src_path)) - - context = repre["context"] - ext = context.get("ext", context.get("representation")) - - if not ext: - msg = "Source extension not found, cannot find collection" - report_items[msg].append(src_path) - log.warning("{} <{}>".format(msg, context)) - return report_items, 0 - - ext = "." + ext - # context.representation could be .psd - ext = ext.replace("..", ".") - - src_collections, remainder = clique.assemble(os.listdir(dir_path)) - src_collection = None - for col in src_collections: - if col.tail != ext: - continue - - src_collection = col - break - - if src_collection is None: - msg = "Source collection of files was not found" - report_items[msg].append(src_path) - log.warning("{} <{}>".format(msg, src_path)) - return report_items, 0 - - frame_indicator = "@####@" - - anatomy_data["frame"] = frame_indicator - anatomy_filled = anatomy.format(anatomy_data) - - if format_dict: - template_result = anatomy_filled["delivery"][template_name] - delivery_path = template_result.rootless.format(**format_dict) - else: - delivery_path = anatomy_filled["delivery"][template_name] - - delivery_path = os.path.normpath(delivery_path.replace("\\", "/")) - delivery_folder = os.path.dirname(delivery_path) - dst_head, dst_tail = delivery_path.split(frame_indicator) - dst_padding = src_collection.padding - dst_collection = clique.Collection( - head=dst_head, - tail=dst_tail, - padding=dst_padding + return deliver_sequence( + src_path, repre, anatomy, template_name, anatomy_data, format_dict, + report_items, log ) - - if not os.path.exists(delivery_folder): - os.makedirs(delivery_folder) - - src_head = src_collection.head - src_tail = src_collection.tail - uploaded = 0 - for index in src_collection.indexes: - src_padding = src_collection.format("{padding}") % index - src_file_name = "{}{}{}".format(src_head, src_padding, src_tail) - src = os.path.normpath( - os.path.join(dir_path, src_file_name) - ) - - dst_padding = dst_collection.format("{padding}") % index - dst = "{}{}{}".format(dst_head, dst_padding, dst_tail) - log.debug("Copying single: {} -> {}".format(src, dst)) - copy_file(src, dst) - uploaded += 1 - - return report_items, uploaded diff --git a/openpype/modules/ftrack/event_handlers_user/action_delivery.py b/openpype/modules/ftrack/event_handlers_user/action_delivery.py index fe91670c3d..a400c8f5f0 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_delivery.py +++ b/openpype/modules/ftrack/event_handlers_user/action_delivery.py @@ -22,9 +22,7 @@ from openpype.pipeline.delivery import ( get_format_dict, check_destination_path, deliver_single_file, -) -from openpype.lib.delivery import ( - process_sequence + deliver_sequence, ) @@ -598,7 +596,7 @@ class Delivery(BaseAction): if not frame: deliver_single_file(*args) else: - process_sequence(*args) + deliver_sequence(*args) return self.report(report_items) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index 7c5121aa53..8cf9a43aac 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -125,7 +125,7 @@ def deliver_single_file( Args: src_path(str): path of source representation file - repre (dict): full repre, used only in process_sequence, here only + repre (dict): full repre, used only in deliver_sequence, here only as to share same signature anatomy (Anatomy) template_name (string): user selected delivery template name @@ -166,3 +166,145 @@ def deliver_single_file( _copy_file(src_path, delivery_path) return report_items, 1 + + +def deliver_sequence( + src_path, + repre, + anatomy, + template_name, + anatomy_data, + format_dict, + report_items, + log +): + """ For Pype2(mainly - works in 3 too) where representation might not + contain files. + + Uses listing physical files (not 'files' on repre as a)might not be + present, b)might not be reliable for representation and copying them. + + TODO Should be refactored when files are sufficient to drive all + representations. + + Args: + src_path(str): path of source representation file + repre (dict): full representation + anatomy (Anatomy) + template_name (string): user selected delivery template name + anatomy_data (dict): data from repre to fill anatomy with + format_dict (dict): root dictionary with names and values + report_items (collections.defaultdict): to return error messages + log (logging.Logger): for log printing + + Returns: + (collections.defaultdict, int) + """ + + src_path = os.path.normpath(src_path.replace("\\", "/")) + + def hash_path_exist(myPath): + res = myPath.replace('#', '*') + glob_search_results = glob.glob(res) + if len(glob_search_results) > 0: + return True + return False + + if not hash_path_exist(src_path): + msg = "{} doesn't exist for {}".format( + src_path, repre["_id"]) + report_items["Source file was not found"].append(msg) + return report_items, 0 + + delivery_templates = anatomy.templates.get("delivery") or {} + delivery_template = delivery_templates.get(template_name) + if delivery_template is None: + msg = ( + "Delivery template \"{}\" in anatomy of project \"{}\"" + " was not found" + ).format(template_name, anatomy.project_name) + report_items[""].append(msg) + return report_items, 0 + + # Check if 'frame' key is available in template which is required + # for sequence delivery + if "{frame" not in delivery_template: + msg = ( + "Delivery template \"{}\" in anatomy of project \"{}\"" + "does not contain '{{frame}}' key to fill. Delivery of sequence" + " can't be processed." + ).format(template_name, anatomy.project_name) + report_items[""].append(msg) + return report_items, 0 + + dir_path, file_name = os.path.split(str(src_path)) + + context = repre["context"] + ext = context.get("ext", context.get("representation")) + + if not ext: + msg = "Source extension not found, cannot find collection" + report_items[msg].append(src_path) + log.warning("{} <{}>".format(msg, context)) + return report_items, 0 + + ext = "." + ext + # context.representation could be .psd + ext = ext.replace("..", ".") + + src_collections, remainder = clique.assemble(os.listdir(dir_path)) + src_collection = None + for col in src_collections: + if col.tail != ext: + continue + + src_collection = col + break + + if src_collection is None: + msg = "Source collection of files was not found" + report_items[msg].append(src_path) + log.warning("{} <{}>".format(msg, src_path)) + return report_items, 0 + + frame_indicator = "@####@" + + anatomy_data["frame"] = frame_indicator + anatomy_filled = anatomy.format(anatomy_data) + + if format_dict: + template_result = anatomy_filled["delivery"][template_name] + delivery_path = template_result.rootless.format(**format_dict) + else: + delivery_path = anatomy_filled["delivery"][template_name] + + delivery_path = os.path.normpath(delivery_path.replace("\\", "/")) + delivery_folder = os.path.dirname(delivery_path) + dst_head, dst_tail = delivery_path.split(frame_indicator) + dst_padding = src_collection.padding + dst_collection = clique.Collection( + head=dst_head, + tail=dst_tail, + padding=dst_padding + ) + + if not os.path.exists(delivery_folder): + os.makedirs(delivery_folder) + + src_head = src_collection.head + src_tail = src_collection.tail + uploaded = 0 + for index in src_collection.indexes: + src_padding = src_collection.format("{padding}") % index + src_file_name = "{}{}{}".format(src_head, src_padding, src_tail) + src = os.path.normpath( + os.path.join(dir_path, src_file_name) + ) + + dst_padding = dst_collection.format("{padding}") % index + dst = "{}{}{}".format(dst_head, dst_padding, dst_tail) + log.debug("Copying single: {} -> {}".format(src, dst)) + _copy_file(src, dst) + uploaded += 1 + + return report_items, uploaded diff --git a/openpype/plugins/load/delivery.py b/openpype/plugins/load/delivery.py index a028ac0a87..89c24f2402 100644 --- a/openpype/plugins/load/delivery.py +++ b/openpype/plugins/load/delivery.py @@ -17,9 +17,7 @@ from openpype.pipeline.delivery import ( get_format_dict, check_destination_path, deliver_single_file, -) -from openpype.lib.delivery import ( - process_sequence, + deliver_sequence, ) @@ -219,7 +217,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): if not frame: new_report_items, uploaded = deliver_single_file(*args) else: - new_report_items, uploaded = process_sequence(*args) + new_report_items, uploaded = deliver_sequence(*args) report_items.update(new_report_items) self._update_progress(uploaded) From 19c7d2b8a150d4d052db9a9b4a813bd2922b272f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 29 Aug 2022 15:20:21 +0200 Subject: [PATCH 37/39] marked 'copy_file' as deprecated --- openpype/lib/delivery.py | 3 +-- openpype/lib/path_tools.py | 11 ++++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/lib/delivery.py b/openpype/lib/delivery.py index d44a4edb3f..efb542de75 100644 --- a/openpype/lib/delivery.py +++ b/openpype/lib/delivery.py @@ -1,8 +1,6 @@ """Functions useful for delivery action or loader""" import os import shutil -import glob -import clique import functools import warnings @@ -109,6 +107,7 @@ def path_from_representation(representation, anatomy): return get_representation_path_with_anatomy(representation, anatomy) +@deprecated def copy_file(src_path, dst_path): """Hardlink file if possible(to save space), copy if not""" from openpype.lib import create_hard_link # safer importing diff --git a/openpype/lib/path_tools.py b/openpype/lib/path_tools.py index 45aa54d6cb..1835c71644 100644 --- a/openpype/lib/path_tools.py +++ b/openpype/lib/path_tools.py @@ -179,12 +179,12 @@ def get_version_from_path(file): """Find version number in file path string. Args: - file (string): file path + file (str): file path Returns: - v: version number in string ('001') - + str: version number in string ('001') """ + pattern = re.compile(r"[\._]v([0-9]+)", re.IGNORECASE) try: return pattern.findall(file)[-1] @@ -200,16 +200,17 @@ def get_last_version_from_path(path_dir, filter): """Find last version of given directory content. Args: - path_dir (string): directory path + path_dir (str): directory path filter (list): list of strings used as file name filter Returns: - string: file name with last version + str: file name with last version Example: last_version_file = get_last_version_from_path( "/project/shots/shot01/work", ["shot01", "compositing", "nk"]) """ + assert os.path.isdir(path_dir), "`path_dir` argument needs to be directory" assert isinstance(filter, list) and ( len(filter) != 0), "`filter` argument needs to be list and not empty" From cd09f23b968ee0162441c388172ec0027e825a5e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 29 Aug 2022 16:44:51 +0200 Subject: [PATCH 38/39] removed unused import --- openpype/plugins/load/delete_old_versions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/load/delete_old_versions.py b/openpype/plugins/load/delete_old_versions.py index 8c8546d9c8..b7ac015268 100644 --- a/openpype/plugins/load/delete_old_versions.py +++ b/openpype/plugins/load/delete_old_versions.py @@ -10,7 +10,7 @@ from Qt import QtWidgets, QtCore from openpype import style from openpype.client import get_versions, get_representations from openpype.modules import ModulesManager -from openpype.lib import StringTemplate, format_file_size +from openpype.lib import format_file_size from openpype.pipeline import load, AvalonMongoDB, Anatomy from openpype.pipeline.load import ( get_representation_path_with_anatomy, From b14bb4b91e2bf7053a3e5d057f6b54d479535072 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 29 Aug 2022 17:23:22 +0200 Subject: [PATCH 39/39] Fix typo for Maya argument `with_focus` -> `withFocus` --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 2 +- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 54ef09e060..871adda0c3 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -128,7 +128,7 @@ class ExtractPlayblast(openpype.api.Extractor): # Update preset with current panel setting # if override_viewport_options is turned off if not override_viewport_options: - panel = cmds.getPanel(with_focus=True) + panel = cmds.getPanel(withFocus=True) panel_preset = capture.parse_active_view() preset.update(panel_preset) cmds.setFocus(panel) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 01980578cf..9380da5128 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -100,9 +100,9 @@ class ExtractThumbnail(openpype.api.Extractor): # camera. if preset.pop("isolate_view", False) and instance.data.get("isolate"): preset["isolate"] = instance.data["setMembers"] - + # Show or Hide Image Plane - image_plane = instance.data.get("imagePlane", True) + image_plane = instance.data.get("imagePlane", True) if "viewport_options" in preset: preset["viewport_options"]["imagePlane"] = image_plane else: @@ -117,7 +117,7 @@ class ExtractThumbnail(openpype.api.Extractor): # Update preset with current panel setting # if override_viewport_options is turned off if not override_viewport_options: - panel = cmds.getPanel(with_focus=True) + panel = cmds.getPanel(withFocus=True) panel_preset = capture.parse_active_view() preset.update(panel_preset) cmds.setFocus(panel)