mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 05:14:40 +01:00
401 lines
9.8 KiB
Python
401 lines
9.8 KiB
Python
"""Plug-in system
|
|
|
|
Works similar to how OSs look for executables; i.e. a number of
|
|
absolute paths are searched for a given match. The predicate for
|
|
executables is whether or not an extension matches a number of
|
|
options, such as ".exe" or ".bat".
|
|
|
|
In this system, the predicate is whether or not a fname ends with ".py"
|
|
|
|
"""
|
|
|
|
# Standard library
|
|
import os
|
|
import sys
|
|
import types
|
|
import logging
|
|
import inspect
|
|
|
|
from .vendor.Qt import QtCore, QtWidgets
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
_registered_paths = list()
|
|
_registered_plugins = dict()
|
|
|
|
|
|
class classproperty(object):
|
|
def __init__(self, getter):
|
|
self.getter = getter
|
|
|
|
def __get__(self, instance, owner):
|
|
return self.getter(owner)
|
|
|
|
|
|
class Plugin(QtWidgets.QWidget):
|
|
"""Base class for Option plug-in Widgets.
|
|
|
|
This is a regular Qt widget that can be added to the capture interface
|
|
as an additional component, like a plugin.
|
|
|
|
The plug-ins are sorted in the interface by their `order` attribute and
|
|
will be displayed in the main interface when `section` is set to "app"
|
|
and displayed in the additional settings pop-up when set to "config".
|
|
|
|
When `hidden` is set to True the widget will not be shown in the interface.
|
|
This could be useful as a plug-in that supplies solely default values to
|
|
the capture GUI command.
|
|
|
|
"""
|
|
|
|
label = ""
|
|
section = "app" # "config" or "app"
|
|
hidden = False
|
|
options_changed = QtCore.Signal()
|
|
label_changed = QtCore.Signal(str)
|
|
order = 0
|
|
highlight = "border: 1px solid red;"
|
|
validate_state = True
|
|
|
|
def on_playblast_finished(self, options):
|
|
pass
|
|
|
|
def validate(self):
|
|
"""
|
|
Ensure outputs of the widget are possible, when errors are raised it
|
|
will return a message with what has caused the error
|
|
:return:
|
|
"""
|
|
errors = []
|
|
return errors
|
|
|
|
def get_outputs(self):
|
|
"""Return the options as set in this plug-in widget.
|
|
|
|
This is used to identify the settings to be used for the playblast.
|
|
As such the values should be returned in a way that a call to
|
|
`capture.capture()` would understand as arguments.
|
|
|
|
Args:
|
|
panel (str): The active modelPanel of the user. This is passed so
|
|
values could potentially be parsed from the active panel
|
|
|
|
Returns:
|
|
dict: The options for this plug-in. (formatted `capture` style)
|
|
|
|
"""
|
|
return dict()
|
|
|
|
def get_inputs(self, as_preset):
|
|
"""Return widget's child settings.
|
|
|
|
This should provide a dictionary of input settings of the plug-in
|
|
that results in a dictionary that can be supplied to `apply_input()`
|
|
This is used to save the settings of the preset to a widget.
|
|
|
|
:param as_preset:
|
|
:param as_presets: Toggle to mute certain input values of the widget
|
|
:type as_presets: bool
|
|
|
|
Returns:
|
|
dict: The currently set inputs of this widget.
|
|
|
|
"""
|
|
return dict()
|
|
|
|
def apply_inputs(self, settings):
|
|
"""Apply a dictionary of settings to the widget.
|
|
|
|
This should update the widget's inputs to the settings provided in
|
|
the dictionary. This is used to apply settings from a preset.
|
|
|
|
Returns:
|
|
None
|
|
|
|
"""
|
|
pass
|
|
|
|
def initialize(self):
|
|
"""
|
|
This method is used to register any callbacks
|
|
:return:
|
|
"""
|
|
pass
|
|
|
|
def uninitialize(self):
|
|
"""
|
|
Unregister any callback created when deleting the widget
|
|
|
|
A general explation:
|
|
|
|
The deletion method is an attribute that lives inside the object to be
|
|
deleted, and that is the problem:
|
|
Destruction seems not to care about the order of destruction,
|
|
and the __dict__ that also holds the onDestroy bound method
|
|
gets destructed before it is called.
|
|
|
|
Another solution is to use a weakref
|
|
|
|
:return: None
|
|
"""
|
|
pass
|
|
|
|
def __str__(self):
|
|
return self.label or type(self).__name__
|
|
|
|
def __repr__(self):
|
|
return u"%s.%s(%r)" % (__name__, type(self).__name__, self.__str__())
|
|
|
|
id = classproperty(lambda cls: cls.__name__)
|
|
|
|
|
|
def register_plugin_path(path):
|
|
"""Plug-ins are looked up at run-time from directories registered here
|
|
|
|
To register a new directory, run this command along with the absolute
|
|
path to where you"re plug-ins are located.
|
|
|
|
Example:
|
|
>>> import os
|
|
>>> my_plugins = "/server/plugins"
|
|
>>> register_plugin_path(my_plugins)
|
|
'/server/plugins'
|
|
|
|
Returns:
|
|
Actual path added, including any post-processing
|
|
|
|
"""
|
|
|
|
if path in _registered_paths:
|
|
return log.warning("Path already registered: {0}".format(path))
|
|
|
|
_registered_paths.append(path)
|
|
|
|
return path
|
|
|
|
|
|
def deregister_plugin_path(path):
|
|
"""Remove a _registered_paths path
|
|
|
|
Raises:
|
|
KeyError if `path` isn't registered
|
|
|
|
"""
|
|
|
|
_registered_paths.remove(path)
|
|
|
|
|
|
def deregister_all_plugin_paths():
|
|
"""Mainly used in tests"""
|
|
_registered_paths[:] = []
|
|
|
|
|
|
def registered_plugin_paths():
|
|
"""Return paths added via registration
|
|
|
|
..note:: This returns a copy of the registered paths
|
|
and can therefore not be modified directly.
|
|
|
|
"""
|
|
|
|
return list(_registered_paths)
|
|
|
|
|
|
def registered_plugins():
|
|
"""Return plug-ins added via :func:`register_plugin`
|
|
|
|
.. note:: This returns a copy of the registered plug-ins
|
|
and can therefore not be modified directly
|
|
|
|
"""
|
|
|
|
return _registered_plugins.values()
|
|
|
|
|
|
def register_plugin(plugin):
|
|
"""Register a new plug-in
|
|
|
|
Arguments:
|
|
plugin (Plugin): Plug-in to register
|
|
|
|
Raises:
|
|
TypeError if `plugin` is not callable
|
|
|
|
"""
|
|
|
|
if not hasattr(plugin, "__call__"):
|
|
raise TypeError("Plug-in must be callable "
|
|
"returning an instance of a class")
|
|
|
|
if not plugin_is_valid(plugin):
|
|
raise TypeError("Plug-in invalid: %s", plugin)
|
|
|
|
_registered_plugins[plugin.__name__] = plugin
|
|
|
|
|
|
def plugin_paths():
|
|
"""Collect paths from all sources.
|
|
|
|
This function looks at the three potential sources of paths
|
|
and returns a list with all of them together.
|
|
|
|
The sources are:
|
|
|
|
- Registered paths using :func:`register_plugin_path`
|
|
|
|
Returns:
|
|
list of paths in which plugins may be locat
|
|
|
|
"""
|
|
|
|
paths = list()
|
|
|
|
for path in registered_plugin_paths():
|
|
if path in paths:
|
|
continue
|
|
paths.append(path)
|
|
|
|
return paths
|
|
|
|
|
|
def discover(paths=None):
|
|
"""Find and return available plug-ins
|
|
|
|
This function looks for files within paths registered via
|
|
:func:`register_plugin_path`.
|
|
|
|
Arguments:
|
|
paths (list, optional): Paths to discover plug-ins from.
|
|
If no paths are provided, all paths are searched.
|
|
|
|
"""
|
|
|
|
plugins = dict()
|
|
|
|
# Include plug-ins from registered paths
|
|
for path in paths or plugin_paths():
|
|
path = os.path.normpath(path)
|
|
|
|
if not os.path.isdir(path):
|
|
continue
|
|
|
|
for fname in os.listdir(path):
|
|
if fname.startswith("_"):
|
|
continue
|
|
|
|
abspath = os.path.join(path, fname)
|
|
|
|
if not os.path.isfile(abspath):
|
|
continue
|
|
|
|
mod_name, mod_ext = os.path.splitext(fname)
|
|
|
|
if not mod_ext == ".py":
|
|
continue
|
|
|
|
module = types.ModuleType(mod_name)
|
|
module.__file__ = abspath
|
|
|
|
try:
|
|
execfile(abspath, module.__dict__)
|
|
|
|
# Store reference to original module, to avoid
|
|
# garbage collection from collecting it's global
|
|
# imports, such as `import os`.
|
|
sys.modules[mod_name] = module
|
|
|
|
except Exception as err:
|
|
log.debug("Skipped: \"%s\" (%s)", mod_name, err)
|
|
continue
|
|
|
|
for plugin in plugins_from_module(module):
|
|
if plugin.id in plugins:
|
|
log.debug("Duplicate plug-in found: %s", plugin)
|
|
continue
|
|
|
|
plugins[plugin.id] = plugin
|
|
|
|
# Include plug-ins from registration.
|
|
# Directly registered plug-ins take precedence.
|
|
for name, plugin in _registered_plugins.items():
|
|
if name in plugins:
|
|
log.debug("Duplicate plug-in found: %s", plugin)
|
|
continue
|
|
plugins[name] = plugin
|
|
|
|
plugins = list(plugins.values())
|
|
sort(plugins) # In-place
|
|
|
|
return plugins
|
|
|
|
|
|
def plugins_from_module(module):
|
|
"""Return plug-ins from module
|
|
|
|
Arguments:
|
|
module (types.ModuleType): Imported module from which to
|
|
parse valid plug-ins.
|
|
|
|
Returns:
|
|
List of plug-ins, or empty list if none is found.
|
|
|
|
"""
|
|
|
|
plugins = list()
|
|
|
|
for name in dir(module):
|
|
if name.startswith("_"):
|
|
continue
|
|
|
|
# It could be anything at this point
|
|
obj = getattr(module, name)
|
|
|
|
if not inspect.isclass(obj):
|
|
continue
|
|
|
|
if not issubclass(obj, Plugin):
|
|
continue
|
|
|
|
if not plugin_is_valid(obj):
|
|
log.debug("Plug-in invalid: %s", obj)
|
|
continue
|
|
|
|
plugins.append(obj)
|
|
|
|
return plugins
|
|
|
|
|
|
def plugin_is_valid(plugin):
|
|
"""Determine whether or not plug-in `plugin` is valid
|
|
|
|
Arguments:
|
|
plugin (Plugin): Plug-in to assess
|
|
|
|
"""
|
|
|
|
if not plugin:
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def sort(plugins):
|
|
"""Sort `plugins` in-place
|
|
|
|
Their order is determined by their `order` attribute.
|
|
|
|
Arguments:
|
|
plugins (list): Plug-ins to sort
|
|
|
|
"""
|
|
|
|
if not isinstance(plugins, list):
|
|
raise TypeError("plugins must be of type list")
|
|
|
|
plugins.sort(key=lambda p: p.order)
|
|
return plugins
|
|
|
|
|
|
# Register default paths
|
|
default_plugins_path = os.path.join(os.path.dirname(__file__), "plugins")
|
|
register_plugin_path(default_plugins_path)
|