From e3e8fef0d87d780660f4f845d099c7886636571d 2014-03-03 20:29:03 From: Thomas Kluyver Date: 2014-03-03 20:29:03 Subject: [PATCH] Start of new callback system --- diff --git a/IPython/core/callbacks.py b/IPython/core/callbacks.py new file mode 100644 index 0000000..98a293b --- /dev/null +++ b/IPython/core/callbacks.py @@ -0,0 +1,57 @@ +from __future__ import print_function + +class CallbackManager(object): + def __init__(self, shell, available_callbacks): + self.shell = shell + self.callbacks = {n:[] for n in available_callbacks} + + def register(self, name, function): + if not callable(function): + raise TypeError('Need a callable, got %r' % function) + self.callbacks[name].append(function) + + def unregister(self, name, function): + self.callbacks[name].remove(function) + + def reset(self, name): + self.callbacks[name] = [] + + def reset_all(self): + self.callbacks = {n:[] for n in self.callbacks} + + def fire(self, name, *args, **kwargs): + for func in self.callbacks[name]: + try: + func(*args, **kwargs) + except Exception: + print("Error in callback {} (for {}):".format(func, name)) + self.shell.showtraceback() + +available_callbacks = {} +def _collect(callback_proto): + available_callbacks[callback_proto.__name__] = callback_proto + return callback_proto + +@_collect +def pre_execute(): + """Fires before code is executed in response to user/frontend action. + + This includes comm and widget messages.""" + pass + +@_collect +def pre_execute_explicit(): + """Fires before user-entered code runs.""" + pass + +@_collect +def post_execute(): + """Fires after code is executed in response to user/frontend action. + + This includes comm and widget messages.""" + pass + +@_collect +def post_execute_explicit(): + """Fires after user-entered code runs.""" + pass \ No newline at end of file diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 553894c..de5be75 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -41,6 +41,7 @@ from IPython.core import ultratb from IPython.core.alias import AliasManager, AliasError from IPython.core.autocall import ExitAutocall from IPython.core.builtin_trap import BuiltinTrap +from IPython.core.callbacks import CallbackManager, available_callbacks from IPython.core.compilerop import CachingCompiler, check_linecache_ipython from IPython.core.display_trap import DisplayTrap from IPython.core.displayhook import DisplayHook @@ -467,6 +468,7 @@ class InteractiveShell(SingletonConfigurable): self.init_syntax_highlighting() self.init_hooks() + self.init_callbacks() self.init_pushd_popd_magic() # self.init_traceback_handlers use to be here, but we moved it below # because it and init_io have to come after init_readline. @@ -827,12 +829,21 @@ class InteractiveShell(SingletonConfigurable): setattr(self.hooks,name, dp) + #------------------------------------------------------------------------- + # Things related to callbacks + #------------------------------------------------------------------------- + + def init_callbacks(self): + self.callbacks = CallbackManager(self, available_callbacks) + def register_post_execute(self, func): - """Register a function for calling after code execution. + """DEPRECATED: Use ip.callbacks.register('post_execute_explicit', func) + + Register a function for calling after code execution. """ - if not callable(func): - raise ValueError('argument %s must be callable' % func) - self._post_execute[func] = True + warn("ip.register_post_execute is deprecated, use " + "ip.callbacks.register('post_execute_explicit', func) instead.") + self.callbacks.register('post_execute_explicit', func) #------------------------------------------------------------------------- # Things related to the "main" module @@ -2649,6 +2660,10 @@ class InteractiveShell(SingletonConfigurable): if silent: store_history = False + self.callbacks.fire('pre_execute') + if not silent: + self.callbacks.fire('pre_execute_explicit') + # If any of our input transformation (input_transformer_manager or # prefilter_manager) raises an exception, we store it in this variable # so that we can display the error after logging the input and storing @@ -2717,28 +2732,10 @@ class InteractiveShell(SingletonConfigurable): interactivity = "none" if silent else self.ast_node_interactivity self.run_ast_nodes(code_ast.body, cell_name, interactivity=interactivity, compiler=compiler) - - # Execute any registered post-execution functions. - # unless we are silent - post_exec = [] if silent else iteritems(self._post_execute) - - for func, status in post_exec: - if self.disable_failing_post_execute and not status: - continue - try: - func() - except KeyboardInterrupt: - print("\nKeyboardInterrupt", file=io.stderr) - except Exception: - # register as failing: - self._post_execute[func] = False - self.showtraceback() - print('\n'.join([ - "post-execution function %r produced an error." % func, - "If this problem persists, you can disable failing post-exec functions with:", - "", - " get_ipython().disable_failing_post_execute = True" - ]), file=io.stderr) + + self.callbacks.fire('post_execute') + if not silent: + self.callbacks.fire('post_execute_explicit') if store_history: # Write output to the database. Does nothing unless diff --git a/IPython/core/tests/test_interactiveshell.py b/IPython/core/tests/test_interactiveshell.py index 5b1784a..eedcdc0 100644 --- a/IPython/core/tests/test_interactiveshell.py +++ b/IPython/core/tests/test_interactiveshell.py @@ -27,6 +27,10 @@ import shutil import sys import tempfile import unittest +try: + from unittest import mock +except ImportError: + import mock from os.path import join # third-party @@ -277,21 +281,32 @@ class InteractiveShellTestCase(unittest.TestCase): # ZeroDivisionError self.assertEqual(ip.var_expand(u"{1/0}"), u"{1/0}") - def test_silent_nopostexec(self): - """run_cell(silent=True) doesn't invoke post-exec funcs""" - d = dict(called=False) - def set_called(): - d['called'] = True + def test_silent_postexec(self): + """run_cell(silent=True) doesn't invoke pre/post_execute_explicit callbacks""" + pre_explicit = mock.Mock() + pre_always = mock.Mock() + post_explicit = mock.Mock() + post_always = mock.Mock() - ip.register_post_execute(set_called) - ip.run_cell("1", silent=True) - self.assertFalse(d['called']) - # double-check that non-silent exec did what we expected - # silent to avoid - ip.run_cell("1") - self.assertTrue(d['called']) - # remove post-exec - ip._post_execute.pop(set_called) + ip.callbacks.register('pre_execute_explicit', pre_explicit) + ip.callbacks.register('pre_execute', pre_always) + ip.callbacks.register('post_execute_explicit', post_explicit) + ip.callbacks.register('post_execute', post_always) + + try: + ip.run_cell("1", silent=True) + assert pre_always.called + assert not pre_explicit.called + assert post_always.called + assert not post_explicit.called + # double-check that non-silent exec did what we expected + # silent to avoid + ip.run_cell("1") + assert pre_explicit.called + assert post_explicit.called + finally: + # remove post-exec + ip.callbacks.reset_all() def test_silent_noadvance(self): """run_cell(silent=True) doesn't advance execution_count""" diff --git a/IPython/kernel/comm/comm.py b/IPython/kernel/comm/comm.py index 28a4ece..30fcad8 100644 --- a/IPython/kernel/comm/comm.py +++ b/IPython/kernel/comm/comm.py @@ -134,7 +134,9 @@ class Comm(LoggingConfigurable): """Handle a comm_msg message""" self.log.debug("handle_msg[%s](%s)", self.comm_id, msg) if self._msg_callback: + self.shell.callbacks.fire('pre_execute') self._msg_callback(msg) + self.shell.callbacks.fire('post_execute') __all__ = ['Comm']