From bcdeba18ac65d401b9b90add03f4c8bdce3be1ee Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 2 Jun 2025 09:29:04 +0200 Subject: [PATCH 01/40] :wrench: implementation WIP --- client/ayon_core/pipeline/create/context.py | 18 +++++++++++ .../pipeline/create/creator_plugins.py | 32 ++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index f0d9fa8927..e6cc4393c5 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -18,6 +18,7 @@ from typing import ( Callable, Union, ) +from warnings import warn import pyblish.logic import pyblish.api @@ -31,6 +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 .exceptions import ( CreatorError, @@ -1194,6 +1196,22 @@ class CreateContext: "productType": creator.product_type, "variant": variant } + + # Add product base type if supported. + # TODO (antirotor): Once all creators support product base type + # remove this check. + if is_supporting_product_base_type(): + + if hasattr(creator, "product_base_type"): + instance_data["productBaseType"] = creator.product_base_type + else: + warn( + f"Creator {creator_identifier} does not support " + "product base type. This will be required in future.", + DeprecationWarning, + stacklevel=2, + ) + if active is not None: if not isinstance(active, bool): self.log.warning( diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index cbc06145fb..ad4b777db5 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -3,6 +3,7 @@ import os import copy import collections from typing import TYPE_CHECKING, Optional, Dict, Any +from warnings import warn from abc import ABC, abstractmethod @@ -16,6 +17,7 @@ from ayon_core.pipeline.plugin_discover import ( deregister_plugin_path ) from ayon_core.pipeline.staging_dir import get_staging_dir_info, StagingDir +from ayon_core.pipeline import is_supporting_product_base_type from .constants import DEFAULT_VARIANT_VALUE from .product_name import get_product_name @@ -308,6 +310,9 @@ class BaseCreator(ABC): Default implementation returns plugin's product type. """ + if is_supporting_product_base_type(): + return self.product_base_type + return self.product_type @property @@ -317,6 +322,16 @@ class BaseCreator(ABC): pass + @property + @abstractmethod + def product_base_type(self): + """Base product type that plugin represents. + + This is used to group products in UI. + """ + + pass + @property def project_name(self): """Current project name. @@ -378,7 +393,8 @@ class BaseCreator(ABC): self, product_name: str, data: Dict[str, Any], - product_type: Optional[str] = None + product_type: Optional[str] = None, + product_base_type: Optional[str] = None ) -> CreatedInstance: """Create instance and add instance to context. @@ -387,6 +403,8 @@ class BaseCreator(ABC): data (Dict[str, Any]): Instance data. product_type (Optional[str]): Product type, object attribute 'product_type' is used if not passed. + product_base_type (Optional[str]): Product base type, object + attribute 'product_type' is used if not passed. Returns: CreatedInstance: Created instance. @@ -394,6 +412,18 @@ class BaseCreator(ABC): """ if product_type is None: product_type = self.product_type + + if is_supporting_product_base_type() and not product_base_type: + if not self.product_base_type: + warn( + f"Creator {self.identifier} does not support " + "product base type. This will be required in future.", + DeprecationWarning, + stacklevel=2, + ) + else: + product_base_type = self.product_base_type + instance = CreatedInstance( product_type, product_name, From 9e730a6b5b1ecdf1cc67ea8609034e43929ab303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 3 Jun 2025 10:58:43 +0200 Subject: [PATCH 02/40] :wrench: changes in Plugin anc CreateInstance WIP --- .../pipeline/create/creator_plugins.py | 63 +++++++++++-------- .../ayon_core/pipeline/create/structures.py | 20 ++++++ 2 files changed, 58 insertions(+), 25 deletions(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index ad4b777db5..bb824f52e3 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -17,7 +17,7 @@ from ayon_core.pipeline.plugin_discover import ( deregister_plugin_path ) from ayon_core.pipeline.staging_dir import get_staging_dir_info, StagingDir -from ayon_core.pipeline import is_supporting_product_base_type +from ayon_core.pipeline.compatibility import is_supporting_product_base_type from .constants import DEFAULT_VARIANT_VALUE from .product_name import get_product_name @@ -308,12 +308,15 @@ class BaseCreator(ABC): """Identifier of creator (must be unique). Default implementation returns plugin's product type. + """ - + identifier = self.product_type if is_supporting_product_base_type(): - return self.product_base_type + identifier = self.product_base_type + if self.product_type: + identifier = f"{identifier}.{self.product_type}" + return identifier - return self.product_type @property @abstractmethod @@ -323,14 +326,19 @@ class BaseCreator(ABC): pass @property - @abstractmethod - def product_base_type(self): + def product_base_type(self) -> Optional[str]: """Base product type that plugin represents. - This is used to group products in UI. - """ + Todo (antirotor): This should be required in future - it + should be made abstract then. - pass + Returns: + Optional[str]: Base product type that plugin represents. + If not set, it is assumed that the creator plugin is obsolete + and does not support product base type. + + """ + return None @property def project_name(self): @@ -361,13 +369,14 @@ class BaseCreator(ABC): Default implementation use attributes in this order: - 'group_label' -> 'label' -> 'identifier' - Keep in mind that 'identifier' use 'product_type' by default. + + Keep in mind that 'identifier' uses 'product_base_type' by default. Returns: str: Group label that can be used for grouping of instances in UI. - Group label can be overridden by instance itself. + Group label can be overridden by the instance itself. + """ - if self._cached_group_label is None: label = self.identifier if self.group_label: @@ -413,22 +422,26 @@ class BaseCreator(ABC): if product_type is None: product_type = self.product_type - if is_supporting_product_base_type() and not product_base_type: - if not self.product_base_type: - warn( - f"Creator {self.identifier} does not support " - "product base type. This will be required in future.", - DeprecationWarning, - stacklevel=2, - ) - else: - product_base_type = self.product_base_type + if ( + is_supporting_product_base_type() + and not product_base_type + and not self.product_base_type + ): + warn( + f"Creator {self.identifier} does not support " + "product base type. This will be required in future.", + DeprecationWarning, + stacklevel=2, + ) + else: + product_base_type = self.product_base_type instance = CreatedInstance( - product_type, - product_name, - data, + product_type=product_type, + product_name=product_name, + data=data, creator=self, + product_base_type=product_base_type ) self._add_instance_to_context(instance) return instance diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index d7ba6b9c24..389ce25961 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -3,6 +3,7 @@ import collections from uuid import uuid4 import typing from typing import Optional, Dict, List, Any +import warnings from ayon_core.lib.attribute_definitions import ( AbstractAttrDef, @@ -10,6 +11,9 @@ from ayon_core.lib.attribute_definitions import ( serialize_attr_defs, deserialize_attr_defs, ) + +from ayon_core.pipeline.compatibility import is_supporting_product_base_type + from ayon_core.pipeline import ( AYON_INSTANCE_ID, AVALON_INSTANCE_ID, @@ -471,6 +475,7 @@ class CreatedInstance: "id", "instance_id", "productType", + "productBaseType", "creator_identifier", "creator_attributes", "publish_attributes" @@ -490,7 +495,17 @@ class CreatedInstance: data: Dict[str, Any], creator: "BaseCreator", 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 + ) + self._creator = creator creator_identifier = creator.identifier group_label = creator.get_group_label() @@ -540,6 +555,11 @@ class CreatedInstance: self._data["id"] = item_id self._data["productType"] = product_type self._data["productName"] = product_name + + if is_supporting_product_base_type(): + data.pop("productBaseType", None) + self._data["productBaseType"] = product_base_type + self._data["active"] = data.get("active", True) self._data["creator_identifier"] = creator_identifier From d237e5f54cd466957699a350e9f8978a743eac7f Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 3 Jun 2025 17:25:32 +0200 Subject: [PATCH 03/40] :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( From 67db5c123ff72b41b53209310c6bae339de1d8ed Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 4 Jun 2025 11:24:22 +0200 Subject: [PATCH 04/40] :dog: linter fixes --- .../pipeline/create/creator_plugins.py | 99 +++++-------------- 1 file changed, 25 insertions(+), 74 deletions(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 155a443b53..040ed073f2 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -1,32 +1,32 @@ -# -*- coding: utf-8 -*- -import os -import copy +"""Creator plugins for the create process.""" import collections -from typing import TYPE_CHECKING, Optional, Dict, Any +import copy +import os +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any, Dict, Optional from warnings import warn -from abc import ABC, abstractmethod - -from ayon_core.settings import get_project_settings from ayon_core.lib import Logger, get_version_from_path +from ayon_core.pipeline.compatibility import is_supporting_product_base_type from ayon_core.pipeline.plugin_discover import ( + deregister_plugin, + deregister_plugin_path, discover, register_plugin, register_plugin_path, - deregister_plugin, - deregister_plugin_path ) -from ayon_core.pipeline.staging_dir import get_staging_dir_info, StagingDir -from ayon_core.pipeline.compatibility import is_supporting_product_base_type +from ayon_core.pipeline.staging_dir import StagingDir, get_staging_dir_info +from ayon_core.settings import get_project_settings from .constants import DEFAULT_VARIANT_VALUE -from .product_name import get_product_name -from .utils import get_next_versions_for_instances from .legacy_create import LegacyCreator +from .product_name import get_product_name from .structures import CreatedInstance +from .utils import get_next_versions_for_instances if TYPE_CHECKING: from ayon_core.lib import AbstractAttrDef + # Avoid cyclic imports from .context import CreateContext, UpdateData # noqa: F401 @@ -70,7 +70,6 @@ class ProductConvertorPlugin(ABC): Returns: logging.Logger: Logger with name of the plugin. """ - if self._log is None: self._log = Logger.get_logger(self.__class__.__name__) return self._log @@ -86,9 +85,8 @@ class ProductConvertorPlugin(ABC): Returns: str: Converted identifier unique for all converters in host. - """ - pass + """ @abstractmethod def find_instances(self): @@ -98,14 +96,10 @@ class ProductConvertorPlugin(ABC): convert. """ - pass - @abstractmethod def convert(self): """Conversion code.""" - pass - @property def create_context(self): """Quick access to create context. @@ -113,7 +107,6 @@ class ProductConvertorPlugin(ABC): Returns: CreateContext: Context which initialized the plugin. """ - return self._create_context @property @@ -126,7 +119,6 @@ class ProductConvertorPlugin(ABC): Raises: UnavailableSharedData: When called out of collection phase. """ - return self._create_context.collection_shared_data def add_convertor_item(self, label): @@ -135,12 +127,10 @@ class ProductConvertorPlugin(ABC): Args: label (str): Label of item which will show in UI. """ - self._create_context.add_convertor_item(self.identifier, label) def remove_convertor_item(self): """Remove legacy item from create context when conversion finished.""" - self._create_context.remove_convertor_item(self.identifier) @@ -159,7 +149,6 @@ class BaseCreator(ABC): create_context (CreateContext): Context which initialized creator. headless (bool): Running in headless mode. """ - # Label shown in UI label = None group_label = None @@ -223,7 +212,6 @@ class BaseCreator(ABC): Returns: Optional[dict[str, Any]]: Settings values or None. """ - settings = project_settings.get(category_name) if not settings: return None @@ -269,7 +257,6 @@ class BaseCreator(ABC): Args: project_settings (dict[str, Any]): Project settings. """ - settings_category = self.settings_category if not settings_category: return @@ -281,18 +268,17 @@ class BaseCreator(ABC): project_settings, settings_category, settings_name ) if settings is None: - self.log.debug("No settings found for {}".format(cls_name)) + self.log.debug(f"No settings found for {cls_name}") return for key, value in settings.items(): # Log out attributes that are not defined on plugin object # - those may be potential dangerous typos in settings if not hasattr(self, key): - self.log.debug(( - "Applying settings to unknown attribute '{}' on '{}'." - ).format( + self.log.debug( + "Applying settings to unknown attribute '%' on '%'.", key, cls_name - )) + ) setattr(self, key, value) def register_callbacks(self): @@ -301,7 +287,6 @@ class BaseCreator(ABC): Default implementation does nothing. It can be overridden to register callbacks for creator. """ - pass @property def identifier(self): @@ -322,8 +307,6 @@ class BaseCreator(ABC): def product_type(self): """Family that plugin represents.""" - pass - @property def product_base_type(self) -> Optional[str]: """Base product type that plugin represents. @@ -346,7 +329,6 @@ class BaseCreator(ABC): Returns: str: Name of a project. """ - return self.create_context.project_name @property @@ -356,7 +338,6 @@ class BaseCreator(ABC): Returns: Anatomy: Project anatomy object. """ - return self.create_context.project_anatomy @property @@ -368,13 +349,13 @@ class BaseCreator(ABC): Default implementation use attributes in this order: - 'group_label' -> 'label' -> 'identifier' - + Keep in mind that 'identifier' uses 'product_base_type' by default. Returns: str: Group label that can be used for grouping of instances in UI. Group label can be overridden by the instance itself. - + """ if self._cached_group_label is None: label = self.identifier @@ -392,7 +373,6 @@ class BaseCreator(ABC): Returns: logging.Logger: Logger with name of the plugin. """ - if self._log is None: self._log = Logger.get_logger(self.__class__.__name__) return self._log @@ -456,7 +436,6 @@ class BaseCreator(ABC): Args: instance (CreatedInstance): New created instance. """ - self.create_context.creator_adds_instance(instance) def _remove_instance_from_context(self, instance): @@ -469,7 +448,6 @@ class BaseCreator(ABC): Args: instance (CreatedInstance): Instance which should be removed. """ - self.create_context.creator_removed_instance(instance) @abstractmethod @@ -481,8 +459,6 @@ class BaseCreator(ABC): implementation """ - pass - @abstractmethod def collect_instances(self): """Collect existing instances related to this creator plugin. @@ -508,8 +484,6 @@ class BaseCreator(ABC): ``` """ - pass - @abstractmethod def update_instances(self, update_list): """Store changes of existing instances so they can be recollected. @@ -519,8 +493,6 @@ class BaseCreator(ABC): contain changed instance and it's changes. """ - pass - @abstractmethod def remove_instances(self, instances): """Method called on instance removal. @@ -533,14 +505,11 @@ class BaseCreator(ABC): removed. """ - pass - def get_icon(self): """Icon of creator (product type). Can return path to image file or awesome icon name. """ - return self.icon def get_dynamic_data( @@ -556,7 +525,6 @@ class BaseCreator(ABC): These may be dynamically created based on current context of workfile. """ - return {} def get_product_name( @@ -633,15 +601,15 @@ class BaseCreator(ABC): and values are stored to metadata for future usage and for publishing purposes. - NOTE: - Convert method should be implemented which should care about updating - keys/values when plugin attributes change. + Note: + Convert method should be implemented which should care about + updating keys/values when plugin attributes change. Returns: list[AbstractAttrDef]: Attribute definitions that can be tweaked for created instance. - """ + """ return self.instance_attr_defs def get_attr_defs_for_instance(self, instance): @@ -664,12 +632,10 @@ class BaseCreator(ABC): Raises: UnavailableSharedData: When called out of collection phase. """ - return self.create_context.collection_shared_data def set_instance_thumbnail_path(self, instance_id, thumbnail_path=None): """Set path to thumbnail for instance.""" - self.create_context.thumbnail_paths_by_instance_id[instance_id] = ( thumbnail_path ) @@ -690,7 +656,6 @@ class BaseCreator(ABC): Returns: dict[str, int]: Next versions by instance id. """ - return get_next_versions_for_instances( self.create_context.project_name, instances ) @@ -757,7 +722,6 @@ class Creator(BaseCreator): int: Order in which is creator shown (less == earlier). By default is using Creator's 'order' or processing. """ - return self.order @abstractmethod @@ -772,11 +736,9 @@ class Creator(BaseCreator): pre_create_data(dict): Data based on pre creation attributes. Those may affect how creator works. """ - # instance = CreatedInstance( # self.product_type, product_name, instance_data # ) - pass def get_description(self): """Short description of product type and plugin. @@ -784,7 +746,6 @@ class Creator(BaseCreator): Returns: str: Short description of product type. """ - return self.description def get_detail_description(self): @@ -795,7 +756,6 @@ class Creator(BaseCreator): Returns: str: Detailed description of product type for artist. """ - return self.detailed_description def get_default_variants(self): @@ -809,7 +769,6 @@ class Creator(BaseCreator): Returns: list[str]: Whisper variants for user input. """ - return copy.deepcopy(self.default_variants) def get_default_variant(self, only_explicit=False): @@ -829,7 +788,6 @@ class Creator(BaseCreator): Returns: str: Variant value. """ - if only_explicit or self._default_variant: return self._default_variant @@ -850,7 +808,6 @@ class Creator(BaseCreator): Returns: str: Variant value. """ - return self.get_default_variant() def _set_default_variant_wrap(self, variant): @@ -862,7 +819,6 @@ class Creator(BaseCreator): Args: variant (str): New default variant value. """ - self._default_variant = variant default_variant = property( @@ -1012,7 +968,6 @@ class AutoCreator(BaseCreator): def remove_instances(self, instances): """Skip removal.""" - pass def discover_creator_plugins(*args, **kwargs): @@ -1036,9 +991,7 @@ def discover_legacy_creator_plugins(): plugin.apply_settings(project_settings) except Exception: log.warning( - "Failed to apply settings to creator {}".format( - plugin.__name__ - ), + "Failed to apply settings to creator %s", plugin.__name__, exc_info=True ) return plugins @@ -1055,7 +1008,6 @@ def get_legacy_creator_by_name(creator_name, case_sensitive=False): Returns: Creator: Return first matching plugin or `None`. """ - # Lower input creator name if is not case sensitive if not case_sensitive: creator_name = creator_name.lower() @@ -1127,7 +1079,6 @@ def cache_and_get_instances(creator, shared_key, list_instances_func): dict[str, dict[str, Any]]: Cached instances by creator identifier from result of passed function. """ - if shared_key not in creator.collection_shared_data: value = collections.defaultdict(list) for instance in list_instances_func(): From fce1ef248d4292b80a479eb156326aa002d4c48f Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 4 Jun 2025 11:28:07 +0200 Subject: [PATCH 05/40] :dog: some more linter fixes --- client/ayon_core/pipeline/create/product_name.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index 3fdf786b0e..1f0e8f3ba5 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -117,8 +117,8 @@ def get_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. + 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 From dfd8fe6e8cd9fbd5fe6e930189065623183b3020 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Fri, 6 Jun 2025 10:33:25 +0200 Subject: [PATCH 06/40] :bug: report correctly skipped abstract creators --- client/ayon_core/pipeline/create/context.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index f267450543..fcc18555b5 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -731,13 +731,12 @@ class CreateContext: manual_creators = {} report = discover_creator_plugins(return_report=True) self.creator_discover_result = report - for creator_class in report.plugins: - if inspect.isabstract(creator_class): - self.log.debug( - "Skipping abstract Creator {}".format(str(creator_class)) - ) - continue + for creator_class in report.abstract_plugins: + self.log.debug( + f"Skipping abstract Creator '%s'", str(creator_class) + ) + for creator_class in report.plugins: creator_identifier = creator_class.identifier if creator_identifier in creators: self.log.warning( From fa8c05488943dcd4f8195b4bcffc83e9cd45c37a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 9 Jun 2025 13:54:41 +0200 Subject: [PATCH 07/40] :recycle: refactor support feature check function name --- client/ayon_core/pipeline/create/context.py | 4 ++-- client/ayon_core/pipeline/create/creator_plugins.py | 8 ++++---- client/ayon_core/pipeline/create/product_name.py | 6 +++--- client/ayon_core/pipeline/create/structures.py | 6 +++--- client/ayon_core/tools/publisher/models/create.py | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index fcc18555b5..a5a9d5a64a 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.compatibility import is_supporting_product_base_type +from ayon_core.pipeline.compatibility import is_product_base_type_supported from .exceptions import ( CreatorError, @@ -1199,7 +1199,7 @@ class CreateContext: # Add product base type if supported. # TODO (antirotor): Once all creators support product base type # remove this check. - if is_supporting_product_base_type(): + if is_product_base_type_supported(): if hasattr(creator, "product_base_type"): instance_data["productBaseType"] = creator.product_base_type diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 040ed073f2..78fb723567 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Any, Dict, Optional from warnings import warn from ayon_core.lib import Logger, get_version_from_path -from ayon_core.pipeline.compatibility import is_supporting_product_base_type +from ayon_core.pipeline.compatibility import is_product_base_type_supported from ayon_core.pipeline.plugin_discover import ( deregister_plugin, deregister_plugin_path, @@ -296,7 +296,7 @@ class BaseCreator(ABC): """ identifier = self.product_type - if is_supporting_product_base_type(): + if is_product_base_type_supported(): identifier = self.product_base_type if self.product_type: identifier = f"{identifier}.{self.product_type}" @@ -402,7 +402,7 @@ class BaseCreator(ABC): product_type = self.product_type if ( - is_supporting_product_base_type() + is_product_base_type_supported() and not product_base_type and not self.product_base_type ): @@ -557,7 +557,7 @@ class BaseCreator(ABC): product_base_type (Optional[str]): Product base type. """ - if is_supporting_product_base_type() and (instance and hasattr(instance, "product_base_type")): # noqa: E501 + if is_product_base_type_supported() and (instance and hasattr(instance, "product_base_type")): # noqa: E501 product_base_type = instance.product_base_type if host_name is None: diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index 1f0e8f3ba5..ab7de0c9e8 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -10,7 +10,7 @@ from ayon_core.lib import ( filter_profiles, prepare_template_data, ) -from ayon_core.pipeline.compatibility import is_supporting_product_base_type +from ayon_core.pipeline.compatibility import is_product_base_type_supported from ayon_core.settings import get_project_settings from .constants import DEFAULT_PRODUCT_TEMPLATE @@ -58,7 +58,7 @@ def get_product_name_template( "task_types": task_type } - if is_supporting_product_base_type(): + if is_product_base_type_supported(): if product_base_type: filtering_criteria["product_base_types"] = product_base_type else: @@ -193,7 +193,7 @@ def get_product_name( # 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 is_product_base_type_supported(): if product_base_type: product["baseType"] = product_base_type elif "{product[basetype]}" in template.lower(): diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index a6b57c29ca..aad85a546a 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -12,7 +12,7 @@ from ayon_core.lib.attribute_definitions import ( deserialize_attr_defs, ) -from ayon_core.pipeline.compatibility import is_supporting_product_base_type +from ayon_core.pipeline.compatibility import is_product_base_type_supported from ayon_core.pipeline import ( AYON_INSTANCE_ID, @@ -502,7 +502,7 @@ class CreatedInstance: product_base_type: Optional[str] = None ): """Initialize CreatedInstance.""" - if is_supporting_product_base_type(): + if is_product_base_type_supported(): if not hasattr(creator, "product_base_type"): warn( f"Provided creator {creator!r} doesn't have " @@ -564,7 +564,7 @@ class CreatedInstance: self._data["productType"] = product_type self._data["productName"] = product_name - if is_supporting_product_base_type(): + if is_product_base_type_supported(): data.pop("productBaseType", None) self._data["productBaseType"] = product_base_type diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 862bd1ea03..77e50dc788 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -34,7 +34,7 @@ from ayon_core.pipeline.create import ( ConvertorsOperationFailed, ConvertorItem, ) -from ayon_core.pipeline.compatibility import is_supporting_product_base_type +from ayon_core.pipeline.compatibility import is_product_base_type_supported from ayon_core.tools.publisher.abstract import ( AbstractPublisherBackend, @@ -634,7 +634,7 @@ class CreateModel: "project_entity": project_entity, } - if is_supporting_product_base_type() and hasattr(creator, "product_base_type"): # noqa: E501 + if is_product_base_type_supported() and hasattr(creator, "product_base_type"): # noqa: E501 kwargs["product_base_type"] = creator.product_base_type # Backwards compatibility for 'project_entity' argument From da286e3cfb5e8fb76a7328e5aab5de685316bb95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 10 Jun 2025 11:23:37 +0200 Subject: [PATCH 08/40] :recycle: remove check for attribute --- client/ayon_core/pipeline/create/context.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index a5a9d5a64a..a37fefc1f9 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -1201,15 +1201,14 @@ class CreateContext: # remove this check. if is_product_base_type_supported(): - if hasattr(creator, "product_base_type"): - instance_data["productBaseType"] = creator.product_base_type - else: - warn( - f"Creator {creator_identifier} does not support " - "product base type. This will be required in future.", - DeprecationWarning, - stacklevel=2, + instance_data["productBaseType"] = creator.product_base_type + if creator.product_base_type is None: + msg = ( + f"Creator {creator_identifier} does not set " + "product base type. This will be required in future." ) + warn(msg, DeprecationWarning, stacklevel=2) + self.log.warning(msg) if active is not None: if not isinstance(active, bool): From e2a413f20eb64001703c2ff5220d232fbdf0d0b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 10 Jun 2025 11:40:43 +0200 Subject: [PATCH 09/40] :dog: remove unneeded f-string --- client/ayon_core/pipeline/create/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index a37fefc1f9..17a5dea7dc 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -733,7 +733,7 @@ class CreateContext: self.creator_discover_result = report for creator_class in report.abstract_plugins: self.log.debug( - f"Skipping abstract Creator '%s'", str(creator_class) + "Skipping abstract Creator '%s'", str(creator_class) ) for creator_class in report.plugins: From 50045d71bd58466e4e1b94209fda8cedafa69d37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 10 Jun 2025 12:16:49 +0200 Subject: [PATCH 10/40] :sparkles: support product base types in the integrator --- client/ayon_core/pipeline/publish/lib.py | 6 +++++- client/ayon_core/plugins/publish/integrate.py | 9 +++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 49ecab2221..fba7f1d84b 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -106,7 +106,8 @@ def get_publish_template_name( task_type, project_settings=None, hero=False, - logger=None + logger=None, + product_base_type: Optional[str] = None ): """Get template name which should be used for passed context. @@ -126,6 +127,8 @@ def get_publish_template_name( hero (bool): Template is for hero version publishing. logger (logging.Logger): Custom logger used for 'filter_profiles' function. + product_base_type (Optional[str]): Product type for which should + be found template. Returns: str: Template name which should be used for integration. @@ -135,6 +138,7 @@ def get_publish_template_name( filter_criteria = { "hosts": host_name, "product_types": product_type, + "product_base_types": product_base_type, "task_names": task_name, "task_types": task_type, } diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index f1e066018c..41e71207e7 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -368,6 +368,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): folder_entity = instance.data["folderEntity"] product_name = instance.data["productName"] product_type = instance.data["productType"] + product_base_type = instance.data.get("productBaseType") + self.log.debug("Product: {}".format(product_name)) # Get existing product if it exists @@ -401,7 +403,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): folder_entity["id"], data=data, attribs=attributes, - entity_id=product_id + entity_id=product_id, + product_base_type=product_base_type ) if existing_product_entity is None: @@ -917,6 +920,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): host_name = context.data["hostName"] anatomy_data = instance.data["anatomyData"] product_type = instance.data["productType"] + product_base_type = instance.data.get("productBaseType") task_info = anatomy_data.get("task") or {} return get_publish_template_name( @@ -926,7 +930,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): task_name=task_info.get("name"), task_type=task_info.get("type"), project_settings=context.data["project_settings"], - logger=self.log + logger=self.log, + product_base_type=product_base_type ) def get_rootless_path(self, anatomy, path): From 2597469b30bfa1fc386218d0aa3a51154445ec52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 3 Sep 2025 15:16:27 +0200 Subject: [PATCH 11/40] :fire: remove deprecated code --- .../pipeline/create/creator_plugins.py | 46 ------------------- 1 file changed, 46 deletions(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 95db3f260f..26dbc5f3d3 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -977,52 +977,6 @@ def discover_convertor_plugins(*args, **kwargs): return discover(ProductConvertorPlugin, *args, **kwargs) -def discover_legacy_creator_plugins(): - from ayon_core.pipeline import get_current_project_name - - log = Logger.get_logger("CreatorDiscover") - - plugins = discover(LegacyCreator) - project_name = get_current_project_name() - project_settings = get_project_settings(project_name) - for plugin in plugins: - try: - plugin.apply_settings(project_settings) - except Exception: - log.warning( - "Failed to apply settings to creator %s", plugin.__name__, - exc_info=True - ) - return plugins - - -def get_legacy_creator_by_name(creator_name, case_sensitive=False): - """Find creator plugin by name. - - Args: - creator_name (str): Name of creator class that should be returned. - case_sensitive (bool): Match of creator plugin name is case sensitive. - Set to `False` by default. - - Returns: - Creator: Return first matching plugin or `None`. - """ - # Lower input creator name if is not case sensitive - if not case_sensitive: - creator_name = creator_name.lower() - - for creator_plugin in discover_legacy_creator_plugins(): - _creator_name = creator_plugin.__name__ - - # Lower creator plugin name if is not case sensitive - if not case_sensitive: - _creator_name = _creator_name.lower() - - if _creator_name == creator_name: - return creator_plugin - return None - - def register_creator_plugin(plugin): if issubclass(plugin, BaseCreator): register_plugin(BaseCreator, plugin) From 51965a9de160caf8fa334043b5f62adfaab1d374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 3 Sep 2025 15:18:50 +0200 Subject: [PATCH 12/40] :fire: remove unused import --- client/ayon_core/pipeline/create/creator_plugins.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 26dbc5f3d3..480ef28432 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -16,7 +16,6 @@ from ayon_core.pipeline.plugin_discover import ( register_plugin_path, ) from ayon_core.pipeline.staging_dir import StagingDir, get_staging_dir_info -from ayon_core.settings import get_project_settings from .constants import DEFAULT_VARIANT_VALUE from .product_name import get_product_name From f147d28c528f67635c9aafe1d03fa7f8a51cbafd Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 17 Oct 2025 17:36:47 +0200 Subject: [PATCH 13/40] =?UTF-8?q?=E2=9A=97=EF=B8=8F=20add=20tests=20for=20?= =?UTF-8?q?product=20names?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pipeline/create/test_product_name.py | 372 ++++++++++++++++++ 1 file changed, 372 insertions(+) create mode 100644 tests/client/ayon_core/pipeline/create/test_product_name.py diff --git a/tests/client/ayon_core/pipeline/create/test_product_name.py b/tests/client/ayon_core/pipeline/create/test_product_name.py new file mode 100644 index 0000000000..b4507e39f1 --- /dev/null +++ b/tests/client/ayon_core/pipeline/create/test_product_name.py @@ -0,0 +1,372 @@ +"""Tests for product_name helpers.""" +import pytest +from unittest.mock import patch + +from ayon_core.pipeline.create.product_name import ( + get_product_name_template, + get_product_name, +) +from ayon_core.pipeline.create.constants import DEFAULT_PRODUCT_TEMPLATE +from ayon_core.pipeline.create.exceptions import ( + TaskNotSetError, + TemplateFillError, +) + + +class TestGetProductNameTemplate: + @patch("ayon_core.pipeline.create.product_name.get_project_settings") + @patch("ayon_core.pipeline.create.product_name.filter_profiles") + @patch("ayon_core.pipeline.create.product_name." + "is_product_base_type_supported") + def test_matching_profile_with_replacements( + self, + mock_is_supported, + mock_filter_profiles, + mock_get_settings, + ): + """Matching profile applies legacy replacement tokens.""" + mock_get_settings.return_value = { + "core": {"tools": {"creator": {"product_name_profiles": []}}} + } + # The function should replace {task}/{family}/{asset} variants + mock_filter_profiles.return_value = { + "template": ("{task}-{Task}-{TASK}-{family}-{Family}" + "-{FAMILY}-{asset}-{Asset}-{ASSET}") + } + mock_is_supported.return_value = False + + result = get_product_name_template( + project_name="proj", + product_type="model", + task_name="modeling", + task_type="Modeling", + host_name="maya", + ) + assert result == ( + "{task[name]}-{Task[name]}-{TASK[NAME]}-" + "{product[type]}-{Product[type]}-{PRODUCT[TYPE]}-" + "{folder[name]}-{Folder[name]}-{FOLDER[NAME]}" + ) + + @patch("ayon_core.pipeline.create.product_name.get_project_settings") + @patch("ayon_core.pipeline.create.product_name.filter_profiles") + @patch("ayon_core.pipeline.create.product_name." + "is_product_base_type_supported") + def test_no_matching_profile_uses_default( + self, + mock_is_supported, + mock_filter_profiles, + mock_get_settings, + ): + mock_get_settings.return_value = { + "core": {"tools": {"creator": {"product_name_profiles": []}}} + } + mock_filter_profiles.return_value = None + mock_is_supported.return_value = False + + assert ( + get_product_name_template( + project_name="proj", + product_type="model", + task_name="modeling", + task_type="Modeling", + host_name="maya", + ) + == DEFAULT_PRODUCT_TEMPLATE + ) + + @patch("ayon_core.pipeline.create.product_name.get_project_settings") + @patch("ayon_core.pipeline.create.product_name.filter_profiles") + @patch("ayon_core.pipeline.create.product_name." + "is_product_base_type_supported") + def test_custom_default_template_used( + self, + mock_is_supported, + mock_filter_profiles, + mock_get_settings, + ): + mock_get_settings.return_value = { + "core": {"tools": {"creator": {"product_name_profiles": []}}} + } + mock_filter_profiles.return_value = None + mock_is_supported.return_value = False + + custom_default = "{variant}_{family}" + assert ( + get_product_name_template( + project_name="proj", + product_type="model", + task_name="modeling", + task_type="Modeling", + host_name="maya", + default_template=custom_default, + ) + == custom_default + ) + + @patch("ayon_core.pipeline.create.product_name.warn") + @patch("ayon_core.pipeline.create.product_name.get_project_settings") + @patch("ayon_core.pipeline.create.product_name.filter_profiles") + @patch("ayon_core.pipeline.create.product_name." + "is_product_base_type_supported") + def test_product_base_type_warns_when_supported_and_missing( + self, + mock_is_supported, + mock_filter_profiles, + mock_get_settings, + mock_warn, + ): + mock_get_settings.return_value = { + "core": {"tools": {"creator": {"product_name_profiles": []}}} + } + mock_filter_profiles.return_value = None + mock_is_supported.return_value = True + + get_product_name_template( + project_name="proj", + product_type="model", + task_name="modeling", + task_type="Modeling", + host_name="maya", + ) + mock_warn.assert_called_once() + + @patch("ayon_core.pipeline.create.product_name.get_project_settings") + @patch("ayon_core.pipeline.create.product_name.filter_profiles") + @patch("ayon_core.pipeline.create.product_name." + "is_product_base_type_supported") + def test_product_base_type_added_to_filtering_when_provided( + self, + mock_is_supported, + mock_filter_profiles, + mock_get_settings, + ): + mock_get_settings.return_value = { + "core": {"tools": {"creator": {"product_name_profiles": []}}} + } + mock_filter_profiles.return_value = None + mock_is_supported.return_value = True + + get_product_name_template( + project_name="proj", + product_type="model", + task_name="modeling", + task_type="Modeling", + host_name="maya", + product_base_type="asset", + ) + args, kwargs = mock_filter_profiles.call_args + # args[1] is filtering_criteria + assert args[1]["product_base_types"] == "asset" + + +class TestGetProductName: + @patch("ayon_core.pipeline.create.product_name.get_product_name_template") + @patch("ayon_core.pipeline.create.product_name." + "StringTemplate.format_strict_template") + @patch("ayon_core.pipeline.create.product_name.prepare_template_data") + def test_empty_product_type_returns_empty( + self, mock_prepare, mock_format, mock_get_tmpl + ): + assert ( + get_product_name( + project_name="proj", + task_name="modeling", + task_type="Modeling", + host_name="maya", + product_type="", + variant="Main", + ) + == "" + ) + mock_get_tmpl.assert_not_called() + mock_format.assert_not_called() + mock_prepare.assert_not_called() + + @patch("ayon_core.pipeline.create.product_name.get_product_name_template") + @patch("ayon_core.pipeline.create.product_name." + "StringTemplate.format_strict_template") + @patch("ayon_core.pipeline.create.product_name.prepare_template_data") + def test_happy_path( + self, mock_prepare, mock_format, mock_get_tmpl + ): + mock_get_tmpl.return_value = "{task[name]}_{product[type]}_{variant}" + mock_prepare.return_value = { + "task": {"name": "modeling"}, + "product": {"type": "model"}, + "variant": "Main", + "family": "model", + } + mock_format.return_value = "modeling_model_Main" + + result = get_product_name( + project_name="proj", + task_name="modeling", + task_type="Modeling", + host_name="maya", + product_type="model", + variant="Main", + ) + assert result == "modeling_model_Main" + mock_get_tmpl.assert_called_once() + mock_prepare.assert_called_once() + mock_format.assert_called_once() + + @patch("ayon_core.pipeline.create.product_name.get_product_name_template") + @patch("ayon_core.pipeline.create.product_name." + "StringTemplate.format_strict_template") + @patch("ayon_core.pipeline.create.product_name.prepare_template_data") + def test_product_name_with_base_type( + self, mock_prepare, mock_format, mock_get_tmpl + ): + mock_get_tmpl.return_value = "{task[name]}_{product[basetype]}_{variant}" + mock_prepare.return_value = { + "task": {"name": "modeling"}, + "product": {"type": "model"}, + "variant": "Main", + "family": "model", + } + mock_format.return_value = "modeling_modelBase_Main" + + result = get_product_name( + project_name="proj", + task_name="modeling", + task_type="Modeling", + host_name="maya", + product_type="model", + product_base_type="modelBase", + variant="Main", + ) + assert result == "modeling_modelBase_Main" + mock_get_tmpl.assert_called_once() + mock_prepare.assert_called_once() + mock_format.assert_called_once() + + @patch("ayon_core.pipeline.create.product_name.get_product_name_template") + def test_task_required_but_missing_raises(self, mock_get_tmpl): + mock_get_tmpl.return_value = "{task[name]}_{variant}" + with pytest.raises(TaskNotSetError): + get_product_name( + project_name="proj", + task_name="", + task_type="Modeling", + host_name="maya", + product_type="model", + variant="Main", + ) + + @patch("ayon_core.pipeline.create.product_name.get_product_name_template") + @patch("ayon_core.pipeline.create.product_name.ayon_api.get_project") + @patch("ayon_core.pipeline.create.product_name.StringTemplate." + "format_strict_template") + @patch("ayon_core.pipeline.create.product_name.prepare_template_data") + def test_task_short_name_is_used( + self, mock_prepare, mock_format, mock_get_project, mock_get_tmpl + ): + mock_get_tmpl.return_value = "{task[short]}_{variant}" + mock_get_project.return_value = { + "taskTypes": [{"name": "Modeling", "shortName": "mdl"}] + } + mock_prepare.return_value = {"task": {"short": "mdl"}, "variant": "Main"} + mock_format.return_value = "mdl_Main" + + result = get_product_name( + project_name="proj", + task_name="modeling", + task_type="Modeling", + host_name="maya", + product_type="model", + variant="Main", + ) + assert result == "mdl_Main" + + @patch("ayon_core.pipeline.create.product_name.get_product_name_template") + @patch("ayon_core.pipeline.create.product_name.StringTemplate." + "format_strict_template") + @patch("ayon_core.pipeline.create.product_name.prepare_template_data") + def test_template_fill_error_translated( + self, mock_prepare, mock_format, mock_get_tmpl + ): + mock_get_tmpl.return_value = "{missing_key}_{variant}" + mock_prepare.return_value = {"variant": "Main"} + mock_format.side_effect = KeyError("missing_key") + with pytest.raises(TemplateFillError): + get_product_name( + project_name="proj", + task_name="modeling", + task_type="Modeling", + host_name="maya", + product_type="model", + variant="Main", + ) + + @patch("ayon_core.pipeline.create.product_name.warn") + @patch("ayon_core.pipeline.create.product_name." + "is_product_base_type_supported") + @patch("ayon_core.pipeline.create.product_name.get_product_name_template") + @patch("ayon_core.pipeline.create.product_name." + "StringTemplate.format_strict_template") + @patch("ayon_core.pipeline.create.product_name.prepare_template_data") + def test_warns_when_template_needs_base_type_but_missing( + self, + mock_prepare, + mock_format, + mock_get_tmpl, + mock_is_supported, + mock_warn, + ): + mock_get_tmpl.return_value = "{product[basetype]}_{variant}" + mock_is_supported.return_value = True + mock_prepare.return_value = { + "product": {"type": "model"}, + "variant": "Main", + "family": "model", + } + mock_format.return_value = "asset_Main" + + _ = get_product_name( + project_name="proj", + task_name="modeling", + task_type="Modeling", + host_name="maya", + product_type="model", + variant="Main", + ) + mock_warn.assert_called_once() + + @patch("ayon_core.pipeline.create.product_name.get_product_name_template") + @patch("ayon_core.pipeline.create.product_name." + "StringTemplate.format_strict_template") + @patch("ayon_core.pipeline.create.product_name.prepare_template_data") + def test_dynamic_data_overrides_defaults( + self, mock_prepare, mock_format, mock_get_tmpl + ): + mock_get_tmpl.return_value = "{custom}_{variant}" + mock_prepare.return_value = {"custom": "overridden", "variant": "Main"} + mock_format.return_value = "overridden_Main" + + result = get_product_name( + project_name="proj", + task_name="modeling", + task_type="Modeling", + host_name="maya", + product_type="model", + variant="Main", + dynamic_data={"custom": "overridden"}, + ) + assert result == "overridden_Main" + + @patch("ayon_core.pipeline.create.product_name.get_product_name_template") + def test_product_type_filter_is_used(self, mock_get_tmpl): + mock_get_tmpl.return_value = DEFAULT_PRODUCT_TEMPLATE + _ = get_product_name( + project_name="proj", + task_name="modeling", + task_type="Modeling", + host_name="maya", + product_type="model", + variant="Main", + product_type_filter="look", + ) + args, kwargs = mock_get_tmpl.call_args + assert kwargs["product_type"] == "look" From 0ca2d25ef651775b7f27b4d6cacc18a40c41bdb1 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 17 Oct 2025 17:41:50 +0200 Subject: [PATCH 14/40] =?UTF-8?q?=E2=9A=97=EF=B8=8F=20fix=20linting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ayon_core/pipeline/create/test_product_name.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/client/ayon_core/pipeline/create/test_product_name.py b/tests/client/ayon_core/pipeline/create/test_product_name.py index b4507e39f1..a8a8566aa8 100644 --- a/tests/client/ayon_core/pipeline/create/test_product_name.py +++ b/tests/client/ayon_core/pipeline/create/test_product_name.py @@ -219,7 +219,9 @@ class TestGetProductName: def test_product_name_with_base_type( self, mock_prepare, mock_format, mock_get_tmpl ): - mock_get_tmpl.return_value = "{task[name]}_{product[basetype]}_{variant}" + mock_get_tmpl.return_value = ( + "{task[name]}_{product[basetype]}_{variant}" + ) mock_prepare.return_value = { "task": {"name": "modeling"}, "product": {"type": "model"}, @@ -267,7 +269,12 @@ class TestGetProductName: mock_get_project.return_value = { "taskTypes": [{"name": "Modeling", "shortName": "mdl"}] } - mock_prepare.return_value = {"task": {"short": "mdl"}, "variant": "Main"} + mock_prepare.return_value = { + "task": { + "short": "mdl" + }, + "variant": "Main" + } mock_format.return_value = "mdl_Main" result = get_product_name( From 04527b00616908377547a6c1a71a88c1a2db7f76 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 21 Nov 2025 19:06:36 +0100 Subject: [PATCH 15/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20change=20usage=20of?= =?UTF-8?q?=20product=5Fbase=5Ftypes=20in=20plugins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pipeline/create/creator_plugins.py | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 480ef28432..93dd763ed9 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -1,4 +1,6 @@ """Creator plugins for the create process.""" +from __future__ import annotations + import collections import copy import os @@ -7,7 +9,6 @@ from typing import TYPE_CHECKING, Any, Dict, Optional from warnings import warn from ayon_core.lib import Logger, get_version_from_path -from ayon_core.pipeline.compatibility import is_product_base_type_supported from ayon_core.pipeline.plugin_discover import ( deregister_plugin, deregister_plugin_path, @@ -274,7 +275,7 @@ class BaseCreator(ABC): # - those may be potential dangerous typos in settings if not hasattr(self, key): self.log.debug( - "Applying settings to unknown attribute '%' on '%'.", + "Applying settings to unknown attribute '%s' on '%s'.", key, cls_name ) setattr(self, key, value) @@ -293,11 +294,9 @@ class BaseCreator(ABC): Default implementation returns plugin's product type. """ - identifier = self.product_type - if is_product_base_type_supported(): - identifier = self.product_base_type - if self.product_type: - identifier = f"{identifier}.{self.product_type}" + identifier = self.product_base_type + if not identifier: + identifier = self.product_type return identifier @property @@ -399,19 +398,14 @@ class BaseCreator(ABC): if product_type is None: product_type = self.product_type - if ( - is_product_base_type_supported() - and not product_base_type - and not self.product_base_type - ): + if not product_base_type and not self.product_base_type: warn( f"Creator {self.identifier} does not support " "product base type. This will be required in future.", DeprecationWarning, stacklevel=2, ) - else: - product_base_type = self.product_base_type + product_base_type = product_type instance = CreatedInstance( product_type=product_type, @@ -534,7 +528,6 @@ class BaseCreator(ABC): 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. @@ -552,11 +545,11 @@ 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_product_base_type_supported() and (instance and hasattr(instance, "product_base_type")): # noqa: E501 - product_base_type = instance.product_base_type + product_base_type = None + if hasattr(self, "product_base_type"): # noqa: E501 + product_base_type = self.product_base_type if host_name is None: host_name = self.create_context.host_name @@ -589,7 +582,8 @@ class BaseCreator(ABC): dynamic_data=dynamic_data, project_settings=self.project_settings, project_entity=project_entity, - product_base_type=product_base_type + # until we make product_base_type mandatory + product_base_type=self.product_base_type ) def get_instance_attr_defs(self): From 2cf392633e24b4465e846dbd534bd7461730da44 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 26 Nov 2025 14:08:50 +0100 Subject: [PATCH 16/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20remove=20unnecessary?= =?UTF-8?q?=20checks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ayon_core/pipeline/create/product_name.py | 47 ++++++++++--------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index ab7de0c9e8..f1076e51b3 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -10,7 +10,6 @@ from ayon_core.lib import ( filter_profiles, prepare_template_data, ) -from ayon_core.pipeline.compatibility import is_product_base_type_supported from ayon_core.settings import get_project_settings from .constants import DEFAULT_PRODUCT_TEMPLATE @@ -36,10 +35,10 @@ 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 (Optional, str): 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 (Optional[dict[str, Any]]): Prepared settings for project. Settings are queried if not passed. product_base_type (Optional[str]): Base type of product. @@ -58,14 +57,16 @@ def get_product_name_template( "task_types": task_type } - if is_product_base_type_supported(): - 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) + if not product_base_type: + 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 + ) + filtering_criteria["product_base_types"] = product_base_type + matching_profile = filter_profiles(profiles, filtering_criteria) template = None @@ -192,16 +193,20 @@ def get_product_name( # 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_product_base_type_supported(): - 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) + product: dict[str, str] = { + "type": product_type, + "baseType": product_base_type + } + if not product_base_type and "{product[basetype]}" in template.lower(): + product["baseType"] = product_type + 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, From 05547c752ee788ec42fcf6c0b3235fc6f981353a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 26 Nov 2025 14:27:22 +0100 Subject: [PATCH 17/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20remove=20the=20check?= =?UTF-8?q?=20for=20product=20base=20type=20support=20-=20publisher=20mode?= =?UTF-8?q?l?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/ayon_core/tools/publisher/models/create.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index ed3fd04d5c..86f0cd2d07 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -35,7 +35,6 @@ from ayon_core.pipeline.create import ( ConvertorsOperationFailed, ConvertorItem, ) -from ayon_core.pipeline.compatibility import is_product_base_type_supported from ayon_core.tools.publisher.abstract import ( AbstractPublisherBackend, @@ -666,18 +665,14 @@ class CreateModel: kwargs = { "instance": instance, "project_entity": project_entity, + "product_base_type": creator.product_base_type, } - - if is_product_base_type_supported() 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( From b967f8f818b74e15ce62e06374c6213d721ecf1f Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 26 Nov 2025 15:01:25 +0100 Subject: [PATCH 18/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20consolidate=20warnin?= =?UTF-8?q?ings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/ayon_core/pipeline/create/context.py | 27 +++++++++---------- .../pipeline/create/creator_plugins.py | 11 -------- .../ayon_core/pipeline/create/product_name.py | 13 ++------- .../ayon_core/pipeline/create/structures.py | 16 +++-------- 4 files changed, 17 insertions(+), 50 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 6df437202e..0350c00977 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -29,7 +29,6 @@ from ayon_core.host import IWorkfileHost, IPublishHost 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.compatibility import is_product_base_type_supported from .exceptions import ( CreatorError, @@ -1237,6 +1236,15 @@ class CreateContext: """ creator = self._get_creator_in_create(creator_identifier) + 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 + ) + project_name = self.project_name if folder_entity is None: folder_path = self.get_current_folder_path() @@ -1290,23 +1298,12 @@ class CreateContext: "folderPath": folder_entity["path"], "task": task_entity["name"] if task_entity else None, "productType": creator.product_type, + # Add product base type if supported. Fallback to product type + "productBaseType": ( + creator.product_base_type or creator.product_type), "variant": variant } - # Add product base type if supported. - # TODO (antirotor): Once all creators support product base type - # remove this check. - if is_product_base_type_supported(): - - instance_data["productBaseType"] = creator.product_base_type - if creator.product_base_type is None: - msg = ( - f"Creator {creator_identifier} does not set " - "product base type. This will be required in future." - ) - warn(msg, DeprecationWarning, stacklevel=2) - self.log.warning(msg) - if active is not None: if not isinstance(active, bool): self.log.warning( diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 93dd763ed9..21d8596dea 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -6,7 +6,6 @@ import copy import os from abc import ABC, abstractmethod from typing import TYPE_CHECKING, Any, Dict, Optional -from warnings import warn from ayon_core.lib import Logger, get_version_from_path from ayon_core.pipeline.plugin_discover import ( @@ -399,12 +398,6 @@ class BaseCreator(ABC): product_type = self.product_type if not product_base_type and not self.product_base_type: - warn( - f"Creator {self.identifier} does not support " - "product base type. This will be required in future.", - DeprecationWarning, - stacklevel=2, - ) product_base_type = product_type instance = CreatedInstance( @@ -547,10 +540,6 @@ class BaseCreator(ABC): project_entity (Optional[dict[str, Any]]): Project entity. """ - product_base_type = None - if hasattr(self, "product_base_type"): # noqa: E501 - product_base_type = self.product_base_type - if host_name is None: host_name = self.create_context.host_name diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index f1076e51b3..c4ddb34652 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -57,16 +57,7 @@ def get_product_name_template( "task_types": task_type } - if not product_base_type: - 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 - ) filtering_criteria["product_base_types"] = product_base_type - matching_profile = filter_profiles(profiles, filtering_criteria) template = None @@ -127,8 +118,8 @@ def get_product_name( Args: project_name (str): Project name. - task_name (str): Task name. - task_type (str): Task type. + task_name (Optional[str]): Task name. + task_type (Optional[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. diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index e93270b357..dfa9d69938 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -4,7 +4,6 @@ from uuid import uuid4 from enum import Enum import typing from typing import Optional, Dict, List, Any -from warnings import warn from ayon_core.lib.attribute_definitions import ( AbstractAttrDef, @@ -521,17 +520,9 @@ class CreatedInstance: product_base_type: Optional[str] = None ): """Initialize CreatedInstance.""" - if is_product_base_type_supported(): - 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 + # fallback to product type for backward compatibility + if not product_base_type: + product_base_type = creator.product_base_type or product_type self._creator = creator creator_identifier = creator.identifier @@ -587,7 +578,6 @@ class CreatedInstance: self._data["productName"] = product_name if is_product_base_type_supported(): - data.pop("productBaseType", None) self._data["productBaseType"] = product_base_type self._data["active"] = data.get("active", True) From 1cddb86918fbd2806cba78c549c2a0ca971caa30 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 26 Nov 2025 15:17:19 +0100 Subject: [PATCH 19/40] =?UTF-8?q?=E2=9A=97=EF=B8=8F=20fix=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pipeline/create/test_product_name.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/tests/client/ayon_core/pipeline/create/test_product_name.py b/tests/client/ayon_core/pipeline/create/test_product_name.py index a8a8566aa8..03b13d2c25 100644 --- a/tests/client/ayon_core/pipeline/create/test_product_name.py +++ b/tests/client/ayon_core/pipeline/create/test_product_name.py @@ -20,7 +20,6 @@ class TestGetProductNameTemplate: "is_product_base_type_supported") def test_matching_profile_with_replacements( self, - mock_is_supported, mock_filter_profiles, mock_get_settings, ): @@ -33,7 +32,6 @@ class TestGetProductNameTemplate: "template": ("{task}-{Task}-{TASK}-{family}-{Family}" "-{FAMILY}-{asset}-{Asset}-{ASSET}") } - mock_is_supported.return_value = False result = get_product_name_template( project_name="proj", @@ -54,7 +52,6 @@ class TestGetProductNameTemplate: "is_product_base_type_supported") def test_no_matching_profile_uses_default( self, - mock_is_supported, mock_filter_profiles, mock_get_settings, ): @@ -62,7 +59,6 @@ class TestGetProductNameTemplate: "core": {"tools": {"creator": {"product_name_profiles": []}}} } mock_filter_profiles.return_value = None - mock_is_supported.return_value = False assert ( get_product_name_template( @@ -81,7 +77,6 @@ class TestGetProductNameTemplate: "is_product_base_type_supported") def test_custom_default_template_used( self, - mock_is_supported, mock_filter_profiles, mock_get_settings, ): @@ -89,7 +84,6 @@ class TestGetProductNameTemplate: "core": {"tools": {"creator": {"product_name_profiles": []}}} } mock_filter_profiles.return_value = None - mock_is_supported.return_value = False custom_default = "{variant}_{family}" assert ( @@ -111,7 +105,6 @@ class TestGetProductNameTemplate: "is_product_base_type_supported") def test_product_base_type_warns_when_supported_and_missing( self, - mock_is_supported, mock_filter_profiles, mock_get_settings, mock_warn, @@ -120,7 +113,6 @@ class TestGetProductNameTemplate: "core": {"tools": {"creator": {"product_name_profiles": []}}} } mock_filter_profiles.return_value = None - mock_is_supported.return_value = True get_product_name_template( project_name="proj", @@ -137,7 +129,6 @@ class TestGetProductNameTemplate: "is_product_base_type_supported") def test_product_base_type_added_to_filtering_when_provided( self, - mock_is_supported, mock_filter_profiles, mock_get_settings, ): @@ -145,7 +136,6 @@ class TestGetProductNameTemplate: "core": {"tools": {"creator": {"product_name_profiles": []}}} } mock_filter_profiles.return_value = None - mock_is_supported.return_value = True get_product_name_template( project_name="proj", @@ -308,8 +298,6 @@ class TestGetProductName: ) @patch("ayon_core.pipeline.create.product_name.warn") - @patch("ayon_core.pipeline.create.product_name." - "is_product_base_type_supported") @patch("ayon_core.pipeline.create.product_name.get_product_name_template") @patch("ayon_core.pipeline.create.product_name." "StringTemplate.format_strict_template") @@ -319,11 +307,10 @@ class TestGetProductName: mock_prepare, mock_format, mock_get_tmpl, - mock_is_supported, mock_warn, ): mock_get_tmpl.return_value = "{product[basetype]}_{variant}" - mock_is_supported.return_value = True + mock_prepare.return_value = { "product": {"type": "model"}, "variant": "Main", From 794bb716b268a385dd978dab1099bba13ffb01de Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 26 Nov 2025 15:25:54 +0100 Subject: [PATCH 20/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20small=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/ayon_core/pipeline/create/product_name.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index c4ddb34652..bf3d3b0abc 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -54,11 +54,9 @@ def get_product_name_template( "product_types": product_type, "hosts": host_name, "tasks": task_name, - "task_types": task_type + "task_types": task_type, + "product_base_types": product_base_type, } - - filtering_criteria["product_base_types"] = product_base_type - matching_profile = filter_profiles(profiles, filtering_criteria) template = None if matching_profile: From 00e2e3c2ade7b46c81377f083f8a321aae90c3e0 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 26 Nov 2025 15:33:43 +0100 Subject: [PATCH 21/40] =?UTF-8?q?=F0=9F=8E=9B=EF=B8=8F=20fix=20type=20hint?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/ayon_core/pipeline/create/product_name.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index bf3d3b0abc..f5a7418b57 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -83,8 +83,8 @@ def get_product_name_template( def get_product_name( project_name: str, task_name: str, - task_type: str, - host_name: str, + task_type: Optional[str], + host_name: Optional[str], product_type: str, variant: str, default_template: Optional[str] = None, From e6007b2cee79e82521850ce5d3658eb5a8d7279d Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 26 Nov 2025 17:25:10 +0100 Subject: [PATCH 22/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit type hints, checks --- client/ayon_core/pipeline/create/context.py | 18 +++++++++--------- .../pipeline/create/creator_plugins.py | 4 ++-- .../ayon_core/pipeline/create/product_name.py | 16 ++++++---------- client/ayon_core/pipeline/create/structures.py | 4 +--- 4 files changed, 18 insertions(+), 24 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 0350c00977..a09f1924da 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -766,6 +766,15 @@ class CreateContext: "and skipping: %s", creator_identifier, creator_class ) continue + if not creator_class.product_base_type: + warn( + f"Provided creator {creator_class!r} doesn't have " + "product base type attribute defined. This will be " + "required in future.", + DeprecationWarning, + stacklevel=2 + ) + continue # Filter by host name if ( @@ -1236,15 +1245,6 @@ class CreateContext: """ creator = self._get_creator_in_create(creator_identifier) - 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 - ) - project_name = self.project_name if folder_entity is None: folder_path = self.get_current_folder_path() diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 21d8596dea..92eb3b6946 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -405,7 +405,7 @@ class BaseCreator(ABC): product_name=product_name, data=data, creator=self, - product_base_type=product_base_type + product_base_type=product_base_type, ) self._add_instance_to_context(instance) return instance @@ -516,7 +516,7 @@ class BaseCreator(ABC): self, project_name: str, folder_entity: dict[str, Any], - task_entity: dict[str, Any], + task_entity: Optional[dict[str, Any]], variant: str, host_name: Optional[str] = None, instance: Optional[CreatedInstance] = None, diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index f5a7418b57..cc1014173c 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -82,9 +82,9 @@ def get_product_name_template( def get_product_name( project_name: str, - task_name: str, + task_name: Optional[str], task_type: Optional[str], - host_name: Optional[str], + host_name: str, product_type: str, variant: str, default_template: Optional[str] = None, @@ -180,14 +180,7 @@ def get_product_name( task_short = task_types_by_name.get(task_type, {}).get("shortName") task_value["short"] = task_short - # 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, - "baseType": product_base_type - } if not product_base_type and "{product[basetype]}" in template.lower(): - product["baseType"] = product_type warn( "You have Product base type in product name template, " "but it is not provided by the creator, please update your " @@ -200,7 +193,10 @@ def get_product_name( "variant": variant, "family": product_type, "task": task_value, - "product": product, + "product": { + "type": product_type, + "baseType": product_base_type or product_type, + } } if dynamic_data: diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index dfa9d69938..6f53a61b25 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -12,7 +12,6 @@ from ayon_core.lib.attribute_definitions import ( deserialize_attr_defs, ) -from ayon_core.pipeline.compatibility import is_product_base_type_supported from ayon_core.pipeline import ( AYON_INSTANCE_ID, @@ -577,8 +576,7 @@ class CreatedInstance: self._data["productType"] = product_type self._data["productName"] = product_name - if is_product_base_type_supported(): - self._data["productBaseType"] = product_base_type + self._data["productBaseType"] = product_base_type self._data["active"] = data.get("active", True) self._data["creator_identifier"] = creator_identifier From 700b025024faf91a54a904d10b0bd7b45fa81f6e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 26 Nov 2025 17:34:01 +0100 Subject: [PATCH 23/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20move=20plugin=20chec?= =?UTF-8?q?k=20earlier,=20fix=20hints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/ayon_core/pipeline/create/context.py | 16 +++++++++------- client/ayon_core/pipeline/create/product_name.py | 8 ++++---- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index a09f1924da..f1a5b0e9f8 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -759,13 +759,6 @@ class CreateContext: ) for creator_class in report.plugins: - creator_identifier = creator_class.identifier - if creator_identifier in creators: - self.log.warning( - "Duplicate Creator identifier: '%s'. Using first Creator " - "and skipping: %s", creator_identifier, creator_class - ) - continue if not creator_class.product_base_type: warn( f"Provided creator {creator_class!r} doesn't have " @@ -776,6 +769,15 @@ class CreateContext: ) continue + creator_identifier = creator_class.identifier + if creator_identifier in creators: + self.log.warning( + "Duplicate Creator identifier: '%s'. Using first Creator " + "and skipping: %s", creator_identifier, creator_class + ) + continue + + # Filter by host name if ( creator_class.host_name diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index cc1014173c..7f87145595 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -19,8 +19,8 @@ from .exceptions import TaskNotSetError, TemplateFillError def get_product_name_template( project_name: str, product_type: str, - task_name: str, - task_type: str, + task_name: Optional[str], + task_type: Optional[str], host_name: str, default_template: Optional[str] = None, project_settings: Optional[dict[str, Any]] = None, @@ -33,8 +33,8 @@ def get_product_name_template( product_type (str): Product type for which the product name is calculated. 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. + task_name (Optional[str]): Name of task in which context the product is created. + task_type (Optional[str]): Type of task in which context the product is created. 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. From bb430342d8b7b7d395d8af99916297f52353d047 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 26 Nov 2025 17:35:49 +0100 Subject: [PATCH 24/40] :dog: fix linter --- client/ayon_core/pipeline/create/context.py | 1 - client/ayon_core/pipeline/create/product_name.py | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index f1a5b0e9f8..2b9556d005 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -777,7 +777,6 @@ class CreateContext: ) continue - # Filter by host name if ( creator_class.host_name diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index 7f87145595..d59e8f9b67 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -33,8 +33,10 @@ def get_product_name_template( product_type (str): Product type for which the product name is calculated. host_name (str): Name of host in which the product name is calculated. - task_name (Optional[str]): Name of task in which context the product is created. - task_type (Optional[str]): Type of task in which context the product is created. + task_name (Optional[str]): Name of task in which context the + product is created. + task_type (Optional[str]): Type of task in which context the + product is created. 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. From b0005180f269f6139ca355c897052e4b417116eb Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 26 Nov 2025 17:40:27 +0100 Subject: [PATCH 25/40] :alembic: fix tests --- .../pipeline/create/test_product_name.py | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/tests/client/ayon_core/pipeline/create/test_product_name.py b/tests/client/ayon_core/pipeline/create/test_product_name.py index 03b13d2c25..7181e18b43 100644 --- a/tests/client/ayon_core/pipeline/create/test_product_name.py +++ b/tests/client/ayon_core/pipeline/create/test_product_name.py @@ -16,8 +16,6 @@ from ayon_core.pipeline.create.exceptions import ( class TestGetProductNameTemplate: @patch("ayon_core.pipeline.create.product_name.get_project_settings") @patch("ayon_core.pipeline.create.product_name.filter_profiles") - @patch("ayon_core.pipeline.create.product_name." - "is_product_base_type_supported") def test_matching_profile_with_replacements( self, mock_filter_profiles, @@ -48,8 +46,6 @@ class TestGetProductNameTemplate: @patch("ayon_core.pipeline.create.product_name.get_project_settings") @patch("ayon_core.pipeline.create.product_name.filter_profiles") - @patch("ayon_core.pipeline.create.product_name." - "is_product_base_type_supported") def test_no_matching_profile_uses_default( self, mock_filter_profiles, @@ -73,8 +69,6 @@ class TestGetProductNameTemplate: @patch("ayon_core.pipeline.create.product_name.get_project_settings") @patch("ayon_core.pipeline.create.product_name.filter_profiles") - @patch("ayon_core.pipeline.create.product_name." - "is_product_base_type_supported") def test_custom_default_template_used( self, mock_filter_profiles, @@ -98,35 +92,8 @@ class TestGetProductNameTemplate: == custom_default ) - @patch("ayon_core.pipeline.create.product_name.warn") @patch("ayon_core.pipeline.create.product_name.get_project_settings") @patch("ayon_core.pipeline.create.product_name.filter_profiles") - @patch("ayon_core.pipeline.create.product_name." - "is_product_base_type_supported") - def test_product_base_type_warns_when_supported_and_missing( - self, - mock_filter_profiles, - mock_get_settings, - mock_warn, - ): - mock_get_settings.return_value = { - "core": {"tools": {"creator": {"product_name_profiles": []}}} - } - mock_filter_profiles.return_value = None - - get_product_name_template( - project_name="proj", - product_type="model", - task_name="modeling", - task_type="Modeling", - host_name="maya", - ) - mock_warn.assert_called_once() - - @patch("ayon_core.pipeline.create.product_name.get_project_settings") - @patch("ayon_core.pipeline.create.product_name.filter_profiles") - @patch("ayon_core.pipeline.create.product_name." - "is_product_base_type_supported") def test_product_base_type_added_to_filtering_when_provided( self, mock_filter_profiles, From 3a24db94f51915440b0a963b1ef9402d1681f0c0 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 26 Nov 2025 17:45:17 +0100 Subject: [PATCH 26/40] :memo: log deprecation warning --- client/ayon_core/pipeline/create/context.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 2b9556d005..6495a9d6e9 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -760,13 +760,15 @@ class CreateContext: for creator_class in report.plugins: if not creator_class.product_base_type: - warn( - f"Provided creator {creator_class!r} doesn't have " + message = (f"Provided creator {creator_class!r} doesn't have " "product base type attribute defined. This will be " - "required in future.", + "required in future.") + warn( + message, DeprecationWarning, stacklevel=2 ) + self.log.warning(message) continue creator_identifier = creator_class.identifier From 64f549c4956376b2c8fc92fc80cecfeb136a6432 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 26 Nov 2025 17:48:52 +0100 Subject: [PATCH 27/40] ugly thing in name of compatibility? --- client/ayon_core/pipeline/create/product_name.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index d59e8f9b67..f10e375fb1 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -170,7 +170,7 @@ def get_product_name( "type": task_type, } if "{task}" in template.lower(): - task_value["name"] = task_name + task_value = task_name elif "{task[short]}" in template.lower(): if project_entity is None: From 1f88b0031dcdd72507c966cfb1dec0352fdc5ed3 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 26 Nov 2025 17:56:35 +0100 Subject: [PATCH 28/40] :recycle: fix discovery --- client/ayon_core/pipeline/create/context.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 6495a9d6e9..08b6574db8 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -769,7 +769,6 @@ class CreateContext: stacklevel=2 ) self.log.warning(message) - continue creator_identifier = creator_class.identifier if creator_identifier in creators: From 67364633f0d3f68278d1d292a83eebd5e6ffd925 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 27 Nov 2025 10:56:02 +0100 Subject: [PATCH 29/40] :robot: implement copilot suggestions --- client/ayon_core/pipeline/create/context.py | 41 +++++++++---------- .../pipeline/create/creator_plugins.py | 5 ++- .../ayon_core/pipeline/create/product_name.py | 2 +- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 08b6574db8..c379dd38f2 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -758,18 +758,6 @@ class CreateContext: "Skipping abstract Creator '%s'", str(creator_class) ) - for creator_class in report.plugins: - if not creator_class.product_base_type: - message = (f"Provided creator {creator_class!r} doesn't have " - "product base type attribute defined. This will be " - "required in future.") - warn( - message, - DeprecationWarning, - stacklevel=2 - ) - self.log.warning(message) - creator_identifier = creator_class.identifier if creator_identifier in creators: self.log.warning( @@ -783,19 +771,17 @@ class CreateContext: creator_class.host_name and creator_class.host_name != self.host_name ): - self.log.info(( - "Creator's host name \"{}\"" - " is not supported for current host \"{}\"" - ).format(creator_class.host_name, self.host_name)) + self.log.info( + ( + 'Creator\'s host name "{}"' + ' is not supported for current host "{}"' + ).format(creator_class.host_name, self.host_name) + ) continue # TODO report initialization error try: - creator = creator_class( - project_settings, - self, - self.headless - ) + creator = creator_class(project_settings, self, self.headless) except Exception: self.log.error( f"Failed to initialize plugin: {creator_class}", @@ -803,6 +789,19 @@ class CreateContext: ) continue + if not creator.product_base_type: + message = ( + f"Provided creator {creator!r} doesn't have " + "product base type attribute defined. This will be " + "required in future." + ) + warn( + message, + DeprecationWarning, + stacklevel=2 + ) + self.log.warning(message) + if not creator.enabled: disabled_creators[creator_identifier] = creator continue diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 92eb3b6946..8d1dbd0f2e 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -290,7 +290,8 @@ class BaseCreator(ABC): def identifier(self): """Identifier of creator (must be unique). - Default implementation returns plugin's product type. + Default implementation returns plugin's product base type, or falls back + to product type if product base type is not set. """ identifier = self.product_base_type @@ -388,7 +389,7 @@ class BaseCreator(ABC): product_type (Optional[str]): Product type, object attribute 'product_type' is used if not passed. product_base_type (Optional[str]): Product base type, object - attribute 'product_type' is used if not passed. + attribute 'product_base_type' is used if not passed. Returns: CreatedInstance: Created instance. diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index f10e375fb1..2bf84db0f4 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -197,7 +197,7 @@ def get_product_name( "task": task_value, "product": { "type": product_type, - "baseType": product_base_type or product_type, + "basetype": product_base_type or product_type, } } From f8e8ab2b27f0619b98bca16069da99d033ea0ee0 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 27 Nov 2025 10:58:13 +0100 Subject: [PATCH 30/40] :dog: fix long line --- client/ayon_core/pipeline/create/creator_plugins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 8d1dbd0f2e..a034451d83 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -290,8 +290,8 @@ class BaseCreator(ABC): def identifier(self): """Identifier of creator (must be unique). - Default implementation returns plugin's product base type, or falls back - to product type if product base type is not set. + Default implementation returns plugin's product base type, + or falls back to product type if product base type is not set. """ identifier = self.product_base_type From bb8f214e475ec657730f8bbb861e99896795005a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 27 Nov 2025 11:59:20 +0100 Subject: [PATCH 31/40] :bug: fix the abstract plugin debug print --- client/ayon_core/pipeline/create/context.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index c379dd38f2..d8cb9d1b9e 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -755,9 +755,11 @@ class CreateContext: self.creator_discover_result = report for creator_class in report.abstract_plugins: self.log.debug( - "Skipping abstract Creator '%s'", str(creator_class) + "Skipping abstract Creator '%s'", + str(creator_class) ) + for creator_class in report.plugins: creator_identifier = creator_class.identifier if creator_identifier in creators: self.log.warning( From feb16122009857c4192914b1b3b9d9ca4936d4f7 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 27 Nov 2025 17:18:35 +0100 Subject: [PATCH 32/40] =?UTF-8?q?=E2=9E=96=20remove=20argument?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/ayon_core/tools/publisher/models/create.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 86f0cd2d07..b8518a7de6 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -665,7 +665,6 @@ class CreateModel: kwargs = { "instance": instance, "project_entity": project_entity, - "product_base_type": creator.product_base_type, } # Backwards compatibility for 'project_entity' argument # - 'get_product_name' signature changed 24/07/08 From 43b557d95e10978090492434ad8282dcd79d0958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 1 Dec 2025 18:17:08 +0100 Subject: [PATCH 33/40] :recycle: check for compatibility --- client/ayon_core/pipeline/compatibility.py | 3 ++- client/ayon_core/pipeline/publish/lib.py | 6 ++--- client/ayon_core/plugins/publish/integrate.py | 25 ++++++++++++------- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/client/ayon_core/pipeline/compatibility.py b/client/ayon_core/pipeline/compatibility.py index f7d48526b7..dce627e391 100644 --- a/client/ayon_core/pipeline/compatibility.py +++ b/client/ayon_core/pipeline/compatibility.py @@ -13,4 +13,5 @@ def is_product_base_type_supported() -> bool: bool: True if product base types are supported, False otherwise. """ - return False + import ayon_api + return ayon_api.product_base_type_supported() diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index c2dcc89cd5..729e694c4f 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -122,8 +122,8 @@ def get_publish_template_name( task_type, project_settings=None, hero=False, + product_base_type: Optional[str] = None, logger=None, - product_base_type: Optional[str] = None ): """Get template name which should be used for passed context. @@ -141,10 +141,10 @@ def get_publish_template_name( task_type (str): Task type on which is instance working. project_settings (Dict[str, Any]): Prepared project settings. hero (bool): Template is for hero version publishing. - logger (logging.Logger): Custom logger used for 'filter_profiles' - function. product_base_type (Optional[str]): Product type for which should be found template. + logger (logging.Logger): Custom logger used for 'filter_profiles' + function. Returns: str: Template name which should be used for integration. diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index 4589f6f542..eaf82b37c4 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -28,6 +28,7 @@ from ayon_core.pipeline.publish import ( KnownPublishError, get_publish_template_name, ) +from pipeline import is_product_base_type_supported log = logging.getLogger(__name__) @@ -396,15 +397,21 @@ class IntegrateAsset(pyblish.api.InstancePlugin): product_id = None if existing_product_entity: product_id = existing_product_entity["id"] - product_entity = new_product_entity( - product_name, - product_type, - folder_entity["id"], - data=data, - attribs=attributes, - entity_id=product_id, - product_base_type=product_base_type - ) + + new_product_entity_kwargs = { + "product_name": product_name, + "product_type": product_type, + "folder_id": folder_entity["id"], + "data": data, + "attribs": attributes, + "entity_id": product_id, + "product_base_type": product_base_type, + } + + if not is_product_base_type_supported(): + new_product_entity_kwargs.pop("product_base_type") + + product_entity = new_product_entity(**new_product_entity_kwargs) if existing_product_entity is None: # Create a new product From 2efda3d3fec2745ef658b79a1f280d57177e827e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 2 Dec 2025 15:41:37 +0100 Subject: [PATCH 34/40] :bug: fix import and function call/check --- client/ayon_core/pipeline/compatibility.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/compatibility.py b/client/ayon_core/pipeline/compatibility.py index dce627e391..78ba5ad71e 100644 --- a/client/ayon_core/pipeline/compatibility.py +++ b/client/ayon_core/pipeline/compatibility.py @@ -1,4 +1,5 @@ """Package to handle compatibility checks for pipeline components.""" +import ayon_api def is_product_base_type_supported() -> bool: @@ -13,5 +14,7 @@ def is_product_base_type_supported() -> bool: bool: True if product base types are supported, False otherwise. """ - import ayon_api - return ayon_api.product_base_type_supported() + + if not hasattr(ayon_api, "is_product_base_type_supported"): + return False + return ayon_api.is_product_base_type_supported() From 1e6601786110e39f26ad26f7925471548cc45ab6 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 2 Dec 2025 15:47:39 +0100 Subject: [PATCH 35/40] :recycle: use product base type if defined when product base types are not supported by api, product base type should be the source of truth. --- client/ayon_core/plugins/publish/integrate.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index eaf82b37c4..e93cf62a3c 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -410,6 +410,18 @@ class IntegrateAsset(pyblish.api.InstancePlugin): if not is_product_base_type_supported(): new_product_entity_kwargs.pop("product_base_type") + if ( + product_base_type is not None + and product_base_type != product_type): + self.log.warning(( + "Product base type %s is not supported by the server, " + "but it's defined - and it differs from product type %s. " + "Using product base type as product type." + ), product_base_type, product_type) + + new_product_entity_kwargs["product_type"] = ( + product_base_type + ) product_entity = new_product_entity(**new_product_entity_kwargs) From 206bcfe7176f3960bc764b5eee323c224aeeca6e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 2 Dec 2025 15:47:58 +0100 Subject: [PATCH 36/40] :memo: add warning --- client/ayon_core/pipeline/publish/lib.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 729e694c4f..7365ffee09 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -149,6 +149,15 @@ def get_publish_template_name( Returns: str: Template name which should be used for integration. """ + if not product_base_type: + msg = ( + "Argument 'product_base_type' is not provided to" + " 'get_publish_template_name' function. This argument" + " will be required in future versions." + ) + warnings.warn(msg, DeprecationWarning) + if logger: + logger.warning(msg) template = None filter_criteria = { From a9c77857001bedb0b1f4dc5ec1b4e3b4599ac772 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 2 Dec 2025 15:59:14 +0100 Subject: [PATCH 37/40] :dog: fix linter --- client/ayon_core/plugins/publish/integrate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index e93cf62a3c..45209764ee 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -418,7 +418,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "but it's defined - and it differs from product type %s. " "Using product base type as product type." ), product_base_type, product_type) - + new_product_entity_kwargs["product_type"] = ( product_base_type ) From 24ff7f02d695a48511f323e180268a20a0cf4176 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 4 Dec 2025 18:05:42 +0100 Subject: [PATCH 38/40] Fix wrongly resolved line --- client/ayon_core/pipeline/create/product_name.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index 0cd3a1c51f..c4c27edc3b 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -54,7 +54,7 @@ def get_product_name_template( profiles = tools_settings["creator"]["product_name_profiles"] filtering_criteria = { "product_types": product_type, - "host_names": host_name,: host_name, + "host_names": host_name, "task_names": task_name, "task_types": task_type, "product_base_types": product_base_type, From f7f0005511c7aaa34e967100afa6db5f5ad53a1d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 5 Dec 2025 12:23:30 +0100 Subject: [PATCH 39/40] Fix import --- client/ayon_core/plugins/publish/integrate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index 45209764ee..8fce5574e9 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -28,7 +28,7 @@ from ayon_core.pipeline.publish import ( KnownPublishError, get_publish_template_name, ) -from pipeline import is_product_base_type_supported +from ayon_core.pipeline import is_product_base_type_supported log = logging.getLogger(__name__) From f665528ee7a64e416bab859537b24a8eaabaec00 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:36:01 +0100 Subject: [PATCH 40/40] fix 'product_name' to 'name' --- client/ayon_core/plugins/publish/integrate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index 8fce5574e9..9f24b35754 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -399,7 +399,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): product_id = existing_product_entity["id"] new_product_entity_kwargs = { - "product_name": product_name, + "name": product_name, "product_type": product_type, "folder_id": folder_entity["id"], "data": data,