From e495e28ee0f94936aeae802fe16cc522546e3869 Mon Sep 17 00:00:00 2001 From: Christopher Allan Webber Date: Sun, 7 Apr 2013 17:46:11 -0500 Subject: [PATCH 1/5] Convenience functions for callable hooks --- mediagoblin/tools/pluginapi.py | 65 ++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/mediagoblin/tools/pluginapi.py b/mediagoblin/tools/pluginapi.py index 784bede9..f233fe51 100644 --- a/mediagoblin/tools/pluginapi.py +++ b/mediagoblin/tools/pluginapi.py @@ -272,3 +272,68 @@ def get_hook_templates(hook_name): A list of strings representing template paths. """ return PluginManager().get_template_hooks(hook_name) + + +########################### +# Callable convenience code +########################### + +class CantHandleIt(Exception): + """ + A callable may call this method if they look at the relevant + arguments passed and decide it's not possible for them to handle + things. + """ + pass + +class UnhandledCallable(Exception): + """ + Raise this method if no callables were available to handle the + specified hook. Only used by callable_runone. + """ + pass + + +def callable_runone(hookname, unhandled_okay=False, *args, **kwargs): + """ + Run the callable hook HOOKNAME... run until the first response, + then return. + + This function will run stop at the first hook that handles the + result. Hooks raising CantHandleIt will be skipped. + + Unless unhandled_okay is True, this will error out if no hooks + have been registered to handle this function. + """ + callables = PluginManager().get_hook_callables(hookname) + + for callable in callables: + try: + return callable(*args, **kwargs) + except CantHandleIt: + continue + + if unhandled_okay is False: + raise UnhandledCallable( + "No hooks registered capable of handling '%s'" % hookname) + + +def callable_runall(hookname, *args, **kwargs): + """ + Run all callables for HOOKNAME. + + This method will run *all* hooks that handle this method (skipping + those that raise CantHandleIt), and will return a list of all + results. + """ + callables = PluginManager().get_hook_callables(hookname) + + results = [] + + for callable in callables: + try: + results.append(callable(*args, **kwargs)) + except CantHandleIt: + continue + + return results From 70c061955c56d62956d707a0828ca2f1dc059aad Mon Sep 17 00:00:00 2001 From: Christopher Allan Webber Date: Tue, 9 Apr 2013 11:21:31 -0500 Subject: [PATCH 2/5] tests for new pluginapi methods that can't possibly pass yet :) --- mediagoblin/tests/test_pluginapi.py | 111 ++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/mediagoblin/tests/test_pluginapi.py b/mediagoblin/tests/test_pluginapi.py index 245c396d..4381859d 100644 --- a/mediagoblin/tests/test_pluginapi.py +++ b/mediagoblin/tests/test_pluginapi.py @@ -15,7 +15,10 @@ # along with this program. If not, see . import sys + from configobj import ConfigObj +import pytest + from mediagoblin import mg_globals from mediagoblin.init.plugins import setup_plugins from mediagoblin.tools import pluginapi @@ -172,3 +175,111 @@ def test_disabled_plugin(): # Make sure we didn't load the plugin assert len(pman.plugins) == 0 + + +@with_cleanup() +def test_callable_runone(): + """ + Test the callable_runone method + """ + cfg = build_config([ + ('mediagoblin', {}, []), + ('plugins', {}, [ + ('mediagoblin.tests.testplugins.callables1', {}, []), + ('mediagoblin.tests.testplugins.callables2', {}, []), + ('mediagoblin.tests.testplugins.callables3', {}, []), + ]) + ]) + + mg_globals.app_config = cfg['mediagoblin'] + mg_globals.global_config = cfg + + setup_plugins() + + # Just one hook provided + call_log = [] + assert pluginapi.callable_runone( + "just_one", call_log) == "Called just once" + assert call_log == ["expect this one call"] + + # Nothing provided and unhandled not okay + call_log = [] + with pytest.raises(pluginapi.UnhandledCallable): + pluginapi.callable_runone( + "nothing_handling", call_log) + assert call_log == [] + + # Nothing provided and unhandled okay + call_log = [] + assert pluginapi.callable_runone( + "nothing_handling", call_log, unhandled_okay=True) is None + assert call_log == [] + + # Multiple provided, go with the first! + call_log = [] + assert pluginapi.callable_runone( + "multi_handle", call_log) is "the first returns" + assert call_log == ["Hi, I'm the first"] + + # Multiple provided, one has CantHandleIt + call_log = [] + assert pluginapi.callable_runone( + "multi_handle_with_canthandle", + call_log) is "the second returns" + assert call_log == ["Hi, I'm the second"] + + +@with_cleanup() +def test_callable_runall(): + """ + Test the callable_runall method + """ + cfg = build_config([ + ('mediagoblin', {}, []), + ('plugins', {}, [ + ('mediagoblin.tests.testplugins.callables1', {}, []), + ('mediagoblin.tests.testplugins.callables2', {}, []), + ('mediagoblin.tests.testplugins.callables3', {}, []), + ]) + ]) + + mg_globals.app_config = cfg['mediagoblin'] + mg_globals.global_config = cfg + + setup_plugins() + + # Just one hook, check results + call_log = [] + assert pluginapi.callable_runall( + "just_one", call_log) == ["Called just once"] + assert call_log == ["expect this one call"] + + # None provided, check results + call_log = [] + assert pluginapi.callable_runall( + "nothing_handling", call_log) == [] + assert call_log == [] + + # Multiple provided, check results + call_log = [] + assert pluginapi.callable_runall( + "multi_handle", call_log) == [ + "the first returns", + "the second returns", + "the third returns", + ] + assert call_log == [ + "Hi, I'm the first", + "Hi, I'm the second", + "Hi, I'm the third"] + + # Multiple provided, one has CantHandleIt, check results + call_log = [] + assert pluginapi.callable_runall( + "multi_handle_with_canthandle", call_log) == [ + "the second returns", + "the third returns", + ] + assert call_log == [ + "Hi, I'm the second", + "Hi, I'm the third"] From cdc821eb743729fe81dea573ef71091b777301c3 Mon Sep 17 00:00:00 2001 From: Christopher Allan Webber Date: Wed, 10 Apr 2013 17:36:21 -0500 Subject: [PATCH 3/5] callable hook convenience functions.. now work, and with tests! - Added three "callables" test plugins. - updated callable_runone to check for unhandled_okay in the kwargs dict. All passing! --- mediagoblin/tests/test_pluginapi.py | 6 +-- mediagoblin/tests/testplugins/__init__.py | 15 +++++++ .../tests/testplugins/callables1/__init__.py | 41 +++++++++++++++++++ .../tests/testplugins/callables2/__init__.py | 38 +++++++++++++++++ .../tests/testplugins/callables3/__init__.py | 38 +++++++++++++++++ mediagoblin/tools/pluginapi.py | 4 +- 6 files changed, 137 insertions(+), 5 deletions(-) create mode 100644 mediagoblin/tests/testplugins/__init__.py create mode 100644 mediagoblin/tests/testplugins/callables1/__init__.py create mode 100644 mediagoblin/tests/testplugins/callables2/__init__.py create mode 100644 mediagoblin/tests/testplugins/callables3/__init__.py diff --git a/mediagoblin/tests/test_pluginapi.py b/mediagoblin/tests/test_pluginapi.py index 4381859d..d40a5081 100644 --- a/mediagoblin/tests/test_pluginapi.py +++ b/mediagoblin/tests/test_pluginapi.py @@ -218,14 +218,14 @@ def test_callable_runone(): # Multiple provided, go with the first! call_log = [] assert pluginapi.callable_runone( - "multi_handle", call_log) is "the first returns" + "multi_handle", call_log) == "the first returns" assert call_log == ["Hi, I'm the first"] # Multiple provided, one has CantHandleIt call_log = [] assert pluginapi.callable_runone( "multi_handle_with_canthandle", - call_log) is "the second returns" + call_log) == "the second returns" assert call_log == ["Hi, I'm the second"] @@ -251,7 +251,7 @@ def test_callable_runall(): # Just one hook, check results call_log = [] assert pluginapi.callable_runall( - "just_one", call_log) == ["Called just once"] + "just_one", call_log) == ["Called just once", None, None] assert call_log == ["expect this one call"] # None provided, check results diff --git a/mediagoblin/tests/testplugins/__init__.py b/mediagoblin/tests/testplugins/__init__.py new file mode 100644 index 00000000..621845ba --- /dev/null +++ b/mediagoblin/tests/testplugins/__init__.py @@ -0,0 +1,15 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . diff --git a/mediagoblin/tests/testplugins/callables1/__init__.py b/mediagoblin/tests/testplugins/callables1/__init__.py new file mode 100644 index 00000000..9c278b49 --- /dev/null +++ b/mediagoblin/tests/testplugins/callables1/__init__.py @@ -0,0 +1,41 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from mediagoblin.tools.pluginapi import CantHandleIt + +def setup_plugin(): + pass + + +def just_one(call_log): + call_log.append("expect this one call") + return "Called just once" + + +def multi_handle(call_log): + call_log.append("Hi, I'm the first") + return "the first returns" + +def multi_handle_with_canthandle(call_log): + raise CantHandleIt("I just can't accept this stupid method") + + +hooks = { + 'setup': setup_plugin, + 'just_one': just_one, + 'multi_handle': multi_handle, + 'multi_handle_with_canthandle': multi_handle_with_canthandle, + } diff --git a/mediagoblin/tests/testplugins/callables2/__init__.py b/mediagoblin/tests/testplugins/callables2/__init__.py new file mode 100644 index 00000000..aaab5b21 --- /dev/null +++ b/mediagoblin/tests/testplugins/callables2/__init__.py @@ -0,0 +1,38 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +def setup_plugin(): + pass + + +def just_one(call_log): + assert "SHOULD NOT HAPPEN" + +def multi_handle(call_log): + call_log.append("Hi, I'm the second") + return "the second returns" + +def multi_handle_with_canthandle(call_log): + call_log.append("Hi, I'm the second") + return "the second returns" + + +hooks = { + 'setup': setup_plugin, + 'just_one': just_one, + 'multi_handle': multi_handle, + 'multi_handle_with_canthandle': multi_handle_with_canthandle, + } diff --git a/mediagoblin/tests/testplugins/callables3/__init__.py b/mediagoblin/tests/testplugins/callables3/__init__.py new file mode 100644 index 00000000..8d0c9c25 --- /dev/null +++ b/mediagoblin/tests/testplugins/callables3/__init__.py @@ -0,0 +1,38 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +def setup_plugin(): + pass + + +def just_one(call_log): + assert "SHOULD NOT HAPPEN" + +def multi_handle(call_log): + call_log.append("Hi, I'm the third") + return "the third returns" + +def multi_handle_with_canthandle(call_log): + call_log.append("Hi, I'm the third") + return "the third returns" + + +hooks = { + 'setup': setup_plugin, + 'just_one': just_one, + 'multi_handle': multi_handle, + 'multi_handle_with_canthandle': multi_handle_with_canthandle, + } diff --git a/mediagoblin/tools/pluginapi.py b/mediagoblin/tools/pluginapi.py index f233fe51..2c0394aa 100644 --- a/mediagoblin/tools/pluginapi.py +++ b/mediagoblin/tools/pluginapi.py @@ -294,7 +294,7 @@ class UnhandledCallable(Exception): pass -def callable_runone(hookname, unhandled_okay=False, *args, **kwargs): +def callable_runone(hookname, *args, **kwargs): """ Run the callable hook HOOKNAME... run until the first response, then return. @@ -313,7 +313,7 @@ def callable_runone(hookname, unhandled_okay=False, *args, **kwargs): except CantHandleIt: continue - if unhandled_okay is False: + if kwargs.get("unhandled_okay", False) is False: raise UnhandledCallable( "No hooks registered capable of handling '%s'" % hookname) From 0a5c6ec9011f678d9759183cb22e747d1084769f Mon Sep 17 00:00:00 2001 From: Christopher Allan Webber Date: Wed, 10 Apr 2013 17:42:29 -0500 Subject: [PATCH 4/5] Remove unhandled_okay from kwargs if it's there before passing to functions. --- mediagoblin/tools/pluginapi.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mediagoblin/tools/pluginapi.py b/mediagoblin/tools/pluginapi.py index 2c0394aa..283350a8 100644 --- a/mediagoblin/tools/pluginapi.py +++ b/mediagoblin/tools/pluginapi.py @@ -307,13 +307,15 @@ def callable_runone(hookname, *args, **kwargs): """ callables = PluginManager().get_hook_callables(hookname) + unhandled_okay = kwargs.pop("unhandled_okay", False) + for callable in callables: try: return callable(*args, **kwargs) except CantHandleIt: continue - if kwargs.get("unhandled_okay", False) is False: + if unhandled_okay is False: raise UnhandledCallable( "No hooks registered capable of handling '%s'" % hookname) From 04f295e20db4f8b170d2b0ec21eea850f7aa2bd5 Mon Sep 17 00:00:00 2001 From: Christopher Allan Webber Date: Wed, 10 Apr 2013 17:52:49 -0500 Subject: [PATCH 5/5] Switch several functions over to using the new callable_run* tools! --- mediagoblin/init/celery/__init__.py | 5 ++--- mediagoblin/init/celery/from_celery.py | 6 ++---- mediagoblin/init/plugins/__init__.py | 4 +--- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/mediagoblin/init/celery/__init__.py b/mediagoblin/init/celery/__init__.py index 8d7a41bd..bb0d5989 100644 --- a/mediagoblin/init/celery/__init__.py +++ b/mediagoblin/init/celery/__init__.py @@ -18,7 +18,7 @@ import os import sys from celery import Celery -from mediagoblin.tools.pluginapi import PluginManager +from mediagoblin.tools.pluginapi import callable_runall MANDATORY_CELERY_IMPORTS = ['mediagoblin.processing.task'] @@ -66,8 +66,7 @@ def setup_celery_app(app_config, global_config, celery_app = Celery() celery_app.config_from_object(celery_settings) - for callable_hook in PluginManager().get_hook_callables('celery_setup'): - callable_hook(celery_app) + callable_runall('celery_setup', celery_app) def setup_celery_from_config(app_config, global_config, diff --git a/mediagoblin/init/celery/from_celery.py b/mediagoblin/init/celery/from_celery.py index 8a794abb..e2899c0b 100644 --- a/mediagoblin/init/celery/from_celery.py +++ b/mediagoblin/init/celery/from_celery.py @@ -22,7 +22,7 @@ from celery.signals import setup_logging from mediagoblin import app, mg_globals from mediagoblin.init.celery import setup_celery_from_config -from mediagoblin.tools.pluginapi import PluginManager +from mediagoblin.tools.pluginapi import callable_runall OUR_MODULENAME = __name__ @@ -47,9 +47,7 @@ def setup_logging_from_paste_ini(loglevel, **kw): logging.config.fileConfig(logging_conf_file) - for callable_hook in \ - PluginManager().get_hook_callables('celery_logging_setup'): - callable_hook() + callable_runall('celery_logging_setup') setup_logging.connect(setup_logging_from_paste_ini) diff --git a/mediagoblin/init/plugins/__init__.py b/mediagoblin/init/plugins/__init__.py index cdf9b5ad..72bd5c7d 100644 --- a/mediagoblin/init/plugins/__init__.py +++ b/mediagoblin/init/plugins/__init__.py @@ -59,6 +59,4 @@ def setup_plugins(): pman.register_hooks(plugin.hooks) # Execute anything registered to the setup hook. - setup_list = pman.get_hook_callables('setup') - for fun in setup_list: - fun() + pluginapi.callable_runall('setup')