Merge pull request #1417 from ynput/enhancement/copy_multiple_library_project

Multiple products could be pushed to another project
This commit is contained in:
Petr Kalis 2025-08-29 15:09:04 +02:00 committed by GitHub
commit 8bd1f08cc3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 266 additions and 149 deletions

View file

@ -28,10 +28,12 @@ class PushToProject(load.ProductLoaderPlugin):
if not filtered_contexts:
raise LoadError("Nothing to push for your selection")
if len(filtered_contexts) > 1:
raise LoadError("Please select only one item")
context = tuple(filtered_contexts)[0]
folder_ids = set(
context["folder"]["id"]
for context in filtered_contexts
)
if len(folder_ids) > 1:
raise LoadError("Please select products from single folder")
push_tool_script_path = os.path.join(
AYON_CORE_ROOT,
@ -39,13 +41,16 @@ class PushToProject(load.ProductLoaderPlugin):
"push_to_project",
"main.py"
)
project_name = filtered_contexts[0]["project"]["name"]
project_name = context["project"]["name"]
version_id = context["version"]["id"]
version_ids = {
context["version"]["id"]
for context in filtered_contexts
}
args = get_ayon_launcher_args(
push_tool_script_path,
"--project", project_name,
"--version", version_id
"--versions", ",".join(version_ids)
)
run_detached_process(args)

View file

