ayon-core/pype/vendor/capture_gui/plugin.py
2019-11-22 18:15:57 +01:00

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)