##// END OF EJS Templates
Merge pull request #5188 from takluyver/callbacks...
Min RK -
r15667:4c8ff1de merge
parent child Browse files
Show More
@@ -0,0 +1,139 b''
1 """Infrastructure for registering and firing callbacks on application events.
2
3 Unlike :mod:`IPython.core.hooks`, which lets end users set single functions to
4 be called at specific times, or a collection of alternative methods to try,
5 callbacks are designed to be used by extension authors. A number of callbacks
6 can be registered for the same event without needing to be aware of one another.
7
8 The functions defined in this module are no-ops indicating the names of available
9 events and the arguments which will be passed to them.
10
11 .. note::
12
13 This API is experimental in IPython 2.0, and may be revised in future versions.
14 """
15 from __future__ import print_function
16
17 class EventManager(object):
18 """Manage a collection of events and a sequence of callbacks for each.
19
20 This is attached to :class:`~IPython.core.interactiveshell.InteractiveShell`
21 instances as an ``events`` attribute.
22
23 .. note::
24
25 This API is experimental in IPython 2.0, and may be revised in future versions.
26 """
27 def __init__(self, shell, available_events):
28 """Initialise the :class:`CallbackManager`.
29
30 Parameters
31 ----------
32 shell
33 The :class:`~IPython.core.interactiveshell.InteractiveShell` instance
34 available_callbacks
35 An iterable of names for callback events.
36 """
37 self.shell = shell
38 self.callbacks = {n:[] for n in available_events}
39
40 def register(self, event, function):
41 """Register a new event callback
42
43 Parameters
44 ----------
45 event : str
46 The event for which to register this callback.
47 function : callable
48 A function to be called on the given event. It should take the same
49 parameters as the appropriate callback prototype.
50
51 Raises
52 ------
53 TypeError
54 If ``function`` is not callable.
55 KeyError
56 If ``event`` is not one of the known events.
57 """
58 if not callable(function):
59 raise TypeError('Need a callable, got %r' % function)
60 self.callbacks[event].append(function)
61
62 def unregister(self, event, function):
63 """Remove a callback from the given event."""
64 self.callbacks[event].remove(function)
65
66 def reset(self, event):
67 """Clear all callbacks for the given event."""
68 self.callbacks[event] = []
69
70 def reset_all(self):
71 """Clear all callbacks for all events."""
72 self.callbacks = {n:[] for n in self.callbacks}
73
74 def trigger(self, event, *args, **kwargs):
75 """Call callbacks for ``event``.
76
77 Any additional arguments are passed to all callbacks registered for this
78 event. Exceptions raised by callbacks are caught, and a message printed.
79 """
80 for func in self.callbacks[event]:
81 try:
82 func(*args, **kwargs)
83 except Exception:
84 print("Error in callback {} (for {}):".format(func, event))
85 self.shell.showtraceback()
86
87 # event_name -> prototype mapping
88 available_events = {}
89
90 def _define_event(callback_proto):
91 available_events[callback_proto.__name__] = callback_proto
92 return callback_proto
93
94 # ------------------------------------------------------------------------------
95 # Callback prototypes
96 #
97 # No-op functions which describe the names of available events and the
98 # signatures of callbacks for those events.
99 # ------------------------------------------------------------------------------
100
101 @_define_event
102 def pre_execute():
103 """Fires before code is executed in response to user/frontend action.
104
105 This includes comm and widget messages and silent execution, as well as user
106 code cells."""
107 pass
108
109 @_define_event
110 def pre_run_cell():
111 """Fires before user-entered code runs."""
112 pass
113
114 @_define_event
115 def post_execute():
116 """Fires after code is executed in response to user/frontend action.
117
118 This includes comm and widget messages and silent execution, as well as user
119 code cells."""
120 pass
121
122 @_define_event
123 def post_run_cell():
124 """Fires after user-entered code runs."""
125 pass
126
127 @_define_event
128 def shell_initialized(ip):
129 """Fires after initialisation of :class:`~IPython.core.interactiveshell.InteractiveShell`.
130
131 This is before extensions and startup scripts are loaded, so it can only be
132 set by subclassing.
133
134 Parameters
135 ----------
136 ip : :class:`~IPython.core.interactiveshell.InteractiveShell`
137 The newly initialised shell.
138 """
139 pass
@@ -0,0 +1,46 b''
1 import unittest
2 try: # Python 3.3 +
3 from unittest.mock import Mock
4 except ImportError:
5 from mock import Mock
6
7 from IPython.core import events
8 import IPython.testing.tools as tt
9
10 def ping_received():
11 pass
12
13 class CallbackTests(unittest.TestCase):
14 def setUp(self):
15 self.em = events.EventManager(get_ipython(), {'ping_received': ping_received})
16
17 def test_register_unregister(self):
18 cb = Mock()
19
20 self.em.register('ping_received', cb)
21 self.em.trigger('ping_received')
22 self.assertEqual(cb.call_count, 1)
23
24 self.em.unregister('ping_received', cb)
25 self.em.trigger('ping_received')
26 self.assertEqual(cb.call_count, 1)
27
28 def test_reset(self):
29 cb = Mock()
30 self.em.register('ping_received', cb)
31 self.em.reset('ping_received')
32 self.em.trigger('ping_received')
33 assert not cb.called
34
35 def test_reset_all(self):
36 cb = Mock()
37 self.em.register('ping_received', cb)
38 self.em.reset_all()
39 self.em.trigger('ping_received')
40 assert not cb.called
41
42 def test_cb_error(self):
43 cb = Mock(side_effect=ValueError)
44 self.em.register('ping_received', cb)
45 with tt.AssertPrints("Error in callback"):
46 self.em.trigger('ping_received') No newline at end of file
@@ -0,0 +1,42 b''
1 =====================
2 Registering callbacks
3 =====================
4
5 Extension code can register callbacks functions which will be called on specific
6 events within the IPython code. You can see the current list of available
7 callbacks, and the parameters that will be passed with each, in the callback
8 prototype functions defined in :mod:`IPython.core.callbacks`.
9
10 To register callbacks, use :meth:`IPython.core.events.EventManager.register`.
11 For example::
12
13 class VarWatcher(object):
14 def __init__(self, ip):
15 self.shell = ip
16 self.last_x = None
17
18 def pre_execute(self):
19 self.last_x = self.shell.user_ns.get('x', None)
20
21 def post_execute(self):
22 if self.shell.user_ns.get('x', None) != self.last_x:
23 print("x changed!")
24
25 def load_ipython_extension(ip):
26 vw = VarWatcher(ip)
27 ip.events.register('pre_execute', vw.pre_execute)
28 ip.events.register('post_execute', vw.post_execute)
29
30 .. note::
31
32 This API is experimental in IPython 2.0, and may be revised in future versions.
33
34 .. seealso::
35
36 Module :mod:`IPython.core.hooks`
37 The older 'hooks' system allows end users to customise some parts of
38 IPython's behaviour.
39
40 :doc:`inputtransforms`
41 By registering input transformers that don't change code, you can monitor
42 what is being executed.
@@ -0,0 +1,1 b''
1 * A new callback system has been introduced. For details, see :doc:`/config/callbacks`.
@@ -7,7 +7,7 b' before_install:'
7 7 # workaround for https://github.com/travis-ci/travis-cookbooks/issues/155
8 8 - sudo rm -rf /dev/shm && sudo ln -s /run/shm /dev/shm
9 9 - easy_install -q pyzmq
10 - pip install jinja2 sphinx pygments tornado requests
10 - pip install jinja2 sphinx pygments tornado requests mock
11 11 # Pierre Carrier's PPA for PhantomJS and CasperJS
12 12 - sudo add-apt-repository -y ppa:pcarrier/ppa
13 13 - sudo apt-get update
@@ -49,6 +49,11 b" __all__ = ['editor', 'fix_error_editor', 'synchronize_with_editor',"
49 49 'show_in_pager','pre_prompt_hook',
50 50 'pre_run_code_hook', 'clipboard_get']
51 51
52 deprecated = {'pre_run_code_hook': "a callback for the 'pre_execute' or 'pre_run_cell' event",
53 'late_startup_hook': "a callback for the 'shell_initialized' event",
54 'shutdown_hook': "the atexit module",
55 }
56
52 57 def editor(self, filename, linenum=None, wait=True):
53 58 """Open the default editor at the given filename and linenumber.
54 59
@@ -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.events import EventManager, available_events
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_events()
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.
@@ -510,6 +512,7 b' class InteractiveShell(SingletonConfigurable):'
510 512 self.init_payload()
511 513 self.init_comms()
512 514 self.hooks.late_startup_hook()
515 self.events.trigger('shell_initialized', self)
513 516 atexit.register(self.atexit_operations)
514 517
515 518 def get_ipython(self):
@@ -785,9 +788,10 b' class InteractiveShell(SingletonConfigurable):'
785 788 for hook_name in hooks.__all__:
786 789 # default hooks have priority 100, i.e. low; user hooks should have
787 790 # 0-100 priority
788 self.set_hook(hook_name,getattr(hooks,hook_name), 100)
791 self.set_hook(hook_name,getattr(hooks,hook_name), 100, _warn_deprecated=False)
789 792
790 def set_hook(self,name,hook, priority = 50, str_key = None, re_key = None):
793 def set_hook(self,name,hook, priority=50, str_key=None, re_key=None,
794 _warn_deprecated=True):
791 795 """set_hook(name,hook) -> sets an internal IPython hook.
792 796
793 797 IPython exposes some of its internal API as user-modifiable hooks. By
@@ -816,6 +820,11 b' class InteractiveShell(SingletonConfigurable):'
816 820 if name not in IPython.core.hooks.__all__:
817 821 print("Warning! Hook '%s' is not one of %s" % \
818 822 (name, IPython.core.hooks.__all__ ))
823
824 if _warn_deprecated and (name in IPython.core.hooks.deprecated):
825 alternative = IPython.core.hooks.deprecated[name]
826 warn("Hook {} is deprecated. Use {} instead.".format(name, alternative))
827
819 828 if not dp:
820 829 dp = IPython.core.hooks.CommandChainDispatcher()
821 830
@@ -827,12 +836,21 b' class InteractiveShell(SingletonConfigurable):'
827 836
828 837 setattr(self.hooks,name, dp)
829 838
839 #-------------------------------------------------------------------------
840 # Things related to events
841 #-------------------------------------------------------------------------
842
843 def init_events(self):
844 self.events = EventManager(self, available_events)
845
830 846 def register_post_execute(self, func):
831 """Register a function for calling after code execution.
847 """DEPRECATED: Use ip.events.register('post_run_cell', func)
848
849 Register a function for calling after code execution.
832 850 """
833 if not callable(func):
834 raise ValueError('argument %s must be callable' % func)
835 self._post_execute[func] = True
851 warn("ip.register_post_execute is deprecated, use "
852 "ip.events.register('post_run_cell', func) instead.")
853 self.events.register('post_run_cell', func)
836 854
837 855 #-------------------------------------------------------------------------
838 856 # Things related to the "main" module
@@ -2649,6 +2667,10 b' class InteractiveShell(SingletonConfigurable):'
2649 2667 if silent:
2650 2668 store_history = False
2651 2669
2670 self.events.trigger('pre_execute')
2671 if not silent:
2672 self.events.trigger('pre_run_cell')
2673
2652 2674 # If any of our input transformation (input_transformer_manager or
2653 2675 # prefilter_manager) raises an exception, we store it in this variable
2654 2676 # so that we can display the error after logging the input and storing
@@ -2717,28 +2739,10 b' class InteractiveShell(SingletonConfigurable):'
2717 2739 interactivity = "none" if silent else self.ast_node_interactivity
2718 2740 self.run_ast_nodes(code_ast.body, cell_name,
2719 2741 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)
2742
2743 self.events.trigger('post_execute')
2744 if not silent:
2745 self.events.trigger('post_run_cell')
2742 2746
2743 2747 if store_history:
2744 2748 # Write output to the database. Does nothing unless
@@ -353,7 +353,7 b' def configure_inline_support(shell, backend):'
353 353
354 354 if backend == backends['inline']:
355 355 from IPython.kernel.zmq.pylab.backend_inline import flush_figures
356 shell.register_post_execute(flush_figures)
356 shell.events.register('post_execute', flush_figures)
357 357
358 358 # Save rcParams that will be overwrittern
359 359 shell._saved_rcParams = dict()
@@ -363,8 +363,10 b' def configure_inline_support(shell, backend):'
363 363 pyplot.rcParams.update(cfg.rc)
364 364 else:
365 365 from IPython.kernel.zmq.pylab.backend_inline import flush_figures
366 if flush_figures in shell._post_execute:
367 shell._post_execute.pop(flush_figures)
366 try:
367 shell.events.unregister('post_execute', flush_figures)
368 except ValueError:
369 pass
368 370 if hasattr(shell, '_saved_rcParams'):
369 371 pyplot.rcParams.update(shell._saved_rcParams)
370 372 del shell._saved_rcParams
@@ -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_run_cell 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.events.register('pre_run_cell', pre_explicit)
292 ip.events.register('pre_execute', pre_always)
293 ip.events.register('post_run_cell', post_explicit)
294 ip.events.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.events.reset_all()
295 310
296 311 def test_silent_noadvance(self):
297 312 """run_cell(silent=True) doesn't advance execution_count"""
@@ -392,7 +392,6 b' def superreload(module, reload=reload, old_objects={}):'
392 392 # IPython connectivity
393 393 #------------------------------------------------------------------------------
394 394
395 from IPython.core.hooks import TryNext
396 395 from IPython.core.magic import Magics, magics_class, line_magic
397 396
398 397 @magics_class
@@ -491,17 +490,16 b' class AutoreloadMagics(Magics):'
491 490 # Inject module to user namespace
492 491 self.shell.push({top_name: top_module})
493 492
494 def pre_run_code_hook(self, ip):
495 if not self._reloader.enabled:
496 raise TryNext
497 try:
498 self._reloader.check()
499 except:
500 pass
493 def pre_run_cell(self):
494 if self._reloader.enabled:
495 try:
496 self._reloader.check()
497 except:
498 pass
501 499
502 500
503 501 def load_ipython_extension(ip):
504 502 """Load the extension in IPython."""
505 503 auto_reload = AutoreloadMagics(ip)
506 504 ip.register_magics(auto_reload)
507 ip.set_hook('pre_run_code_hook', auto_reload.pre_run_code_hook)
505 ip.events.register('pre_run_cell', auto_reload.pre_run_cell)
@@ -23,7 +23,7 b' import nose.tools as nt'
23 23 import IPython.testing.tools as tt
24 24
25 25 from IPython.extensions.autoreload import AutoreloadMagics
26 from IPython.core.hooks import TryNext
26 from IPython.core.events import EventManager, pre_run_cell
27 27 from IPython.utils.py3compat import PY3
28 28
29 29 if PY3:
@@ -41,15 +41,14 b' class FakeShell(object):'
41 41
42 42 def __init__(self):
43 43 self.ns = {}
44 self.events = EventManager(self, {'pre_run_cell', pre_run_cell})
44 45 self.auto_magics = AutoreloadMagics(shell=self)
46 self.events.register('pre_run_cell', self.auto_magics.pre_run_cell)
45 47
46 48 register_magics = set_hook = noop
47 49
48 50 def run_code(self, code):
49 try:
50 self.auto_magics.pre_run_code_hook(self)
51 except TryNext:
52 pass
51 self.events.trigger('pre_run_cell')
53 52 exec(code, self.ns)
54 53
55 54 def push(self, items):
@@ -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.events.trigger('pre_execute')
137 138 self._msg_callback(msg)
139 self.shell.events.trigger('post_execute')
138 140
139 141
140 142 __all__ = ['Comm']
@@ -1,7 +1,7 b''
1 """Function signature objects for callables
1 """Function signature objects for callables.
2 2
3 3 Back port of Python 3.3's function signature tools from the inspect module,
4 modified to be compatible with Python 2.6, 2.7 and 3.2+.
4 modified to be compatible with Python 2.7 and 3.2+.
5 5 """
6 6
7 7 #-----------------------------------------------------------------------------
@@ -346,20 +346,20 b' class Parameter(object):'
346 346
347 347
348 348 class BoundArguments(object):
349 '''Result of `Signature.bind` call. Holds the mapping of arguments
349 '''Result of :meth:`Signature.bind` call. Holds the mapping of arguments
350 350 to the function's parameters.
351 351
352 352 Has the following public attributes:
353 353
354 * arguments : OrderedDict
355 An ordered mutable mapping of parameters' names to arguments' values.
356 Does not contain arguments' default values.
357 * signature : Signature
358 The Signature object that created this instance.
359 * args : tuple
360 Tuple of positional arguments values.
361 * kwargs : dict
362 Dict of keyword arguments values.
354 arguments : :class:`collections.OrderedDict`
355 An ordered mutable mapping of parameters' names to arguments' values.
356 Does not contain arguments' default values.
357 signature : :class:`Signature`
358 The Signature object that created this instance.
359 args : tuple
360 Tuple of positional arguments values.
361 kwargs : dict
362 Dict of keyword arguments values.
363 363 '''
364 364
365 365 def __init__(self, signature, arguments):
@@ -447,22 +447,16 b' class Signature(object):'
447 447 It stores a Parameter object for each parameter accepted by the
448 448 function, as well as information specific to the function itself.
449 449
450 A Signature object has the following public attributes and methods:
451
452 * parameters : OrderedDict
453 An ordered mapping of parameters' names to the corresponding
454 Parameter objects (keyword-only arguments are in the same order
455 as listed in `code.co_varnames`).
456 * return_annotation : object
457 The annotation for the return type of the function if specified.
458 If the function has no annotation for its return type, this
459 attribute is not set.
460 * bind(*args, **kwargs) -> BoundArguments
461 Creates a mapping from positional and keyword arguments to
462 parameters.
463 * bind_partial(*args, **kwargs) -> BoundArguments
464 Creates a partial mapping from positional and keyword arguments
465 to parameters (simulating 'functools.partial' behavior.)
450 A Signature object has the following public attributes:
451
452 parameters : :class:`collections.OrderedDict`
453 An ordered mapping of parameters' names to the corresponding
454 Parameter objects (keyword-only arguments are in the same order
455 as listed in `code.co_varnames`).
456 return_annotation
457 The annotation for the return type of the function if specified.
458 If the function has no annotation for its return type, this
459 attribute is not set.
466 460 '''
467 461
468 462 __slots__ = ('_return_annotation', '_parameters')
@@ -775,16 +769,16 b' class Signature(object):'
775 769 return self._bound_arguments_cls(self, arguments)
776 770
777 771 def bind(self, *args, **kwargs):
778 '''Get a BoundArguments object, that maps the passed `args`
779 and `kwargs` to the function's signature. Raises `TypeError`
772 '''Get a :class:`BoundArguments` object, that maps the passed `args`
773 and `kwargs` to the function's signature. Raises :exc:`TypeError`
780 774 if the passed arguments can not be bound.
781 775 '''
782 776 return self._bind(args, kwargs)
783 777
784 778 def bind_partial(self, *args, **kwargs):
785 '''Get a BoundArguments object, that partially maps the
779 '''Get a :class:`BoundArguments` object, that partially maps the
786 780 passed `args` and `kwargs` to the function's signature.
787 Raises `TypeError` if the passed arguments can not be bound.
781 Raises :exc:`TypeError` if the passed arguments can not be bound.
788 782 '''
789 783 return self._bind(args, kwargs, partial=True)
790 784
@@ -28,3 +28,4 b' Extending and integrating with IPython'
28 28 extensions/index
29 29 integrating
30 30 inputtransforms
31 callbacks
@@ -273,10 +273,14 b' extras_require = dict('
273 273 notebook = ['tornado>=3.1', 'pyzmq>=2.1.11', 'jinja2'],
274 274 nbconvert = ['pygments', 'jinja2', 'Sphinx>=0.3']
275 275 )
276 if sys.version_info < (3, 3):
277 extras_require['test'].append('mock')
278
276 279 everything = set()
277 280 for deps in extras_require.values():
278 281 everything.update(deps)
279 282 extras_require['all'] = everything
283
280 284 install_requires = []
281 285 if sys.platform == 'darwin':
282 286 if any(arg.startswith('bdist') for arg in sys.argv) or not setupext.check_for_readline():
@@ -639,10 +639,11 b' def get_bdist_wheel():'
639 639 if found:
640 640 lis.pop(idx)
641 641
642 for pkg in ("gnureadline", "pyreadline"):
642 for pkg in ("gnureadline", "pyreadline", "mock"):
643 643 _remove_startswith(requires, pkg)
644 644 requires.append("gnureadline; sys.platform == 'darwin' and platform.python_implementation == 'CPython'")
645 645 requires.append("pyreadline (>=2.0); sys.platform == 'win32' and platform.python_implementation == 'CPython'")
646 requires.append("mock; extra == 'test' and python_version < '3.3'")
646 647 for r in requires:
647 648 pkg_info['Requires-Dist'] = r
648 649 write_pkg_info(metadata_path, pkg_info)
@@ -9,6 +9,7 b' envlist = py27, py33'
9 9 [testenv]
10 10 deps =
11 11 nose
12 mock
12 13 tornado
13 14 jinja2
14 15 sphinx
General Comments 0
You need to be logged in to leave comments. Login now