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