Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Addons: Preparation steps for child processes #781

Merged
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ec419f5
added 'ensure_is_process_ready' method to addon base class
iLLiCiTiT Jul 15, 2024
9de1f23
prepare utils to run process preparation
iLLiCiTiT Jul 15, 2024
01983de
implemented dialog showing error
iLLiCiTiT Jul 15, 2024
0331722
add missing import
iLLiCiTiT Jul 16, 2024
c866139
better handling of the error
iLLiCiTiT Jul 17, 2024
54f2269
'run_ayon_launcher_process' can add sys path to python path
iLLiCiTiT Jul 17, 2024
909e88b
run the UI by adding sys path and skipping bootstrap
iLLiCiTiT Jul 17, 2024
4fefd1e
Fix grammar
iLLiCiTiT Jul 17, 2024
a256d0a
Merge branch 'develop' into feature/AY-6021_Addons-initialization-for…
iLLiCiTiT Jul 23, 2024
e7f8167
store tracebacks in case stdout is not available
iLLiCiTiT Jul 23, 2024
e15a2b2
fix ruff linting
iLLiCiTiT Jul 23, 2024
8eedbd0
Merge branch 'develop' into feature/AY-6021_Addons-initialization-for…
iLLiCiTiT Jul 24, 2024
ddd5313
add optional output of ensude function
iLLiCiTiT Jul 30, 2024
cb0dfc0
Merge branch 'develop' into feature/AY-6021_Addons-initialization-for…
iLLiCiTiT Jul 30, 2024
f8e1b6e
added one more ensure function for easier approach.
iLLiCiTiT Jul 30, 2024
943245b
start tray if is not running
iLLiCiTiT Jul 30, 2024
fa729cd
don't require process context to start tray function
iLLiCiTiT Jul 30, 2024
e68345d
Merge branch 'develop' into feature/AY-6021_Addons-initialization-for…
iLLiCiTiT Jul 31, 2024
041b93d
use power of sets
iLLiCiTiT Aug 1, 2024
20c3381
Merge branch 'develop' into feature/AY-6021_Addons-initialization-for…
iLLiCiTiT Aug 1, 2024
3e0d422
fix paths ordering
iLLiCiTiT Aug 1, 2024
a90c7a0
update lookup set
iLLiCiTiT Aug 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions client/ayon_core/addon/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,18 @@
)

from .base import (
ProcessPreparationError,
ProcessContext,
AYONAddon,
AddonsManager,
load_addons,
)

from .utils import (
ensure_addons_are_process_context_ready,
ensure_addons_are_process_ready,
)


__all__ = (
"click_wrap",
Expand All @@ -24,7 +31,12 @@
"ITrayService",
"IHostAddon",

"ProcessPreparationError",
"ProcessContext",
"AYONAddon",
"AddonsManager",
"load_addons",

"ensure_addons_are_process_context_ready",
"ensure_addons_are_process_ready",
)
73 changes: 73 additions & 0 deletions client/ayon_core/addon/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import collections
from uuid import uuid4
from abc import ABC, abstractmethod
from typing import Optional

import appdirs
import ayon_api
Expand Down Expand Up @@ -64,6 +65,56 @@
}


class ProcessPreparationError(Exception):
"""Exception that can be used when process preparation failed.

The message is shown to user (either as UI dialog or printed). If
different error is raised a "generic" error message is shown to user
with option to copy error message to clipboard.

"""
pass


class ProcessContext:
"""Context of child process.

Notes:
This class is used to pass context to child process. It can be used
to use different behavior of addon based on information in
the context.
The context can be enhanced in future versions.

Args:
addon_name (Optional[str]): Addon name which triggered process.
addon_version (Optional[str]): Addon version which triggered process.
project_name (Optional[str]): Project name. Can be filled in case
process is triggered for specific project. Some addons can have
different behavior based on project.
headless (Optional[bool]): Is process running in headless mode.

"""
def __init__(
self,
addon_name: Optional[str] = None,
addon_version: Optional[str] = None,
project_name: Optional[str] = None,
headless: Optional[bool] = None,
**kwargs,
):
if headless is None:
# TODO use lib function to get headless mode
headless = os.getenv("AYON_HEADLESS_MODE") == "1"
self.addon_name: Optional[str] = addon_name
self.addon_version: Optional[str] = addon_version
self.project_name: Optional[str] = project_name
self.headless: bool = headless

if kwargs:
unknown_keys = ", ".join([f'"{key}"' for key in kwargs.keys()])
print(f"Unknown keys in ProcessContext: {unknown_keys}")


