From d237e5f54cd466957699a350e9f8978a743eac7f Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 3 Jun 2025 17:25:32 +0200 Subject: [PATCH] :art: add support for product base type to basic creator logic --- client/ayon_core/pipeline/create/context.py | 2 +- .../pipeline/create/creator_plugins.py | 23 ++-- .../ayon_core/pipeline/create/product_name.py | 117 ++++++++++++------ .../ayon_core/pipeline/create/structures.py | 26 ++-- .../tools/publisher/models/create.py | 7 ++ 5 files changed, 119 insertions(+), 56 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index e6cc4393c5..f267450543 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -32,7 +32,7 @@ from ayon_core.host import IPublishHost, IWorkfileHost from ayon_core.pipeline import Anatomy from ayon_core.pipeline.template_data import get_template_data from ayon_core.pipeline.plugin_discover import DiscoverResult -from ayon_core.pipeline import is_supporting_product_base_type +from ayon_core.pipeline.compatibility import is_supporting_product_base_type from .exceptions import ( CreatorError, diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index bb824f52e3..155a443b53 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -317,7 +317,6 @@ class BaseCreator(ABC): identifier = f"{identifier}.{self.product_type}" return identifier - @property @abstractmethod def product_type(self): @@ -562,14 +561,15 @@ class BaseCreator(ABC): def get_product_name( self, - project_name, - folder_entity, - task_entity, - variant, - host_name=None, - instance=None, - project_entity=None, - ): + project_name: str, + folder_entity: dict[str, Any], + task_entity: dict[str, Any], + variant: str, + host_name: Optional[str] = None, + instance: Optional[CreatedInstance] = None, + project_entity: Optional[dict[str, Any]] = None, + product_base_type: Optional[str] = None, + ) -> str: """Return product name for passed context. Method is also called on product name update. In that case origin @@ -586,8 +586,12 @@ class BaseCreator(ABC): for which is product name updated. Passed only on product name update. project_entity (Optional[dict[str, Any]]): Project entity. + product_base_type (Optional[str]): Product base type. """ + if is_supporting_product_base_type() and (instance and hasattr(instance, "product_base_type")): # noqa: E501 + product_base_type = instance.product_base_type + if host_name is None: host_name = self.create_context.host_name @@ -619,6 +623,7 @@ class BaseCreator(ABC): dynamic_data=dynamic_data, project_settings=self.project_settings, project_entity=project_entity, + product_base_type=product_base_type ) def get_instance_attr_defs(self): diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index ecffa4a340..3fdf786b0e 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -1,9 +1,16 @@ +"""Functions for handling product names.""" +from __future__ import annotations + +from typing import Any, Optional, Union +from warnings import warn + import ayon_api from ayon_core.lib import ( StringTemplate, filter_profiles, prepare_template_data, ) +from ayon_core.pipeline.compatibility import is_supporting_product_base_type from ayon_core.settings import get_project_settings from .constants import DEFAULT_PRODUCT_TEMPLATE @@ -11,14 +18,15 @@ from .exceptions import TaskNotSetError, TemplateFillError def get_product_name_template( - project_name, - product_type, - task_name, - task_type, - host_name, - default_template=None, - project_settings=None -): + project_name: str, + product_type: str, + task_name: str, + task_type: str, + host_name: str, + default_template: Optional[str] = None, + project_settings: Optional[dict[str, Any]] = None, + product_base_type: Optional[str] = None +) -> str: """Get product name template based on passed context. Args: @@ -28,13 +36,17 @@ def get_product_name_template( host_name (str): Name of host in which the product name is calculated. task_name (str): Name of task in which context the product is created. task_type (str): Type of task in which context the product is created. - default_template (Union[str, None]): Default template which is used if + default_template (Optional, str): Default template which is used if settings won't find any matching possibility. Constant 'DEFAULT_PRODUCT_TEMPLATE' is used if not defined. project_settings (Union[Dict[str, Any], None]): Prepared settings for project. Settings are queried if not passed. - """ + product_base_type (Optional[str]): Base type of product. + Returns: + str: Product name template. + + """ if project_settings is None: project_settings = get_project_settings(project_name) tools_settings = project_settings["core"]["tools"] @@ -46,6 +58,15 @@ def get_product_name_template( "task_types": task_type } + if is_supporting_product_base_type(): + if product_base_type: + filtering_criteria["product_base_types"] = product_base_type + else: + warn( + "Product base type is not provided, please update your" + "creation code to include it. It will be required in " + "the future.", DeprecationWarning, stacklevel=2) + matching_profile = filter_profiles(profiles, filtering_criteria) template = None if matching_profile: @@ -70,17 +91,18 @@ def get_product_name_template( def get_product_name( - project_name, - task_name, - task_type, - host_name, - product_type, - variant, - default_template=None, - dynamic_data=None, - project_settings=None, - product_type_filter=None, - project_entity=None, + project_name: str, + task_name: str, + task_type: str, + host_name: str, + product_type: str, + variant: str, + default_template: Optional[str] = None, + dynamic_data: Optional[dict[str, Any]] = None, + project_settings: Optional[dict[str, Any]] = None, + product_type_filter: Optional[str] = None, + project_entity: Optional[dict[str, Any]] = None, + product_base_type: Optional[str] = None ): """Calculate product name based on passed context and AYON settings. @@ -92,14 +114,20 @@ def get_product_name( That's main reason why so many arguments are required to calculate product name. + Deprecation: + The `product_base_type` argument is optional now, but it will be + mandatory in future versions. It is recommended to pass it now to + avoid issues in the future. If it is not passed, a warning will be raised + to inform about this change. + Todos: Find better filtering options to avoid requirement of argument 'family_filter'. Args: project_name (str): Project name. - task_name (Union[str, None]): Task name. - task_type (Union[str, None]): Task type. + task_name (str): Task name. + task_type (str): Task type. host_name (str): Host name. product_type (str): Product type. variant (str): In most of the cases it is user input during creation. @@ -115,6 +143,8 @@ def get_product_name( not passed. project_entity (Optional[Dict[str, Any]]): Project entity used when task short name is required by template. + product_base_type (Optional[str]): Base type of product. + This will be mandatory in future versions. Returns: str: Product name. @@ -129,13 +159,14 @@ def get_product_name( return "" template = get_product_name_template( - project_name, - product_type_filter or product_type, - task_name, - task_type, - host_name, + project_name=project_name, + product_type=product_type_filter or product_type, + task_name=task_name, + task_type=task_type, + host_name=host_name, default_template=default_template, - project_settings=project_settings + project_settings=project_settings, + product_base_type=product_base_type, ) # Simple check of task name existence for template with {task} in # - missing task should be possible only in Standalone publisher @@ -147,7 +178,7 @@ def get_product_name( "type": task_type, } if "{task}" in template.lower(): - task_value = task_name + task_value["name"] = task_name elif "{task[short]}" in template.lower(): if project_entity is None: @@ -159,14 +190,25 @@ def get_product_name( task_short = task_types_by_name.get(task_type, {}).get("shortName") task_value["short"] = task_short - fill_pairs = { + # look what we have to do to make mypy happy. We should stop using + # those undefined dict based types. + product: dict[str, str] = {"type": product_type} + if is_supporting_product_base_type(): + if product_base_type: + product["baseType"] = product_base_type + elif "{product[basetype]}" in template.lower(): + warn( + "You have Product base type in product name template," + "but it is not provided by the creator, please update your" + "creation code to include it. It will be required in " + "the future.", DeprecationWarning, stacklevel=2) + fill_pairs: dict[str, Union[str, dict[str, str]]] = { "variant": variant, "family": product_type, "task": task_value, - "product": { - "type": product_type - } + "product": product, } + if dynamic_data: # Dynamic data may override default values for key, value in dynamic_data.items(): @@ -178,7 +220,8 @@ def get_product_name( data=prepare_template_data(fill_pairs) ) except KeyError as exp: - raise TemplateFillError( - "Value for {} key is missing in template '{}'." - " Available values are {}".format(str(exp), template, fill_pairs) + msg = ( + f"Value for {exp} key is missing in template '{template}'." + f" Available values are {fill_pairs}" ) + raise TemplateFillError(msg) from exp diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index 389ce25961..a6b57c29ca 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -3,7 +3,7 @@ import collections from uuid import uuid4 import typing from typing import Optional, Dict, List, Any -import warnings +from warnings import warn from ayon_core.lib.attribute_definitions import ( AbstractAttrDef, @@ -465,6 +465,10 @@ class CreatedInstance: data (Dict[str, Any]): Data used for filling product name or override data from already existing instance. creator (BaseCreator): Creator responsible for instance. + product_base_type (Optional[str]): Product base type that will be + created. If not provided then product base type is taken from + creator plugin. If creator does not have product base type then + deprecation warning is raised. """ # Keys that can't be changed or removed from data after loading using @@ -497,14 +501,18 @@ class CreatedInstance: transient_data: Optional[Dict[str, Any]] = None, product_base_type: Optional[str] = None ): - - if is_supporting_product_base_type() and product_base_type is None: - warnings.warn( - f"Creator {creator!r} doesn't support " - "product base type. This will be required in future.", - DeprecationWarning, - stacklevel=2 - ) + """Initialize CreatedInstance.""" + if is_supporting_product_base_type(): + if not hasattr(creator, "product_base_type"): + warn( + f"Provided creator {creator!r} doesn't have " + "product base type attribute defined. This will be " + "required in future.", + DeprecationWarning, + stacklevel=2 + ) + elif not product_base_type: + product_base_type = creator.product_base_type self._creator = creator creator_identifier = creator.identifier diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 900168eaef..862bd1ea03 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -34,6 +34,8 @@ from ayon_core.pipeline.create import ( ConvertorsOperationFailed, ConvertorItem, ) +from ayon_core.pipeline.compatibility import is_supporting_product_base_type + from ayon_core.tools.publisher.abstract import ( AbstractPublisherBackend, CardMessageTypes, @@ -631,12 +633,17 @@ class CreateModel: "instance": instance, "project_entity": project_entity, } + + if is_supporting_product_base_type() and hasattr(creator, "product_base_type"): # noqa: E501 + kwargs["product_base_type"] = creator.product_base_type + # Backwards compatibility for 'project_entity' argument # - 'get_product_name' signature changed 24/07/08 if not is_func_signature_supported( creator.get_product_name, *args, **kwargs ): kwargs.pop("project_entity") + kwargs.pop("product_base_type") return creator.get_product_name(*args, **kwargs) def create(