diff --git a/pyblish/__init__.py b/pyblish/__init__.py index 51546b9f..eb4f62d4 100644 --- a/pyblish/__init__.py +++ b/pyblish/__init__.py @@ -7,11 +7,25 @@ """ -from .version import * +from .version import version, version_info, __version__ _registered_paths = list() +_registered_callbacks = dict() _registered_plugins = dict() _registered_services = dict() -_registered_test = None +_registered_test = dict() _registered_hosts = list() + + +__all__ = [ + "version", + "version_info", + "__version__", + "_registered_paths", + "_registered_callbacks", + "_registered_plugins", + "_registered_services", + "_registered_test", + "_registered_hosts", +] diff --git a/pyblish/api.py b/pyblish/api.py index dfce64be..53ec1991 100644 --- a/pyblish/api.py +++ b/pyblish/api.py @@ -16,8 +16,8 @@ from __future__ import absolute_import +from . import version import getpass -import pyblish from .plugin import ( Context, @@ -58,6 +58,11 @@ deregister_all_services, registered_services, + register_callback, + deregister_callback, + deregister_all_callbacks, + registered_callbacks, + sort as sort_plugins, registered_paths, @@ -70,6 +75,7 @@ log, time as __time, format_filename, + emit ) from .logic import ( @@ -98,7 +104,6 @@ ) -version = pyblish.version config = __Config() @@ -168,6 +173,11 @@ def __init__(): "deregister_all_services", "registered_services", + "register_callback", + "deregister_callback", + "deregister_all_callbacks", + "registered_callbacks", + "register_plugin_path", "deregister_plugin_path", "deregister_all_paths", @@ -192,6 +202,7 @@ def __init__(): # Utilities "log", + "emit", # Exceptions "PyblishError", diff --git a/pyblish/lib.py b/pyblish/lib.py index 41392e94..8f03e1a8 100644 --- a/pyblish/lib.py +++ b/pyblish/lib.py @@ -5,6 +5,8 @@ import datetime import traceback +from . import _registered_callbacks + _filename_ascii_strip_re = re.compile(r'[^-\w.]') _windows_device_files = ('CON', 'AUX', 'COM1', 'COM2', 'COM3', 'COM4', 'LPT1', 'LPT2', 'LPT3', 'PRN', 'NUL') @@ -349,3 +351,27 @@ def _resolve_name(name, package, level): __import__(name) return sys.modules[name] + + +def emit(signal, **kwargs): + """Trigger registered callbacks + + Keyword arguments are passed from caller to callee. + + Arguments: + signal (string): Name of signal emitted + + Example: + >>> import sys + >>> from .plugin import register_callback + >>> register_callback("mysignal", lambda data: sys.stdout.write(data)) + >>> emit("mysignal", data={"something": "cool"}) + {'something': 'cool'} + + """ + + for callback in _registered_callbacks.get(signal, []): + try: + callback(**kwargs) + except Exception as e: + traceback.print_exc(e) diff --git a/pyblish/logic.py b/pyblish/logic.py index 428b7021..bd4bab65 100644 --- a/pyblish/logic.py +++ b/pyblish/logic.py @@ -4,8 +4,8 @@ import inspect import traceback -import pyblish -import lib +from . import _registered_test, lib +from .plugin import Validator class TestFailed(Exception): @@ -37,7 +37,7 @@ def default_test(**vars): """ offset = 0.5 - validation_order = pyblish.api.Validator.order + validation_order = Validator.order # If validation is done if vars["nextOrder"] >= validation_order + offset: @@ -193,12 +193,12 @@ def register_test(test): """ - pyblish._registered_test = test + _registered_test["default"] = test def registered_test(): """Return the currently registered test""" - return pyblish._registered_test + return _registered_test["default"] def deregister_test(): diff --git a/pyblish/plugin.py b/pyblish/plugin.py index e0d1d381..bd353652 100644 --- a/pyblish/plugin.py +++ b/pyblish/plugin.py @@ -8,9 +8,6 @@ In this system, the predicate is whether or not a fname starts with "validate" and ends with ".py" -Attributes: - patterns: Regular expressions used for lookup of plugins. - """ # Standard library @@ -24,10 +21,17 @@ import contextlib # Local library -import pyblish -import pyblish.lib -import pyblish.error - +from . import ( + __version__, + version_info, + _registered_callbacks, + _registered_services, + _registered_plugins, + _registered_hosts, + _registered_paths, +) + +from . import lib from .vendor import yaml from .vendor import iscompatible @@ -54,7 +58,7 @@ def get(self, service): @property def services(self): - services = pyblish._registered_services.copy() + services = _registered_services.copy() services.update(self._services) # Forwards-compatibility alias @@ -236,7 +240,7 @@ def __init__(cls, *args, **kwargs): return super(MetaPlugin, cls).__init__(*args, **kwargs) -@pyblish.lib.log +@lib.log class Plugin(object): """Base-class for plugins @@ -277,7 +281,7 @@ class Plugin(object): requires = "pyblish>=1" actions = [] - id = pyblish.lib.classproperty(lambda cls: cls.__name__) + id = lib.classproperty(lambda cls: cls.__name__) def __str__(self): return self.label or type(self).__name__ @@ -358,7 +362,7 @@ def __init__(cls, *args, **kwargs): return super(MetaAction, cls).__init__(*args, **kwargs) -@pyblish.lib.log +@lib.log class Action(object): """User-supplied interactive action @@ -390,7 +394,7 @@ class Action(object): on = "all" icon = None - id = pyblish.lib.classproperty(lambda cls: cls.__name__) + id = lib.classproperty(lambda cls: cls.__name__) def __str__(self): return self.label or type(self).__name__ @@ -468,9 +472,9 @@ def process(plugin, context, instance=None, action=None): runner = action().process records = list() - handler = pyblish.lib.MessageHandler(records) + handler = lib.MessageHandler(records) - provider = pyblish.plugin.Provider() + provider = Provider() provider.inject("plugin", plugin) provider.inject("context", context) provider.inject("instance", instance) @@ -482,7 +486,9 @@ def process(plugin, context, instance=None, action=None): provider.invoke(runner) result["success"] = True except Exception as error: - pyblish.lib.extract_traceback(error) + lib.emit("pluginFailed", plugin=plugin, context=context, + instance=instance, error=error) + lib.extract_traceback(error) result["error"] = error __end = time.time() @@ -523,9 +529,9 @@ def repair(plugin, context, instance=None): plugin = plugin() records = list() - handler = pyblish.lib.MessageHandler(records) + handler = lib.MessageHandler(records) - provider = pyblish.plugin.Provider() + provider = Provider() provider.inject("context", context) provider.inject("instance", instance) @@ -536,7 +542,7 @@ def repair(plugin, context, instance=None): provider.invoke(plugin.repair) result["success"] = True except Exception as error: - pyblish.lib.extract_traceback(error) + lib.extract_traceback(error) result["error"] = error __end = time.time() @@ -694,7 +700,7 @@ def add(self, other): return super(Context, self).append(other) -@pyblish.lib.log +@lib.log class Instance(AbstractEntity): """An in-memory representation of one or more files @@ -777,7 +783,52 @@ def current_host(): """ - return pyblish._registered_hosts[-1] or "unknown" + return _registered_hosts[-1] or "unknown" + + +def register_callback(signal, callback): + """Register a new callback + + Arguments: + signal (string): Name of signal to register the callback with. + callback (func): Function to execute when a signal is emitted. + + Raises: + ValueError if `callback` is not callable. + + """ + + if not hasattr(callback, "__call__"): + raise ValueError("%s is not callable" % callback) + + if signal in _registered_callbacks: + _registered_callbacks[signal].append(callback) + else: + _registered_callbacks[signal] = [callback] + + +def deregister_callback(signal, callback): + """Deregister a callback + + Arguments: + signal (string): Name of signal to deregister the callback with. + callback (func): Function to execute when a signal is emitted. + """ + + if callback in _registered_callbacks[signal]: + _registered_callbacks[signal].remove(callback) + + +def deregister_all_callbacks(): + """Deregisters all callback""" + + _registered_callbacks.clear() + + +def registered_callbacks(): + """Returns registered callbacks""" + + return _registered_callbacks def register_plugin(plugin): @@ -802,13 +853,13 @@ def register_plugin(plugin): raise TypeError( "Plug-in %s not compatible with " "this version (%s) of Pyblish." % ( - plugin, pyblish.__version__)) + plugin, __version__)) if not host_is_compatible(plugin): raise TypeError("Plug-in %s is not compatible " "with this host" % plugin) - pyblish._registered_plugins[plugin.__name__] = plugin + _registered_plugins[plugin.__name__] = plugin def deregister_plugin(plugin): @@ -819,12 +870,12 @@ def deregister_plugin(plugin): """ - pyblish._registered_plugins.pop(plugin.__name__) + _registered_plugins.pop(plugin.__name__) def deregister_all_plugins(): """De-register all plug-ins""" - pyblish._registered_plugins.clear() + _registered_plugins.clear() def register_service(name, obj): @@ -836,7 +887,7 @@ def register_service(name, obj): """ - pyblish._registered_services[name] = obj + _registered_services[name] = obj def deregister_service(name): @@ -847,12 +898,12 @@ def deregister_service(name): """ - pyblish._registered_services.pop(name) + _registered_services.pop(name) def deregister_all_services(): """De-register all existing services""" - pyblish._registered_services.clear() + _registered_services.clear() def registered_services(): @@ -863,7 +914,7 @@ def registered_services(): """ - return pyblish._registered_services.copy() + return _registered_services.copy() def register_plugin_path(path): @@ -883,10 +934,10 @@ def register_plugin_path(path): """ - if path in pyblish._registered_paths: + if path in _registered_paths: return log.warning("Path already registered: {0}".format(path)) - pyblish._registered_paths.append(path) + _registered_paths.append(path) return path @@ -899,12 +950,12 @@ def deregister_plugin_path(path): """ - pyblish._registered_paths.remove(path) + _registered_paths.remove(path) def deregister_all_paths(): """Mainly used in tests""" - pyblish._registered_paths[:] = [] + _registered_paths[:] = [] def registered_paths(): @@ -915,7 +966,7 @@ def registered_paths(): """ - return list(pyblish._registered_paths) + return list(_registered_paths) def registered_plugins(): @@ -926,7 +977,7 @@ def registered_plugins(): """ - return pyblish._registered_plugins.values() + return _registered_plugins.values() def register_host(host): @@ -942,8 +993,8 @@ def register_host(host): """ - if host not in pyblish._registered_hosts: - pyblish._registered_hosts.append(host) + if host not in _registered_hosts: + _registered_hosts.append(host) def deregister_host(host, quiet=False): @@ -958,19 +1009,19 @@ def deregister_host(host, quiet=False): """ try: - pyblish._registered_hosts.remove(host) + _registered_hosts.remove(host) except Exception as e: if not quiet: raise e def deregister_all_hosts(): - pyblish._registered_hosts[:] = [] + _registered_hosts[:] = [] def registered_hosts(): """Return the currently registered hosts""" - return list(pyblish._registered_hosts) + return list(_registered_hosts) def configured_paths(): @@ -979,7 +1030,7 @@ def configured_paths(): config = Config() for path_template in config["paths"]: - variables = {"pyblish": pyblish.lib.main_package_path()} + variables = {"pyblish": lib.main_package_path()} plugin_path = path_template.format(**variables) @@ -1062,7 +1113,7 @@ def discover(type=None, regex=None, paths=None): warnings.warn("type argument has been deprecated and does nothing") if regex is not None: - warnings.warn("pyblish.plugin.discover(): regex argument " + warnings.warn("discover(): regex argument " "has been deprecated and does nothing") plugins = dict() @@ -1111,7 +1162,7 @@ def discover(type=None, regex=None, paths=None): # Include plug-ins from registration. # Directly registered plug-ins take precedence. - for name, plugin in pyblish._registered_plugins.iteritems(): + for name, plugin in _registered_plugins.iteritems(): if name in plugins: log.debug("Duplicate plug-in found: %s", plugin) continue @@ -1157,7 +1208,7 @@ def plugins_from_module(module): if not version_is_compatible(obj): log.debug("Plug-in %s not compatible with " "this version (%s) of Pyblish." % ( - obj, pyblish.__version__)) + obj, __version__)) continue if not host_is_compatible(obj): @@ -1210,7 +1261,7 @@ def version_is_compatible(plugin): """ if not iscompatible.iscompatible(requirements=plugin.requires, - version=pyblish.version_info): + version=version_info): return False return True diff --git a/pyblish/util.py b/pyblish/util.py index abb85e80..0350a438 100644 --- a/pyblish/util.py +++ b/pyblish/util.py @@ -21,10 +21,7 @@ import warnings # Local library -import pyblish -import pyblish.lib -import pyblish.logic -import pyblish.plugin +from . import logic, plugin log = logging.getLogger("pyblish.util") @@ -37,37 +34,37 @@ def publish(context=None, plugins=None, **kwargs): during selection. Arguments: - context (pyblish.plugin.Context): Optional Context, + context (plugin.Context): Optional Context, defaults to creating a new context plugins (list): (Optional) Plug-ins to include, defaults to discover() Usage: - >> context = pyblish.plugin.Context() + >> context = plugin.Context() >> publish(context) # Pass.. >> context = publish() # ..or receive a new """ - assert context is None or isinstance(context, pyblish.plugin.Context) + assert context is None or isinstance(context, plugin.Context) # Must check against None, as the # Context may come in empty. if context is None: - context = pyblish.plugin.Context() + context = plugin.Context() if plugins is None: - plugins = pyblish.plugin.discover() + plugins = plugin.discover() # Do not consider inactive plug-ins plugins = list(p for p in plugins if p.active) - for result in pyblish.logic.process( - func=pyblish.plugin.process, + for result in logic.process( + func=plugin.process, plugins=plugins, context=context): - if isinstance(result, pyblish.logic.TestFailed): + if isinstance(result, logic.TestFailed): log.error("Stopped due to: %s" % result) break @@ -108,7 +105,7 @@ def conform(*args, **kwargs): def _convenience(order, *args, **kwargs): - plugins = [p for p in pyblish.plugin.discover() + plugins = [p for p in plugin.discover() if p.order < order] print [p.id for p in plugins] diff --git a/tests/lib.py b/tests/lib.py index acf14321..b4f391f2 100644 --- a/tests/lib.py +++ b/tests/lib.py @@ -1,4 +1,9 @@ import os +import sys +import shutil +import tempfile +import contextlib +from StringIO import StringIO import pyblish import pyblish.cli @@ -29,6 +34,7 @@ def setup_empty(): setup() pyblish.plugin.deregister_all_paths() pyblish.plugin.deregister_all_hosts() + pyblish.plugin.deregister_all_callbacks() def teardown(): @@ -46,3 +52,33 @@ def teardown(): pyblish.api.deregister_all_hosts() pyblish.api.deregister_test() pyblish.api.__init__() + + +@contextlib.contextmanager +def captured_stdout(): + """Temporarily reassign stdout to a local variable""" + try: + sys.stdout = StringIO() + yield sys.stdout + finally: + sys.stdout = sys.__stdout__ + + +@contextlib.contextmanager +def captured_stderr(): + """Temporarily reassign stderr to a local variable""" + try: + sys.stderr = StringIO() + yield sys.stderr + finally: + sys.stderr = sys.__stderr__ + + +@contextlib.contextmanager +def tempdir(): + """Provide path to temporary directory""" + try: + tempdir = tempfile.mkdtemp() + yield tempdir + finally: + shutil.rmtree(tempdir) diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 8120d0c4..ae7bfba4 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -1,7 +1,6 @@ import os import shutil import tempfile -import contextlib import pyblish.plugin from pyblish.vendor.nose.tools import ( @@ -11,16 +10,7 @@ raises, ) -import lib - - -@contextlib.contextmanager -def tempdir(): - try: - tempdir = tempfile.mkdtemp() - yield tempdir - finally: - shutil.rmtree(tempdir) +from . import lib def test_unique_id(): @@ -79,7 +69,7 @@ def test_import_mechanism_duplication(): """ - with tempdir() as temp: + with lib.tempdir() as temp: print("Writing temporarily to: %s" % temp) module = os.path.join(temp, "selector.py") pyblish.api.register_plugin_path(temp) @@ -177,7 +167,7 @@ class NotDiscoverable(pyblish.api.Plugin): pass """ - with tempdir() as d: + with lib.tempdir() as d: pyblish.api.register_plugin_path(d) with open(os.path.join(d, "discoverable.py"), "w") as f: @@ -498,3 +488,61 @@ def process(self, instance): pyblish.util.publish() assert count["#"] == 110, count["#"] + + +@with_setup(lib.setup_empty, lib.teardown) +def test_register_callback(): + """Callback registration/deregistration works well""" + + def my_callback(): + pass + + def other_callback(data=None): + pass + + pyblish.plugin.register_callback("mySignal", my_callback) + + msg = "Registering a callback failed" + data = {"mySignal": [my_callback]} + assert "mySignal" in pyblish.plugin.registered_callbacks() == data, msg + + pyblish.plugin.deregister_callback("mySignal", my_callback) + + msg = "Deregistering a callback failed" + data = {"mySignal": []} + assert pyblish.plugin.registered_callbacks() == data, msg + + pyblish.plugin.register_callback("mySignal", my_callback) + pyblish.plugin.register_callback("otherSignal", other_callback) + pyblish.plugin.deregister_all_callbacks() + + msg = "Deregistering all callbacks failed" + assert pyblish.plugin.registered_callbacks() == {}, msg + + +@with_setup(lib.setup_empty, lib.teardown) +def test_emit_signal_wrongly(): + """Exception from callback prints traceback""" + + def other_callback(data=None): + print "Ping from 'other_callback' with %s" % data + + pyblish.plugin.register_callback("otherSignal", other_callback) + + with lib.captured_stderr() as stderr: + pyblish.lib.emit("otherSignal", akeyword="") + output = stderr.getvalue().strip() + assert output.startswith("Traceback") + + +@raises(ValueError) +@with_setup(lib.setup_empty, lib.teardown) +def test_registering_invalid_callback(): + """Can't register non-callables""" + pyblish.plugin.register_callback("invalid", None) + + +@raises(KeyError) +def test_deregistering_nonexisting_callback(): + """Can't deregister a callback that doesn't exist""" + pyblish.plugin.deregister_callback("invalid", lambda: "")