# Inherit from `object` for Python 2 hosts
class _ModuleClass(object):
"""Fake module class for storing AYON addons.
Expand Down Expand Up @@ -584,7 +635,29 @@ def connect_with_addons(self, enabled_addons):
Args:
enabled_addons (list[AYONAddon]): Addons that are enabled.
"""
pass

def ensure_is_process_ready(
self, process_context: ProcessContext
):
"""Make sure addon is prepared for a process.

This method is called when some action makes sure that addon has set
necessary data. For example if user should be logged in
and filled credentials in environment variables this method should
ask user for credentials.

Implementation of this method is optional.

Note:
The logic can be similar to logic in tray, but tray does not require
to be logged in.

Args:
process_context (ProcessContext): Context of child
process.

"""
pass

def get_global_environments(self):
Expand Down
132 changes: 132 additions & 0 deletions client/ayon_core/addon/ui/process_ready_error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import sys
import json
from typing import Optional

from qtpy import QtWidgets, QtCore

from ayon_core.style import load_stylesheet
from ayon_core.tools.utils import get_ayon_qt_app


class DetailDialog(QtWidgets.QDialog):
def __init__(self, detail, parent):
super().__init__(parent)

self.setWindowTitle("Detail")

detail_input = QtWidgets.QPlainTextEdit(self)
detail_input.setPlainText(detail)
detail_input.setReadOnly(True)

layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(detail_input, 1)

def showEvent(self, event):
self.resize(600, 400)
super().showEvent(event)


class ErrorDialog(QtWidgets.QDialog):
def __init__(
self,
message: str,
detail: Optional[str],
parent: Optional[QtWidgets.QWidget] = None
):
super().__init__(parent)

self.setWindowTitle("Preparation failed")
self.setWindowFlags(
self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint
)

message_label = QtWidgets.QLabel(self)

detail_wrapper = QtWidgets.QWidget(self)

detail_label = QtWidgets.QLabel(detail_wrapper)

detail_layout = QtWidgets.QVBoxLayout(detail_wrapper)
detail_layout.setContentsMargins(0, 0, 0, 0)
detail_layout.addWidget(detail_label)

btns_wrapper = QtWidgets.QWidget(self)

copy_detail_btn = QtWidgets.QPushButton("Copy detail", btns_wrapper)
show_detail_btn = QtWidgets.QPushButton("Show detail", btns_wrapper)
confirm_btn = QtWidgets.QPushButton("Close", btns_wrapper)

btns_layout = QtWidgets.QHBoxLayout(btns_wrapper)
btns_layout.setContentsMargins(0, 0, 0, 0)
btns_layout.addWidget(copy_detail_btn, 0)
btns_layout.addWidget(show_detail_btn, 0)
btns_layout.addStretch(1)
btns_layout.addWidget(confirm_btn, 0)

layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(message_label, 0)
layout.addWidget(detail_wrapper, 1)
layout.addWidget(btns_wrapper, 0)

copy_detail_btn.clicked.connect(self._on_copy_clicked)
show_detail_btn.clicked.connect(self._on_show_detail_clicked)
confirm_btn.clicked.connect(self._on_confirm_clicked)

self._message_label = message_label
self._detail_wrapper = detail_wrapper
self._detail_label = detail_label

self._copy_detail_btn = copy_detail_btn
self._show_detail_btn = show_detail_btn
self._confirm_btn = confirm_btn

self._detail_dialog = None

self._detail = detail

self.set_message(message, detail)

def showEvent(self, event):
self.setStyleSheet(load_stylesheet())
self.resize(320, 140)
super().showEvent(event)

def set_message(self, message, detail):
self._message_label.setText(message)
self._detail = detail

for widget in (
self._copy_detail_btn,
self._show_detail_btn,
):
widget.setVisible(bool(detail))

def _on_copy_clicked(self):
if self._detail:
clipboard = QtWidgets.QApplication.clipboard()
clipboard.setText(self._detail)

def _on_show_detail_clicked(self):
if self._detail_dialog is None:
self._detail_dialog = DetailDialog(self._detail, self)
self._detail_dialog.show()

def _on_confirm_clicked(self):
self.accept()


def main():
json_path = sys.argv[-1]
with open(json_path, "r") as stream:
data = json.load(stream)

message = data["message"]
detail = data["detail"]
app = get_ayon_qt_app()
dialog = ErrorDialog(message, detail)
dialog.show()
app.exec_()


if __name__ == "__main__":
main()
Loading