From 92cc5cd06b1cffe2308866607bc2b5756d3d127c Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 19 Mar 2025 18:47:09 +0100 Subject: [PATCH] :bug: replace published path in traits after copy and add some more tests --- .../plugins/publish/integrate_traits.py | 59 +++++++++++++-- .../plugins/publish/test_integrate_traits.py | 71 +++++++++++++++++++ 2 files changed, 123 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate_traits.py b/client/ayon_core/plugins/publish/integrate_traits.py index 72a7ddd479..2a6755093e 100644 --- a/client/ayon_core/plugins/publish/integrate_traits.py +++ b/client/ayon_core/plugins/publish/integrate_traits.py @@ -4,9 +4,10 @@ from __future__ import annotations import contextlib import copy import hashlib +import json from dataclasses import dataclass from pathlib import Path -from typing import TYPE_CHECKING, Any, List +from typing import TYPE_CHECKING, Any import pyblish.api from ayon_api import ( @@ -85,6 +86,7 @@ class TransferItem: template: str template_data: dict[str, Any] representation: Representation + related_trait: FileLocation @staticmethod def get_size(file_path: Path) -> int: @@ -146,7 +148,7 @@ class RepresentationEntity: status: str -def get_instance_families(instance: pyblish.api.Instance) -> List[str]: +def get_instance_families(instance: pyblish.api.Instance) -> list[str]: """Get all families of the instance. Todo: @@ -156,7 +158,7 @@ def get_instance_families(instance: pyblish.api.Instance) -> List[str]: instance (pyblish.api.Instance): Instance to get families from. Returns: - List[str]: List of families. + list[str]: List of families. """ family = instance.data.get("family") @@ -207,10 +209,39 @@ def get_changed_attributes( return changes +def prepare_for_json(data: dict[str, Any]) -> dict[str, Any]: + """Prepare data for JSON serialization. + + If there are values that json cannot serialize, this function will + convert them to strings. + + Args: + data (dict[str, Any]): Data to prepare. + + Returns: + dict[str, Any]: Prepared data. + + Raises: + TypeError: If the data cannot be converted to JSON. + + """ + prepared = {} + for key, value in data.items(): + if isinstance(value, dict): + value = prepare_for_json(value) + try: + json.dumps(value) + except TypeError: + value = value.as_posix() if issubclass( + value.__class__, Path) else str(value) + prepared[key] = value + return prepared + + class IntegrateTraits(pyblish.api.InstancePlugin): """Integrate representations with traits.""" - label = "Integrate Asset" + label = "Integrate Traits of an Asset" order = pyblish.api.IntegratorOrder log: logging.Logger @@ -226,7 +257,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): """ # 1) skip farm and integrate == False - if not instance.data.get("integrate"): + if instance.data.get("integrate", True) is False: self.log.debug("Instance is marked to skip integrating. Skipping") return @@ -246,7 +277,8 @@ class IntegrateTraits(pyblish.api.InstancePlugin): instance.data["representations_with_traits"] ) - representations: list[Representation] = instance.data["representations_with_traits"] # noqa: E501 + representations: list[Representation] = ( + instance.data["representations_with_traits"]) # noqa: E501 if not representations: self.log.debug( "Instance has no persistent representations. Skipping") @@ -284,6 +316,10 @@ class IntegrateTraits(pyblish.api.InstancePlugin): self.log.debug( "Transferred files %s", [file_transactions.transferred]) + # replace original paths with the destination in traits. + for transfer in transfers: + transfer.related_trait.file_path = transfer.destination + # 9) Create representation entities for representation in representations: representation_entity = new_representation_entity( @@ -300,8 +336,14 @@ class IntegrateTraits(pyblish.api.InstancePlugin): ) # add traits to representation entity representation_entity["traits"] = representation.traits_as_dict() + op_session.create_entity( + project_name=instance.context.data["projectName"], + entity_type="representation", + data=prepare_for_json(representation_entity), + ) # 10) Commit the session to AYON + self.log.debug("{}".format(op_session.to_data())) op_session.commit() def get_transfers_from_representations( @@ -805,7 +847,7 @@ 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"] + # template_data["hierarchy"] = instance.data["hierarchy"] # add colorspace data to template data if representation.contains_trait(ColorManaged): @@ -938,6 +980,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): template=template_item.template, template_data=template_item.template_data, representation=representation, + related_trait=file_loc ) ) @@ -995,6 +1038,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): template=template_item.template, template_data=template_item.template_data, representation=representation, + related_trait=file_loc ) ) # add template path and the data to resolve it @@ -1052,6 +1096,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): template=template_item.template, template_data=template_item.template_data, representation=representation, + related_trait=file_loc ) ) # add template path and the data to resolve it 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 98f02bdff6..1fa80d7b8a 100644 --- a/tests/client/ayon_core/plugins/publish/test_integrate_traits.py +++ b/tests/client/ayon_core/plugins/publish/test_integrate_traits.py @@ -237,6 +237,8 @@ def mock_context( return context + + def test_get_template_name(mock_context: pyblish.api.Context) -> None: """Test get_template_name. @@ -252,6 +254,75 @@ def test_get_template_name(mock_context: pyblish.api.Context) -> None: assert template_name == "default" +class TestGetSize: + @staticmethod + def get_size(file_path: Path) -> int: + """Get size of the file. + + Args: + file_path (Path): File path. + + Returns: + int: Size of the file. + + """ + return file_path.stat().st_size + + @pytest.mark.parametrize( + "file_path, expected_size", + [ + (Path("./test_file_1.txt"), 10), # id: happy_path_small_file + (Path("./test_file_2.txt"), 1024), # id: happy_path_medium_file + (Path("./test_file_3.txt"), 10485760) # id: happy_path_large_file + ], + ids=["happy_path_small_file", "happy_path_medium_file", "happy_path_large_file"] + ) + def test_get_size_happy_path(self, file_path: Path, expected_size: int, tmp_path: Path): + # Arrange + file_path = tmp_path / file_path + file_path.write_bytes(b"\0" * expected_size) + + # Act + size = self.get_size(file_path) + + # Assert + assert size == expected_size + + + @pytest.mark.parametrize( + "file_path, expected_size", + [ + (Path("./test_file_empty.txt"), 0) # id: edge_case_empty_file + ], + ids=["edge_case_empty_file"] + ) + def test_get_size_edge_cases(self, file_path: Path, expected_size: int, tmp_path: Path): + # Arrange + file_path = tmp_path / file_path + file_path.touch() # Create an empty file + + # Act + size = self.get_size(file_path) + + # Assert + assert size == expected_size + + @pytest.mark.parametrize( + "file_path, expected_exception", + [ + (Path("./non_existent_file.txt"), FileNotFoundError), # id: error_file_not_found + (123, TypeError) # id: error_invalid_input_type + ], + ids=["error_file_not_found", "error_invalid_input_type"] + ) + def test_get_size_error_cases(self, file_path, expected_exception, tmp_path): + + # Act & Assert + with pytest.raises(expected_exception): + file_path = tmp_path / file_path + self.get_size(file_path) + + def test_filter_lifecycle() -> None: """Test filter_lifecycle.""" integrator = IntegrateTraits()