From f3beea1ff85cf3c6a51f4ce0c2ec8347f501c5c6 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 16 May 2025 18:13:00 +0200 Subject: [PATCH] :dog: some type hints and linting changes --- client/ayon_core/pipeline/create/__init__.py | 162 +++++----- client/ayon_core/pipeline/create/changes.py | 135 +++++--- client/ayon_core/pipeline/create/constants.py | 6 +- client/ayon_core/pipeline/create/context.py | 305 ++++++++++-------- .../pipeline/create/creator_plugins.py | 100 ++---- 5 files changed, 354 insertions(+), 354 deletions(-) diff --git a/client/ayon_core/pipeline/create/__init__.py b/client/ayon_core/pipeline/create/__init__.py index ced43528eb..aaaa7c6225 100644 --- a/client/ayon_core/pipeline/create/__init__.py +++ b/client/ayon_core/pipeline/create/__init__.py @@ -1,123 +1,107 @@ +"""Create module for Ayon Core.""" from .constants import ( - PRODUCT_NAME_ALLOWED_SYMBOLS, DEFAULT_PRODUCT_TEMPLATE, - PRE_CREATE_THUMBNAIL_KEY, DEFAULT_VARIANT_VALUE, + PRE_CREATE_THUMBNAIL_KEY, + PRODUCT_NAME_ALLOWED_SYMBOLS, +) +from .context import CreateContext +from .creator_plugins import ( + AutoCreator, + BaseCreator, + Creator, + HiddenCreator, + cache_and_get_instances, + deregister_creator_plugin, + deregister_creator_plugin_path, + discover_creator_plugins, + discover_legacy_creator_plugins, + get_legacy_creator_by_name, + register_creator_plugin, + register_creator_plugin_path, ) from .exceptions import ( - UnavailableSharedData, - ImmutableKeyError, - HostMissRequiredMethod, - ConvertorsOperationFailed, - ConvertorsFindFailed, ConvertorsConversionFailed, + ConvertorsFindFailed, + ConvertorsOperationFailed, CreatorError, - CreatorsCreateFailed, CreatorsCollectionFailed, - CreatorsSaveFailed, - CreatorsRemoveFailed, + CreatorsCreateFailed, CreatorsOperationFailed, + CreatorsRemoveFailed, + CreatorsSaveFailed, + HostMissRequiredMethod, + ImmutableKeyError, TaskNotSetError, TemplateFillError, + UnavailableSharedData, +) +from .legacy_create import ( + LegacyCreator, + legacy_create, +) +from .product_name import ( + get_product_name, + get_product_name_template, ) from .structures import ( - CreatedInstance, - ConvertorItem, AttributeValues, + ConvertorItem, + CreatedInstance, CreatorAttributeValues, - PublishAttributeValues, PublishAttributes, + PublishAttributeValues, ) from .utils import ( get_last_versions_for_instances, get_next_versions_for_instances, ) -from .product_name import ( - get_product_name, - get_product_name_template, -) - -from .creator_plugins import ( - BaseCreator, - Creator, - AutoCreator, - HiddenCreator, - - discover_legacy_creator_plugins, - get_legacy_creator_by_name, - - discover_creator_plugins, - register_creator_plugin, - deregister_creator_plugin, - register_creator_plugin_path, - deregister_creator_plugin_path, - - cache_and_get_instances, -) - -from .context import CreateContext - -from .legacy_create import ( - LegacyCreator, - legacy_create, -) - - __all__ = ( - "PRODUCT_NAME_ALLOWED_SYMBOLS", "DEFAULT_PRODUCT_TEMPLATE", - "PRE_CREATE_THUMBNAIL_KEY", "DEFAULT_VARIANT_VALUE", + "PRE_CREATE_THUMBNAIL_KEY", + "PRODUCT_NAME_ALLOWED_SYMBOLS", - "UnavailableSharedData", - "ImmutableKeyError", - "HostMissRequiredMethod", - "ConvertorsOperationFailed", - "ConvertorsFindFailed", - "ConvertorsConversionFailed", - "CreatorError", - "CreatorsCreateFailed", - "CreatorsCollectionFailed", - "CreatorsSaveFailed", - "CreatorsRemoveFailed", - "CreatorsOperationFailed", - "TaskNotSetError", - "TemplateFillError", - - "CreatedInstance", - "ConvertorItem", "AttributeValues", + "AutoCreator", + "BaseCreator", + "ConvertorItem", + "ConvertorsConversionFailed", + "ConvertorsFindFailed", + "ConvertorsOperationFailed", + "CreateContext", + "CreatedInstance", + "Creator", "CreatorAttributeValues", + "CreatorError", + "CreatorError", + "CreatorsCollectionFailed", + "CreatorsCreateFailed", + "CreatorsOperationFailed", + "CreatorsRemoveFailed", + "CreatorsSaveFailed", + "HiddenCreator", + "HostMissRequiredMethod", + "ImmutableKeyError", + "LegacyCreator", "PublishAttributeValues", "PublishAttributes", - - "get_last_versions_for_instances", - "get_next_versions_for_instances", - - "get_product_name", - "get_product_name_template", - - "CreatorError", - - "BaseCreator", - "Creator", - "AutoCreator", - "HiddenCreator", - - "discover_legacy_creator_plugins", - "get_legacy_creator_by_name", - - "discover_creator_plugins", - "register_creator_plugin", - "deregister_creator_plugin", - "register_creator_plugin_path", - "deregister_creator_plugin_path", + "TaskNotSetError", + "TemplateFillError", + "UnavailableSharedData", "cache_and_get_instances", - - "CreateContext", - - "LegacyCreator", + "deregister_creator_plugin", + "deregister_creator_plugin_path", + "discover_creator_plugins", + "discover_legacy_creator_plugins", + "get_last_versions_for_instances", + "get_legacy_creator_by_name", + "get_next_versions_for_instances", + "get_product_name", + "get_product_name_template", "legacy_create", + "register_creator_plugin", + "register_creator_plugin_path", ) diff --git a/client/ayon_core/pipeline/create/changes.py b/client/ayon_core/pipeline/create/changes.py index c8b81cac48..0c8c35aa31 100644 --- a/client/ayon_core/pipeline/create/changes.py +++ b/client/ayon_core/pipeline/create/changes.py @@ -1,7 +1,16 @@ +"""Implementation of TrackChangesItem class.""" +from __future__ import annotations + import copy +from typing import TYPE_CHECKING, Any, Optional, TypeVar + +if TYPE_CHECKING: + from typing_extensions import Self _EMPTY_VALUE = object() +T = TypeVar("T", bound="TrackChangesItem") + class TrackChangesItem: """Helper object to track changes in data. @@ -39,26 +48,26 @@ class TrackChangesItem: ... "key_3": "value_3" ... } - >>> changes = TrackChangesItem(old_value, new_value) - >>> changes.changed + >>> tracked_changes = TrackChangesItem(old_value, new_value) + >>> tracked_changes.changed True - >>> changes["key_2"]["key_sub_1"].new_value is None + >>> tracked_changes["key_2"]["key_sub_1"].new_value is None True - >>> list(sorted(changes.changed_keys)) + >>> list(sorted(tracked_changes.changed_keys)) ['key_2', 'key_3'] - >>> changes["key_2"]["key_sub_2"]["enabled"].changed + >>> tracked_changes["key_2"]["key_sub_2"]["enabled"].changed True - >>> changes["key_2"].removed_keys + >>> tracked_changes["key_2"].removed_keys {'key_sub_1'} - >>> list(sorted(changes["key_2"].available_keys)) + >>> list(sorted(tracked_changes["key_2"].available_keys)) ['key_sub_1', 'key_sub_2', 'key_sub_3'] - >>> changes.new_value == new_value + >>> tracked_changes.new_value == new_value True # Get only changed values @@ -73,7 +82,8 @@ class TrackChangesItem: new_value (Any): New value. """ - def __init__(self, old_value, new_value): + def __init__(self, old_value: Any, new_value: Any): # noqa: ANN401 + """Constructor of TrackChangesItem.""" self._changed = old_value != new_value # Resolve if value is '_EMPTY_VALUE' after comparison of the values if old_value is _EMPTY_VALUE: @@ -86,76 +96,94 @@ class TrackChangesItem: self._old_is_dict = isinstance(old_value, dict) self._new_is_dict = isinstance(new_value, dict) - self._old_keys = None - self._new_keys = None - self._available_keys = None - self._removed_keys = None + self._old_keys: set[str] = set() + self._new_keys: set[str] = set() + self._available_keys: set[str] = set() + self._removed_keys: set[str] = set() - self._changed_keys = None + self._changed_keys: set[str] = set() - self._sub_items = None + self._sub_items: dict[str, Self] = {} - def __getitem__(self, key): - """Getter looks into subitems if object is dictionary.""" + def __getitem__(self, key: str) -> Self: + """Getter looks into subitems if object is dictionary. - if self._sub_items is None: + Args: + key (str): Key to get sub item. + + Returns: + TrackChangesItem: Sub item of the object. + + """ + if not self._sub_items: self._prepare_sub_items() - return self._sub_items[key] + # ignore mypy error as Self won't work in Python 3.9 yet + return self._sub_items[key] # type: ignore[return-value] def __bool__(self): - """Boolean of object is if old and new value are the same.""" + """Boolean of object is if old and new value are the same. + Returns: + bool: If object is changed. + + """ return self._changed - def get(self, key, default=None): - """Try to get sub item.""" + def get(self, + key: str, default: Optional[Self] = None) -> Self: + """Try to get sub item. - if self._sub_items is None: + Args: + key (str): Key to get sub item. + default (Optional[str]): Default value if key is not found. + + Returns: + TrackChangesItem: Sub item of the object. + + """ + if not self._sub_items: self._prepare_sub_items() - return self._sub_items.get(key, default) + return self._sub_items.get( + key, default) # type: ignore[return-value, arg-type] @property - def old_value(self): + def old_value(self) -> Any: # noqa: ANN401 """Get copy of old value. Returns: Any: Whatever old value was. """ - return copy.deepcopy(self._old_value) @property - def new_value(self): + def new_value(self) -> Any: # noqa: ANN401 """Get copy of new value. Returns: Any: Whatever new value was. """ - return copy.deepcopy(self._new_value) @property - def changed(self): + def changed(self) -> bool: """Value changed. Returns: bool: If data changed. """ - return self._changed @property - def is_dict(self): + def is_dict(self) -> bool: """Object can be used as dictionary. Returns: bool: When can be used that way. """ - return self._old_is_dict or self._new_is_dict @property - def changes(self): + def changes(self) -> dict[str, tuple]: """Get changes in raw data. This method should be used only if 'is_dict' value is 'True'. @@ -165,68 +193,67 @@ class TrackChangesItem: (, ). If 'is_dict' is 'False' then output is always empty dictionary. """ - - output = {} + output: dict[str, tuple] = {} if not self.is_dict: return output old_value = self.old_value new_value = self.new_value for key in self.changed_keys: - _old = None - _new = None + old = None + new = None if self._old_is_dict: - _old = old_value.get(key) + old = old_value.get(key) if self._new_is_dict: - _new = new_value.get(key) - output[key] = (_old, _new) + new = new_value.get(key) + output[key] = (old, new) return output # Methods/properties that can be used when 'is_dict' is 'True' @property - def old_keys(self): + def old_keys(self) -> set[str]: """Keys from old value. Empty set is returned if old value is not a dict. Returns: Set[str]: Keys from old value. - """ + """ if self._old_keys is None: self._prepare_keys() return set(self._old_keys) @property - def new_keys(self): + def new_keys(self) -> set[str]: """Keys from new value. Empty set is returned if old value is not a dict. Returns: Set[str]: Keys from new value. - """ + """ if self._new_keys is None: self._prepare_keys() return set(self._new_keys) @property - def changed_keys(self): + def changed_keys(self) -> set[str]: """Keys that has changed from old to new value. Empty set is returned if both old and new value are not a dict. Returns: Set[str]: Keys of changed keys. - """ + """ if self._changed_keys is None: self._prepare_sub_items() return set(self._changed_keys) @property - def available_keys(self): + def available_keys(self) -> set[str]: """All keys that are available in old and new value. Empty set is returned if both old and new value are not a dict. @@ -234,25 +261,25 @@ class TrackChangesItem: Returns: Set[str]: All keys from old and new value. - """ + """ if self._available_keys is None: self._prepare_keys() return set(self._available_keys) @property - def removed_keys(self): + def removed_keys(self) -> set[str]: """Key that are not available in new value but were in old value. Returns: Set[str]: All removed keys. - """ + """ if self._removed_keys is None: self._prepare_sub_items() return set(self._removed_keys) - def _prepare_keys(self): + def _prepare_keys(self) -> None: old_keys = set() new_keys = set() if self._old_is_dict and self._new_is_dict: @@ -270,7 +297,8 @@ class TrackChangesItem: self._available_keys = old_keys | new_keys self._removed_keys = old_keys - new_keys - def _prepare_sub_items(self): + def _prepare_sub_items(self) -> None: + """Prepare sub items and changed keys.""" sub_items = {} changed_keys = set() @@ -309,5 +337,6 @@ class TrackChangesItem: _EMPTY_VALUE, new_value.get(key) ) - self._sub_items = sub_items + # this is also not resolved correctly in Python 3.9 with Self type + self._sub_items = sub_items # type: ignore[assignment] self._changed_keys = changed_keys diff --git a/client/ayon_core/pipeline/create/constants.py b/client/ayon_core/pipeline/create/constants.py index a0bcea55ff..1fc98c4b6d 100644 --- a/client/ayon_core/pipeline/create/constants.py +++ b/client/ayon_core/pipeline/create/constants.py @@ -1,3 +1,5 @@ +"""Constants for the create module.""" + PRODUCT_NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_." DEFAULT_PRODUCT_TEMPLATE = "{family}{Variant}" PRE_CREATE_THUMBNAIL_KEY = "thumbnail_source" @@ -5,8 +7,8 @@ DEFAULT_VARIANT_VALUE = "Main" __all__ = ( - "PRODUCT_NAME_ALLOWED_SYMBOLS", "DEFAULT_PRODUCT_TEMPLATE", - "PRE_CREATE_THUMBNAIL_KEY", "DEFAULT_VARIANT_VALUE", + "PRE_CREATE_THUMBNAIL_KEY", + "PRODUCT_NAME_ALLOWED_SYMBOLS", ) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index f0d9fa8927..9fa4da3f34 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -1,74 +1,114 @@ +"""Context of instance creation.""" +from __future__ import annotations + +import collections +import copy +import dataclasses +import inspect +import logging import os import sys -import copy -import logging import traceback -import collections -import inspect -from contextlib import contextmanager import typing +from contextlib import contextmanager from typing import ( - Optional, - Iterable, - Tuple, - List, - Set, - Dict, Any, Callable, + Dict, + Iterable, + List, + NamedTuple, + Optional, + Set, + Tuple, Union, ) -import pyblish.logic -import pyblish.api import ayon_api +import pyblish.api +import pyblish.logic -from ayon_core.settings import get_project_settings -from ayon_core.lib import is_func_signature_supported -from ayon_core.lib.events import QueuedEventSystem -from ayon_core.lib.attribute_definitions import get_default_values from ayon_core.host import IPublishHost, IWorkfileHost +from ayon_core.lib import is_func_signature_supported +from ayon_core.lib.attribute_definitions import get_default_values +from ayon_core.lib.events import QueuedEventSystem 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.template_data import get_template_data +from ayon_core.settings import get_project_settings -from .exceptions import ( - CreatorError, - CreatorsCreateFailed, - CreatorsCollectionFailed, - CreatorsSaveFailed, - CreatorsRemoveFailed, - ConvertorsFindFailed, - ConvertorsConversionFailed, - UnavailableSharedData, - HostMissRequiredMethod, -) from .changes import TrackChangesItem -from .structures import PublishAttributes, ConvertorItem, InstanceContextInfo from .creator_plugins import ( - Creator, AutoCreator, - discover_creator_plugins, + Creator, discover_convertor_plugins, + discover_creator_plugins, ) -if typing.TYPE_CHECKING: - from .structures import CreatedInstance - -# Import of functions and classes that were moved to different file -# TODO Should be removed in future release - Added 24/08/28, 0.4.3-dev.1 from .exceptions import ( - ImmutableKeyError, # noqa: F401 - CreatorsOperationFailed, # noqa: F401 - ConvertorsOperationFailed, # noqa: F401 -) -from .structures import ( - AttributeValues, # noqa: F401 - CreatorAttributeValues, # noqa: F401 - PublishAttributeValues, # noqa: F401 + ConvertorsConversionFailed, + ConvertorsFindFailed, + CreatorError, + CreatorsCollectionFailed, + CreatorsCreateFailed, + CreatorsRemoveFailed, + CreatorsSaveFailed, + HostMissRequiredMethod, + UnavailableSharedData, ) +from .structures import ConvertorItem, InstanceContextInfo, PublishAttributes + +if typing.TYPE_CHECKING: + from ayon_core.host import HostBase + from ayon_core.pipeline.create.structures import CreatedInstance + # Changes of instances and context are send as tuple of 2 information -UpdateData = collections.namedtuple("UpdateData", ["instance", "changes"]) +class UpdateData(NamedTuple): + """"Update data for instance. + + Args: + instance (CreatedInstance): Instance which was changed. + changes (dict[str, Any]): Changes of instance. + """ + + # Instance which was changed + instance: Optional[CreatedInstance] + # Changes of instance. + changes: dict[str, Any] + + +@dataclasses.dataclass +class ConvertorOperationInfo: + """Convertor operation information. + + Args: + identifier (str): Identifier of convertor. + message (str): Message of convertor. + traceback (str): Traceback of convertor. + """ + + identifier: str + message: str + traceback: str + + +@dataclasses.dataclass +class CreatorOperationInfo: + """Creator operation information. + + Args: + creator_identifier (str): Identifier of creator. + creator_label (str): Label of creator. + message (str): Message of convertor. + traceback (str): Traceback of convertor. + """ + + creator_identifier: str + creator_label: str + message: str + traceback: str + + _NOT_SET = object() INSTANCE_ADDED_TOPIC = "instances.added" @@ -79,22 +119,45 @@ CREATE_ATTR_DEFS_CHANGED_TOPIC = "create.attr.defs.changed" PUBLISH_ATTR_DEFS_CHANGED_TOPIC = "publish.attr.defs.changed" -def prepare_failed_convertor_operation_info(identifier, exc_info): +def prepare_failed_convertor_operation_info( + identifier: str, exc_info: tuple) -> ConvertorOperationInfo: + """Prepare convertor operation information. + + Returns: + ConvertorOperationInfo: Information about convertor operation. + + """ exc_type, exc_value, exc_traceback = exc_info formatted_traceback = "".join(traceback.format_exception( exc_type, exc_value, exc_traceback )) - return { - "convertor_identifier": identifier, - "message": str(exc_value), - "traceback": formatted_traceback - } + return ConvertorOperationInfo( + identifier=identifier, + message=str(exc_value), + traceback=formatted_traceback + ) def prepare_failed_creator_operation_info( - identifier, label, exc_info, add_traceback=True -): + identifier: str, + label: str, + exc_info: tuple, + *, + add_traceback: bool = True +) -> CreatorOperationInfo: + """Prepare creator operation information. + + Args: + identifier (str): Identifier of creator. + label (str): Label of creator. + exc_info (tuple): Exception information. + add_traceback (bool): Add traceback to the message. + + Returns: + CreatorOperationInfo: Information about creator operation. + + """ formatted_traceback = None exc_type, exc_value, exc_traceback = exc_info if add_traceback: @@ -102,15 +165,16 @@ def prepare_failed_creator_operation_info( exc_type, exc_value, exc_traceback )) - return { - "creator_identifier": identifier, - "creator_label": label, - "message": str(exc_value), - "traceback": formatted_traceback - } + return CreatorOperationInfo( + creator_identifier=identifier, + creator_label=label, + message=str(exc_value), + traceback=formatted_traceback + ) class BulkInfo: + """Bulk information.""" def __init__(self): self._count = 0 self._data = [] @@ -146,18 +210,19 @@ class BulkInfo: return data -class CreateContext: +class CreateContext: # noqa: PLR0904 """Context of instance creation. Context itself also can store data related to whole creation (workfile). - those are mainly for Context publish plugins - Todos: - Don't use 'AvalonMongoDB'. It's used only to keep track about current - context which should be handled by host. + Todo: + This is now using PLR0904 to handle "too many public method" issue. + It has around 70 of them and that is too much for one class. We + should split this class into multiple classes. Args: - host(ModuleType): Host implementation which handles implementation and + host(HostBase): Host implementation which handles implementation and global metadata. headless(bool): Context is created out of UI (Current not used). reset(bool): Reset context on initialization. @@ -166,8 +231,14 @@ class CreateContext: """ def __init__( - self, host, headless=False, reset=True, discover_publish_plugins=True + self, + host: HostBase, + *, + headless: bool = False, + reset: bool = True, + discover_publish_plugins: bool = True, ): + """Initialization of CreateContext.""" self.host = host # Prepare attribute for logger (Created on demand in `log` property) @@ -185,12 +256,11 @@ class CreateContext: if missing_methods: host_is_valid = False joined_methods = ", ".join( - ['"{}"'.format(name) for name in missing_methods] + [f'"{name}"' for name in missing_methods] ) - self.log.warning(( + self.log.warning( "Host miss required methods to be able use creation." - " Missing methods: {}" - ).format(joined_methods)) + " Missing methods: %s", joined_methods) self._current_project_name = None self._current_folder_path = None @@ -281,7 +351,7 @@ class CreateContext: def get_instance_by_id( self, instance_id: str - ) -> Optional["CreatedInstance"]: + ) -> Optional[CreatedInstance]: """Receive instance by id. Args: @@ -326,7 +396,6 @@ class CreateContext: Returns: List[BaseCreator]: Sorted creator plugins by 'order' value. """ - return self.get_sorted_creators() @property @@ -336,23 +405,18 @@ class CreateContext: Returns: List[AutoCreator]: Sorted plugins by 'order' value. """ - return sorted( self.autocreators.values(), key=lambda creator: creator.order ) @classmethod - def get_host_misssing_methods(cls, host): + def get_host_misssing_methods(cls, host: HostBase) -> Set[str]: """Collect missing methods from host. Args: host(ModuleType): Host implementaion. """ - - missing = set( - IPublishHost.get_missing_publish_methods(host) - ) - return missing + return set(IPublishHost.get_missing_publish_methods(host)) @property def host_is_valid(self): @@ -371,7 +435,6 @@ class CreateContext: Returns: Union[str, None]: Project name. """ - return self._current_project_name def get_current_folder_path(self) -> Optional[str]: @@ -380,7 +443,6 @@ class CreateContext: Returns: Union[str, None]: Folder path. """ - return self._current_folder_path def get_current_task_name(self) -> Optional[str]: @@ -389,7 +451,6 @@ class CreateContext: Returns: Union[str, None]: Task name. """ - return self._current_task_name def get_current_task_type(self) -> Optional[str]: @@ -460,7 +521,6 @@ class CreateContext: Returns: Union[str, None]: Workfile path. """ - return self._current_workfile_path def get_current_project_anatomy(self): @@ -469,7 +529,6 @@ class CreateContext: Returns: Anatomy: Anatomy object ready to be used. """ - if self._current_project_anatomy is None: self._current_project_anatomy = Anatomy( self._current_project_name) @@ -521,7 +580,6 @@ class CreateContext: Returns: bool: Context changed. """ - project_name, folder_path, task_name, workfile_path = ( self._get_current_host_context() ) @@ -547,7 +605,6 @@ class CreateContext: All changes will be lost if were not saved explicitely. """ - self.reset_preparation() self.reset_current_context() @@ -567,7 +624,6 @@ class CreateContext: Remove all thumbnail filepaths that are empty or lead to files which does not exist or of instances that are not available anymore. """ - invalid = set() for instance_id, path in self.thumbnail_paths_by_instance_id.items(): instance_available = True @@ -586,7 +642,6 @@ class CreateContext: def reset_preparation(self): """Prepare attributes that must be prepared/cleaned before reset.""" - # Give ability to store shared data for collection phase self._collection_shared_data = {} @@ -600,7 +655,6 @@ class CreateContext: def reset_finalization(self): """Cleanup of attributes after reset.""" - # Stop access to collection shared data self._collection_shared_data = None self.refresh_thumbnails() @@ -635,7 +689,6 @@ class CreateContext: as current context information as that's where the metadata are stored. We should store the workfile (if is available) too. """ - project_name, folder_path, task_name, workfile_path = ( self._get_current_host_context() ) @@ -659,16 +712,13 @@ class CreateContext: Reloads creators from preregistered paths and can load publish plugins if it's enabled on context. """ - self._reset_publish_plugins(discover_publish_plugins) self._reset_creator_plugins() self._reset_convertor_plugins() def _reset_publish_plugins(self, discover_publish_plugins): from ayon_core.pipeline import AYONPyblishPluginMixin - from ayon_core.pipeline.publish import ( - publish_plugins_discover - ) + from ayon_core.pipeline.publish import publish_plugins_discover discover_result = DiscoverResult(pyblish.api.Plugin) plugins_with_defs = [] @@ -732,7 +782,7 @@ class CreateContext: for creator_class in report.plugins: if inspect.isabstract(creator_class): self.log.debug( - "Skipping abstract Creator {}".format(str(creator_class)) + f"Skipping abstract Creator {creator_class!s}" ) continue @@ -749,10 +799,10 @@ 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( + f"Creator's host name \"{creator_class.host_name}\"" + f' is not supported for current host "{self.host_name}"' + ) continue # TODO report initialization error @@ -791,16 +841,16 @@ class CreateContext: for convertor_class in report.plugins: if inspect.isabstract(convertor_class): self.log.info( - "Skipping abstract Creator {}".format(str(convertor_class)) + f"Skipping abstract Creator {convertor_class!s}" ) continue convertor_identifier = convertor_class.identifier if convertor_identifier in convertors_plugins: - self.log.warning(( + self.log.warning( "Duplicated Converter identifier. " "Using first and skipping following" - )) + ) continue convertors_plugins[convertor_identifier] = convertor_class(self) @@ -1033,7 +1083,6 @@ class CreateContext: def context_data_changes(self): """Changes of attributes.""" - return TrackChangesItem( self._original_context_data, self.context_data_to_store() ) @@ -1053,7 +1102,7 @@ class CreateContext: None, plugin_name ) - def creator_adds_instance(self, instance: "CreatedInstance"): + def creator_adds_instance(self, instance: CreatedInstance): """Creator adds new instance to context. Instances should be added only from creators. @@ -1066,9 +1115,9 @@ class CreateContext: """ # Add instance to instances list if instance.id in self._instances_by_id: - self.log.warning(( - "Instance with id {} is already added to context." - ).format(instance.id)) + self.log.warning( + f"Instance with id {instance.id} is already added to context." + ) return self._instances_by_id[instance.id] = instance @@ -1093,12 +1142,11 @@ class CreateContext: Raises: CreatorError: When identifier is not known. """ - creator = self.creators.get(identifier) # Fake CreatorError (Could be maybe specific exception?) if creator is None: raise CreatorError( - "Creator {} was not found".format(identifier) + f"Creator {identifier} was not found" ) return creator @@ -1147,7 +1195,7 @@ class CreateContext: ) if folder_entity is None: raise CreatorError( - "Folder '{}' was not found".format(folder_path) + f"Folder '{folder_path}' was not found" ) if task_entity is None: @@ -1233,7 +1281,7 @@ class CreateContext: raise CreatorsCreateFailed([fail_info]) return result - def creator_removed_instance(self, instance: "CreatedInstance"): + def creator_removed_instance(self, instance: CreatedInstance): """When creator removes instance context should be acknowledged. If creator removes instance context should know about it to avoid @@ -1243,7 +1291,6 @@ class CreateContext: instance (CreatedInstance): Object of instance which was removed from scene metadata. """ - self._remove_instances([instance]) def add_convertor_item(self, convertor_identifier, label): @@ -1424,7 +1471,6 @@ class CreateContext: ConvertorsFindFailed: When one or more convertors fails during finding. """ - self.convertor_items_by_id = {} failed_info = [] @@ -1439,9 +1485,7 @@ class CreateContext: ) ) self.log.warning( - "Failed to find instances of convertor \"{}\"".format( - convertor.identifier - ), + f'Failed to find instances of convertor "{convertor.identifier}"', exc_info=True ) @@ -1453,7 +1497,6 @@ class CreateContext: Reset instances if any autocreator executed properly. """ - failed_info = [] for creator in self.sorted_autocreators: identifier = creator.identifier @@ -1474,10 +1517,7 @@ class CreateContext: Dict[str, Optional[Dict[str, Any]]]: Folder entities by path. """ - output = { - folder_path: None - for folder_path in folder_paths - } + output = dict.fromkeys(folder_paths) remainder_paths = set() for folder_path in output: # Skip invalid folder paths (folder name or empty path) @@ -1649,7 +1689,7 @@ class CreateContext: return output.get(folder_path, {}).get(task_name) def get_instances_folder_entities( - self, instances: Optional[Iterable["CreatedInstance"]] = None + self, instances: Optional[Iterable[CreatedInstance]] = None ) -> Dict[str, Optional[Dict[str, Any]]]: if instances is None: instances = self._instances_by_id.values() @@ -1673,7 +1713,7 @@ class CreateContext: return output def get_instances_task_entities( - self, instances: Optional[Iterable["CreatedInstance"]] = None + self, instances: Optional[Iterable[CreatedInstance]] = None ): """Get task entities for instances. @@ -1719,7 +1759,7 @@ class CreateContext: return output def get_instances_context_info( - self, instances: Optional[Iterable["CreatedInstance"]] = None + self, instances: Optional[Iterable[CreatedInstance]] = None ) -> Dict[str, InstanceContextInfo]: """Validate 'folder' and 'task' instance context. @@ -1894,7 +1934,7 @@ class CreateContext: if not instances_by_identifier: return - error_message = "Instances update of creator \"{}\" failed. {}" + error_message = 'Instances update of creator "{}" failed. {}' failed_info = [] for creator in self.get_sorted_creators( @@ -1967,7 +2007,7 @@ class CreateContext: self._remove_instances(instances, sender) - error_message = "Instances removement of creator \"{}\" failed. {}" + error_message = 'Instances removement of creator "{}" failed. {}' failed_info = [] # Remove instances by creator plugin order for creator in self.get_sorted_creators( @@ -2022,7 +2062,6 @@ class CreateContext: Raises: UnavailableSharedData: When called out of collection phase. """ - if self._collection_shared_data is None: raise UnavailableSharedData( "Accessed Collection shared data out of collection phase" @@ -2037,7 +2076,6 @@ class CreateContext: Args: convertor_identifier (str): Identifier of convertor. """ - convertor = self.convertors_plugins.get(convertor_identifier) if convertor is not None: convertor.convert() @@ -2055,7 +2093,6 @@ class CreateContext: Raises: ConvertorsConversionFailed: When one or more convertors fails. """ - failed_info = [] for convertor_identifier in convertor_identifiers: try: @@ -2068,9 +2105,7 @@ class CreateContext: ) ) self.log.warning( - "Failed to convert instances of convertor \"{}\"".format( - convertor_identifier - ), + f'Failed to convert instances of convertor "{convertor_identifier}"', exc_info=True ) @@ -2101,7 +2136,7 @@ class CreateContext: def _create_with_unified_error( self, identifier, creator, *args, **kwargs ): - error_message = "Failed to run Creator with identifier \"{}\". {}" + error_message = 'Failed to run Creator with identifier "{}". {}' label = None add_traceback = False @@ -2201,7 +2236,7 @@ class CreateContext: def _bulk_add_instances_finished( self, - instances_to_validate: List["CreatedInstance"], + instances_to_validate: List[CreatedInstance], sender: Optional[str] ): if not instances_to_validate: @@ -2264,7 +2299,7 @@ class CreateContext: def _bulk_remove_instances_finished( self, - instances_to_remove: List["CreatedInstance"], + instances_to_remove: List[CreatedInstance], sender: Optional[str] ): if not instances_to_remove: diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index cbc06145fb..0160ffffe1 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -1,32 +1,34 @@ -# -*- coding: utf-8 -*- -import os -import copy +"""Definition of creator plugins.""" import collections -from typing import TYPE_CHECKING, Optional, Dict, Any - +import copy +import logging +import os from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any, Dict, Optional -from ayon_core.settings import get_project_settings from ayon_core.lib import Logger, get_version_from_path 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.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 + +# Avoid cyclic imports 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 + + from .context import CreateContext class ProductConvertorPlugin(ABC): @@ -58,17 +60,17 @@ class ProductConvertorPlugin(ABC): _log = None - def __init__(self, create_context): + def __init__(self, create_context: CreateContext): + """Constructor of convertor plugin.""" self._create_context = create_context @property - def log(self): + def log(self) -> logging.Logger: """Logger of the plugin. 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,8 +88,6 @@ class ProductConvertorPlugin(ABC): str: Converted identifier unique for all converters in host. """ - pass - @abstractmethod def find_instances(self): """Look for legacy instances in the scene. @@ -96,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. @@ -111,7 +107,6 @@ class ProductConvertorPlugin(ABC): Returns: CreateContext: Context which initialized the plugin. """ - return self._create_context @property @@ -124,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): @@ -133,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) @@ -221,7 +213,6 @@ class BaseCreator(ABC): Returns: Optional[dict[str, Any]]: Settings values or None. """ - settings = project_settings.get(category_name) if not settings: return None @@ -267,7 +258,6 @@ class BaseCreator(ABC): Args: project_settings (dict[str, Any]): Project settings. """ - settings_category = self.settings_category if not settings_category: return @@ -279,18 +269,16 @@ 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( - key, cls_name - )) + self.log.debug( + f"Applying settings to unknown attribute '{key}' on '{cls_name}'." + ) setattr(self, key, value) def register_callbacks(self): @@ -299,15 +287,13 @@ class BaseCreator(ABC): Default implementation does nothing. It can be overridden to register callbacks for creator. """ - pass @property def identifier(self): """Identifier of creator (must be unique). - Default implementation returns plugin's product type. + Default implementation returns plugin's product base type. """ - return self.product_type @property @@ -315,8 +301,6 @@ class BaseCreator(ABC): def product_type(self): """Family that plugin represents.""" - pass - @property def project_name(self): """Current project name. @@ -324,7 +308,6 @@ class BaseCreator(ABC): Returns: str: Name of a project. """ - return self.create_context.project_name @property @@ -334,7 +317,6 @@ class BaseCreator(ABC): Returns: Anatomy: Project anatomy object. """ - return self.create_context.project_anatomy @property @@ -352,7 +334,6 @@ class BaseCreator(ABC): str: Group label that can be used for grouping of instances in UI. Group label can be overridden by instance itself. """ - if self._cached_group_label is None: label = self.identifier if self.group_label: @@ -369,7 +350,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 @@ -414,7 +394,6 @@ class BaseCreator(ABC): Args: instance (CreatedInstance): New created instance. """ - self.create_context.creator_adds_instance(instance) def _remove_instance_from_context(self, instance): @@ -427,7 +406,6 @@ class BaseCreator(ABC): Args: instance (CreatedInstance): Instance which should be removed. """ - self.create_context.creator_removed_instance(instance) @abstractmethod @@ -439,8 +417,6 @@ class BaseCreator(ABC): implementation """ - pass - @abstractmethod def collect_instances(self): """Collect existing instances related to this creator plugin. @@ -466,8 +442,6 @@ class BaseCreator(ABC): ``` """ - pass - @abstractmethod def update_instances(self, update_list): """Store changes of existing instances so they can be recollected. @@ -477,8 +451,6 @@ class BaseCreator(ABC): contain changed instance and it's changes. """ - pass - @abstractmethod def remove_instances(self, instances): """Method called on instance removal. @@ -491,14 +463,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( @@ -514,7 +483,6 @@ class BaseCreator(ABC): These may be dynamically created based on current context of workfile. """ - return {} def get_product_name( @@ -585,7 +553,7 @@ class BaseCreator(ABC): and values are stored to metadata for future usage and for publishing purposes. - NOTE: + Note: Convert method should be implemented which should care about updating keys/values when plugin attributes change. @@ -593,7 +561,6 @@ class BaseCreator(ABC): list[AbstractAttrDef]: Attribute definitions that can be tweaked for created instance. """ - return self.instance_attr_defs def get_attr_defs_for_instance(self, instance): @@ -616,12 +583,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 ) @@ -642,7 +607,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 ) @@ -709,7 +673,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 @@ -724,11 +687,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. @@ -736,7 +697,6 @@ class Creator(BaseCreator): Returns: str: Short description of product type. """ - return self.description def get_detail_description(self): @@ -747,7 +707,6 @@ class Creator(BaseCreator): Returns: str: Detailed description of product type for artist. """ - return self.detailed_description def get_default_variants(self): @@ -761,7 +720,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): @@ -781,7 +739,6 @@ class Creator(BaseCreator): Returns: str: Variant value. """ - if only_explicit or self._default_variant: return self._default_variant @@ -802,7 +759,6 @@ class Creator(BaseCreator): Returns: str: Variant value. """ - return self.get_default_variant() def _set_default_variant_wrap(self, variant): @@ -814,7 +770,6 @@ class Creator(BaseCreator): Args: variant (str): New default variant value. """ - self._default_variant = variant default_variant = property( @@ -964,7 +919,6 @@ class AutoCreator(BaseCreator): def remove_instances(self, instances): """Skip removal.""" - pass def discover_creator_plugins(*args, **kwargs): @@ -988,9 +942,7 @@ def discover_legacy_creator_plugins(): plugin.apply_settings(project_settings) except Exception: log.warning( - "Failed to apply settings to creator {}".format( - plugin.__name__ - ), + f"Failed to apply settings to creator {plugin.__name__}", exc_info=True ) return plugins @@ -1007,7 +959,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() @@ -1079,7 +1030,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():