##// END OF EJS Templates
Start of new callback system
Thomas Kluyver -
Show More
@@ -0,0 +1,57 b''
1 from __future__ import print_function
2
3 class CallbackManager(object):
4 def __init__(self, shell, available_callbacks):
5 self.shell = shell
6 self.callbacks = {n:[] for n in available_callbacks}
7
8 def register(self, name, function):
9 if not callable(function):
10 raise TypeError('Need a callable, got %r' % function)
11 self.callbacks[name].append(function)
12
13 def unregister(self, name, function):
14 self.callbacks[name].remove(function)
15
16 def reset(self, name):
17 self.callbacks[name] = []
18
19 def reset_all(self):
20 self.callbacks = {n:[] for n in self.callbacks}
21
22 def fire(self, name, *args, **kwargs):
23 for func in self.callbacks[name]:
24 try:
25 func(*args, **kwargs)
26 except Exception:
27 print("Error in callback {} (for {}):".format(func, name))
28 self.shell.showtraceback()
29
30 available_callbacks = {}
31 def _collect(callback_proto):
32 available_callbacks[callback_proto.__name__] = callback_proto
33 return callback_proto
34
35 @_collect
36 def pre_execute():
37 """Fires before code is executed in response to user/frontend action.
38
39 This includes comm and widget messages."""
40 pass
41
42 @_collect
43 def pre_execute_explicit():
44 """Fires before user-entered code runs."""
45 pass
46
47 @_collect
48 def post_execute():
49 """Fires after code is executed in response to user/frontend action.
50
51 This includes comm and widget messages."""
52 pass
53
54 @_collect
55 def post_execute_explicit():
56 """Fires after user-entered code runs."""
57 pass No newline at end of file
@@ -41,6 +41,7 b' from IPython.core import ultratb'
41 41 from IPython.core.alias import AliasManager, AliasError
42 42 from IPython.core.autocall import ExitAutocall
43 43 from IPython.core.builtin_trap import BuiltinTrap
44 from IPython.core.callbacks import CallbackManager, available_callbacks
44 45 from IPython.core.compilerop import CachingCompiler, check_linecache_ipython
45 46 from IPython.core.display_trap import DisplayTrap
46 47 from IPython.core.displayhook import DisplayHook
@@ -467,6 +468,7 b' class InteractiveShell(SingletonConfigurable):'
467 468
468 469 self.init_syntax_highlighting()
469 470 self.init_hooks()
471 self.init_callbacks()
470 472 self.init_pushd_popd_magic()
471 473 # self.init_traceback_handlers use to be here, but we moved it below
472 474 # because it and init_io have to come after init_readline.
@@ -827,12 +829,21 b' class InteractiveShell(SingletonConfigurable):'
827 829
828 830 setattr(self.hooks,name, dp)
829 831
832 #-------------------------------------------------------------------------
833 # Things related to callbacks
834 #-------------------------------------------------------------------------
835
836 def init_callbacks(self):
837 self.callbacks = CallbackManager(self, available_callbacks)
838
830 839 def register_post_execute(self, func):
831 """Register a function for calling after code execution.
840 """DEPRECATED: Use ip.callbacks.register('post_execute_explicit', func)
841
842 Register a function for calling after code execution.
832 843 """
833 if not callable(func):
834 raise ValueError('argument %s must be callable' % func)
835 self._post_execute[func] = True
844 warn("ip.register_post_execute is deprecated, use "
845 "ip.callbacks.register('post_execute_explicit', func) instead.")
846 self.callbacks.register('post_execute_explicit', func)
836 847
837 848 #-------------------------------------------------------------------------
838 849 # Things related to the "main" module
@@ -2649,6 +2660,10 b' class InteractiveShell(SingletonConfigurable):'
2649 2660 if silent:
2650 2661 store_history = False
2651 2662
2663 self.callbacks.fire('pre_execute')
2664 if not silent:
2665 self.callbacks.fire('pre_execute_explicit')
2666
2652 2667 # If any of our input transformation (input_transformer_manager or
2653 2668 # prefilter_manager) raises an exception, we store it in this variable
2654 2669 # so that we can display the error after logging the input and storing
@@ -2717,28 +2732,10 b' class InteractiveShell(SingletonConfigurable):'
2717 2732 interactivity = "none" if silent else self.ast_node_interactivity
2718 2733 self.run_ast_nodes(code_ast.body, cell_name,
2719 2734 interactivity=interactivity, compiler=compiler)
2720
2721 # Execute any registered post-execution functions.
2722 # unless we are silent
2723 post_exec = [] if silent else iteritems(self._post_execute)
2724
2725 for func, status in post_exec:
2726 if self.disable_failing_post_execute and not status:
2727 continue
2728 try:
2729 func()
2730 except KeyboardInterrupt:
2731 print("\nKeyboardInterrupt", file=io.stderr)
2732 except Exception:
2733 # register as failing:
2734 self._post_execute[func] = False
2735 self.showtraceback()
2736 print('\n'.join([
2737 "post-execution function %r produced an error." % func,
2738 "If this problem persists, you can disable failing post-exec functions with:",
2739 "",
2740 " get_ipython().disable_failing_post_execute = True"
2741 ]), file=io.stderr)
2735
2736 self.callbacks.fire('post_execute')
2737 if not silent:
2738 self.callbacks.fire('post_execute_explicit')
2742 2739
2743 2740 if store_history:
2744 2741 # Write output to the database. Does nothing unless
@@ -27,6 +27,10 b' import shutil'
27 27 import sys
28 28 import tempfile
29 29 import unittest
30 try:
31 from unittest import mock
32 except ImportError:
33 import mock
30 34 from os.path import join
31 35
32 36 # third-party
@@ -277,21 +281,32 b' class InteractiveShellTestCase(unittest.TestCase):'
277 281 # ZeroDivisionError
278 282 self.assertEqual(ip.var_expand(u"{1/0}"), u"{1/0}")
279 283
280 def test_silent_nopostexec(self):
281 """run_cell(silent=True) doesn't invoke post-exec funcs"""
282 d = dict(called=False)
283 def set_called():
284 d['called'] = True
284 def test_silent_postexec(self):
285 """run_cell(silent=True) doesn't invoke pre/post_execute_explicit callbacks"""
286 pre_explicit = mock.Mock()
287 pre_always = mock.Mock()
288 post_explicit = mock.Mock()
289 post_always = mock.Mock()
285 290
286 ip.register_post_execute(set_called)
287 ip.run_cell("1", silent=True)
288 self.assertFalse(d['called'])
289 # double-check that non-silent exec did what we expected
290 # silent to avoid
291 ip.run_cell("1")
292 self.assertTrue(d['called'])
293 # remove post-exec
294 ip._post_execute.pop(set_called)
291 ip.callbacks.register('pre_execute_explicit', pre_explicit)
292 ip.callbacks.register('pre_execute', pre_always)
293 ip.callbacks.register('post_execute_explicit', post_explicit)
294 ip.callbacks.register('post_execute', post_always)
295
296 try:
297 ip.run_cell("1", silent=True)
298 assert pre_always.called
299 assert not pre_explicit.called
300 assert post_always.called
301 assert not post_explicit.called
302 # double-check that non-silent exec did what we expected
303 # silent to avoid
304 ip.run_cell("1")
305 assert pre_explicit.called
306 assert post_explicit.called
307 finally:
308 # remove post-exec
309 ip.callbacks.reset_all()
295 310
296 311 def test_silent_noadvance(self):
297 312 """run_cell(silent=True) doesn't advance execution_count"""
@@ -134,7 +134,9 b' class Comm(LoggingConfigurable):'
134 134 """Handle a comm_msg message"""
135 135 self.log.debug("handle_msg[%s](%s)", self.comm_id, msg)
136 136 if self._msg_callback:
137 self.shell.callbacks.fire('pre_execute')
137 138 self._msg_callback(msg)
139 self.shell.callbacks.fire('post_execute')
138 140
139 141
140 142 __all__ = ['Comm']
General Comments 0
You need to be logged in to leave comments. Login now