ayon-core/pype/tools/pyblish_pype/control.py

475 lines
16 KiB
Python

"""The Controller in a Model/View/Controller-based application
The graphical components of Pyblish Lite use this object to perform
publishing. It communicates via the Qt Signals/Slots mechanism
and has no direct connection to any graphics. This is important,
because this is how unittests are able to run without requiring
an active window manager; such as via Travis-CI.
"""
import os
import sys
import traceback
import inspect
import logging
from Qt import QtCore
import pyblish.api
import pyblish.util
import pyblish.logic
import pyblish.lib
import pyblish.version
from . import util
from .constants import InstanceStates
from pype.api import config
class IterationBreak(Exception):
pass
class Controller(QtCore.QObject):
log = logging.getLogger("PyblishController")
# Emitted when the GUI is about to start processing;
# e.g. resetting, validating or publishing.
about_to_process = QtCore.Signal(object, object)
# ??? Emitted for each process
was_processed = QtCore.Signal(dict)
# Emmited when reset
# - all data are reset (plugins, processing, pari yielder, etc.)
was_reset = QtCore.Signal()
# Emmited when previous group changed
passed_group = QtCore.Signal(object)
# Emmited when want to change state of instances
switch_toggleability = QtCore.Signal(bool)
# On action finished
was_acted = QtCore.Signal(dict)
# Emitted when processing has stopped
was_stopped = QtCore.Signal()
# Emitted when processing has finished
was_finished = QtCore.Signal()
# Emitted when plugin was skipped
was_skipped = QtCore.Signal(object)
# store OrderGroups - now it is a singleton
order_groups = util.OrderGroups
# When instance is toggled
instance_toggled = QtCore.Signal(object, object, object)
def __init__(self, parent=None):
super(Controller, self).__init__(parent)
self.context = None
self.plugins = {}
self.optional_default = {}
self.instance_toggled.connect(self._on_instance_toggled)
def reset_variables(self):
self.log.debug("Resetting pyblish context variables")
# Data internal to the GUI itself
self.is_running = False
self.stopped = False
self.errored = False
# Active producer of pairs
self.pair_generator = None
# Active pair
self.current_pair = None
# Orders which changes GUI
# - passing collectors order disables plugin/instance toggle
self.collectors_order = None
self.collect_state = 0
# - passing validators order disables validate button and gives ability
# to know when to stop on validate button press
self.validators_order = None
self.validated = False
# Get collectors and validators order
self.order_groups.reset()
plugin_groups = self.order_groups.groups()
plugin_groups_keys = list(plugin_groups.keys())
self.collectors_order = plugin_groups_keys[0]
self.validators_order = self.order_groups.validation_order()
next_group_order = None
if len(plugin_groups_keys) > 1:
next_group_order = plugin_groups_keys[1]
# This is used to track whether or not to continue
# processing when, for example, validation has failed.
self.processing = {
"stop_on_validation": False,
# Used?
"last_plugin_order": None,
"current_group_order": self.collectors_order,
"next_group_order": next_group_order,
"nextOrder": None,
"ordersWithError": set()
}
self.log.debug("Reset of pyblish context variables done")
def presets_by_hosts(self):
# Get global filters as base
presets = config.get_presets().get("plugins", {})
if not presets:
return {}
result = presets.get("global", {}).get("filter", {})
hosts = pyblish.api.registered_hosts()
for host in hosts:
host_presets = presets.get(host, {}).get("filter")
if not host_presets:
continue
for key, value in host_presets.items():
if value is None:
if key in result:
result.pop(key)
continue
result[key] = value
return result
def reset_context(self):
self.log.debug("Resetting pyblish context object")
self.context = pyblish.api.Context()
self.context._publish_states = InstanceStates.ContextType
self.context.optional = False
self.context.data["publish"] = True
self.context.data["label"] = "Context"
self.context.data["name"] = "context"
self.context.data["host"] = reversed(pyblish.api.registered_hosts())
self.context.data["port"] = int(
os.environ.get("PYBLISH_CLIENT_PORT", -1)
)
self.context.data["connectTime"] = pyblish.lib.time(),
self.context.data["pyblishVersion"] = pyblish.version,
self.context.data["pythonVersion"] = sys.version
self.context.data["icon"] = "book"
self.context.families = ("__context__",)
self.log.debug("Reset of pyblish context object done")
def reset(self):
"""Discover plug-ins and run collection."""
self.reset_context()
self.reset_variables()
self.possible_presets = self.presets_by_hosts()
# Load plugins and set pair generator
self.load_plugins()
self.pair_generator = self._pair_yielder(self.plugins)
self.was_reset.emit()
# Process collectors load rest of plugins with collected instances
self.collect()
def load_plugins(self):
self.test = pyblish.logic.registered_test()
self.optional_default = {}
plugins = pyblish.api.discover()
targets = pyblish.logic.registered_targets() or ["default"]
plugins_by_targets = pyblish.logic.plugins_by_targets(plugins, targets)
_plugins = []
for plugin in plugins_by_targets:
# Skip plugin if is not optional and not active
if (
not getattr(plugin, "optional", False)
and not getattr(plugin, "active", True)
):
continue
_plugins.append(plugin)
self.plugins = _plugins
def on_published(self):
if self.is_running:
self.is_running = False
self.was_finished.emit()
def stop(self):
self.log.debug("Stopping")
self.stopped = True
def act(self, plugin, action):
def on_next():
result = pyblish.plugin.process(
plugin, self.context, None, action.id
)
self.is_running = False
self.was_acted.emit(result)
self.is_running = True
util.defer(100, on_next)
def emit_(self, signal, kwargs):
pyblish.api.emit(signal, **kwargs)
def _process(self, plugin, instance=None):
"""Produce `result` from `plugin` and `instance`
:func:`process` shares state with :func:`_iterator` such that
an instance/plugin pair can be fetched and processed in isolation.
Arguments:
plugin (pyblish.api.Plugin): Produce result using plug-in
instance (optional, pyblish.api.Instance): Process this instance,
if no instance is provided, context is processed.
"""
self.processing["nextOrder"] = plugin.order
try:
result = pyblish.plugin.process(plugin, self.context, instance)
# Make note of the order at which the
# potential error error occured.
if result["error"] is not None:
self.processing["ordersWithError"].add(plugin.order)
except Exception as exc:
raise Exception("Unknown error({}): {}".format(
plugin.__name__, str(exc)
))
return result
def _pair_yielder(self, plugins):
for plugin in plugins:
if (
self.processing["current_group_order"] is not None
and plugin.order > self.processing["current_group_order"]
):
current_group_order = self.processing["current_group_order"]
new_next_group_order = None
new_current_group_order = self.processing["next_group_order"]
if new_current_group_order is not None:
current_next_order_found = False
for order in self.order_groups.groups().keys():
if current_next_order_found:
new_next_group_order = order
break
if order == new_current_group_order:
current_next_order_found = True
self.processing["next_group_order"] = new_next_group_order
self.processing["current_group_order"] = (
new_current_group_order
)
if self.collect_state == 0:
self.collect_state = 1
self.switch_toggleability.emit(True)
self.passed_group.emit(current_group_order)
yield IterationBreak("Collected")
else:
self.passed_group.emit(current_group_order)
if self.errored:
yield IterationBreak("Last group errored")
if self.collect_state == 1:
self.collect_state = 2
self.switch_toggleability.emit(False)
if not self.validated and plugin.order > self.validators_order:
self.validated = True
if self.processing["stop_on_validation"]:
yield IterationBreak("Validated")
# Stop if was stopped
if self.stopped:
self.stopped = False
yield IterationBreak("Stopped")
# check test if will stop
self.processing["nextOrder"] = plugin.order
message = self.test(**self.processing)
if message:
yield IterationBreak("Stopped due to \"{}\"".format(message))
self.processing["last_plugin_order"] = plugin.order
if not plugin.active:
pyblish.logic.log.debug("%s was inactive, skipping.." % plugin)
self.was_skipped.emit(plugin)
continue
if plugin.__instanceEnabled__:
instances = pyblish.logic.instances_by_plugin(
self.context, plugin
)
if not instances:
self.was_skipped.emit(plugin)
continue
for instance in instances:
if instance.data.get("publish") is False:
pyblish.logic.log.debug(
"%s was inactive, skipping.." % instance
)
continue
# Stop if was stopped
if self.stopped:
self.stopped = False
yield IterationBreak("Stopped")
yield (plugin, instance)
else:
families = util.collect_families_from_instances(
self.context, only_active=True
)
plugins = pyblish.logic.plugins_by_families(
[plugin], families
)
if not plugins:
self.was_skipped.emit(plugin)
continue
yield (plugin, None)
self.passed_group.emit(self.processing["next_group_order"])
def iterate_and_process(self, on_finished=lambda: None):
""" Iterating inserted plugins with current context.
Collectors do not contain instances, they are None when collecting!
This process don't stop on one
"""
def on_next():
self.log.debug("Looking for next pair to process")
try:
self.current_pair = next(self.pair_generator)
if isinstance(self.current_pair, IterationBreak):
raise self.current_pair
except IterationBreak:
self.log.debug("Iteration break was raised")
self.is_running = False
self.was_stopped.emit()
return
except StopIteration:
self.log.debug("Iteration stop was raised")
self.is_running = False
# All pairs were processed successfully!
return util.defer(500, on_finished)
except Exception as exc:
self.log.warning(
"Unexpected exception during `on_next` happened",
exc_info=True
)
exc_msg = str(exc)
return util.defer(
500, lambda: on_unexpected_error(error=exc_msg)
)
self.about_to_process.emit(*self.current_pair)
util.defer(100, on_process)
def on_process():
try:
self.log.debug(
"Processing pair: {}".format(str(self.current_pair))
)
result = self._process(*self.current_pair)
if result["error"] is not None:
self.log.debug("Error happened")
self.errored = True
self.log.debug("Pair processed")
self.was_processed.emit(result)
except Exception as exc:
self.log.warning(
"Unexpected exception during `on_process` happened",
exc_info=True
)
exc_msg = str(exc)
return util.defer(
500, lambda: on_unexpected_error(error=exc_msg)
)
util.defer(10, on_next)
def on_unexpected_error(error):
# TODO this should be handled much differently
# TODO emit crash signal to show message box with traceback?
self.is_running = False
self.was_stopped.emit()
util.u_print(u"An unexpected error occurred:\n %s" % error)
return util.defer(500, on_finished)
self.is_running = True
util.defer(10, on_next)
def collect(self):
""" Iterate and process Collect plugins
- load_plugins method is launched again when finished
"""
self.iterate_and_process()
def validate(self):
""" Process plugins to validations_order value."""
self.processing["stop_on_validation"] = True
self.iterate_and_process()
def publish(self):
""" Iterate and process all remaining plugins."""
self.processing["stop_on_validation"] = False
self.iterate_and_process(self.on_published)
def cleanup(self):
"""Forcefully delete objects from memory
In an ideal world, this shouldn't be necessary. Garbage
collection guarantees that anything without reference
is automatically removed.
However, because this application is designed to be run
multiple times from the same interpreter process, extra
case must be taken to ensure there are no memory leaks.
Explicitly deleting objects shines a light on where objects
may still be referenced in the form of an error. No errors
means this was uneccesary, but that's ok.
"""
for instance in self.context:
del(instance)
for plugin in self.plugins:
del(plugin)
def _on_instance_toggled(self, instance, old_value, new_value):
callbacks = pyblish.api.registered_callbacks().get("instanceToggled")
if not callbacks:
return
for callback in callbacks:
try:
callback(instance, old_value, new_value)
except Exception:
self.log.warning(
"Callback for `instanceToggled` crashed. {}".format(
os.path.abspath(inspect.getfile(callback))
),
exc_info=True
)