ayon-core/pype/modules/base.py
2020-12-22 17:17:54 +01:00

576 lines
17 KiB
Python

# -*- coding: utf-8 -*-
"""Base class for Pype Modules."""
import inspect
import logging
from uuid import uuid4
from abc import ABCMeta, abstractmethod
import six
import pype
from pype.settings import get_system_settings
from pype.lib import PypeLogger
from pype import resources
@six.add_metaclass(ABCMeta)
class PypeModule:
"""Base class of pype module.
Attributes:
id (UUID): Module's id.
enabled (bool): Is module enabled.
name (str): Module name.
manager (ModulesManager): Manager that created the module.
"""
# Disable by default
enabled = False
_id = None
@property
@abstractmethod
def name(self):
"""Module's name."""
pass
def __init__(self, manager, settings):
self.manager = manager
self.log = PypeLogger().get_logger(self.name)
self.initialize(settings)
@property
def id(self):
if self._id is None:
self._id = uuid4()
return self._id
@abstractmethod
def initialize(self, module_settings):
"""Initialization of module attributes.
It is not recommended to override __init__ that's why specific method
was implemented.
"""
pass
@abstractmethod
def connect_with_modules(self, enabled_modules):
"""Connect with other enabled modules."""
pass
def get_global_environments(self):
"""Get global environments values of module.
Environment variables that can be get only from system settings.
"""
return {}
@six.add_metaclass(ABCMeta)
class IPluginPaths:
"""Module has plugin paths to return.
Expected result is dictionary with keys "publish", "create", "load" or
"actions" and values as list or string.
{
"publish": ["path/to/publish_plugins"]
}
"""
# TODO validation of an output
@abstractmethod
def get_plugin_paths(self):
pass
@six.add_metaclass(ABCMeta)
class ILaunchHookPaths:
"""Module has launch hook paths to return.
Expected result is list of paths.
["path/to/launch_hooks_dir"]
"""
@abstractmethod
def get_launch_hook_paths(self):
pass
@six.add_metaclass(ABCMeta)
class ITrayModule:
"""Module has special procedures when used in Pype Tray.
IMPORTANT:
The module still must be usable if is not used in tray even if
would do nothing.
"""
tray_initialized = False
@abstractmethod
def tray_init(self):
"""Initialization part of tray implementation.
Triggered between `initialization` and `connect_with_modules`.
This is where GUIs should be loaded or tray specific parts should be
prepared.
"""
pass
@abstractmethod
def tray_menu(self, tray_menu):
"""Add module's action to tray menu."""
pass
@abstractmethod
def tray_start(self):
"""Start procedure in Pype tray."""
pass
@abstractmethod
def tray_exit(self):
"""Cleanup method which is executed on tray shutdown.
This is place where all threads should be shut.
"""
pass
class ITrayAction(ITrayModule):
"""Implementation of Tray action.
Add action to tray menu which will trigger `on_action_trigger`.
It is expected to be used for showing tools.
Methods `tray_start`, `tray_exit` and `connect_with_modules` are overriden
as it's not expected that action will use them. But it is possible if
necessary.
"""
@property
@abstractmethod
def label(self):
"""Service label showed in menu."""
pass
@abstractmethod
def on_action_trigger(self):
"""What happens on actions click."""
pass
def tray_menu(self, tray_menu):
from Qt import QtWidgets
action = QtWidgets.QAction(self.label, tray_menu)
action.triggered.connect(self.on_action_trigger)
tray_menu.addAction(action)
def tray_start(self):
return
def tray_exit(self):
return
class ITrayService(ITrayModule):
# Module's property
menu_action = None
# Class properties
_services_submenu = None
_icon_failed = None
_icon_running = None
_icon_idle = None
@property
@abstractmethod
def label(self):
"""Service label showed in menu."""
pass
# TODO be able to get any sort of information to show/print
# @abstractmethod
# def get_service_info(self):
# pass
@staticmethod
def services_submenu(tray_menu):
if ITrayService._services_submenu is None:
from Qt import QtWidgets
services_submenu = QtWidgets.QMenu("Services", tray_menu)
services_submenu.menuAction().setVisible(False)
ITrayService._services_submenu = services_submenu
return ITrayService._services_submenu
@staticmethod
def add_service_action(action):
ITrayService._services_submenu.addAction(action)
if not ITrayService._services_submenu.menuAction().isVisible():
ITrayService._services_submenu.menuAction().setVisible(True)
@staticmethod
def _load_service_icons():
from Qt import QtGui
ITrayService._failed_icon = QtGui.QIcon(
resources.get_resource("icons", "circle_red.png")
)
ITrayService._icon_running = QtGui.QIcon(
resources.get_resource("icons", "circle_green.png")
)
ITrayService._icon_idle = QtGui.QIcon(
resources.get_resource("icons", "circle_orange.png")
)
@staticmethod
def get_icon_running():
if ITrayService._icon_running is None:
ITrayService._load_service_icons()
return ITrayService._icon_running
@staticmethod
def get_icon_idle():
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:
ITrayService._load_service_icons()
return ITrayService._failed_icon
def tray_menu(self, tray_menu):
from Qt import QtWidgets
action = QtWidgets.QAction(
self.label,
self.services_submenu(tray_menu)
)
self.menu_action = action
self.add_service_action(action)
self.set_service_running_icon()
def set_service_running_icon(self):
"""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):
"""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):
"""Change icon of an QAction to orange circle."""
if self.menu_action:
self.menu_action.setIcon(self.get_icon_idle())
class ModulesManager:
def __init__(self):
self.log = logging.getLogger(self.__class__.__name__)
self.modules = []
self.modules_by_id = {}
self.modules_by_name = {}
self.initialize_modules()
self.connect_modules()
def initialize_modules(self):
"""Import and initialize modules."""
self.log.debug("*** Pype modules initialization.")
# Prepare settings for modules
modules_settings = get_system_settings()["modules"]
# Go through globals in `pype.modules`
for name in dir(pype.modules):
modules_item = getattr(pype.modules, name, None)
# Filter globals that are not classes which inherit from PypeModule
if (
not inspect.isclass(modules_item)
or modules_item is pype.modules.PypeModule
or not issubclass(modules_item, pype.modules.PypeModule)
):
continue
# Check if class is abstract (Developing purpose)
if inspect.isabstract(modules_item):
# Find missing implementations by convetion on `abc` module
not_implemented = []
for attr_name in dir(modules_item):
attr = getattr(modules_item, attr_name, None)
if attr and getattr(attr, "__isabstractmethod__", None):
not_implemented.append(attr_name)
# Log missing implementations
self.log.warning((
"Skipping abstract Class: {}. Missing implementations: {}"
).format(name, ", ".join(not_implemented)))
continue
try:
# Try initialize module
module = modules_item(self, modules_settings)
# Store initialized object
self.modules.append(module)
self.modules_by_id[module.id] = module
self.modules_by_name[module.name] = module
enabled_str = "X"
if not module.enabled:
enabled_str = " "
self.log.debug("[{}] {}".format(enabled_str, name))
except Exception:
self.log.warning(
"Initialization of module {} failed.".format(name),
exc_info=True
)
def connect_modules(self):
"""Trigger connection with other enabled modules.
Modules should handle their interfaces in `connect_with_modules`.
"""
enabled_modules = self.get_enabled_modules()
self.log.debug("Has {} enabled modules.".format(len(enabled_modules)))
for module in enabled_modules:
try:
module.connect_with_modules(enabled_modules)
except Exception:
self.log.error(
"BUG: Module failed on connection with other modules.",
exc_info=True
)
def get_enabled_modules(self):
"""Enabled modules initialized by the manager.
Returns:
list: Initialized and enabled modules.
"""
return [
module
for module in self.modules
if module.enabled
]
def collect_global_environments(self):
"""Helper to collect global enviornment variabled from modules.
Returns:
dict: Global environment variables from enabled modules.
Raises:
AssertionError: Gobal environment variables must be unique for
all modules.
"""
module_envs = {}
for module in self.get_enabled_modules():
# Collect global module's global environments
_envs = module.get_global_environments()
for key, value in _envs.items():
if key in module_envs:
# TODO better error message
raise AssertionError(
"Duplicated environment key {}".format(key)
)
module_envs[key] = value
return module_envs
def collect_plugin_paths(self):
"""Helper to collect all plugins from modules inherited IPluginPaths.
Unknown keys are logged out.
Returns:
dict: Output is dictionary with keys "publish", "create", "load"
and "actions" each containing list of paths.
"""
# Output structure
output = {
"publish": [],
"create": [],
"load": [],
"actions": []
}
unknown_keys_by_module = {}
for module in self.get_enabled_modules():
# Skip module that do not inherit from `IPluginPaths`
if not isinstance(module, IPluginPaths):
continue
plugin_paths = module.get_plugin_paths()
for key, value in plugin_paths.items():
# Filter unknown keys
if key not in output:
if module.name not in unknown_keys_by_module:
unknown_keys_by_module[module.name] = []
unknown_keys_by_module[module.name].append(key)
continue
# Skip if value is empty
if not value:
continue
# Convert to list if value is not list
if not isinstance(value, (list, tuple, set)):
value = [value]
output[key].extend(value)
# Report unknown keys (Developing purposes)
if unknown_keys_by_module:
expected_keys = ", ".join([
"\"{}\"".format(key) for key in output.keys()
])
msg_template = "Module: \"{}\" - got key {}"
msg_items = []
for module_name, keys in unknown_keys_by_module.items():
joined_keys = ", ".join([
"\"{}\"".format(key) for key in keys
])
msg_items.append(msg_template.format(module_name, joined_keys))
self.log.warning((
"Expected keys from `get_plugin_paths` are {}. {}"
).format(expected_keys, " | ".join(msg_items)))
return output
def collect_launch_hook_paths(self):
"""Helper to collect hooks from modules inherited ILaunchHookPaths.
Returns:
list: Paths to launch hook directories.
"""
str_type = type("")
expected_types = (list, tuple, set)
output = []
for module in self.get_enabled_modules():
# Skip module that do not inherit from `ILaunchHookPaths`
if not isinstance(module, ILaunchHookPaths):
continue
hook_paths = module.get_launch_hook_paths()
if not hook_paths:
continue
# Convert string to list
if isinstance(hook_paths, str_type):
hook_paths = [hook_paths]
# Skip invalid types
if not isinstance(hook_paths, expected_types):
self.log.warning((
"Result of `get_launch_hook_paths`"
" has invalid type {}. Expected {}"
).format(type(hook_paths), expected_types))
continue
output.extend(hook_paths)
return output
class TrayModulesManager(ModulesManager):
# Define order of modules in menu
modules_menu_order = (
"user",
"ftrack",
"muster",
"launcher_tool",
"avalon",
"clockify",
"standalonepublish_tool",
"log_viewer",
"settings"
)
def __init__(self):
self.log = PypeLogger().get_logger(self.__class__.__name__)
self.modules = []
self.modules_by_id = {}
self.modules_by_name = {}
def initialize(self, tray_menu):
self.initialize_modules()
self.tray_init()
self.connect_modules()
self.tray_menu(tray_menu)
def get_enabled_tray_modules(self):
output = []
for module in self.modules:
if module.enabled and isinstance(module, ITrayModule):
output.append(module)
return output
def tray_init(self):
for module in self.get_enabled_tray_modules():
try:
module.tray_init()
module.tray_initialized = True
except Exception:
self.log.warning(
"Module \"{}\" crashed on `tray_init`.".format(
module.name
),
exc_info=True
)
def tray_menu(self, tray_menu):
ordered_modules = []
enabled_by_name = {
module.name: module
for module in self.get_enabled_tray_modules()
}
for name in self.modules_menu_order:
module_by_name = enabled_by_name.pop(name, None)
if module_by_name:
ordered_modules.append(module_by_name)
ordered_modules.extend(enabled_by_name.values())
for module in ordered_modules:
if not module.tray_initialized:
continue
try:
module.tray_menu(tray_menu)
except Exception:
# Unset initialized mark
module.tray_initialized = False
self.log.warning(
"Module \"{}\" crashed on `tray_menu`.".format(
module.name
),
exc_info=True
)
def start_modules(self):
for module in self.get_enabled_tray_modules():
if not module.tray_initialized:
if isinstance(module, ITrayService):
module.set_service_failed_icon()
continue
try:
module.tray_start()
except Exception:
self.log.warning(
"Module \"{}\" crashed on `tray_start`.".format(
module.name
),
exc_info=True
)
def on_exit(self):
for module in self.get_enabled_tray_modules():
if module.tray_initialized:
try:
module.tray_exit()
except Exception:
self.log.warning(
"Module \"{}\" crashed on `tray_exit`.".format(
module.name
),
exc_info=True
)