🐶 some type hints and linting changes

This commit is contained in:
Ondrej Samohel 2025-05-16 18:13:00 +02:00
parent 4b3220c1de
commit f3beea1ff8
No known key found for this signature in database
GPG key ID: 02376E18990A97C6
5 changed files with 354 additions and 354 deletions

View file

@ -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",
)

View file

@ -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:
(<old value>, <new value>). 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

View file

@ -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",
)

View file

@ -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:

View file

@ -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():