##// END OF EJS Templates
Attempt to remove backcall. (#14216)...
Matthias Bussonnier -
r28471:d55e23e1 merge
parent child Browse files
Show More
@@ -1,166 +1,154 b''
1 1 """Infrastructure for registering and firing callbacks on application events.
2 2
3 3 Unlike :mod:`IPython.core.hooks`, which lets end users set single functions to
4 4 be called at specific times, or a collection of alternative methods to try,
5 5 callbacks are designed to be used by extension authors. A number of callbacks
6 6 can be registered for the same event without needing to be aware of one another.
7 7
8 8 The functions defined in this module are no-ops indicating the names of available
9 9 events and the arguments which will be passed to them.
10 10
11 11 .. note::
12 12
13 13 This API is experimental in IPython 2.0, and may be revised in future versions.
14 14 """
15 15
16 from backcall import callback_prototype
17
18 16
19 17 class EventManager(object):
20 18 """Manage a collection of events and a sequence of callbacks for each.
21 19
22 20 This is attached to :class:`~IPython.core.interactiveshell.InteractiveShell`
23 21 instances as an ``events`` attribute.
24 22
25 23 .. note::
26 24
27 25 This API is experimental in IPython 2.0, and may be revised in future versions.
28 26 """
29 27
30 28 def __init__(self, shell, available_events, print_on_error=True):
31 29 """Initialise the :class:`CallbackManager`.
32 30
33 31 Parameters
34 32 ----------
35 33 shell
36 34 The :class:`~IPython.core.interactiveshell.InteractiveShell` instance
37 35 available_events
38 36 An iterable of names for callback events.
39 37 print_on_error:
40 38 A boolean flag to set whether the EventManager will print a warning which a event errors.
41 39 """
42 40 self.shell = shell
43 41 self.callbacks = {n:[] for n in available_events}
44 42 self.print_on_error = print_on_error
45 43
46 44 def register(self, event, function):
47 45 """Register a new event callback.
48 46
49 47 Parameters
50 48 ----------
51 49 event : str
52 50 The event for which to register this callback.
53 51 function : callable
54 52 A function to be called on the given event. It should take the same
55 53 parameters as the appropriate callback prototype.
56 54
57 55 Raises
58 56 ------
59 57 TypeError
60 58 If ``function`` is not callable.
61 59 KeyError
62 60 If ``event`` is not one of the known events.
63 61 """
64 62 if not callable(function):
65 63 raise TypeError('Need a callable, got %r' % function)
66 callback_proto = available_events.get(event)
67 64 if function not in self.callbacks[event]:
68 self.callbacks[event].append(callback_proto.adapt(function))
65 self.callbacks[event].append(function)
69 66
70 67 def unregister(self, event, function):
71 68 """Remove a callback from the given event."""
72 69 if function in self.callbacks[event]:
73 70 return self.callbacks[event].remove(function)
74 71
75 # Remove callback in case ``function`` was adapted by `backcall`.
76 for callback in self.callbacks[event]:
77 try:
78 if callback.__wrapped__ is function:
79 return self.callbacks[event].remove(callback)
80 except AttributeError:
81 pass
82
83 72 raise ValueError('Function {!r} is not registered as a {} callback'.format(function, event))
84 73
85 74 def trigger(self, event, *args, **kwargs):
86 75 """Call callbacks for ``event``.
87 76
88 77 Any additional arguments are passed to all callbacks registered for this
89 78 event. Exceptions raised by callbacks are caught, and a message printed.
90 79 """
91 80 for func in self.callbacks[event][:]:
92 81 try:
93 82 func(*args, **kwargs)
94 83 except (Exception, KeyboardInterrupt):
95 84 if self.print_on_error:
96 85 print("Error in callback {} (for {}):".format(func, event))
97 86 self.shell.showtraceback()
98 87
99 88 # event_name -> prototype mapping
100 89 available_events = {}
101 90
102 91 def _define_event(callback_function):
103 callback_proto = callback_prototype(callback_function)
104 available_events[callback_function.__name__] = callback_proto
105 return callback_proto
92 available_events[callback_function.__name__] = callback_function
93 return callback_function
106 94
107 95 # ------------------------------------------------------------------------------
108 96 # Callback prototypes
109 97 #
110 98 # No-op functions which describe the names of available events and the
111 99 # signatures of callbacks for those events.
112 100 # ------------------------------------------------------------------------------
113 101
114 102 @_define_event
115 103 def pre_execute():
116 104 """Fires before code is executed in response to user/frontend action.
117 105
118 106 This includes comm and widget messages and silent execution, as well as user
119 107 code cells.
120 108 """
121 109 pass
122 110
123 111 @_define_event
124 112 def pre_run_cell(info):
125 113 """Fires before user-entered code runs.
126 114
127 115 Parameters
128 116 ----------
129 117 info : :class:`~IPython.core.interactiveshell.ExecutionInfo`
130 118 An object containing information used for the code execution.
131 119 """
132 120 pass
133 121
134 122 @_define_event
135 123 def post_execute():
136 124 """Fires after code is executed in response to user/frontend action.
137 125
138 126 This includes comm and widget messages and silent execution, as well as user
139 127 code cells.
140 128 """
141 129 pass
142 130
143 131 @_define_event
144 132 def post_run_cell(result):
145 133 """Fires after user-entered code runs.
146 134
147 135 Parameters
148 136 ----------
149 137 result : :class:`~IPython.core.interactiveshell.ExecutionResult`
150 138 The object which will be returned as the execution result.
151 139 """
152 140 pass
153 141
154 142 @_define_event
155 143 def shell_initialized(ip):
156 144 """Fires after initialisation of :class:`~IPython.core.interactiveshell.InteractiveShell`.
157 145
158 146 This is before extensions and startup scripts are loaded, so it can only be
159 147 set by subclassing.
160 148
161 149 Parameters
162 150 ----------
163 151 ip : :class:`~IPython.core.interactiveshell.InteractiveShell`
164 152 The newly initialised shell.
165 153 """
166 154 pass
@@ -1,91 +1,78 b''
1 1 import unittest
2 2 from unittest.mock import Mock
3 3
4 4 from IPython.core import events
5 5 import IPython.testing.tools as tt
6 6
7 7
8 8 @events._define_event
9 9 def ping_received():
10 10 pass
11 11
12 12
13 13 @events._define_event
14 14 def event_with_argument(argument):
15 15 pass
16 16
17 17
18 18 class CallbackTests(unittest.TestCase):
19 19 def setUp(self):
20 20 self.em = events.EventManager(get_ipython(),
21 21 {'ping_received': ping_received,
22 22 'event_with_argument': event_with_argument})
23 23
24 24 def test_register_unregister(self):
25 25 cb = Mock()
26 26
27 27 self.em.register('ping_received', cb)
28 28 self.em.trigger('ping_received')
29 29 self.assertEqual(cb.call_count, 1)
30 30
31 31 self.em.unregister('ping_received', cb)
32 32 self.em.trigger('ping_received')
33 33 self.assertEqual(cb.call_count, 1)
34 34
35 35 def test_bare_function_missed_unregister(self):
36 36 def cb1():
37 37 ...
38 38
39 39 def cb2():
40 40 ...
41 41
42 42 self.em.register("ping_received", cb1)
43 43 self.assertRaises(ValueError, self.em.unregister, "ping_received", cb2)
44 44 self.em.unregister("ping_received", cb1)
45 45
46 46 def test_cb_error(self):
47 47 cb = Mock(side_effect=ValueError)
48 48 self.em.register('ping_received', cb)
49 49 with tt.AssertPrints("Error in callback"):
50 50 self.em.trigger('ping_received')
51 51
52 52 def test_cb_keyboard_interrupt(self):
53 53 cb = Mock(side_effect=KeyboardInterrupt)
54 54 self.em.register('ping_received', cb)
55 55 with tt.AssertPrints("Error in callback"):
56 56 self.em.trigger('ping_received')
57 57
58 58 def test_unregister_during_callback(self):
59 59 invoked = [False] * 3
60 60
61 61 def func1(*_):
62 62 invoked[0] = True
63 63 self.em.unregister('ping_received', func1)
64 64 self.em.register('ping_received', func3)
65 65
66 66 def func2(*_):
67 67 invoked[1] = True
68 68 self.em.unregister('ping_received', func2)
69 69
70 70 def func3(*_):
71 71 invoked[2] = True
72 72
73 73 self.em.register('ping_received', func1)
74 74 self.em.register('ping_received', func2)
75 75
76 76 self.em.trigger('ping_received')
77 77 self.assertEqual([True, True, False], invoked)
78 78 self.assertEqual([func3], self.em.callbacks['ping_received'])
79
80 def test_ignore_event_arguments_if_no_argument_required(self):
81 call_count = [0]
82 def event_with_no_argument():
83 call_count[0] += 1
84
85 self.em.register('event_with_argument', event_with_no_argument)
86 self.em.trigger('event_with_argument', 'the argument')
87 self.assertEqual(call_count[0], 1)
88
89 self.em.unregister('event_with_argument', event_with_no_argument)
90 self.em.trigger('ping_received')
91 self.assertEqual(call_count[0], 1)
@@ -1,118 +1,117 b''
1 1 [metadata]
2 2 name = ipython
3 3 version = attr: IPython.core.release.__version__
4 4 url = https://ipython.org
5 5 description = IPython: Productive Interactive Computing
6 6 long_description_content_type = text/x-rst
7 7 long_description = file: long_description.rst
8 8 license_file = LICENSE
9 9 project_urls =
10 10 Documentation = https://ipython.readthedocs.io/
11 11 Funding = https://numfocus.org/
12 12 Source = https://github.com/ipython/ipython
13 13 Tracker = https://github.com/ipython/ipython/issues
14 14 keywords = Interactive, Interpreter, Shell, Embedding
15 15 platforms = Linux, Mac OSX, Windows
16 16 classifiers =
17 17 Framework :: IPython
18 18 Framework :: Jupyter
19 19 Intended Audience :: Developers
20 20 Intended Audience :: Science/Research
21 21 License :: OSI Approved :: BSD License
22 22 Programming Language :: Python
23 23 Programming Language :: Python :: 3
24 24 Programming Language :: Python :: 3 :: Only
25 25 Topic :: System :: Shells
26 26
27 27 [options]
28 28 packages = find:
29 29 python_requires = >=3.9
30 30 zip_safe = False
31 31 install_requires =
32 32 appnope; sys_platform == "darwin"
33 backcall
34 33 colorama; sys_platform == "win32"
35 34 decorator
36 35 exceptiongroup; python_version<'3.11'
37 36 jedi>=0.16
38 37 matplotlib-inline
39 38 pexpect>4.3; sys_platform != "win32"
40 39 pickleshare
41 40 prompt_toolkit>=3.0.30,<3.1.0,!=3.0.37
42 41 pygments>=2.4.0
43 42 stack_data
44 43 traitlets>=5
45 44 typing_extensions ; python_version<'3.10'
46 45
47 46 [options.extras_require]
48 47 black =
49 48 black
50 49 doc =
51 50 ipykernel
52 51 setuptools>=18.5
53 52 sphinx>=1.3
54 53 sphinx-rtd-theme
55 54 docrepr
56 55 matplotlib
57 56 stack_data
58 57 pytest<7
59 58 typing_extensions
60 59 exceptiongroup
61 60 %(test)s
62 61 kernel =
63 62 ipykernel
64 63 nbconvert =
65 64 nbconvert
66 65 nbformat =
67 66 nbformat
68 67 notebook =
69 68 ipywidgets
70 69 notebook
71 70 parallel =
72 71 ipyparallel
73 72 qtconsole =
74 73 qtconsole
75 74 terminal =
76 75 test =
77 76 pytest<7.1
78 77 pytest-asyncio
79 78 testpath
80 79 test_extra =
81 80 %(test)s
82 81 curio
83 82 matplotlib!=3.2.0
84 83 nbformat
85 84 numpy>=1.22
86 85 pandas
87 86 trio
88 87 all =
89 88 %(black)s
90 89 %(doc)s
91 90 %(kernel)s
92 91 %(nbconvert)s
93 92 %(nbformat)s
94 93 %(notebook)s
95 94 %(parallel)s
96 95 %(qtconsole)s
97 96 %(terminal)s
98 97 %(test_extra)s
99 98 %(test)s
100 99
101 100 [options.packages.find]
102 101 exclude =
103 102 setupext
104 103
105 104 [options.package_data]
106 105 IPython = py.typed
107 106 IPython.core = profile/README*
108 107 IPython.core.tests = *.png, *.jpg, daft_extension/*.py
109 108 IPython.lib.tests = *.wav
110 109 IPython.testing.plugin = *.txt
111 110
112 111 [velin]
113 112 ignore_patterns =
114 113 IPython/core/tests
115 114 IPython/testing
116 115
117 116 [tool.black]
118 117 exclude = 'timing\.py'
General Comments 0
You need to be logged in to leave comments. Login now