From 8e2c0045489f4f9d99af963a88e6c42db92b2ceb Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 20 Jun 2024 16:35:31 +0200 Subject: [PATCH 01/15] moved 'PublishReportMaker' from control file --- client/ayon_core/tools/publisher/control.py | 264 +---------------- .../tools/publisher/models/__init__.py | 6 + .../tools/publisher/models/publish.py | 266 ++++++++++++++++++ 3 files changed, 274 insertions(+), 262 deletions(-) create mode 100644 client/ayon_core/tools/publisher/models/__init__.py create mode 100644 client/ayon_core/tools/publisher/models/publish.py diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index 4a8ec72e4f..2b5e957811 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -11,11 +11,11 @@ import re import six -import arrow import pyblish.api import ayon_api from ayon_core.lib.events import QueuedEventSystem +from ayon_core.lib.profiles_filtering import filter_profiles from ayon_core.lib.attribute_definitions import ( UIDef, serialize_attr_defs, @@ -38,9 +38,8 @@ CreatorsOperationFailed, ConvertorsOperationFailed, ) -from ayon_core.pipeline.publish import get_publish_instance_label +from ayon_core.tools.publisher.models import PublishReportMaker from ayon_core.tools.common_models import ProjectsModel, HierarchyModel -from ayon_core.lib.profiles_filtering import filter_profiles # Define constant for plugin orders offset PLUGIN_ORDER_OFFSET = 0.5 @@ -64,265 +63,6 @@ def process(self): self.callback(*self.args, **self.kwargs) -class PublishReportMaker: - """Report for single publishing process. - - Report keeps current state of publishing and currently processed plugin. - """ - - def __init__(self, controller): - self.controller = controller - self._create_discover_result = None - self._convert_discover_result = None - self._publish_discover_result = None - - self._plugin_data_by_id = {} - self._current_plugin = None - self._current_plugin_data = {} - self._all_instances_by_id = {} - self._current_context = None - - def reset(self, context, create_context): - """Reset report and clear all data.""" - - self._create_discover_result = create_context.creator_discover_result - self._convert_discover_result = ( - create_context.convertor_discover_result - ) - self._publish_discover_result = create_context.publish_discover_result - - self._plugin_data_by_id = {} - self._current_plugin = None - self._current_plugin_data = {} - self._all_instances_by_id = {} - self._current_context = context - - for plugin in create_context.publish_plugins_mismatch_targets: - plugin_data = self._add_plugin_data_item(plugin) - plugin_data["skipped"] = True - - def add_plugin_iter(self, plugin, context): - """Add report about single iteration of plugin.""" - for instance in context: - self._all_instances_by_id[instance.id] = instance - - if self._current_plugin_data: - self._current_plugin_data["passed"] = True - - self._current_plugin = plugin - self._current_plugin_data = self._add_plugin_data_item(plugin) - - def _add_plugin_data_item(self, plugin): - if plugin.id in self._plugin_data_by_id: - # A plugin would be processed more than once. What can cause it: - # - there is a bug in controller - # - plugin class is imported into multiple files - # - this can happen even with base classes from 'pyblish' - raise ValueError( - "Plugin '{}' is already stored".format(str(plugin))) - - plugin_data_item = self._create_plugin_data_item(plugin) - self._plugin_data_by_id[plugin.id] = plugin_data_item - - return plugin_data_item - - def _create_plugin_data_item(self, plugin): - label = None - if hasattr(plugin, "label"): - label = plugin.label - - return { - "id": plugin.id, - "name": plugin.__name__, - "label": label, - "order": plugin.order, - "targets": list(plugin.targets), - "instances_data": [], - "actions_data": [], - "skipped": False, - "passed": False - } - - def set_plugin_skipped(self): - """Set that current plugin has been skipped.""" - self._current_plugin_data["skipped"] = True - - def add_result(self, result): - """Handle result of one plugin and it's instance.""" - - instance = result["instance"] - instance_id = None - if instance is not None: - instance_id = instance.id - self._current_plugin_data["instances_data"].append({ - "id": instance_id, - "logs": self._extract_instance_log_items(result), - "process_time": result["duration"] - }) - - def add_action_result(self, action, result): - """Add result of single action.""" - plugin = result["plugin"] - - store_item = self._plugin_data_by_id.get(plugin.id) - if store_item is None: - store_item = self._add_plugin_data_item(plugin) - - action_name = action.__name__ - action_label = action.label or action_name - log_items = self._extract_log_items(result) - store_item["actions_data"].append({ - "success": result["success"], - "name": action_name, - "label": action_label, - "logs": log_items - }) - - def get_report(self, publish_plugins=None): - """Report data with all details of current state.""" - - now = arrow.utcnow().to("local") - instances_details = {} - for instance in self._all_instances_by_id.values(): - instances_details[instance.id] = self._extract_instance_data( - instance, instance in self._current_context - ) - - plugins_data_by_id = copy.deepcopy( - self._plugin_data_by_id - ) - - # Ensure the current plug-in is marked as `passed` in the result - # so that it shows on reports for paused publishes - if self._current_plugin is not None: - current_plugin_data = plugins_data_by_id.get( - self._current_plugin.id - ) - if current_plugin_data and not current_plugin_data["passed"]: - current_plugin_data["passed"] = True - - if publish_plugins: - for plugin in publish_plugins: - if plugin.id not in plugins_data_by_id: - plugins_data_by_id[plugin.id] = \ - self._create_plugin_data_item(plugin) - - reports = [] - if self._create_discover_result is not None: - reports.append(self._create_discover_result) - - if self._convert_discover_result is not None: - reports.append(self._convert_discover_result) - - if self._publish_discover_result is not None: - reports.append(self._publish_discover_result) - - crashed_file_paths = {} - for report in reports: - items = report.crashed_file_paths.items() - for filepath, exc_info in items: - crashed_file_paths[filepath] = "".join( - traceback.format_exception(*exc_info) - ) - - return { - "plugins_data": list(plugins_data_by_id.values()), - "instances": instances_details, - "context": self._extract_context_data(self._current_context), - "crashed_file_paths": crashed_file_paths, - "id": uuid.uuid4().hex, - "created_at": now.isoformat(), - "report_version": "1.0.1", - } - - def _extract_context_data(self, context): - context_label = "Context" - if context is not None: - context_label = context.data.get("label") - return { - "label": context_label - } - - def _extract_instance_data(self, instance, exists): - return { - "name": instance.data.get("name"), - "label": get_publish_instance_label(instance), - "product_type": instance.data.get("productType"), - "family": instance.data.get("family"), - "families": instance.data.get("families") or [], - "exists": exists, - "creator_identifier": instance.data.get("creator_identifier"), - "instance_id": instance.data.get("instance_id"), - } - - def _extract_instance_log_items(self, result): - instance = result["instance"] - instance_id = None - if instance: - instance_id = instance.id - - log_items = self._extract_log_items(result) - for item in log_items: - item["instance_id"] = instance_id - return log_items - - def _extract_log_items(self, result): - output = [] - records = result.get("records") or [] - for record in records: - record_exc_info = record.exc_info - if record_exc_info is not None: - record_exc_info = "".join( - traceback.format_exception(*record_exc_info) - ) - - try: - msg = record.getMessage() - except Exception: - msg = str(record.msg) - - output.append({ - "type": "record", - "msg": msg, - "name": record.name, - "lineno": record.lineno, - "levelno": record.levelno, - "levelname": record.levelname, - "threadName": record.threadName, - "filename": record.filename, - "pathname": record.pathname, - "msecs": record.msecs, - "exc_info": record_exc_info - }) - - exception = result.get("error") - if exception: - fname, line_no, func, exc = exception.traceback - - # Conversion of exception into string may crash - try: - msg = str(exception) - except BaseException: - msg = ( - "Publisher Controller: ERROR" - " - Failed to get exception message" - ) - - # Action result does not have 'is_validation_error' - is_validation_error = result.get("is_validation_error", False) - output.append({ - "type": "error", - "is_validation_error": is_validation_error, - "msg": msg, - "filename": str(fname), - "lineno": str(line_no), - "func": str(func), - "traceback": exception.formatted_traceback - }) - - return output - - class PublishPluginsProxy: """Wrapper around publish plugin. diff --git a/client/ayon_core/tools/publisher/models/__init__.py b/client/ayon_core/tools/publisher/models/__init__.py new file mode 100644 index 0000000000..3b7117953e --- /dev/null +++ b/client/ayon_core/tools/publisher/models/__init__.py @@ -0,0 +1,6 @@ +from .publish import PublishReportMaker + + +__all__ = ( + "PublishReportMaker", +) diff --git a/client/ayon_core/tools/publisher/models/publish.py b/client/ayon_core/tools/publisher/models/publish.py new file mode 100644 index 0000000000..152bb4cc82 --- /dev/null +++ b/client/ayon_core/tools/publisher/models/publish.py @@ -0,0 +1,266 @@ +import uuid +import copy +import traceback + +import arrow + +from ayon_core.pipeline.publish import get_publish_instance_label + + +class PublishReportMaker: + """Report for single publishing process. + + Report keeps current state of publishing and currently processed plugin. + """ + + def __init__(self, controller): + self.controller = controller + self._create_discover_result = None + self._convert_discover_result = None + self._publish_discover_result = None + + self._plugin_data_by_id = {} + self._current_plugin = None + self._current_plugin_data = {} + self._all_instances_by_id = {} + self._current_context = None + + def reset(self, context, create_context): + """Reset report and clear all data.""" + + self._create_discover_result = create_context.creator_discover_result + self._convert_discover_result = ( + create_context.convertor_discover_result + ) + self._publish_discover_result = create_context.publish_discover_result + + self._plugin_data_by_id = {} + self._current_plugin = None + self._current_plugin_data = {} + self._all_instances_by_id = {} + self._current_context = context + + for plugin in create_context.publish_plugins_mismatch_targets: + plugin_data = self._add_plugin_data_item(plugin) + plugin_data["skipped"] = True + + def add_plugin_iter(self, plugin, context): + """Add report about single iteration of plugin.""" + for instance in context: + self._all_instances_by_id[instance.id] = instance + + if self._current_plugin_data: + self._current_plugin_data["passed"] = True + + self._current_plugin = plugin + self._current_plugin_data = self._add_plugin_data_item(plugin) + + def _add_plugin_data_item(self, plugin): + if plugin.id in self._plugin_data_by_id: + # A plugin would be processed more than once. What can cause it: + # - there is a bug in controller + # - plugin class is imported into multiple files + # - this can happen even with base classes from 'pyblish' + raise ValueError( + "Plugin '{}' is already stored".format(str(plugin))) + + plugin_data_item = self._create_plugin_data_item(plugin) + self._plugin_data_by_id[plugin.id] = plugin_data_item + + return plugin_data_item + + def _create_plugin_data_item(self, plugin): + label = None + if hasattr(plugin, "label"): + label = plugin.label + + return { + "id": plugin.id, + "name": plugin.__name__, + "label": label, + "order": plugin.order, + "targets": list(plugin.targets), + "instances_data": [], + "actions_data": [], + "skipped": False, + "passed": False + } + + def set_plugin_skipped(self): + """Set that current plugin has been skipped.""" + self._current_plugin_data["skipped"] = True + + def add_result(self, result): + """Handle result of one plugin and it's instance.""" + + instance = result["instance"] + instance_id = None + if instance is not None: + instance_id = instance.id + self._current_plugin_data["instances_data"].append({ + "id": instance_id, + "logs": self._extract_instance_log_items(result), + "process_time": result["duration"] + }) + + def add_action_result(self, action, result): + """Add result of single action.""" + plugin = result["plugin"] + + store_item = self._plugin_data_by_id.get(plugin.id) + if store_item is None: + store_item = self._add_plugin_data_item(plugin) + + action_name = action.__name__ + action_label = action.label or action_name + log_items = self._extract_log_items(result) + store_item["actions_data"].append({ + "success": result["success"], + "name": action_name, + "label": action_label, + "logs": log_items + }) + + def get_report(self, publish_plugins=None): + """Report data with all details of current state.""" + + now = arrow.utcnow().to("local") + instances_details = {} + for instance in self._all_instances_by_id.values(): + instances_details[instance.id] = self._extract_instance_data( + instance, instance in self._current_context + ) + + plugins_data_by_id = copy.deepcopy( + self._plugin_data_by_id + ) + + # Ensure the current plug-in is marked as `passed` in the result + # so that it shows on reports for paused publishes + if self._current_plugin is not None: + current_plugin_data = plugins_data_by_id.get( + self._current_plugin.id + ) + if current_plugin_data and not current_plugin_data["passed"]: + current_plugin_data["passed"] = True + + if publish_plugins: + for plugin in publish_plugins: + if plugin.id not in plugins_data_by_id: + plugins_data_by_id[plugin.id] = \ + self._create_plugin_data_item(plugin) + + reports = [] + if self._create_discover_result is not None: + reports.append(self._create_discover_result) + + if self._convert_discover_result is not None: + reports.append(self._convert_discover_result) + + if self._publish_discover_result is not None: + reports.append(self._publish_discover_result) + + crashed_file_paths = {} + for report in reports: + items = report.crashed_file_paths.items() + for filepath, exc_info in items: + crashed_file_paths[filepath] = "".join( + traceback.format_exception(*exc_info) + ) + + return { + "plugins_data": list(plugins_data_by_id.values()), + "instances": instances_details, + "context": self._extract_context_data(self._current_context), + "crashed_file_paths": crashed_file_paths, + "id": uuid.uuid4().hex, + "created_at": now.isoformat(), + "report_version": "1.0.1", + } + + def _extract_context_data(self, context): + context_label = "Context" + if context is not None: + context_label = context.data.get("label") + return { + "label": context_label + } + + def _extract_instance_data(self, instance, exists): + return { + "name": instance.data.get("name"), + "label": get_publish_instance_label(instance), + "product_type": instance.data.get("productType"), + "family": instance.data.get("family"), + "families": instance.data.get("families") or [], + "exists": exists, + "creator_identifier": instance.data.get("creator_identifier"), + "instance_id": instance.data.get("instance_id"), + } + + def _extract_instance_log_items(self, result): + instance = result["instance"] + instance_id = None + if instance: + instance_id = instance.id + + log_items = self._extract_log_items(result) + for item in log_items: + item["instance_id"] = instance_id + return log_items + + def _extract_log_items(self, result): + output = [] + records = result.get("records") or [] + for record in records: + record_exc_info = record.exc_info + if record_exc_info is not None: + record_exc_info = "".join( + traceback.format_exception(*record_exc_info) + ) + + try: + msg = record.getMessage() + except Exception: + msg = str(record.msg) + + output.append({ + "type": "record", + "msg": msg, + "name": record.name, + "lineno": record.lineno, + "levelno": record.levelno, + "levelname": record.levelname, + "threadName": record.threadName, + "filename": record.filename, + "pathname": record.pathname, + "msecs": record.msecs, + "exc_info": record_exc_info + }) + + exception = result.get("error") + if exception: + fname, line_no, func, exc = exception.traceback + + # Conversion of exception into string may crash + try: + msg = str(exception) + except BaseException: + msg = ( + "Publisher Controller: ERROR" + " - Failed to get exception message" + ) + + # Action result does not have 'is_validation_error' + is_validation_error = result.get("is_validation_error", False) + output.append({ + "type": "error", + "is_validation_error": is_validation_error, + "msg": msg, + "filename": str(fname), + "lineno": str(line_no), + "func": str(func), + "traceback": exception.formatted_traceback + }) + + return output From a4d71a7ba6cb2696ae40b35aea79fb2adbbbacf0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 20 Jun 2024 16:39:37 +0200 Subject: [PATCH 02/15] moved CreateItem and related classes to create model --- client/ayon_core/tools/publisher/control.py | 176 +----------------- .../tools/publisher/models/__init__.py | 3 + .../tools/publisher/models/create.py | 167 +++++++++++++++++ 3 files changed, 176 insertions(+), 170 deletions(-) create mode 100644 client/ayon_core/tools/publisher/models/create.py diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index 2b5e957811..fc7f0dc11d 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -16,11 +16,7 @@ from ayon_core.lib.events import QueuedEventSystem from ayon_core.lib.profiles_filtering import filter_profiles -from ayon_core.lib.attribute_definitions import ( - UIDef, - serialize_attr_defs, - deserialize_attr_defs, -) +from ayon_core.lib.attribute_definitions import UIDef from ayon_core.pipeline import ( PublishValidationError, KnownPublishError, @@ -28,18 +24,16 @@ get_process_id, OptionalPyblishPluginMixin, ) -from ayon_core.pipeline.create import ( - CreateContext, - AutoCreator, - HiddenCreator, - Creator, -) +from ayon_core.pipeline.create import CreateContext from ayon_core.pipeline.create.context import ( CreatorsOperationFailed, ConvertorsOperationFailed, ) -from ayon_core.tools.publisher.models import PublishReportMaker from ayon_core.tools.common_models import ProjectsModel, HierarchyModel +from ayon_core.tools.publisher.models import ( + PublishReportMaker, + CreatorItem, +) # Define constant for plugin orders offset PLUGIN_ORDER_OFFSET = 0.5 @@ -472,164 +466,6 @@ def add_error(self, plugin, error, instance): self._plugin_action_items[plugin_id] = plugin_actions -class CreatorType: - def __init__(self, name): - self.name = name - - def __str__(self): - return self.name - - def __eq__(self, other): - return self.name == str(other) - - def __ne__(self, other): - # This is implemented only because of Python 2 - return not self == other - - -class CreatorTypes: - base = CreatorType("base") - auto = CreatorType("auto") - hidden = CreatorType("hidden") - artist = CreatorType("artist") - - @classmethod - def from_str(cls, value): - for creator_type in ( - cls.base, - cls.auto, - cls.hidden, - cls.artist - ): - if value == creator_type: - return creator_type - raise ValueError("Unknown type \"{}\"".format(str(value))) - - -class CreatorItem: - """Wrapper around Creator plugin. - - Object can be serialized and recreated. - """ - - def __init__( - self, - identifier, - creator_type, - product_type, - label, - group_label, - icon, - description, - detailed_description, - default_variant, - default_variants, - create_allow_context_change, - create_allow_thumbnail, - show_order, - pre_create_attributes_defs, - ): - self.identifier = identifier - self.creator_type = creator_type - self.product_type = product_type - self.label = label - self.group_label = group_label - self.icon = icon - self.description = description - self.detailed_description = detailed_description - self.default_variant = default_variant - self.default_variants = default_variants - self.create_allow_context_change = create_allow_context_change - self.create_allow_thumbnail = create_allow_thumbnail - self.show_order = show_order - self.pre_create_attributes_defs = pre_create_attributes_defs - - def get_group_label(self): - return self.group_label - - @classmethod - def from_creator(cls, creator): - if isinstance(creator, AutoCreator): - creator_type = CreatorTypes.auto - elif isinstance(creator, HiddenCreator): - creator_type = CreatorTypes.hidden - elif isinstance(creator, Creator): - creator_type = CreatorTypes.artist - else: - creator_type = CreatorTypes.base - - description = None - detail_description = None - default_variant = None - default_variants = None - pre_create_attr_defs = None - create_allow_context_change = None - create_allow_thumbnail = None - show_order = creator.order - if creator_type is CreatorTypes.artist: - description = creator.get_description() - detail_description = creator.get_detail_description() - default_variant = creator.get_default_variant() - default_variants = creator.get_default_variants() - pre_create_attr_defs = creator.get_pre_create_attr_defs() - create_allow_context_change = creator.create_allow_context_change - create_allow_thumbnail = creator.create_allow_thumbnail - show_order = creator.show_order - - identifier = creator.identifier - return cls( - identifier, - creator_type, - creator.product_type, - creator.label or identifier, - creator.get_group_label(), - creator.get_icon(), - description, - detail_description, - default_variant, - default_variants, - create_allow_context_change, - create_allow_thumbnail, - show_order, - pre_create_attr_defs, - ) - - def to_data(self): - pre_create_attributes_defs = None - if self.pre_create_attributes_defs is not None: - pre_create_attributes_defs = serialize_attr_defs( - self.pre_create_attributes_defs - ) - - return { - "identifier": self.identifier, - "creator_type": str(self.creator_type), - "product_type": self.product_type, - "label": self.label, - "group_label": self.group_label, - "icon": self.icon, - "description": self.description, - "detailed_description": self.detailed_description, - "default_variant": self.default_variant, - "default_variants": self.default_variants, - "create_allow_context_change": self.create_allow_context_change, - "create_allow_thumbnail": self.create_allow_thumbnail, - "show_order": self.show_order, - "pre_create_attributes_defs": pre_create_attributes_defs, - } - - @classmethod - def from_data(cls, data): - pre_create_attributes_defs = data["pre_create_attributes_defs"] - if pre_create_attributes_defs is not None: - data["pre_create_attributes_defs"] = deserialize_attr_defs( - pre_create_attributes_defs - ) - - data["creator_type"] = CreatorTypes.from_str(data["creator_type"]) - return cls(**data) - - @six.add_metaclass(ABCMeta) class AbstractPublisherController(object): """Publisher tool controller. diff --git a/client/ayon_core/tools/publisher/models/__init__.py b/client/ayon_core/tools/publisher/models/__init__.py index 3b7117953e..a89f8e0d52 100644 --- a/client/ayon_core/tools/publisher/models/__init__.py +++ b/client/ayon_core/tools/publisher/models/__init__.py @@ -1,6 +1,9 @@ +from .create import CreatorItem from .publish import PublishReportMaker __all__ = ( + "CreatorItem", + "PublishReportMaker", ) diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py new file mode 100644 index 0000000000..5c4c75a0ca --- /dev/null +++ b/client/ayon_core/tools/publisher/models/create.py @@ -0,0 +1,167 @@ +from ayon_core.lib.attribute_definitions import ( + serialize_attr_defs, + deserialize_attr_defs, +) +from ayon_core.pipeline.create import ( + AutoCreator, + HiddenCreator, + Creator, +) + + +class CreatorType: + def __init__(self, name): + self.name = name + + def __str__(self): + return self.name + + def __eq__(self, other): + return self.name == str(other) + + def __ne__(self, other): + # This is implemented only because of Python 2 + return not self == other + + +class CreatorTypes: + base = CreatorType("base") + auto = CreatorType("auto") + hidden = CreatorType("hidden") + artist = CreatorType("artist") + + @classmethod + def from_str(cls, value): + for creator_type in ( + cls.base, + cls.auto, + cls.hidden, + cls.artist + ): + if value == creator_type: + return creator_type + raise ValueError("Unknown type \"{}\"".format(str(value))) + + +class CreatorItem: + """Wrapper around Creator plugin. + + Object can be serialized and recreated. + """ + + def __init__( + self, + identifier, + creator_type, + product_type, + label, + group_label, + icon, + description, + detailed_description, + default_variant, + default_variants, + create_allow_context_change, + create_allow_thumbnail, + show_order, + pre_create_attributes_defs, + ): + self.identifier = identifier + self.creator_type = creator_type + self.product_type = product_type + self.label = label + self.group_label = group_label + self.icon = icon + self.description = description + self.detailed_description = detailed_description + self.default_variant = default_variant + self.default_variants = default_variants + self.create_allow_context_change = create_allow_context_change + self.create_allow_thumbnail = create_allow_thumbnail + self.show_order = show_order + self.pre_create_attributes_defs = pre_create_attributes_defs + + def get_group_label(self): + return self.group_label + + @classmethod + def from_creator(cls, creator): + if isinstance(creator, AutoCreator): + creator_type = CreatorTypes.auto + elif isinstance(creator, HiddenCreator): + creator_type = CreatorTypes.hidden + elif isinstance(creator, Creator): + creator_type = CreatorTypes.artist + else: + creator_type = CreatorTypes.base + + description = None + detail_description = None + default_variant = None + default_variants = None + pre_create_attr_defs = None + create_allow_context_change = None + create_allow_thumbnail = None + show_order = creator.order + if creator_type is CreatorTypes.artist: + description = creator.get_description() + detail_description = creator.get_detail_description() + default_variant = creator.get_default_variant() + default_variants = creator.get_default_variants() + pre_create_attr_defs = creator.get_pre_create_attr_defs() + create_allow_context_change = creator.create_allow_context_change + create_allow_thumbnail = creator.create_allow_thumbnail + show_order = creator.show_order + + identifier = creator.identifier + return cls( + identifier, + creator_type, + creator.product_type, + creator.label or identifier, + creator.get_group_label(), + creator.get_icon(), + description, + detail_description, + default_variant, + default_variants, + create_allow_context_change, + create_allow_thumbnail, + show_order, + pre_create_attr_defs, + ) + + def to_data(self): + pre_create_attributes_defs = None + if self.pre_create_attributes_defs is not None: + pre_create_attributes_defs = serialize_attr_defs( + self.pre_create_attributes_defs + ) + + return { + "identifier": self.identifier, + "creator_type": str(self.creator_type), + "product_type": self.product_type, + "label": self.label, + "group_label": self.group_label, + "icon": self.icon, + "description": self.description, + "detailed_description": self.detailed_description, + "default_variant": self.default_variant, + "default_variants": self.default_variants, + "create_allow_context_change": self.create_allow_context_change, + "create_allow_thumbnail": self.create_allow_thumbnail, + "show_order": self.show_order, + "pre_create_attributes_defs": pre_create_attributes_defs, + } + + @classmethod + def from_data(cls, data): + pre_create_attributes_defs = data["pre_create_attributes_defs"] + if pre_create_attributes_defs is not None: + data["pre_create_attributes_defs"] = deserialize_attr_defs( + pre_create_attributes_defs + ) + + data["creator_type"] = CreatorTypes.from_str(data["creator_type"]) + return cls(**data) \ No newline at end of file From 69990ea502c21caaa22623e2a799a027097cfd1f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 20 Jun 2024 16:48:57 +0200 Subject: [PATCH 03/15] move more classes from control --- client/ayon_core/tools/publisher/control.py | 414 +----------------- .../tools/publisher/models/__init__.py | 8 +- .../tools/publisher/models/publish.py | 410 +++++++++++++++++ 3 files changed, 419 insertions(+), 413 deletions(-) diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index fc7f0dc11d..c2da3f1386 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -1,9 +1,6 @@ import os -import copy import logging import traceback -import collections -import uuid import tempfile import shutil import inspect @@ -33,6 +30,8 @@ from ayon_core.tools.publisher.models import ( PublishReportMaker, CreatorItem, + PublishValidationErrors, + PublishPluginsProxy, ) # Define constant for plugin orders offset @@ -57,415 +56,6 @@ def process(self): self.callback(*self.args, **self.kwargs) -class PublishPluginsProxy: - """Wrapper around publish plugin. - - Prepare mapping for publish plugins and actions. Also can create - serializable data for plugin actions so UI don't have to have access to - them. - - This object is created in process where publishing is actually running. - - Notes: - Actions have id but single action can be used on multiple plugins so - to run an action is needed combination of plugin and action. - - Args: - plugins [List[pyblish.api.Plugin]]: Discovered plugins that will be - processed. - """ - - def __init__(self, plugins): - plugins_by_id = {} - actions_by_plugin_id = {} - action_ids_by_plugin_id = {} - for plugin in plugins: - plugin_id = plugin.id - plugins_by_id[plugin_id] = plugin - - action_ids = [] - actions_by_id = {} - action_ids_by_plugin_id[plugin_id] = action_ids - actions_by_plugin_id[plugin_id] = actions_by_id - - actions = getattr(plugin, "actions", None) or [] - for action in actions: - action_id = action.id - action_ids.append(action_id) - actions_by_id[action_id] = action - - self._plugins_by_id = plugins_by_id - self._actions_by_plugin_id = actions_by_plugin_id - self._action_ids_by_plugin_id = action_ids_by_plugin_id - - def get_action(self, plugin_id, action_id): - return self._actions_by_plugin_id[plugin_id][action_id] - - def get_plugin(self, plugin_id): - return self._plugins_by_id[plugin_id] - - def get_plugin_id(self, plugin): - """Get id of plugin based on plugin object. - - It's used for validation errors report. - - Args: - plugin (pyblish.api.Plugin): Publish plugin for which id should be - returned. - - Returns: - str: Plugin id. - """ - - return plugin.id - - def get_plugin_action_items(self, plugin_id): - """Get plugin action items for plugin by its id. - - Args: - plugin_id (str): Publish plugin id. - - Returns: - List[PublishPluginActionItem]: Items with information about publish - plugin actions. - """ - - return [ - self._create_action_item( - self.get_action(plugin_id, action_id), plugin_id - ) - for action_id in self._action_ids_by_plugin_id[plugin_id] - ] - - def _create_action_item(self, action, plugin_id): - label = action.label or action.__name__ - icon = getattr(action, "icon", None) - return PublishPluginActionItem( - action.id, - plugin_id, - action.active, - action.on, - label, - icon - ) - - -class PublishPluginActionItem: - """Representation of publish plugin action. - - Data driven object which is used as proxy for controller and UI. - - Args: - action_id (str): Action id. - plugin_id (str): Plugin id. - active (bool): Action is active. - on_filter (str): Actions have 'on' attribte which define when can be - action triggered (e.g. 'all', 'failed', ...). - label (str): Action's label. - icon (Union[str, None]) Action's icon. - """ - - def __init__(self, action_id, plugin_id, active, on_filter, label, icon): - self.action_id = action_id - self.plugin_id = plugin_id - self.active = active - self.on_filter = on_filter - self.label = label - self.icon = icon - - def to_data(self): - """Serialize object to dictionary. - - Returns: - Dict[str, Union[str,bool,None]]: Serialized object. - """ - - return { - "action_id": self.action_id, - "plugin_id": self.plugin_id, - "active": self.active, - "on_filter": self.on_filter, - "label": self.label, - "icon": self.icon - } - - @classmethod - def from_data(cls, data): - """Create object from data. - - Args: - data (Dict[str, Union[str,bool,None]]): Data used to recreate - object. - - Returns: - PublishPluginActionItem: Object created using data. - """ - - return cls(**data) - - -class ValidationErrorItem: - """Data driven validation error item. - - Prepared data container with information about validation error and it's - source plugin. - - Can be converted to raw data and recreated should be used for controller - and UI connection. - - Args: - instance_id (str): Id of pyblish instance to which is validation error - connected. - instance_label (str): Prepared instance label. - plugin_id (str): Id of pyblish Plugin which triggered the validation - error. Id is generated using 'PublishPluginsProxy'. - """ - - def __init__( - self, - instance_id, - instance_label, - plugin_id, - context_validation, - title, - description, - detail - ): - self.instance_id = instance_id - self.instance_label = instance_label - self.plugin_id = plugin_id - self.context_validation = context_validation - self.title = title - self.description = description - self.detail = detail - - def to_data(self): - """Serialize object to dictionary. - - Returns: - Dict[str, Union[str, bool, None]]: Serialized object data. - """ - - return { - "instance_id": self.instance_id, - "instance_label": self.instance_label, - "plugin_id": self.plugin_id, - "context_validation": self.context_validation, - "title": self.title, - "description": self.description, - "detail": self.detail, - } - - @classmethod - def from_result(cls, plugin_id, error, instance): - """Create new object based on resukt from controller. - - Returns: - ValidationErrorItem: New object with filled data. - """ - - instance_label = None - instance_id = None - if instance is not None: - instance_label = ( - instance.data.get("label") or instance.data.get("name") - ) - instance_id = instance.id - - return cls( - instance_id, - instance_label, - plugin_id, - instance is None, - error.title, - error.description, - error.detail, - ) - - @classmethod - def from_data(cls, data): - return cls(**data) - - -class PublishValidationErrorsReport: - """Publish validation errors report that can be parsed to raw data. - - Args: - error_items (List[ValidationErrorItem]): List of validation errors. - plugin_action_items (Dict[str, PublishPluginActionItem]): Action items - by plugin id. - """ - - def __init__(self, error_items, plugin_action_items): - self._error_items = error_items - self._plugin_action_items = plugin_action_items - - def __iter__(self): - for item in self._error_items: - yield item - - def group_items_by_title(self): - """Group errors by plugin and their titles. - - Items are grouped by plugin and title -> same title from different - plugin is different item. Items are ordered by plugin order. - - Returns: - List[Dict[str, Any]]: List where each item title, instance - information related to title and possible plugin actions. - """ - - ordered_plugin_ids = [] - error_items_by_plugin_id = collections.defaultdict(list) - for error_item in self._error_items: - plugin_id = error_item.plugin_id - if plugin_id not in ordered_plugin_ids: - ordered_plugin_ids.append(plugin_id) - error_items_by_plugin_id[plugin_id].append(error_item) - - grouped_error_items = [] - for plugin_id in ordered_plugin_ids: - plugin_action_items = self._plugin_action_items[plugin_id] - error_items = error_items_by_plugin_id[plugin_id] - - titles = [] - error_items_by_title = collections.defaultdict(list) - for error_item in error_items: - title = error_item.title - if title not in titles: - titles.append(error_item.title) - error_items_by_title[title].append(error_item) - - for title in titles: - grouped_error_items.append({ - "id": uuid.uuid4().hex, - "plugin_id": plugin_id, - "plugin_action_items": list(plugin_action_items), - "error_items": error_items_by_title[title], - "title": title - }) - return grouped_error_items - - def to_data(self): - """Serialize object to dictionary. - - Returns: - Dict[str, Any]: Serialized data. - """ - - error_items = [ - item.to_data() - for item in self._error_items - ] - - plugin_action_items = { - plugin_id: [ - action_item.to_data() - for action_item in action_items - ] - for plugin_id, action_items in self._plugin_action_items.items() - } - - return { - "error_items": error_items, - "plugin_action_items": plugin_action_items - } - - @classmethod - def from_data(cls, data): - """Recreate object from data. - - Args: - data (dict[str, Any]): Data to recreate object. Can be created - using 'to_data' method. - - Returns: - PublishValidationErrorsReport: New object based on data. - """ - - error_items = [ - ValidationErrorItem.from_data(error_item) - for error_item in data["error_items"] - ] - plugin_action_items = [ - PublishPluginActionItem.from_data(action_item) - for action_item in data["plugin_action_items"] - ] - return cls(error_items, plugin_action_items) - - -class PublishValidationErrors: - """Object to keep track about validation errors by plugin.""" - - def __init__(self): - self._plugins_proxy = None - self._error_items = [] - self._plugin_action_items = {} - - def __bool__(self): - return self.has_errors - - @property - def has_errors(self): - """At least one error was added.""" - - return bool(self._error_items) - - def reset(self, plugins_proxy): - """Reset object to default state. - - Args: - plugins_proxy (PublishPluginsProxy): Proxy which store plugins, - actions by ids and create mapping of action ids by plugin ids. - """ - - self._plugins_proxy = plugins_proxy - self._error_items = [] - self._plugin_action_items = {} - - def create_report(self): - """Create report based on currently existing errors. - - Returns: - PublishValidationErrorsReport: Validation error report with all - error information and publish plugin action items. - """ - - return PublishValidationErrorsReport( - self._error_items, self._plugin_action_items - ) - - def add_error(self, plugin, error, instance): - """Add error from pyblish result. - - Args: - plugin (pyblish.api.Plugin): Plugin which triggered error. - error (ValidationException): Validation error. - instance (Union[pyblish.api.Instance, None]): Instance on which was - error raised or None if was raised on context. - """ - - # Make sure the cached report is cleared - plugin_id = self._plugins_proxy.get_plugin_id(plugin) - if not error.title: - if hasattr(plugin, "label") and plugin.label: - plugin_label = plugin.label - else: - plugin_label = plugin.__name__ - error.title = plugin_label - - self._error_items.append( - ValidationErrorItem.from_result(plugin_id, error, instance) - ) - if plugin_id in self._plugin_action_items: - return - - plugin_actions = self._plugins_proxy.get_plugin_action_items( - plugin_id - ) - self._plugin_action_items[plugin_id] = plugin_actions - - @six.add_metaclass(ABCMeta) class AbstractPublisherController(object): """Publisher tool controller. diff --git a/client/ayon_core/tools/publisher/models/__init__.py b/client/ayon_core/tools/publisher/models/__init__.py index a89f8e0d52..2dbf8fb63c 100644 --- a/client/ayon_core/tools/publisher/models/__init__.py +++ b/client/ayon_core/tools/publisher/models/__init__.py @@ -1,9 +1,15 @@ from .create import CreatorItem -from .publish import PublishReportMaker +from .publish import ( + PublishReportMaker, + PublishValidationErrors, + PublishPluginsProxy, +) __all__ = ( "CreatorItem", "PublishReportMaker", + "PublishValidationErrors", + "PublishPluginsProxy", ) diff --git a/client/ayon_core/tools/publisher/models/publish.py b/client/ayon_core/tools/publisher/models/publish.py index 152bb4cc82..1cd787a2f5 100644 --- a/client/ayon_core/tools/publisher/models/publish.py +++ b/client/ayon_core/tools/publisher/models/publish.py @@ -1,6 +1,7 @@ import uuid import copy import traceback +import collections import arrow @@ -264,3 +265,412 @@ def _extract_log_items(self, result): }) return output + + +class PublishPluginsProxy: + """Wrapper around publish plugin. + + Prepare mapping for publish plugins and actions. Also can create + serializable data for plugin actions so UI don't have to have access to + them. + + This object is created in process where publishing is actually running. + + Notes: + Actions have id but single action can be used on multiple plugins so + to run an action is needed combination of plugin and action. + + Args: + plugins [List[pyblish.api.Plugin]]: Discovered plugins that will be + processed. + """ + + def __init__(self, plugins): + plugins_by_id = {} + actions_by_plugin_id = {} + action_ids_by_plugin_id = {} + for plugin in plugins: + plugin_id = plugin.id + plugins_by_id[plugin_id] = plugin + + action_ids = [] + actions_by_id = {} + action_ids_by_plugin_id[plugin_id] = action_ids + actions_by_plugin_id[plugin_id] = actions_by_id + + actions = getattr(plugin, "actions", None) or [] + for action in actions: + action_id = action.id + action_ids.append(action_id) + actions_by_id[action_id] = action + + self._plugins_by_id = plugins_by_id + self._actions_by_plugin_id = actions_by_plugin_id + self._action_ids_by_plugin_id = action_ids_by_plugin_id + + def get_action(self, plugin_id, action_id): + return self._actions_by_plugin_id[plugin_id][action_id] + + def get_plugin(self, plugin_id): + return self._plugins_by_id[plugin_id] + + def get_plugin_id(self, plugin): + """Get id of plugin based on plugin object. + + It's used for validation errors report. + + Args: + plugin (pyblish.api.Plugin): Publish plugin for which id should be + returned. + + Returns: + str: Plugin id. + """ + + return plugin.id + + def get_plugin_action_items(self, plugin_id): + """Get plugin action items for plugin by its id. + + Args: + plugin_id (str): Publish plugin id. + + Returns: + List[PublishPluginActionItem]: Items with information about publish + plugin actions. + """ + + return [ + self._create_action_item( + self.get_action(plugin_id, action_id), plugin_id + ) + for action_id in self._action_ids_by_plugin_id[plugin_id] + ] + + def _create_action_item(self, action, plugin_id): + label = action.label or action.__name__ + icon = getattr(action, "icon", None) + return PublishPluginActionItem( + action.id, + plugin_id, + action.active, + action.on, + label, + icon + ) + + +class PublishPluginActionItem: + """Representation of publish plugin action. + + Data driven object which is used as proxy for controller and UI. + + Args: + action_id (str): Action id. + plugin_id (str): Plugin id. + active (bool): Action is active. + on_filter (str): Actions have 'on' attribte which define when can be + action triggered (e.g. 'all', 'failed', ...). + label (str): Action's label. + icon (Union[str, None]) Action's icon. + """ + + def __init__(self, action_id, plugin_id, active, on_filter, label, icon): + self.action_id = action_id + self.plugin_id = plugin_id + self.active = active + self.on_filter = on_filter + self.label = label + self.icon = icon + + def to_data(self): + """Serialize object to dictionary. + + Returns: + Dict[str, Union[str,bool,None]]: Serialized object. + """ + + return { + "action_id": self.action_id, + "plugin_id": self.plugin_id, + "active": self.active, + "on_filter": self.on_filter, + "label": self.label, + "icon": self.icon + } + + @classmethod + def from_data(cls, data): + """Create object from data. + + Args: + data (Dict[str, Union[str,bool,None]]): Data used to recreate + object. + + Returns: + PublishPluginActionItem: Object created using data. + """ + + return cls(**data) + + +class ValidationErrorItem: + """Data driven validation error item. + + Prepared data container with information about validation error and it's + source plugin. + + Can be converted to raw data and recreated should be used for controller + and UI connection. + + Args: + instance_id (str): Id of pyblish instance to which is validation error + connected. + instance_label (str): Prepared instance label. + plugin_id (str): Id of pyblish Plugin which triggered the validation + error. Id is generated using 'PublishPluginsProxy'. + """ + + def __init__( + self, + instance_id, + instance_label, + plugin_id, + context_validation, + title, + description, + detail + ): + self.instance_id = instance_id + self.instance_label = instance_label + self.plugin_id = plugin_id + self.context_validation = context_validation + self.title = title + self.description = description + self.detail = detail + + def to_data(self): + """Serialize object to dictionary. + + Returns: + Dict[str, Union[str, bool, None]]: Serialized object data. + """ + + return { + "instance_id": self.instance_id, + "instance_label": self.instance_label, + "plugin_id": self.plugin_id, + "context_validation": self.context_validation, + "title": self.title, + "description": self.description, + "detail": self.detail, + } + + @classmethod + def from_result(cls, plugin_id, error, instance): + """Create new object based on resukt from controller. + + Returns: + ValidationErrorItem: New object with filled data. + """ + + instance_label = None + instance_id = None + if instance is not None: + instance_label = ( + instance.data.get("label") or instance.data.get("name") + ) + instance_id = instance.id + + return cls( + instance_id, + instance_label, + plugin_id, + instance is None, + error.title, + error.description, + error.detail, + ) + + @classmethod + def from_data(cls, data): + return cls(**data) + + +class PublishValidationErrorsReport: + """Publish validation errors report that can be parsed to raw data. + + Args: + error_items (List[ValidationErrorItem]): List of validation errors. + plugin_action_items (Dict[str, PublishPluginActionItem]): Action items + by plugin id. + """ + + def __init__(self, error_items, plugin_action_items): + self._error_items = error_items + self._plugin_action_items = plugin_action_items + + def __iter__(self): + for item in self._error_items: + yield item + + def group_items_by_title(self): + """Group errors by plugin and their titles. + + Items are grouped by plugin and title -> same title from different + plugin is different item. Items are ordered by plugin order. + + Returns: + List[Dict[str, Any]]: List where each item title, instance + information related to title and possible plugin actions. + """ + + ordered_plugin_ids = [] + error_items_by_plugin_id = collections.defaultdict(list) + for error_item in self._error_items: + plugin_id = error_item.plugin_id + if plugin_id not in ordered_plugin_ids: + ordered_plugin_ids.append(plugin_id) + error_items_by_plugin_id[plugin_id].append(error_item) + + grouped_error_items = [] + for plugin_id in ordered_plugin_ids: + plugin_action_items = self._plugin_action_items[plugin_id] + error_items = error_items_by_plugin_id[plugin_id] + + titles = [] + error_items_by_title = collections.defaultdict(list) + for error_item in error_items: + title = error_item.title + if title not in titles: + titles.append(error_item.title) + error_items_by_title[title].append(error_item) + + for title in titles: + grouped_error_items.append({ + "id": uuid.uuid4().hex, + "plugin_id": plugin_id, + "plugin_action_items": list(plugin_action_items), + "error_items": error_items_by_title[title], + "title": title + }) + return grouped_error_items + + def to_data(self): + """Serialize object to dictionary. + + Returns: + Dict[str, Any]: Serialized data. + """ + + error_items = [ + item.to_data() + for item in self._error_items + ] + + plugin_action_items = { + plugin_id: [ + action_item.to_data() + for action_item in action_items + ] + for plugin_id, action_items in self._plugin_action_items.items() + } + + return { + "error_items": error_items, + "plugin_action_items": plugin_action_items + } + + @classmethod + def from_data(cls, data): + """Recreate object from data. + + Args: + data (dict[str, Any]): Data to recreate object. Can be created + using 'to_data' method. + + Returns: + PublishValidationErrorsReport: New object based on data. + """ + + error_items = [ + ValidationErrorItem.from_data(error_item) + for error_item in data["error_items"] + ] + plugin_action_items = [ + PublishPluginActionItem.from_data(action_item) + for action_item in data["plugin_action_items"] + ] + return cls(error_items, plugin_action_items) + + +class PublishValidationErrors: + """Object to keep track about validation errors by plugin.""" + + def __init__(self): + self._plugins_proxy = None + self._error_items = [] + self._plugin_action_items = {} + + def __bool__(self): + return self.has_errors + + @property + def has_errors(self): + """At least one error was added.""" + + return bool(self._error_items) + + def reset(self, plugins_proxy): + """Reset object to default state. + + Args: + plugins_proxy (PublishPluginsProxy): Proxy which store plugins, + actions by ids and create mapping of action ids by plugin ids. + """ + + self._plugins_proxy = plugins_proxy + self._error_items = [] + self._plugin_action_items = {} + + def create_report(self): + """Create report based on currently existing errors. + + Returns: + PublishValidationErrorsReport: Validation error report with all + error information and publish plugin action items. + """ + + return PublishValidationErrorsReport( + self._error_items, self._plugin_action_items + ) + + def add_error(self, plugin, error, instance): + """Add error from pyblish result. + + Args: + plugin (pyblish.api.Plugin): Plugin which triggered error. + error (ValidationException): Validation error. + instance (Union[pyblish.api.Instance, None]): Instance on which was + error raised or None if was raised on context. + """ + + # Make sure the cached report is cleared + plugin_id = self._plugins_proxy.get_plugin_id(plugin) + if not error.title: + if hasattr(plugin, "label") and plugin.label: + plugin_label = plugin.label + else: + plugin_label = plugin.__name__ + error.title = plugin_label + + self._error_items.append( + ValidationErrorItem.from_result(plugin_id, error, instance) + ) + if plugin_id in self._plugin_action_items: + return + + plugin_actions = self._plugins_proxy.get_plugin_action_items( + plugin_id + ) + self._plugin_action_items[plugin_id] = plugin_actions From d95fcaf9f41cb686de2de75ba5bc437510258a2a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 20 Jun 2024 17:00:55 +0200 Subject: [PATCH 04/15] moved abstract class from control.py --- client/ayon_core/tools/publisher/abstract.py | 414 ++++++++++++++++++ client/ayon_core/tools/publisher/control.py | 416 +------------------ 2 files changed, 416 insertions(+), 414 deletions(-) create mode 100644 client/ayon_core/tools/publisher/abstract.py diff --git a/client/ayon_core/tools/publisher/abstract.py b/client/ayon_core/tools/publisher/abstract.py new file mode 100644 index 0000000000..8b46c58e87 --- /dev/null +++ b/client/ayon_core/tools/publisher/abstract.py @@ -0,0 +1,414 @@ +from abc import ABC, abstractmethod + + +class CardMessageTypes: + standard = None + info = "info" + error = "error" + + +class AbstractPublisherController(ABC): + """Publisher tool controller. + + Define what must be implemented to be able use Publisher functionality. + + Goal is to have "data driven" controller that can be used to control UI + running in different process. That lead to some disadvantages like UI can't + access objects directly but by using wrappers that can be serialized. + """ + + @property + @abstractmethod + def log(self): + """Controller's logger object. + + Returns: + logging.Logger: Logger object that can be used for logging. + """ + + pass + + @property + @abstractmethod + def event_system(self): + """Inner event system for publisher controller.""" + + pass + + @property + @abstractmethod + def project_name(self): + """Current context project name. + + Returns: + str: Name of project. + """ + + pass + + @property + @abstractmethod + def current_folder_path(self): + """Current context folder path. + + Returns: + Union[str, None]: Folder path. + + """ + pass + + @property + @abstractmethod + def current_task_name(self): + """Current context task name. + + Returns: + Union[str, None]: Name of task. + """ + + pass + + @property + @abstractmethod + def host_context_has_changed(self): + """Host context changed after last reset. + + 'CreateContext' has this option available using 'context_has_changed'. + + Returns: + bool: Context has changed. + """ + + pass + + @property + @abstractmethod + def host_is_valid(self): + """Host is valid for creation part. + + Host must have implemented certain functionality to be able create + in Publisher tool. + + Returns: + bool: Host can handle creation of instances. + """ + + pass + + @property + @abstractmethod + def instances(self): + """Collected/created instances. + + Returns: + List[CreatedInstance]: List of created instances. + """ + + pass + + @abstractmethod + def get_context_title(self): + """Get context title for artist shown at the top of main window. + + Returns: + Union[str, None]: Context title for window or None. In case of None + a warning is displayed (not nice for artists). + """ + + pass + + @abstractmethod + def get_existing_product_names(self, folder_path): + pass + + @abstractmethod + def reset(self): + """Reset whole controller. + + This should reset create context, publish context and all variables + that are related to it. + """ + + pass + + @abstractmethod + def get_creator_attribute_definitions(self, instances): + pass + + @abstractmethod + def get_publish_attribute_definitions(self, instances, include_context): + pass + + @abstractmethod + def get_creator_icon(self, identifier): + """Receive creator's icon by identifier. + + Args: + identifier (str): Creator's identifier. + + Returns: + Union[str, None]: Creator's icon string. + """ + + pass + + @abstractmethod + def get_product_name( + self, + creator_identifier, + variant, + task_name, + folder_path, + instance_id=None + ): + """Get product name based on passed data. + + Args: + creator_identifier (str): Identifier of creator which should be + responsible for product name creation. + variant (str): Variant value from user's input. + task_name (str): Name of task for which is instance created. + folder_path (str): Folder path for which is instance created. + instance_id (Union[str, None]): Existing instance id when product + name is updated. + """ + + pass + + @abstractmethod + def create( + self, creator_identifier, product_name, instance_data, options + ): + """Trigger creation by creator identifier. + + Should also trigger refresh of instanes. + + Args: + creator_identifier (str): Identifier of Creator plugin. + product_name (str): Calculated product name. + instance_data (Dict[str, Any]): Base instance data with variant, + folder path and task name. + options (Dict[str, Any]): Data from pre-create attributes. + """ + + pass + + @abstractmethod + def save_changes(self): + """Save changes in create context. + + Save can crash because of unexpected errors. + + Returns: + bool: Save was successful. + """ + + pass + + @abstractmethod + def remove_instances(self, instance_ids): + """Remove list of instances from create context.""" + # TODO expect instance ids + + pass + + @property + @abstractmethod + def publish_has_started(self): + """Has publishing finished. + + Returns: + bool: If publishing finished and all plugins were iterated. + """ + + pass + + @property + @abstractmethod + def publish_has_finished(self): + """Has publishing finished. + + Returns: + bool: If publishing finished and all plugins were iterated. + """ + + pass + + @property + @abstractmethod + def publish_is_running(self): + """Publishing is running right now. + + Returns: + bool: If publishing is in progress. + """ + + pass + + @property + @abstractmethod + def publish_has_validated(self): + """Publish validation passed. + + Returns: + bool: If publishing passed last possible validation order. + """ + + pass + + @property + @abstractmethod + def publish_has_crashed(self): + """Publishing crashed for any reason. + + Returns: + bool: Publishing crashed. + """ + + pass + + @property + @abstractmethod + def publish_has_validation_errors(self): + """During validation happened at least one validation error. + + Returns: + bool: Validation error was raised during validation. + """ + + pass + + @property + @abstractmethod + def publish_max_progress(self): + """Get maximum possible progress number. + + Returns: + int: Number that can be used as 100% of publish progress bar. + """ + + pass + + @property + @abstractmethod + def publish_progress(self): + """Current progress number. + + Returns: + int: Current progress value from 0 to 'publish_max_progress'. + """ + + pass + + @property + @abstractmethod + def publish_error_msg(self): + """Current error message which cause fail of publishing. + + Returns: + Union[str, None]: Message which will be showed to artist or + None. + """ + + pass + + @abstractmethod + def get_publish_report(self): + pass + + @abstractmethod + def get_validation_errors(self): + pass + + @abstractmethod + def publish(self): + """Trigger publishing without any order limitations.""" + + pass + + @abstractmethod + def validate(self): + """Trigger publishing which will stop after validation order.""" + + pass + + @abstractmethod + def stop_publish(self): + """Stop publishing can be also used to pause publishing. + + Pause of publishing is possible only if all plugins successfully + finished. + """ + + pass + + @abstractmethod + def run_action(self, plugin_id, action_id): + """Trigger pyblish action on a plugin. + + Args: + plugin_id (str): Id of publish plugin. + action_id (str): Id of publish action. + """ + + pass + + @property + @abstractmethod + def convertor_items(self): + pass + + @abstractmethod + def trigger_convertor_items(self, convertor_identifiers): + pass + + @abstractmethod + def get_thumbnail_paths_for_instances(self, instance_ids): + pass + + @abstractmethod + def set_thumbnail_paths_for_instances(self, thumbnail_path_mapping): + pass + + @abstractmethod + def set_comment(self, comment): + """Set comment on pyblish context. + + Set "comment" key on current pyblish.api.Context data. + + Args: + comment (str): Artist's comment. + """ + + pass + + @abstractmethod + def emit_card_message( + self, message, message_type=CardMessageTypes.standard + ): + """Emit a card message which can have a lifetime. + + This is for UI purposes. Method can be extended to more arguments + in future e.g. different message timeout or type (color). + + Args: + message (str): Message that will be showed. + """ + + pass + + @abstractmethod + def get_thumbnail_temp_dir_path(self): + """Return path to directory where thumbnails can be temporary stored. + + Returns: + str: Path to a directory. + """ + + pass + + @abstractmethod + def clear_thumbnail_temp_dir_path(self): + """Remove content of thumbnail temp directory.""" + + pass diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index c2da3f1386..a1f5889141 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -34,16 +34,12 @@ PublishPluginsProxy, ) +from .abstract import AbstractPublisherController, CardMessageTypes + # Define constant for plugin orders offset PLUGIN_ORDER_OFFSET = 0.5 -class CardMessageTypes: - standard = None - info = "info" - error = "error" - - class MainThreadItem: """Callback with args and kwargs.""" @@ -56,414 +52,6 @@ def process(self): self.callback(*self.args, **self.kwargs) -@six.add_metaclass(ABCMeta) -class AbstractPublisherController(object): - """Publisher tool controller. - - Define what must be implemented to be able use Publisher functionality. - - Goal is to have "data driven" controller that can be used to control UI - running in different process. That lead to some disadvantages like UI can't - access objects directly but by using wrappers that can be serialized. - """ - - @property - @abstractmethod - def log(self): - """Controller's logger object. - - Returns: - logging.Logger: Logger object that can be used for logging. - """ - - pass - - @property - @abstractmethod - def event_system(self): - """Inner event system for publisher controller.""" - - pass - - @property - @abstractmethod - def project_name(self): - """Current context project name. - - Returns: - str: Name of project. - """ - - pass - - @property - @abstractmethod - def current_folder_path(self): - """Current context folder path. - - Returns: - Union[str, None]: Folder path. - - """ - pass - - @property - @abstractmethod - def current_task_name(self): - """Current context task name. - - Returns: - Union[str, None]: Name of task. - """ - - pass - - @property - @abstractmethod - def host_context_has_changed(self): - """Host context changed after last reset. - - 'CreateContext' has this option available using 'context_has_changed'. - - Returns: - bool: Context has changed. - """ - - pass - - @property - @abstractmethod - def host_is_valid(self): - """Host is valid for creation part. - - Host must have implemented certain functionality to be able create - in Publisher tool. - - Returns: - bool: Host can handle creation of instances. - """ - - pass - - @property - @abstractmethod - def instances(self): - """Collected/created instances. - - Returns: - List[CreatedInstance]: List of created instances. - """ - - pass - - @abstractmethod - def get_context_title(self): - """Get context title for artist shown at the top of main window. - - Returns: - Union[str, None]: Context title for window or None. In case of None - a warning is displayed (not nice for artists). - """ - - pass - - @abstractmethod - def get_existing_product_names(self, folder_path): - pass - - @abstractmethod - def reset(self): - """Reset whole controller. - - This should reset create context, publish context and all variables - that are related to it. - """ - - pass - - @abstractmethod - def get_creator_attribute_definitions(self, instances): - pass - - @abstractmethod - def get_publish_attribute_definitions(self, instances, include_context): - pass - - @abstractmethod - def get_creator_icon(self, identifier): - """Receive creator's icon by identifier. - - Args: - identifier (str): Creator's identifier. - - Returns: - Union[str, None]: Creator's icon string. - """ - - pass - - @abstractmethod - def get_product_name( - self, - creator_identifier, - variant, - task_name, - folder_path, - instance_id=None - ): - """Get product name based on passed data. - - Args: - creator_identifier (str): Identifier of creator which should be - responsible for product name creation. - variant (str): Variant value from user's input. - task_name (str): Name of task for which is instance created. - folder_path (str): Folder path for which is instance created. - instance_id (Union[str, None]): Existing instance id when product - name is updated. - """ - - pass - - @abstractmethod - def create( - self, creator_identifier, product_name, instance_data, options - ): - """Trigger creation by creator identifier. - - Should also trigger refresh of instanes. - - Args: - creator_identifier (str): Identifier of Creator plugin. - product_name (str): Calculated product name. - instance_data (Dict[str, Any]): Base instance data with variant, - folder path and task name. - options (Dict[str, Any]): Data from pre-create attributes. - """ - - pass - - @abstractmethod - def save_changes(self): - """Save changes in create context. - - Save can crash because of unexpected errors. - - Returns: - bool: Save was successful. - """ - - pass - - @abstractmethod - def remove_instances(self, instance_ids): - """Remove list of instances from create context.""" - # TODO expect instance ids - - pass - - @property - @abstractmethod - def publish_has_started(self): - """Has publishing finished. - - Returns: - bool: If publishing finished and all plugins were iterated. - """ - - pass - - @property - @abstractmethod - def publish_has_finished(self): - """Has publishing finished. - - Returns: - bool: If publishing finished and all plugins were iterated. - """ - - pass - - @property - @abstractmethod - def publish_is_running(self): - """Publishing is running right now. - - Returns: - bool: If publishing is in progress. - """ - - pass - - @property - @abstractmethod - def publish_has_validated(self): - """Publish validation passed. - - Returns: - bool: If publishing passed last possible validation order. - """ - - pass - - @property - @abstractmethod - def publish_has_crashed(self): - """Publishing crashed for any reason. - - Returns: - bool: Publishing crashed. - """ - - pass - - @property - @abstractmethod - def publish_has_validation_errors(self): - """During validation happened at least one validation error. - - Returns: - bool: Validation error was raised during validation. - """ - - pass - - @property - @abstractmethod - def publish_max_progress(self): - """Get maximum possible progress number. - - Returns: - int: Number that can be used as 100% of publish progress bar. - """ - - pass - - @property - @abstractmethod - def publish_progress(self): - """Current progress number. - - Returns: - int: Current progress value from 0 to 'publish_max_progress'. - """ - - pass - - @property - @abstractmethod - def publish_error_msg(self): - """Current error message which cause fail of publishing. - - Returns: - Union[str, None]: Message which will be showed to artist or - None. - """ - - pass - - @abstractmethod - def get_publish_report(self): - pass - - @abstractmethod - def get_validation_errors(self): - pass - - @abstractmethod - def publish(self): - """Trigger publishing without any order limitations.""" - - pass - - @abstractmethod - def validate(self): - """Trigger publishing which will stop after validation order.""" - - pass - - @abstractmethod - def stop_publish(self): - """Stop publishing can be also used to pause publishing. - - Pause of publishing is possible only if all plugins successfully - finished. - """ - - pass - - @abstractmethod - def run_action(self, plugin_id, action_id): - """Trigger pyblish action on a plugin. - - Args: - plugin_id (str): Id of publish plugin. - action_id (str): Id of publish action. - """ - - pass - - @property - @abstractmethod - def convertor_items(self): - pass - - @abstractmethod - def trigger_convertor_items(self, convertor_identifiers): - pass - - @abstractmethod - def get_thumbnail_paths_for_instances(self, instance_ids): - pass - - @abstractmethod - def set_thumbnail_paths_for_instances(self, thumbnail_path_mapping): - pass - - @abstractmethod - def set_comment(self, comment): - """Set comment on pyblish context. - - Set "comment" key on current pyblish.api.Context data. - - Args: - comment (str): Artist's comment. - """ - - pass - - @abstractmethod - def emit_card_message( - self, message, message_type=CardMessageTypes.standard - ): - """Emit a card message which can have a lifetime. - - This is for UI purposes. Method can be extended to more arguments - in future e.g. different message timeout or type (color). - - Args: - message (str): Message that will be showed. - """ - - pass - - @abstractmethod - def get_thumbnail_temp_dir_path(self): - """Return path to directory where thumbnails can be temporary stored. - - Returns: - str: Path to a directory. - """ - - pass - - @abstractmethod - def clear_thumbnail_temp_dir_path(self): - """Remove content of thumbnail temp directory.""" - - pass - - class BasePublisherController(AbstractPublisherController): """Implement common logic for controllers. From 7666a5e6c8a48e804601324c4c758ee848a85aa4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 21 Jun 2024 12:00:57 +0200 Subject: [PATCH 05/15] basic implementation publish model --- .../tools/publisher/models/__init__.py | 10 +- .../tools/publisher/models/publish.py | 666 ++++++++++++++++-- 2 files changed, 592 insertions(+), 84 deletions(-) diff --git a/client/ayon_core/tools/publisher/models/__init__.py b/client/ayon_core/tools/publisher/models/__init__.py index 2dbf8fb63c..156141969c 100644 --- a/client/ayon_core/tools/publisher/models/__init__.py +++ b/client/ayon_core/tools/publisher/models/__init__.py @@ -1,15 +1,9 @@ from .create import CreatorItem -from .publish import ( - PublishReportMaker, - PublishValidationErrors, - PublishPluginsProxy, -) +from .publish import PublishModel __all__ = ( "CreatorItem", - "PublishReportMaker", - "PublishValidationErrors", - "PublishPluginsProxy", + "PublishModel", ) diff --git a/client/ayon_core/tools/publisher/models/publish.py b/client/ayon_core/tools/publisher/models/publish.py index 1cd787a2f5..9660862ee8 100644 --- a/client/ayon_core/tools/publisher/models/publish.py +++ b/client/ayon_core/tools/publisher/models/publish.py @@ -1,12 +1,24 @@ import uuid import copy +import inspect import traceback import collections +from functools import partial import arrow +import pyblish.plugin +from ayon_core.pipeline import ( + PublishValidationError, + KnownPublishError, + OptionalPyblishPluginMixin, +) from ayon_core.pipeline.publish import get_publish_instance_label +PUBLISH_EVENT_SOURCE = "publisher.publish.model" +# Define constant for plugin orders offset +PLUGIN_ORDER_OFFSET = 0.5 + class PublishReportMaker: """Report for single publishing process. @@ -14,91 +26,74 @@ class PublishReportMaker: Report keeps current state of publishing and currently processed plugin. """ - def __init__(self, controller): - self.controller = controller + def __init__( + self, + creator_discover_result=None, + convertor_discover_result=None, + publish_discover_result=None, + ): self._create_discover_result = None self._convert_discover_result = None self._publish_discover_result = None - self._plugin_data_by_id = {} - self._current_plugin = None - self._current_plugin_data = {} self._all_instances_by_id = {} - self._current_context = None + self._plugin_data_by_id = {} + self._current_plugin_id = None - def reset(self, context, create_context): + self.reset( + creator_discover_result, + convertor_discover_result, + publish_discover_result, + ) + + def reset( + self, + creator_discover_result, + convertor_discover_result, + publish_discover_result, + ): """Reset report and clear all data.""" - self._create_discover_result = create_context.creator_discover_result - self._convert_discover_result = ( - create_context.convertor_discover_result - ) - self._publish_discover_result = create_context.publish_discover_result + self._create_discover_result = creator_discover_result + self._convert_discover_result = convertor_discover_result + self._publish_discover_result = publish_discover_result - self._plugin_data_by_id = {} - self._current_plugin = None - self._current_plugin_data = {} self._all_instances_by_id = {} - self._current_context = context + self._plugin_data_by_id = {} + self._current_plugin_id = None - for plugin in create_context.publish_plugins_mismatch_targets: - plugin_data = self._add_plugin_data_item(plugin) - plugin_data["skipped"] = True + publish_plugins = [] + if publish_discover_result is not None: + publish_plugins = publish_discover_result.plugins + + for plugin in publish_plugins: + self._add_plugin_data_item(plugin) - def add_plugin_iter(self, plugin, context): + def add_plugin_iter(self, plugin_id, context): """Add report about single iteration of plugin.""" for instance in context: self._all_instances_by_id[instance.id] = instance - if self._current_plugin_data: - self._current_plugin_data["passed"] = True - - self._current_plugin = plugin - self._current_plugin_data = self._add_plugin_data_item(plugin) - - def _add_plugin_data_item(self, plugin): - if plugin.id in self._plugin_data_by_id: - # A plugin would be processed more than once. What can cause it: - # - there is a bug in controller - # - plugin class is imported into multiple files - # - this can happen even with base classes from 'pyblish' - raise ValueError( - "Plugin '{}' is already stored".format(str(plugin))) - - plugin_data_item = self._create_plugin_data_item(plugin) - self._plugin_data_by_id[plugin.id] = plugin_data_item + self._current_plugin_id = plugin_id - return plugin_data_item + def set_plugin_passed(self, plugin_id): + plugin_data = self._plugin_data_by_id[plugin_id] + plugin_data["passed"] = True - def _create_plugin_data_item(self, plugin): - label = None - if hasattr(plugin, "label"): - label = plugin.label - - return { - "id": plugin.id, - "name": plugin.__name__, - "label": label, - "order": plugin.order, - "targets": list(plugin.targets), - "instances_data": [], - "actions_data": [], - "skipped": False, - "passed": False - } - - def set_plugin_skipped(self): + def set_plugin_skipped(self, plugin_id): """Set that current plugin has been skipped.""" - self._current_plugin_data["skipped"] = True + plugin_data = self._plugin_data_by_id[plugin_id] + plugin_data["skipped"] = True - def add_result(self, result): + def add_result(self, plugin_id, result): """Handle result of one plugin and it's instance.""" instance = result["instance"] instance_id = None if instance is not None: instance_id = instance.id - self._current_plugin_data["instances_data"].append({ + plugin_data = self._plugin_data_by_id[plugin_id] + plugin_data["instances_data"].append({ "id": instance_id, "logs": self._extract_instance_log_items(result), "process_time": result["duration"] @@ -108,9 +103,7 @@ def add_action_result(self, action, result): """Add result of single action.""" plugin = result["plugin"] - store_item = self._plugin_data_by_id.get(plugin.id) - if store_item is None: - store_item = self._add_plugin_data_item(plugin) + store_item = self._plugin_data_by_id[plugin.id] action_name = action.__name__ action_label = action.label or action_name @@ -122,15 +115,16 @@ def add_action_result(self, action, result): "logs": log_items }) - def get_report(self, publish_plugins=None): + def get_report(self, publish_context): """Report data with all details of current state.""" now = arrow.utcnow().to("local") - instances_details = {} - for instance in self._all_instances_by_id.values(): - instances_details[instance.id] = self._extract_instance_data( - instance, instance in self._current_context + instances_details = { + instance.id: self._extract_instance_data( + instance, instance in publish_context ) + for instance in self._all_instances_by_id.values() + } plugins_data_by_id = copy.deepcopy( self._plugin_data_by_id @@ -138,19 +132,13 @@ def get_report(self, publish_plugins=None): # Ensure the current plug-in is marked as `passed` in the result # so that it shows on reports for paused publishes - if self._current_plugin is not None: + if self._current_plugin_id is not None: current_plugin_data = plugins_data_by_id.get( - self._current_plugin.id + self._current_plugin_id ) if current_plugin_data and not current_plugin_data["passed"]: current_plugin_data["passed"] = True - if publish_plugins: - for plugin in publish_plugins: - if plugin.id not in plugins_data_by_id: - plugins_data_by_id[plugin.id] = \ - self._create_plugin_data_item(plugin) - reports = [] if self._create_discover_result is not None: reports.append(self._create_discover_result) @@ -172,13 +160,42 @@ def get_report(self, publish_plugins=None): return { "plugins_data": list(plugins_data_by_id.values()), "instances": instances_details, - "context": self._extract_context_data(self._current_context), + "context": self._extract_context_data(publish_context), "crashed_file_paths": crashed_file_paths, "id": uuid.uuid4().hex, "created_at": now.isoformat(), "report_version": "1.0.1", } + def _add_plugin_data_item(self, plugin): + if plugin.id in self._plugin_data_by_id: + # A plugin would be processed more than once. What can cause it: + # - there is a bug in controller + # - plugin class is imported into multiple files + # - this can happen even with base classes from 'pyblish' + raise ValueError( + "Plugin '{}' is already stored".format(str(plugin))) + + plugin_data_item = self._create_plugin_data_item(plugin) + self._plugin_data_by_id[plugin.id] = plugin_data_item + + def _create_plugin_data_item(self, plugin): + label = None + if hasattr(plugin, "label"): + label = plugin.label + + return { + "id": plugin.id, + "name": plugin.__name__, + "label": label, + "order": plugin.order, + "targets": list(plugin.targets), + "instances_data": [], + "actions_data": [], + "skipped": False, + "passed": False + } + def _extract_context_data(self, context): context_label = "Context" if context is not None: @@ -674,3 +691,500 @@ def add_error(self, plugin, error, instance): plugin_id ) self._plugin_action_items[plugin_id] = plugin_actions + + +def collect_families_from_instances(instances, only_active=False): + """Collect all families for passed publish instances. + + Args: + instances(list): List of publish instances from + which are families collected. + only_active(bool): Return families only for active instances. + + Returns: + list[str]: Families available on instances. + """ + + all_families = set() + for instance in instances: + if only_active: + if instance.data.get("publish") is False: + continue + family = instance.data.get("family") + if family: + all_families.add(family) + + families = instance.data.get("families") or tuple() + for family in families: + all_families.add(family) + + return list(all_families) + + +class PublishModel: + def __init__(self, controller): + self._controller = controller + + # Publishing should stop at validation stage + self._publish_up_validation = False + self._publish_comment_is_set = False + + # Any other exception that happened during publishing + self._publish_error_msg = None + # Publishing is in progress + self._publish_is_running = False + # Publishing is over validation order + self._publish_has_validated = False + + self._publish_has_validation_errors = False + self._publish_has_crashed = False + # All publish plugins are processed + self._publish_has_started = False + self._publish_has_finished = False + self._publish_max_progress = 0 + self._publish_progress = 0 + + self._publish_plugins = [] + self._publish_plugins_proxy = None + + # pyblish.api.Context + self._publish_context = None + # Pyblish report + self._publish_report = PublishReportMaker() + # Store exceptions of validation error + self._publish_validation_errors = PublishValidationErrors() + + # This information is not much important for controller but for widget + # which can change (and set) the comment. + self._publish_comment_is_set = False + + # Validation order + # - plugin with order same or higher than this value is extractor or + # higher + self._validation_order = ( + pyblish.api.ValidatorOrder + PLUGIN_ORDER_OFFSET + ) + + # Plugin iterator + self._main_thread_iter = None + + def reset(self, create_context): + self._publish_up_validation = False + self._publish_comment_is_set = False + self._publish_has_started = False + + self._set_publish_error_msg(None) + self._set_progress(0) + self._set_is_running(False) + self._set_has_validated(False) + self._set_is_crashed(False) + self._set_has_validation_errors(False) + self._set_finished(False) + + self._main_thread_iter = self._publish_iterator() + self._publish_context = pyblish.api.Context() + # Make sure "comment" is set on publish context + self._publish_context.data["comment"] = "" + # Add access to create context during publishing + # - must not be used for changing CreatedInstances during publishing! + # QUESTION + # - pop the key after first collector using it would be safest option? + self._publish_context.data["create_context"] = create_context + publish_plugins = create_context.publish_plugins + self._publish_plugins = publish_plugins + self._publish_plugins_proxy = PublishPluginsProxy( + publish_plugins + ) + + self._publish_report.reset( + create_context.creator_discover_result, + create_context.convertor_discover_result, + create_context.publish_discover_result, + ) + for plugin in create_context.publish_plugins_mismatch_targets: + self._publish_report.set_plugin_skipped(plugin.id) + self._publish_validation_errors.reset(self._publish_plugins_proxy) + + self._set_max_progress(len(publish_plugins)) + + self._emit_event("publish.reset.finished") + + def set_publish_up_validation(self, value): + self._publish_up_validation = value + + def start_publish(self, wait=True): + """Run publishing. + + Make sure all changes are saved before method is called (Call + 'save_changes' and check output). + """ + if self._publish_up_validation and self._publish_has_validated: + return + + self._start_publish() + + if not wait: + return + + while self.is_running(): + func = self.get_next_process_func() + func() + + def get_next_process_func(self): + # Validations of progress before using iterator + # - same conditions may be inside iterator but they may be used + # only in specific cases (e.g. when it happens for a first time) + + # There are validation errors and validation is passed + # - can't do any progree + if ( + self._publish_has_validated + and self._publish_has_validation_errors + ): + item = partial(self.stop_publish) + + # Any unexpected error happened + # - everything should stop + elif self._publish_has_crashed: + item = partial(self.stop_publish) + + # Everything is ok so try to get new processing item + else: + item = next(self._main_thread_iter) + + return item + + def stop_publish(self): + if self._publish_is_running: + self._stop_publish() + + def is_running(self): + return self._publish_is_running + + def is_crashed(self): + return self._publish_has_crashed + + def has_started(self): + return self._publish_has_started + + def has_finished(self): + return self._publish_has_finished + + def has_validated(self): + return self._publish_has_validated + + def has_validation_errors(self): + return self._publish_has_validation_errors + + def get_progress(self): + return self._publish_progress + + def get_max_progress(self): + return self._publish_max_progress + + def get_publish_report(self): + return self._publish_report.get_report( + self._publish_context + ) + + def get_validation_errors(self): + return self._publish_validation_errors.create_report() + + def get_error_msg(self): + return self._publish_error_msg + + def set_comment(self, comment): + # Ignore change of comment when publishing started + if self._publish_has_started: + return + self._publish_context.data["comment"] = comment + self._publish_comment_is_set = True + + def run_action(self, plugin_id, action_id): + # TODO handle result in UI + plugin = self._publish_plugins_proxy.get_plugin(plugin_id) + action = self._publish_plugins_proxy.get_action(plugin_id, action_id) + + result = pyblish.plugin.process( + plugin, self._publish_context, None, action.id + ) + exception = result.get("error") + if exception: + self._emit_event( + "publish.action.failed", + { + "title": "Action failed", + "message": "Action failed.", + "traceback": "".join( + traceback.format_exception( + type(exception), + exception, + exception.__traceback__ + ) + ), + "label": action.__name__, + "identifier": action.id + } + ) + + self._publish_report.add_action_result(action, result) + + self._controller.emit_card_message("Action finished.") + + def _emit_event(self, topic, data=None): + self._controller.emit_event(topic, data, PUBLISH_EVENT_SOURCE) + + def _set_finished(self, value): + if self._publish_has_finished != value: + self._publish_has_finished = value + self._emit_event( + "publish.finished.changed", + {"value": value} + ) + + def _set_is_running(self, value): + if self._publish_is_running != value: + self._publish_is_running = value + self._emit_event( + "publish.is_running.changed", + {"value": value} + ) + + def _set_has_validated(self, value): + if self._publish_has_validated != value: + self._publish_has_validated = value + self._emit_event( + "publish.has_validated.changed", + {"value": value} + ) + + def _set_is_crashed(self, value): + if self._publish_has_crashed != value: + self._publish_has_crashed = value + self._emit_event( + "publish.has_crashed.changed", + {"value": value} + ) + + def _set_has_validation_errors(self, value): + if self._publish_has_validation_errors != value: + self._publish_has_validation_errors = value + self._emit_event( + "publish.has_validation_errors.changed", + {"value": value} + ) + + def _set_max_progress(self, value): + if self._publish_max_progress != value: + self._publish_max_progress = value + self._emit_event( + "publish.max_progress.changed", + {"value": value} + ) + + def _set_progress(self, value): + if self._publish_progress != value: + self._publish_progress = value + self._emit_event( + "publish.progress.changed", + {"value": value} + ) + + def _set_publish_error_msg(self, value): + if self._publish_error_msg != value: + self._publish_error_msg = value + self._emit_event( + "publish.publish_error.changed", + {"value": value} + ) + + def _start_publish(self): + """Start or continue in publishing.""" + if self._publish_is_running: + return + + self._set_is_running(True) + self._publish_has_started = True + + self._emit_event("publish.process.started") + + def _stop_publish(self): + """Stop or pause publishing.""" + self._set_is_running(False) + + self._emit_event("publish.process.stopped") + + def _publish_iterator(self): + """Main logic center of publishing. + + Iterator returns `partial` objects with callbacks that should be + processed in main thread (threaded in future?). Cares about changing + states of currently processed publish plugin and instance. Also + change state of processed orders like validation order has passed etc. + + Also stops publishing, if should stop on validation. + """ + + for idx, plugin in enumerate(self._publish_plugins): + self._publish_progress = idx + + # Check if plugin is over validation order + if not self._publish_has_validated: + self._set_has_validated( + plugin.order >= self._validation_order + ) + + # Stop if plugin is over validation order and process + # should process up to validation. + if self._publish_up_validation and self._publish_has_validated: + yield partial(self.stop_publish) + + # Stop if validation is over and validation errors happened + if ( + self._publish_has_validated + and self.has_validation_errors() + ): + yield partial(self.stop_publish) + + # Add plugin to publish report + self._publish_report.add_plugin_iter( + plugin.id, self._publish_context) + + # WARNING This is hack fix for optional plugins + if not self._is_publish_plugin_active(plugin): + self._publish_report.set_plugin_skipped(plugin.id) + continue + + # Trigger callback that new plugin is going to be processed + plugin_label = plugin.__name__ + if hasattr(plugin, "label") and plugin.label: + plugin_label = plugin.label + self._emit_event( + "publish.process.plugin.changed", + {"plugin_label": plugin_label} + ) + + # Plugin is instance plugin + if plugin.__instanceEnabled__: + instances = pyblish.logic.instances_by_plugin( + self._publish_context, plugin + ) + if not instances: + self._publish_report.set_plugin_skipped(plugin.id) + continue + + for instance in instances: + if instance.data.get("publish") is False: + continue + + instance_label = ( + instance.data.get("label") + or instance.data["name"] + ) + self._emit_event( + "publish.process.instance.changed", + {"instance_label": instance_label} + ) + + yield partial( + self._process_and_continue, plugin, instance + ) + else: + families = collect_families_from_instances( + self._publish_context, only_active=True + ) + plugins = pyblish.logic.plugins_by_families( + [plugin], families + ) + if not plugins: + self._publish_report.set_plugin_skipped(plugin.id) + continue + + instance_label = ( + self._publish_context.data.get("label") + or self._publish_context.data.get("name") + or "Context" + ) + self._emit_event( + "publish.process.instance.changed", + {"instance_label": instance_label} + ) + yield partial( + self._process_and_continue, plugin, None + ) + + self._publish_report.set_plugin_passed(plugin.id) + + # Cleanup of publishing process + self._set_finished(True) + self._set_progress(self._publish_max_progress) + yield partial(self.stop_publish) + + def _process_and_continue(self, plugin, instance): + result = pyblish.plugin.process( + plugin, self._publish_context, instance + ) + + exception = result.get("error") + if exception: + has_validation_error = False + if ( + isinstance(exception, PublishValidationError) + and not self._publish_has_validated + ): + has_validation_error = True + self._add_validation_error(result) + + else: + if isinstance(exception, KnownPublishError): + msg = str(exception) + else: + msg = ( + "Something went wrong. Send report" + " to your supervisor or Ynput team." + ) + self._set_publish_error_msg(msg) + self._set_is_crashed(True) + + result["is_validation_error"] = has_validation_error + + self._publish_report.add_result(plugin.id, result) + + def _add_validation_error(self, result): + self._set_has_validation_errors(True) + self._publish_validation_errors.add_error( + result["plugin"], + result["error"], + result["instance"] + ) + + def _is_publish_plugin_active(self, plugin): + """Decide if publish plugin is active. + + This is hack because 'active' is mis-used in mixin + 'OptionalPyblishPluginMixin' where 'active' is used for default value + of optional plugins. Because of that is 'active' state of plugin + which inherit from 'OptionalPyblishPluginMixin' ignored. That affects + headless publishing inside host, potentially remote publishing. + + We have to change that to match pyblish base, but we can do that + only when all hosts use Publisher because the change requires + change of settings schemas. + + Args: + plugin (pyblish.Plugin): Plugin which should be checked if is + active. + + Returns: + bool: Is plugin active. + """ + + if plugin.active: + return True + + if not plugin.optional: + return False + + if OptionalPyblishPluginMixin in inspect.getmro(plugin): + return True + return False From 1f6bc6829afc8c9023dea2c385ce3befe027d790 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 21 Jun 2024 13:58:40 +0200 Subject: [PATCH 06/15] separate most of publish logic to publish model --- client/ayon_core/tools/publisher/abstract.py | 48 +- client/ayon_core/tools/publisher/control.py | 720 ++++-------------- .../ayon_core/tools/publisher/control_qt.py | 50 +- .../tools/publisher/models/publish.py | 7 + .../publisher/widgets/card_view_widgets.py | 4 +- .../widgets/create_context_widgets.py | 16 +- .../tools/publisher/widgets/create_widget.py | 28 +- .../tools/publisher/widgets/folders_dialog.py | 6 +- .../tools/publisher/widgets/help_widget.py | 2 +- .../publisher/widgets/list_view_widgets.py | 4 +- .../publisher/widgets/overview_widget.py | 14 +- .../tools/publisher/widgets/publish_frame.py | 40 +- .../tools/publisher/widgets/report_page.py | 48 +- .../tools/publisher/widgets/tasks_model.py | 2 +- .../tools/publisher/widgets/widgets.py | 2 +- client/ayon_core/tools/publisher/window.py | 54 +- 16 files changed, 313 insertions(+), 732 deletions(-) diff --git a/client/ayon_core/tools/publisher/abstract.py b/client/ayon_core/tools/publisher/abstract.py index 8b46c58e87..c7a9396ae0 100644 --- a/client/ayon_core/tools/publisher/abstract.py +++ b/client/ayon_core/tools/publisher/abstract.py @@ -28,16 +28,18 @@ def log(self): pass - @property @abstractmethod - def event_system(self): - """Inner event system for publisher controller.""" + def emit_event(self, topic, data=None, source=None): + """Use implemented event system to trigger event.""" pass - @property @abstractmethod - def project_name(self): + def register_event_callback(self, topic, callback): + pass + + @abstractmethod + def get_current_project_name(self): """Current context project name. Returns: @@ -46,9 +48,8 @@ def project_name(self): pass - @property @abstractmethod - def current_folder_path(self): + def get_current_folder_path(self): """Current context folder path. Returns: @@ -57,9 +58,8 @@ def current_folder_path(self): """ pass - @property @abstractmethod - def current_task_name(self): + def get_current_task_name(self): """Current context task name. Returns: @@ -68,7 +68,6 @@ def current_task_name(self): pass - @property @abstractmethod def host_context_has_changed(self): """Host context changed after last reset. @@ -81,9 +80,8 @@ def host_context_has_changed(self): pass - @property @abstractmethod - def host_is_valid(self): + def is_host_valid(self): """Host is valid for creation part. Host must have implemented certain functionality to be able create @@ -95,15 +93,22 @@ def host_is_valid(self): pass - @property @abstractmethod - def instances(self): + def get_instances(self): """Collected/created instances. Returns: List[CreatedInstance]: List of created instances. + """ + pass + @abstractmethod + def get_instance_by_id(self, instance_id): + pass + + @abstractmethod + def get_instances_by_id(self, instance_ids=None): pass @abstractmethod @@ -212,7 +217,6 @@ def remove_instances(self, instance_ids): pass - @property @abstractmethod def publish_has_started(self): """Has publishing finished. @@ -223,7 +227,6 @@ def publish_has_started(self): pass - @property @abstractmethod def publish_has_finished(self): """Has publishing finished. @@ -234,7 +237,6 @@ def publish_has_finished(self): pass - @property @abstractmethod def publish_is_running(self): """Publishing is running right now. @@ -278,9 +280,8 @@ def publish_has_validation_errors(self): pass - @property @abstractmethod - def publish_max_progress(self): + def get_publish_max_progress(self): """Get maximum possible progress number. Returns: @@ -289,9 +290,8 @@ def publish_max_progress(self): pass - @property @abstractmethod - def publish_progress(self): + def get_publish_progress(self): """Current progress number. Returns: @@ -300,9 +300,8 @@ def publish_progress(self): pass - @property @abstractmethod - def publish_error_msg(self): + def get_publish_error_msg(self): """Current error message which cause fail of publishing. Returns: @@ -353,9 +352,8 @@ def run_action(self, plugin_id, action_id): pass - @property @abstractmethod - def convertor_items(self): + def get_convertor_items(self): pass @abstractmethod diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index a1f5889141..d12dde92ba 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -1,25 +1,18 @@ import os import logging -import traceback import tempfile import shutil -import inspect -from abc import ABCMeta, abstractmethod +from abc import abstractmethod import re -import six -import pyblish.api import ayon_api from ayon_core.lib.events import QueuedEventSystem from ayon_core.lib.profiles_filtering import filter_profiles from ayon_core.lib.attribute_definitions import UIDef from ayon_core.pipeline import ( - PublishValidationError, - KnownPublishError, registered_host, get_process_id, - OptionalPyblishPluginMixin, ) from ayon_core.pipeline.create import CreateContext from ayon_core.pipeline.create.context import ( @@ -27,30 +20,13 @@ ConvertorsOperationFailed, ) from ayon_core.tools.common_models import ProjectsModel, HierarchyModel -from ayon_core.tools.publisher.models import ( - PublishReportMaker, + +from .models import ( CreatorItem, - PublishValidationErrors, - PublishPluginsProxy, + PublishModel, ) - from .abstract import AbstractPublisherController, CardMessageTypes -# Define constant for plugin orders offset -PLUGIN_ORDER_OFFSET = 0.5 - - -class MainThreadItem: - """Callback with args and kwargs.""" - - def __init__(self, callback, *args, **kwargs): - self.callback = callback - self.args = args - self.kwargs = kwargs - - def process(self): - self.callback(*self.args, **self.kwargs) - class BasePublisherController(AbstractPublisherController): """Implement common logic for controllers. @@ -66,25 +42,9 @@ class BasePublisherController(AbstractPublisherController): def __init__(self): self._log = None - self._event_system = None - - # Host is valid for creation - self._host_is_valid = False - - # Any other exception that happened during publishing - self._publish_error_msg = None - # Publishing is in progress - self._publish_is_running = False - # Publishing is over validation order - self._publish_has_validated = False - - self._publish_has_validation_errors = False - self._publish_has_crashed = False - # All publish plugins are processed - self._publish_has_started = False - self._publish_has_finished = False - self._publish_max_progress = 0 - self._publish_progress = 0 + self._event_system_obj = None + + self._publish_model = PublishModel(self) # Controller must '_collect_creator_items' to fill the value self._creator_items = None @@ -101,197 +61,60 @@ def log(self): self._log = logging.getLogger(self.__class__.__name__) return self._log - @property - def event_system(self): - """Inner event system for publisher controller. - - Is used for communication with UI. Event system is autocreated. - - Known topics: - "show.detailed.help" - Detailed help requested (UI related). - "show.card.message" - Show card message request (UI related). - "instances.refresh.finished" - Instances are refreshed. - "plugins.refresh.finished" - Plugins refreshed. - "publish.reset.finished" - Reset finished. - "controller.reset.started" - Controller reset started. - "controller.reset.finished" - Controller reset finished. - "publish.process.started" - Publishing started. Can be started from - paused state. - "publish.process.stopped" - Publishing stopped/paused process. - "publish.process.plugin.changed" - Plugin state has changed. - "publish.process.instance.changed" - Instance state has changed. - "publish.has_validated.changed" - Attr 'publish_has_validated' - changed. - "publish.is_running.changed" - Attr 'publish_is_running' changed. - "publish.has_crashed.changed" - Attr 'publish_has_crashed' changed. - "publish.publish_error.changed" - Attr 'publish_error' - "publish.has_validation_errors.changed" - Attr - 'has_validation_errors' changed. - "publish.max_progress.changed" - Attr 'publish_max_progress' - changed. - "publish.progress.changed" - Attr 'publish_progress' changed. - "publish.host_is_valid.changed" - Attr 'host_is_valid' changed. - "publish.finished.changed" - Attr 'publish_has_finished' changed. - - Returns: - EventSystem: Event system which can trigger callbacks for topics. - """ - - if self._event_system is None: - self._event_system = QueuedEventSystem() - return self._event_system - # Events system def emit_event(self, topic, data=None, source=None): """Use implemented event system to trigger event.""" if data is None: data = {} - self.event_system.emit(topic, data, source) + self._event_system.emit(topic, data, source) def register_event_callback(self, topic, callback): - self.event_system.add_callback(topic, callback) - - def _emit_event(self, topic, data=None): - self.emit_event(topic, data, "controller") - - def _get_host_is_valid(self): - return self._host_is_valid - - def _set_host_is_valid(self, value): - if self._host_is_valid != value: - self._host_is_valid = value - self._emit_event( - "publish.host_is_valid.changed", {"value": value} - ) + self._event_system.add_callback(topic, callback) - def _get_publish_has_started(self): - return self._publish_has_started + def is_host_valid(self) -> bool: + return self._create_context.host_is_valid - def _set_publish_has_started(self, value): - if value != self._publish_has_started: - self._publish_has_started = value + def publish_has_started(self): + return self._publish_model.has_started() - def _get_publish_has_finished(self): - return self._publish_has_finished + def publish_has_finished(self): + return self._publish_model.has_finished() - def _set_publish_has_finished(self, value): - if self._publish_has_finished != value: - self._publish_has_finished = value - self._emit_event("publish.finished.changed", {"value": value}) + def publish_is_running(self): + return self._publish_model.is_running() - def _get_publish_is_running(self): - return self._publish_is_running + def publish_has_validated(self): + return self._publish_model.has_validated() - def _set_publish_is_running(self, value): - if self._publish_is_running != value: - self._publish_is_running = value - self._emit_event("publish.is_running.changed", {"value": value}) + def publish_has_crashed(self): + return self._publish_model.is_crashed() - def _get_publish_has_validated(self): - return self._publish_has_validated + def publish_has_validation_errors(self): + return self._publish_model.has_validation_errors() - def _set_publish_has_validated(self, value): - if self._publish_has_validated != value: - self._publish_has_validated = value - self._emit_event( - "publish.has_validated.changed", {"value": value} - ) + def publish_can_continue(self): + return self._publish_model.publish_can_continue() - def _get_publish_has_crashed(self): - return self._publish_has_crashed + def get_publish_max_progress(self): + return self._publish_model.get_max_progress() - def _set_publish_has_crashed(self, value): - if self._publish_has_crashed != value: - self._publish_has_crashed = value - self._emit_event("publish.has_crashed.changed", {"value": value}) + def get_publish_progress(self): + return self._publish_model.get_progress() - def _get_publish_has_validation_errors(self): - return self._publish_has_validation_errors + def get_publish_error_msg(self): + return self._publish_model.get_error_msg() - def _set_publish_has_validation_errors(self, value): - if self._publish_has_validation_errors != value: - self._publish_has_validation_errors = value - self._emit_event( - "publish.has_validation_errors.changed", - {"value": value} - ) - - def _get_publish_max_progress(self): - return self._publish_max_progress - - def _set_publish_max_progress(self, value): - if self._publish_max_progress != value: - self._publish_max_progress = value - self._emit_event("publish.max_progress.changed", {"value": value}) - - def _get_publish_progress(self): - return self._publish_progress - - def _set_publish_progress(self, value): - if self._publish_progress != value: - self._publish_progress = value - self._emit_event("publish.progress.changed", {"value": value}) - - def _get_publish_error_msg(self): - return self._publish_error_msg - - def _set_publish_error_msg(self, value): - if self._publish_error_msg != value: - self._publish_error_msg = value - self._emit_event("publish.publish_error.changed", {"value": value}) - - host_is_valid = property( - _get_host_is_valid, _set_host_is_valid - ) - publish_has_started = property( - _get_publish_has_started, _set_publish_has_started - ) - publish_has_finished = property( - _get_publish_has_finished, _set_publish_has_finished - ) - publish_is_running = property( - _get_publish_is_running, _set_publish_is_running - ) - publish_has_validated = property( - _get_publish_has_validated, _set_publish_has_validated - ) - publish_has_crashed = property( - _get_publish_has_crashed, _set_publish_has_crashed - ) - publish_has_validation_errors = property( - _get_publish_has_validation_errors, _set_publish_has_validation_errors - ) - publish_max_progress = property( - _get_publish_max_progress, _set_publish_max_progress - ) - publish_progress = property( - _get_publish_progress, _set_publish_progress - ) - publish_error_msg = property( - _get_publish_error_msg, _set_publish_error_msg - ) - - def _reset_attributes(self): - """Reset most of attributes that can be reset.""" - - self.publish_is_running = False - self.publish_has_started = False - self.publish_has_validated = False - self.publish_has_crashed = False - self.publish_has_validation_errors = False - self.publish_has_finished = False - - self.publish_error_msg = None - self.publish_progress = 0 - - @property - def creator_items(self): + def get_creator_items(self): """Creators that can be shown in create dialog.""" if self._creator_items is None: self._creator_items = self._collect_creator_items() return self._creator_items + def get_creator_item_by_id(self, identifier): + items = self.get_creator_items() + return items.get(identifier) + @abstractmethod def _collect_creator_items(self): """Receive CreatorItems to work with. @@ -309,7 +132,7 @@ def get_creator_icon(self, identifier): str: Creator's identifier for which should be icon returned. """ - creator_item = self.creator_items.get(identifier) + creator_item = self.get_creator_item_by_id(identifier) if creator_item is not None: return creator_item.icon return None @@ -334,6 +157,48 @@ def clear_thumbnail_temp_dir_path(self): if os.path.exists(dirpath): shutil.rmtree(dirpath) + @property + def _event_system(self): + """Inner event system for publisher controller. + + Is used for communication with UI. Event system is autocreated. + + Known topics: + "show.detailed.help" - Detailed help requested (UI related). + "show.card.message" - Show card message request (UI related). + "instances.refresh.finished" - Instances are refreshed. + "plugins.refresh.finished" - Plugins refreshed. + "publish.reset.finished" - Reset finished. + "controller.reset.started" - Controller reset started. + "controller.reset.finished" - Controller reset finished. + "publish.process.started" - Publishing started. Can be started from + paused state. + "publish.process.stopped" - Publishing stopped/paused process. + "publish.process.plugin.changed" - Plugin state has changed. + "publish.process.instance.changed" - Instance state has changed. + "publish.has_validated.changed" - Attr 'publish_has_validated' + changed. + "publish.is_running.changed" - Attr 'publish_is_running' changed. + "publish.has_crashed.changed" - Attr 'publish_has_crashed' changed. + "publish.publish_error.changed" - Attr 'publish_error' + "publish.has_validation_errors.changed" - Attr + 'has_validation_errors' changed. + "publish.max_progress.changed" - Attr 'publish_max_progress' + changed. + "publish.progress.changed" - Attr 'publish_progress' changed. + "publish.finished.changed" - Attr 'publish_has_finished' changed. + + Returns: + EventSystem: Event system which can trigger callbacks for topics. + """ + + if self._event_system_obj is None: + self._event_system_obj = QueuedEventSystem() + return self._event_system_obj + + def _emit_event(self, topic, data=None): + self.emit_event(topic, data, "controller") + class PublisherController(BasePublisherController): """Middleware between UI, CreateContext and publish Context. @@ -347,7 +212,7 @@ class PublisherController(BasePublisherController): _log = None def __init__(self, headless=False): - super(PublisherController, self).__init__() + super().__init__() self._host = registered_host() self._headless = headless @@ -356,31 +221,6 @@ def __init__(self, headless=False): self._host, headless=headless, reset=False ) - self._publish_plugins_proxy = None - - # pyblish.api.Context - self._publish_context = None - # Pyblish report - self._publish_report = PublishReportMaker(self) - # Store exceptions of validation error - self._publish_validation_errors = PublishValidationErrors() - - # Publishing should stop at validation stage - self._publish_up_validation = False - # This information is not much important for controller but for widget - # which can change (and set) the comment. - self._publish_comment_is_set = False - - # Validation order - # - plugin with order same or higher than this value is extractor or - # higher - self._validation_order = ( - pyblish.api.ValidatorOrder + PLUGIN_ORDER_OFFSET - ) - - # Plugin iterator - self._main_thread_iter = None - # State flags to prevent executing method which is already in progress self._resetting_plugins = False self._resetting_instances = False @@ -389,18 +229,16 @@ def __init__(self, headless=False): self._projects_model = ProjectsModel(self) self._hierarchy_model = HierarchyModel(self) - @property - def project_name(self): + def get_current_project_name(self): """Current project context defined by host. Returns: str: Project name. - """ + """ return self._create_context.get_current_project_name() - @property - def current_folder_path(self): + def get_current_folder_path(self): """Current context folder path defined by host. Returns: @@ -409,8 +247,7 @@ def current_folder_path(self): return self._create_context.get_current_folder_path() - @property - def current_task_name(self): + def get_current_task_name(self): """Current context task name defined by host. Returns: @@ -419,17 +256,36 @@ def current_task_name(self): return self._create_context.get_current_task_name() - @property def host_context_has_changed(self): return self._create_context.context_has_changed @property def instances(self): + """Current instances in create context. + + Deprecated: + Use 'get_instances' instead. Kept for backwards compatibility with + traypublisher. + + """ + return self.get_instances() + + def get_instances(self): """Current instances in create context.""" - return self._create_context.instances_by_id + return list(self._create_context.instances_by_id.values()) - @property - def convertor_items(self): + def get_instance_by_id(self, instance_id): + return self._create_context.instances_by_id.get(instance_id) + + def get_instances_by_id(self, instance_ids=None): + if instance_ids is None: + instance_ids = self._create_context.instances_by_id.keys() + return { + instance_id: self.get_instance_by_id(instance_id) + for instance_id in instance_ids + } + + def get_convertor_items(self): return self._create_context.convertor_items_by_id @property @@ -438,11 +294,6 @@ def _creators(self): return self._create_context.creators - @property - def _publish_plugins(self): - """Publish plugins.""" - return self._create_context.publish_plugins - def _get_current_project_settings(self): """Current project settings. @@ -484,7 +335,7 @@ def get_folder_id_from_path(self, folder_path): if not folder_path: return None folder_item = self._hierarchy_model.get_folder_item_by_path( - self.project_name, folder_path + self.get_current_project_name(), folder_path ) if folder_item: return folder_item.entity_id @@ -494,13 +345,13 @@ def get_task_items_by_folder_paths(self, folder_paths): if not folder_paths: return {} folder_items = self._hierarchy_model.get_folder_items_by_paths( - self.project_name, folder_paths + self.get_current_project_name(), folder_paths ) output = { folder_path: [] for folder_path in folder_paths } - project_name = self.project_name + project_name = self.get_current_project_name() for folder_item in folder_items.values(): task_items = self._hierarchy_model.get_task_items( project_name, folder_item.entity_id, None @@ -514,7 +365,7 @@ def are_folder_paths_valid(self, folder_paths): return True folder_paths = set(folder_paths) folder_items = self._hierarchy_model.get_folder_items_by_paths( - self.project_name, folder_paths + self.get_current_project_name(), folder_paths ) for folder_item in folder_items.values(): if folder_item is None: @@ -539,7 +390,7 @@ def get_context_title(self): def get_existing_product_names(self, folder_path): if not folder_path: return None - project_name = self.project_name + project_name = self.get_current_project_name() folder_item = self._hierarchy_model.get_folder_item_by_path( project_name, folder_path ) @@ -562,8 +413,6 @@ def reset(self): self._emit_event("controller.reset.started") - self.host_is_valid = self._create_context.host_is_valid - self._create_context.reset_preparation() # Reset current context @@ -573,7 +422,7 @@ def reset(self): self._reset_plugins() # Publish part must be reset after plugins - self._reset_publish() + self._publish_model.reset(self._create_context) self._reset_instances() self._create_context.reset_finalization() @@ -603,11 +452,12 @@ def _collect_creator_items(self): allowed_creator_pattern = self._get_allowed_creators_pattern() for identifier, creator in self._create_context.creators.items(): try: - if (not self._is_label_allowed( - creator.label, allowed_creator_pattern)): - self.log.debug(f"{creator.label} not allowed for context") + if self._is_label_allowed( + creator.label, allowed_creator_pattern + ): + output[identifier] = CreatorItem.from_creator(creator) continue - output[identifier] = CreatorItem.from_creator(creator) + self.log.debug(f"{creator.label} not allowed for context") except Exception: self.log.error( "Failed to create creator item for '%s'", @@ -638,7 +488,7 @@ def _get_allowed_creators_pattern(self): ["filter_creator_profiles"] ) filtering_criteria = { - "task_names": self.current_task_name, + "task_names": self.get_current_task_name(), "task_types": task_type, "host_names": self._create_context.host_name } @@ -863,9 +713,9 @@ def get_product_name( instance = None if instance_id: - instance = self.instances[instance_id] + instance = self.get_instance_by_id(instance_id) - project_name = self.project_name + project_name = self.get_current_project_name() folder_item = self._hierarchy_model.get_folder_item_by_path( project_name, folder_path ) @@ -1022,37 +872,10 @@ def _on_create_instance_change(self): self._emit_event("instances.refresh.finished") def get_publish_report(self): - return self._publish_report.get_report(self._publish_plugins) + return self._publish_model.get_publish_report() def get_validation_errors(self): - return self._publish_validation_errors.create_report() - - def _reset_publish(self): - self._reset_attributes() - - self._publish_up_validation = False - self._publish_comment_is_set = False - - self._main_thread_iter = self._publish_iterator() - self._publish_context = pyblish.api.Context() - # Make sure "comment" is set on publish context - self._publish_context.data["comment"] = "" - # Add access to create context during publishing - # - must not be used for changing CreatedInstances during publishing! - # QUESTION - # - pop the key after first collector using it would be safest option? - self._publish_context.data["create_context"] = self._create_context - - self._publish_plugins_proxy = PublishPluginsProxy( - self._publish_plugins - ) - - self._publish_report.reset(self._publish_context, self._create_context) - self._publish_validation_errors.reset(self._publish_plugins_proxy) - - self.publish_max_progress = len(self._publish_plugins) - - self._emit_event("publish.reset.finished") + return self._publish_model.get_validation_errors() def set_comment(self, comment): """Set comment from ui to pyblish context. @@ -1062,9 +885,7 @@ def set_comment(self, comment): '_publish_comment_is_set' is used to keep track about the information. """ - if not self._publish_comment_is_set: - self._publish_context.data["comment"] = comment - self._publish_comment_is_set = True + self._publish_model.set_comment(comment) def publish(self): """Run publishing. @@ -1072,9 +893,7 @@ def publish(self): Make sure all changes are saved before method is called (Call 'save_changes' and check output). """ - - self._publish_up_validation = False - self._start_publish() + self._start_publish(False) def validate(self): """Run publishing and stop after Validation. @@ -1082,292 +901,15 @@ def validate(self): Make sure all changes are saved before method is called (Call 'save_changes' and check output). """ - - if self.publish_has_validated: - return - self._publish_up_validation = True - self._start_publish() - - def _start_publish(self): - """Start or continue in publishing.""" - if self.publish_is_running: - return - - self.publish_is_running = True - self.publish_has_started = True - - self._emit_event("publish.process.started") - - self._publish_next_process() - - def _stop_publish(self): - """Stop or pause publishing.""" - self.publish_is_running = False - - self._emit_event("publish.process.stopped") + self._start_publish(True) def stop_publish(self): """Stop publishing process (any reason).""" - - if self.publish_is_running: - self._stop_publish() + self._publish_model.stop_publish() def run_action(self, plugin_id, action_id): - # TODO handle result in UI - plugin = self._publish_plugins_proxy.get_plugin(plugin_id) - action = self._publish_plugins_proxy.get_action(plugin_id, action_id) - - result = pyblish.plugin.process( - plugin, self._publish_context, None, action.id - ) - exception = result.get("error") - if exception: - self._emit_event( - "publish.action.failed", - { - "title": "Action failed", - "message": "Action failed.", - "traceback": "".join( - traceback.format_exception( - type(exception), - exception, - exception.__traceback__ - ) - ), - "label": action.__name__, - "identifier": action.id - } - ) - - self._publish_report.add_action_result(action, result) - - self.emit_card_message("Action finished.") - - def _publish_next_process(self): - # Validations of progress before using iterator - # - same conditions may be inside iterator but they may be used - # only in specific cases (e.g. when it happens for a first time) - - # There are validation errors and validation is passed - # - can't do any progree - if ( - self.publish_has_validated - and self.publish_has_validation_errors - ): - item = MainThreadItem(self.stop_publish) - - # Any unexpected error happened - # - everything should stop - elif self.publish_has_crashed: - item = MainThreadItem(self.stop_publish) - - # Everything is ok so try to get new processing item - else: - item = next(self._main_thread_iter) - - self._process_main_thread_item(item) - - def _process_main_thread_item(self, item): - item() - - def _is_publish_plugin_active(self, plugin): - """Decide if publish plugin is active. - - This is hack because 'active' is mis-used in mixin - 'OptionalPyblishPluginMixin' where 'active' is used for default value - of optional plugins. Because of that is 'active' state of plugin - which inherit from 'OptionalPyblishPluginMixin' ignored. That affects - headless publishing inside host, potentially remote publishing. - - We have to change that to match pyblish base, but we can do that - only when all hosts use Publisher because the change requires - change of settings schemas. - - Args: - plugin (pyblish.Plugin): Plugin which should be checked if is - active. - - Returns: - bool: Is plugin active. - """ - - if plugin.active: - return True - - if not plugin.optional: - return False - - if OptionalPyblishPluginMixin in inspect.getmro(plugin): - return True - return False - - def _publish_iterator(self): - """Main logic center of publishing. - - Iterator returns `MainThreadItem` objects with callbacks that should be - processed in main thread (threaded in future?). Cares about changing - states of currently processed publish plugin and instance. Also - change state of processed orders like validation order has passed etc. - - Also stops publishing, if should stop on validation. - """ - - for idx, plugin in enumerate(self._publish_plugins): - self._publish_progress = idx - - # Check if plugin is over validation order - if not self.publish_has_validated: - self.publish_has_validated = ( - plugin.order >= self._validation_order - ) - - # Stop if plugin is over validation order and process - # should process up to validation. - if self._publish_up_validation and self.publish_has_validated: - yield MainThreadItem(self.stop_publish) - - # Stop if validation is over and validation errors happened - if ( - self.publish_has_validated - and self.publish_has_validation_errors - ): - yield MainThreadItem(self.stop_publish) - - # Add plugin to publish report - self._publish_report.add_plugin_iter( - plugin, self._publish_context) - - # WARNING This is hack fix for optional plugins - if not self._is_publish_plugin_active(plugin): - self._publish_report.set_plugin_skipped() - continue - - # Trigger callback that new plugin is going to be processed - plugin_label = plugin.__name__ - if hasattr(plugin, "label") and plugin.label: - plugin_label = plugin.label - self._emit_event( - "publish.process.plugin.changed", - {"plugin_label": plugin_label} - ) - - # Plugin is instance plugin - if plugin.__instanceEnabled__: - instances = pyblish.logic.instances_by_plugin( - self._publish_context, plugin - ) - if not instances: - self._publish_report.set_plugin_skipped() - continue - - for instance in instances: - if instance.data.get("publish") is False: - continue - - instance_label = ( - instance.data.get("label") - or instance.data["name"] - ) - self._emit_event( - "publish.process.instance.changed", - {"instance_label": instance_label} - ) - - yield MainThreadItem( - self._process_and_continue, plugin, instance - ) - else: - families = collect_families_from_instances( - self._publish_context, only_active=True - ) - plugins = pyblish.logic.plugins_by_families( - [plugin], families - ) - if plugins: - instance_label = ( - self._publish_context.data.get("label") - or self._publish_context.data.get("name") - or "Context" - ) - self._emit_event( - "publish.process.instance.changed", - {"instance_label": instance_label} - ) - yield MainThreadItem( - self._process_and_continue, plugin, None - ) - else: - self._publish_report.set_plugin_skipped() - - # Cleanup of publishing process - self.publish_has_finished = True - self.publish_progress = self.publish_max_progress - yield MainThreadItem(self.stop_publish) - - def _add_validation_error(self, result): - self.publish_has_validation_errors = True - self._publish_validation_errors.add_error( - result["plugin"], - result["error"], - result["instance"] - ) - - def _process_and_continue(self, plugin, instance): - result = pyblish.plugin.process( - plugin, self._publish_context, instance - ) - - exception = result.get("error") - if exception: - has_validation_error = False - if ( - isinstance(exception, PublishValidationError) - and not self.publish_has_validated - ): - has_validation_error = True - self._add_validation_error(result) - - else: - if isinstance(exception, KnownPublishError): - msg = str(exception) - else: - msg = ( - "Something went wrong. Send report" - " to your supervisor or Ynput team." - ) - self.publish_error_msg = msg - self.publish_has_crashed = True - - result["is_validation_error"] = has_validation_error - - self._publish_report.add_result(result) - - self._publish_next_process() - - -def collect_families_from_instances(instances, only_active=False): - """Collect all families for passed publish instances. - - Args: - instances(list): List of publish instances from - which are families collected. - only_active(bool): Return families only for active instances. - - Returns: - list[str]: Families available on instances. - """ - - all_families = set() - for instance in instances: - if only_active: - if instance.data.get("publish") is False: - continue - family = instance.data.get("family") - if family: - all_families.add(family) - - families = instance.data.get("families") or tuple() - for family in families: - all_families.add(family) + self._publish_model.run_action(plugin_id, action_id) - return list(all_families) + def _start_publish(self, up_validation): + self._publish_model.set_publish_up_validation(up_validation) + self._publish_model.start_publish(wait=True) diff --git a/client/ayon_core/tools/publisher/control_qt.py b/client/ayon_core/tools/publisher/control_qt.py index bef3a5af3b..223fae4775 100644 --- a/client/ayon_core/tools/publisher/control_qt.py +++ b/client/ayon_core/tools/publisher/control_qt.py @@ -7,12 +7,26 @@ from ayon_core.pipeline.create import CreatedInstance from .control import ( - MainThreadItem, PublisherController, BasePublisherController, ) +class MainThreadItem: + """Callback with args and kwargs.""" + + def __init__(self, callback, *args, **kwargs): + self.callback = callback + self.args = args + self.kwargs = kwargs + + def __call__(self): + self.process() + + def process(self): + self.callback(*self.args, **self.kwargs) + + class MainThreadProcess(QtCore.QObject): """Qt based main thread process executor. @@ -35,10 +49,6 @@ def __init__(self): self._timer = timer self._switch_counter = self.count_timeout - def process(self, func, *args, **kwargs): - item = MainThreadItem(func, *args, **kwargs) - self.add_item(item) - def add_item(self, item): self._items_to_process.append(item) @@ -75,17 +85,33 @@ def __init__(self, *args, **kwargs): super(QtPublisherController, self).__init__(*args, **kwargs) - self.event_system.add_callback( + self.register_event_callback( "publish.process.started", self._qt_on_publish_start ) - self.event_system.add_callback( + self.register_event_callback( "publish.process.stopped", self._qt_on_publish_stop ) def _reset_publish(self): - super(QtPublisherController, self)._reset_publish() + super()._reset_publish() self._main_thread_processor.clear() + def _start_publish(self, up_validation): + self._publish_model.set_publish_up_validation(up_validation) + self._publish_model.start_publish(wait=False) + self._process_main_thread_item( + MainThreadItem(self._next_publish_item_process) + ) + + def _next_publish_item_process(self): + if not self._publish_model.is_running(): + return + func = self._publish_model.get_next_process_func() + self._process_main_thread_item(MainThreadItem(func)) + self._process_main_thread_item( + MainThreadItem(self._next_publish_item_process) + ) + def _process_main_thread_item(self, item): self._main_thread_processor.add_item(item) @@ -211,8 +237,8 @@ def project_name(self): pass - @abstractproperty - def current_folder_path(self): + @abstractmethod + def get_current_folder_path(self): """Current context folder path from host. Returns: @@ -221,8 +247,8 @@ def current_folder_path(self): """ pass - @abstractproperty - def current_task_name(self): + @abstractmethod + def get_current_task_name(self): """Current context task name from client. Returns: diff --git a/client/ayon_core/tools/publisher/models/publish.py b/client/ayon_core/tools/publisher/models/publish.py index 9660862ee8..c1d1dc1031 100644 --- a/client/ayon_core/tools/publisher/models/publish.py +++ b/client/ayon_core/tools/publisher/models/publish.py @@ -876,6 +876,13 @@ def has_validated(self): def has_validation_errors(self): return self._publish_has_validation_errors + def publish_can_continue(self): + return ( + not self._publish_has_crashed + and not self._publish_has_validation_errors + and not self._publish_has_finished + ) + def get_progress(self): return self._publish_progress diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py index 4e34f9b58c..5c12f0fbb0 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -694,7 +694,7 @@ def refresh(self): # Prepare instances by group and identifiers by group instances_by_group = collections.defaultdict(list) identifiers_by_group = collections.defaultdict(set) - for instance in self._controller.instances.values(): + for instance in self._controller.get_instances(): group_name = instance.group_label instances_by_group[group_name].append(instance) identifiers_by_group[group_name].add( @@ -787,7 +787,7 @@ def _make_sure_context_widget_exists(self): self._content_layout.insertWidget(0, widget) def _update_convertor_items_group(self): - convertor_items = self._controller.convertor_items + convertor_items = self._controller.get_convertor_items() if not convertor_items and self._convertor_items_group is None: return diff --git a/client/ayon_core/tools/publisher/widgets/create_context_widgets.py b/client/ayon_core/tools/publisher/widgets/create_context_widgets.py index 18df798bf0..6e665bc963 100644 --- a/client/ayon_core/tools/publisher/widgets/create_context_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/create_context_widgets.py @@ -118,7 +118,7 @@ def register_event_callback(self, topic, callback): self.event_system.add_callback(topic, callback) def get_project_name(self): - return self._controller.project_name + return self._controller.get_current_project_name() def get_folder_items(self, project_name, sender=None): return self._controller.get_folder_items(project_name, sender) @@ -234,12 +234,12 @@ def get_selected_task_type(self): def update_current_context_btn(self): # Hide set current folder if there is no one - folder_path = self._controller.current_folder_path + folder_path = self._controller.get_current_folder_path() self._current_context_btn.setVisible(bool(folder_path)) def set_selected_context(self, folder_id, task_name): self._hierarchy_controller.set_expected_selection( - self._controller.project_name, + self._controller.get_current_project_name(), folder_id, task_name ) @@ -270,13 +270,13 @@ def set_enabled(self, enabled): ) def refresh(self): - self._last_project_name = self._controller.project_name + self._last_project_name = self._controller.get_current_project_name() folder_id = self._last_folder_id task_name = self._last_selected_task_name if folder_id is None: - folder_path = self._controller.current_folder_path + folder_path = self._controller.get_current_folder_path() folder_id = self._controller.get_folder_id_from_path(folder_path) - task_name = self._controller.current_task_name + task_name = self._controller.get_current_task_name() self._hierarchy_controller.set_selected_project( self._last_project_name ) @@ -295,8 +295,8 @@ def _on_task_change(self): self.task_changed.emit() def _on_current_context_click(self): - folder_path = self._controller.current_folder_path - task_name = self._controller.current_task_name + folder_path = self._controller.get_current_folder_path() + task_name = self._controller.get_current_task_name() folder_id = self._controller.get_folder_id_from_path(folder_path) self._hierarchy_controller.set_expected_selection( self._last_project_name, folder_id, task_name diff --git a/client/ayon_core/tools/publisher/widgets/create_widget.py b/client/ayon_core/tools/publisher/widgets/create_widget.py index 2e4ca34138..fbab0cec84 100644 --- a/client/ayon_core/tools/publisher/widgets/create_widget.py +++ b/client/ayon_core/tools/publisher/widgets/create_widget.py @@ -274,10 +274,10 @@ def __init__(self, controller, parent=None): thumbnail_widget.thumbnail_created.connect(self._on_thumbnail_create) thumbnail_widget.thumbnail_cleared.connect(self._on_thumbnail_clear) - controller.event_system.add_callback( + controller.register_event_callback( "main.window.closed", self._on_main_window_close ) - controller.event_system.add_callback( + controller.register_event_callback( "plugins.refresh.finished", self._on_plugins_refresh ) @@ -313,13 +313,11 @@ def __init__(self, controller, parent=None): self._last_current_context_task = None self._use_current_context = True - @property - def current_folder_path(self): - return self._controller.current_folder_path + def get_current_folder_path(self): + return self._controller.get_current_folder_path() - @property - def current_task_name(self): - return self._controller.current_task_name + def get_current_task_name(self): + return self._controller.get_current_task_name() def _context_change_is_enabled(self): return self._context_widget.is_enabled() @@ -330,7 +328,7 @@ def _get_folder_path(self): folder_path = self._context_widget.get_selected_folder_path() if folder_path is None: - folder_path = self.current_folder_path + folder_path = self.get_current_folder_path() return folder_path or None def _get_folder_id(self): @@ -348,7 +346,7 @@ def _get_task_name(self): task_name = self._context_widget.get_selected_task_name() if not task_name: - task_name = self.current_task_name + task_name = self.get_current_task_name() return task_name def _set_context_enabled(self, enabled): @@ -364,8 +362,8 @@ def _on_main_window_close(self): self._use_current_context = True def refresh(self): - current_folder_path = self._controller.current_folder_path - current_task_name = self._controller.current_task_name + current_folder_path = self._controller.get_current_folder_path() + current_task_name = self._controller.get_current_task_name() # Get context before refresh to keep selection of folder and # task widgets @@ -481,7 +479,7 @@ def _refresh_creators(self): # Add new create plugins new_creators = set() - creator_items_by_identifier = self._controller.creator_items + creator_items_by_identifier = self._controller.get_creator_items() for identifier, creator_item in creator_items_by_identifier.items(): if creator_item.creator_type != "artist": continue @@ -562,7 +560,7 @@ def _set_creator_detailed_text(self, creator_item): description = "" if creator_item is not None: description = creator_item.detailed_description or description - self._controller.event_system.emit( + self._controller.emit_event( "show.detailed.help", { "message": description @@ -571,7 +569,7 @@ def _set_creator_detailed_text(self, creator_item): ) def _set_creator_by_identifier(self, identifier): - creator_item = self._controller.creator_items.get(identifier) + creator_item = self._controller.get_creator_item_by_id(identifier) self._set_creator(creator_item) def _set_creator(self, creator_item): diff --git a/client/ayon_core/tools/publisher/widgets/folders_dialog.py b/client/ayon_core/tools/publisher/widgets/folders_dialog.py index 28bdce37b1..baf229290d 100644 --- a/client/ayon_core/tools/publisher/widgets/folders_dialog.py +++ b/client/ayon_core/tools/publisher/widgets/folders_dialog.py @@ -62,7 +62,7 @@ def __init__(self, controller, parent): layout.addWidget(folders_widget, 1) layout.addLayout(btns_layout, 0) - controller.event_system.add_callback( + controller.register_event_callback( "controller.reset.finished", self._on_controller_reset ) @@ -119,7 +119,9 @@ def reset(self, force=True): if self._soft_reset_enabled: self._soft_reset_enabled = False - self._folders_widget.set_project_name(self._controller.project_name) + self._folders_widget.set_project_name( + self._controller.get_current_project_name() + ) def _on_filter_change(self, text): """Trigger change of filter of folders.""" diff --git a/client/ayon_core/tools/publisher/widgets/help_widget.py b/client/ayon_core/tools/publisher/widgets/help_widget.py index 5d474613df..aaff60acba 100644 --- a/client/ayon_core/tools/publisher/widgets/help_widget.py +++ b/client/ayon_core/tools/publisher/widgets/help_widget.py @@ -64,7 +64,7 @@ def __init__(self, controller, parent): main_layout = QtWidgets.QHBoxLayout(self) main_layout.addWidget(help_content, 1) - controller.event_system.add_callback( + controller.register_event_callback( "show.detailed.help", self._on_help_request ) diff --git a/client/ayon_core/tools/publisher/widgets/list_view_widgets.py b/client/ayon_core/tools/publisher/widgets/list_view_widgets.py index 71be0ab1a4..714edb5eef 100644 --- a/client/ayon_core/tools/publisher/widgets/list_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/list_view_widgets.py @@ -581,7 +581,7 @@ def refresh(self): # Prepare instances by their groups instances_by_group_name = collections.defaultdict(list) group_names = set() - for instance in self._controller.instances.values(): + for instance in self._controller.get_instances(): group_label = instance.group_label group_names.add(group_label) instances_by_group_name[group_label].append(instance) @@ -745,7 +745,7 @@ def _make_sure_context_item_exists(self): def _update_convertor_items_group(self): created_new_items = False - convertor_items_by_id = self._controller.convertor_items + convertor_items_by_id = self._controller.get_convertor_items() group_item = self._convertor_group_item if not convertor_items_by_id and group_item is None: return created_new_items diff --git a/client/ayon_core/tools/publisher/widgets/overview_widget.py b/client/ayon_core/tools/publisher/widgets/overview_widget.py index cedf52ae01..a8eb4c7116 100644 --- a/client/ayon_core/tools/publisher/widgets/overview_widget.py +++ b/client/ayon_core/tools/publisher/widgets/overview_widget.py @@ -139,16 +139,16 @@ def __init__(self, controller, parent): ) # --- Controller callbacks --- - controller.event_system.add_callback( + controller.register_event_callback( "publish.process.started", self._on_publish_start ) - controller.event_system.add_callback( + controller.register_event_callback( "controller.reset.started", self._on_controller_reset_start ) - controller.event_system.add_callback( + controller.register_event_callback( "publish.reset.finished", self._on_publish_reset ) - controller.event_system.add_callback( + controller.register_event_callback( "instances.refresh.finished", self._on_instances_refresh ) @@ -291,7 +291,7 @@ def _on_product_change(self, *_args): # Disable delete button if nothing is selected self._delete_btn.setEnabled(len(instance_ids) > 0) - instances_by_id = self._controller.instances + instances_by_id = self._controller.get_instances_by_id(instance_ids) instances = [ instances_by_id[instance_id] for instance_id in instance_ids @@ -454,7 +454,9 @@ def _on_publish_reset(self): self._create_btn.setEnabled(True) self._product_attributes_wrap.setEnabled(True) - self._product_content_widget.setEnabled(self._controller.host_is_valid) + self._product_content_widget.setEnabled( + self._controller.is_host_valid() + ) def _on_instances_refresh(self): """Controller refreshed instances.""" diff --git a/client/ayon_core/tools/publisher/widgets/publish_frame.py b/client/ayon_core/tools/publisher/widgets/publish_frame.py index ee65c69c19..12f7be8109 100644 --- a/client/ayon_core/tools/publisher/widgets/publish_frame.py +++ b/client/ayon_core/tools/publisher/widgets/publish_frame.py @@ -157,23 +157,23 @@ def __init__(self, controller, borders, parent): shrunk_anim.valueChanged.connect(self._on_shrunk_anim) shrunk_anim.finished.connect(self._on_shrunk_anim_finish) - controller.event_system.add_callback( + controller.register_event_callback( "publish.reset.finished", self._on_publish_reset ) - controller.event_system.add_callback( + controller.register_event_callback( "publish.process.started", self._on_publish_start ) - controller.event_system.add_callback( + controller.register_event_callback( "publish.has_validated.changed", self._on_publish_validated_change ) - controller.event_system.add_callback( + controller.register_event_callback( "publish.process.stopped", self._on_publish_stop ) - controller.event_system.add_callback( + controller.register_event_callback( "publish.process.instance.changed", self._on_instance_change ) - controller.event_system.add_callback( + controller.register_event_callback( "publish.process.plugin.changed", self._on_plugin_change ) @@ -314,8 +314,12 @@ def _on_publish_reset(self): self._validate_btn.setEnabled(True) self._publish_btn.setEnabled(True) - self._progress_bar.setValue(self._controller.publish_progress) - self._progress_bar.setMaximum(self._controller.publish_max_progress) + self._progress_bar.setValue( + self._controller.get_publish_progress() + ) + self._progress_bar.setMaximum( + self._controller.get_publish_max_progress() + ) def _on_publish_start(self): if self._last_plugin_label: @@ -351,12 +355,12 @@ def _on_plugin_change(self, event): """Change plugin label when instance is going to be processed.""" self._last_plugin_label = event["plugin_label"] - self._progress_bar.setValue(self._controller.publish_progress) + self._progress_bar.setValue(self._controller.get_publish_progress()) self._plugin_label.setText(event["plugin_label"]) QtWidgets.QApplication.processEvents() def _on_publish_stop(self): - self._progress_bar.setValue(self._controller.publish_progress) + self._progress_bar.setValue(self._controller.get_publish_progress()) self._reset_btn.setEnabled(True) self._stop_btn.setEnabled(False) @@ -376,19 +380,19 @@ def _on_publish_stop(self): publish_enabled = False else: - publish_enabled = not self._controller.publish_has_finished + publish_enabled = not self._controller.publish_has_finished() self._validate_btn.setEnabled(validate_enabled) self._publish_btn.setEnabled(publish_enabled) - if self._controller.publish_has_crashed: + if self._controller.publish_has_crashed(): self._set_error_msg() - elif self._controller.publish_has_validation_errors: + elif self._controller.publish_has_validation_errors(): self._set_progress_visibility(False) self._set_validation_errors() - elif self._controller.publish_has_finished: + elif self._controller.publish_has_finished(): self._set_finished() else: @@ -411,7 +415,9 @@ def _set_error_msg(self): self._set_main_label("Error happened") - self._message_label_top.setText(self._controller.publish_error_msg) + self._message_label_top.setText( + self._controller.get_publish_error_msg() + ) self._set_success_property(1) @@ -467,11 +473,11 @@ def _set_success_property(self, state=None): def _on_report_triggered(self, identifier): if identifier == "export_report": - self._controller.event_system.emit( + self._controller.emit_event( "export_report.request", {}, "publish_frame") elif identifier == "copy_report": - self._controller.event_system.emit( + self._controller.emit_event( "copy_report.request", {}, "publish_frame") elif identifier == "go_to_report": diff --git a/client/ayon_core/tools/publisher/widgets/report_page.py b/client/ayon_core/tools/publisher/widgets/report_page.py index 7475b39f52..e8b29aefc2 100644 --- a/client/ayon_core/tools/publisher/widgets/report_page.py +++ b/client/ayon_core/tools/publisher/widgets/report_page.py @@ -742,7 +742,7 @@ class PublishInstanceCardWidget(BaseClickableFrame): _success_pix = None _in_progress_pix = None - def __init__(self, instance, icon, publish_finished, parent): + def __init__(self, instance, icon, publish_can_continue, parent): super(PublishInstanceCardWidget, self).__init__(parent) self.setObjectName("CardViewWidget") @@ -756,10 +756,10 @@ def __init__(self, instance, icon, publish_finished, parent): state_pix = self.get_error_pix() elif instance.warned: state_pix = self.get_warning_pix() - elif publish_finished: - state_pix = self.get_success_pix() - else: + elif publish_can_continue: state_pix = self.get_in_progress_pix() + else: + state_pix = self.get_success_pix() state_label = IconValuePixmapLabel(state_pix, self) @@ -970,11 +970,7 @@ def update_instances(self, instance_items): widgets = [] group_widgets = [] - publish_finished = ( - self._controller.publish_has_crashed - or self._controller.publish_has_validation_errors - or self._controller.publish_has_finished - ) + publish_can_continue = self._controller.publish_can_continue() instances_by_family = collections.defaultdict(list) for instance_item in instance_items: if not instance_item.exists: @@ -996,7 +992,10 @@ def update_instances(self, instance_items): icon = identifier_icons[instance_item.creator_identifier] widget = PublishInstanceCardWidget( - instance_item, icon, publish_finished, self._instance_view + instance_item, + icon, + publish_can_continue, + self._instance_view ) widget.selection_requested.connect(self._on_selection_request) self._instance_layout.addWidget(widget, 0) @@ -1502,11 +1501,11 @@ def __init__(self, controller, parent): self._controller = controller def _on_copy_to_clipboard(self): - self._controller.event_system.emit( + self._controller.emit_event( "copy_report.request", {}, "report_page") def _on_save_to_disk_click(self): - self._controller.event_system.emit( + self._controller.emit_event( "export_report.request", {}, "report_page") @@ -1744,8 +1743,8 @@ def update_data(self): view = self._instances_view validation_error_mode = False if ( - not self._controller.publish_has_crashed - and self._controller.publish_has_validation_errors + not self._controller.publish_has_crashed() + and self._controller.publish_has_validation_errors() ): view = self._validation_error_view validation_error_mode = True @@ -1755,8 +1754,9 @@ def update_data(self): self._detail_input_scroll.setVisible(validation_error_mode) self._views_layout.setCurrentWidget(view) - self._crash_widget.setVisible(self._controller.publish_has_crashed) - self._logs_view.setVisible(not self._controller.publish_has_crashed) + is_crashed = self._controller.publish_has_crashed() + self._crash_widget.setVisible(is_crashed) + self._logs_view.setVisible(not is_crashed) # Instance view & logs update instance_items = self._get_instance_items() @@ -1832,13 +1832,13 @@ def __init__(self, controller, parent): layout.addWidget(header_label, 0) layout.addWidget(publish_instances_widget, 0) - controller.event_system.add_callback( + controller.register_event_callback( "publish.process.started", self._on_publish_start ) - controller.event_system.add_callback( + controller.register_event_callback( "publish.reset.finished", self._on_publish_reset ) - controller.event_system.add_callback( + controller.register_event_callback( "publish.process.stopped", self._on_publish_stop ) @@ -1848,14 +1848,14 @@ def __init__(self, controller, parent): self._controller = controller def _update_label(self): - if not self._controller.publish_has_started: + if not self._controller.publish_has_started(): # This probably never happen when this widget is visible header_label = "Nothing to report until you run publish" - elif self._controller.publish_has_crashed: + elif self._controller.publish_has_crashed(): header_label = "Publish error report" - elif self._controller.publish_has_validation_errors: + elif self._controller.publish_has_validation_errors(): header_label = "Publish validation report" - elif self._controller.publish_has_finished: + elif self._controller.publish_has_finished(): header_label = "Publish success report" else: header_label = "Publish report" @@ -1863,7 +1863,7 @@ def _update_label(self): def _update_state(self): self._update_label() - publish_started = self._controller.publish_has_started + publish_started = self._controller.publish_has_started() self._publish_instances_widget.setVisible(publish_started) if publish_started: self._publish_instances_widget.update_data() diff --git a/client/ayon_core/tools/publisher/widgets/tasks_model.py b/client/ayon_core/tools/publisher/widgets/tasks_model.py index 03fb95a310..a7db953821 100644 --- a/client/ayon_core/tools/publisher/widgets/tasks_model.py +++ b/client/ayon_core/tools/publisher/widgets/tasks_model.py @@ -135,7 +135,7 @@ def reset(self): task_type_items = { task_type_item.name: task_type_item for task_type_item in self._controller.get_task_type_items( - self._controller.project_name + self._controller.get_current_project_name() ) } icon_name_by_task_name = {} diff --git a/client/ayon_core/tools/publisher/widgets/widgets.py b/client/ayon_core/tools/publisher/widgets/widgets.py index 12c03c7eeb..4299a572b3 100644 --- a/client/ayon_core/tools/publisher/widgets/widgets.py +++ b/client/ayon_core/tools/publisher/widgets/widgets.py @@ -1734,7 +1734,7 @@ def __init__(self, controller, parent): thumbnail_widget.thumbnail_created.connect(self._on_thumbnail_create) thumbnail_widget.thumbnail_cleared.connect(self._on_thumbnail_clear) - controller.event_system.add_callback( + controller.register_event_callback( "instance.thumbnail.changed", self._on_thumbnail_changed ) diff --git a/client/ayon_core/tools/publisher/window.py b/client/ayon_core/tools/publisher/window.py index 1b13ced317..090eebdd7a 100644 --- a/client/ayon_core/tools/publisher/window.py +++ b/client/ayon_core/tools/publisher/window.py @@ -273,55 +273,55 @@ def __init__(self, parent=None, controller=None, reset_on_show=None): self._on_create_overlay_button_click ) - controller.event_system.add_callback( + controller.register_event_callback( "instances.refresh.finished", self._on_instances_refresh ) - controller.event_system.add_callback( + controller.register_event_callback( "publish.reset.finished", self._on_publish_reset ) - controller.event_system.add_callback( + controller.register_event_callback( "controller.reset.finished", self._on_controller_reset ) - controller.event_system.add_callback( + controller.register_event_callback( "publish.process.started", self._on_publish_start ) - controller.event_system.add_callback( + controller.register_event_callback( "publish.has_validated.changed", self._on_publish_validated_change ) - controller.event_system.add_callback( + controller.register_event_callback( "publish.finished.changed", self._on_publish_finished_change ) - controller.event_system.add_callback( + controller.register_event_callback( "publish.process.stopped", self._on_publish_stop ) - controller.event_system.add_callback( + controller.register_event_callback( "show.card.message", self._on_overlay_message ) - controller.event_system.add_callback( + controller.register_event_callback( "instances.collection.failed", self._on_creator_error ) - controller.event_system.add_callback( + controller.register_event_callback( "instances.save.failed", self._on_creator_error ) - controller.event_system.add_callback( + controller.register_event_callback( "instances.remove.failed", self._on_creator_error ) - controller.event_system.add_callback( + controller.register_event_callback( "instances.create.failed", self._on_creator_error ) - controller.event_system.add_callback( + controller.register_event_callback( "convertors.convert.failed", self._on_convertor_error ) - controller.event_system.add_callback( + controller.register_event_callback( "convertors.find.failed", self._on_convertor_error ) - controller.event_system.add_callback( + controller.register_event_callback( "publish.action.failed", self._on_action_error ) - controller.event_system.add_callback( + controller.register_event_callback( "export_report.request", self._export_report ) - controller.event_system.add_callback( + controller.register_event_callback( "copy_report.request", self._copy_report ) @@ -453,14 +453,14 @@ def closeEvent(self, event): self._window_is_visible = False self._uninstall_app_event_listener() # TODO capture changes and ask user if wants to save changes on close - if not self._controller.host_context_has_changed: + if not self._controller.host_context_has_changed(): self._save_changes(False) self._comment_input.setText("") # clear comment self._reset_on_show = True self._controller.clear_thumbnail_temp_dir_path() # Trigger custom event that should be captured only in UI # - backend (controller) must not be dependent on this event topic!!! - self._controller.event_system.emit("main.window.closed", {}, "window") + self._controller.emit_event("main.window.closed", {}, "window") super(PublisherWindow, self).closeEvent(event) def leaveEvent(self, event): @@ -574,7 +574,7 @@ def _checks_before_save(self, explicit_save): bool: Save can happen. """ - if not self._controller.host_context_has_changed: + if not self._controller.host_context_has_changed(): return True title = "Host context changed" @@ -885,23 +885,23 @@ def _on_publish_stop(self): self._set_publish_overlay_visibility(False) self._reset_btn.setEnabled(True) self._stop_btn.setEnabled(False) - publish_has_crashed = self._controller.publish_has_crashed + publish_has_crashed = self._controller.publish_has_crashed() validate_enabled = not publish_has_crashed publish_enabled = not publish_has_crashed if self._is_on_publish_tab(): self._go_to_report_tab() if validate_enabled: - validate_enabled = not self._controller.publish_has_validated + validate_enabled = not self._controller.publish_has_validated() if publish_enabled: if ( - self._controller.publish_has_validated - and self._controller.publish_has_validation_errors + self._controller.publish_has_validated() + and self._controller.publish_has_validation_errors() ): publish_enabled = False else: - publish_enabled = not self._controller.publish_has_finished + publish_enabled = not self._controller.publish_has_finished() self._validate_btn.setEnabled(validate_enabled) self._publish_btn.setEnabled(publish_enabled) @@ -912,12 +912,12 @@ def _on_publish_stop(self): self._update_publish_details_widget() def _validate_create_instances(self): - if not self._controller.host_is_valid: + if not self._controller.is_host_valid(): self._set_footer_enabled(True) return all_valid = None - for instance in self._controller.instances.values(): + for instance in self._controller.get_instances(): if not instance["active"]: continue From 24f3fa28a815e275774f572b671265c511fbfc08 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 21 Jun 2024 17:53:13 +0200 Subject: [PATCH 07/15] separate create logic to create model --- client/ayon_core/tools/publisher/abstract.py | 14 - client/ayon_core/tools/publisher/control.py | 698 +++++------------- .../ayon_core/tools/publisher/control_qt.py | 368 +-------- .../tools/publisher/models/__init__.py | 4 +- .../tools/publisher/models/create.py | 556 +++++++++++++- .../tools/publisher/models/publish.py | 3 +- .../tools/publisher/widgets/create_widget.py | 4 +- .../tools/publisher/widgets/publish_frame.py | 18 +- client/ayon_core/tools/publisher/window.py | 20 +- 9 files changed, 740 insertions(+), 945 deletions(-) diff --git a/client/ayon_core/tools/publisher/abstract.py b/client/ayon_core/tools/publisher/abstract.py index c7a9396ae0..a04ad298ef 100644 --- a/client/ayon_core/tools/publisher/abstract.py +++ b/client/ayon_core/tools/publisher/abstract.py @@ -17,17 +17,6 @@ class AbstractPublisherController(ABC): access objects directly but by using wrappers that can be serialized. """ - @property - @abstractmethod - def log(self): - """Controller's logger object. - - Returns: - logging.Logger: Logger object that can be used for logging. - """ - - pass - @abstractmethod def emit_event(self, topic, data=None, source=None): """Use implemented event system to trigger event.""" @@ -247,7 +236,6 @@ def publish_is_running(self): pass - @property @abstractmethod def publish_has_validated(self): """Publish validation passed. @@ -258,7 +246,6 @@ def publish_has_validated(self): pass - @property @abstractmethod def publish_has_crashed(self): """Publishing crashed for any reason. @@ -269,7 +256,6 @@ def publish_has_crashed(self): pass - @property @abstractmethod def publish_has_validation_errors(self): """During validation happened at least one validation error. diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index d12dde92ba..a6fbe878b5 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -3,27 +3,21 @@ import tempfile import shutil from abc import abstractmethod -import re import ayon_api from ayon_core.lib.events import QueuedEventSystem -from ayon_core.lib.profiles_filtering import filter_profiles -from ayon_core.lib.attribute_definitions import UIDef + from ayon_core.pipeline import ( registered_host, get_process_id, ) from ayon_core.pipeline.create import CreateContext -from ayon_core.pipeline.create.context import ( - CreatorsOperationFailed, - ConvertorsOperationFailed, -) from ayon_core.tools.common_models import ProjectsModel, HierarchyModel from .models import ( - CreatorItem, PublishModel, + CreateModel, ) from .abstract import AbstractPublisherController, CardMessageTypes @@ -40,103 +34,6 @@ class BasePublisherController(AbstractPublisherController): All prepared implementation is based on calling super '__init__'. """ - def __init__(self): - self._log = None - self._event_system_obj = None - - self._publish_model = PublishModel(self) - - # Controller must '_collect_creator_items' to fill the value - self._creator_items = None - - @property - def log(self): - """Controller's logger object. - - Returns: - logging.Logger: Logger object that can be used for logging. - """ - - if self._log is None: - self._log = logging.getLogger(self.__class__.__name__) - return self._log - - # Events system - def emit_event(self, topic, data=None, source=None): - """Use implemented event system to trigger event.""" - - if data is None: - data = {} - self._event_system.emit(topic, data, source) - - def register_event_callback(self, topic, callback): - self._event_system.add_callback(topic, callback) - - def is_host_valid(self) -> bool: - return self._create_context.host_is_valid - - def publish_has_started(self): - return self._publish_model.has_started() - - def publish_has_finished(self): - return self._publish_model.has_finished() - - def publish_is_running(self): - return self._publish_model.is_running() - - def publish_has_validated(self): - return self._publish_model.has_validated() - - def publish_has_crashed(self): - return self._publish_model.is_crashed() - - def publish_has_validation_errors(self): - return self._publish_model.has_validation_errors() - - def publish_can_continue(self): - return self._publish_model.publish_can_continue() - - def get_publish_max_progress(self): - return self._publish_model.get_max_progress() - - def get_publish_progress(self): - return self._publish_model.get_progress() - - def get_publish_error_msg(self): - return self._publish_model.get_error_msg() - - def get_creator_items(self): - """Creators that can be shown in create dialog.""" - if self._creator_items is None: - self._creator_items = self._collect_creator_items() - return self._creator_items - - def get_creator_item_by_id(self, identifier): - items = self.get_creator_items() - return items.get(identifier) - - @abstractmethod - def _collect_creator_items(self): - """Receive CreatorItems to work with. - - Returns: - Dict[str, CreatorItem]: Creator items by their identifier. - """ - - pass - - def get_creator_icon(self, identifier): - """Function to receive icon for creator identifier. - - Args: - str: Creator's identifier for which should be icon returned. - """ - - creator_item = self.get_creator_item_by_id(identifier) - if creator_item is not None: - return creator_item.icon - return None - def get_thumbnail_temp_dir_path(self): """Return path to directory where thumbnails can be temporary stored. @@ -157,78 +54,94 @@ def clear_thumbnail_temp_dir_path(self): if os.path.exists(dirpath): shutil.rmtree(dirpath) - @property - def _event_system(self): - """Inner event system for publisher controller. - - Is used for communication with UI. Event system is autocreated. - - Known topics: - "show.detailed.help" - Detailed help requested (UI related). - "show.card.message" - Show card message request (UI related). - "instances.refresh.finished" - Instances are refreshed. - "plugins.refresh.finished" - Plugins refreshed. - "publish.reset.finished" - Reset finished. - "controller.reset.started" - Controller reset started. - "controller.reset.finished" - Controller reset finished. - "publish.process.started" - Publishing started. Can be started from - paused state. - "publish.process.stopped" - Publishing stopped/paused process. - "publish.process.plugin.changed" - Plugin state has changed. - "publish.process.instance.changed" - Instance state has changed. - "publish.has_validated.changed" - Attr 'publish_has_validated' - changed. - "publish.is_running.changed" - Attr 'publish_is_running' changed. - "publish.has_crashed.changed" - Attr 'publish_has_crashed' changed. - "publish.publish_error.changed" - Attr 'publish_error' - "publish.has_validation_errors.changed" - Attr - 'has_validation_errors' changed. - "publish.max_progress.changed" - Attr 'publish_max_progress' - changed. - "publish.progress.changed" - Attr 'publish_progress' changed. - "publish.finished.changed" - Attr 'publish_has_finished' changed. - - Returns: - EventSystem: Event system which can trigger callbacks for topics. - """ - - if self._event_system_obj is None: - self._event_system_obj = QueuedEventSystem() - return self._event_system_obj - - def _emit_event(self, topic, data=None): - self.emit_event(topic, data, "controller") - class PublisherController(BasePublisherController): """Middleware between UI, CreateContext and publish Context. Handle both creation and publishing parts. + Known topics: + "show.detailed.help" - Detailed help requested (UI related). + "show.card.message" - Show card message request (UI related). + "instances.refresh.finished" - Instances are refreshed. + "plugins.refresh.finished" - Plugins refreshed. + "publish.reset.finished" - Reset finished. + "controller.reset.started" - Controller reset started. + "controller.reset.finished" - Controller reset finished. + "publish.process.started" - Publishing started. Can be started from + paused state. + "publish.process.stopped" - Publishing stopped/paused process. + "publish.process.plugin.changed" - Plugin state has changed. + "publish.process.instance.changed" - Instance state has changed. + "publish.has_validated.changed" - Attr 'publish_has_validated' + changed. + "publish.is_running.changed" - Attr 'publish_is_running' changed. + "publish.has_crashed.changed" - Attr 'publish_has_crashed' changed. + "publish.publish_error.changed" - Attr 'publish_error' + "publish.has_validation_errors.changed" - Attr + 'has_validation_errors' changed. + "publish.max_progress.changed" - Attr 'publish_max_progress' + changed. + "publish.progress.changed" - Attr 'publish_progress' changed. + "publish.finished.changed" - Attr 'publish_has_finished' changed. + Args: headless (bool): Headless publishing. ATM not implemented or used. - """ + """ _log = None def __init__(self, headless=False): super().__init__() + self._log = None + self._event_system = self._create_event_system() + self._host = registered_host() self._headless = headless - self._create_context = CreateContext( - self._host, headless=headless, reset=False - ) - - # State flags to prevent executing method which is already in progress - self._resetting_plugins = False - self._resetting_instances = False + self._create_model = CreateModel(self) + self._publish_model = PublishModel(self) # Cacher of avalon documents self._projects_model = ProjectsModel(self) self._hierarchy_model = HierarchyModel(self) + @property + def log(self): + """Controller's logger object. + + Returns: + logging.Logger: Logger object that can be used for logging. + """ + + if self._log is None: + self._log = logging.getLogger(self.__class__.__name__) + return self._log + + def is_headless(self): + return self._headless + + def get_host(self): + return self._host + + def get_create_context(self): + return self._create_model.get_create_context() + + def is_host_valid(self) -> bool: + return self._create_model.is_host_valid() + + # Events system + def emit_event(self, topic, data=None, source=None): + """Use implemented event system to trigger event.""" + + if data is None: + data = {} + self._event_system.emit(topic, data, source) + + def register_event_callback(self, topic, callback): + self._event_system.add_callback(topic, callback) + def get_current_project_name(self): """Current project context defined by host. @@ -236,7 +149,7 @@ def get_current_project_name(self): str: Project name. """ - return self._create_context.get_current_project_name() + return self._create_model.get_current_project_name() def get_current_folder_path(self): """Current context folder path defined by host. @@ -245,7 +158,7 @@ def get_current_folder_path(self): Union[str, None]: Folder path or None if folder is not set. """ - return self._create_context.get_current_folder_path() + return self._create_model.get_current_folder_path() def get_current_task_name(self): """Current context task name defined by host. @@ -254,10 +167,26 @@ def get_current_task_name(self): Union[str, None]: Task name or None if task is not set. """ - return self._create_context.get_current_task_name() + return self._create_model.get_current_task_name() def host_context_has_changed(self): - return self._create_context.context_has_changed + return self._create_model.host_context_has_changed() + + def get_creator_items(self): + """Creators that can be shown in create dialog.""" + return self._create_model.get_creator_items() + + def get_creator_item_by_id(self, identifier): + return self._create_model.get_creator_item_by_id(identifier) + + def get_creator_icon(self, identifier): + """Function to receive icon for creator identifier. + + Args: + identifier (str): Creator's identifier for which should + be icon returned. + """ + return self._create_model.get_creator_icon(identifier) @property def instances(self): @@ -272,36 +201,16 @@ def instances(self): def get_instances(self): """Current instances in create context.""" - return list(self._create_context.instances_by_id.values()) + return self._create_model.get_instances() def get_instance_by_id(self, instance_id): - return self._create_context.instances_by_id.get(instance_id) + return self._create_model.get_instance_by_id(instance_id) def get_instances_by_id(self, instance_ids=None): - if instance_ids is None: - instance_ids = self._create_context.instances_by_id.keys() - return { - instance_id: self.get_instance_by_id(instance_id) - for instance_id in instance_ids - } + return self._create_model.get_instances_by_id(instance_ids) def get_convertor_items(self): - return self._create_context.convertor_items_by_id - - @property - def _creators(self): - """All creators loaded in create context.""" - - return self._create_context.creators - - def _get_current_project_settings(self): - """Current project settings. - - Returns: - dict - """ - - return self._create_context.get_current_project_settings() + return self._create_model.get_convertor_items() def get_folder_type_items(self, project_name, sender=None): return self._projects_model.get_folder_type_items( @@ -330,11 +239,23 @@ def get_folder_entity(self, project_name, folder_id): def get_task_entity(self, project_name, task_id): return self._hierarchy_model.get_task_entity(project_name, task_id) + def get_folder_item_by_path(self, project_name, folder_path): + return self._hierarchy_model.get_folder_item_by_path( + project_name, folder_path + ) + + def get_task_item_by_name( + self, project_name, folder_id, task_name, sender=None + ): + return self._hierarchy_model.get_task_item_by_name( + project_name, folder_id, task_name, sender + ) + # Publisher custom method def get_folder_id_from_path(self, folder_path): if not folder_path: return None - folder_item = self._hierarchy_model.get_folder_item_by_path( + folder_item = self.get_folder_item_by_path( self.get_current_project_name(), folder_path ) if folder_item: @@ -413,182 +334,24 @@ def reset(self): self._emit_event("controller.reset.started") - self._create_context.reset_preparation() - - # Reset current context - self._create_context.reset_current_context() - self._hierarchy_model.reset() - self._reset_plugins() # Publish part must be reset after plugins - self._publish_model.reset(self._create_context) - self._reset_instances() - - self._create_context.reset_finalization() + self._create_model.reset() + self._publish_model.reset() self._emit_event("controller.reset.finished") self.emit_card_message("Refreshed..") - def _reset_plugins(self): - """Reset to initial state.""" - if self._resetting_plugins: - return - - self._resetting_plugins = True - - self._create_context.reset_plugins() - # Reset creator items - self._creator_items = None - - self._resetting_plugins = False - - self._emit_event("plugins.refresh.finished") - - def _collect_creator_items(self): - # TODO add crashed initialization of create plugins to report - output = {} - allowed_creator_pattern = self._get_allowed_creators_pattern() - for identifier, creator in self._create_context.creators.items(): - try: - if self._is_label_allowed( - creator.label, allowed_creator_pattern - ): - output[identifier] = CreatorItem.from_creator(creator) - continue - self.log.debug(f"{creator.label} not allowed for context") - except Exception: - self.log.error( - "Failed to create creator item for '%s'", - identifier, - exc_info=True - ) - - return output - - def _get_allowed_creators_pattern(self): - """Provide regex pattern for configured creator labels in this context - - If no profile matches current context, it shows all creators. - Support usage of regular expressions for configured values. - Returns: - (re.Pattern)[optional]: None or regex compiled patterns - into single one ('Render|Image.*') - """ - - task_type = self._create_context.get_current_task_type() - project_settings = self._get_current_project_settings() - - filter_creator_profiles = ( - project_settings - ["core"] - ["tools"] - ["creator"] - ["filter_creator_profiles"] - ) - filtering_criteria = { - "task_names": self.get_current_task_name(), - "task_types": task_type, - "host_names": self._create_context.host_name - } - profile = filter_profiles( - filter_creator_profiles, - filtering_criteria, - logger=self.log - ) - - allowed_creator_pattern = None - if profile: - allowed_creator_labels = { - label - for label in profile["creator_labels"] - if label - } - self.log.debug(f"Only allowed `{allowed_creator_labels}` creators") - allowed_creator_pattern = ( - re.compile("|".join(allowed_creator_labels))) - return allowed_creator_pattern - - def _is_label_allowed(self, label, allowed_labels_regex): - """Implement regex support for allowed labels. - - Args: - label (str): Label of creator - shown in Publisher - allowed_labels_regex (re.Pattern): compiled regular expression - """ - if not allowed_labels_regex: - return True - return bool(allowed_labels_regex.match(label)) - - def _reset_instances(self): - """Reset create instances.""" - if self._resetting_instances: - return - - self._resetting_instances = True - - self._create_context.reset_context_data() - with self._create_context.bulk_instances_collection(): - try: - self._create_context.reset_instances() - except CreatorsOperationFailed as exc: - self._emit_event( - "instances.collection.failed", - { - "title": "Instance collection failed", - "failed_info": exc.failed_info - } - ) - - try: - self._create_context.find_convertor_items() - except ConvertorsOperationFailed as exc: - self._emit_event( - "convertors.find.failed", - { - "title": "Collection of unsupported product failed", - "failed_info": exc.failed_info - } - ) - - try: - self._create_context.execute_autocreators() - - except CreatorsOperationFailed as exc: - self._emit_event( - "instances.create.failed", - { - "title": "AutoCreation failed", - "failed_info": exc.failed_info - } - ) - - self._resetting_instances = False - - self._on_create_instance_change() - def get_thumbnail_paths_for_instances(self, instance_ids): - thumbnail_paths_by_instance_id = ( - self._create_context.thumbnail_paths_by_instance_id + return self._create_model.get_thumbnail_paths_for_instances( + instance_ids ) - return { - instance_id: thumbnail_paths_by_instance_id.get(instance_id) - for instance_id in instance_ids - } def set_thumbnail_paths_for_instances(self, thumbnail_path_mapping): - thumbnail_paths_by_instance_id = ( - self._create_context.thumbnail_paths_by_instance_id - ) - for instance_id, thumbnail_path in thumbnail_path_mapping.items(): - thumbnail_paths_by_instance_id[instance_id] = thumbnail_path - - self._emit_event( - "instance.thumbnail.changed", - { - "mapping": thumbnail_path_mapping - } + self._create_model.set_thumbnail_paths_for_instances( + thumbnail_path_mapping ) def emit_card_message( @@ -608,32 +371,11 @@ def get_creator_attribute_definitions(self, instances): Args: instances(List[CreatedInstance]): List of created instances for which should be attribute definitions returned. - """ - # NOTE it would be great if attrdefs would have hash method implemented - # so they could be used as keys in dictionary - output = [] - _attr_defs = {} - for instance in instances: - for attr_def in instance.creator_attribute_defs: - found_idx = None - for idx, _attr_def in _attr_defs.items(): - if attr_def == _attr_def: - found_idx = idx - break - - value = None - if attr_def.is_value_def: - value = instance.creator_attributes[attr_def.key] - if found_idx is None: - idx = len(output) - output.append((attr_def, [instance], [value])) - _attr_defs[idx] = attr_def - else: - item = output[found_idx] - item[1].append(instance) - item[2].append(value) - return output + """ + return self._create_model.get_creator_attribute_definitions( + instances + ) def get_publish_attribute_definitions(self, instances, include_context): """Collect publish attribute definitions for passed instances. @@ -642,52 +384,11 @@ def get_publish_attribute_definitions(self, instances, include_context): instances(list): List of created instances for which should be attribute definitions returned. include_context(bool): Add context specific attribute definitions. - """ - _tmp_items = [] - if include_context: - _tmp_items.append(self._create_context) - - for instance in instances: - _tmp_items.append(instance) - - all_defs_by_plugin_name = {} - all_plugin_values = {} - for item in _tmp_items: - for plugin_name, attr_val in item.publish_attributes.items(): - attr_defs = attr_val.attr_defs - if not attr_defs: - continue - - if plugin_name not in all_defs_by_plugin_name: - all_defs_by_plugin_name[plugin_name] = attr_val.attr_defs - - if plugin_name not in all_plugin_values: - all_plugin_values[plugin_name] = {} - - plugin_values = all_plugin_values[plugin_name] - - for attr_def in attr_defs: - if isinstance(attr_def, UIDef): - continue - if attr_def.key not in plugin_values: - plugin_values[attr_def.key] = [] - attr_values = plugin_values[attr_def.key] - - value = attr_val[attr_def.key] - attr_values.append((item, value)) - - output = [] - for plugin in self._create_context.plugins_with_defs: - plugin_name = plugin.__name__ - if plugin_name not in all_defs_by_plugin_name: - continue - output.append(( - plugin_name, - all_defs_by_plugin_name[plugin_name], - all_plugin_values - )) - return output + """ + return self._create_model.get_publish_attribute_definitions( + instances, include_context + ) def get_product_name( self, @@ -709,38 +410,12 @@ def get_product_name( name is updated. """ - creator = self._creators[creator_identifier] - - instance = None - if instance_id: - instance = self.get_instance_by_id(instance_id) - - project_name = self.get_current_project_name() - folder_item = self._hierarchy_model.get_folder_item_by_path( - project_name, folder_path - ) - folder_entity = None - task_item = None - task_entity = None - if folder_item is not None: - folder_entity = self._hierarchy_model.get_folder_entity( - project_name, folder_item.entity_id - ) - task_item = self._hierarchy_model.get_task_item_by_name( - project_name, folder_item.entity_id, task_name, "controller" - ) - - if task_item is not None: - task_entity = self._hierarchy_model.get_task_entity( - project_name, task_item.task_id - ) - - return creator.get_product_name( - project_name, - folder_entity, - task_entity, + return self._create_model.get_product_name( + creator_identifier, variant, - instance=instance + task_name, + folder_path, + instance_id=None ) def trigger_convertor_items(self, convertor_identifiers): @@ -754,24 +429,7 @@ def trigger_convertor_items(self, convertor_identifiers): plugins. """ - success = True - try: - self._create_context.run_convertors(convertor_identifiers) - - except ConvertorsOperationFailed as exc: - success = False - self._emit_event( - "convertors.convert.failed", - { - "title": "Conversion failed", - "failed_info": exc.failed_info - } - ) - - if success: - self.emit_card_message("Conversion finished") - else: - self.emit_card_message("Conversion failed", CardMessageTypes.error) + self._create_model.trigger_convertor_items(convertor_identifiers) self.reset() @@ -780,24 +438,9 @@ def create( ): """Trigger creation and refresh of instances in UI.""" - success = True - try: - self._create_context.create_with_unified_error( - creator_identifier, product_name, instance_data, options - ) - - except CreatorsOperationFailed as exc: - success = False - self._emit_event( - "instances.create.failed", - { - "title": "Creation failed", - "failed_info": exc.failed_info - } - ) - - self._on_create_instance_change() - return success + return self._create_model.create( + creator_identifier, product_name, instance_data, options + ) def save_changes(self, show_message=True): """Save changes happened during creation. @@ -813,63 +456,48 @@ def save_changes(self, show_message=True): Returns: bool: Save of changes was successful. - """ - - if not self._create_context.host_is_valid: - # TODO remove - # Fake success save when host is not valid for CreateContext - # this is for testing as experimental feature - return True - try: - self._create_context.save_changes() - if show_message: - self.emit_card_message("Saved changes..") - return True - - except CreatorsOperationFailed as exc: - self._emit_event( - "instances.save.failed", - { - "title": "Instances save failed", - "failed_info": exc.failed_info - } - ) - - return False + """ + return self._create_model.save_changes(show_message) def remove_instances(self, instance_ids): """Remove instances based on instance ids. Args: instance_ids (List[str]): List of instance ids to remove. + """ + self._create_model.remove_instances(instance_ids) - # QUESTION Expect that instances are really removed? In that case reset - # is not required. - self._remove_instances_from_context(instance_ids) - - self._on_create_instance_change() - - def _remove_instances_from_context(self, instance_ids): - instances_by_id = self._create_context.instances_by_id - instances = [ - instances_by_id[instance_id] - for instance_id in instance_ids - ] - try: - self._create_context.remove_instances(instances) - except CreatorsOperationFailed as exc: - self._emit_event( - "instances.remove.failed", - { - "title": "Instance removement failed", - "failed_info": exc.failed_info - } - ) + def publish_has_started(self): + return self._publish_model.has_started() + + def publish_has_finished(self): + return self._publish_model.has_finished() + + def publish_is_running(self): + return self._publish_model.is_running() + + def publish_has_validated(self): + return self._publish_model.has_validated() + + def publish_has_crashed(self): + return self._publish_model.is_crashed() - def _on_create_instance_change(self): - self._emit_event("instances.refresh.finished") + def publish_has_validation_errors(self): + return self._publish_model.has_validation_errors() + + def publish_can_continue(self): + return self._publish_model.publish_can_continue() + + def get_publish_max_progress(self): + return self._publish_model.get_max_progress() + + def get_publish_progress(self): + return self._publish_model.get_progress() + + def get_publish_error_msg(self): + return self._publish_model.get_error_msg() def get_publish_report(self): return self._publish_model.get_publish_report() @@ -910,6 +538,12 @@ def stop_publish(self): def run_action(self, plugin_id, action_id): self._publish_model.run_action(plugin_id, action_id) + def _create_event_system(self): + return QueuedEventSystem() + + def _emit_event(self, topic, data=None): + self.emit_event(topic, data, "controller") + def _start_publish(self, up_validation): self._publish_model.set_publish_up_validation(up_validation) self._publish_model.start_publish(wait=True) diff --git a/client/ayon_core/tools/publisher/control_qt.py b/client/ayon_core/tools/publisher/control_qt.py index 223fae4775..b42b9afea3 100644 --- a/client/ayon_core/tools/publisher/control_qt.py +++ b/client/ayon_core/tools/publisher/control_qt.py @@ -1,15 +1,8 @@ import collections -from abc import abstractmethod, abstractproperty from qtpy import QtCore -from ayon_core.lib.events import Event -from ayon_core.pipeline.create import CreatedInstance - -from .control import ( - PublisherController, - BasePublisherController, -) +from .control import PublisherController class MainThreadItem: @@ -38,7 +31,7 @@ class MainThreadProcess(QtCore.QObject): count_timeout = 2 def __init__(self): - super(MainThreadProcess, self).__init__() + super().__init__() self._items_to_process = collections.deque() timer = QtCore.QTimer() @@ -47,7 +40,6 @@ def __init__(self): timer.timeout.connect(self._execute) self._timer = timer - self._switch_counter = self.count_timeout def add_item(self, item): self._items_to_process.append(item) @@ -56,12 +48,6 @@ def _execute(self): if not self._items_to_process: return - if self._switch_counter > 0: - self._switch_counter -= 1 - return - - self._switch_counter = self.count_timeout - item = self._items_to_process.popleft() item.process() @@ -83,7 +69,7 @@ class QtPublisherController(PublisherController): def __init__(self, *args, **kwargs): self._main_thread_processor = MainThreadProcess() - super(QtPublisherController, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.register_event_callback( "publish.process.started", self._qt_on_publish_start @@ -92,9 +78,9 @@ def __init__(self, *args, **kwargs): "publish.process.stopped", self._qt_on_publish_stop ) - def _reset_publish(self): - super()._reset_publish() + def reset(self): self._main_thread_processor.clear() + super().reset() def _start_publish(self, up_validation): self._publish_model.set_publish_up_validation(up_validation) @@ -120,347 +106,3 @@ def _qt_on_publish_start(self): def _qt_on_publish_stop(self): self._main_thread_processor.stop() - - -class QtRemotePublishController(BasePublisherController): - """Abstract Remote controller for Qt UI. - - This controller should be used in process where UI is running and should - listen and ask for data on a client side. - - All objects that are used during UI processing should be able to convert - on client side to json serializable data and then recreated here. Keep in - mind that all changes made here should be send back to client controller - before critical actions. - - ATM Was not tested and will require some changes. All code written here is - based on theoretical idea how it could work. - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self._created_instances = {} - self._thumbnail_paths_by_instance_id = None - - def _reset_attributes(self): - super()._reset_attributes() - self._thumbnail_paths_by_instance_id = None - - @abstractmethod - def _get_serialized_instances(self): - """Receive serialized instances from client process. - - Returns: - List[Dict[str, Any]]: Serialized instances. - """ - - pass - - def _on_create_instance_change(self): - serialized_instances = self._get_serialized_instances() - - created_instances = {} - for serialized_data in serialized_instances: - item = CreatedInstance.deserialize_on_remote(serialized_data) - created_instances[item.id] = item - - self._created_instances = created_instances - self._emit_event("instances.refresh.finished") - - def remote_events_handler(self, event_data): - event = Event.from_data(event_data) - - # Topics that cause "replication" of controller changes - if event.topic == "publish.max_progress.changed": - self.publish_max_progress = event["value"] - return - - if event.topic == "publish.progress.changed": - self.publish_progress = event["value"] - return - - if event.topic == "publish.has_validated.changed": - self.publish_has_validated = event["value"] - return - - if event.topic == "publish.is_running.changed": - self.publish_is_running = event["value"] - return - - if event.topic == "publish.publish_error.changed": - self.publish_error_msg = event["value"] - return - - if event.topic == "publish.has_crashed.changed": - self.publish_has_crashed = event["value"] - return - - if event.topic == "publish.has_validation_errors.changed": - self.publish_has_validation_errors = event["value"] - return - - if event.topic == "publish.finished.changed": - self.publish_has_finished = event["value"] - return - - if event.topic == "publish.host_is_valid.changed": - self.host_is_valid = event["value"] - return - - # Don't skip because UI want know about it too - if event.topic == "instance.thumbnail.changed": - for instance_id, path in event["mapping"].items(): - self.thumbnail_paths_by_instance_id[instance_id] = path - - # Topics that can be just passed by because are not affecting - # controller itself - # - "show.card.message" - # - "show.detailed.help" - # - "publish.reset.finished" - # - "instances.refresh.finished" - # - "plugins.refresh.finished" - # - "controller.reset.finished" - # - "publish.process.started" - # - "publish.process.stopped" - # - "publish.process.plugin.changed" - # - "publish.process.instance.changed" - self.event_system.emit_event(event) - - @abstractproperty - def project_name(self): - """Current context project name from client. - - Returns: - str: Name of project. - """ - - pass - - @abstractmethod - def get_current_folder_path(self): - """Current context folder path from host. - - Returns: - Union[str, None]: Folder path. - - """ - pass - - @abstractmethod - def get_current_task_name(self): - """Current context task name from client. - - Returns: - Union[str, None]: Name of task. - """ - - pass - - @property - def instances(self): - """Collected/created instances. - - Returns: - List[CreatedInstance]: List of created instances. - """ - - return self._created_instances - - def get_context_title(self): - """Get context title for artist shown at the top of main window. - - Returns: - Union[str, None]: Context title for window or None. In case of None - a warning is displayed (not nice for artists). - """ - - pass - - def get_existing_product_names(self, folder_path): - pass - - @property - def thumbnail_paths_by_instance_id(self): - if self._thumbnail_paths_by_instance_id is None: - self._thumbnail_paths_by_instance_id = ( - self._collect_thumbnail_paths_by_instance_id() - ) - return self._thumbnail_paths_by_instance_id - - def get_thumbnail_path_for_instance(self, instance_id): - return self.thumbnail_paths_by_instance_id.get(instance_id) - - def set_thumbnail_path_for_instance(self, instance_id, thumbnail_path): - self._set_thumbnail_path_on_context(self, instance_id, thumbnail_path) - - @abstractmethod - def _collect_thumbnail_paths_by_instance_id(self): - """Collect thumbnail paths by instance id in remote controller. - - These should be collected from 'CreatedContext' there. - - Returns: - Dict[str, str]: Mapping of thumbnail path by instance id. - """ - - pass - - @abstractmethod - def _set_thumbnail_path_on_context(self, instance_id, thumbnail_path): - """Send change of thumbnail path in remote controller. - - That should trigger event 'instance.thumbnail.changed' which is - captured and handled in default implementation in this class. - """ - - pass - - @abstractmethod - def get_product_name( - self, - creator_identifier, - variant, - task_name, - folder_path, - instance_id=None - ): - """Get product name based on passed data. - - Args: - creator_identifier (str): Identifier of creator which should be - responsible for product name creation. - variant (str): Variant value from user's input. - task_name (str): Name of task for which is instance created. - folder_path (str): Folder path for which is instance created. - instance_id (Union[str, None]): Existing instance id when product - name is updated. - """ - - pass - - @abstractmethod - def create( - self, creator_identifier, product_name, instance_data, options - ): - """Trigger creation by creator identifier. - - Should also trigger refresh of instanes. - - Args: - creator_identifier (str): Identifier of Creator plugin. - product_name (str): Calculated product name. - instance_data (Dict[str, Any]): Base instance data with variant, - folder path and task name. - options (Dict[str, Any]): Data from pre-create attributes. - """ - - pass - - def _get_instance_changes_for_client(self): - """Preimplemented method to receive instance changes for client.""" - - created_instance_changes = {} - for instance_id, instance in self._created_instances.items(): - created_instance_changes[instance_id] = ( - instance.remote_changes() - ) - return created_instance_changes - - @abstractmethod - def _send_instance_changes_to_client(self): - # TODO Implement to send 'instance_changes' value to client - # instance_changes = self._get_instance_changes_for_client() - pass - - @abstractmethod - def save_changes(self): - """Save changes happened during creation.""" - - self._send_instance_changes_to_client() - - @abstractmethod - def remove_instances(self, instance_ids): - """Remove list of instances from create context.""" - # TODO add Args: - - pass - - @abstractmethod - def get_publish_report(self): - pass - - @abstractmethod - def get_validation_errors(self): - pass - - @abstractmethod - def reset(self): - """Reset whole controller. - - This should reset create context, publish context and all variables - that are related to it. - """ - - self._send_instance_changes_to_client() - pass - - @abstractmethod - def publish(self): - """Trigger publishing without any order limitations.""" - - self._send_instance_changes_to_client() - pass - - @abstractmethod - def validate(self): - """Trigger publishing which will stop after validation order.""" - - self._send_instance_changes_to_client() - pass - - @abstractmethod - def stop_publish(self): - """Stop publishing can be also used to pause publishing. - - Pause of publishing is possible only if all plugins successfully - finished. - """ - - pass - - @abstractmethod - def run_action(self, plugin_id, action_id): - """Trigger pyblish action on a plugin. - - Args: - plugin_id (str): Id of publish plugin. - action_id (str): Id of publish action. - """ - - pass - - @abstractmethod - def set_comment(self, comment): - """Set comment on pyblish context. - - Set "comment" key on current pyblish.api.Context data. - - Args: - comment (str): Artist's comment. - """ - - pass - - @abstractmethod - def emit_card_message(self, message): - """Emit a card message which can have a lifetime. - - This is for UI purposes. Method can be extended to more arguments - in future e.g. different message timeout or type (color). - - Args: - message (str): Message that will be showed. - """ - - pass diff --git a/client/ayon_core/tools/publisher/models/__init__.py b/client/ayon_core/tools/publisher/models/__init__.py index 156141969c..194ea944ef 100644 --- a/client/ayon_core/tools/publisher/models/__init__.py +++ b/client/ayon_core/tools/publisher/models/__init__.py @@ -1,9 +1,9 @@ -from .create import CreatorItem +from .create import CreateModel from .publish import PublishModel __all__ = ( - "CreatorItem", + "CreateModel", "PublishModel", ) diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 5c4c75a0ca..9fcfeb2802 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -1,12 +1,23 @@ +import logging +import re + from ayon_core.lib.attribute_definitions import ( serialize_attr_defs, deserialize_attr_defs, ) +from ayon_core.lib.profiles_filtering import filter_profiles +from ayon_core.lib.attribute_definitions import UIDef from ayon_core.pipeline.create import ( AutoCreator, HiddenCreator, Creator, + CreateContext, +) +from ayon_core.pipeline.create.context import ( + CreatorsOperationFailed, + ConvertorsOperationFailed, ) +from ..abstract import CardMessageTypes class CreatorType: @@ -164,4 +175,547 @@ def from_data(cls, data): ) data["creator_type"] = CreatorTypes.from_str(data["creator_type"]) - return cls(**data) \ No newline at end of file + return cls(**data) + + +class CreateModel: + def __init__(self, controller): + self._log = None + self._controller = controller + + self._create_context = CreateContext( + controller.get_host(), + headless=controller.is_headless(), + reset=False + ) + # State flags to prevent executing method which is already in progress + self._creator_items = None + + @property + def log(self): + if self._log is None: + self._log = logging.getLogger(self.__class__.__name__) + return self._log + + def is_host_valid(self): + return self._create_context.host_is_valid + + def get_create_context(self): + return self._create_context + + def get_current_project_name(self): + """Current project context defined by host. + + Returns: + str: Project name. + + """ + return self._create_context.get_current_project_name() + + def get_current_folder_path(self): + """Current context folder path defined by host. + + Returns: + Union[str, None]: Folder path or None if folder is not set. + """ + + return self._create_context.get_current_folder_path() + + def get_current_task_name(self): + """Current context task name defined by host. + + Returns: + Union[str, None]: Task name or None if task is not set. + """ + + return self._create_context.get_current_task_name() + + def host_context_has_changed(self): + return self._create_context.context_has_changed + + def reset(self): + self._creator_items = None + + self._create_context.reset_preparation() + + # Reset current context + self._create_context.reset_current_context() + + self._reset_plugins() + self._reset_instances() + self._create_context.reset_finalization() + + def get_creator_items(self): + """Creators that can be shown in create dialog.""" + if self._creator_items is None: + self._creator_items = self._collect_creator_items() + return self._creator_items + + def get_creator_item_by_id(self, identifier): + items = self.get_creator_items() + return items.get(identifier) + + def get_creator_icon(self, identifier): + """Function to receive icon for creator identifier. + + Args: + str: Creator's identifier for which should be icon returned. + """ + + creator_item = self.get_creator_item_by_id(identifier) + if creator_item is not None: + return creator_item.icon + return None + + def get_instances(self): + """Current instances in create context.""" + return list(self._create_context.instances_by_id.values()) + + def get_instance_by_id(self, instance_id): + return self._create_context.instances_by_id.get(instance_id) + + def get_instances_by_id(self, instance_ids=None): + if instance_ids is None: + instance_ids = self._create_context.instances_by_id.keys() + return { + instance_id: self.get_instance_by_id(instance_id) + for instance_id in instance_ids + } + + def get_convertor_items(self): + return self._create_context.convertor_items_by_id + + def get_product_name( + self, + creator_identifier, + variant, + task_name, + folder_path, + instance_id=None + ): + """Get product name based on passed data. + + Args: + creator_identifier (str): Identifier of creator which should be + responsible for product name creation. + variant (str): Variant value from user's input. + task_name (str): Name of task for which is instance created. + folder_path (str): Folder path for which is instance created. + instance_id (Union[str, None]): Existing instance id when product + name is updated. + """ + + creator = self._creators[creator_identifier] + + instance = None + if instance_id: + instance = self.get_instance_by_id(instance_id) + + project_name = self._controller.get_current_project_name() + folder_item = self._controller.get_folder_item_by_path( + project_name, folder_path + ) + folder_entity = None + task_item = None + task_entity = None + if folder_item is not None: + folder_entity = self._controller.get_folder_entity( + project_name, folder_item.entity_id + ) + task_item = self._controller.get_task_item_by_name( + project_name, folder_item.entity_id, task_name, "controller" + ) + + if task_item is not None: + task_entity = self._controller.get_task_entity( + project_name, task_item.task_id + ) + + return creator.get_product_name( + project_name, + folder_entity, + task_entity, + variant, + instance=instance + ) + + def create( + self, creator_identifier, product_name, instance_data, options + ): + """Trigger creation and refresh of instances in UI.""" + + success = True + try: + self._create_context.create_with_unified_error( + creator_identifier, product_name, instance_data, options + ) + + except CreatorsOperationFailed as exc: + success = False + self._emit_event( + "instances.create.failed", + { + "title": "Creation failed", + "failed_info": exc.failed_info + } + ) + + self._on_create_instance_change() + return success + + def trigger_convertor_items(self, convertor_identifiers): + """Trigger legacy item convertors. + + This functionality requires to save and reset CreateContext. The reset + is needed so Creators can collect converted items. + + Args: + convertor_identifiers (list[str]): Identifiers of convertor + plugins. + """ + + success = True + try: + self._create_context.run_convertors(convertor_identifiers) + + except ConvertorsOperationFailed as exc: + success = False + self._emit_event( + "convertors.convert.failed", + { + "title": "Conversion failed", + "failed_info": exc.failed_info + } + ) + + if success: + self._controller.emit_card_message( + "Conversion finished" + ) + else: + self._controller.emit_card_message( + "Conversion failed", + CardMessageTypes.error + ) + + def save_changes(self, show_message=True): + """Save changes happened during creation. + + Trigger save of changes using host api. This functionality does not + validate anything. It is required to do checks before this method is + called to be able to give user actionable response e.g. check of + context using 'host_context_has_changed'. + + Args: + show_message (bool): Show message that changes were + saved successfully. + + Returns: + bool: Save of changes was successful. + """ + + if not self._create_context.host_is_valid: + # TODO remove + # Fake success save when host is not valid for CreateContext + # this is for testing as experimental feature + return True + + try: + self._create_context.save_changes() + if show_message: + self._controller.emit_card_message("Saved changes..") + return True + + except CreatorsOperationFailed as exc: + self._emit_event( + "instances.save.failed", + { + "title": "Instances save failed", + "failed_info": exc.failed_info + } + ) + + return False + + def remove_instances(self, instance_ids): + """Remove instances based on instance ids. + + Args: + instance_ids (List[str]): List of instance ids to remove. + """ + + # QUESTION Expect that instances are really removed? In that case reset + # is not required. + self._remove_instances_from_context(instance_ids) + + self._on_create_instance_change() + + def get_creator_attribute_definitions(self, instances): + """Collect creator attribute definitions for multuple instances. + + Args: + instances(List[CreatedInstance]): List of created instances for + which should be attribute definitions returned. + """ + + # NOTE it would be great if attrdefs would have hash method implemented + # so they could be used as keys in dictionary + output = [] + _attr_defs = {} + for instance in instances: + for attr_def in instance.creator_attribute_defs: + found_idx = None + for idx, _attr_def in _attr_defs.items(): + if attr_def == _attr_def: + found_idx = idx + break + + value = None + if attr_def.is_value_def: + value = instance.creator_attributes[attr_def.key] + if found_idx is None: + idx = len(output) + output.append((attr_def, [instance], [value])) + _attr_defs[idx] = attr_def + else: + item = output[found_idx] + item[1].append(instance) + item[2].append(value) + return output + + def get_publish_attribute_definitions(self, instances, include_context): + """Collect publish attribute definitions for passed instances. + + Args: + instances(list): List of created instances for + which should be attribute definitions returned. + include_context(bool): Add context specific attribute definitions. + """ + + _tmp_items = [] + if include_context: + _tmp_items.append(self._create_context) + + for instance in instances: + _tmp_items.append(instance) + + all_defs_by_plugin_name = {} + all_plugin_values = {} + for item in _tmp_items: + for plugin_name, attr_val in item.publish_attributes.items(): + attr_defs = attr_val.attr_defs + if not attr_defs: + continue + + if plugin_name not in all_defs_by_plugin_name: + all_defs_by_plugin_name[plugin_name] = attr_val.attr_defs + + if plugin_name not in all_plugin_values: + all_plugin_values[plugin_name] = {} + + plugin_values = all_plugin_values[plugin_name] + + for attr_def in attr_defs: + if isinstance(attr_def, UIDef): + continue + if attr_def.key not in plugin_values: + plugin_values[attr_def.key] = [] + attr_values = plugin_values[attr_def.key] + + value = attr_val[attr_def.key] + attr_values.append((item, value)) + + output = [] + for plugin in self._create_context.plugins_with_defs: + plugin_name = plugin.__name__ + if plugin_name not in all_defs_by_plugin_name: + continue + output.append(( + plugin_name, + all_defs_by_plugin_name[plugin_name], + all_plugin_values + )) + return output + + def get_thumbnail_paths_for_instances(self, instance_ids): + thumbnail_paths_by_instance_id = ( + self._create_context.thumbnail_paths_by_instance_id + ) + return { + instance_id: thumbnail_paths_by_instance_id.get(instance_id) + for instance_id in instance_ids + } + + def set_thumbnail_paths_for_instances(self, thumbnail_path_mapping): + thumbnail_paths_by_instance_id = ( + self._create_context.thumbnail_paths_by_instance_id + ) + for instance_id, thumbnail_path in thumbnail_path_mapping.items(): + thumbnail_paths_by_instance_id[instance_id] = thumbnail_path + + self._emit_event( + "instance.thumbnail.changed", + { + "mapping": thumbnail_path_mapping + } + ) + + def _emit_event(self, topic, data=None): + self._controller.emit_event(topic, data) + + def _get_current_project_settings(self): + """Current project settings. + + Returns: + dict + """ + + return self._create_context.get_current_project_settings() + + @property + def _creators(self): + """All creators loaded in create context.""" + + return self._create_context.creators + + def _reset_plugins(self): + """Reset to initial state.""" + self._create_context.reset_plugins() + # Reset creator items + self._creator_items = None + + def _reset_instances(self): + """Reset create instances.""" + + self._create_context.reset_context_data() + with self._create_context.bulk_instances_collection(): + try: + self._create_context.reset_instances() + except CreatorsOperationFailed as exc: + self._emit_event( + "instances.collection.failed", + { + "title": "Instance collection failed", + "failed_info": exc.failed_info + } + ) + + try: + self._create_context.find_convertor_items() + except ConvertorsOperationFailed as exc: + self._emit_event( + "convertors.find.failed", + { + "title": "Collection of unsupported product failed", + "failed_info": exc.failed_info + } + ) + + try: + self._create_context.execute_autocreators() + + except CreatorsOperationFailed as exc: + self._emit_event( + "instances.create.failed", + { + "title": "AutoCreation failed", + "failed_info": exc.failed_info + } + ) + + self._on_create_instance_change() + + def _remove_instances_from_context(self, instance_ids): + instances_by_id = self._create_context.instances_by_id + instances = [ + instances_by_id[instance_id] + for instance_id in instance_ids + ] + try: + self._create_context.remove_instances(instances) + except CreatorsOperationFailed as exc: + self._emit_event( + "instances.remove.failed", + { + "title": "Instance removement failed", + "failed_info": exc.failed_info + } + ) + + def _on_create_instance_change(self): + self._emit_event("instances.refresh.finished") + + def _collect_creator_items(self): + # TODO add crashed initialization of create plugins to report + output = {} + allowed_creator_pattern = self._get_allowed_creators_pattern() + for identifier, creator in self._create_context.creators.items(): + try: + if self._is_label_allowed( + creator.label, allowed_creator_pattern + ): + output[identifier] = CreatorItem.from_creator(creator) + continue + self.log.debug(f"{creator.label} not allowed for context") + except Exception: + self.log.error( + "Failed to create creator item for '%s'", + identifier, + exc_info=True + ) + + return output + + def _get_allowed_creators_pattern(self): + """Provide regex pattern for configured creator labels in this context + + If no profile matches current context, it shows all creators. + Support usage of regular expressions for configured values. + Returns: + (re.Pattern)[optional]: None or regex compiled patterns + into single one ('Render|Image.*') + """ + + task_type = self._create_context.get_current_task_type() + project_settings = self._get_current_project_settings() + + filter_creator_profiles = ( + project_settings + ["core"] + ["tools"] + ["creator"] + ["filter_creator_profiles"] + ) + filtering_criteria = { + "task_names": self.get_current_task_name(), + "task_types": task_type, + "host_names": self._create_context.host_name + } + profile = filter_profiles( + filter_creator_profiles, + filtering_criteria, + logger=self.log + ) + + allowed_creator_pattern = None + if profile: + allowed_creator_labels = { + label + for label in profile["creator_labels"] + if label + } + self.log.debug(f"Only allowed `{allowed_creator_labels}` creators") + allowed_creator_pattern = ( + re.compile("|".join(allowed_creator_labels))) + return allowed_creator_pattern + + def _is_label_allowed(self, label, allowed_labels_regex): + """Implement regex support for allowed labels. + + Args: + label (str): Label of creator - shown in Publisher + allowed_labels_regex (re.Pattern): compiled regular expression + """ + if not allowed_labels_regex: + return True + return bool(allowed_labels_regex.match(label)) diff --git a/client/ayon_core/tools/publisher/models/publish.py b/client/ayon_core/tools/publisher/models/publish.py index c1d1dc1031..7b8169fa5c 100644 --- a/client/ayon_core/tools/publisher/models/publish.py +++ b/client/ayon_core/tools/publisher/models/publish.py @@ -768,7 +768,8 @@ def __init__(self, controller): # Plugin iterator self._main_thread_iter = None - def reset(self, create_context): + def reset(self): + create_context = self._controller.get_create_context() self._publish_up_validation = False self._publish_comment_is_set = False self._publish_has_started = False diff --git a/client/ayon_core/tools/publisher/widgets/create_widget.py b/client/ayon_core/tools/publisher/widgets/create_widget.py index fbab0cec84..72859a4ad2 100644 --- a/client/ayon_core/tools/publisher/widgets/create_widget.py +++ b/client/ayon_core/tools/publisher/widgets/create_widget.py @@ -278,7 +278,7 @@ def __init__(self, controller, parent=None): "main.window.closed", self._on_main_window_close ) controller.register_event_callback( - "plugins.refresh.finished", self._on_plugins_refresh + "controller.reset.finished", self._on_controler_reset ) self._main_splitter_widget = main_splitter_widget @@ -529,7 +529,7 @@ def _refresh_creators(self): self._set_creator(create_item) - def _on_plugins_refresh(self): + def _on_controler_reset(self): # Trigger refresh only if is visible self.refresh() diff --git a/client/ayon_core/tools/publisher/widgets/publish_frame.py b/client/ayon_core/tools/publisher/widgets/publish_frame.py index 12f7be8109..264b0a2e02 100644 --- a/client/ayon_core/tools/publisher/widgets/publish_frame.py +++ b/client/ayon_core/tools/publisher/widgets/publish_frame.py @@ -368,20 +368,10 @@ def _on_publish_stop(self): self._instance_label.setText("") self._plugin_label.setText("") - validate_enabled = not self._controller.publish_has_crashed - publish_enabled = not self._controller.publish_has_crashed - if validate_enabled: - validate_enabled = not self._controller.publish_has_validated - if publish_enabled: - if ( - self._controller.publish_has_validated - and self._controller.publish_has_validation_errors - ): - publish_enabled = False - - else: - publish_enabled = not self._controller.publish_has_finished() - + publish_enabled = self._controller.publish_can_continue() + validate_enabled = ( + publish_enabled and not self._controller.publish_has_validated() + ) self._validate_btn.setEnabled(validate_enabled) self._publish_btn.setEnabled(publish_enabled) diff --git a/client/ayon_core/tools/publisher/window.py b/client/ayon_core/tools/publisher/window.py index 090eebdd7a..14938b145f 100644 --- a/client/ayon_core/tools/publisher/window.py +++ b/client/ayon_core/tools/publisher/window.py @@ -831,7 +831,6 @@ def _on_publish_reset(self): self._set_comment_input_visiblity(True) self._set_publish_overlay_visibility(False) self._set_publish_visibility(False) - self._set_footer_enabled(False) self._update_publish_details_widget() def _on_controller_reset(self): @@ -885,24 +884,13 @@ def _on_publish_stop(self): self._set_publish_overlay_visibility(False) self._reset_btn.setEnabled(True) self._stop_btn.setEnabled(False) - publish_has_crashed = self._controller.publish_has_crashed() - validate_enabled = not publish_has_crashed - publish_enabled = not publish_has_crashed if self._is_on_publish_tab(): self._go_to_report_tab() - if validate_enabled: - validate_enabled = not self._controller.publish_has_validated() - if publish_enabled: - if ( - self._controller.publish_has_validated() - and self._controller.publish_has_validation_errors() - ): - publish_enabled = False - - else: - publish_enabled = not self._controller.publish_has_finished() - + publish_enabled = self._controller.publish_can_continue() + validate_enabled = ( + publish_enabled and not self._controller.publish_has_validated() + ) self._validate_btn.setEnabled(validate_enabled) self._publish_btn.setEnabled(publish_enabled) From 76071c4b87807ed37639199ff7a035cfcf125811 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 21 Jun 2024 18:22:01 +0200 Subject: [PATCH 08/15] added typehints to create model --- .../tools/publisher/models/create.py | 231 ++++++++++-------- 1 file changed, 133 insertions(+), 98 deletions(-) diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 9fcfeb2802..a80a782773 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -1,28 +1,37 @@ import logging import re +from typing import Union, List, Dict, Tuple, Any, Optional, Iterable, Pattern from ayon_core.lib.attribute_definitions import ( serialize_attr_defs, deserialize_attr_defs, + AbstractAttrDef, ) from ayon_core.lib.profiles_filtering import filter_profiles from ayon_core.lib.attribute_definitions import UIDef from ayon_core.pipeline.create import ( + BaseCreator, AutoCreator, HiddenCreator, Creator, CreateContext, + CreatedInstance, ) from ayon_core.pipeline.create.context import ( CreatorsOperationFailed, ConvertorsOperationFailed, + ConvertorItem, ) -from ..abstract import CardMessageTypes +from ayon_core.tools.publisher.abstract import ( + AbstractPublisherController, + CardMessageTypes, +) +CREATE_EVENT_SOURCE = "publisher.create.model" class CreatorType: - def __init__(self, name): - self.name = name + def __init__(self, name: str): + self.name: str = name def __str__(self): return self.name @@ -42,7 +51,7 @@ class CreatorTypes: artist = CreatorType("artist") @classmethod - def from_str(cls, value): + def from_str(cls, value: str) -> CreatorType: for creator_type in ( cls.base, cls.auto, @@ -62,49 +71,52 @@ class CreatorItem: def __init__( self, - identifier, - creator_type, - product_type, - label, - group_label, - icon, - description, - detailed_description, - default_variant, - default_variants, - create_allow_context_change, - create_allow_thumbnail, - show_order, - pre_create_attributes_defs, + identifier: str, + creator_type: CreatorType, + product_type: str, + label: str, + group_label: str, + icon: Union[str, Dict[str, Any], None], + description: Union[str, None], + detailed_description: Union[str, None], + default_variant: Union[str, None], + default_variants: Union[List[str], None], + create_allow_context_change: Union[bool, None], + create_allow_thumbnail: Union[bool, None], + show_order: int, + pre_create_attributes_defs: List[AbstractAttrDef], ): - self.identifier = identifier - self.creator_type = creator_type - self.product_type = product_type - self.label = label - self.group_label = group_label - self.icon = icon - self.description = description - self.detailed_description = detailed_description - self.default_variant = default_variant - self.default_variants = default_variants - self.create_allow_context_change = create_allow_context_change - self.create_allow_thumbnail = create_allow_thumbnail - self.show_order = show_order - self.pre_create_attributes_defs = pre_create_attributes_defs - - def get_group_label(self): + self.identifier: str = identifier + self.creator_type: CreatorType = creator_type + self.product_type: str = product_type + self.label: str = label + self.group_label: str = group_label + self.icon: Union[str, Dict[str, Any], None] = icon + self.description: Union[str, None] = description + self.detailed_description: Union[bool, None] = detailed_description + self.default_variant: Union[bool, None] = default_variant + self.default_variants: Union[List[str], None] = default_variants + self.create_allow_context_change: Union[bool, None] = ( + create_allow_context_change + ) + self.create_allow_thumbnail: Union[bool, None] = create_allow_thumbnail + self.show_order: int = show_order + self.pre_create_attributes_defs: List[AbstractAttrDef] = ( + pre_create_attributes_defs + ) + + def get_group_label(self) -> str: return self.group_label @classmethod - def from_creator(cls, creator): + def from_creator(cls, creator: BaseCreator): + creator_type: CreatorType = CreatorTypes.base if isinstance(creator, AutoCreator): creator_type = CreatorTypes.auto elif isinstance(creator, HiddenCreator): creator_type = CreatorTypes.hidden elif isinstance(creator, Creator): creator_type = CreatorTypes.artist - else: - creator_type = CreatorTypes.base description = None detail_description = None @@ -142,7 +154,7 @@ def from_creator(cls, creator): pre_create_attr_defs, ) - def to_data(self): + def to_data(self) -> Dict[str, Any]: pre_create_attributes_defs = None if self.pre_create_attributes_defs is not None: pre_create_attributes_defs = serialize_attr_defs( @@ -167,7 +179,7 @@ def to_data(self): } @classmethod - def from_data(cls, data): + def from_data(cls, data: Dict[str, Any]) -> "CreatorItem": pre_create_attributes_defs = data["pre_create_attributes_defs"] if pre_create_attributes_defs is not None: data["pre_create_attributes_defs"] = deserialize_attr_defs( @@ -179,7 +191,7 @@ def from_data(cls, data): class CreateModel: - def __init__(self, controller): + def __init__(self, controller: AbstractPublisherController): self._log = None self._controller = controller @@ -192,18 +204,18 @@ def __init__(self, controller): self._creator_items = None @property - def log(self): + def log(self) -> logging.Logger: if self._log is None: self._log = logging.getLogger(self.__class__.__name__) return self._log - def is_host_valid(self): + def is_host_valid(self) -> bool: return self._create_context.host_is_valid - def get_create_context(self): + def get_create_context(self) -> CreateContext: return self._create_context - def get_current_project_name(self): + def get_current_project_name(self) -> Union[str, None]: """Current project context defined by host. Returns: @@ -212,7 +224,7 @@ def get_current_project_name(self): """ return self._create_context.get_current_project_name() - def get_current_folder_path(self): + def get_current_folder_path(self) -> Union[str, None]: """Current context folder path defined by host. Returns: @@ -221,7 +233,7 @@ def get_current_folder_path(self): return self._create_context.get_current_folder_path() - def get_current_task_name(self): + def get_current_task_name(self) -> Union[str, None]: """Current context task name defined by host. Returns: @@ -230,51 +242,61 @@ def get_current_task_name(self): return self._create_context.get_current_task_name() - def host_context_has_changed(self): + def host_context_has_changed(self) -> bool: return self._create_context.context_has_changed def reset(self): - self._creator_items = None - self._create_context.reset_preparation() # Reset current context self._create_context.reset_current_context() - self._reset_plugins() + self._create_context.reset_plugins() + # Reset creator items + self._creator_items = None + self._reset_instances() self._create_context.reset_finalization() - def get_creator_items(self): + def get_creator_items(self) -> Dict[str, CreatorItem]: """Creators that can be shown in create dialog.""" if self._creator_items is None: self._creator_items = self._collect_creator_items() return self._creator_items - def get_creator_item_by_id(self, identifier): + def get_creator_item_by_id( + self, identifier: str + ) -> Union[CreatorItem, None]: items = self.get_creator_items() return items.get(identifier) - def get_creator_icon(self, identifier): + def get_creator_icon( + self, identifier: str + ) -> Union[str, Dict[str, Any], None]: """Function to receive icon for creator identifier. Args: - str: Creator's identifier for which should be icon returned. - """ + identifier (str): Creator's identifier for which should + be icon returned. + """ creator_item = self.get_creator_item_by_id(identifier) if creator_item is not None: return creator_item.icon return None - def get_instances(self): + def get_instances(self) -> List[CreatedInstance]: """Current instances in create context.""" return list(self._create_context.instances_by_id.values()) - def get_instance_by_id(self, instance_id): + def get_instance_by_id( + self, instance_id: str + ) -> Union[CreatedInstance, None]: return self._create_context.instances_by_id.get(instance_id) - def get_instances_by_id(self, instance_ids=None): + def get_instances_by_id( + self, instance_ids: Optional[Iterable[str]] = None + ) -> Dict[str, Union[CreatedInstance, None]]: if instance_ids is None: instance_ids = self._create_context.instances_by_id.keys() return { @@ -282,17 +304,17 @@ def get_instances_by_id(self, instance_ids=None): for instance_id in instance_ids } - def get_convertor_items(self): + def get_convertor_items(self) -> Dict[str, ConvertorItem]: return self._create_context.convertor_items_by_id def get_product_name( self, - creator_identifier, - variant, - task_name, - folder_path, - instance_id=None - ): + creator_identifier: str, + variant: str, + task_name: Union[str, None], + folder_path: Union[str, None], + instance_id: Optional[str] = None + ) -> str: """Get product name based on passed data. Args: @@ -323,7 +345,10 @@ def get_product_name( project_name, folder_item.entity_id ) task_item = self._controller.get_task_item_by_name( - project_name, folder_item.entity_id, task_name, "controller" + project_name, + folder_item.entity_id, + task_name, + CREATE_EVENT_SOURCE ) if task_item is not None: @@ -340,7 +365,11 @@ def get_product_name( ) def create( - self, creator_identifier, product_name, instance_data, options + self, + creator_identifier: str, + product_name: str, + instance_data: Dict[str, Any], + options: Dict[str, Any], ): """Trigger creation and refresh of instances in UI.""" @@ -363,7 +392,7 @@ def create( self._on_create_instance_change() return success - def trigger_convertor_items(self, convertor_identifiers): + def trigger_convertor_items(self, convertor_identifiers: List[str]): """Trigger legacy item convertors. This functionality requires to save and reset CreateContext. The reset @@ -398,7 +427,7 @@ def trigger_convertor_items(self, convertor_identifiers): CardMessageTypes.error ) - def save_changes(self, show_message=True): + def save_changes(self, show_message: Optional[bool] = True): """Save changes happened during creation. Trigger save of changes using host api. This functionality does not @@ -437,7 +466,7 @@ def save_changes(self, show_message=True): return False - def remove_instances(self, instance_ids): + def remove_instances(self, instance_ids: List[str]): """Remove instances based on instance ids. Args: @@ -450,11 +479,13 @@ def remove_instances(self, instance_ids): self._on_create_instance_change() - def get_creator_attribute_definitions(self, instances): + def get_creator_attribute_definitions( + self, instances: List[CreatedInstance] + ) -> List[Tuple[AbstractAttrDef, List[CreatedInstance], List[Any]]]: """Collect creator attribute definitions for multuple instances. Args: - instances(List[CreatedInstance]): List of created instances for + instances (List[CreatedInstance]): List of created instances for which should be attribute definitions returned. """ @@ -483,15 +514,21 @@ def get_creator_attribute_definitions(self, instances): item[2].append(value) return output - def get_publish_attribute_definitions(self, instances, include_context): + def get_publish_attribute_definitions( + self, instances: List[CreatedInstance], include_context: bool + ) -> List[Tuple[ + str, + List[AbstractAttrDef], + Dict[str, List[Tuple[CreatedInstance, Any]]] + ]]: """Collect publish attribute definitions for passed instances. Args: - instances(list): List of created instances for + instances (list[CreatedInstance]): List of created instances for which should be attribute definitions returned. - include_context(bool): Add context specific attribute definitions. - """ + include_context (bool): Add context specific attribute definitions. + """ _tmp_items = [] if include_context: _tmp_items.append(self._create_context) @@ -510,17 +547,13 @@ def get_publish_attribute_definitions(self, instances, include_context): if plugin_name not in all_defs_by_plugin_name: all_defs_by_plugin_name[plugin_name] = attr_val.attr_defs - if plugin_name not in all_plugin_values: - all_plugin_values[plugin_name] = {} - - plugin_values = all_plugin_values[plugin_name] + plugin_values = all_plugin_values.setdefault(plugin_name, {}) for attr_def in attr_defs: if isinstance(attr_def, UIDef): continue - if attr_def.key not in plugin_values: - plugin_values[attr_def.key] = [] - attr_values = plugin_values[attr_def.key] + + attr_values = plugin_values.setdefault(attr_def.key, []) value = attr_val[attr_def.key] attr_values.append((item, value)) @@ -537,7 +570,9 @@ def get_publish_attribute_definitions(self, instances, include_context): )) return output - def get_thumbnail_paths_for_instances(self, instance_ids): + def get_thumbnail_paths_for_instances( + self, instance_ids: List[str] + ) -> Dict[str, Union[str, None]]: thumbnail_paths_by_instance_id = ( self._create_context.thumbnail_paths_by_instance_id ) @@ -546,7 +581,9 @@ def get_thumbnail_paths_for_instances(self, instance_ids): for instance_id in instance_ids } - def set_thumbnail_paths_for_instances(self, thumbnail_path_mapping): + def set_thumbnail_paths_for_instances( + self, thumbnail_path_mapping: Dict[str, str] + ): thumbnail_paths_by_instance_id = ( self._create_context.thumbnail_paths_by_instance_id ) @@ -560,10 +597,10 @@ def set_thumbnail_paths_for_instances(self, thumbnail_path_mapping): } ) - def _emit_event(self, topic, data=None): + def _emit_event(self, topic: str, data: Optional[Dict[str, Any]] = None): self._controller.emit_event(topic, data) - def _get_current_project_settings(self): + def _get_current_project_settings(self) -> Dict[str, Any]: """Current project settings. Returns: @@ -573,17 +610,11 @@ def _get_current_project_settings(self): return self._create_context.get_current_project_settings() @property - def _creators(self): + def _creators(self) -> Dict[str, BaseCreator]: """All creators loaded in create context.""" return self._create_context.creators - def _reset_plugins(self): - """Reset to initial state.""" - self._create_context.reset_plugins() - # Reset creator items - self._creator_items = None - def _reset_instances(self): """Reset create instances.""" @@ -625,7 +656,7 @@ def _reset_instances(self): self._on_create_instance_change() - def _remove_instances_from_context(self, instance_ids): + def _remove_instances_from_context(self, instance_ids: List[str]): instances_by_id = self._create_context.instances_by_id instances = [ instances_by_id[instance_id] @@ -645,7 +676,7 @@ def _remove_instances_from_context(self, instance_ids): def _on_create_instance_change(self): self._emit_event("instances.refresh.finished") - def _collect_creator_items(self): + def _collect_creator_items(self) -> Dict[str, CreatorItem]: # TODO add crashed initialization of create plugins to report output = {} allowed_creator_pattern = self._get_allowed_creators_pattern() @@ -666,7 +697,7 @@ def _collect_creator_items(self): return output - def _get_allowed_creators_pattern(self): + def _get_allowed_creators_pattern(self) -> Union[Pattern, None]: """Provide regex pattern for configured creator labels in this context If no profile matches current context, it shows all creators. @@ -709,7 +740,11 @@ def _get_allowed_creators_pattern(self): re.compile("|".join(allowed_creator_labels))) return allowed_creator_pattern - def _is_label_allowed(self, label, allowed_labels_regex): + def _is_label_allowed( + self, + label: str, + allowed_labels_regex: Union[Pattern, None] + ) -> bool: """Implement regex support for allowed labels. Args: From fba4d1079a6c261a58c1d9d05dbf7dbc0f660bd2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 21 Jun 2024 19:05:47 +0200 Subject: [PATCH 09/15] added typehints to publish model --- .../tools/publisher/models/publish.py | 441 ++++++++++-------- 1 file changed, 258 insertions(+), 183 deletions(-) diff --git a/client/ayon_core/tools/publisher/models/publish.py b/client/ayon_core/tools/publisher/models/publish.py index 7b8169fa5c..27827be316 100644 --- a/client/ayon_core/tools/publisher/models/publish.py +++ b/client/ayon_core/tools/publisher/models/publish.py @@ -4,6 +4,7 @@ import traceback import collections from functools import partial +from typing import Optional, Dict, List, Union, Any, Iterable, Literal import arrow import pyblish.plugin @@ -13,12 +14,24 @@ KnownPublishError, OptionalPyblishPluginMixin, ) +from ayon_core.pipeline.plugin_discover import DiscoverResult from ayon_core.pipeline.publish import get_publish_instance_label +from ayon_core.tools.publisher.abstract import AbstractPublisherController PUBLISH_EVENT_SOURCE = "publisher.publish.model" # Define constant for plugin orders offset PLUGIN_ORDER_OFFSET = 0.5 +ActionFilterType = Literal[ + "all", + "notProcessed", + "processed", + "failed", + "warning", + "failedOrWarning", + "succeeded" +] + class PublishReportMaker: """Report for single publishing process. @@ -28,17 +41,17 @@ class PublishReportMaker: def __init__( self, - creator_discover_result=None, - convertor_discover_result=None, - publish_discover_result=None, + creator_discover_result: Optional[DiscoverResult] = None, + convertor_discover_result: Optional[DiscoverResult] = None, + publish_discover_result: Optional[DiscoverResult] = None, ): - self._create_discover_result = None - self._convert_discover_result = None - self._publish_discover_result = None + self._create_discover_result: Union[DiscoverResult, None] = None + self._convert_discover_result: Union[DiscoverResult, None] = None + self._publish_discover_result: Union[DiscoverResult, None] = None - self._all_instances_by_id = {} - self._plugin_data_by_id = {} - self._current_plugin_id = None + self._all_instances_by_id: Dict[str, pyblish.api.Instance] = {} + self._plugin_data_by_id: Dict[str, Any] = {} + self._current_plugin_id: Union[str, None] = None self.reset( creator_discover_result, @@ -48,9 +61,9 @@ def __init__( def reset( self, - creator_discover_result, - convertor_discover_result, - publish_discover_result, + creator_discover_result: Union[DiscoverResult, None], + convertor_discover_result: Union[DiscoverResult, None], + publish_discover_result: Union[DiscoverResult, None], ): """Reset report and clear all data.""" @@ -69,23 +82,23 @@ def reset( for plugin in publish_plugins: self._add_plugin_data_item(plugin) - def add_plugin_iter(self, plugin_id, context): + def add_plugin_iter(self, plugin_id: str, context: pyblish.api.Context): """Add report about single iteration of plugin.""" for instance in context: self._all_instances_by_id[instance.id] = instance self._current_plugin_id = plugin_id - def set_plugin_passed(self, plugin_id): + def set_plugin_passed(self, plugin_id: str): plugin_data = self._plugin_data_by_id[plugin_id] plugin_data["passed"] = True - def set_plugin_skipped(self, plugin_id): + def set_plugin_skipped(self, plugin_id: str): """Set that current plugin has been skipped.""" plugin_data = self._plugin_data_by_id[plugin_id] plugin_data["skipped"] = True - def add_result(self, plugin_id, result): + def add_result(self, plugin_id: str, result: Dict[str, Any]): """Handle result of one plugin and it's instance.""" instance = result["instance"] @@ -99,7 +112,9 @@ def add_result(self, plugin_id, result): "process_time": result["duration"] }) - def add_action_result(self, action, result): + def add_action_result( + self, action: pyblish.api.Action, result: Dict[str, Any] + ): """Add result of single action.""" plugin = result["plugin"] @@ -115,7 +130,9 @@ def add_action_result(self, action, result): "logs": log_items }) - def get_report(self, publish_context): + def get_report( + self, publish_context: pyblish.api.Context + ) -> Dict[str, Any]: """Report data with all details of current state.""" now = arrow.utcnow().to("local") @@ -167,7 +184,7 @@ def get_report(self, publish_context): "report_version": "1.0.1", } - def _add_plugin_data_item(self, plugin): + def _add_plugin_data_item(self, plugin: pyblish.api.Plugin): if plugin.id in self._plugin_data_by_id: # A plugin would be processed more than once. What can cause it: # - there is a bug in controller @@ -179,7 +196,9 @@ def _add_plugin_data_item(self, plugin): plugin_data_item = self._create_plugin_data_item(plugin) self._plugin_data_by_id[plugin.id] = plugin_data_item - def _create_plugin_data_item(self, plugin): + def _create_plugin_data_item( + self, plugin: pyblish.api.Plugin + ) -> Dict[str, Any]: label = None if hasattr(plugin, "label"): label = plugin.label @@ -196,7 +215,9 @@ def _create_plugin_data_item(self, plugin): "passed": False } - def _extract_context_data(self, context): + def _extract_context_data( + self, context: pyblish.api.Context + ) -> Dict[str, Any]: context_label = "Context" if context is not None: context_label = context.data.get("label") @@ -204,7 +225,9 @@ def _extract_context_data(self, context): "label": context_label } - def _extract_instance_data(self, instance, exists): + def _extract_instance_data( + self, instance: pyblish.api.Instance, exists: bool + ) -> Dict[str, Any]: return { "name": instance.data.get("name"), "label": get_publish_instance_label(instance), @@ -216,7 +239,9 @@ def _extract_instance_data(self, instance, exists): "instance_id": instance.data.get("instance_id"), } - def _extract_instance_log_items(self, result): + def _extract_instance_log_items( + self, result: Dict[str, Any] + ) -> List[Dict[str, Any]]: instance = result["instance"] instance_id = None if instance: @@ -284,6 +309,68 @@ def _extract_log_items(self, result): return output +class PublishPluginActionItem: + """Representation of publish plugin action. + + Data driven object which is used as proxy for controller and UI. + + Args: + action_id (str): Action id. + plugin_id (str): Plugin id. + active (bool): Action is active. + on_filter (ActionFilterType): Actions have 'on' attribte which define when can be + action triggered (e.g. 'all', 'failed', ...). + label (str): Action's label. + icon (Union[str, None]) Action's icon. + """ + + def __init__( + self, + action_id: str, + plugin_id: str, + active: bool, + on_filter: ActionFilterType, + label: str, + icon: Union[str, None], + ): + self.action_id: str = action_id + self.plugin_id: str = plugin_id + self.active: bool = active + self.on_filter: ActionFilterType = on_filter + self.label: str = label + self.icon: Union[str, None] = icon + + def to_data(self) -> Dict[str, Any]: + """Serialize object to dictionary. + + Returns: + Dict[str, Union[str,bool,None]]: Serialized object. + """ + + return { + "action_id": self.action_id, + "plugin_id": self.plugin_id, + "active": self.active, + "on_filter": self.on_filter, + "label": self.label, + "icon": self.icon + } + + @classmethod + def from_data(cls, data: Dict[str, Any]) -> "PublishPluginActionItem": + """Create object from data. + + Args: + data (Dict[str, Union[str,bool,None]]): Data used to recreate + object. + + Returns: + PublishPluginActionItem: Object created using data. + """ + + return cls(**data) + + class PublishPluginsProxy: """Wrapper around publish plugin. @@ -302,10 +389,10 @@ class PublishPluginsProxy: processed. """ - def __init__(self, plugins): - plugins_by_id = {} - actions_by_plugin_id = {} - action_ids_by_plugin_id = {} + def __init__(self, plugins: List[pyblish.api.Plugin]): + plugins_by_id: Dict[str, pyblish.api.Plugin] = {} + actions_by_plugin_id: Dict[str, Dict[str, pyblish.api.Action]] = {} + action_ids_by_plugin_id: Dict[str, List[str]] = {} for plugin in plugins: plugin_id = plugin.id plugins_by_id[plugin_id] = plugin @@ -321,17 +408,23 @@ def __init__(self, plugins): action_ids.append(action_id) actions_by_id[action_id] = action - self._plugins_by_id = plugins_by_id - self._actions_by_plugin_id = actions_by_plugin_id - self._action_ids_by_plugin_id = action_ids_by_plugin_id + self._plugins_by_id: Dict[str, pyblish.api.Plugin] = plugins_by_id + self._actions_by_plugin_id: Dict[ + str, Dict[str, pyblish.api.Action] + ] = actions_by_plugin_id + self._action_ids_by_plugin_id: Dict[str, List[str]] = ( + action_ids_by_plugin_id + ) - def get_action(self, plugin_id, action_id): + def get_action( + self, plugin_id: str, action_id: str + ) -> pyblish.api.Action: return self._actions_by_plugin_id[plugin_id][action_id] - def get_plugin(self, plugin_id): + def get_plugin(self, plugin_id: str) -> pyblish.api.Plugin: return self._plugins_by_id[plugin_id] - def get_plugin_id(self, plugin): + def get_plugin_id(self, plugin: pyblish.api.Plugin) -> str: """Get id of plugin based on plugin object. It's used for validation errors report. @@ -346,7 +439,9 @@ def get_plugin_id(self, plugin): return plugin.id - def get_plugin_action_items(self, plugin_id): + def get_plugin_action_items( + self, plugin_id: str + ) -> List[PublishPluginActionItem]: """Get plugin action items for plugin by its id. Args: @@ -364,7 +459,9 @@ def get_plugin_action_items(self, plugin_id): for action_id in self._action_ids_by_plugin_id[plugin_id] ] - def _create_action_item(self, action, plugin_id): + def _create_action_item( + self, action: pyblish.api.Action, plugin_id: str + ) -> PublishPluginActionItem: label = action.label or action.__name__ icon = getattr(action, "icon", None) return PublishPluginActionItem( @@ -377,60 +474,6 @@ def _create_action_item(self, action, plugin_id): ) -class PublishPluginActionItem: - """Representation of publish plugin action. - - Data driven object which is used as proxy for controller and UI. - - Args: - action_id (str): Action id. - plugin_id (str): Plugin id. - active (bool): Action is active. - on_filter (str): Actions have 'on' attribte which define when can be - action triggered (e.g. 'all', 'failed', ...). - label (str): Action's label. - icon (Union[str, None]) Action's icon. - """ - - def __init__(self, action_id, plugin_id, active, on_filter, label, icon): - self.action_id = action_id - self.plugin_id = plugin_id - self.active = active - self.on_filter = on_filter - self.label = label - self.icon = icon - - def to_data(self): - """Serialize object to dictionary. - - Returns: - Dict[str, Union[str,bool,None]]: Serialized object. - """ - - return { - "action_id": self.action_id, - "plugin_id": self.plugin_id, - "active": self.active, - "on_filter": self.on_filter, - "label": self.label, - "icon": self.icon - } - - @classmethod - def from_data(cls, data): - """Create object from data. - - Args: - data (Dict[str, Union[str,bool,None]]): Data used to recreate - object. - - Returns: - PublishPluginActionItem: Object created using data. - """ - - return cls(**data) - - class ValidationErrorItem: """Data driven validation error item. @@ -441,22 +484,26 @@ class ValidationErrorItem: and UI connection. Args: - instance_id (str): Id of pyblish instance to which is validation error - connected. + instance_id (Union[str, None]): Pyblish instance id to which is + validation error connected. instance_label (str): Prepared instance label. - plugin_id (str): Id of pyblish Plugin which triggered the validation + plugin_id (str): Pyblish plugin id which triggered the validation error. Id is generated using 'PublishPluginsProxy'. - """ + context_validation (bool): Error happened on context. + title (str): Error title. + descripttion (str): Error description. + detail (str): Error detail. + """ def __init__( self, - instance_id, - instance_label, - plugin_id, - context_validation, - title, - description, - detail + instance_id: Union[str, None], + instance_label: Union[str, None], + plugin_id: str, + context_validation: bool, + title: str, + description: str, + detail: str ): self.instance_id = instance_id self.instance_label = instance_label @@ -466,7 +513,7 @@ def __init__( self.description = description self.detail = detail - def to_data(self): + def to_data(self) -> Dict[str, Any]: """Serialize object to dictionary. Returns: @@ -484,7 +531,12 @@ def to_data(self): } @classmethod - def from_result(cls, plugin_id, error, instance): + def from_result( + cls, + plugin_id: str, + error: PublishValidationError, + instance: Union[pyblish.api.Instance, None] + ): """Create new object based on resukt from controller. Returns: @@ -519,19 +571,19 @@ class PublishValidationErrorsReport: Args: error_items (List[ValidationErrorItem]): List of validation errors. - plugin_action_items (Dict[str, PublishPluginActionItem]): Action items - by plugin id. - """ + plugin_action_items (Dict[str, List[PublishPluginActionItem]]): Action + items by plugin id. + """ def __init__(self, error_items, plugin_action_items): self._error_items = error_items self._plugin_action_items = plugin_action_items - def __iter__(self): + def __iter__(self) -> Iterable[ValidationErrorItem]: for item in self._error_items: yield item - def group_items_by_title(self): + def group_items_by_title(self) -> List[Dict[str, Any]]: """Group errors by plugin and their titles. Items are grouped by plugin and title -> same title from different @@ -599,7 +651,9 @@ def to_data(self): } @classmethod - def from_data(cls, data): + def from_data( + cls, data: Dict[str, Any] + ) -> "PublishValidationErrorsReport": """Recreate object from data. Args: @@ -614,10 +668,12 @@ def from_data(cls, data): ValidationErrorItem.from_data(error_item) for error_item in data["error_items"] ] - plugin_action_items = [ - PublishPluginActionItem.from_data(action_item) - for action_item in data["plugin_action_items"] - ] + plugin_action_items = {} + for action_item in data["plugin_action_items"]: + item = PublishPluginActionItem.from_data(action_item) + action_items = plugin_action_items.setdefault(item.plugin_id, []) + action_items.append(item) + return cls(error_items, plugin_action_items) @@ -625,20 +681,22 @@ class PublishValidationErrors: """Object to keep track about validation errors by plugin.""" def __init__(self): - self._plugins_proxy = None - self._error_items = [] - self._plugin_action_items = {} + self._plugins_proxy: Union[PublishPluginsProxy, None] = None + self._error_items: List[ValidationErrorItem] = [] + self._plugin_action_items: Dict[ + str, List[PublishPluginActionItem] + ] = {} def __bool__(self): return self.has_errors @property - def has_errors(self): + def has_errors(self) -> bool: """At least one error was added.""" return bool(self._error_items) - def reset(self, plugins_proxy): + def reset(self, plugins_proxy: PublishPluginsProxy): """Reset object to default state. Args: @@ -650,7 +708,7 @@ def reset(self, plugins_proxy): self._error_items = [] self._plugin_action_items = {} - def create_report(self): + def create_report(self) -> PublishValidationErrorsReport: """Create report based on currently existing errors. Returns: @@ -662,12 +720,17 @@ def create_report(self): self._error_items, self._plugin_action_items ) - def add_error(self, plugin, error, instance): + def add_error( + self, + plugin: pyblish.api.Plugin, + error: PublishValidationError, + instance: Union[pyblish.api.Instance, None] + ): """Add error from pyblish result. Args: plugin (pyblish.api.Plugin): Plugin which triggered error. - error (ValidationException): Validation error. + error (PublishValidationError): Validation error. instance (Union[pyblish.api.Instance, None]): Instance on which was error raised or None if was raised on context. """ @@ -693,18 +756,21 @@ def add_error(self, plugin, error, instance): self._plugin_action_items[plugin_id] = plugin_actions -def collect_families_from_instances(instances, only_active=False): +def collect_families_from_instances( + instances: List[pyblish.api.Instance], + only_active: Optional[bool] = False +) -> List[str]: """Collect all families for passed publish instances. Args: - instances(list): List of publish instances from + instances (list[pyblish.api.Instance]): List of publish instances from which are families collected. - only_active(bool): Return families only for active instances. + only_active (bool): Return families only for active instances. Returns: list[str]: Families available on instances. - """ + """ all_families = set() for instance in instances: if only_active: @@ -722,51 +788,55 @@ def collect_families_from_instances(instances, only_active=False): class PublishModel: - def __init__(self, controller): + def __init__(self, controller: AbstractPublisherController): self._controller = controller # Publishing should stop at validation stage - self._publish_up_validation = False - self._publish_comment_is_set = False + self._publish_up_validation: bool = False + self._publish_comment_is_set: bool = False # Any other exception that happened during publishing - self._publish_error_msg = None + self._publish_error_msg: Union[str, None] = None # Publishing is in progress - self._publish_is_running = False + self._publish_is_running: bool = False # Publishing is over validation order - self._publish_has_validated = False + self._publish_has_validated: bool = False - self._publish_has_validation_errors = False - self._publish_has_crashed = False + self._publish_has_validation_errors: bool = False + self._publish_has_crashed: bool = False # All publish plugins are processed - self._publish_has_started = False - self._publish_has_finished = False - self._publish_max_progress = 0 - self._publish_progress = 0 - - self._publish_plugins = [] - self._publish_plugins_proxy = None + self._publish_has_started: bool = False + self._publish_has_finished: bool = False + self._publish_max_progress: int = 0 + self._publish_progress: int = 0 + + self._publish_plugins: List[pyblish.api.Plugin] = [] + self._publish_plugins_proxy: PublishPluginsProxy = ( + PublishPluginsProxy([]) + ) # pyblish.api.Context self._publish_context = None # Pyblish report - self._publish_report = PublishReportMaker() + self._publish_report: PublishReportMaker = PublishReportMaker() # Store exceptions of validation error - self._publish_validation_errors = PublishValidationErrors() + self._publish_validation_errors: PublishValidationErrors = ( + PublishValidationErrors() + ) # This information is not much important for controller but for widget # which can change (and set) the comment. - self._publish_comment_is_set = False + self._publish_comment_is_set: bool = False # Validation order # - plugin with order same or higher than this value is extractor or # higher - self._validation_order = ( + self._validation_order: int = ( pyblish.api.ValidatorOrder + PLUGIN_ORDER_OFFSET ) # Plugin iterator - self._main_thread_iter = None + self._main_thread_iter: Iterable[partial] = [] def reset(self): create_context = self._controller.get_create_context() @@ -810,10 +880,10 @@ def reset(self): self._emit_event("publish.reset.finished") - def set_publish_up_validation(self, value): + def set_publish_up_validation(self, value: bool): self._publish_up_validation = value - def start_publish(self, wait=True): + def start_publish(self, wait: Optional[bool] = True): """Run publishing. Make sure all changes are saved before method is called (Call @@ -831,24 +901,25 @@ def start_publish(self, wait=True): func = self.get_next_process_func() func() - def get_next_process_func(self): + def get_next_process_func(self) -> partial: # Validations of progress before using iterator # - same conditions may be inside iterator but they may be used # only in specific cases (e.g. when it happens for a first time) - # There are validation errors and validation is passed - # - can't do any progree if ( - self._publish_has_validated - and self._publish_has_validation_errors + self._main_thread_iter is None + # There are validation errors and validation is passed + # - can't do any progree + or ( + self._publish_has_validated + and self._publish_has_validation_errors + ) + # Any unexpected error happened + # - everything should stop + or self._publish_has_crashed ): item = partial(self.stop_publish) - # Any unexpected error happened - # - everything should stop - elif self._publish_has_crashed: - item = partial(self.stop_publish) - # Everything is ok so try to get new processing item else: item = next(self._main_thread_iter) @@ -859,56 +930,56 @@ def stop_publish(self): if self._publish_is_running: self._stop_publish() - def is_running(self): + def is_running(self) -> bool: return self._publish_is_running - def is_crashed(self): + def is_crashed(self) -> bool: return self._publish_has_crashed - def has_started(self): + def has_started(self) -> bool: return self._publish_has_started - def has_finished(self): + def has_finished(self) -> bool: return self._publish_has_finished - def has_validated(self): + def has_validated(self) -> bool: return self._publish_has_validated - def has_validation_errors(self): + def has_validation_errors(self) -> bool: return self._publish_has_validation_errors - def publish_can_continue(self): + def publish_can_continue(self) -> bool: return ( not self._publish_has_crashed and not self._publish_has_validation_errors and not self._publish_has_finished ) - def get_progress(self): + def get_progress(self) -> int: return self._publish_progress - def get_max_progress(self): + def get_max_progress(self) -> int: return self._publish_max_progress - def get_publish_report(self): + def get_publish_report(self) -> Dict[str, Any]: return self._publish_report.get_report( self._publish_context ) - def get_validation_errors(self): + def get_validation_errors(self) -> PublishValidationErrorsReport: return self._publish_validation_errors.create_report() - def get_error_msg(self): + def get_error_msg(self) -> Union[str, None]: return self._publish_error_msg - def set_comment(self, comment): + def set_comment(self, comment: str): # Ignore change of comment when publishing started if self._publish_has_started: return self._publish_context.data["comment"] = comment self._publish_comment_is_set = True - def run_action(self, plugin_id, action_id): + def run_action(self, plugin_id: str, action_id: str): # TODO handle result in UI plugin = self._publish_plugins_proxy.get_plugin(plugin_id) action = self._publish_plugins_proxy.get_action(plugin_id, action_id) @@ -939,10 +1010,10 @@ def run_action(self, plugin_id, action_id): self._controller.emit_card_message("Action finished.") - def _emit_event(self, topic, data=None): + def _emit_event(self, topic: str, data: Optional[Dict[str, Any]] = None): self._controller.emit_event(topic, data, PUBLISH_EVENT_SOURCE) - def _set_finished(self, value): + def _set_finished(self, value: bool): if self._publish_has_finished != value: self._publish_has_finished = value self._emit_event( @@ -950,7 +1021,7 @@ def _set_finished(self, value): {"value": value} ) - def _set_is_running(self, value): + def _set_is_running(self, value: bool): if self._publish_is_running != value: self._publish_is_running = value self._emit_event( @@ -958,7 +1029,7 @@ def _set_is_running(self, value): {"value": value} ) - def _set_has_validated(self, value): + def _set_has_validated(self, value: bool): if self._publish_has_validated != value: self._publish_has_validated = value self._emit_event( @@ -966,7 +1037,7 @@ def _set_has_validated(self, value): {"value": value} ) - def _set_is_crashed(self, value): + def _set_is_crashed(self, value: bool): if self._publish_has_crashed != value: self._publish_has_crashed = value self._emit_event( @@ -974,7 +1045,7 @@ def _set_is_crashed(self, value): {"value": value} ) - def _set_has_validation_errors(self, value): + def _set_has_validation_errors(self, value: bool): if self._publish_has_validation_errors != value: self._publish_has_validation_errors = value self._emit_event( @@ -982,7 +1053,7 @@ def _set_has_validation_errors(self, value): {"value": value} ) - def _set_max_progress(self, value): + def _set_max_progress(self, value: int): if self._publish_max_progress != value: self._publish_max_progress = value self._emit_event( @@ -990,7 +1061,7 @@ def _set_max_progress(self, value): {"value": value} ) - def _set_progress(self, value): + def _set_progress(self, value: int): if self._publish_progress != value: self._publish_progress = value self._emit_event( @@ -998,7 +1069,7 @@ def _set_progress(self, value): {"value": value} ) - def _set_publish_error_msg(self, value): + def _set_publish_error_msg(self, value: Union[str, None]): if self._publish_error_msg != value: self._publish_error_msg = value self._emit_event( @@ -1022,7 +1093,7 @@ def _stop_publish(self): self._emit_event("publish.process.stopped") - def _publish_iterator(self): + def _publish_iterator(self) -> Iterable[partial]: """Main logic center of publishing. Iterator returns `partial` objects with callbacks that should be @@ -1128,7 +1199,11 @@ def _publish_iterator(self): self._set_progress(self._publish_max_progress) yield partial(self.stop_publish) - def _process_and_continue(self, plugin, instance): + def _process_and_continue( + self, + plugin: pyblish.api.Plugin, + instance: pyblish.api.Instance + ): result = pyblish.plugin.process( plugin, self._publish_context, instance ) @@ -1158,7 +1233,7 @@ def _process_and_continue(self, plugin, instance): self._publish_report.add_result(plugin.id, result) - def _add_validation_error(self, result): + def _add_validation_error(self, result: Dict[str, Any]): self._set_has_validation_errors(True) self._publish_validation_errors.add_error( result["plugin"], @@ -1166,7 +1241,7 @@ def _add_validation_error(self, result): result["instance"] ) - def _is_publish_plugin_active(self, plugin): + def _is_publish_plugin_active(self, plugin: pyblish.api.Plugin) -> bool: """Decide if publish plugin is active. This is hack because 'active' is mis-used in mixin From c25bccc36d7cf912c5e22c03907f48b93c5baef7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 21 Jun 2024 19:23:29 +0200 Subject: [PATCH 10/15] more typehints --- client/ayon_core/tools/publisher/abstract.py | 161 +++++++++++++----- client/ayon_core/tools/publisher/control.py | 2 - .../tools/publisher/models/create.py | 6 +- .../tools/publisher/models/publish.py | 42 ++--- 4 files changed, 144 insertions(+), 67 deletions(-) diff --git a/client/ayon_core/tools/publisher/abstract.py b/client/ayon_core/tools/publisher/abstract.py index a04ad298ef..1562edb4dc 100644 --- a/client/ayon_core/tools/publisher/abstract.py +++ b/client/ayon_core/tools/publisher/abstract.py @@ -1,4 +1,11 @@ from abc import ABC, abstractmethod +from typing import Optional, Dict, List, Tuple, Any, Callable, Union, Iterable + +from ayon_core.lib import AbstractAttrDef +from ayon_core.host import HostBase +from ayon_core.tools.common_models import FolderItem, TaskItem +from ayon_core.pipeline.create import CreateContext, CreatedInstance +from ayon_core.pipeline.create.context import ConvertorItem class CardMessageTypes: @@ -18,17 +25,28 @@ class AbstractPublisherController(ABC): """ @abstractmethod - def emit_event(self, topic, data=None, source=None): - """Use implemented event system to trigger event.""" + def is_headless(self) -> bool: + pass + @abstractmethod + def get_host(self) -> HostBase: pass @abstractmethod - def register_event_callback(self, topic, callback): + def emit_event( + self, topic: str, + data: Optional[Dict[str, Any]] = None, + source: Optional[str] = None + ): + """Use implemented event system to trigger event.""" pass @abstractmethod - def get_current_project_name(self): + def register_event_callback(self, topic: str, callback: Callable): + pass + + @abstractmethod + def get_current_project_name(self) -> Union[str, None]: """Current context project name. Returns: @@ -38,7 +56,7 @@ def get_current_project_name(self): pass @abstractmethod - def get_current_folder_path(self): + def get_current_folder_path(self) -> Union[str, None]: """Current context folder path. Returns: @@ -48,7 +66,7 @@ def get_current_folder_path(self): pass @abstractmethod - def get_current_task_name(self): + def get_current_task_name(self) -> Union[str, None]: """Current context task name. Returns: @@ -58,7 +76,7 @@ def get_current_task_name(self): pass @abstractmethod - def host_context_has_changed(self): + def host_context_has_changed(self) -> bool: """Host context changed after last reset. 'CreateContext' has this option available using 'context_has_changed'. @@ -70,7 +88,7 @@ def host_context_has_changed(self): pass @abstractmethod - def is_host_valid(self): + def is_host_valid(self) -> bool: """Host is valid for creation part. Host must have implemented certain functionality to be able create @@ -83,7 +101,39 @@ def is_host_valid(self): pass @abstractmethod - def get_instances(self): + def get_folder_entity( + self, project_name: str, folder_id: str + ) -> Union[Dict[str, Any], None]: + pass + + @abstractmethod + def get_task_entity( + self, project_name: str, task_id: str + ) -> Union[Dict[str, Any], None]: + pass + + @abstractmethod + def get_folder_item_by_path( + self, project_name: str, folder_path: str + ) -> Union[FolderItem, None]: + pass + + @abstractmethod + def get_task_item_by_name( + self, + project_name: str, + folder_id: str, + task_name: str, + sender: Optional[str] = None + ) -> Union[TaskItem, None]: + pass + + @abstractmethod + def get_create_context(self) -> CreateContext: + pass + + @abstractmethod + def get_instances(self) -> List[CreatedInstance]: """Collected/created instances. Returns: @@ -93,15 +143,19 @@ def get_instances(self): pass @abstractmethod - def get_instance_by_id(self, instance_id): + def get_instance_by_id( + self, instance_id: str + ) -> Union[CreatedInstance, None]: pass @abstractmethod - def get_instances_by_id(self, instance_ids=None): + def get_instances_by_id( + self, instance_ids: Optional[Iterable[str]] = None + ) -> Dict[str, Union[CreatedInstance, None]]: pass @abstractmethod - def get_context_title(self): + def get_context_title(self) -> Union[str, None]: """Get context title for artist shown at the top of main window. Returns: @@ -112,7 +166,7 @@ def get_context_title(self): pass @abstractmethod - def get_existing_product_names(self, folder_path): + def get_existing_product_names(self, folder_path: str) -> List[str]: pass @abstractmethod @@ -126,15 +180,27 @@ def reset(self): pass @abstractmethod - def get_creator_attribute_definitions(self, instances): + def get_creator_attribute_definitions( + self, instances: List[CreatedInstance] + ) -> List[Tuple[AbstractAttrDef, List[CreatedInstance], List[Any]]]: pass @abstractmethod - def get_publish_attribute_definitions(self, instances, include_context): + def get_publish_attribute_definitions( + self, + instances: List[CreatedInstance], + include_context: bool + ) -> List[Tuple[ + str, + List[AbstractAttrDef], + Dict[str, List[Tuple[CreatedInstance, Any]]] + ]]: pass @abstractmethod - def get_creator_icon(self, identifier): + def get_creator_icon( + self, identifier: str + ) -> Union[str, Dict[str, Any], None]: """Receive creator's icon by identifier. Args: @@ -149,11 +215,11 @@ def get_creator_icon(self, identifier): @abstractmethod def get_product_name( self, - creator_identifier, - variant, - task_name, - folder_path, - instance_id=None + creator_identifier: str, + variant: str, + task_name: Union[str, None], + folder_path: Union[str, None], + instance_id: Optional[str] = None ): """Get product name based on passed data. @@ -171,7 +237,11 @@ def get_product_name( @abstractmethod def create( - self, creator_identifier, product_name, instance_data, options + self, + creator_identifier: str, + product_name: str, + instance_data: Dict[str, Any], + options: Dict[str, Any], ): """Trigger creation by creator identifier. @@ -188,7 +258,7 @@ def create( pass @abstractmethod - def save_changes(self): + def save_changes(self) -> bool: """Save changes in create context. Save can crash because of unexpected errors. @@ -200,14 +270,14 @@ def save_changes(self): pass @abstractmethod - def remove_instances(self, instance_ids): + def remove_instances(self, instance_ids: List[str]): """Remove list of instances from create context.""" # TODO expect instance ids pass @abstractmethod - def publish_has_started(self): + def publish_has_started(self) -> bool: """Has publishing finished. Returns: @@ -217,7 +287,7 @@ def publish_has_started(self): pass @abstractmethod - def publish_has_finished(self): + def publish_has_finished(self) -> bool: """Has publishing finished. Returns: @@ -227,7 +297,7 @@ def publish_has_finished(self): pass @abstractmethod - def publish_is_running(self): + def publish_is_running(self) -> bool: """Publishing is running right now. Returns: @@ -237,7 +307,7 @@ def publish_is_running(self): pass @abstractmethod - def publish_has_validated(self): + def publish_has_validated(self) -> bool: """Publish validation passed. Returns: @@ -247,7 +317,7 @@ def publish_has_validated(self): pass @abstractmethod - def publish_has_crashed(self): + def publish_has_crashed(self) -> bool: """Publishing crashed for any reason. Returns: @@ -257,7 +327,7 @@ def publish_has_crashed(self): pass @abstractmethod - def publish_has_validation_errors(self): + def publish_has_validation_errors(self) -> bool: """During validation happened at least one validation error. Returns: @@ -267,7 +337,7 @@ def publish_has_validation_errors(self): pass @abstractmethod - def get_publish_max_progress(self): + def get_publish_max_progress(self) -> int: """Get maximum possible progress number. Returns: @@ -277,7 +347,7 @@ def get_publish_max_progress(self): pass @abstractmethod - def get_publish_progress(self): + def get_publish_progress(self) -> int: """Current progress number. Returns: @@ -287,7 +357,7 @@ def get_publish_progress(self): pass @abstractmethod - def get_publish_error_msg(self): + def get_publish_error_msg(self) -> Union[str, None]: """Current error message which cause fail of publishing. Returns: @@ -298,7 +368,7 @@ def get_publish_error_msg(self): pass @abstractmethod - def get_publish_report(self): + def get_publish_report(self) -> Dict[str, Any]: pass @abstractmethod @@ -328,7 +398,7 @@ def stop_publish(self): pass @abstractmethod - def run_action(self, plugin_id, action_id): + def run_action(self, plugin_id: str, action_id: str): """Trigger pyblish action on a plugin. Args: @@ -339,23 +409,27 @@ def run_action(self, plugin_id, action_id): pass @abstractmethod - def get_convertor_items(self): + def get_convertor_items(self) -> Dict[str, ConvertorItem]: pass @abstractmethod - def trigger_convertor_items(self, convertor_identifiers): + def trigger_convertor_items(self, convertor_identifiers: List[str]): pass @abstractmethod - def get_thumbnail_paths_for_instances(self, instance_ids): + def get_thumbnail_paths_for_instances( + self, instance_ids: List[str] + ) -> Dict[str, Union[str, None]]: pass @abstractmethod - def set_thumbnail_paths_for_instances(self, thumbnail_path_mapping): + def set_thumbnail_paths_for_instances( + self, thumbnail_path_mapping: Dict[str, str] + ): pass @abstractmethod - def set_comment(self, comment): + def set_comment(self, comment: str): """Set comment on pyblish context. Set "comment" key on current pyblish.api.Context data. @@ -368,7 +442,9 @@ def set_comment(self, comment): @abstractmethod def emit_card_message( - self, message, message_type=CardMessageTypes.standard + self, + message: str, + message_type: Optional[str] = CardMessageTypes.standard ): """Emit a card message which can have a lifetime. @@ -377,12 +453,13 @@ def emit_card_message( Args: message (str): Message that will be showed. + message_type (Optional[str]): Message type. """ pass @abstractmethod - def get_thumbnail_temp_dir_path(self): + def get_thumbnail_temp_dir_path(self) -> str: """Return path to directory where thumbnails can be temporary stored. Returns: diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index a6fbe878b5..35cc8fb15f 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -2,7 +2,6 @@ import logging import tempfile import shutil -from abc import abstractmethod import ayon_api @@ -12,7 +11,6 @@ registered_host, get_process_id, ) -from ayon_core.pipeline.create import CreateContext from ayon_core.tools.common_models import ProjectsModel, HierarchyModel from .models import ( diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index a80a782773..fc561cf6d0 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -427,7 +427,7 @@ def trigger_convertor_items(self, convertor_identifiers: List[str]): CardMessageTypes.error ) - def save_changes(self, show_message: Optional[bool] = True): + def save_changes(self, show_message: Optional[bool] = True) -> bool: """Save changes happened during creation. Trigger save of changes using host api. This functionality does not @@ -515,7 +515,9 @@ def get_creator_attribute_definitions( return output def get_publish_attribute_definitions( - self, instances: List[CreatedInstance], include_context: bool + self, + instances: List[CreatedInstance], + include_context: bool ) -> List[Tuple[ str, List[AbstractAttrDef], diff --git a/client/ayon_core/tools/publisher/models/publish.py b/client/ayon_core/tools/publisher/models/publish.py index 27827be316..7f32e8c057 100644 --- a/client/ayon_core/tools/publisher/models/publish.py +++ b/client/ayon_core/tools/publisher/models/publish.py @@ -51,7 +51,7 @@ def __init__( self._all_instances_by_id: Dict[str, pyblish.api.Instance] = {} self._plugin_data_by_id: Dict[str, Any] = {} - self._current_plugin_id: Union[str, None] = None + self._current_plugin_id: Optional[str] = None self.reset( creator_discover_result, @@ -318,10 +318,10 @@ class PublishPluginActionItem: action_id (str): Action id. plugin_id (str): Plugin id. active (bool): Action is active. - on_filter (ActionFilterType): Actions have 'on' attribte which define when can be - action triggered (e.g. 'all', 'failed', ...). + on_filter (ActionFilterType): Actions have 'on' attribute which define + when can be action triggered (e.g. 'all', 'failed', ...). label (str): Action's label. - icon (Union[str, None]) Action's icon. + icon (Optional[str]) Action's icon. """ def __init__( @@ -331,14 +331,14 @@ def __init__( active: bool, on_filter: ActionFilterType, label: str, - icon: Union[str, None], + icon: Optional[str], ): self.action_id: str = action_id self.plugin_id: str = plugin_id self.active: bool = active self.on_filter: ActionFilterType = on_filter self.label: str = label - self.icon: Union[str, None] = icon + self.icon: Optional[str] = icon def to_data(self) -> Dict[str, Any]: """Serialize object to dictionary. @@ -484,34 +484,34 @@ class ValidationErrorItem: and UI connection. Args: - instance_id (Union[str, None]): Pyblish instance id to which is + instance_id (Optional[str]): Pyblish instance id to which is validation error connected. - instance_label (str): Prepared instance label. + instance_label (Optional[str]): Prepared instance label. plugin_id (str): Pyblish plugin id which triggered the validation error. Id is generated using 'PublishPluginsProxy'. context_validation (bool): Error happened on context. title (str): Error title. - descripttion (str): Error description. + description (str): Error description. detail (str): Error detail. """ def __init__( self, - instance_id: Union[str, None], - instance_label: Union[str, None], + instance_id: Optional[str], + instance_label: Optional[str], plugin_id: str, context_validation: bool, title: str, description: str, detail: str ): - self.instance_id = instance_id - self.instance_label = instance_label - self.plugin_id = plugin_id - self.context_validation = context_validation - self.title = title - self.description = description - self.detail = detail + self.instance_id: Optional[str] = instance_id + self.instance_label: Optional[str] = instance_label + self.plugin_id: str = plugin_id + self.context_validation: bool = context_validation + self.title: str = title + self.description: str = description + self.detail: str = detail def to_data(self) -> Dict[str, Any]: """Serialize object to dictionary. @@ -796,7 +796,7 @@ def __init__(self, controller: AbstractPublisherController): self._publish_comment_is_set: bool = False # Any other exception that happened during publishing - self._publish_error_msg: Union[str, None] = None + self._publish_error_msg: Optional[str] = None # Publishing is in progress self._publish_is_running: bool = False # Publishing is over validation order @@ -969,7 +969,7 @@ def get_publish_report(self) -> Dict[str, Any]: def get_validation_errors(self) -> PublishValidationErrorsReport: return self._publish_validation_errors.create_report() - def get_error_msg(self) -> Union[str, None]: + def get_error_msg(self) -> Optional[str]: return self._publish_error_msg def set_comment(self, comment: str): @@ -1069,7 +1069,7 @@ def _set_progress(self, value: int): {"value": value} ) - def _set_publish_error_msg(self, value: Union[str, None]): + def _set_publish_error_msg(self, value: Optional[str]): if self._publish_error_msg != value: self._publish_error_msg = value self._emit_event( From bafb3e7cedff3ce7569d5646f74a39913e9000a4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 24 Jun 2024 11:44:02 +0200 Subject: [PATCH 11/15] handle missing folder items --- client/ayon_core/tools/publisher/control.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index 35cc8fb15f..4f8e7e8582 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -263,19 +263,23 @@ def get_folder_id_from_path(self, folder_path): def get_task_items_by_folder_paths(self, folder_paths): if not folder_paths: return {} + folder_items = self._hierarchy_model.get_folder_items_by_paths( self.get_current_project_name(), folder_paths ) + output = { folder_path: [] for folder_path in folder_paths } project_name = self.get_current_project_name() - for folder_item in folder_items.values(): - task_items = self._hierarchy_model.get_task_items( - project_name, folder_item.entity_id, None - ) - output[folder_item.path] = task_items + for folder_path, folder_item in folder_items.items(): + task_items = [] + if folder_item is not None: + task_items = self._hierarchy_model.get_task_items( + project_name, folder_item.entity_id, None + ) + output[folder_path] = task_items return output From 3de2d65c5b2c759d2c2738c42f5da511208eeb73 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 24 Jun 2024 14:02:49 +0200 Subject: [PATCH 12/15] added typehints for controller --- .../ayon_core/tools/common_models/__init__.py | 4 + client/ayon_core/tools/publisher/abstract.py | 373 +++++++++++------- client/ayon_core/tools/publisher/constants.py | 3 + client/ayon_core/tools/publisher/control.py | 86 ++-- .../tools/publisher/models/__init__.py | 3 +- .../tools/publisher/models/create.py | 6 +- .../tools/publisher/models/publish.py | 4 +- .../publisher/widgets/card_view_widgets.py | 17 +- .../widgets/create_context_widgets.py | 15 +- .../tools/publisher/widgets/create_widget.py | 20 +- .../tools/publisher/widgets/folders_dialog.py | 5 +- .../tools/publisher/widgets/help_widget.py | 8 +- .../publisher/widgets/list_view_widgets.py | 15 +- .../publisher/widgets/overview_widget.py | 11 +- .../tools/publisher/widgets/publish_frame.py | 13 +- .../tools/publisher/widgets/report_page.py | 54 ++- .../tools/publisher/widgets/tasks_model.py | 16 +- .../publisher/widgets/thumbnail_widget.py | 11 +- .../tools/publisher/widgets/widgets.py | 76 ++-- client/ayon_core/tools/publisher/window.py | 24 +- 20 files changed, 470 insertions(+), 294 deletions(-) diff --git a/client/ayon_core/tools/common_models/__init__.py b/client/ayon_core/tools/common_models/__init__.py index f09edfeab2..7b644f5dba 100644 --- a/client/ayon_core/tools/common_models/__init__.py +++ b/client/ayon_core/tools/common_models/__init__.py @@ -5,6 +5,8 @@ ProjectItem, ProjectsModel, PROJECTS_MODEL_SENDER, + FolderTypeItem, + TaskTypeItem, ) from .hierarchy import ( FolderItem, @@ -24,6 +26,8 @@ "ProjectItem", "ProjectsModel", "PROJECTS_MODEL_SENDER", + "FolderTypeItem", + "TaskTypeItem", "FolderItem", "TaskItem", diff --git a/client/ayon_core/tools/publisher/abstract.py b/client/ayon_core/tools/publisher/abstract.py index 1562edb4dc..447fa5607f 100644 --- a/client/ayon_core/tools/publisher/abstract.py +++ b/client/ayon_core/tools/publisher/abstract.py @@ -3,9 +3,16 @@ from ayon_core.lib import AbstractAttrDef from ayon_core.host import HostBase -from ayon_core.tools.common_models import FolderItem, TaskItem from ayon_core.pipeline.create import CreateContext, CreatedInstance from ayon_core.pipeline.create.context import ConvertorItem +from ayon_core.tools.common_models import ( + FolderItem, + TaskItem, + FolderTypeItem, + TaskTypeItem, +) + +from .models import CreatorItem class CardMessageTypes: @@ -14,22 +21,19 @@ class CardMessageTypes: error = "error" -class AbstractPublisherController(ABC): - """Publisher tool controller. - - Define what must be implemented to be able use Publisher functionality. +class AbstractPublisherCommon(ABC): + @abstractmethod + def register_event_callback(self, topic, callback): + """Register event callback. - Goal is to have "data driven" controller that can be used to control UI - running in different process. That lead to some disadvantages like UI can't - access objects directly but by using wrappers that can be serialized. - """ + Listen for events with given topic. - @abstractmethod - def is_headless(self) -> bool: - pass + Args: + topic (str): Name of topic. + callback (Callable): Callback that will be called when event + is triggered. + """ - @abstractmethod - def get_host(self) -> HostBase: pass @abstractmethod @@ -38,11 +42,32 @@ def emit_event( data: Optional[Dict[str, Any]] = None, source: Optional[str] = None ): - """Use implemented event system to trigger event.""" + """Emit event. + + Args: + topic (str): Event topic used for callbacks filtering. + data (Optional[dict[str, Any]]): Event data. + source (Optional[str]): Event source. + + """ pass @abstractmethod - def register_event_callback(self, topic: str, callback: Callable): + def emit_card_message( + self, + message: str, + message_type: Optional[str] = CardMessageTypes.standard + ): + """Emit a card message which can have a lifetime. + + This is for UI purposes. Method can be extended to more arguments + in future e.g. different message timeout or type (color). + + Args: + message (str): Message that will be showed. + message_type (Optional[str]): Message type. + """ + pass @abstractmethod @@ -88,34 +113,36 @@ def host_context_has_changed(self) -> bool: pass @abstractmethod - def is_host_valid(self) -> bool: - """Host is valid for creation part. - - Host must have implemented certain functionality to be able create - in Publisher tool. + def reset(self): + """Reset whole controller. - Returns: - bool: Host can handle creation of instances. + This should reset create context, publish context and all variables + that are related to it. """ pass + +class AbstractPublisherBackend(AbstractPublisherCommon): @abstractmethod - def get_folder_entity( - self, project_name: str, folder_id: str - ) -> Union[Dict[str, Any], None]: + def is_headless(self) -> bool: + """Controller is in headless mode. + + Notes: + Not sure if this method is relevant in UI tool? + + Returns: + bool: Headless mode. + + """ pass @abstractmethod - def get_task_entity( - self, project_name: str, task_id: str - ) -> Union[Dict[str, Any], None]: + def get_host(self) -> HostBase: pass @abstractmethod - def get_folder_item_by_path( - self, project_name: str, folder_path: str - ) -> Union[FolderItem, None]: + def get_create_context(self) -> CreateContext: pass @abstractmethod @@ -129,29 +156,40 @@ def get_task_item_by_name( pass @abstractmethod - def get_create_context(self) -> CreateContext: + def get_folder_entity( + self, project_name: str, folder_id: str + ) -> Union[Dict[str, Any], None]: pass @abstractmethod - def get_instances(self) -> List[CreatedInstance]: - """Collected/created instances. - - Returns: - List[CreatedInstance]: List of created instances. + def get_folder_item_by_path( + self, project_name: str, folder_path: str + ) -> Union[FolderItem, None]: + pass - """ + @abstractmethod + def get_task_entity( + self, project_name: str, task_id: str + ) -> Union[Dict[str, Any], None]: pass + +class AbstractPublisherFrontend(AbstractPublisherCommon): @abstractmethod - def get_instance_by_id( - self, instance_id: str - ) -> Union[CreatedInstance, None]: + def register_event_callback(self, topic: str, callback: Callable): pass @abstractmethod - def get_instances_by_id( - self, instance_ids: Optional[Iterable[str]] = None - ) -> Dict[str, Union[CreatedInstance, None]]: + def is_host_valid(self) -> bool: + """Host is valid for creation part. + + Host must have implemented certain functionality to be able create + in Publisher tool. + + Returns: + bool: Host can handle creation of instances. + + """ pass @abstractmethod @@ -166,35 +204,57 @@ def get_context_title(self) -> Union[str, None]: pass @abstractmethod - def get_existing_product_names(self, folder_path: str) -> List[str]: + def get_task_items_by_folder_paths( + self, folder_paths: Iterable[str] + ) -> Dict[str, List[TaskItem]]: pass @abstractmethod - def reset(self): - """Reset whole controller. + def get_folder_items( + self, project_name: str, sender: Optional[str] = None + ) -> List[FolderItem]: + pass - This should reset create context, publish context and all variables - that are related to it. - """ + @abstractmethod + def get_task_items( + self, project_name: str, folder_id: str, sender: Optional[str] = None + ) -> List[TaskItem]: + pass + @abstractmethod + def get_folder_type_items( + self, project_name: str, sender: Optional[str] = None + ) -> List[FolderTypeItem]: pass @abstractmethod - def get_creator_attribute_definitions( - self, instances: List[CreatedInstance] - ) -> List[Tuple[AbstractAttrDef, List[CreatedInstance], List[Any]]]: + def get_task_type_items( + self, project_name: str, sender: Optional[str] = None + ) -> List[TaskTypeItem]: pass @abstractmethod - def get_publish_attribute_definitions( - self, - instances: List[CreatedInstance], - include_context: bool - ) -> List[Tuple[ - str, - List[AbstractAttrDef], - Dict[str, List[Tuple[CreatedInstance, Any]]] - ]]: + def are_folder_paths_valid(self, folder_paths: Iterable[str]) -> bool: + """Folder paths do exist in project. + + Args: + folder_paths (Iterable[str]): List of folder paths. + + Returns: + bool: All folder paths exist in project. + + """ + pass + + # --- Create --- + @abstractmethod + def get_creator_items(self) -> Dict[str, CreatorItem]: + """Creator items by identifier. + + Returns: + Dict[str, CreatorItem]: Creator items that will be shown to user. + + """ pass @abstractmethod @@ -203,6 +263,9 @@ def get_creator_icon( ) -> Union[str, Dict[str, Any], None]: """Receive creator's icon by identifier. + Todos: + Icon should be part of 'CreatorItem'. + Args: identifier (str): Creator's identifier. @@ -212,6 +275,55 @@ def get_creator_icon( pass + @abstractmethod + def get_convertor_items(self) -> Dict[str, ConvertorItem]: + """Convertor items by identifier. + + Returns: + Dict[str, ConvertorItem]: Convertor items that can be triggered + by user. + + """ + pass + + @abstractmethod + def get_instances(self) -> List[CreatedInstance]: + """Collected/created instances. + + Returns: + List[CreatedInstance]: List of created instances. + + """ + pass + + @abstractmethod + def get_instances_by_id( + self, instance_ids: Optional[Iterable[str]] = None + ) -> Dict[str, Union[CreatedInstance, None]]: + pass + + @abstractmethod + def get_existing_product_names(self, folder_path: str) -> List[str]: + pass + + @abstractmethod + def get_creator_attribute_definitions( + self, instances: List[CreatedInstance] + ) -> List[Tuple[AbstractAttrDef, List[CreatedInstance], List[Any]]]: + pass + + @abstractmethod + def get_publish_attribute_definitions( + self, + instances: List[CreatedInstance], + include_context: bool + ) -> List[Tuple[ + str, + List[AbstractAttrDef], + Dict[str, List[Tuple[CreatedInstance, Any]]] + ]]: + pass + @abstractmethod def get_product_name( self, @@ -257,6 +369,17 @@ def create( pass + @abstractmethod + def trigger_convertor_items(self, convertor_identifiers: List[str]): + pass + + @abstractmethod + def remove_instances(self, instance_ids: Iterable[str]): + """Remove list of instances from create context.""" + # TODO expect instance ids + + pass + @abstractmethod def save_changes(self) -> bool: """Save changes in create context. @@ -269,10 +392,37 @@ def save_changes(self) -> bool: pass + # --- Publish --- @abstractmethod - def remove_instances(self, instance_ids: List[str]): - """Remove list of instances from create context.""" - # TODO expect instance ids + def publish(self): + """Trigger publishing without any order limitations.""" + + pass + + @abstractmethod + def validate(self): + """Trigger publishing which will stop after validation order.""" + + pass + + @abstractmethod + def stop_publish(self): + """Stop publishing can be also used to pause publishing. + + Pause of publishing is possible only if all plugins successfully + finished. + """ + + pass + + @abstractmethod + def run_action(self, plugin_id: str, action_id: str): + """Trigger pyblish action on a plugin. + + Args: + plugin_id (str): Id of publish plugin. + action_id (str): Id of publish action. + """ pass @@ -312,8 +462,18 @@ def publish_has_validated(self) -> bool: Returns: bool: If publishing passed last possible validation order. + """ + pass + + @abstractmethod + def publish_can_continue(self): + """Publish has still plugins to process and did not crash yet. + + Returns: + bool: Publishing can continue in processing. + """ pass @abstractmethod @@ -337,21 +497,21 @@ def publish_has_validation_errors(self) -> bool: pass @abstractmethod - def get_publish_max_progress(self) -> int: - """Get maximum possible progress number. + def get_publish_progress(self) -> int: + """Current progress number. Returns: - int: Number that can be used as 100% of publish progress bar. + int: Current progress value from 0 to 'publish_max_progress'. """ pass @abstractmethod - def get_publish_progress(self) -> int: - """Current progress number. + def get_publish_max_progress(self) -> int: + """Get maximum possible progress number. Returns: - int: Current progress value from 0 to 'publish_max_progress'. + int: Number that can be used as 100% of publish progress bar. """ pass @@ -376,46 +536,17 @@ def get_validation_errors(self): pass @abstractmethod - def publish(self): - """Trigger publishing without any order limitations.""" - - pass - - @abstractmethod - def validate(self): - """Trigger publishing which will stop after validation order.""" - - pass - - @abstractmethod - def stop_publish(self): - """Stop publishing can be also used to pause publishing. - - Pause of publishing is possible only if all plugins successfully - finished. - """ - - pass + def set_comment(self, comment: str): + """Set comment on pyblish context. - @abstractmethod - def run_action(self, plugin_id: str, action_id: str): - """Trigger pyblish action on a plugin. + Set "comment" key on current pyblish.api.Context data. Args: - plugin_id (str): Id of publish plugin. - action_id (str): Id of publish action. + comment (str): Artist's comment. """ pass - @abstractmethod - def get_convertor_items(self) -> Dict[str, ConvertorItem]: - pass - - @abstractmethod - def trigger_convertor_items(self, convertor_identifiers: List[str]): - pass - @abstractmethod def get_thumbnail_paths_for_instances( self, instance_ids: List[str] @@ -424,40 +555,10 @@ def get_thumbnail_paths_for_instances( @abstractmethod def set_thumbnail_paths_for_instances( - self, thumbnail_path_mapping: Dict[str, str] + self, thumbnail_path_mapping: Dict[str, Optional[str]] ): pass - @abstractmethod - def set_comment(self, comment: str): - """Set comment on pyblish context. - - Set "comment" key on current pyblish.api.Context data. - - Args: - comment (str): Artist's comment. - """ - - pass - - @abstractmethod - def emit_card_message( - self, - message: str, - message_type: Optional[str] = CardMessageTypes.standard - ): - """Emit a card message which can have a lifetime. - - This is for UI purposes. Method can be extended to more arguments - in future e.g. different message timeout or type (color). - - Args: - message (str): Message that will be showed. - message_type (Optional[str]): Message type. - """ - - pass - @abstractmethod def get_thumbnail_temp_dir_path(self) -> str: """Return path to directory where thumbnails can be temporary stored. diff --git a/client/ayon_core/tools/publisher/constants.py b/client/ayon_core/tools/publisher/constants.py index 6676f14c3d..285724727d 100644 --- a/client/ayon_core/tools/publisher/constants.py +++ b/client/ayon_core/tools/publisher/constants.py @@ -37,6 +37,9 @@ "CONTEXT_ID", "CONTEXT_LABEL", + "CONTEXT_GROUP", + "CONVERTOR_ITEM_GROUP", + "VARIANT_TOOLTIP", "INPUTS_LAYOUT_HSPACING", diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index 4f8e7e8582..f26f8fc524 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -17,43 +17,17 @@ PublishModel, CreateModel, ) -from .abstract import AbstractPublisherController, CardMessageTypes - - -class BasePublisherController(AbstractPublisherController): - """Implement common logic for controllers. - - Implement event system, logger and common attributes. Attributes are - triggering value changes so anyone can listen to their topics. - - Prepare implementation for creator items. Controller must implement just - their filling by '_collect_creator_items'. - - All prepared implementation is based on calling super '__init__'. - """ - - def get_thumbnail_temp_dir_path(self): - """Return path to directory where thumbnails can be temporary stored. - - Returns: - str: Path to a directory. - """ - - return os.path.join( - tempfile.gettempdir(), - "publisher_thumbnails", - get_process_id() - ) - - def clear_thumbnail_temp_dir_path(self): - """Remove content of thumbnail temp directory.""" - - dirpath = self.get_thumbnail_temp_dir_path() - if os.path.exists(dirpath): - shutil.rmtree(dirpath) +from .abstract import ( + AbstractPublisherBackend, + AbstractPublisherFrontend, + CardMessageTypes +) -class PublisherController(BasePublisherController): +class PublisherController( + AbstractPublisherBackend, + AbstractPublisherFrontend, +): """Middleware between UI, CreateContext and publish Context. Handle both creation and publishing parts. @@ -137,6 +111,17 @@ def emit_event(self, topic, data=None, source=None): data = {} self._event_system.emit(topic, data, source) + def emit_card_message( + self, message, message_type=CardMessageTypes.standard + ): + self._emit_event( + "show.card.message", + { + "message": message, + "message_type": message_type + } + ) + def register_event_callback(self, topic, callback): self._event_system.add_callback(topic, callback) @@ -183,6 +168,7 @@ def get_creator_icon(self, identifier): Args: identifier (str): Creator's identifier for which should be icon returned. + """ return self._create_model.get_creator_icon(identifier) @@ -201,9 +187,6 @@ def get_instances(self): """Current instances in create context.""" return self._create_model.get_instances() - def get_instance_by_id(self, instance_id): - return self._create_model.get_instance_by_id(instance_id) - def get_instances_by_id(self, instance_ids=None): return self._create_model.get_instances_by_id(instance_ids) @@ -356,17 +339,26 @@ def set_thumbnail_paths_for_instances(self, thumbnail_path_mapping): thumbnail_path_mapping ) - def emit_card_message( - self, message, message_type=CardMessageTypes.standard - ): - self._emit_event( - "show.card.message", - { - "message": message, - "message_type": message_type - } + def get_thumbnail_temp_dir_path(self): + """Return path to directory where thumbnails can be temporary stored. + + Returns: + str: Path to a directory. + """ + + return os.path.join( + tempfile.gettempdir(), + "publisher_thumbnails", + get_process_id() ) + def clear_thumbnail_temp_dir_path(self): + """Remove content of thumbnail temp directory.""" + + dirpath = self.get_thumbnail_temp_dir_path() + if os.path.exists(dirpath): + shutil.rmtree(dirpath) + def get_creator_attribute_definitions(self, instances): """Collect creator attribute definitions for multuple instances. diff --git a/client/ayon_core/tools/publisher/models/__init__.py b/client/ayon_core/tools/publisher/models/__init__.py index 194ea944ef..bd593be29b 100644 --- a/client/ayon_core/tools/publisher/models/__init__.py +++ b/client/ayon_core/tools/publisher/models/__init__.py @@ -1,9 +1,10 @@ -from .create import CreateModel +from .create import CreateModel, CreatorItem from .publish import PublishModel __all__ = ( "CreateModel", + "CreatorItem", "PublishModel", ) diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index fc561cf6d0..6da3a51a31 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -23,7 +23,7 @@ ConvertorItem, ) from ayon_core.tools.publisher.abstract import ( - AbstractPublisherController, + AbstractPublisherBackend, CardMessageTypes, ) CREATE_EVENT_SOURCE = "publisher.create.model" @@ -191,9 +191,9 @@ def from_data(cls, data: Dict[str, Any]) -> "CreatorItem": class CreateModel: - def __init__(self, controller: AbstractPublisherController): + def __init__(self, controller: AbstractPublisherBackend): self._log = None - self._controller = controller + self._controller: AbstractPublisherBackend = controller self._create_context = CreateContext( controller.get_host(), diff --git a/client/ayon_core/tools/publisher/models/publish.py b/client/ayon_core/tools/publisher/models/publish.py index 7f32e8c057..da7b64ceae 100644 --- a/client/ayon_core/tools/publisher/models/publish.py +++ b/client/ayon_core/tools/publisher/models/publish.py @@ -16,7 +16,7 @@ ) from ayon_core.pipeline.plugin_discover import DiscoverResult from ayon_core.pipeline.publish import get_publish_instance_label -from ayon_core.tools.publisher.abstract import AbstractPublisherController +from ayon_core.tools.publisher.abstract import AbstractPublisherBackend PUBLISH_EVENT_SOURCE = "publisher.publish.model" # Define constant for plugin orders offset @@ -788,7 +788,7 @@ def collect_families_from_instances( class PublishModel: - def __init__(self, controller: AbstractPublisherController): + def __init__(self, controller: AbstractPublisherBackend): self._controller = controller # Publishing should stop at validation stage diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py index 5c12f0fbb0..3178f0c591 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -29,18 +29,21 @@ from ayon_core.tools.utils import BaseClickableFrame from ayon_core.tools.utils.lib import html_escape + +from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend +from ayon_core.tools.publisher.constants import ( + CONTEXT_ID, + CONTEXT_LABEL, + CONTEXT_GROUP, + CONVERTOR_ITEM_GROUP, +) + from .widgets import ( AbstractInstanceView, ContextWarningLabel, IconValuePixmapLabel, PublishPixmapLabel ) -from ..constants import ( - CONTEXT_ID, - CONTEXT_LABEL, - CONTEXT_GROUP, - CONVERTOR_ITEM_GROUP, -) class SelectionTypes: @@ -560,7 +563,7 @@ class InstanceCardView(AbstractInstanceView): def __init__(self, controller, parent): super(InstanceCardView, self).__init__(parent) - self._controller = controller + self._controller: AbstractPublisherFrontend = controller scroll_area = QtWidgets.QScrollArea(self) scroll_area.setWidgetResizable(True) diff --git a/client/ayon_core/tools/publisher/widgets/create_context_widgets.py b/client/ayon_core/tools/publisher/widgets/create_context_widgets.py index 6e665bc963..faf2248181 100644 --- a/client/ayon_core/tools/publisher/widgets/create_context_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/create_context_widgets.py @@ -5,6 +5,7 @@ from ayon_core.tools.common_models import HierarchyExpectedSelection from ayon_core.tools.utils import FoldersWidget, TasksWidget +from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend class CreateSelectionModel(object): @@ -18,8 +19,8 @@ class CreateSelectionModel(object): event_source = "publisher.create.selection.model" - def __init__(self, controller): - self._controller = controller + def __init__(self, controller: "CreateHierarchyController"): + self._controller: CreateHierarchyController = controller self._project_name = None self._folder_id = None @@ -94,9 +95,9 @@ class CreateHierarchyController: controller (PublisherController): Publisher controller. """ - def __init__(self, controller): + def __init__(self, controller: AbstractPublisherFrontend): self._event_system = QueuedEventSystem() - self._controller = controller + self._controller: AbstractPublisherFrontend = controller self._selection_model = CreateSelectionModel(self) self._expected_selection = HierarchyExpectedSelection( self, handle_project=False @@ -168,10 +169,10 @@ class CreateContextWidget(QtWidgets.QWidget): folder_changed = QtCore.Signal() task_changed = QtCore.Signal() - def __init__(self, controller, parent): - super(CreateContextWidget, self).__init__(parent) + def __init__(self, controller: AbstractPublisherFrontend, parent): + super().__init__(parent) - self._controller = controller + self._controller: AbstractPublisherFrontend = controller self._enabled = True self._last_project_name = None self._last_folder_id = None diff --git a/client/ayon_core/tools/publisher/widgets/create_widget.py b/client/ayon_core/tools/publisher/widgets/create_widget.py index 72859a4ad2..af6fee533e 100644 --- a/client/ayon_core/tools/publisher/widgets/create_widget.py +++ b/client/ayon_core/tools/publisher/widgets/create_widget.py @@ -9,14 +9,8 @@ TaskNotSetError, ) -from .thumbnail_widget import ThumbnailWidget -from .widgets import ( - IconValuePixmapLabel, - CreateBtn, -) -from .create_context_widgets import CreateContextWidget -from .precreate_widget import PreCreateWidget -from ..constants import ( +from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend +from ayon_core.tools.publisher.constants import ( VARIANT_TOOLTIP, PRODUCT_TYPE_ROLE, CREATOR_IDENTIFIER_ROLE, @@ -26,6 +20,14 @@ INPUTS_LAYOUT_VSPACING, ) +from .thumbnail_widget import ThumbnailWidget +from .widgets import ( + IconValuePixmapLabel, + CreateBtn, +) +from .create_context_widgets import CreateContextWidget +from .precreate_widget import PreCreateWidget + SEPARATORS = ("---separator---", "---") @@ -106,7 +108,7 @@ class CreateWidget(QtWidgets.QWidget): def __init__(self, controller, parent=None): super(CreateWidget, self).__init__(parent) - self._controller = controller + self._controller: AbstractPublisherFrontend = controller self._folder_path = None self._product_names = None diff --git a/client/ayon_core/tools/publisher/widgets/folders_dialog.py b/client/ayon_core/tools/publisher/widgets/folders_dialog.py index baf229290d..fa9a174c9c 100644 --- a/client/ayon_core/tools/publisher/widgets/folders_dialog.py +++ b/client/ayon_core/tools/publisher/widgets/folders_dialog.py @@ -2,12 +2,13 @@ from ayon_core.lib.events import QueuedEventSystem from ayon_core.tools.utils import PlaceholderLineEdit, FoldersWidget +from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend class FoldersDialogController: - def __init__(self, controller): + def __init__(self, controller: AbstractPublisherFrontend): self._event_system = QueuedEventSystem() - self._controller = controller + self._controller: AbstractPublisherFrontend = controller @property def event_system(self): diff --git a/client/ayon_core/tools/publisher/widgets/help_widget.py b/client/ayon_core/tools/publisher/widgets/help_widget.py index aaff60acba..062b117220 100644 --- a/client/ayon_core/tools/publisher/widgets/help_widget.py +++ b/client/ayon_core/tools/publisher/widgets/help_widget.py @@ -5,6 +5,8 @@ from qtpy import QtWidgets, QtCore +from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend + class HelpButton(QtWidgets.QPushButton): """Button used to trigger help dialog.""" @@ -54,7 +56,9 @@ class HelpDialog(QtWidgets.QDialog): default_width = 530 default_height = 340 - def __init__(self, controller, parent): + def __init__( + self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget + ): super(HelpDialog, self).__init__(parent) self.setWindowTitle("Help dialog") @@ -68,7 +72,7 @@ def __init__(self, controller, parent): "show.detailed.help", self._on_help_request ) - self._controller = controller + self._controller: AbstractPublisherFrontend = controller self._help_content = help_content diff --git a/client/ayon_core/tools/publisher/widgets/list_view_widgets.py b/client/ayon_core/tools/publisher/widgets/list_view_widgets.py index 714edb5eef..74fd3265cc 100644 --- a/client/ayon_core/tools/publisher/widgets/list_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/list_view_widgets.py @@ -29,8 +29,9 @@ from ayon_core.style import get_objected_colors from ayon_core.tools.utils import NiceCheckbox from ayon_core.tools.utils.lib import html_escape, checkstate_int_to_enum -from .widgets import AbstractInstanceView -from ..constants import ( + +from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend +from ayon_core.tools.publisher.constants import ( INSTANCE_ID_ROLE, SORT_VALUE_ROLE, IS_GROUP_ROLE, @@ -41,6 +42,8 @@ CONVERTOR_ITEM_GROUP, ) +from .widgets import AbstractInstanceView + class ListItemDelegate(QtWidgets.QStyledItemDelegate): """Generic delegate for instance group. @@ -442,10 +445,12 @@ class InstanceListView(AbstractInstanceView): double_clicked = QtCore.Signal() - def __init__(self, controller, parent): - super(InstanceListView, self).__init__(parent) + def __init__( + self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget + ): + super().__init__(parent) - self._controller = controller + self._controller: AbstractPublisherFrontend = controller instance_view = InstanceTreeView(self) instance_delegate = ListItemDelegate(instance_view) diff --git a/client/ayon_core/tools/publisher/widgets/overview_widget.py b/client/ayon_core/tools/publisher/widgets/overview_widget.py index a8eb4c7116..52a45d0881 100644 --- a/client/ayon_core/tools/publisher/widgets/overview_widget.py +++ b/client/ayon_core/tools/publisher/widgets/overview_widget.py @@ -1,7 +1,8 @@ from qtpy import QtWidgets, QtCore -from .border_label_widget import BorderedLabelWidget +from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend +from .border_label_widget import BorderedLabelWidget from .card_view_widgets import InstanceCardView from .list_view_widgets import InstanceListView from .widgets import ( @@ -23,11 +24,13 @@ class OverviewWidget(QtWidgets.QFrame): anim_end_value = 200 anim_duration = 200 - def __init__(self, controller, parent): - super(OverviewWidget, self).__init__(parent) + def __init__( + self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget + ): + super().__init__(parent) self._refreshing_instances = False - self._controller = controller + self._controller: AbstractPublisherFrontend = controller product_content_widget = QtWidgets.QWidget(self) diff --git a/client/ayon_core/tools/publisher/widgets/publish_frame.py b/client/ayon_core/tools/publisher/widgets/publish_frame.py index 264b0a2e02..4041a58ec7 100644 --- a/client/ayon_core/tools/publisher/widgets/publish_frame.py +++ b/client/ayon_core/tools/publisher/widgets/publish_frame.py @@ -1,5 +1,7 @@ from qtpy import QtWidgets, QtCore +from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend + from .widgets import ( StopBtn, ResetBtn, @@ -31,8 +33,13 @@ class PublishFrame(QtWidgets.QWidget): details_page_requested = QtCore.Signal() - def __init__(self, controller, borders, parent): - super(PublishFrame, self).__init__(parent) + def __init__( + self, + controller: AbstractPublisherFrontend, + borders: int, + parent: QtWidgets.QWidget + ): + super().__init__(parent) # Bottom part of widget where process and callback buttons are showed # - QFrame used to be able set background using stylesheets easily @@ -179,7 +186,7 @@ def __init__(self, controller, borders, parent): self._shrunk_anim = shrunk_anim - self._controller = controller + self._controller: AbstractPublisherFrontend = controller self._content_frame = content_frame self._content_layout = content_layout diff --git a/client/ayon_core/tools/publisher/widgets/report_page.py b/client/ayon_core/tools/publisher/widgets/report_page.py index e8b29aefc2..896e8f93af 100644 --- a/client/ayon_core/tools/publisher/widgets/report_page.py +++ b/client/ayon_core/tools/publisher/widgets/report_page.py @@ -19,16 +19,18 @@ paint_image_with_color, SeparatorWidget, ) +from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend +from ayon_core.tools.publisher.constants import ( + INSTANCE_ID_ROLE, + CONTEXT_ID, + CONTEXT_LABEL, +) + from .widgets import IconValuePixmapLabel from .icons import ( get_pixmap, get_image, ) -from ..constants import ( - INSTANCE_ID_ROLE, - CONTEXT_ID, - CONTEXT_LABEL, -) LOG_DEBUG_VISIBLE = 1 << 0 LOG_INFO_VISIBLE = 1 << 1 @@ -159,8 +161,10 @@ class ValidateActionsWidget(QtWidgets.QFrame): Change actions based on selected validation error. """ - def __init__(self, controller, parent): - super(ValidateActionsWidget, self).__init__(parent) + def __init__( + self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget + ): + super().__init__(parent) self.setAttribute(QtCore.Qt.WA_TranslucentBackground) @@ -172,7 +176,7 @@ def __init__(self, controller, parent): layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(content_widget) - self._controller = controller + self._controller: AbstractPublisherFrontend = controller self._content_widget = content_widget self._content_layout = content_layout @@ -874,8 +878,10 @@ class PublishInstancesViewWidget(QtWidgets.QWidget): _min_width_measure_string = 24 * "O" selection_changed = QtCore.Signal() - def __init__(self, controller, parent): - super(PublishInstancesViewWidget, self).__init__(parent) + def __init__( + self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget + ): + super().__init__(parent) scroll_area = VerticalScrollArea(self) scroll_area.setWidgetResizable(True) @@ -898,7 +904,7 @@ def __init__(self, controller, parent): layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(scroll_area, 1) - self._controller = controller + self._controller: AbstractPublisherFrontend = controller self._scroll_area = scroll_area self._instance_view = instance_view self._instance_layout = instance_layout @@ -1357,7 +1363,7 @@ def hideEvent(self, event): self._is_showed = False def closeEvent(self, event): - super(InstancesLogsView, self).closeEvent(event) + super().closeEvent(event) self._is_showed = False def _update_instances(self): @@ -1455,8 +1461,10 @@ class CrashWidget(QtWidgets.QWidget): actions. """ - def __init__(self, controller, parent): - super(CrashWidget, self).__init__(parent) + def __init__( + self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget + ): + super().__init__(parent) main_label = QtWidgets.QLabel("This is not your fault", self) main_label.setAlignment(QtCore.Qt.AlignCenter) @@ -1498,7 +1506,7 @@ def __init__(self, controller, parent): copy_clipboard_btn.clicked.connect(self._on_copy_to_clipboard) save_to_disk_btn.clicked.connect(self._on_save_to_disk_click) - self._controller = controller + self._controller: AbstractPublisherFrontend = controller def _on_copy_to_clipboard(self): self._controller.emit_event( @@ -1623,8 +1631,10 @@ class ReportsWidget(QtWidgets.QWidget): └──────┴─────────┴─────────┘ """ - def __init__(self, controller, parent): - super(ReportsWidget, self).__init__(parent) + def __init__( + self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget + ): + super().__init__(parent) # Instances view views_widget = QtWidgets.QWidget(self) @@ -1708,7 +1718,7 @@ def __init__(self, controller, parent): self._detail_input_scroll = detail_input_scroll self._crash_widget = crash_widget - self._controller = controller + self._controller: AbstractPublisherFrontend = controller self._validation_errors_by_id = {} @@ -1818,8 +1828,10 @@ class ReportPageWidget(QtWidgets.QFrame): and validation error detail with possible actions (repair). """ - def __init__(self, controller, parent): - super(ReportPageWidget, self).__init__(parent) + def __init__( + self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget + ): + super().__init__(parent) header_label = QtWidgets.QLabel(self) header_label.setAlignment(QtCore.Qt.AlignCenter) @@ -1845,7 +1857,7 @@ def __init__(self, controller, parent): self._header_label = header_label self._publish_instances_widget = publish_instances_widget - self._controller = controller + self._controller: AbstractPublisherFrontend = controller def _update_label(self): if not self._controller.publish_has_started(): diff --git a/client/ayon_core/tools/publisher/widgets/tasks_model.py b/client/ayon_core/tools/publisher/widgets/tasks_model.py index a7db953821..16a4111f59 100644 --- a/client/ayon_core/tools/publisher/widgets/tasks_model.py +++ b/client/ayon_core/tools/publisher/widgets/tasks_model.py @@ -1,7 +1,10 @@ +from typing import Optional + from qtpy import QtCore, QtGui from ayon_core.style import get_default_entity_icon_color from ayon_core.tools.utils import get_qt_icon +from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend TASK_NAME_ROLE = QtCore.Qt.UserRole + 1 TASK_TYPE_ROLE = QtCore.Qt.UserRole + 2 @@ -19,14 +22,19 @@ class TasksModel(QtGui.QStandardItemModel): tasks with same names then model is empty too. Args: - controller (PublisherController): Controller which handles creation and + controller (AbstractPublisherFrontend): Controller which handles creation and publishing. + """ - def __init__(self, controller, allow_empty_task=False): - super(TasksModel, self).__init__() + def __init__( + self, + controller: AbstractPublisherFrontend, + allow_empty_task: Optional[bool] = False + ): + super().__init__() self._allow_empty_task = allow_empty_task - self._controller = controller + self._controller: AbstractPublisherFrontend = controller self._items_by_name = {} self._folder_paths = [] self._task_names_by_folder_path = {} diff --git a/client/ayon_core/tools/publisher/widgets/thumbnail_widget.py b/client/ayon_core/tools/publisher/widgets/thumbnail_widget.py index 07dc532534..de7af80d50 100644 --- a/client/ayon_core/tools/publisher/widgets/thumbnail_widget.py +++ b/client/ayon_core/tools/publisher/widgets/thumbnail_widget.py @@ -19,7 +19,10 @@ paint_image_with_color, PixmapButton, ) -from ayon_core.tools.publisher.control import CardMessageTypes +from ayon_core.tools.publisher.abstract import ( + CardMessageTypes, + AbstractPublisherFrontend, +) from .icons import get_image from .screenshot_widget import capture_to_file @@ -299,7 +302,9 @@ class ThumbnailWidget(QtWidgets.QWidget): thumbnail_created = QtCore.Signal(str) thumbnail_cleared = QtCore.Signal() - def __init__(self, controller, parent): + def __init__( + self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget + ): # Missing implementation for thumbnail # - widget kept to make a visial offset of global attr widget offset super(ThumbnailWidget, self).__init__(parent) @@ -355,7 +360,7 @@ def __init__(self, controller, parent): paste_btn.clicked.connect(self._on_paste_from_clipboard) browse_btn.clicked.connect(self._on_browse_clicked) - self._controller = controller + self._controller: AbstractPublisherFrontend = controller self._output_dir = controller.get_thumbnail_temp_dir_path() self._review_extensions = set(IMAGE_EXTENSIONS) | set(VIDEO_EXTENSIONS) diff --git a/client/ayon_core/tools/publisher/widgets/widgets.py b/client/ayon_core/tools/publisher/widgets/widgets.py index 4299a572b3..9e1d2d0525 100644 --- a/client/ayon_core/tools/publisher/widgets/widgets.py +++ b/client/ayon_core/tools/publisher/widgets/widgets.py @@ -10,6 +10,11 @@ import qtawesome from ayon_core.lib.attribute_definitions import UnknownDef +from ayon_core.style import get_objected_colors +from ayon_core.pipeline.create import ( + PRODUCT_NAME_ALLOWED_SYMBOLS, + TaskNotSetError, +) from ayon_core.tools.attribute_defs import create_widget_for_attr_def from ayon_core.tools import resources from ayon_core.tools.flickcharm import FlickCharm @@ -20,11 +25,14 @@ BaseClickableFrame, set_style_property, ) -from ayon_core.style import get_objected_colors -from ayon_core.pipeline.create import ( - PRODUCT_NAME_ALLOWED_SYMBOLS, - TaskNotSetError, +from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend +from ayon_core.tools.publisher.constants import ( + VARIANT_TOOLTIP, + ResetKeySequence, + INPUTS_LAYOUT_HSPACING, + INPUTS_LAYOUT_VSPACING, ) + from .thumbnail_widget import ThumbnailWidget from .folders_dialog import FoldersDialog from .tasks_model import TasksModel @@ -33,13 +41,6 @@ get_icon_path ) -from ..constants import ( - VARIANT_TOOLTIP, - ResetKeySequence, - INPUTS_LAYOUT_HSPACING, - INPUTS_LAYOUT_VSPACING, -) - FA_PREFIXES = ["", "fa.", "fa5.", "fa5b.", "fa5s.", "ei.", "mdi."] @@ -363,7 +364,9 @@ def get_selected_items(self): "{} Method 'get_selected_items' is not implemented." ).format(self.__class__.__name__)) - def set_selected_items(self, instance_ids, context_selected): + def set_selected_items( + self, instance_ids, context_selected, convertor_identifiers + ): """Change selection for instances and context. Used to applying selection from one view to other. @@ -371,8 +374,9 @@ def set_selected_items(self, instance_ids, context_selected): Args: instance_ids (List[str]): Selected instance ids. context_selected (bool): Context is selected. - """ + convertor_identifiers (List[str]): Selected convertor identifiers. + """ raise NotImplementedError(( "{} Method 'set_selected_items' is not implemented." ).format(self.__class__.__name__)) @@ -429,8 +433,10 @@ class FoldersFields(BaseClickableFrame): """ value_changed = QtCore.Signal() - def __init__(self, controller, parent): - super(FoldersFields, self).__init__(parent) + def __init__( + self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget + ): + super().__init__(parent) self.setObjectName("FolderPathInputWidget") # Don't use 'self' for parent! @@ -465,7 +471,7 @@ def __init__(self, controller, parent): icon_btn.clicked.connect(self._mouse_release_callback) dialog.finished.connect(self._on_dialog_finish) - self._controller = controller + self._controller: AbstractPublisherFrontend = controller self._dialog = dialog self._name_input = name_input self._icon_btn = icon_btn @@ -613,8 +619,10 @@ class TasksCombobox(QtWidgets.QComboBox): """ value_changed = QtCore.Signal() - def __init__(self, controller, parent): - super(TasksCombobox, self).__init__(parent) + def __init__( + self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget + ): + super().__init__(parent) self.setObjectName("TasksCombobox") # Set empty delegate to propagate stylesheet to a combobox @@ -1095,10 +1103,12 @@ class GlobalAttrsWidget(QtWidgets.QWidget): multiselection_text = "< Multiselection >" unknown_value = "N/A" - def __init__(self, controller, parent): - super(GlobalAttrsWidget, self).__init__(parent) + def __init__( + self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget + ): + super().__init__(parent) - self._controller = controller + self._controller: AbstractPublisherFrontend = controller self._current_instances = [] variant_input = VariantInputWidget(self) @@ -1338,8 +1348,10 @@ class CreatorAttrsWidget(QtWidgets.QWidget): widgets are merged into one (different label does not count). """ - def __init__(self, controller, parent): - super(CreatorAttrsWidget, self).__init__(parent) + def __init__( + self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget + ): + super().__init__(parent) scroll_area = QtWidgets.QScrollArea(self) scroll_area.setWidgetResizable(True) @@ -1351,7 +1363,7 @@ def __init__(self, controller, parent): self._main_layout = main_layout - self._controller = controller + self._controller: AbstractPublisherFrontend = controller self._scroll_area = scroll_area self._attr_def_id_to_instances = {} @@ -1476,8 +1488,10 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): does not count). """ - def __init__(self, controller, parent): - super(PublishPluginAttrsWidget, self).__init__(parent) + def __init__( + self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget + ): + super().__init__(parent) scroll_area = QtWidgets.QScrollArea(self) scroll_area.setWidgetResizable(True) @@ -1489,7 +1503,7 @@ def __init__(self, controller, parent): self._main_layout = main_layout - self._controller = controller + self._controller: AbstractPublisherFrontend = controller self._scroll_area = scroll_area self._attr_def_id_to_instances = {} @@ -1635,8 +1649,10 @@ class ProductAttributesWidget(QtWidgets.QWidget): instance_context_changed = QtCore.Signal() convert_requested = QtCore.Signal() - def __init__(self, controller, parent): - super(ProductAttributesWidget, self).__init__(parent) + def __init__( + self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget + ): + super().__init__(parent) # TOP PART top_widget = QtWidgets.QWidget(self) @@ -1738,7 +1754,7 @@ def __init__(self, controller, parent): "instance.thumbnail.changed", self._on_thumbnail_changed ) - self._controller = controller + self._controller: AbstractPublisherFrontend = controller self._convert_widget = convert_widget diff --git a/client/ayon_core/tools/publisher/window.py b/client/ayon_core/tools/publisher/window.py index 14938b145f..9d3a033acd 100644 --- a/client/ayon_core/tools/publisher/window.py +++ b/client/ayon_core/tools/publisher/window.py @@ -3,6 +3,8 @@ import time import collections import copy +from typing import Optional + from qtpy import QtWidgets, QtCore, QtGui from ayon_core import ( @@ -19,7 +21,7 @@ from .constants import ResetKeySequence from .publish_report_viewer import PublishReportViewerWidget -from .control import CardMessageTypes +from .abstract import CardMessageTypes, AbstractPublisherFrontend from .control_qt import QtPublisherController from .widgets import ( OverviewWidget, @@ -48,8 +50,13 @@ class PublisherWindow(QtWidgets.QDialog): footer_border = 8 publish_footer_spacer = 2 - def __init__(self, parent=None, controller=None, reset_on_show=None): - super(PublisherWindow, self).__init__(parent) + def __init__( + self, + parent: Optional[QtWidgets.QWidget] = None, + controller: Optional[AbstractPublisherFrontend] = None, + reset_on_show: Optional[bool] = None + ): + super().__init__(parent) self.setObjectName("PublishWindow") @@ -362,7 +369,7 @@ def __init__(self, parent=None, controller=None, reset_on_show=None): self._overlay_object = overlay_object - self._controller = controller + self._controller: AbstractPublisherFrontend = controller self._first_show = True self._first_reset = True @@ -386,7 +393,8 @@ def __init__(self, parent=None, controller=None, reset_on_show=None): self._window_is_visible = False @property - def controller(self): + def controller(self) -> AbstractPublisherFrontend: + """Kept for compatibility with traypublisher.""" return self._controller def show_and_publish(self, comment=None): @@ -520,7 +528,7 @@ def keyPressEvent(self, event): ) if reset_match_result == QtGui.QKeySequence.ExactMatch: - if not self.controller.publish_is_running: + if not self._controller.publish_is_running: self.reset() event.accept() return @@ -643,7 +651,7 @@ def _update_publish_details_widget(self, force=False): if not force and not self._is_on_details_tab(): return - report_data = self.controller.get_publish_report() + report_data = self._controller.get_publish_report() self._publish_details_widget.set_report_data(report_data) def _on_help_click(self): @@ -921,7 +929,7 @@ def _validate_create_instances(self): def _on_instances_refresh(self): self._validate_create_instances() - context_title = self.controller.get_context_title() + context_title = self._controller.get_context_title() self.set_context_label(context_title) self._update_publish_details_widget() From 04e257a29ef3a731dd581282461461601ebd42af Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 24 Jun 2024 14:03:29 +0200 Subject: [PATCH 13/15] fix default output of folder type items --- client/ayon_core/tools/utils/folders_widget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/utils/folders_widget.py b/client/ayon_core/tools/utils/folders_widget.py index bb75f3b6e5..965f4cc8a7 100644 --- a/client/ayon_core/tools/utils/folders_widget.py +++ b/client/ayon_core/tools/utils/folders_widget.py @@ -165,7 +165,7 @@ def _thread_getter(self, project_name): folder_items = self._controller.get_folder_items( project_name, FOLDERS_MODEL_SENDER_NAME ) - folder_type_items = {} + folder_type_items = [] if hasattr(self._controller, "get_folder_type_items"): folder_type_items = self._controller.get_folder_type_items( project_name, FOLDERS_MODEL_SENDER_NAME @@ -194,7 +194,7 @@ def _on_refresh_thread(self, thread_id): return if thread.failed: # TODO visualize that refresh failed - folder_items, folder_type_items = {}, {} + folder_items, folder_type_items = {}, [] else: folder_items, folder_type_items = thread.get_result() self._fill_items(folder_items, folder_type_items) From 05f2328e21885578a697d91355da215a33e18e55 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 24 Jun 2024 14:07:21 +0200 Subject: [PATCH 14/15] use py3 super calls --- .../publisher/publish_report_viewer/model.py | 8 ++-- .../publish_report_viewer/widgets.py | 22 ++++----- .../publisher/publish_report_viewer/window.py | 18 ++++---- .../publisher/widgets/border_label_widget.py | 10 ++-- .../publisher/widgets/card_view_widgets.py | 20 ++++---- .../tools/publisher/widgets/create_widget.py | 10 ++-- .../tools/publisher/widgets/folders_dialog.py | 4 +- .../tools/publisher/widgets/help_widget.py | 8 ++-- .../publisher/widgets/list_view_widgets.py | 24 +++++----- .../publisher/widgets/precreate_widget.py | 4 +- .../tools/publisher/widgets/publish_frame.py | 2 +- .../tools/publisher/widgets/report_page.py | 44 +++++++++--------- .../publisher/widgets/screenshot_widget.py | 4 +- .../tools/publisher/widgets/tabs_widget.py | 4 +- .../publisher/widgets/thumbnail_widget.py | 8 ++-- .../tools/publisher/widgets/widgets.py | 46 +++++++++---------- client/ayon_core/tools/publisher/window.py | 14 +++--- 17 files changed, 125 insertions(+), 125 deletions(-) diff --git a/client/ayon_core/tools/publisher/publish_report_viewer/model.py b/client/ayon_core/tools/publisher/publish_report_viewer/model.py index 9ed1bf555d..0bd2f9292a 100644 --- a/client/ayon_core/tools/publisher/publish_report_viewer/model.py +++ b/client/ayon_core/tools/publisher/publish_report_viewer/model.py @@ -17,7 +17,7 @@ class InstancesModel(QtGui.QStandardItemModel): def __init__(self, *args, **kwargs): - super(InstancesModel, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self._items_by_id = {} self._plugin_items_by_id = {} @@ -83,7 +83,7 @@ def set_report(self, report_item): class InstanceProxyModel(QtCore.QSortFilterProxyModel): def __init__(self, *args, **kwargs): - super(InstanceProxyModel, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self._ignore_removed = True @@ -116,7 +116,7 @@ class PluginsModel(QtGui.QStandardItemModel): ) def __init__(self, *args, **kwargs): - super(PluginsModel, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self._items_by_id = {} self._plugin_items_by_id = {} @@ -185,7 +185,7 @@ def set_report(self, report_item): class PluginProxyModel(QtCore.QSortFilterProxyModel): def __init__(self, *args, **kwargs): - super(PluginProxyModel, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self._ignore_skipped = True diff --git a/client/ayon_core/tools/publisher/publish_report_viewer/widgets.py b/client/ayon_core/tools/publisher/publish_report_viewer/widgets.py index 544d45ce89..61a52533ba 100644 --- a/client/ayon_core/tools/publisher/publish_report_viewer/widgets.py +++ b/client/ayon_core/tools/publisher/publish_report_viewer/widgets.py @@ -52,7 +52,7 @@ def set_report(self, report): class DetailWidget(QtWidgets.QTextEdit): def __init__(self, text, *args, **kwargs): - super(DetailWidget, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.setReadOnly(True) self.setHtml(text) @@ -73,7 +73,7 @@ def sizeHint(self): class PluginLoadReportWidget(QtWidgets.QWidget): def __init__(self, parent): - super(PluginLoadReportWidget, self).__init__(parent) + super().__init__(parent) view = QtWidgets.QTreeView(self) view.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) @@ -101,11 +101,11 @@ def _on_expand(self, index): self._create_widget(child_index) def showEvent(self, event): - super(PluginLoadReportWidget, self).showEvent(event) + super().showEvent(event) self._update_widgets_size_hints() def resizeEvent(self, event): - super(PluginLoadReportWidget, self).resizeEvent(event) + super().resizeEvent(event) self._update_widgets_size_hints() def _update_widgets_size_hints(self): @@ -146,7 +146,7 @@ class ZoomPlainText(QtWidgets.QPlainTextEdit): max_point_size = 200.0 def __init__(self, *args, **kwargs): - super(ZoomPlainText, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) anim_timer = QtCore.QTimer() anim_timer.setInterval(20) @@ -160,7 +160,7 @@ def __init__(self, *args, **kwargs): def wheelEvent(self, event): modifiers = QtWidgets.QApplication.keyboardModifiers() if modifiers != QtCore.Qt.ControlModifier: - super(ZoomPlainText, self).wheelEvent(event) + super().wheelEvent(event) return if hasattr(event, "angleDelta"): @@ -219,7 +219,7 @@ def _scaling_callback(self): class DetailsWidget(QtWidgets.QWidget): def __init__(self, parent): - super(DetailsWidget, self).__init__(parent) + super().__init__(parent) output_widget = ZoomPlainText(self) output_widget.setObjectName("PublishLogConsole") @@ -327,7 +327,7 @@ class DetailsPopup(QtWidgets.QDialog): closed = QtCore.Signal() def __init__(self, parent, center_widget): - super(DetailsPopup, self).__init__(parent) + super().__init__(parent) self.setWindowTitle("Report Details") layout = QtWidgets.QHBoxLayout(self) @@ -338,19 +338,19 @@ def __init__(self, parent, center_widget): def showEvent(self, event): layout = self.layout() layout.insertWidget(0, self._center_widget) - super(DetailsPopup, self).showEvent(event) + super().showEvent(event) if self._first_show: self._first_show = False self.resize(700, 400) def closeEvent(self, event): - super(DetailsPopup, self).closeEvent(event) + super().closeEvent(event) self.closed.emit() class PublishReportViewerWidget(QtWidgets.QFrame): def __init__(self, parent=None): - super(PublishReportViewerWidget, self).__init__(parent) + super().__init__(parent) instances_model = InstancesModel() instances_proxy = InstanceProxyModel() diff --git a/client/ayon_core/tools/publisher/publish_report_viewer/window.py b/client/ayon_core/tools/publisher/publish_report_viewer/window.py index 6427b915a8..3ee986e6f7 100644 --- a/client/ayon_core/tools/publisher/publish_report_viewer/window.py +++ b/client/ayon_core/tools/publisher/publish_report_viewer/window.py @@ -302,7 +302,7 @@ class LoadedFilesModel(QtGui.QStandardItemModel): header_labels = ("Reports", "Created") def __init__(self, *args, **kwargs): - super(LoadedFilesModel, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) # Column count must be set before setting header data self.setColumnCount(len(self.header_labels)) @@ -350,7 +350,7 @@ def data(self, index, role=None): if col != 0: index = self.index(index.row(), 0, index.parent()) - return super(LoadedFilesModel, self).data(index, role) + return super().data(index, role) def setData(self, index, value, role=None): if role is None: @@ -364,13 +364,13 @@ def setData(self, index, value, role=None): report_item.save() value = report_item.label - return super(LoadedFilesModel, self).setData(index, value, role) + return super().setData(index, value, role) def flags(self, index): # Allow editable flag only for first column if index.column() > 0: return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled - return super(LoadedFilesModel, self).flags(index) + return super().flags(index) def _create_item(self, report_item): if report_item.id in self._items_by_id: @@ -451,7 +451,7 @@ class LoadedFilesView(QtWidgets.QTreeView): selection_changed = QtCore.Signal() def __init__(self, *args, **kwargs): - super(LoadedFilesView, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.setEditTriggers( QtWidgets.QAbstractItemView.EditKeyPressed | QtWidgets.QAbstractItemView.SelectedClicked @@ -502,11 +502,11 @@ def _on_rows_inserted(self): self._update_remove_btn() def resizeEvent(self, event): - super(LoadedFilesView, self).resizeEvent(event) + super().resizeEvent(event) self._update_remove_btn() def showEvent(self, event): - super(LoadedFilesView, self).showEvent(event) + super().showEvent(event) self._model.refresh() header = self.header() header.resizeSections(QtWidgets.QHeaderView.ResizeToContents) @@ -548,7 +548,7 @@ class LoadedFilesWidget(QtWidgets.QWidget): report_changed = QtCore.Signal() def __init__(self, parent): - super(LoadedFilesWidget, self).__init__(parent) + super().__init__(parent) self.setAcceptDrops(True) @@ -598,7 +598,7 @@ class PublishReportViewerWindow(QtWidgets.QWidget): default_height = 600 def __init__(self, parent=None): - super(PublishReportViewerWindow, self).__init__(parent) + super().__init__(parent) self.setWindowTitle("Publish report viewer") icon = QtGui.QIcon(get_ayon_icon_filepath()) self.setWindowIcon(icon) diff --git a/client/ayon_core/tools/publisher/widgets/border_label_widget.py b/client/ayon_core/tools/publisher/widgets/border_label_widget.py index 324c70df34..244f9260ff 100644 --- a/client/ayon_core/tools/publisher/widgets/border_label_widget.py +++ b/client/ayon_core/tools/publisher/widgets/border_label_widget.py @@ -15,7 +15,7 @@ class _VLineWidget(QtWidgets.QWidget): It is expected that parent widget will set width. """ def __init__(self, color, line_size, left, parent): - super(_VLineWidget, self).__init__(parent) + super().__init__(parent) self._color = color self._left = left self._line_size = line_size @@ -69,7 +69,7 @@ class _HBottomLineWidget(QtWidgets.QWidget): It is expected that parent widget will set height and radius. """ def __init__(self, color, line_size, parent): - super(_HBottomLineWidget, self).__init__(parent) + super().__init__(parent) self._color = color self._radius = 0 self._line_size = line_size @@ -128,7 +128,7 @@ class _HTopCornerLineWidget(QtWidgets.QWidget): """ def __init__(self, color, line_size, left_side, parent): - super(_HTopCornerLineWidget, self).__init__(parent) + super().__init__(parent) self._left_side = left_side self._line_size = line_size self._color = color @@ -192,7 +192,7 @@ class BorderedLabelWidget(QtWidgets.QFrame): └──────────────────────┘ """ def __init__(self, label, parent): - super(BorderedLabelWidget, self).__init__(parent) + super().__init__(parent) color_value = get_objected_colors("border") color = None if color_value: @@ -269,7 +269,7 @@ def set_line_size(self, line_size): self._recalculate_sizes() def showEvent(self, event): - super(BorderedLabelWidget, self).showEvent(event) + super().showEvent(event) self._recalculate_sizes() def _recalculate_sizes(self): diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py index 3178f0c591..d67252e302 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -58,7 +58,7 @@ class BaseGroupWidget(QtWidgets.QWidget): double_clicked = QtCore.Signal() def __init__(self, group_name, parent): - super(BaseGroupWidget, self).__init__(parent) + super().__init__(parent) label_widget = QtWidgets.QLabel(group_name, self) @@ -210,7 +210,7 @@ class InstanceGroupWidget(BaseGroupWidget): active_changed = QtCore.Signal(str, str, bool) def __init__(self, group_icons, *args, **kwargs): - super(InstanceGroupWidget, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self._group_icons = group_icons @@ -280,14 +280,14 @@ class CardWidget(BaseClickableFrame): double_clicked = QtCore.Signal() def __init__(self, parent): - super(CardWidget, self).__init__(parent) + super().__init__(parent) self.setObjectName("CardViewWidget") self._selected = False self._id = None def mouseDoubleClickEvent(self, event): - super(CardWidget, self).mouseDoubleClickEvent(event) + super().mouseDoubleClickEvent(event) if self._is_valid_double_click(event): self.double_clicked.emit() @@ -335,7 +335,7 @@ class ContextCardWidget(CardWidget): """ def __init__(self, parent): - super(ContextCardWidget, self).__init__(parent) + super().__init__(parent) self._id = CONTEXT_ID self._group_identifier = CONTEXT_GROUP @@ -365,7 +365,7 @@ class ConvertorItemCardWidget(CardWidget): """ def __init__(self, item, parent): - super(ConvertorItemCardWidget, self).__init__(parent) + super().__init__(parent) self._id = item.id self.identifier = item.identifier @@ -398,7 +398,7 @@ class InstanceCardWidget(CardWidget): active_changed = QtCore.Signal(str, bool) def __init__(self, instance, group_icon, parent): - super(InstanceCardWidget, self).__init__(parent) + super().__init__(parent) self._id = instance.id self._group_identifier = instance.group_label @@ -561,7 +561,7 @@ class InstanceCardView(AbstractInstanceView): double_clicked = QtCore.Signal() def __init__(self, controller, parent): - super(InstanceCardView, self).__init__(parent) + super().__init__(parent) self._controller: AbstractPublisherFrontend = controller @@ -613,7 +613,7 @@ def sizeHint(self): + scroll_bar.sizeHint().width() ) - result = super(InstanceCardView, self).sizeHint() + result = super().sizeHint() result.setWidth(width) return result @@ -654,7 +654,7 @@ def keyPressEvent(self, event): self._toggle_instances(1) return True - return super(InstanceCardView, self).keyPressEvent(event) + return super().keyPressEvent(event) def _get_selected_widgets(self): output = [] diff --git a/client/ayon_core/tools/publisher/widgets/create_widget.py b/client/ayon_core/tools/publisher/widgets/create_widget.py index af6fee533e..479a63ebc9 100644 --- a/client/ayon_core/tools/publisher/widgets/create_widget.py +++ b/client/ayon_core/tools/publisher/widgets/create_widget.py @@ -35,14 +35,14 @@ class ResizeControlWidget(QtWidgets.QWidget): resized = QtCore.Signal() def resizeEvent(self, event): - super(ResizeControlWidget, self).resizeEvent(event) + super().resizeEvent(event) self.resized.emit() # TODO add creator identifier/label to details class CreatorShortDescWidget(QtWidgets.QWidget): def __init__(self, parent=None): - super(CreatorShortDescWidget, self).__init__(parent=parent) + super().__init__(parent=parent) # --- Short description widget --- icon_widget = IconValuePixmapLabel(None, self) @@ -100,13 +100,13 @@ def lessThan(self, left, right): l_show_order = left.data(CREATOR_SORT_ROLE) r_show_order = right.data(CREATOR_SORT_ROLE) if l_show_order == r_show_order: - return super(CreatorsProxyModel, self).lessThan(left, right) + return super().lessThan(left, right) return l_show_order < r_show_order class CreateWidget(QtWidgets.QWidget): def __init__(self, controller, parent=None): - super(CreateWidget, self).__init__(parent) + super().__init__(parent) self._controller: AbstractPublisherFrontend = controller @@ -755,7 +755,7 @@ def _on_first_show(self): self._creators_splitter.setSizes([part, rem_width]) def showEvent(self, event): - super(CreateWidget, self).showEvent(event) + super().showEvent(event) if self._first_show: self._first_show = False self._on_first_show() diff --git a/client/ayon_core/tools/publisher/widgets/folders_dialog.py b/client/ayon_core/tools/publisher/widgets/folders_dialog.py index fa9a174c9c..d2eb68310e 100644 --- a/client/ayon_core/tools/publisher/widgets/folders_dialog.py +++ b/client/ayon_core/tools/publisher/widgets/folders_dialog.py @@ -40,7 +40,7 @@ class FoldersDialog(QtWidgets.QDialog): """Dialog to select folder for a context of instance.""" def __init__(self, controller, parent): - super(FoldersDialog, self).__init__(parent) + super().__init__(parent) self.setWindowTitle("Select folder") filter_input = PlaceholderLineEdit(self) @@ -105,7 +105,7 @@ def _on_controller_reset(self): def showEvent(self, event): """Refresh folders widget on show.""" - super(FoldersDialog, self).showEvent(event) + super().showEvent(event) if self._first_show: self._first_show = False self._on_first_show() diff --git a/client/ayon_core/tools/publisher/widgets/help_widget.py b/client/ayon_core/tools/publisher/widgets/help_widget.py index 062b117220..40f8b255dd 100644 --- a/client/ayon_core/tools/publisher/widgets/help_widget.py +++ b/client/ayon_core/tools/publisher/widgets/help_widget.py @@ -12,7 +12,7 @@ class HelpButton(QtWidgets.QPushButton): """Button used to trigger help dialog.""" def __init__(self, parent): - super(HelpButton, self).__init__(parent) + super().__init__(parent) self.setObjectName("CreateDialogHelpButton") self.setText("?") @@ -21,7 +21,7 @@ class HelpWidget(QtWidgets.QWidget): """Widget showing help for single functionality.""" def __init__(self, parent): - super(HelpWidget, self).__init__(parent) + super().__init__(parent) # TODO add hints what to help with? detail_description_input = QtWidgets.QTextEdit(self) @@ -59,7 +59,7 @@ class HelpDialog(QtWidgets.QDialog): def __init__( self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget ): - super(HelpDialog, self).__init__(parent) + super().__init__(parent) self.setWindowTitle("Help dialog") @@ -84,5 +84,5 @@ def set_detailed_text(self, text=None): self._help_content.set_detailed_text(text) def showEvent(self, event): - super(HelpDialog, self).showEvent(event) + super().showEvent(event) self.resize(self.default_width, self.default_height) diff --git a/client/ayon_core/tools/publisher/widgets/list_view_widgets.py b/client/ayon_core/tools/publisher/widgets/list_view_widgets.py index 74fd3265cc..930d6bb88c 100644 --- a/client/ayon_core/tools/publisher/widgets/list_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/list_view_widgets.py @@ -58,7 +58,7 @@ class ListItemDelegate(QtWidgets.QStyledItemDelegate): radius_ratio = 0.3 def __init__(self, parent): - super(ListItemDelegate, self).__init__(parent) + super().__init__(parent) group_color_info = get_objected_colors("publisher", "list-view-group") @@ -71,7 +71,7 @@ def paint(self, painter, option, index): if index.data(IS_GROUP_ROLE): self.group_item_paint(painter, option, index) else: - super(ListItemDelegate, self).paint(painter, option, index) + super().paint(painter, option, index) def group_item_paint(self, painter, option, index): """Paint group item.""" @@ -116,7 +116,7 @@ class InstanceListItemWidget(QtWidgets.QWidget): double_clicked = QtCore.Signal() def __init__(self, instance, parent): - super(InstanceListItemWidget, self).__init__(parent) + super().__init__(parent) self.instance = instance @@ -155,7 +155,7 @@ def __init__(self, instance, parent): def mouseDoubleClickEvent(self, event): widget = self.childAt(event.pos()) - super(InstanceListItemWidget, self).mouseDoubleClickEvent(event) + super().mouseDoubleClickEvent(event) if widget is not self._active_checkbox: self.double_clicked.emit() @@ -222,7 +222,7 @@ class ListContextWidget(QtWidgets.QFrame): double_clicked = QtCore.Signal() def __init__(self, parent): - super(ListContextWidget, self).__init__(parent) + super().__init__(parent) label_widget = QtWidgets.QLabel(CONTEXT_LABEL, self) @@ -238,7 +238,7 @@ def __init__(self, parent): self.label_widget = label_widget def mouseDoubleClickEvent(self, event): - super(ListContextWidget, self).mouseDoubleClickEvent(event) + super().mouseDoubleClickEvent(event) self.double_clicked.emit() @@ -252,7 +252,7 @@ class InstanceListGroupWidget(QtWidgets.QFrame): toggle_requested = QtCore.Signal(str, int) def __init__(self, group_name, parent): - super(InstanceListGroupWidget, self).__init__(parent) + super().__init__(parent) self.setObjectName("InstanceListGroupWidget") self.group_name = group_name @@ -336,7 +336,7 @@ class InstanceTreeView(QtWidgets.QTreeView): double_clicked = QtCore.Signal() def __init__(self, *args, **kwargs): - super(InstanceTreeView, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.setObjectName("InstanceListView") self.setHeaderHidden(True) @@ -387,7 +387,7 @@ def event(self, event): self.toggle_requested.emit(1) return True - return super(InstanceTreeView, self).event(event) + return super().event(event) def _mouse_press(self, event): """Store index of pressed group. @@ -407,11 +407,11 @@ def _mouse_press(self, event): def mousePressEvent(self, event): self._mouse_press(event) - super(InstanceTreeView, self).mousePressEvent(event) + super().mousePressEvent(event) def mouseDoubleClickEvent(self, event): self._mouse_press(event) - super(InstanceTreeView, self).mouseDoubleClickEvent(event) + super().mouseDoubleClickEvent(event) def _mouse_release(self, event, pressed_index): if event.button() != QtCore.Qt.LeftButton: @@ -434,7 +434,7 @@ def mouseReleaseEvent(self, event): self._pressed_group_index = None result = self._mouse_release(event, pressed_index) if not result: - super(InstanceTreeView, self).mouseReleaseEvent(event) + super().mouseReleaseEvent(event) class InstanceListView(AbstractInstanceView): diff --git a/client/ayon_core/tools/publisher/widgets/precreate_widget.py b/client/ayon_core/tools/publisher/widgets/precreate_widget.py index ae0deb8410..5ad203d370 100644 --- a/client/ayon_core/tools/publisher/widgets/precreate_widget.py +++ b/client/ayon_core/tools/publisher/widgets/precreate_widget.py @@ -7,7 +7,7 @@ class PreCreateWidget(QtWidgets.QWidget): def __init__(self, parent): - super(PreCreateWidget, self).__init__(parent) + super().__init__(parent) # Precreate attribute defininitions of Creator scroll_area = QtWidgets.QScrollArea(self) @@ -79,7 +79,7 @@ def set_creator_item(self, creator_item): class AttributesWidget(QtWidgets.QWidget): def __init__(self, parent=None): - super(AttributesWidget, self).__init__(parent) + super().__init__(parent) layout = QtWidgets.QGridLayout(self) layout.setContentsMargins(0, 0, 0, 0) diff --git a/client/ayon_core/tools/publisher/widgets/publish_frame.py b/client/ayon_core/tools/publisher/widgets/publish_frame.py index 4041a58ec7..6eaeb6daf2 100644 --- a/client/ayon_core/tools/publisher/widgets/publish_frame.py +++ b/client/ayon_core/tools/publisher/widgets/publish_frame.py @@ -215,7 +215,7 @@ def __init__( self._last_plugin_label = None def mouseReleaseEvent(self, event): - super(PublishFrame, self).mouseReleaseEvent(event) + super().mouseReleaseEvent(event) self._change_shrunk_state() def _change_shrunk_state(self): diff --git a/client/ayon_core/tools/publisher/widgets/report_page.py b/client/ayon_core/tools/publisher/widgets/report_page.py index 896e8f93af..ecf1376ec0 100644 --- a/client/ayon_core/tools/publisher/widgets/report_page.py +++ b/client/ayon_core/tools/publisher/widgets/report_page.py @@ -52,7 +52,7 @@ class VerticalScrollArea(QtWidgets.QScrollArea): """ def __init__(self, *args, **kwargs): - super(VerticalScrollArea, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) @@ -82,7 +82,7 @@ def setVerticalScrollBar(self, widget): if old_widget: old_widget.removeEventFilter(self) - super(VerticalScrollArea, self).setVerticalScrollBar(widget) + super().setVerticalScrollBar(widget) if widget: widget.installEventFilter(self) @@ -91,7 +91,7 @@ def setWidget(self, widget): if old_widget: old_widget.removeEventFilter(self) - super(VerticalScrollArea, self).setWidget(widget) + super().setWidget(widget) if widget: widget.installEventFilter(self) @@ -107,7 +107,7 @@ def eventFilter(self, obj, event): and (obj is self.widget() or obj is self.verticalScrollBar()) ): self._size_changed_timer.start() - return super(VerticalScrollArea, self).eventFilter(obj, event) + return super().eventFilter(obj, event) # --- Publish actions widget --- @@ -124,7 +124,7 @@ class ActionButton(BaseClickableFrame): action_clicked = QtCore.Signal(str, str) def __init__(self, plugin_action_item, parent): - super(ActionButton, self).__init__(parent) + super().__init__(parent) self.setObjectName("ValidationActionButton") @@ -250,7 +250,7 @@ class ValidationErrorInstanceList(QtWidgets.QListView): Instances are collected per plugin's validation error title. """ def __init__(self, *args, **kwargs): - super(ValidationErrorInstanceList, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.setObjectName("ValidationErrorInstanceList") @@ -261,7 +261,7 @@ def minimumSizeHint(self): return self.sizeHint() def sizeHint(self): - result = super(ValidationErrorInstanceList, self).sizeHint() + result = super().sizeHint() row_count = self.model().rowCount() height = 0 if row_count > 0: @@ -284,7 +284,7 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget): instance_changed = QtCore.Signal(str) def __init__(self, title_id, error_info, parent): - super(ValidationErrorTitleWidget, self).__init__(parent) + super().__init__(parent) self._title_id = title_id self._error_info = error_info @@ -375,7 +375,7 @@ def __init__(self, title_id, error_info, parent): self._expanded = False def sizeHint(self): - result = super(ValidationErrorTitleWidget, self).sizeHint() + result = super().sizeHint() expected_width = max( self._view_widget.minimumSizeHint().width(), self._view_widget.sizeHint().width() @@ -479,7 +479,7 @@ def get_available_instances(self): class ValidationArtistMessage(QtWidgets.QWidget): def __init__(self, message, parent): - super(ValidationArtistMessage, self).__init__(parent) + super().__init__(parent) artist_msg_label = QtWidgets.QLabel(message, self) artist_msg_label.setAlignment(QtCore.Qt.AlignCenter) @@ -495,7 +495,7 @@ class ValidationErrorsView(QtWidgets.QWidget): selection_changed = QtCore.Signal() def __init__(self, parent): - super(ValidationErrorsView, self).__init__(parent) + super().__init__(parent) errors_scroll = VerticalScrollArea(self) errors_scroll.setWidgetResizable(True) @@ -719,7 +719,7 @@ def extract_basic_log_info(logs): class FamilyGroupLabel(QtWidgets.QWidget): def __init__(self, family, parent): - super(FamilyGroupLabel, self).__init__(parent) + super().__init__(parent) self.setLayoutDirection(QtCore.Qt.LeftToRight) @@ -747,7 +747,7 @@ class PublishInstanceCardWidget(BaseClickableFrame): _in_progress_pix = None def __init__(self, instance, icon, publish_can_continue, parent): - super(PublishInstanceCardWidget, self).__init__(parent) + super().__init__(parent) self.setObjectName("CardViewWidget") @@ -933,7 +933,7 @@ def sizeHint(self): + scroll_bar.sizeHint().width() ) - result = super(PublishInstancesViewWidget, self).sizeHint() + result = super().sizeHint() result.setWidth(width) return result @@ -1045,7 +1045,7 @@ class LogIconFrame(QtWidgets.QFrame): _validation_error_pix = None def __init__(self, parent, log_type, log_level, is_validation_error): - super(LogIconFrame, self).__init__(parent) + super().__init__(parent) self.setAttribute(QtCore.Qt.WA_TranslucentBackground) @@ -1113,7 +1113,7 @@ class LogItemWidget(QtWidgets.QWidget): } def __init__(self, log, parent): - super(LogItemWidget, self).__init__(parent) + super().__init__(parent) type_flag, level_n = self._get_log_info(log) icon_label = LogIconFrame( @@ -1190,7 +1190,7 @@ class LogsWithIconsView(QtWidgets.QWidget): """ def __init__(self, logs, parent): - super(LogsWithIconsView, self).__init__(parent) + super().__init__(parent) self.setAttribute(QtCore.Qt.WA_TranslucentBackground) logs_layout = QtWidgets.QVBoxLayout(self) @@ -1270,7 +1270,7 @@ class InstanceLogsWidget(QtWidgets.QWidget): """ def __init__(self, instance, parent): - super(InstanceLogsWidget, self).__init__(parent) + super().__init__(parent) self.setAttribute(QtCore.Qt.WA_TranslucentBackground) @@ -1301,7 +1301,7 @@ class InstancesLogsView(QtWidgets.QFrame): """Publish instances logs view widget.""" def __init__(self, parent): - super(InstancesLogsView, self).__init__(parent) + super().__init__(parent) self.setObjectName("InstancesLogsView") scroll_area = QtWidgets.QScrollArea(self) @@ -1354,12 +1354,12 @@ def __init__(self, parent): self._plugin_ids_filter = None def showEvent(self, event): - super(InstancesLogsView, self).showEvent(event) + super().showEvent(event) self._is_showed = True self._update_instances() def hideEvent(self, event): - super(InstancesLogsView, self).hideEvent(event) + super().hideEvent(event) self._is_showed = False def closeEvent(self, event): @@ -1519,7 +1519,7 @@ def _on_save_to_disk_click(self): class ErrorDetailsWidget(QtWidgets.QWidget): def __init__(self, parent): - super(ErrorDetailsWidget, self).__init__(parent) + super().__init__(parent) inputs_widget = QtWidgets.QWidget(self) # Error 'Description' input diff --git a/client/ayon_core/tools/publisher/widgets/screenshot_widget.py b/client/ayon_core/tools/publisher/widgets/screenshot_widget.py index 37b958c1c7..08a0a790b7 100644 --- a/client/ayon_core/tools/publisher/widgets/screenshot_widget.py +++ b/client/ayon_core/tools/publisher/widgets/screenshot_widget.py @@ -15,7 +15,7 @@ class ScreenMarquee(QtWidgets.QDialog): """ def __init__(self, parent=None): - super(ScreenMarquee, self).__init__(parent=parent) + super().__init__(parent=parent) self.setWindowFlags( QtCore.Qt.Window @@ -138,7 +138,7 @@ def keyPressEvent(self, event): event.accept() self.close() return - return super(ScreenMarquee, self).keyPressEvent(event) + return super().keyPressEvent(event) def showEvent(self, event): self._fit_screen_geometry() diff --git a/client/ayon_core/tools/publisher/widgets/tabs_widget.py b/client/ayon_core/tools/publisher/widgets/tabs_widget.py index e484dc8681..cd2f927f8c 100644 --- a/client/ayon_core/tools/publisher/widgets/tabs_widget.py +++ b/client/ayon_core/tools/publisher/widgets/tabs_widget.py @@ -6,7 +6,7 @@ class PublisherTabBtn(QtWidgets.QPushButton): tab_clicked = QtCore.Signal(str) def __init__(self, identifier, label, parent): - super(PublisherTabBtn, self).__init__(label, parent) + super().__init__(label, parent) self._identifier = identifier self._active = False @@ -36,7 +36,7 @@ class PublisherTabsWidget(QtWidgets.QFrame): tab_changed = QtCore.Signal(str, str) def __init__(self, parent=None): - super(PublisherTabsWidget, self).__init__(parent) + super().__init__(parent) btns_widget = QtWidgets.QWidget(self) btns_layout = QtWidgets.QHBoxLayout(btns_widget) diff --git a/client/ayon_core/tools/publisher/widgets/thumbnail_widget.py b/client/ayon_core/tools/publisher/widgets/thumbnail_widget.py index de7af80d50..261dcfb43d 100644 --- a/client/ayon_core/tools/publisher/widgets/thumbnail_widget.py +++ b/client/ayon_core/tools/publisher/widgets/thumbnail_widget.py @@ -37,7 +37,7 @@ class ThumbnailPainterWidget(QtWidgets.QWidget): checker_boxes_count = 20 def __init__(self, parent): - super(ThumbnailPainterWidget, self).__init__(parent) + super().__init__(parent) border_color = get_objected_colors("bg-buttons").get_qcolor() thumbnail_bg_color = get_objected_colors("bg-view").get_qcolor() @@ -307,7 +307,7 @@ def __init__( ): # Missing implementation for thumbnail # - widget kept to make a visial offset of global attr widget offset - super(ThumbnailWidget, self).__init__(parent) + super().__init__(parent) self.setAcceptDrops(True) thumbnail_painter = ThumbnailPainterWidget(self) @@ -575,12 +575,12 @@ def _update_buttons_position(self): ) def resizeEvent(self, event): - super(ThumbnailWidget, self).resizeEvent(event) + super().resizeEvent(event) self._adapt_to_size() self._update_buttons_position() def showEvent(self, event): - super(ThumbnailWidget, self).showEvent(event) + super().showEvent(event) self._adapt_to_size() self._update_buttons_position() diff --git a/client/ayon_core/tools/publisher/widgets/widgets.py b/client/ayon_core/tools/publisher/widgets/widgets.py index 9e1d2d0525..1f782ddc67 100644 --- a/client/ayon_core/tools/publisher/widgets/widgets.py +++ b/client/ayon_core/tools/publisher/widgets/widgets.py @@ -95,7 +95,7 @@ class IconValuePixmapLabel(PublishPixmapLabel): def __init__(self, icon_def, parent): source_pixmap = self._parse_icon_def(icon_def) - super(IconValuePixmapLabel, self).__init__(source_pixmap, parent) + super().__init__(source_pixmap, parent) def set_icon_def(self, icon_def): """Set icon by it's definition name. @@ -123,7 +123,7 @@ class ContextWarningLabel(PublishPixmapLabel): def __init__(self, parent): pix = get_pixmap("warning") - super(ContextWarningLabel, self).__init__(pix, parent) + super().__init__(pix, parent) self.setToolTip( "Contain invalid context. Please check details." @@ -146,7 +146,7 @@ class PublishIconBtn(IconButton): """ def __init__(self, pixmap_path, *args, **kwargs): - super(PublishIconBtn, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) colors = get_objected_colors() icon = self.generate_icon( @@ -209,7 +209,7 @@ class CreateBtn(PublishIconBtn): def __init__(self, parent=None): icon_path = get_icon_path("create") - super(CreateBtn, self).__init__(icon_path, "Create", parent) + super().__init__(icon_path, "Create", parent) self.setToolTip("Create new product/s") self.setLayoutDirection(QtCore.Qt.RightToLeft) @@ -218,7 +218,7 @@ class SaveBtn(PublishIconBtn): """Save context and instances information.""" def __init__(self, parent=None): icon_path = get_icon_path("save") - super(SaveBtn, self).__init__(icon_path, parent) + super().__init__(icon_path, parent) self.setToolTip( "Save changes ({})".format( QtGui.QKeySequence(QtGui.QKeySequence.Save).toString() @@ -230,7 +230,7 @@ class ResetBtn(PublishIconBtn): """Publish reset button.""" def __init__(self, parent=None): icon_path = get_icon_path("refresh") - super(ResetBtn, self).__init__(icon_path, parent) + super().__init__(icon_path, parent) self.setToolTip( "Reset & discard changes ({})".format(ResetKeySequence.toString()) ) @@ -240,7 +240,7 @@ class StopBtn(PublishIconBtn): """Publish stop button.""" def __init__(self, parent): icon_path = get_icon_path("stop") - super(StopBtn, self).__init__(icon_path, parent) + super().__init__(icon_path, parent) self.setToolTip("Stop/Pause publishing") @@ -248,7 +248,7 @@ class ValidateBtn(PublishIconBtn): """Publish validate button.""" def __init__(self, parent=None): icon_path = get_icon_path("validate") - super(ValidateBtn, self).__init__(icon_path, parent) + super().__init__(icon_path, parent) self.setToolTip("Validate") @@ -256,7 +256,7 @@ class PublishBtn(PublishIconBtn): """Publish start publish button.""" def __init__(self, parent=None): icon_path = get_icon_path("play") - super(PublishBtn, self).__init__(icon_path, "Publish", parent) + super().__init__(icon_path, "Publish", parent) self.setToolTip("Publish") @@ -264,7 +264,7 @@ class CreateInstanceBtn(PublishIconBtn): """Create add button.""" def __init__(self, parent=None): icon_path = get_icon_path("add") - super(CreateInstanceBtn, self).__init__(icon_path, parent) + super().__init__(icon_path, parent) self.setToolTip("Create new instance") @@ -275,7 +275,7 @@ class PublishReportBtn(PublishIconBtn): def __init__(self, parent=None): icon_path = get_icon_path("view_report") - super(PublishReportBtn, self).__init__(icon_path, parent) + super().__init__(icon_path, parent) self.setToolTip("Copy report") self._actions = [] @@ -288,7 +288,7 @@ def _on_action_trigger(self, identifier): self.triggered.emit(identifier) def mouseReleaseEvent(self, event): - super(PublishReportBtn, self).mouseReleaseEvent(event) + super().mouseReleaseEvent(event) menu = QtWidgets.QMenu(self) actions = [] for item in self._actions: @@ -306,7 +306,7 @@ class RemoveInstanceBtn(PublishIconBtn): """Create remove button.""" def __init__(self, parent=None): icon_path = resources.get_icon_path("delete") - super(RemoveInstanceBtn, self).__init__(icon_path, parent) + super().__init__(icon_path, parent) self.setToolTip("Remove selected instances") @@ -314,7 +314,7 @@ class ChangeViewBtn(PublishIconBtn): """Create toggle view button.""" def __init__(self, parent=None): icon_path = get_icon_path("change_view") - super(ChangeViewBtn, self).__init__(icon_path, parent) + super().__init__(icon_path, parent) self.setToolTip("Swap between views") @@ -403,7 +403,7 @@ class ClickableLineEdit(QtWidgets.QLineEdit): clicked = QtCore.Signal() def __init__(self, *args, **kwargs): - super(ClickableLineEdit, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.setReadOnly(True) self._mouse_pressed = False @@ -588,7 +588,7 @@ def confirm_value(self): class TasksComboboxProxy(QtCore.QSortFilterProxyModel): def __init__(self, *args, **kwargs): - super(TasksComboboxProxy, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self._filter_empty = False def set_filter_empty(self, filter_empty): @@ -900,7 +900,7 @@ class VariantInputWidget(PlaceholderLineEdit): value_changed = QtCore.Signal() def __init__(self, parent): - super(VariantInputWidget, self).__init__(parent) + super().__init__(parent) self.setObjectName("VariantInput") self.setToolTip(VARIANT_TOOLTIP) @@ -1011,7 +1011,7 @@ class MultipleItemWidget(QtWidgets.QWidget): """ def __init__(self, parent): - super(MultipleItemWidget, self).__init__(parent) + super().__init__(parent) model = QtGui.QStandardItemModel() @@ -1051,7 +1051,7 @@ def _update_size(self): self.setMaximumHeight(height + (2 * self._view.spacing())) def showEvent(self, event): - super(MultipleItemWidget, self).showEvent(event) + super().showEvent(event) tmp_item = None if not self._value: # Add temp item to be able calculate maximum height of widget @@ -1063,7 +1063,7 @@ def showEvent(self, event): self._model.clear() def resizeEvent(self, event): - super(MultipleItemWidget, self).resizeEvent(event) + super().resizeEvent(event) self._update_size() def set_value(self, value=None): @@ -1893,7 +1893,7 @@ class CreateNextPageOverlay(QtWidgets.QWidget): clicked = QtCore.Signal() def __init__(self, parent): - super(CreateNextPageOverlay, self).__init__(parent) + super().__init__(parent) self.setCursor(QtCore.Qt.PointingHandCursor) self._arrow_color = ( get_objected_colors("font").get_qcolor() @@ -1983,7 +1983,7 @@ def _check_anim_timer(self): def mousePressEvent(self, event): if event.button() == QtCore.Qt.LeftButton: self._mouse_pressed = True - super(CreateNextPageOverlay, self).mousePressEvent(event) + super().mousePressEvent(event) def mouseReleaseEvent(self, event): if self._mouse_pressed: @@ -1991,7 +1991,7 @@ def mouseReleaseEvent(self, event): if self.rect().contains(event.pos()): self.clicked.emit() - super(CreateNextPageOverlay, self).mouseReleaseEvent(event) + super().mouseReleaseEvent(event) def paintEvent(self, event): painter = QtGui.QPainter() diff --git a/client/ayon_core/tools/publisher/window.py b/client/ayon_core/tools/publisher/window.py index 9d3a033acd..1218221420 100644 --- a/client/ayon_core/tools/publisher/window.py +++ b/client/ayon_core/tools/publisher/window.py @@ -445,7 +445,7 @@ def make_sure_is_visible(self): def showEvent(self, event): self._window_is_visible = True - super(PublisherWindow, self).showEvent(event) + super().showEvent(event) if self._first_show: self._first_show = False self._on_first_show() @@ -453,7 +453,7 @@ def showEvent(self, event): self._show_timer.start() def resizeEvent(self, event): - super(PublisherWindow, self).resizeEvent(event) + super().resizeEvent(event) self._update_publish_frame_rect() self._update_create_overlay_size() @@ -469,16 +469,16 @@ def closeEvent(self, event): # Trigger custom event that should be captured only in UI # - backend (controller) must not be dependent on this event topic!!! self._controller.emit_event("main.window.closed", {}, "window") - super(PublisherWindow, self).closeEvent(event) + super().closeEvent(event) def leaveEvent(self, event): - super(PublisherWindow, self).leaveEvent(event) + super().leaveEvent(event) self._update_create_overlay_visibility() def eventFilter(self, obj, event): if event.type() == QtCore.QEvent.MouseMove: self._update_create_overlay_visibility(event.globalPos()) - return super(PublisherWindow, self).eventFilter(obj, event) + return super().eventFilter(obj, event) def _install_app_event_listener(self): if self._app_event_listener_installed: @@ -533,7 +533,7 @@ def keyPressEvent(self, event): event.accept() return - super(PublisherWindow, self).keyPressEvent(event) + super().keyPressEvent(event) def _on_overlay_message(self, event): self._overlay_object.add_message( @@ -1087,7 +1087,7 @@ def __init__(self, error_title, failed_info, message_start, parent): self._tabs_widget = None self._stack_layout = None - super(ErrorsMessageBox, self).__init__(error_title, parent) + super().__init__(error_title, parent) layout = self.layout() layout.setContentsMargins(0, 0, 0, 0) From 4dbb71372616dc6d23fbcbc54c95950b0057e932 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 24 Jun 2024 14:07:29 +0200 Subject: [PATCH 15/15] fix typehint import --- client/ayon_core/tools/publisher/abstract.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/publisher/abstract.py b/client/ayon_core/tools/publisher/abstract.py index 447fa5607f..a9142396f5 100644 --- a/client/ayon_core/tools/publisher/abstract.py +++ b/client/ayon_core/tools/publisher/abstract.py @@ -1,5 +1,15 @@ from abc import ABC, abstractmethod -from typing import Optional, Dict, List, Tuple, Any, Callable, Union, Iterable +from typing import ( + Optional, + Dict, + List, + Tuple, + Any, + Callable, + Union, + Iterable, + TYPE_CHECKING, +) from ayon_core.lib import AbstractAttrDef from ayon_core.host import HostBase @@ -12,7 +22,8 @@ TaskTypeItem, ) -from .models import CreatorItem +if TYPE_CHECKING: + from .models import CreatorItem class CardMessageTypes: @@ -248,7 +259,7 @@ def are_folder_paths_valid(self, folder_paths: Iterable[str]) -> bool: # --- Create --- @abstractmethod - def get_creator_items(self) -> Dict[str, CreatorItem]: + def get_creator_items(self) -> Dict[str, "CreatorItem"]: """Creator items by identifier. Returns: