From 1dd3c00eb5802254a21bd5d7ea51f562a93ac366 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 10 Dec 2024 23:02:10 +0100 Subject: [PATCH] :bug: fix template data --- .../plugins/publish/integrate_traits.py | 104 +++++++++--- .../plugins/publish/test_integrate_traits.py | 156 ++++++++++++++++-- 2 files changed, 219 insertions(+), 41 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate_traits.py b/client/ayon_core/plugins/publish/integrate_traits.py index 447be6665c..a816a42c3f 100644 --- a/client/ayon_core/plugins/publish/integrate_traits.py +++ b/client/ayon_core/plugins/publish/integrate_traits.py @@ -3,6 +3,7 @@ from __future__ import annotations import contextlib import copy +from copy import deepcopy from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING, Any, List @@ -47,8 +48,8 @@ if TYPE_CHECKING: from ayon_core.pipeline import Anatomy from ayon_core.pipeline.anatomy.templates import ( - TemplateItem as AnatomyTemplateItem, - ) + TemplateItem as AnatomyTemplateItem, AnatomyStringTemplate, +) @dataclass(frozen=True) @@ -203,23 +204,43 @@ class IntegrateTraits(pyblish.api.InstancePlugin): "Instance has no persistent representations. Skipping") return - # 3) get template and template data - template: str = self.get_publish_template(instance) - - # 4) initialize OperationsSession() op_session = OperationsSession() - # 5) Prepare product product_entity = self.prepare_product(instance, op_session) - # 6) Prepare version version_entity = self.prepare_version( instance, op_session, product_entity ) instance.data["versionEntity"] = version_entity - instance_template_data: dict[str, str] = {} + transfers = self.get_transfers_from_representations( + instance, representations) + def get_transfers_from_representations( + self, + instance: pyblish.api.Instance, + representations: list[Representation]) -> list[TransferItem]: + """Get transfers from representations. + + This method will go through all representations and prepare transfers + based on the traits they contain. First it will validate the + representation, and then it will prepare template data for the + representation. It specifically handles FileLocations, FileLocation, + Bundle, Sequence and UDIM traits. + + Args: + instance (pyblish.api.Instance): Instance to process. + representations (list[Representation]): List of representations. + + Returns: + list[TransferItem]: List of transfers. + + Raises: + PublishError: If representation is invalid. + + """ + template: str = self.get_publish_template(instance) + instance_template_data: dict[str, str] = {} transfers: list[TransferItem] = [] # prepare template and data to format it for representation in representations: @@ -235,6 +256,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): template_data = self.get_template_data_from_representation( representation, instance) # add instance based template data + template_data.update(instance_template_data) # treat Variant as `output` in template data @@ -246,7 +268,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): template_item = TemplateItem( anatomy=instance.context.data["anatomy"], template=template, - template_data=template_data, + template_data=copy.deepcopy(template_data), template_object=self.get_publish_template_object(instance), ) @@ -273,7 +295,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): self.get_transfers_from_bundle( representation, template_item, transfers ) - + return transfers def _get_relative_to_root_original_dirname( self, instance: pyblish.api.Instance) -> str: @@ -668,6 +690,8 @@ class IntegrateTraits(pyblish.api.InstancePlugin): """ template_data = copy.deepcopy(instance.data["anatomyData"]) template_data["representation"] = representation.name + template_data["version"] = instance.data["version"] + template_data["hierarchy"] = instance.data["hierarchy"] # add colorspace data to template data if representation.contains_trait(ColorManaged): @@ -770,23 +794,26 @@ class IntegrateTraits(pyblish.api.InstancePlugin): if template_padding > dst_padding: dst_padding = template_padding - # add template path and the data to resolve it - representation.add_trait(TemplatePath( - template=template_item.template, - data=template_item.template_data - )) - # go through all frames in the sequence # find their corresponding file locations # format their template and add them to transfers for frame in frames: - template_item.template_data["frame"] = frame - template_filled = path_template_object.format_strict( - template_item.template_data - ) file_loc: FileLocation = representation.get_trait( FileLocations).get_file_location_for_frame( frame, sequence) + + template_item.template_data["frame"] = frame + template_item.template_data["ext"] = ( + file_loc.file_path.suffix + ) + template_filled = path_template_object.format_strict( + template_item.template_data + ) + + # add used values to the template data + used_values: dict = template_filled.used_values + template_item.template_data.update(used_values) + transfers.append( TransferItem( source=file_loc.file_path, @@ -799,6 +826,12 @@ class IntegrateTraits(pyblish.api.InstancePlugin): ) ) + # add template path and the data to resolve it + representation.add_trait(TemplatePath( + template=template_item.template, + data=template_item.template_data + )) + @staticmethod def get_transfers_from_udim( @@ -819,13 +852,23 @@ class IntegrateTraits(pyblish.api.InstancePlugin): """ udim: UDIM = representation.get_trait(UDIM) + path_template_object: AnatomyStringTemplate = ( + template_item.template_object["path"] + ) for file_loc in representation.get_trait( FileLocations).file_paths: template_item.template_data["udim"] = ( udim.get_udim_from_file_location(file_loc) ) - template_filled = template_item.template.format( - **template_item.template_data) + + template_filled = path_template_object.format_strict( + template_item.template_data + ) + + # add used values to the template data + used_values: dict = template_filled.used_values + template_item.template_data.update(used_values) + transfers.append( TransferItem( source=file_loc.file_path, @@ -861,7 +904,12 @@ class IntegrateTraits(pyblish.api.InstancePlugin): template_item (TemplateItem): Template item. """ - path_template_object = template_item.template_object["path"] + path_template_object: AnatomyStringTemplate = ( + template_item.template_object["path"] + ) + template_item.template_data["ext"] = ( + representation.get_trait(FileLocation).file_path.suffix + ) template_item.template_data.pop("frame", None) with contextlib.suppress(MissingTraitError): udim = representation.get_trait(UDIM) @@ -870,6 +918,11 @@ class IntegrateTraits(pyblish.api.InstancePlugin): template_filled = path_template_object.format_strict( template_item.template_data ) + + # add used values to the template data + used_values: dict = template_filled.used_values + template_item.template_data.update(used_values) + file_loc: FileLocation = representation.get_trait(FileLocation) transfers.append( TransferItem( @@ -878,7 +931,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): size=file_loc.file_size, checksum=file_loc.file_hash, template=template_item.template, - template_data=template_item.template_data.copy(), + template_data=template_item.template_data, representation=representation, ) ) @@ -928,3 +981,4 @@ class IntegrateTraits(pyblish.api.InstancePlugin): IntegrateTraits.get_transfers_from_bundle( sub_representation, template_item, transfers ) + diff --git a/tests/client/ayon_core/plugins/publish/test_integrate_traits.py b/tests/client/ayon_core/plugins/publish/test_integrate_traits.py index 11922f6c9a..a54d0a3cfa 100644 --- a/tests/client/ayon_core/plugins/publish/test_integrate_traits.py +++ b/tests/client/ayon_core/plugins/publish/test_integrate_traits.py @@ -2,10 +2,16 @@ from __future__ import annotations import base64 +import time from pathlib import Path import pyblish.api import pytest +import pytest_ayon +from ayon_api.operations import ( + OperationsSession, +) +from ayon_core.pipeline.anatomy import Anatomy from ayon_core.pipeline.traits import ( FileLocation, FileLocations, @@ -18,6 +24,7 @@ from ayon_core.pipeline.traits import ( Sequence, Transient, ) +from ayon_core.pipeline.version_start import get_versioning_start # Tagged, # TemplatePath, @@ -26,6 +33,8 @@ from ayon_core.settings import get_project_settings PNG_FILE_B64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAAAACklEQVR4AWNgAAAAAgABc3UBGAAAAABJRU5ErkJggg==" # noqa: E501 SEQUENCE_LENGTH = 10 +CURRENT_TIME = time.time() + @pytest.fixture(scope="session") def single_file(tmp_path_factory: pytest.TempPathFactory) -> Path: @@ -48,7 +57,7 @@ def sequence_files(tmp_path_factory: pytest.TempPathFactory) -> list[Path]: @pytest.fixture() def mock_context( - project: object, + project: pytest_ayon.ProjectInfo, single_file: Path, sequence_files: list[Path]) -> pyblish.api.Context: """Return a mock instance. @@ -56,18 +65,6 @@ def mock_context( This is mocking pyblish context for testing. It is using real AYON project thanks to the ``project`` fixture. - This returns following data:: - - project_name: str - project_code: str - project_root_folders: dict[str, str] - folder: IdNamePair - task: IdNamePair - product: IdNamePair - version: IdNamePair - representations: List[IdNamePair] - links: List[str] - Args: project (object): The project info. It is `ProjectInfo` object returned by pytest fixture. @@ -75,25 +72,68 @@ def mock_context( sequence_files (list[Path]): The paths to a sequence of image files. """ + anatomy = Anatomy(project.project_name) context = pyblish.api.Context() context.data["projectName"] = project.project_name context.data["hostName"] = "test_host" context.data["project_settings"] = get_project_settings( project.project_name) + context.data["anatomy"] = anatomy + context.data["time"] = CURRENT_TIME + context.data["user"] = "test_user" + context.data["machine"] = "test_machine" + context.data["fps"] = 25 instance = context.create_instance("mock_instance") + instance.data["source"] = "test_source" + instance.data["families"] = ["render"] instance.data["anatomyData"] = { - "project": project.project_name, + "project": { + "name": project.project_name, + "code": project.project_code + }, "task": { "name": project.task.name, "type": "test" # pytest-ayon doesn't return the task type yet - } + }, + "folder": { + "name": project.folder.name, + "type": "test" # pytest-ayon doesn't return the folder type yet + }, + "product": { + "name": project.product.name, + "type": "test" # pytest-ayon doesn't return the product type yet + }, + } + instance.data["folderEntity"] = project.folder_entity instance.data["productType"] = "test_product" + instance.data["productName"] = project.product.name + instance.data["anatomy"] = anatomy + instance.data["comment"] = "test_comment" instance.data["integrate"] = True instance.data["farm"] = False + parents = project.folder_entity["path"].lstrip("/").split("/") + + hierarchy = "" + if parents: + hierarchy = "/".join(parents) + + instance.data["hierarchy"] = hierarchy + + version_number = get_versioning_start( + context.data["projectName"], + instance.context.data["hostName"], + task_name=project.task.name, + task_type="test", + product_type=instance.data["productType"], + product_name=instance.data["productName"] + ) + + instance.data["version"] = version_number + _file_size = len(base64.b64decode(PNG_FILE_B64)) file_locations = [ FileLocation( @@ -121,7 +161,7 @@ def mock_context( ), Sequence( frame_padding=4, - frame_regex=r"^img\.(\d{4})\.png$", + frame_regex=r"^img\.(?P\d{4})\.png$", ), FileLocations( file_paths=file_locations, @@ -176,3 +216,87 @@ def test_filter_lifecycle() -> None: assert len(filtered) == 1 assert filtered[0] == persistent_representation + + +def test_prepare_product( + project: pytest_ayon.ProjectInfo, + mock_context: pyblish.api.Context) -> None: + """Test prepare_product.""" + integrator = IntegrateTraits() + op_session = OperationsSession() + product = integrator.prepare_product(mock_context[0], op_session) + + assert product == { + "attrib": {}, + "data": { + "families": ["default", "render"], + }, + "folderId": project.folder_entity["id"], + "name": "renderMain", + "productType": "test_product", + "id": project.product_entity["id"], + } + +def test_prepare_version( + project: pytest_ayon.ProjectInfo, + mock_context: pyblish.api.Context) -> None: + """Test prepare_version.""" + integrator = IntegrateTraits() + op_session = OperationsSession() + product = integrator.prepare_product(mock_context[0], op_session) + version = integrator.prepare_version( + mock_context[0], op_session , product) + + assert version == { + "attrib": { + "comment": "test_comment", + "families": ["default", "render"], + "fps": 25, + "machine": "test_machine", + "source": "test_source", + }, + "data": { + "author": "test_user", + "time": CURRENT_TIME, + }, + "id": project.version_entity["id"], + "productId": project.product_entity["id"], + "version": 1, + } + + +def test_get_transfers_from_representation( + mock_context: pyblish.api.Context) -> None: + """Test get_transfers_from_representation.""" + integrator = IntegrateTraits() + + instance = mock_context[0] + representations: list[Representation] = instance.data[ + "representations_with_traits"] + transfers = integrator.get_transfers_from_representations( + instance, representations) + + assert transfers == [ + { + "file_path": Path("test"), + "file_size": 1234, + "traits": [ + "Persistent", + "Image", + "MimeType" + ] + }, + { + "file_path": Path("test"), + "file_size": 1234, + "traits": [ + "Persistent", + "FrameRanged", + "Sequence", + "FileLocations", + "Image", + "PixelBased", + "MimeType" + ] + } + ]