Merge remote-tracking branch 'origin/feature/909-define-basic-trait-type-using-dataclasses' into feature/911-new-traits-based-integrator

This commit is contained in:
Ondřej Samohel 2024-12-09 16:26:08 +01:00
commit 40f11ef0b6
No known key found for this signature in database
GPG key ID: 02376E18990A97C6
3 changed files with 175 additions and 85 deletions

View file

@ -6,6 +6,7 @@ from .interfaces import (
ITrayAction,
ITrayService,
IHostAddon,
ITraits,
)
from .base import (
@ -30,6 +31,7 @@ __all__ = (
"ITrayAction",
"ITrayService",
"IHostAddon",
"ITraits",
"ProcessPreparationError",
"ProcessContext",

View file

@ -1,13 +1,24 @@
from __future__ import annotations
import logging
from abc import ABCMeta, abstractmethod
from typing import TYPE_CHECKING, Callable, Optional, Type
from ayon_core import resources
if TYPE_CHECKING:
from qtpy import QtWidgets
from ayon_core.addon import AddonsManager
from ayon_core.pipeline.traits import TraitBase
from ayon_core.tools.tray import TrayManager
class _AYONInterfaceMeta(ABCMeta):
"""AYONInterface meta class to print proper string."""
"""AYONInterface metaclass to print proper string."""
def __str__(self):
return "<'AYONInterface.{}'>".format(self.__name__)
return f"<'AYONInterface.{self.__name__}'>"
def __repr__(self):
return str(self)
@ -24,7 +35,11 @@ class AYONInterface(metaclass=_AYONInterfaceMeta):
in the interface. By default, interface does not have any abstract parts.
"""
pass
log = None
def __init__(self):
"""Initialize interface."""
self.log = logging.getLogger(self.__class__.__name__)
class IPluginPaths(AYONInterface):
@ -38,10 +53,25 @@ class IPluginPaths(AYONInterface):
"""
@abstractmethod
def get_plugin_paths(self):
pass
def get_plugin_paths(self) -> dict[str, list[str]]:
"""Return plugin paths for addon.
def _get_plugin_paths_by_type(self, plugin_type):
Returns:
dict[str, list[str]]: Plugin paths for addon.
"""
def _get_plugin_paths_by_type(
self, plugin_type: str) -> list[str]:
"""Get plugin paths by type.
Args:
plugin_type (str): Type of plugin paths to get.
Returns:
list[str]: List of plugin paths.
"""
paths = self.get_plugin_paths()
if not paths or plugin_type not in paths:
return []
@ -54,7 +84,7 @@ class IPluginPaths(AYONInterface):
paths = [paths]
return paths
def get_create_plugin_paths(self, host_name):
def get_create_plugin_paths(self, host_name: str) -> list[str]:
"""Receive create plugin paths.
Give addons ability to add create plugin paths based on host name.
@ -65,11 +95,11 @@ class IPluginPaths(AYONInterface):
Args:
host_name (str): For which host are the plugins meant.
"""
"""
return self._get_plugin_paths_by_type("create")
def get_load_plugin_paths(self, host_name):
def get_load_plugin_paths(self, host_name: str) -> list[str]:
"""Receive load plugin paths.
Give addons ability to add load plugin paths based on host name.
@ -80,11 +110,11 @@ class IPluginPaths(AYONInterface):
Args:
host_name (str): For which host are the plugins meant.
"""
"""
return self._get_plugin_paths_by_type("load")
def get_publish_plugin_paths(self, host_name):
def get_publish_plugin_paths(self, host_name: str) -> list[str]:
"""Receive publish plugin paths.
Give addons ability to add publish plugin paths based on host name.
@ -95,11 +125,11 @@ class IPluginPaths(AYONInterface):
Args:
host_name (str): For which host are the plugins meant.
"""
"""
return self._get_plugin_paths_by_type("publish")
def get_inventory_action_paths(self, host_name):
def get_inventory_action_paths(self, host_name: str) -> list[str]:
"""Receive inventory action paths.
Give addons ability to add inventory action plugin paths.
@ -110,76 +140,84 @@ class IPluginPaths(AYONInterface):
Args:
host_name (str): For which host are the plugins meant.
"""
"""
return self._get_plugin_paths_by_type("inventory")
class ITrayAddon(AYONInterface):
"""Addon has special procedures when used in Tray tool.
IMPORTANT:
The addon. still must be usable if is not used in tray even if
would do nothing.
"""
Important:
The addon. still must be usable if is not used in tray even if it
would do nothing.
"""
tray_initialized = False
_tray_manager = None
manager: AddonsManager = None
_tray_manager: TrayManager = None
@abstractmethod
def tray_init(self):
def tray_init(self) -> None:
"""Initialization part of tray implementation.
Triggered between `initialization` and `connect_with_addons`.
This is where GUIs should be loaded or tray specific parts should be
prepared.
prepared
"""
pass
raise NotImplementedError
@abstractmethod
def tray_menu(self, tray_menu):
def tray_menu(self, tray_menu: QtWidgets.QMenu) -> None:
"""Add addon's action to tray menu."""
raise NotImplementedError
pass
@abstractmethod
def tray_start(self):
def tray_start(self) -> None:
"""Start procedure in tray tool."""
pass
raise NotImplementedError
@abstractmethod
def tray_exit(self):
def tray_exit(self) -> None:
"""Cleanup method which is executed on tray shutdown.
This is place where all threads should be shut.
"""
raise NotImplementedError
pass
def execute_in_main_thread(self, callback: Callable) -> None:
"""Pushes callback to the queue or process 'callback' on a main thread.
def execute_in_main_thread(self, callback):
""" Pushes callback to the queue or process 'callback' on a main thread
Some callbacks need to be processed on main thread (menu actions
must be added on main thread else they won't get triggered etc.)
Args:
callback (Callable): Function to be executed on main thread
Some callbacks need to be processed on main thread (menu actions
must be added on main thread or they won't get triggered etc.)
"""
if not self.tray_initialized:
# TODO Called without initialized tray, still main thread needed
# TODO: Called without initialized tray, still main thread needed
try:
callback()
except Exception:
except Exception: # noqa: BLE001
self.log.warning(
"Failed to execute {} in main thread".format(callback),
exc_info=True)
"Failed to execute %s callback in main thread",
str(callback), exc_info=True)
return
self.manager.tray_manager.execute_in_main_thread(callback)
self._tray_manager.tray_manager.execute_in_main_thread(callback)
def show_tray_message(self, title, message, icon=None, msecs=None):
def show_tray_message(
self,
title: str,
message: str,
icon: Optional[QtWidgets.QSystemTrayIcon]=None,
msecs: Optional[int]=None) -> None:
"""Show tray message.
Args:
@ -190,11 +228,11 @@ class ITrayAddon(AYONInterface):
msecs (int): Duration of message visibility in milliseconds.
Default is 10000 msecs, may differ by Qt version.
"""
if self._tray_manager:
self._tray_manager.show_tray_message(title, message, icon, msecs)
def add_doubleclick_callback(self, callback):
def add_doubleclick_callback(self, callback: Callable) -> None:
"""Add callback to be triggered on tray icon double click."""
if hasattr(self.manager, "add_doubleclick_callback"):
self.manager.add_doubleclick_callback(self, callback)
@ -216,16 +254,17 @@ class ITrayAction(ITrayAddon):
@property
@abstractmethod
def label(self):
def label(self) -> str:
"""Service label showed in menu."""
pass
raise NotImplementedError
@abstractmethod
def on_action_trigger(self):
def on_action_trigger(self) -> None:
"""What happens on actions click."""
pass
raise NotImplementedError
def tray_menu(self, tray_menu):
def tray_menu(self, tray_menu: QtWidgets.QMenu) -> None:
"""Add action to tray menu."""
from qtpy import QtWidgets
if self.admin_action:
@ -242,14 +281,17 @@ class ITrayAction(ITrayAddon):
action.triggered.connect(self.on_action_trigger)
self._action_item = action
def tray_start(self):
def tray_start(self) -> None:
"""Start procedure in tray tool."""
return
def tray_exit(self):
def tray_exit(self) -> None:
"""Cleanup method which is executed on tray shutdown."""
return
@staticmethod
def admin_submenu(tray_menu):
def admin_submenu(tray_menu: QtWidgets.QMenu) -> QtWidgets.QMenu:
"""Get or create admin submenu."""
if ITrayAction._admin_submenu is None:
from qtpy import QtWidgets
@ -260,20 +302,21 @@ class ITrayAction(ITrayAddon):
class ITrayService(ITrayAddon):
"""Tray service Interface."""
# Module's property
menu_action = None
menu_action: QtWidgets.QAction = None
# Class properties
_services_submenu = None
_icon_failed = None
_icon_running = None
_icon_idle = None
_services_submenu: QtWidgets.QMenu = None
_icon_failed: QtWidgets.QIcon = None
_icon_running: QtWidgets.QIcon = None
_icon_idle: QtWidgets.QIcon = None
@property
@abstractmethod
def label(self):
def label(self) -> str:
"""Service label showed in menu."""
pass
raise NotImplementedError
# TODO be able to get any sort of information to show/print
# @abstractmethod
@ -281,7 +324,8 @@ class ITrayService(ITrayAddon):
# pass
@staticmethod
def services_submenu(tray_menu):
def services_submenu(tray_menu: QtWidgets.QMenu) -> QtWidgets.QMenu:
"""Get or create services submenu."""
if ITrayService._services_submenu is None:
from qtpy import QtWidgets
@ -291,13 +335,15 @@ class ITrayService(ITrayAddon):
return ITrayService._services_submenu
@staticmethod
def add_service_action(action):
def add_service_action(action: QtWidgets.QAction) -> None:
"""Add service action to services submenu."""
ITrayService._services_submenu.addAction(action)
if not ITrayService._services_submenu.menuAction().isVisible():
ITrayService._services_submenu.menuAction().setVisible(True)
@staticmethod
def _load_service_icons():
def _load_service_icons() -> None:
"""Load service icons."""
from qtpy import QtGui
ITrayService._failed_icon = QtGui.QIcon(
@ -311,24 +357,28 @@ class ITrayService(ITrayAddon):
)
@staticmethod
def get_icon_running():
def get_icon_running() -> QtWidgets.QIcon:
"""Get running icon."""
if ITrayService._icon_running is None:
ITrayService._load_service_icons()
return ITrayService._icon_running
@staticmethod
def get_icon_idle():
def get_icon_idle() -> QtWidgets.QIcon:
"""Get idle icon."""
if ITrayService._icon_idle is None:
ITrayService._load_service_icons()
return ITrayService._icon_idle
@staticmethod
def get_icon_failed():
if ITrayService._failed_icon is None:
def get_icon_failed() -> QtWidgets.QIcon:
"""Get failed icon."""
if ITrayService._icon_failed is None:
ITrayService._load_service_icons()
return ITrayService._failed_icon
return ITrayService._icon_failed
def tray_menu(self, tray_menu):
def tray_menu(self, tray_menu: QtWidgets.QMenu) -> None:
"""Add service to tray menu."""
from qtpy import QtWidgets
action = QtWidgets.QAction(
@ -341,21 +391,18 @@ class ITrayService(ITrayAddon):
self.set_service_running_icon()
def set_service_running_icon(self):
def set_service_running_icon(self) -> None:
"""Change icon of an QAction to green circle."""
if self.menu_action:
self.menu_action.setIcon(self.get_icon_running())
def set_service_failed_icon(self):
def set_service_failed_icon(self) -> None:
"""Change icon of an QAction to red circle."""
if self.menu_action:
self.menu_action.setIcon(self.get_icon_failed())
def set_service_idle_icon(self):
def set_service_idle_icon(self) -> None:
"""Change icon of an QAction to orange circle."""
if self.menu_action:
self.menu_action.setIcon(self.get_icon_idle())
@ -365,18 +412,31 @@ class IHostAddon(AYONInterface):
@property
@abstractmethod
def host_name(self):
def host_name(self) -> str:
"""Name of host which addon represents."""
raise NotImplementedError
pass
def get_workfile_extensions(self):
def get_workfile_extensions(self) -> list[str]:
"""Define workfile extensions for host.
Not all hosts support workfiles thus this is optional implementation.
Returns:
List[str]: Extensions used for workfiles with dot.
"""
"""
return []
class ITraits(AYONInterface):
"""Interface for traits."""
@abstractmethod
def get_addon_traits(self) -> list[Type[TraitBase]]:
"""Get trait classes for the addon.
Returns:
list[Type[TraitBase]]: Traits for the addon.
"""
raise NotImplementedError

View file

@ -271,13 +271,41 @@ rep = Representation.from_dict(name="image", rep_dict)
```
## Future
Apart of some new additions to traits if needed, there are few thing that needs to be done.
## Addon specific traits
### Traits plugin system
Addon can define its own traits. To do so, it needs to implement `ITraits` interface:
Traits are now ordinary python classes, but to extend its usability more, it would be good to
have addon level API to expose traits defined by individual addons. This API would then be used not
only by discovery logic but also by the AYON server that can display and work with the information
defined by them.
```python
from ayon_core.pipeline.traits import TraitBase
from ayon_core.addon import (
AYONAddon,
ITraits,
)
class MyTraitFoo(TraitBase):
id = "myaddon.mytrait.foo.v1"
name = "My Trait Foo"
description = "This is my trait foo"
persistent = True
class MyTraitBar(TraitBase):
id = "myaddon.mytrait.bar.v1"
name = "My Trait Bar"
description = "This is my trait bar"
persistent = True
class MyAddon(AYONAddon, ITraits):
def __init__(self):
super().__init__()
def get_addon_traits(self):
return [
MyTraitFoo,
MyTraitBar,
]
```