From 4eece5e6e9157e61a69ef960cd1065414403319d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 10 Dec 2025 16:55:44 +0100 Subject: [PATCH 01/25] Allow to define department layers scoped only to a particular department layer type, e.g. "shot" versus "asset". This way, you can scope same layer names for both shot and asset at different orders if they have differing target scopes --- .../extract_usd_layer_contributions.py | 58 ++++++++++++------- server/settings/publish_plugins.py | 30 ++++++---- 2 files changed, 57 insertions(+), 31 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py index 2c4cc5aac2..7da14a714b 100644 --- a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py +++ b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py @@ -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 @@ -278,19 +279,23 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, # level, you can add itdirectly 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 +310,13 @@ 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) cls.profiles = plugin_settings.get("profiles", []) @@ -489,14 +495,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 +515,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 +570,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), BoolDef("contribution_apply_as_variant", @@ -606,7 +618,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"] diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index d7b794cb5b..5524f7920d 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -74,9 +74,19 @@ class CollectFramesFixDefModel(BaseSettingsModel): ) +def usd_contribution_layer_types(): + return [ + {"value": "asset", "label": "Asset"}, + {"value": "shot", "label": "Shot"}, + ] + + class ContributionLayersModel(BaseSettingsModel): _layout = "compact" name: str = SettingsField(title="Name") + scope: list[str] = SettingsField( + title="Scope", + enum_resolver=usd_contribution_layer_types) order: str = SettingsField( title="Order", description="Higher order means a higher strength and stacks the " @@ -1345,17 +1355,17 @@ DEFAULT_PUBLISH_VALUES = { "enabled": True, "contribution_layers": [ # Asset layers - {"name": "model", "order": 100}, - {"name": "assembly", "order": 150}, - {"name": "groom", "order": 175}, - {"name": "look", "order": 200}, - {"name": "rig", "order": 300}, + {"name": "model", "order": 100, "scope": ["asset"]}, + {"name": "assembly", "order": 150, "scope": ["asset"]}, + {"name": "groom", "order": 175, "scope": ["asset"]}, + {"name": "look", "order": 200, "scope": ["asset"]}, + {"name": "rig", "order": 300, "scope": ["asset"]}, # Shot layers - {"name": "layout", "order": 200}, - {"name": "animation", "order": 300}, - {"name": "simulation", "order": 400}, - {"name": "fx", "order": 500}, - {"name": "lighting", "order": 600}, + {"name": "layout", "order": 200, "scope": ["shot"]}, + {"name": "animation", "order": 300, "scope": ["shot"]}, + {"name": "simulation", "order": 400, "scope": ["shot"]}, + {"name": "fx", "order": 500, "scope": ["shot"]}, + {"name": "lighting", "order": 600, "scope": ["shot"]}, ], "profiles": [ { From 2871ecac7d319dd4e3b9dc8d1469b2a253b06453 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 12 Dec 2025 22:52:52 +0100 Subject: [PATCH 02/25] Fix setting the correct order --- .../plugins/publish/extract_usd_layer_contributions.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py index 7da14a714b..63b642b685 100644 --- a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py +++ b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py @@ -340,8 +340,9 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, attr_values[key] = attr_values[key].format(**data) # Define contribution - order = self.contribution_layers.get( - attr_values["contribution_layer"], 0 + scope: str = attr_values["contribution_target_product_init"] + order: int = ( + self.contribution_layers[scope][attr_values["contribution_layer"]] ) if attr_values["contribution_apply_as_variant"]: From c93eb31b54b10089ca2652dddeefe36d34f51915 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 12 Dec 2025 22:53:05 +0100 Subject: [PATCH 03/25] Fix typo --- .../plugins/publish/extract_usd_layer_contributions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py index 63b642b685..a78767a76d 100644 --- a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py +++ b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py @@ -276,7 +276,7 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, # the contributions so that we can design a system where custom # contributions outside the predefined orders are possible to be # managed. So that if a particular asset requires an extra contribution - # level, you can add itdirectly from the publisher at that particular + # 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, Dict[str, int]] = { From 19f84805bd45cf9fb33faa6178c7f32c370da3c5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 15 Dec 2025 22:11:12 +0100 Subject: [PATCH 04/25] Fix scope defaults, fix order to `int` and enforce `name` to not be empty --- server/settings/publish_plugins.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index a328465932..90f6d1c5ef 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -83,14 +83,21 @@ def usd_contribution_layer_types(): class ContributionLayersModel(BaseSettingsModel): _layout = "compact" - name: str = SettingsField(title="Name") + name: str = SettingsField( + default="", + regex="[A-Za-z0-9_-]+", + title="Name") scope: list[str] = SettingsField( + default_factory=list, title="Scope", enum_resolver=usd_contribution_layer_types) - order: str = SettingsField( + order: int = SettingsField( + default=0, title="Order", - description="Higher order means a higher strength and stacks the " - "layer on top.") + description=( + "Higher order means a higher strength and stacks the layer on top." + ) + ) class CollectUSDLayerContributionsProfileModel(BaseSettingsModel): From 74971bd3dcac94d87fd190ab83bdfd89384ef81c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 15 Dec 2025 22:14:14 +0100 Subject: [PATCH 05/25] Cosmetics --- .../plugins/publish/extract_usd_layer_contributions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py index c6f6497a6a..eb51f1d491 100644 --- a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py +++ b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py @@ -18,7 +18,7 @@ from ayon_core.lib import ( UISeparatorDef, UILabelDef, EnumDef, - filter_profiles + filter_profiles, ) try: from ayon_core.pipeline.usdlib import ( From 3f72115a5edeb572135694249df1a3be7f21a352 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 16 Dec 2025 16:12:35 +0100 Subject: [PATCH 06/25] added option to filter crashed files --- client/ayon_core/pipeline/publish/lib.py | 59 ++++++++++++++++++++++++ server/settings/publish_plugins.py | 31 +++++++++++++ 2 files changed, 90 insertions(+) diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index e512a0116f..6af37aa388 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -246,6 +246,65 @@ 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"]["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. diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index d7b794cb5b..f47cf24fea 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -34,6 +34,28 @@ class ValidateBaseModel(BaseSettingsModel): active: bool = SettingsField(True, title="Active") +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 CollectAnatomyInstanceDataModel(BaseSettingsModel): _isGroup = True follow_workfile_version: bool = SettingsField( @@ -1188,6 +1210,11 @@ class CleanUpFarmModel(BaseSettingsModel): class PublishPuginsModel(BaseSettingsModel): + discover_validation: DiscoverValidationModel = SettingsField( + default_factory=DiscoverValidationModel, + title="Validate plugins discovery", + ) + CollectAnatomyInstanceData: CollectAnatomyInstanceDataModel = ( SettingsField( default_factory=CollectAnatomyInstanceDataModel, @@ -1308,6 +1335,10 @@ class PublishPuginsModel(BaseSettingsModel): DEFAULT_PUBLISH_VALUES = { + "discover_validation": { + "enabled": False, + "ignore_paths": [], + }, "CollectAnatomyInstanceData": { "follow_workfile_version": False }, From 09364a4f7e9adc2485e5544468117b15ef96f224 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 16 Dec 2025 16:20:40 +0100 Subject: [PATCH 07/25] use the function in main cli publish --- client/ayon_core/pipeline/publish/lib.py | 42 +++++++++++++++++------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 6af37aa388..2777f7511c 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -8,19 +8,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, @@ -1158,14 +1158,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"], @@ -1188,17 +1190,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.") From 096a5a809e1225374a9f6777ec322293f698427b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 16 Dec 2025 16:28:30 +0100 Subject: [PATCH 08/25] fix imports --- client/ayon_core/pipeline/publish/__init__.py | 1 + client/ayon_core/pipeline/publish/lib.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/client/ayon_core/pipeline/publish/__init__.py b/client/ayon_core/pipeline/publish/__init__.py index ede7fc3a35..105ab53fb0 100644 --- a/client/ayon_core/pipeline/publish/__init__.py +++ b/client/ayon_core/pipeline/publish/__init__.py @@ -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, diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 2777f7511c..934542bc1c 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -1,6 +1,8 @@ """Library functions for publishing.""" from __future__ import annotations import os +import platform +import re import sys import inspect import copy From c53a2f68e54e99dcfbd41f411fcd25fc2806e72b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 16 Dec 2025 18:01:10 +0100 Subject: [PATCH 09/25] fix settings load --- client/ayon_core/pipeline/publish/lib.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 934542bc1c..d511ad8b45 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -279,7 +279,9 @@ def filter_crashed_publish_paths( if project_settings is None: project_settings = get_project_settings(project_name) - discover_validation = project_settings["core"]["discover_validation"] + discover_validation = ( + project_settings["core"]["publish"]["discover_validation"] + ) # Strict mode is not enabled. if not discover_validation["enabled"]: return filtered_paths From 856a58dc35ecbccd2b17e4e73f77d6047e3c8362 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 16 Dec 2025 18:03:12 +0100 Subject: [PATCH 10/25] block publisher on blocking failed plugins --- .../tools/publisher/models/publish.py | 17 ++++- client/ayon_core/tools/publisher/window.py | 73 ++++++++++++++----- 2 files changed, 71 insertions(+), 19 deletions(-) diff --git a/client/ayon_core/tools/publisher/models/publish.py b/client/ayon_core/tools/publisher/models/publish.py index 97070d106f..cd99a952e3 100644 --- a/client/ayon_core/tools/publisher/models/publish.py +++ b/client/ayon_core/tools/publisher/models/publish.py @@ -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) diff --git a/client/ayon_core/tools/publisher/window.py b/client/ayon_core/tools/publisher/window.py index 19994f9f62..2d7bcc5a2a 100644 --- a/client/ayon_core/tools/publisher/window.py +++ b/client/ayon_core/tools/publisher/window.py @@ -393,6 +393,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 +409,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,12 +669,27 @@ class PublisherWindow(QtWidgets.QDialog): self._tab_on_reset = 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=False): if not force and not self._is_on_details_tab(): return report_data = self._controller.get_publish_report() + blocked = bool(report_data["blocking_crashed_paths"]) self._publish_details_widget.set_report_data(report_data) + self._set_blocked(blocked) def _on_help_click(self): if self._help_dialog.isVisible(): @@ -752,19 +772,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,8 +872,39 @@ 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: + if self._create_context_valid is valid: + return + self._create_context_valid = valid + self._update_footer_state() + + def _set_blocked(self, blocked: bool) -> None: + if self._blocked_by_crashed_paths is blocked: + return + self._blocked_by_crashed_paths = blocked + self._overview_widget.setEnabled(not blocked) + self._update_footer_state() + if not blocked: + return + + QtWidgets.QMessageBox.critical( + self, + "Failed to load plugins", + ( + "Failed to load plugins that do prevent you from" + " using publish tool." + " 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) @@ -885,6 +923,7 @@ class PublisherWindow(QtWidgets.QDialog): self._update_publish_details_widget() 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 +991,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 +1012,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() From ae7726bdef849c58634cdf01e84bc21753230a39 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 16 Dec 2025 18:31:35 +0100 Subject: [PATCH 11/25] change tabs and mark blocking filepath --- .../publish_report_viewer/report_items.py | 3 ++ .../publish_report_viewer/widgets.py | 38 +++++++++++++++---- client/ayon_core/tools/publisher/window.py | 3 ++ 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/tools/publisher/publish_report_viewer/report_items.py b/client/ayon_core/tools/publisher/publish_report_viewer/report_items.py index a3c5a7a2fd..24955d18c3 100644 --- a/client/ayon_core/tools/publisher/publish_report_viewer/report_items.py +++ b/client/ayon_core/tools/publisher/publish_report_viewer/report_items.py @@ -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", [] + ) 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 5fa1c04dc0..74ff647538 100644 --- a/client/ayon_core/tools/publisher/publish_report_viewer/widgets.py +++ b/client/ayon_core/tools/publisher/publish_report_viewer/widgets.py @@ -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): @@ -91,12 +96,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 + + if filepath.replace("\\", "/") in self._blocking_crashed_paths: + item.setData( + self._get_blocking_icon(), + QtCore.Qt.DecorationRole + ) if new_items: parent.appendRows(new_items) @@ -113,6 +124,15 @@ 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 +876,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 +886,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 diff --git a/client/ayon_core/tools/publisher/window.py b/client/ayon_core/tools/publisher/window.py index 2d7bcc5a2a..859fa96132 100644 --- a/client/ayon_core/tools/publisher/window.py +++ b/client/ayon_core/tools/publisher/window.py @@ -887,6 +887,9 @@ class PublisherWindow(QtWidgets.QDialog): if not blocked: return + self.set_tab_on_reset("details") + self._go_to_details_tab() + QtWidgets.QMessageBox.critical( self, "Failed to load plugins", From e8635725facb681982dc791d5cd1c3c68037e4f9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 16 Dec 2025 18:54:28 +0100 Subject: [PATCH 12/25] ruff fixes --- client/ayon_core/pipeline/publish/__init__.py | 1 + .../ayon_core/tools/publisher/publish_report_viewer/widgets.py | 1 + server/settings/publish_plugins.py | 1 - 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/publish/__init__.py b/client/ayon_core/pipeline/publish/__init__.py index 105ab53fb0..179d749f48 100644 --- a/client/ayon_core/pipeline/publish/__init__.py +++ b/client/ayon_core/pipeline/publish/__init__.py @@ -88,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", 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 74ff647538..7e7c31db04 100644 --- a/client/ayon_core/tools/publisher/publish_report_viewer/widgets.py +++ b/client/ayon_core/tools/publisher/publish_report_viewer/widgets.py @@ -134,6 +134,7 @@ class PluginLoadReportModel(QtGui.QStandardItemModel): }) return cls._blocking_icon + class DetailWidget(QtWidgets.QTextEdit): def __init__(self, text, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 2d0ac8aa31..e9eb5f7e93 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -55,7 +55,6 @@ class DiscoverValidationModel(BaseSettingsModel): ) - class CollectAnatomyInstanceDataModel(BaseSettingsModel): _isGroup = True follow_workfile_version: bool = SettingsField( From 5404153b942db01a0efd0ccfdb79ddce1c3ffa9f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Dec 2025 09:51:05 +0100 Subject: [PATCH 13/25] Add new line character. Co-authored-by: Roy Nieterau --- client/ayon_core/tools/publisher/window.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/publisher/window.py b/client/ayon_core/tools/publisher/window.py index 859fa96132..50bccb8aa0 100644 --- a/client/ayon_core/tools/publisher/window.py +++ b/client/ayon_core/tools/publisher/window.py @@ -895,8 +895,8 @@ class PublisherWindow(QtWidgets.QDialog): "Failed to load plugins", ( "Failed to load plugins that do prevent you from" - " using publish tool." - " Please contact your TD or administrator." + " using publish tool.\n" + "Please contact your TD or administrator." ) ) From f4bd5d49f949b2ff45c6c95a5fd25427873662ea Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Dec 2025 10:38:48 +0100 Subject: [PATCH 14/25] move settings to tools --- server/settings/publish_plugins.py | 30 ------------------------------ server/settings/tools.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index e9eb5f7e93..da22e4206f 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -34,27 +34,6 @@ class ValidateBaseModel(BaseSettingsModel): active: bool = SettingsField(True, title="Active") -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 CollectAnatomyInstanceDataModel(BaseSettingsModel): _isGroup = True follow_workfile_version: bool = SettingsField( @@ -1236,11 +1215,6 @@ class CleanUpFarmModel(BaseSettingsModel): class PublishPuginsModel(BaseSettingsModel): - discover_validation: DiscoverValidationModel = SettingsField( - default_factory=DiscoverValidationModel, - title="Validate plugins discovery", - ) - CollectAnatomyInstanceData: CollectAnatomyInstanceDataModel = ( SettingsField( default_factory=CollectAnatomyInstanceDataModel, @@ -1371,10 +1345,6 @@ class PublishPuginsModel(BaseSettingsModel): DEFAULT_PUBLISH_VALUES = { - "discover_validation": { - "enabled": False, - "ignore_paths": [], - }, "CollectAnatomyInstanceData": { "follow_workfile_version": False }, diff --git a/server/settings/tools.py b/server/settings/tools.py index da3b4ebff8..9b16ca5d94 100644 --- a/server/settings/tools.py +++ b/server/settings/tools.py @@ -352,6 +352,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, @@ -369,6 +390,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", @@ -691,6 +716,10 @@ DEFAULT_TOOLS_VALUES = { "template_name": "simpleUnrealTextureHero" } ], + "discover_validation": { + "enabled": False, + "ignore_paths": [], + }, "comment_minimum_required_chars": 0, } } From 73cc4c53b4ad02f8e0e6971bd456935fd29e5970 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Dec 2025 10:53:43 +0100 Subject: [PATCH 15/25] use correct settings --- client/ayon_core/pipeline/publish/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index d511ad8b45..8492145979 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -280,7 +280,7 @@ def filter_crashed_publish_paths( project_settings = get_project_settings(project_name) discover_validation = ( - project_settings["core"]["publish"]["discover_validation"] + project_settings["core"]["tools"]["publish"]["discover_validation"] ) # Strict mode is not enabled. if not discover_validation["enabled"]: From e1dc93cb4452d9ed535009efc31d0ca366e6fe4c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Dec 2025 10:55:27 +0100 Subject: [PATCH 16/25] unset icon if is not blocking anymore --- .../tools/publisher/publish_report_viewer/widgets.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 7e7c31db04..d595593898 100644 --- a/client/ayon_core/tools/publisher/publish_report_viewer/widgets.py +++ b/client/ayon_core/tools/publisher/publish_report_viewer/widgets.py @@ -103,11 +103,11 @@ class PluginLoadReportModel(QtGui.QStandardItemModel): new_items_by_filepath[filepath] = item self._items_by_filepath[filepath] = item + icon = None if filepath.replace("\\", "/") in self._blocking_crashed_paths: - item.setData( - self._get_blocking_icon(), - QtCore.Qt.DecorationRole - ) + icon = self._get_blocking_icon() + + item.setData(icon, QtCore.Qt.DecorationRole) if new_items: parent.appendRows(new_items) From cd1c2cdb0f38f611a4133fc4d8eaba9a94d5e3c7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 17 Dec 2025 11:57:29 +0100 Subject: [PATCH 17/25] Set the default value for new entries to be scoped to `asset`, `task` so that copying from older releases automatically sets it to both. This way, also newly added entries will have both by default which is better than none. --- server/settings/publish_plugins.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 90f6d1c5ef..6b1ee5562f 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -88,7 +88,11 @@ class ContributionLayersModel(BaseSettingsModel): regex="[A-Za-z0-9_-]+", title="Name") scope: list[str] = SettingsField( - default_factory=list, + # This should actually be returned from a callable to `default_factory` + # because lists are mutable. However, the frontend can't interpret + # the callable. It will fail to apply it as the default. Specifying + # this default directly did not show any ill side effects. + default=["asset", "shot"], title="Scope", enum_resolver=usd_contribution_layer_types) order: int = SettingsField( From 108286aa341cffefd135fc0ac52bffbbe06f9606 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Dec 2025 11:59:41 +0100 Subject: [PATCH 18/25] fix refresh issue --- .../ayon_core/tools/publisher/publish_report_viewer/widgets.py | 1 + 1 file changed, 1 insertion(+) 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 d595593898..225dd15ade 100644 --- a/client/ayon_core/tools/publisher/publish_report_viewer/widgets.py +++ b/client/ayon_core/tools/publisher/publish_report_viewer/widgets.py @@ -88,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 = [] From 3e3cd49beaddf28e1685d0147deb65ebc3bcf5d5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 17 Dec 2025 11:59:48 +0100 Subject: [PATCH 19/25] Add a warning if plug-in defaults are used --- .../plugins/publish/extract_usd_layer_contributions.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py index eb51f1d491..ed3c16b5c2 100644 --- a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py +++ b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py @@ -318,6 +318,11 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, contribution_layers[scope][entry["name"]] = int(entry["order"]) if 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", []) From 2baffc253ceeaf21c67727c84d1b322740abdf6c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 17 Dec 2025 12:13:21 +0100 Subject: [PATCH 20/25] Set `min_items=1` for `scope` attribute in `CollectUSDLayerContributions` --- server/settings/publish_plugins.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index df76170a20..eb41c75699 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -94,6 +94,7 @@ class ContributionLayersModel(BaseSettingsModel): # this default directly did not show any ill side effects. default=["asset", "shot"], title="Scope", + min_items=1, enum_resolver=usd_contribution_layer_types) order: int = SettingsField( default=0, From de7b49e68ff80368695c569b6a86baadc3c8974a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Dec 2025 12:31:49 +0100 Subject: [PATCH 21/25] simplify update state --- client/ayon_core/tools/publisher/window.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/tools/publisher/window.py b/client/ayon_core/tools/publisher/window.py index 50bccb8aa0..ddbda8eedd 100644 --- a/client/ayon_core/tools/publisher/window.py +++ b/client/ayon_core/tools/publisher/window.py @@ -909,14 +909,9 @@ class PublisherWindow(QtWidgets.QDialog): 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) From 5982ad7944b23bfcdfede55132b5e75c295d9737 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Dec 2025 12:32:19 +0100 Subject: [PATCH 22/25] call set_blocked only on reset --- client/ayon_core/tools/publisher/window.py | 27 ++++++++++++++++------ 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/tools/publisher/window.py b/client/ayon_core/tools/publisher/window.py index ddbda8eedd..f74ca9abb9 100644 --- a/client/ayon_core/tools/publisher/window.py +++ b/client/ayon_core/tools/publisher/window.py @@ -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 @@ -682,14 +684,21 @@ class PublisherWindow(QtWidgets.QDialog): if not self._window_is_visible: self.set_tab_on_reset(tab) - def _update_publish_details_widget(self, force=False): - if not force and not self._is_on_details_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() - blocked = bool(report_data["blocking_crashed_paths"]) + if report_data is None: + report_data = self._controller.get_publish_report() self._publish_details_widget.set_report_data(report_data) - self._set_blocked(blocked) def _on_help_click(self): if self._help_dialog.isVisible(): @@ -918,7 +927,11 @@ class PublisherWindow(QtWidgets.QDialog): 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) From e462dca88986b339fd7a7860df05597820039654 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Dec 2025 14:01:54 +0100 Subject: [PATCH 23/25] always call '_update_footer_state' --- client/ayon_core/tools/publisher/window.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/client/ayon_core/tools/publisher/window.py b/client/ayon_core/tools/publisher/window.py index f74ca9abb9..6f7444ed04 100644 --- a/client/ayon_core/tools/publisher/window.py +++ b/client/ayon_core/tools/publisher/window.py @@ -882,14 +882,10 @@ class PublisherWindow(QtWidgets.QDialog): self._comment_input.setStyleSheet("") def _set_create_context_valid(self, valid: bool) -> None: - if self._create_context_valid is valid: - return self._create_context_valid = valid self._update_footer_state() def _set_blocked(self, blocked: bool) -> None: - if self._blocked_by_crashed_paths is blocked: - return self._blocked_by_crashed_paths = blocked self._overview_widget.setEnabled(not blocked) self._update_footer_state() From 3fe508e773718c314b04c3a83685cc41cf8bd494 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Dec 2025 14:32:11 +0100 Subject: [PATCH 24/25] pass thumbnail def to _create_colorspace_thumbnail --- client/ayon_core/plugins/publish/extract_thumbnail.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index b94a4c4dbb..ff492f8021 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -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 From a9af964f4c2364d058eee53fdbf8eb3f178c1d94 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Dec 2025 14:34:36 +0100 Subject: [PATCH 25/25] added some typehints --- .../plugins/publish/extract_thumbnail.py | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index ff492f8021..1dde8cfb55 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -401,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: @@ -421,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 @@ -437,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 ) @@ -600,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) @@ -704,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 []