diff --git a/pype/lib/python_module_tools.py b/pype/lib/python_module_tools.py index 2ce2f60dca..b5400c9981 100644 --- a/pype/lib/python_module_tools.py +++ b/pype/lib/python_module_tools.py @@ -18,10 +18,20 @@ def modules_from_path(folder_path): Returns: List of modules. """ + modules = [] + # Just skip and return empty list if path is not set + if not folder_path: + return modules + + # Do not allow relative imports + if folder_path.startswith("."): + log.warning(( + "BUG: Relative paths are not allowed for security reasons. {}" + ).format(folder_path)) + return modules folder_path = os.path.normpath(folder_path) - modules = [] if not os.path.isdir(folder_path): log.warning("Not a directory path: {}".format(folder_path)) return modules diff --git a/pype/modules/README.md b/pype/modules/README.md new file mode 100644 index 0000000000..818375461f --- /dev/null +++ b/pype/modules/README.md @@ -0,0 +1,103 @@ +# Pype modules +Pype modules should contain separated logic of specific kind of implementation, like Ftrack connection and usage code or Deadline farm rendering. + +## Base class `PypeModule` +- abstract class as base for each module +- implementation should be module's api withou GUI parts +- may implement `get_global_environments` method which should return dictionary of environments that are globally appliable and value is the same for whole studio if launched at any workstation (except os specific paths) +- abstract parts: + - `name` attribute - name of a module + - `initialize` method - method for own initialization of a module (should not override `__init__`) + - `connect_with_modules` method - where module may look for it's interfaces implementations or check for other modules +- `__init__` should not be overriden and `initialize` should not do time consuming part but only prepare base data about module + - also keep in mind that they may be initialized in headless mode +- connection with other modules is made with help of interfaces + +# Interfaces +- interface is class that has defined abstract methods to implement and may contain preimplemented helper methods +- module that inherit from an interface must implement those abstract methods otherwise won't be initialized +- it is easy to find which module object inherited from which interfaces withh 100% chance they have implemented required methods + +## Global interfaces +- few interfaces are implemented for global usage + +### IPluginPaths +- module want to add directory path/s to avalon or publish plugins +- module must implement `get_plugin_paths` which must return dictionary with possible keys `"publish"`, `"load"`, `"create"` or `"actions"` + - each key may contain list or string with path to directory with plugins + +### ITrayModule +- module has more logic when used in tray + - it is possible that module can be used only in tray +- abstract methods + - `tray_init` - initialization triggered after `initialize` when used in `TrayModulesManager` and before `connect_with_modules` + - `tray_menu` - add actions to tray widget's menu that represent the module + - `tray_start` - start of module's login in tray + - module is initialized and connected with other modules + - `tray_exit` - module's cleanup like stop and join threads etc. + - order of calling is based on implementation this order is how it works with `TrayModulesManager` + - it is recommended to import and use GUI implementaion only in these methods +- has attribute `tray_initialized` (bool) which is set to False by default and is set by `TrayModulesManager` to True after `tray_init` + - if module has logic only in tray or for both then should be checking for `tray_initialized` attribute to decide how should handle situations + +### ITrayService +- inherit from `ITrayModule` and implement `tray_menu` method for you + - add action to submenu "Services" in tray widget menu with icon and label +- abstract atttribute `label` + - label shown in menu +- interface has preimplemented methods to change icon color + - `set_service_running` - green icon + - `set_service_failed` - red icon + - `set_service_idle` - orange icon + - these states must be set by module itself `set_service_running` is default state on initialization + +### ITrayAction +- inherit from `ITrayModule` and implement `tray_menu` method for you + - add action to tray widget menu with label +- abstract atttribute `label` + - label shown in menu +- abstract method `on_action_trigger` + - what should happen when action is triggered +- NOTE: It is good idea to implement logic in `on_action_trigger` to api method and trigger that methods on callbacks this gives ability to trigger that method outside tray + +## Modules interfaces +- modules may have defined their interfaces to be able recognize other modules that would want to use their features +- +### Example: +- Ftrack module has `IFtrackEventHandlerPaths` which helps to tell Ftrack module which of other modules want to add paths to server/user event handlers + - Clockify module use `IFtrackEventHandlerPaths` and return paths to clockify ftrack synchronizers + +- Clockify has more inharitance it's class definition looks like +``` +class ClockifyModule( + PypeModule, # Says it's Pype module so ModulesManager will try to initialize. + ITrayModule, # Says has special implementation when used in tray. + IPluginPaths, # Says has plugin paths that want to register (paths to clockify actions for launcher). + IFtrackEventHandlerPaths, # Says has Ftrack actions/events for user/server. + ITimersManager # Listen to other modules with timer and can trigger changes in other module timers through `TimerManager` module. +): +``` + +### ModulesManager +- collect module classes and tries to initialize them +- important attributes + - `modules` - list of available attributes + - `modules_by_id` - dictionary of modules mapped by their ids + - `modules_by_name` - dictionary of modules mapped by their names + - all these attributes contain all found modules even if are not enabled +- helper methods + - `collect_global_environments` to collect all global environments from enabled modules with calling `get_global_environments` on each of them + - `collect_plugin_paths` collect plugin paths from all enabled modules + - output is always dictionary with all keys and values as list + ``` + { + "publish": [], + "create": [], + "load": [], + "actions": [] + } + ``` + +### TrayModulesManager +- inherit from `ModulesManager` +- has specific implementations for Pype Tray tool and handle `ITrayModule` methods diff --git a/pype/modules/__init__.py b/pype/modules/__init__.py index 9fa985ce57..4f76dc2df0 100644 --- a/pype/modules/__init__.py +++ b/pype/modules/__init__.py @@ -2,12 +2,13 @@ from .base import ( PypeModule, ITrayModule, + ITrayAction, ITrayService, IPluginPaths, ModulesManager, TrayModulesManager ) -from .settings_module import SettingsModule +from .settings_action import SettingsAction from .rest_api import ( RestApiModule, IRestApi @@ -25,6 +26,7 @@ from .timers_manager import ( ITimersManager ) from .avalon_apps import AvalonModule +from .launcher_action import LauncherAction from .ftrack import ( FtrackModule, IFtrackEventHandlerPaths @@ -32,7 +34,7 @@ from .ftrack import ( from .clockify import ClockifyModule from .logging import LoggingModule from .muster import MusterModule -from .standalonepublish import StandAlonePublishModule +from .standalonepublish_action import StandAlonePublishAction from .websocket_server import WebsocketModule from .sync_server import SyncServer @@ -40,12 +42,13 @@ from .sync_server import SyncServer __all__ = ( "PypeModule", "ITrayModule", + "ITrayAction", "ITrayService", "IPluginPaths", "ModulesManager", "TrayModulesManager", - "SettingsModule", + "SettingsAction", "UserModule", "IUserModule", @@ -60,6 +63,7 @@ __all__ = ( "IRestApi", "AvalonModule", + "LauncherAction", "FtrackModule", "IFtrackEventHandlerPaths", @@ -68,7 +72,7 @@ __all__ = ( "IdleManager", "LoggingModule", "MusterModule", - "StandAlonePublishModule", + "StandAlonePublishAction", "WebsocketModule", "SyncServer" diff --git a/pype/modules/avalon_apps/avalon_app.py b/pype/modules/avalon_apps/avalon_app.py index 683d804412..d00a306e9e 100644 --- a/pype/modules/avalon_apps/avalon_app.py +++ b/pype/modules/avalon_apps/avalon_app.py @@ -44,7 +44,6 @@ class AvalonModule(PypeModule, ITrayModule, IRestApi): ) # Tray attributes - self.app_launcher = None self.libraryloader = None self.rest_api_obj = None @@ -99,29 +98,8 @@ class AvalonModule(PypeModule, ITrayModule, IRestApi): exc_info=True ) - # Add launcher - try: - from pype.tools.launcher import LauncherWindow - self.app_launcher = LauncherWindow() - except Exception: - self.log.warning( - "Couldn't load Launch for tray.", - exc_info=True - ) - def connect_with_modules(self, _enabled_modules): - plugin_paths = self.manager.collect_plugin_paths()["actions"] - if plugin_paths: - env_paths_str = os.environ.get("AVALON_ACTIONS") or "" - env_paths = env_paths_str.split(os.pathsep) - env_paths.extend(plugin_paths) - os.environ["AVALON_ACTIONS"] = os.pathsep.join(env_paths) - - if self.tray_initialized: - from pype.tools.launcher import actions - # actions.register_default_actions() - actions.register_config_actions() - actions.register_environment_actions() + return def rest_api_initialization(self, rest_api_module): if self.tray_initialized: @@ -132,15 +110,12 @@ class AvalonModule(PypeModule, ITrayModule, IRestApi): def tray_menu(self, tray_menu): from Qt import QtWidgets # Actions - action_launcher = QtWidgets.QAction("Launcher", tray_menu) action_library_loader = QtWidgets.QAction( "Library loader", tray_menu ) - action_launcher.triggered.connect(self.show_launcher) action_library_loader.triggered.connect(self.show_library_loader) - tray_menu.addAction(action_launcher) tray_menu.addAction(action_library_loader) def tray_start(self, *_a, **_kw): @@ -149,12 +124,6 @@ class AvalonModule(PypeModule, ITrayModule, IRestApi): def tray_exit(self, *_a, **_kw): return - def show_launcher(self): - # if app_launcher don't exist create it/otherwise only show main window - self.app_launcher.show() - self.app_launcher.raise_() - self.app_launcher.activateWindow() - def show_library_loader(self): self.libraryloader.show() diff --git a/pype/modules/base.py b/pype/modules/base.py index 3c2c2e7e21..525320f1a7 100644 --- a/pype/modules/base.py +++ b/pype/modules/base.py @@ -124,6 +124,41 @@ class ITrayModule: 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 @@ -287,7 +322,13 @@ class ModulesManager: enabled_modules = self.get_enabled_modules() self.log.debug("Has {} enabled modules.".format(len(enabled_modules))) for module in enabled_modules: - module.connect_with_modules(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. @@ -387,6 +428,7 @@ class TrayModulesManager(ModulesManager): "user", "ftrack", "muster", + "launcher_tool", "avalon", "clockify", "standalonepublish_tool", diff --git a/pype/modules/launcher_action.py b/pype/modules/launcher_action.py new file mode 100644 index 0000000000..9c2120cf9a --- /dev/null +++ b/pype/modules/launcher_action.py @@ -0,0 +1,44 @@ +from . import PypeModule, ITrayAction + + +class LauncherAction(PypeModule, ITrayAction): + label = "Launcher" + name = "launcher_tool" + + def initialize(self, _modules_settings): + # This module is always enabled + self.enabled = True + + # Tray attributes + self.window = None + + def tray_init(self): + self.create_window() + + def tray_start(self): + return + + def connect_with_modules(self, enabled_modules): + # Register actions + if self.tray_initialized: + from pype.tools.launcher import actions + # actions.register_default_actions() + actions.register_config_actions() + actions_paths = self.manager.collect_plugin_paths()["actions"] + actions.register_actions_from_paths(actions_paths) + actions.register_environment_actions() + + def create_window(self): + if self.window: + return + from pype.tools.launcher import LauncherWindow + self.window = LauncherWindow() + + def on_action_trigger(self): + self.show_launcher() + + def show_launcher(self): + if self.window: + self.window.show() + self.window.raise_() + self.window.activateWindow() diff --git a/pype/modules/settings_module.py b/pype/modules/settings_action.py similarity index 58% rename from pype/modules/settings_module.py rename to pype/modules/settings_action.py index 0651170148..0d56a6c5ae 100644 --- a/pype/modules/settings_module.py +++ b/pype/modules/settings_action.py @@ -1,11 +1,13 @@ -from . import PypeModule, ITrayModule +from . import PypeModule, ITrayAction -class SettingsModule(PypeModule, ITrayModule): +class SettingsAction(PypeModule, ITrayAction): + """Action to show Setttings tool.""" name = "settings" + label = "Settings" def initialize(self, _modules_settings): - # This module is always enabled + # This action is always enabled self.enabled = True # User role @@ -18,13 +20,28 @@ class SettingsModule(PypeModule, ITrayModule): def connect_with_modules(self, *_a, **_kw): return + def tray_init(self): + """Initialization in tray implementation of ITrayAction.""" + self.create_settings_window() + + def on_action_trigger(self): + """Implementation for action trigger of ITrayAction.""" + self.show_settings_window() + def create_settings_window(self): + """Initializa Settings Qt window.""" if self.settings_window: return from pype.tools.settings import MainWidget self.settings_window = MainWidget(self.user_role) def show_settings_window(self): + """Show settings tool window. + + Raises: + AssertionError: Window must be already created. Call + `create_settings_window` before callint this method. + """ if not self.settings_window: raise AssertionError("Window is not initialized.") @@ -33,21 +50,3 @@ class SettingsModule(PypeModule, ITrayModule): # Pull window to the front. self.settings_window.raise_() self.settings_window.activateWindow() - - def tray_init(self): - self.create_settings_window() - - def tray_menu(self, tray_menu): - """Add **change credentials** option to tray menu.""" - from Qt import QtWidgets - - # Actions - action = QtWidgets.QAction("Settings", tray_menu) - action.triggered.connect(self.show_settings_window) - tray_menu.addAction(action) - - def tray_start(self): - return - - def tray_exit(self): - return diff --git a/pype/modules/standalonepublish/__init__.py b/pype/modules/standalonepublish/__init__.py deleted file mode 100644 index 5c40deb6f0..0000000000 --- a/pype/modules/standalonepublish/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .standalonepublish_module import StandAlonePublishModule - -__all__ = ( - "StandAlonePublishModule", -) diff --git a/pype/modules/standalonepublish/standalonepublish_module.py b/pype/modules/standalonepublish_action.py similarity index 68% rename from pype/modules/standalonepublish/standalonepublish_module.py rename to pype/modules/standalonepublish_action.py index 5b0cfe14bf..4bcb5b6018 100644 --- a/pype/modules/standalonepublish/standalonepublish_module.py +++ b/pype/modules/standalonepublish_action.py @@ -1,15 +1,15 @@ import os import sys import subprocess -import pype -from .. import PypeModule, ITrayModule +from . import PypeModule, ITrayAction -class StandAlonePublishModule(PypeModule, ITrayModule): - menu_label = "Publish" +class StandAlonePublishAction(PypeModule, ITrayAction): + label = "Publish" name = "standalonepublish_tool" def initialize(self, modules_settings): + import pype self.enabled = modules_settings[self.name]["enabled"] self.publish_paths = [ os.path.join( @@ -20,17 +20,8 @@ class StandAlonePublishModule(PypeModule, ITrayModule): def tray_init(self): return - def tray_start(self): - return - - def tray_exit(self): - return - - def tray_menu(self, parent_menu): - from Qt import QtWidgets - run_action = QtWidgets.QAction(self.menu_label, parent_menu) - run_action.triggered.connect(self.run_standalone_publisher) - parent_menu.addAction(run_action) + def on_action_trigger(self): + self.run_standalone_publisher() def connect_with_modules(self, enabled_modules): """Collect publish paths from other modules.""" diff --git a/pype/tools/launcher/actions.py b/pype/tools/launcher/actions.py index 6d0c94b676..aefa190768 100644 --- a/pype/tools/launcher/actions.py +++ b/pype/tools/launcher/actions.py @@ -85,29 +85,32 @@ def register_config_actions(): config.register_launcher_actions() -def register_environment_actions(): - """Register actions from AVALON_ACTIONS for Launcher.""" - - paths = os.environ.get("AVALON_ACTIONS") +def register_actions_from_paths(paths): if not paths: return - for path in paths.split(os.pathsep): + for path in paths: + if not path: + continue + + if path.startswith("."): + print(( + "BUG: Relative paths are not allowed for security reasons. {}" + ).format(path)) + continue + + if not os.path.exists(path): + print("Path was not found: {}".format(path)) + continue + api.register_plugin_path(api.Action, path) - # Run "register" if found. - for module in lib.modules_from_path(path): - if "register" not in dir(module): - continue - try: - module.register() - except Exception as e: - print( - "Register method in {0} failed: {1}".format( - module, str(e) - ) - ) +def register_environment_actions(): + """Register actions from AVALON_ACTIONS for Launcher.""" + + paths_str = os.environ.get("AVALON_ACTIONS") or "" + register_actions_from_paths(paths_str.split(os.pathsep)) class ApplicationAction(api.Action):