New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New events system #5188
New events system #5188
Conversation
Yeah! I haven't looked in detail, but I strongly think this is a great idea. Thanks so much for working on this. One comment about the naming of things. I agree that the callables being registered should be called "callbacks", but the actual abstraction is one of "events". This is almost the same exact design that we have/use in JavaScript and I think the naming of things should follow: events, trigger, on, rather than callbacks, fire and register. We also use the |
Shameless self advertising 😉 |
The best kind of self advertising ;-) I'll look into giving things event-style names after lunch. |
OK, it took a bit longer, but I've used 'events' and 'trigger' in the names. I've left 'register' instead of 'on', which I don't like. 'on' seems to brief and generic to be useful e.g. when searching, and doesn't have a neat counterpart like 'unregister': jquery's use of 'off' is cute but silly. |
As discussed in the dev meeting, I've added notes in the docs that this API is experimental for IPython 2. |
From discussion with @minrk and @ivanov , the event names are now:
(In the plain terminal case, both also fire when running user entered code) |
Needs rebase. |
"""Manage a collection of events and a sequence of callbacks for each. | ||
|
||
This is attached to :class:`~IPython.core.interactiveshell.InteractiveShell` | ||
instances as a ``callbacks`` attribute. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as an events
attribute
self.callbacks = {n:[] for n in available_events} | ||
|
||
def register(self, event, function): | ||
"""Register a new callback |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a new event callback
# event_name -> prototype mapping | ||
available_events = {} | ||
|
||
def _collect(callback_proto): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the _collect
name here is a bit vague. Maybe something like _define_event
?
Rebased and changed those things. |
# No-op functions which describe the names of available events and the | ||
# signatures of callbacks for those events. | ||
# ------------------------------------------------------------------------------ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To the reader who is not very familiar with how the internals of IPython work, I think the names of these events are still very confusing (pre_execute
, post_execute
, pre_run_cell
, post_run_cell
) and the descriptions don't really help. It is also not clear the order in which these are triggered. Even if we don't rename them we should add better comments to clarify when they are triggered.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should also clarify how silent affects these things.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We had a long discussion about this over lunch on Friday, and those were the clearest set of names we could come up with. Any alternative suggestions will be duly considered.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
possible clarifications:
- execute events fire before/after any code might be executed (interactive code, silent execution, widget interaction, comm messages).
- run_cell events fire only before/after explicit "regular" execution, e.g. shift-enter or interactive typing at the terminal.
For those interested in the internal implementation, this means that silent=False
execution is the only circumstance to trigger run_cell events. All potential execution triggers the execute events, so when run_cell fires, execute will also fire.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find the wording a bit tricky - after all, the internal kernel code is running all the time, so it's not before/after 'any' code runs. I want to call it 'user defined' code, but the frontend can send code for execution without the user writing it, as is the case in most silent execution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am fine with the names, but let's add a comment about how silent affects which are called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a mention.
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_run_cell', func) instead.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
callbacks -> events like above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
One non-inline question: do we need to update the pylab inline stuff to use this event system for rendering figures? |
See |
Good point, that was actually what prompted these changes in the first place. I've switched it over. |
Oops, missed a bit. Give me a moment to amend that commit. Github views don't auto update quickly enough. |
OK, there we go. Less hackish now, although it's actually more lines of code in order to catch the exception. We should possibly consider having a pop-style API for unregister (i.e. ignore if not found). Or just wait for PEP 463 ;-) |
try: | ||
func(*args, **kwargs) | ||
except Exception: | ||
print("Error in callback {} (for {}):".format(func, event)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
print to stderr, or no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
showtraceback()
goes to stdout, looking at the implementation. Possibly it shouldn't, but this should go to the same stream, and changing showtraceback()
is more invasive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
makes sense.
I can confirm this fixes #5122 yeah! |
@@ -827,12 +836,21 @@ def set_hook(self,name,hook, priority = 50, str_key = None, re_key = None): | |||
|
|||
setattr(self.hooks,name, dp) | |||
|
|||
#------------------------------------------------------------------------- | |||
# Things related to callbacks |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/callbacks/events/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
_remove_startswith(requires, pkg) | ||
requires.append("gnureadline; sys.platform == 'darwin' and platform.python_implementation == 'CPython'") | ||
requires.append("pyreadline (>=2.0); sys.platform == 'win32' and platform.python_implementation == 'CPython'") | ||
requires.append("mock; extra == 'test'; python_version < '3.3'") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's test whether ;
or and
should separate extra and platform conditions. Hopefully these are not both correct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tested, and ;
fails with SyntaxError. This should be and
.
Excellent, merging. |
New events system
As discussed in #5122, there's a need for a richer set of callbacks. This is the initial architecture and a few callbacks.
ip.callbacks
is an instance ofCallbackManager
(I know @ivanov will love this name)IPython.core.callbacks
.ip.callbacks.fire('event_name', *args, **kwargs)
. Again, there's not yet any checking that the args passed match the specified signature, but there easily could be in the future.I've deprecated a couple of hooks in favour of callbacks. Our hooks system is designed for end users to customise things that shouldn't be repeated if they succeed - e.g. 'load this in an editor' or 'get text from the clipboard'. In contrast, callbacks are for extension code listening for particular events, so it should be possible to register several callbacks for the same event without them interfering with one another.