Merge branch 'develop' into enhancement/product-name-template-settings

This commit is contained in:
Jakub Trllo 2025-12-17 14:50:24 +01:00 committed by GitHub
commit 4051d679dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 251 additions and 51 deletions

View file

@ -29,6 +29,7 @@ from .lib import (
get_publish_template_name,
publish_plugins_discover,
filter_crashed_publish_paths,
load_help_content_from_plugin,
load_help_content_from_filepath,
@ -87,6 +88,7 @@ __all__ = (
"get_publish_template_name",
"publish_plugins_discover",
"filter_crashed_publish_paths",
"load_help_content_from_plugin",
"load_help_content_from_filepath",

View file

@ -1,6 +1,8 @@
"""Library functions for publishing."""
from __future__ import annotations
import os
import platform
import re
import sys
import inspect
import copy
@ -8,19 +10,19 @@ import warnings
import hashlib
import xml.etree.ElementTree
from typing import TYPE_CHECKING, Optional, Union, List, Any
import clique
import speedcopy
import logging
import pyblish.util
import pyblish.plugin
import pyblish.api
from ayon_api import (
get_server_api_connection,
get_representations,
get_last_version_by_product_name
)
import clique
import pyblish.util
import pyblish.plugin
import pyblish.api
import speedcopy
from ayon_core.lib import (
import_filepath,
Logger,
@ -246,6 +248,67 @@ def load_help_content_from_plugin(
return load_help_content_from_filepath(filepath)
def filter_crashed_publish_paths(
project_name: str,
crashed_paths: set[str],
*,
project_settings: Optional[dict[str, Any]] = None,
) -> set[str]:
"""Filter crashed paths happened during plugins discovery.
Check if plugins discovery has enabled strict mode and filter crashed
paths that happened during discover based on regexes from settings.
Publishing should not start if any paths are returned.
Args:
project_name (str): Project name in which context plugins discovery
happened.
crashed_paths (set[str]): Crashed paths from plugins discovery report.
project_settings (Optional[dict[str, Any]]): Project settings.
Returns:
set[str]: Filtered crashed paths.
"""
filtered_paths = set()
# Nothing crashed all good...
if not crashed_paths:
return filtered_paths
if project_settings is None:
project_settings = get_project_settings(project_name)
discover_validation = (
project_settings["core"]["tools"]["publish"]["discover_validation"]
)
# Strict mode is not enabled.
if not discover_validation["enabled"]:
return filtered_paths
regexes = [
re.compile(value, re.IGNORECASE)
for value in discover_validation["ignore_paths"]
if value
]
is_windows = platform.system().lower() == "windows"
# Fitler path with regexes from settings
for path in crashed_paths:
# Normalize paths to use forward slashes on windows
if is_windows:
path = path.replace("\\", "/")
is_invalid = True
for regex in regexes:
if regex.match(path):
is_invalid = False
break
if is_invalid:
filtered_paths.add(path)
return filtered_paths
def publish_plugins_discover(
paths: Optional[list[str]] = None) -> DiscoverResult:
"""Find and return available pyblish plug-ins.
@ -1099,14 +1162,16 @@ def main_cli_publish(
except ValueError:
pass
context = get_global_context()
project_settings = get_project_settings(context["project_name"])
install_ayon_plugins()
if addons_manager is None:
addons_manager = AddonsManager()
addons_manager = AddonsManager(project_settings)
applications_addon = addons_manager.get_enabled_addon("applications")
if applications_addon is not None:
context = get_global_context()
env = applications_addon.get_farm_publish_environment_variables(
context["project_name"],
context["folder_path"],
@ -1129,17 +1194,33 @@ def main_cli_publish(
log.info("Running publish ...")
discover_result = publish_plugins_discover()
publish_plugins = discover_result.plugins
print(discover_result.get_report(only_errors=False))
filtered_crashed_paths = filter_crashed_publish_paths(
context["project_name"],
set(discover_result.crashed_file_paths),
project_settings=project_settings,
)
if filtered_crashed_paths:
joined_paths = "\n".join([
f"- {path}"
for path in filtered_crashed_paths
])
log.error(
"Plugin discovery strict mode is enabled."
" Crashed plugin paths that prevent from publishing:"
f"\n{joined_paths}"
)
sys.exit(1)
publish_plugins = discover_result.plugins
# Error exit as soon as any error occurs.
error_format = ("Failed {plugin.__name__}: "
"{error} -- {error.traceback}")
error_format = "Failed {plugin.__name__}: {error} -- {error.traceback}"
for result in pyblish.util.publish_iter(plugins=publish_plugins):
if result["error"]:
log.error(error_format.format(**result))
# uninstall()
sys.exit(1)
log.info("Publish finished.")

View file

@ -21,6 +21,7 @@ from ayon_core.pipeline.plugin_discover import DiscoverResult
from ayon_core.pipeline.publish import (
get_publish_instance_label,
PublishError,
filter_crashed_publish_paths,
)
from ayon_core.tools.publisher.abstract import AbstractPublisherBackend
@ -107,11 +108,14 @@ class PublishReportMaker:
creator_discover_result: Optional[DiscoverResult] = None,
convertor_discover_result: Optional[DiscoverResult] = None,
publish_discover_result: Optional[DiscoverResult] = None,
blocking_crashed_paths: Optional[list[str]] = 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._blocking_crashed_paths: list[str] = []
self._all_instances_by_id: Dict[str, pyblish.api.Instance] = {}
self._plugin_data_by_id: Dict[str, Any] = {}
self._current_plugin_id: Optional[str] = None
@ -120,6 +124,7 @@ class PublishReportMaker:
creator_discover_result,
convertor_discover_result,
publish_discover_result,
blocking_crashed_paths,
)
def reset(
@ -127,12 +132,14 @@ class PublishReportMaker:
creator_discover_result: Union[DiscoverResult, None],
convertor_discover_result: Union[DiscoverResult, None],
publish_discover_result: Union[DiscoverResult, None],
blocking_crashed_paths: list[str],
):
"""Reset report and clear all data."""
self._create_discover_result = creator_discover_result
self._convert_discover_result = convertor_discover_result
self._publish_discover_result = publish_discover_result
self._blocking_crashed_paths = blocking_crashed_paths
self._all_instances_by_id = {}
self._plugin_data_by_id = {}
@ -242,9 +249,10 @@ class PublishReportMaker:
"instances": instances_details,
"context": self._extract_context_data(publish_context),
"crashed_file_paths": crashed_file_paths,
"blocking_crashed_paths": list(self._blocking_crashed_paths),
"id": uuid.uuid4().hex,
"created_at": now.isoformat(),
"report_version": "1.1.0",
"report_version": "1.1.1",
}
def _add_plugin_data_item(self, plugin: pyblish.api.Plugin):
@ -959,11 +967,16 @@ class PublishModel:
self._publish_plugins_proxy = PublishPluginsProxy(
publish_plugins
)
blocking_crashed_paths = filter_crashed_publish_paths(
create_context.get_current_project_name(),
set(create_context.publish_discover_result.crashed_file_paths),
project_settings=create_context.get_current_project_settings(),
)
self._publish_report.reset(
create_context.creator_discover_result,
create_context.convertor_discover_result,
create_context.publish_discover_result,
blocking_crashed_paths,
)
for plugin in create_context.publish_plugins_mismatch_targets:
self._publish_report.set_plugin_skipped(plugin.id)

View file

@ -139,3 +139,6 @@ class PublishReport:
self.logs = logs
self.crashed_plugin_paths = report_data["crashed_file_paths"]
self.blocking_crashed_paths = report_data.get(
"blocking_crashed_paths", []
)

View file

@ -7,6 +7,7 @@ from ayon_core.tools.utils import (
SeparatorWidget,
IconButton,
paint_image_with_color,
get_qt_icon,
)
from ayon_core.resources import get_image_path
from ayon_core.style import get_objected_colors
@ -46,10 +47,13 @@ def get_pretty_milliseconds(value):
class PluginLoadReportModel(QtGui.QStandardItemModel):
_blocking_icon = None
def __init__(self):
super().__init__()
self._traceback_by_filepath = {}
self._items_by_filepath = {}
self._blocking_crashed_paths = set()
self._is_active = True
self._need_refresh = False
@ -75,6 +79,7 @@ class PluginLoadReportModel(QtGui.QStandardItemModel):
for filepath in to_remove:
self._traceback_by_filepath.pop(filepath)
self._blocking_crashed_paths = set(report.blocking_crashed_paths)
self._update_items()
def _update_items(self):
@ -83,6 +88,7 @@ class PluginLoadReportModel(QtGui.QStandardItemModel):
parent = self.invisibleRootItem()
if not self._traceback_by_filepath:
parent.removeRows(0, parent.rowCount())
self._items_by_filepath = {}
return
new_items = []
@ -91,12 +97,18 @@ class PluginLoadReportModel(QtGui.QStandardItemModel):
set(self._items_by_filepath) - set(self._traceback_by_filepath)
)
for filepath in self._traceback_by_filepath:
if filepath in self._items_by_filepath:
continue
item = QtGui.QStandardItem(filepath)
new_items.append(item)
new_items_by_filepath[filepath] = item
self._items_by_filepath[filepath] = item
item = self._items_by_filepath.get(filepath)
if item is None:
item = QtGui.QStandardItem(filepath)
new_items.append(item)
new_items_by_filepath[filepath] = item
self._items_by_filepath[filepath] = item
icon = None
if filepath.replace("\\", "/") in self._blocking_crashed_paths:
icon = self._get_blocking_icon()
item.setData(icon, QtCore.Qt.DecorationRole)
if new_items:
parent.appendRows(new_items)
@ -113,6 +125,16 @@ class PluginLoadReportModel(QtGui.QStandardItemModel):
item = self._items_by_filepath.pop(filepath)
parent.removeRow(item.row())
@classmethod
def _get_blocking_icon(cls):
if cls._blocking_icon is None:
cls._blocking_icon = get_qt_icon({
"type": "material-symbols",
"name": "block",
"color": "red",
})
return cls._blocking_icon
class DetailWidget(QtWidgets.QTextEdit):
def __init__(self, text, *args, **kwargs):
@ -856,7 +878,7 @@ class PublishReportViewerWidget(QtWidgets.QFrame):
report = PublishReport(report_data)
self.set_report(report)
def set_report(self, report):
def set_report(self, report: PublishReport) -> None:
self._ignore_selection_changes = True
self._report_item = report
@ -866,6 +888,10 @@ class PublishReportViewerWidget(QtWidgets.QFrame):
self._logs_text_widget.set_report(report)
self._plugin_load_report_widget.set_report(report)
self._plugins_details_widget.set_report(report)
if report.blocking_crashed_paths:
self._details_tab_widget.setCurrentWidget(
self._plugin_load_report_widget
)
self._ignore_selection_changes = False

View file

@ -1,9 +1,11 @@
from __future__ import annotations
import os
import json
import time
import collections
import copy
from typing import Optional
from typing import Optional, Any
from qtpy import QtWidgets, QtCore, QtGui
@ -393,6 +395,9 @@ class PublisherWindow(QtWidgets.QDialog):
self._publish_frame_visible = None
self._tab_on_reset = None
self._create_context_valid: bool = True
self._blocked_by_crashed_paths: bool = False
self._error_messages_to_show = collections.deque()
self._errors_dialog_message_timer = errors_dialog_message_timer
@ -406,6 +411,8 @@ class PublisherWindow(QtWidgets.QDialog):
self._show_counter = 0
self._window_is_visible = False
self._update_footer_state()
@property
def controller(self) -> AbstractPublisherFrontend:
"""Kept for compatibility with traypublisher."""
@ -664,11 +671,33 @@ class PublisherWindow(QtWidgets.QDialog):
self._tab_on_reset = tab
def _update_publish_details_widget(self, force=False):
if not force and not self._is_on_details_tab():
def set_current_tab(self, tab):
if tab == "create":
self._go_to_create_tab()
elif tab == "publish":
self._go_to_publish_tab()
elif tab == "report":
self._go_to_report_tab()
elif tab == "details":
self._go_to_details_tab()
if not self._window_is_visible:
self.set_tab_on_reset(tab)
def _update_publish_details_widget(
self,
force: bool = False,
report_data: Optional[dict[str, Any]] = None,
) -> None:
if (
report_data is None
and not force
and not self._is_on_details_tab()
):
return
report_data = self._controller.get_publish_report()
if report_data is None:
report_data = self._controller.get_publish_report()
self._publish_details_widget.set_report_data(report_data)
def _on_help_click(self):
@ -752,19 +781,6 @@ class PublisherWindow(QtWidgets.QDialog):
def _set_current_tab(self, identifier):
self._tabs_widget.set_current_tab(identifier)
def set_current_tab(self, tab):
if tab == "create":
self._go_to_create_tab()
elif tab == "publish":
self._go_to_publish_tab()
elif tab == "report":
self._go_to_report_tab()
elif tab == "details":
self._go_to_details_tab()
if not self._window_is_visible:
self.set_tab_on_reset(tab)
def _is_current_tab(self, identifier):
return self._tabs_widget.is_current_tab(identifier)
@ -865,26 +881,56 @@ class PublisherWindow(QtWidgets.QDialog):
# Reset style
self._comment_input.setStyleSheet("")
def _set_footer_enabled(self, enabled):
self._save_btn.setEnabled(True)
def _set_create_context_valid(self, valid: bool) -> None:
self._create_context_valid = valid
self._update_footer_state()
def _set_blocked(self, blocked: bool) -> None:
self._blocked_by_crashed_paths = blocked
self._overview_widget.setEnabled(not blocked)
self._update_footer_state()
if not blocked:
return
self.set_tab_on_reset("details")
self._go_to_details_tab()
QtWidgets.QMessageBox.critical(
self,
"Failed to load plugins",
(
"Failed to load plugins that do prevent you from"
" using publish tool.\n"
"Please contact your TD or administrator."
)
)
def _update_footer_state(self) -> None:
enabled = (
not self._blocked_by_crashed_paths
and self._create_context_valid
)
save_enabled = not self._blocked_by_crashed_paths
self._save_btn.setEnabled(save_enabled)
self._reset_btn.setEnabled(True)
if enabled:
self._stop_btn.setEnabled(False)
self._validate_btn.setEnabled(True)
self._publish_btn.setEnabled(True)
else:
self._stop_btn.setEnabled(enabled)
self._validate_btn.setEnabled(enabled)
self._publish_btn.setEnabled(enabled)
self._stop_btn.setEnabled(False)
self._validate_btn.setEnabled(enabled)
self._publish_btn.setEnabled(enabled)
def _on_publish_reset(self):
self._create_tab.setEnabled(True)
self._set_comment_input_visiblity(True)
self._set_publish_overlay_visibility(False)
self._set_publish_visibility(False)
self._update_publish_details_widget()
report_data = self._controller.get_publish_report()
blocked = bool(report_data["blocking_crashed_paths"])
self._set_blocked(blocked)
self._update_publish_details_widget(report_data=report_data)
def _on_controller_reset(self):
self._update_publish_details_widget(force=True)
self._first_reset, first_reset = False, self._first_reset
if self._tab_on_reset is not None:
self._tab_on_reset, new_tab = None, self._tab_on_reset
@ -952,7 +998,7 @@ class PublisherWindow(QtWidgets.QDialog):
def _validate_create_instances(self):
if not self._controller.is_host_valid():
self._set_footer_enabled(True)
self._set_create_context_valid(True)
return
active_instances_by_id = {
@ -973,7 +1019,7 @@ class PublisherWindow(QtWidgets.QDialog):
if all_valid is None:
all_valid = True
self._set_footer_enabled(bool(all_valid))
self._set_create_context_valid(bool(all_valid))
def _on_create_model_reset(self):
self._validate_create_instances()

View file

@ -356,6 +356,27 @@ class CustomStagingDirProfileModel(BaseSettingsModel):
)
class DiscoverValidationModel(BaseSettingsModel):
"""Strictly validate publish plugins discovery.
Artist won't be able to publish if path to publish plugin fails to be
imported.
"""
_isGroup = True
enabled: bool = SettingsField(
False,
description="Enable strict mode of plugins discovery",
)
ignore_paths: list[str] = SettingsField(
default_factory=list,
title="Ignored paths (regex)",
description=(
"Paths that do match regex will be skipped in validation."
),
)
class PublishToolModel(BaseSettingsModel):
template_name_profiles: list[PublishTemplateNameProfile] = SettingsField(
default_factory=list,
@ -373,6 +394,10 @@ class PublishToolModel(BaseSettingsModel):
title="Custom Staging Dir Profiles"
)
)
discover_validation: DiscoverValidationModel = SettingsField(
default_factory=DiscoverValidationModel,
title="Validate plugins discovery",
)
comment_minimum_required_chars: int = SettingsField(
0,
title="Publish comment minimum required characters",
@ -705,6 +730,10 @@ DEFAULT_TOOLS_VALUES = {
"template_name": "simpleUnrealTextureHero"
}
],
"discover_validation": {
"enabled": False,
"ignore_paths": [],
},
"comment_minimum_required_chars": 0,
}
}