Merge branch 'develop' into enhancement/integrate_inputlinks_no_workfile_to_debug

This commit is contained in:
Jakub Trllo 2025-12-18 10:21:35 +01:00 committed by GitHub
commit 80f303c735
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 352 additions and 105 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

@ -259,7 +259,8 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
repre_thumb_created = self._create_colorspace_thumbnail(
full_input_path,
full_output_path,
colorspace_data
colorspace_data,
thumbnail_def,
)
# Try to use FFMPEG if OIIO is not supported or for cases when
@ -400,7 +401,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
return review_repres + other_repres
def _is_valid_images_repre(self, repre):
def _is_valid_images_repre(self, repre: dict) -> bool:
"""Check if representation contains valid image files
Args:
@ -420,10 +421,10 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
def _create_colorspace_thumbnail(
self,
src_path,
dst_path,
colorspace_data,
thumbnail_def
src_path: str,
dst_path: str,
colorspace_data: dict,
thumbnail_def: ThumbnailDef,
):
"""Create thumbnail using OIIO tool oiiotool
@ -436,11 +437,12 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
config (dict)
display (Optional[str])
view (Optional[str])
thumbnail_def (ThumbnailDefinition): Thumbnail definition.
Returns:
str: path to created thumbnail
"""
self.log.info("Extracting thumbnail {}".format(dst_path))
self.log.info(f"Extracting thumbnail {dst_path}")
resolution_arg = self._get_resolution_args(
"oiiotool", src_path, thumbnail_def
)
@ -599,10 +601,10 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
def _create_frame_from_video(
self,
video_file_path,
output_dir,
thumbnail_def
):
video_file_path: str,
output_dir: str,
thumbnail_def: ThumbnailDef,
) -> Optional[str]:
"""Convert video file to one frame image via ffmpeg"""
# create output file path
base_name = os.path.basename(video_file_path)
@ -703,10 +705,10 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
def _get_resolution_args(
self,
application,
input_path,
thumbnail_def
):
application: str,
input_path: str,
thumbnail_def: ThumbnailDef,
) -> list:
# get settings
if thumbnail_def.target_size["type"] == "source":
return []

View file

@ -2,6 +2,7 @@ from operator import attrgetter
import dataclasses
import os
import platform
from collections import defaultdict
from typing import Any, Dict, List
import pyblish.api
@ -13,10 +14,11 @@ except ImportError:
from ayon_core.lib import (
TextDef,
BoolDef,
NumberDef,
UISeparatorDef,
UILabelDef,
EnumDef,
filter_profiles, NumberDef
filter_profiles,
)
try:
from ayon_core.pipeline.usdlib import (
@ -278,19 +280,23 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
# level, you can add it directly from the publisher at that particular
# order. Future publishes will then see the existing contribution and will
# persist adding it to future bootstraps at that order
contribution_layers: Dict[str, int] = {
contribution_layers: Dict[str, Dict[str, int]] = {
# asset layers
"model": 100,
"assembly": 150,
"groom": 175,
"look": 200,
"rig": 300,
"asset": {
"model": 100,
"assembly": 150,
"groom": 175,
"look": 200,
"rig": 300,
},
# shot layers
"layout": 200,
"animation": 300,
"simulation": 400,
"fx": 500,
"lighting": 600,
"shot": {
"layout": 200,
"animation": 300,
"simulation": 400,
"fx": 500,
"lighting": 600,
}
}
# Default profiles to set certain instance attribute defaults based on
# profiles in settings
@ -305,12 +311,18 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
cls.enabled = plugin_settings.get("enabled", cls.enabled)
# Define contribution layers via settings
contribution_layers = {}
# Define contribution layers via settings by their scope
contribution_layers = defaultdict(dict)
for entry in plugin_settings.get("contribution_layers", []):
contribution_layers[entry["name"]] = int(entry["order"])
for scope in entry.get("scope", []):
contribution_layers[scope][entry["name"]] = int(entry["order"])
if contribution_layers:
cls.contribution_layers = contribution_layers
cls.contribution_layers = dict(contribution_layers)
else:
cls.log.warning(
"No scoped contribution layers found in settings, falling back"
" to CollectUSDLayerContributions plug-in defaults..."
)
cls.profiles = plugin_settings.get("profiles", [])
@ -355,10 +367,11 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
asset_product = contribution.target_product
layer_product = "{}_{}".format(asset_product, contribution.layer_id)
layer_order: int = self.contribution_layers.get(
attr_values["contribution_layer"], 0
)
scope: str = attr_values["contribution_target_product_init"]
layer_order: int = (
self.contribution_layers[scope][attr_values["contribution_layer"]]
)
# Layer contribution instance
layer_instance = self.get_or_create_instance(
product_name=layer_product,
@ -489,14 +502,14 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
profile = {}
# Define defaults
default_enabled = profile.get("contribution_enabled", True)
default_enabled: bool = profile.get("contribution_enabled", True)
default_contribution_layer = profile.get(
"contribution_layer", None)
default_apply_as_variant = profile.get(
default_apply_as_variant: bool = profile.get(
"contribution_apply_as_variant", False)
default_target_product = profile.get(
default_target_product: str = profile.get(
"contribution_target_product", "usdAsset")
default_init_as = (
default_init_as: str = (
"asset"
if profile.get("contribution_target_product") == "usdAsset"
else "shot")
@ -509,6 +522,12 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
visible = publish_attributes.get("contribution_enabled", True)
variant_visible = visible and publish_attributes.get(
"contribution_apply_as_variant", True)
init_as: str = publish_attributes.get(
"contribution_target_product_init", default_init_as)
contribution_layers = cls.contribution_layers.get(
init_as, {}
)
return [
UISeparatorDef("usd_container_settings1"),
@ -558,7 +577,7 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
"predefined ordering.\nA higher order (further down "
"the list) will contribute as a stronger opinion."
),
items=list(cls.contribution_layers.keys()),
items=list(contribution_layers.keys()),
default=default_contribution_layer,
visible=visible),
# TODO: We may want to make the visibility of this optional
@ -619,7 +638,11 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
# Update attributes if any of the following plug-in attributes
# change:
keys = ["contribution_enabled", "contribution_apply_as_variant"]
keys = {
"contribution_enabled",
"contribution_apply_as_variant",
"contribution_target_product_init",
}
for instance_change in event["changes"]:
instance = instance_change["instance"]

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