@ -1,4 +1,5 @@
import threading
from typing import Dict
import ayon_api
@ -13,10 +14,11 @@ from .models import (
UserPublishValuesModel,
IntegrateModel,
)
from .models.integrate import ProjectPushItemProcess
class PushToContextController:
def __init__(self, project_name=None, version_id=None):
def __init__(self, project_name=None, version_ids=None):
self._event_system = self._create_event_system()
self._projects_model = ProjectsModel(self)
@ -27,18 +29,20 @@ class PushToContextController:
self._user_values = UserPublishValuesModel(self)
self._src_project_name = None
self._src_version_id = None
self._src_version_ids = []
self._src_folder_entity = None
self._src_folder_task_entities = {}
self._src_product_entity = None
self._src_version_entity = None
self._src_version_entities = []
self._src_product_entities = {}
self._src_label = None
self._submission_enabled = False
self._process_thread = None
self._process_item_id = None
self.set_source(project_name, version_id)
self._use_original_name = False
self.set_source(project_name, version_ids)
# Events system
def emit_event(self, topic, data=None, source=None):
@ -51,38 +55,47 @@ class PushToContextController:
def register_event_callback(self, topic, callback):
self._event_system.add_callback(topic, callback)
def set_source(self, project_name, version_id):
def set_source(self, project_name, version_ids):
"""Set source project and version.
There is currently assumption that tool is working on products of same
folder.
Args:
project_name (Union[str, None]): Source project name.
version_id (Union[str, None]): Source version id.
version_ids (Optional[list[str]]): Version ids.
"""
if not project_name or not version_ids:
return
if (
project_name == self._src_project_name
and version_id == self._src_version_id
and version_ids == self._src_version_ids
):
return
self._src_project_name = project_name
self._src_version_id = version_id
self._src_version_ids = version_ids
self._src_label = None
folder_entity = None
task_entities = {}
product_entity = None
version_entity = None
if project_name and version_id:
version_entity = ayon_api.get_version_by_id(
project_name, version_id
product_entities = []
version_entities = []
if project_name and self._src_version_ids:
version_entities = list(ayon_api.get_versions(
project_name, version_ids=self._src_version_ids))
if version_entities:
product_ids = [
version_entity["productId"]
for version_entity in version_entities
]
product_entities = list(ayon_api.get_products(
project_name, product_ids=product_ids)
)
if version_entity:
product_entity = ayon_api.get_product_by_id(
project_name, version_entity["productId"]
)
if product_entity:
if product_entities:
# all products for same folder
product_entity = product_entities[0]
folder_entity = ayon_api.get_folder_by_id(
project_name, product_entity["folderId"]
)
@ -97,15 +110,18 @@ class PushToContextController:
self._src_folder_entity = folder_entity
self._src_folder_task_entities = task_entities
self._src_product_entity = product_entity
self._src_version_entity = version_entity
self._src_version_entities = version_entities
self._src_product_entities = {
product["id"]: product
for product in product_entities
}
if folder_entity:
self._user_values.set_new_folder_name(folder_entity["name"])
variant = self._get_src_variant()
if variant:
self._user_values.set_variant(variant)
comment = version_entity["attrib"].get("comment")
comment = version_entities[0]["attrib"].get("comment")
if comment:
self._user_values.set_comment(comment)
@ -113,7 +129,7 @@ class PushToContextController:
"source.changed",
{
"project_name": project_name,
"version_id": version_id
"version_ids": self._src_version_ids
}
)
@ -142,6 +158,14 @@ class PushToContextController:
def get_user_values(self):
return self._user_values.get_data()
def original_names_required(self):
"""Checks if original product names must be used.
Currently simple check if multiple versions, but if multiple products
with different product_type were used, it wouldn't be necessary.
"""
return len(self._src_version_entities) > 1
def set_user_value_folder_name(self, folder_name):
self._user_values.set_new_folder_name(folder_name)
self._invalidate()
@ -165,8 +189,9 @@ class PushToContextController:
def set_selected_task(self, task_id, task_name):
self._selection_model.set_selected_task(task_id, task_name)
def get_process_item_status(self, item_id):
return self._integrate_model.get_item_status(item_id)
def get_process_items(self) -> Dict[str, ProjectPushItemProcess]:
"""Returns dict of all ProjectPushItemProcess items """
return self._integrate_model.get_items()
# Processing methods
def submit(self, wait=True):
@ -176,29 +201,33 @@ class PushToContextController:
if self._process_thread is not None:
return
item_id = self._integrate_model.create_process_item(
self._src_project_name,
self._src_version_id,
self._selection_model.get_selected_project_name(),
self._selection_model.get_selected_folder_id(),
self._selection_model.get_selected_task_name(),
self._user_values.variant,
comment=self._user_values.comment,
new_folder_name=self._user_values.new_folder_name,
dst_version=1
)
item_ids = []
for src_version_entity in self._src_version_entities:
item_id = self._integrate_model.create_process_item(
self._src_project_name,
src_version_entity["id"],
self._selection_model.get_selected_project_name(),
self._selection_model.get_selected_folder_id(),
self._selection_model.get_selected_task_name(),
self._user_values.variant,
comment=self._user_values.comment,
new_folder_name=self._user_values.new_folder_name,
dst_version=1,
use_original_name=self._use_original_name,
)
item_ids.append(item_id)
self._process_item_id = item_id
self._process_item_ids = item_ids
self._emit_event("submit.started")
if wait:
self._submit_callback()
self._process_item_id = None
self._process_item_ids = []
return item_id
thread = threading.Thread(target=self._submit_callback)
self._process_thread = thread
thread.start()
return item_id
return item_ids
def wait_for_process_thread(self):
if self._process_thread is None:
@ -207,7 +236,7 @@ class PushToContextController:
self._process_thread = None
def _prepare_source_label(self):
if not self._src_project_name or not self._src_version_id:
if not self._src_project_name or not self._src_version_ids:
return "Source is not defined"
folder_entity = self._src_folder_entity
@ -215,14 +244,21 @@ class PushToContextController:
return "Source is invalid"
folder_path = folder_entity["path"]
product_entity = self._src_product_entity
version_entity = self._src_version_entity
return "Source: {}{}/{}/v{:0>3}".format(
self._src_project_name,
folder_path,
product_entity["name"],
version_entity["version"]
)
src_labels = []
for version_entity in self._src_version_entities:
product_entity = self._src_product_entities.get(
version_entity["productId"]
)
src_labels.append(
"Source: {}{}/{}/v{:0>3}".format(
self._src_project_name,
folder_path,
product_entity["name"],
version_entity["version"],
)
)
return "\n".join(src_labels)
def _get_task_info_from_repre_entities(
self, task_entities, repre_entities
@ -256,7 +292,8 @@ class PushToContextController:
def _get_src_variant(self):
project_name = self._src_project_name
version_entity = self._src_version_entity
# parse variant only from first version
version_entity = self._src_version_entities[0]
task_entities = self._src_folder_task_entities
repre_entities = ayon_api.get_representations(
project_name, version_ids={version_entity["id"]}
@ -264,9 +301,12 @@ class PushToContextController:
task_name, task_type = self._get_task_info_from_repre_entities(
task_entities, repre_entities
)
product_entity = self._src_product_entities.get(
version_entity["productId"]
)
project_settings = get_project_settings(project_name)
product_type = self._src_product_entity["productType"]
product_type = product_entity["productType"]
template = get_product_name_template(
self._src_project_name,
product_type,
@ -300,7 +340,7 @@ class PushToContextController:
print("Failed format", exc)
return ""
product_name = self._src_product_entity["name"]
product_name = product_entity["name"]
if (
(product_s and not product_name.startswith(product_s))
or (product_e and not product_name.endswith(product_e))
@ -314,9 +354,6 @@ class PushToContextController:
return product_name
def _check_submit_validations(self):
if not self._user_values.is_valid:
return False
if not self._selection_model.get_selected_project_name():
return False
@ -325,6 +362,13 @@ class PushToContextController:
and not self._selection_model.get_selected_folder_id()
):
return False
if self._use_original_name:
return True
if not self._user_values.is_valid:
return False
return True
def _invalidate(self):
@ -338,13 +382,14 @@ class PushToContextController:
)
def _submit_callback(self):
process_item_id = self._process_item_id
if process_item_id is None:
return
self._integrate_model.integrate_item(process_item_id)
process_item_ids = self._process_item_ids
for process_item_id in process_item_ids:
self._integrate_model.integrate_item(process_item_id)
self._emit_event("submit.finished", {})
if process_item_id == self._process_item_id:
self._process_item_id = None
if process_item_ids is self._process_item_ids:
self._process_item_ids = []
def _emit_event(self, topic, data=None):
if data is None:

View file

@ -4,28 +4,28 @@ from ayon_core.tools.utils import get_ayon_qt_app
from ayon_core.tools.push_to_project.ui import PushToContextSelectWindow
def main_show(project_name, version_id):
def main_show(project_name, version_ids):
app = get_ayon_qt_app()
window = PushToContextSelectWindow()
window.show()
window.set_source(project_name, version_id)
window.set_source(project_name, version_ids)
app.exec_()
@click.command()
@click.option("--project", help="Source project name")
@click.option("--version", help="Source version id")
def main(project, version):
@click.option("--versions", help="Source version ids")
def main(project, versions):
"""Run PushToProject tool to integrate version in different project.
Args:
project (str): Source project name.
version (str): Version id.
versions (str): comma separated versions for same context
"""
main_show(project, version)
main_show(project, versions.split(","))
if __name__ == "__main__":

View file

@ -5,7 +5,7 @@ import itertools
import sys
import traceback
import uuid
from typing import Optional
from typing import Optional, Dict
import ayon_api
from ayon_api.utils import create_entity_id
@ -90,6 +90,7 @@ class ProjectPushItem:
new_folder_name,
dst_version,
item_id=None,
use_original_name=False
):
if not item_id:
item_id = uuid.uuid4().hex
@ -104,6 +105,7 @@ class ProjectPushItem:
self.comment = comment or ""
self.item_id = item_id
self._repr_value = None
self.use_original_name = use_original_name
@property
def _repr(self):
@ -115,7 +117,8 @@ class ProjectPushItem:
str(self.dst_folder_id),
str(self.new_folder_name),
str(self.dst_task_name),
str(self.dst_version)
str(self.dst_version),
self.use_original_name
])
return self._repr_value
@ -134,6 +137,7 @@ class ProjectPushItem:
"comment": self.comment,
"new_folder_name": self.new_folder_name,
"item_id": self.item_id,
"use_original_name": self.use_original_name
}
@classmethod
@ -313,7 +317,7 @@ class ProjectPushRepreItem:
if self._src_files is not None:
return self._src_files, self._resource_files
repre_context = self._repre_entity["context"]
repre_context = self.repre_entity["context"]
if "frame" in repre_context or "udim" in repre_context:
src_files, resource_files = self._get_source_files_with_frames()
else:
@ -330,7 +334,7 @@ class ProjectPushRepreItem:
udim_placeholder = "__udim__"
src_files = []
resource_files = []
template = self._repre_entity["attrib"]["template"]
template = self.repre_entity["attrib"]["template"]
# Remove padding from 'udim' and 'frame' formatting keys
# - "{frame:0>4}" -> "{frame}"
for key in ("udim", "frame"):
@ -338,7 +342,7 @@ class ProjectPushRepreItem:
replacement = "{{{}}}".format(key)
template = re.sub(sub_part, replacement, template)
repre_context = self._repre_entity["context"]
repre_context = self.repre_entity["context"]
fill_repre_context = copy.deepcopy(repre_context)
if "frame" in fill_repre_context:
fill_repre_context["frame"] = frame_placeholder
@ -359,7 +363,7 @@ class ProjectPushRepreItem:
.replace(udim_placeholder, "(?P<udim>[0-9]+)")
)
src_basename_regex = re.compile("^{}$".format(src_basename))
for file_info in self._repre_entity["files"]:
for file_info in self.repre_entity["files"]:
filepath_template = self._clean_path(file_info["path"])
filepath = self._clean_path(
filepath_template.format(root=self._roots)
@ -390,8 +394,8 @@ class ProjectPushRepreItem:
def _get_source_files(self):
src_files = []
resource_files = []
template = self._repre_entity["attrib"]["template"]
repre_context = self._repre_entity["context"]
template = self.repre_entity["attrib"]["template"]
repre_context = self.repre_entity["context"]
fill_repre_context = copy.deepcopy(repre_context)
fill_roots = fill_repre_context["root"]
for root_name in tuple(fill_roots.keys()):
@ -400,7 +404,7 @@ class ProjectPushRepreItem:
fill_repre_context)
repre_path = self._clean_path(repre_path)
src_dirpath = os.path.dirname(repre_path)
for file_info in self._repre_entity["files"]:
for file_info in self.repre_entity["files"]:
filepath_template = self._clean_path(file_info["path"])
filepath = self._clean_path(
filepath_template.format(root=self._roots))
@ -493,8 +497,11 @@ class ProjectPushItemProcess:
except Exception as exc:
_exc, _value, _tb = sys.exc_info()
product_name = self._src_product_entity["name"]
self._status.set_failed(
"Unhandled error happened: {}".format(str(exc)),
"Unhandled error happened for `{}`: {}".format(
product_name, str(exc)
),
(_exc, _value, _tb)
)
@ -817,31 +824,34 @@ class ProjectPushItemProcess:
self._template_name = template_name
def _determine_product_name(self):
product_type = self._product_type
task_info = self._task_info
task_name = task_type = None
if task_info:
task_name = task_info["name"]
task_type = task_info["taskType"]
if self._item.use_original_name:
product_name = self._src_product_entity["name"]
else:
product_type = self._product_type
task_info = self._task_info
task_name = task_type = None
if task_info:
task_name = task_info["name"]
task_type = task_info["taskType"]
try:
product_name = get_product_name(
self._item.dst_project_name,
task_name,
task_type,
self.host_name,
product_type,
self._item.variant,
project_settings=self._project_settings
)
except TaskNotSetError:
self._status.set_failed(
"Target product name template requires task name. To continue"
" you have to select target task or change settings"
" <b>ayon+settings://core/tools/creator/product_name_profiles"
f"?project={self._item.dst_project_name}</b>."
)
raise PushToProjectError(self._status.fail_reason)
try:
product_name = get_product_name(
self._item.dst_project_name,
task_name,
task_type,
self.host_name,
product_type,
self._item.variant,
project_settings=self._project_settings
)
except TaskNotSetError:
self._status.set_failed(
"Target product name template requires task name. To "
"continue you have to select target task or change settings " # noqa: E501
" <b>ayon+settings://core/tools/creator/product_name_profiles" # noqa: E501
f"?project={self._item.dst_project_name}</b>."
)
raise PushToProjectError(self._status.fail_reason)
self._log_info(
f"Push will be integrating to product with name '{product_name}'"
@ -1012,10 +1022,18 @@ class ProjectPushItemProcess:
self, anatomy, template_name, formatting_data, file_template
):
processed_repre_items = []
repre_context = None
for repre_item in self._src_repre_items:
repre_entity = repre_item.repre_entity
repre_name = repre_entity["name"]
repre_format_data = copy.deepcopy(formatting_data)
if not repre_context:
repre_context = self._update_repre_context(
copy.deepcopy(repre_entity),
formatting_data
)
repre_format_data["representation"] = repre_name
for src_file in repre_item.src_files:
ext = os.path.splitext(src_file.path)[-1]
@ -1031,7 +1049,6 @@ class ProjectPushItemProcess:
"publish", template_name, "directory"
)
folder_path = template_obj.format_strict(formatting_data)
repre_context = folder_path.used_values
folder_path_rootless = folder_path.rootless
repre_filepaths = []
published_path = None
@ -1054,7 +1071,6 @@ class ProjectPushItemProcess:
)
if published_path is None or frame == repre_item.frame:
published_path = dst_filepath
repre_context.update(filename.used_values)
repre_filepaths.append((dst_filepath, dst_rootless_path))
self._file_transaction.add(src_file.path, dst_filepath)
@ -1141,7 +1157,7 @@ class ProjectPushItemProcess:
self._item.dst_project_name,
"representation",
entity_id,
changes
changes,
)
existing_repre_names = set(existing_repres_by_low_name.keys())
@ -1171,6 +1187,28 @@ class ProjectPushItemProcess:
path
)
def _update_repre_context(self, repre_entity, formatting_data):
"""Replace old context value with new ones.
Folder might change, project definitely changes etc.
"""
repre_context = repre_entity["context"]
for context_key, context_value in repre_context.items():
if context_value and isinstance(context_value, dict):
for context_sub_key in context_value.keys():
value_to_update = formatting_data.get(context_key, {}).get(
context_sub_key)
if value_to_update:
repre_context[context_key][
context_sub_key] = value_to_update
else:
value_to_update = formatting_data.get(context_key)
if value_to_update:
repre_context[context_key] = value_to_update
if "task" not in formatting_data:
repre_context.pop("task")
return repre_context
class IntegrateModel:
def __init__(self, controller):
@ -1194,6 +1232,7 @@ class IntegrateModel:
comment,
new_folder_name,
dst_version,
use_original_name
):
"""Create new item for integration.
@ -1207,6 +1246,7 @@ class IntegrateModel:
comment (Union[str, None]): Comment.
new_folder_name (Union[str, None]): New folder name.
dst_version (int): Destination version number.
use_original_name (bool): If original product names should be used
Returns:
str: Item id. The id can be used to trigger integration or get
@ -1222,7 +1262,8 @@ class IntegrateModel:
variant,
comment=comment,
new_folder_name=new_folder_name,
dst_version=dst_version
dst_version=dst_version,
use_original_name=use_original_name
)
process_item = ProjectPushItemProcess(self, item)
self._process_items[item.item_id] = process_item
@ -1240,17 +1281,6 @@ class IntegrateModel:
return
item.integrate()
def get_item_status(self, item_id):
"""Status of an item.
Args:
item_id (str): Item id for which status should be returned.
Returns:
dict[str, Any]: Status data.
"""
item = self._process_items.get(item_id)
if item is not None:
return item.get_status_data()
return None
def get_items(self) -> Dict[str, ProjectPushItemProcess]:
"""Returns dict of all ProjectPushItemProcess items """
return self._process_items

View file

@ -133,6 +133,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
inputs_widget = QtWidgets.QWidget(main_splitter)
new_folder_checkbox = NiceCheckbox(True, parent=inputs_widget)
original_names_checkbox = NiceCheckbox(False, parent=inputs_widget)
folder_name_input = PlaceholderLineEdit(inputs_widget)
folder_name_input.setPlaceholderText("< Name of new folder >")
@ -151,6 +152,8 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
inputs_layout.addRow("Create new folder", new_folder_checkbox)
inputs_layout.addRow("New folder name", folder_name_input)
inputs_layout.addRow("Variant", variant_input)
inputs_layout.addRow(
"Use original product names", original_names_checkbox)
inputs_layout.addRow("Comment", comment_input)
main_splitter.addWidget(context_widget)
@ -205,6 +208,10 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
show_detail_btn.setToolTip(
"Show error detail dialog to copy full error."
)
original_names_checkbox.setToolTip(
"Required for multi copy, doesn't allow changes "
"variant values."
)
overlay_close_btn = QtWidgets.QPushButton(
"Close", overlay_btns_widget
@ -250,6 +257,8 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
variant_input.textChanged.connect(self._on_variant_change)
comment_input.textChanged.connect(self._on_comment_change)
library_only_checkbox.stateChanged.connect(self._on_library_only_change)
original_names_checkbox.stateChanged.connect(
self._on_original_names_change)
publish_btn.clicked.connect(self._on_select_click)
cancel_btn.clicked.connect(self._on_close_click)
@ -298,6 +307,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
self._new_folder_checkbox = new_folder_checkbox
self._folder_name_input = folder_name_input
self._comment_input = comment_input
self._use_original_names_checkbox = original_names_checkbox
self._publish_btn = publish_btn
@ -326,7 +336,6 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
self._main_thread_timer = main_thread_timer
self._main_thread_timer_can_stop = True
self._last_submit_message = None
self._process_item_id = None
self._variant_is_valid = None
self._folder_is_valid = None
@ -337,17 +346,17 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
overlay_try_btn.setVisible(False)
# Support of public api function of controller
def set_source(self, project_name, version_id):
def set_source(self, project_name, version_ids):
"""Set source project and version.
Call the method on controller.
Args:
project_name (Union[str, None]): Name of project.
version_id (Union[str, None]): Version id.
version_ids (Union[str, None]): comma separated Version ids.
"""
self._controller.set_source(project_name, version_id)
self._controller.set_source(project_name, version_ids)
def showEvent(self, event):
super(PushToContextSelectWindow, self).showEvent(event)
@ -362,10 +371,12 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
self._folder_name_input.setText(new_folder_name or "")
self._variant_input.setText(variant or "")
self._invalidate_variant(user_values["is_variant_valid"])
self._invalidate_use_original_names(
self._use_original_names_checkbox.isChecked())
self._invalidate_new_folder_name(
new_folder_name, user_values["is_new_folder_name_valid"]
)
self._controller._invalidate()
self._projects_combobox.refresh()
def _on_first_show(self):
@ -409,6 +420,10 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
state = bool(state)
self._projects_combobox.set_standard_filter_enabled(state)
def _on_original_names_change(self, state: int) -> None:
use_original_name = bool(state)
self._invalidate_use_original_names(use_original_name)
def _on_user_input_timer(self):
folder_name_enabled = self._new_folder_name_enabled
folder_name = self._new_folder_name_input_text
@ -471,17 +486,27 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
state = ""
if folder_name is not None:
state = "valid" if is_valid else "invalid"
set_style_property(
self._folder_name_input, "state", state
)
set_style_property(self._folder_name_input, "state", state)
def _invalidate_variant(self, is_valid):
if self._variant_is_valid is is_valid:
return
self._variant_is_valid = is_valid
state = "valid" if is_valid else "invalid"
set_style_property(self._variant_input, "state", state)
def _invalidate_use_original_names(self, use_original_names):
"""Checks if original names must be used.
Invalidates Variant if necessary
"""
if self._controller.original_names_required():
use_original_names = True
self._variant_input.setEnabled(not use_original_names)
self._invalidate_variant(not use_original_names)
self._controller._use_original_name = use_original_names
self._use_original_names_checkbox.setChecked(use_original_names)
def _on_submission_change(self, event):
self._publish_btn.setEnabled(event["enabled"])
@ -510,31 +535,43 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
self._overlay_label.setText(self._last_submit_message)
self._last_submit_message = None
process_status = self._controller.get_process_item_status(
self._process_item_id
)
push_failed = process_status["failed"]
fail_traceback = process_status["full_traceback"]
failed_pushes = []
fail_tracebacks = []
for process_item in self._controller.get_process_items().values():
process_status = process_item.get_status_data()
if process_status["failed"]:
failed_pushes.append(process_status)
# push_failed = process_status["failed"]
# fail_traceback = process_status["full_traceback"]
if self._main_thread_timer_can_stop:
self._main_thread_timer.stop()
self._overlay_close_btn.setVisible(True)
if push_failed:
if failed_pushes:
self._overlay_try_btn.setVisible(True)
if fail_traceback:
fail_tracebacks = [
process_status["full_traceback"]
for process_status in failed_pushes
if process_status["full_traceback"]
]
if fail_tracebacks:
self._show_detail_btn.setVisible(True)
if push_failed:
reason = process_status["fail_reason"]
if fail_traceback:
if failed_pushes:
reasons = [
process_status["fail_reason"]
for process_status in failed_pushes
]
if fail_tracebacks:
reason = "\n".join(reasons)
message = (
"Unhandled error happened."
" Check error detail for more information."
)
self._error_detail_dialog.set_detail(
reason, fail_traceback
reason, "\n".join(fail_tracebacks)
)
else:
message = f"Push Failed:\n{reason}"
message = f"Push Failed:\n{reasons}"
self._overlay_label.setText(message)
set_style_property(self._overlay_close_btn, "state", "error")