From 8fa65442d85ddf8cfd6786a0386dae6eb8b6a86e 2011-11-24 21:55:15 From: Fernando Perez <fperez.net@gmail.com> Date: 2011-11-24 21:55:15 Subject: [PATCH] Merge pull request #956 from Carreau/all-magic-menu-live Generate "All magics..." menu in the Qt console dynamically, so it correctly shows available magics instead of an internally hardcoded list. --- diff --git a/IPython/frontend/qt/console/frontend_widget.py b/IPython/frontend/qt/console/frontend_widget.py index 893c5d8..1f62432 100644 --- a/IPython/frontend/qt/console/frontend_widget.py +++ b/IPython/frontend/qt/console/frontend_widget.py @@ -4,6 +4,7 @@ from __future__ import print_function from collections import namedtuple import sys import time +import uuid # System library imports from pygments.lexers import PythonLexer @@ -137,6 +138,7 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): self._input_splitter = self._input_splitter_class(input_mode='cell') self._kernel_manager = None self._request_info = {} + self._callback_dict = {} # Configure the ConsoleWidget. self.tab_width = 4 @@ -311,6 +313,62 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): cursor.movePosition(QtGui.QTextCursor.Left, n=len(text)) self._complete_with_items(cursor, rep['content']['matches']) + def _silent_exec_callback(self, expr, callback): + """Silently execute `expr` in the kernel and call `callback` with reply + + the `expr` is evaluated silently in the kernel (without) output in + the frontend. Call `callback` with the + `repr <http://docs.python.org/library/functions.html#repr> `_ as first argument + + Parameters + ---------- + expr : string + valid string to be executed by the kernel. + callback : function + function accepting one arguement, as a string. The string will be + the `repr` of the result of evaluating `expr` + + The `callback` is called with the 'repr()' of the result of `expr` as + first argument. To get the object, do 'eval()' onthe passed value. + + See Also + -------- + _handle_exec_callback : private method, deal with calling callback with reply + + """ + + # generate uuid, which would be used as a indication of wether or not + # the unique request originate from here (can use msg id ?) + local_uuid = str(uuid.uuid1()) + msg_id = self.kernel_manager.shell_channel.execute('', + silent=True, user_expressions={ local_uuid:expr }) + self._callback_dict[local_uuid] = callback + self._request_info['execute'] = self._ExecutionRequest(msg_id, 'silent_exec_callback') + + def _handle_exec_callback(self, msg): + """Execute `callback` corresonding to `msg` reply, after ``_silent_exec_callback`` + + Parameters + ---------- + msg : raw message send by the kernel containing an `user_expressions` + and having a 'silent_exec_callback' kind. + + Notes + ----- + This fonction will look for a `callback` associated with the + corresponding message id. Association has been made by + `_silent_exec_callback`. `callback` is then called with the `repr()` + of the value of corresponding `user_expressions` as argument. + `callback` is then removed from the known list so that any message + coming again with the same id won't trigger it. + + """ + + user_exp = msg['content']['user_expressions'] + for expression in user_exp: + if expression in self._callback_dict: + self._callback_dict.pop(expression)(user_exp[expression]) + def _handle_execute_reply(self, msg): """ Handles replies for code execution. """ @@ -342,6 +400,9 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): self._show_interpreter_prompt_for_reply(msg) self.executed.emit(msg) + elif info and info.id == msg['parent_header']['msg_id'] and \ + info.kind == 'silent_exec_callback' and not self._hidden: + self._handle_exec_callback(msg) else: super(FrontendWidget, self)._handle_execute_reply(msg) diff --git a/IPython/frontend/qt/console/mainwindow.py b/IPython/frontend/qt/console/mainwindow.py index d088e20..eaa5fe1 100644 --- a/IPython/frontend/qt/console/mainwindow.py +++ b/IPython/frontend/qt/console/mainwindow.py @@ -20,6 +20,7 @@ Authors: # stdlib imports import sys +import re import webbrowser from threading import Thread @@ -544,10 +545,89 @@ class MainWindow(QtGui.QMainWindow): self.kernel_menu.addSeparator() + def _make_dynamic_magic(self,magic): + """Return a function `fun` that will execute `magic` on active frontend. + + Parameters + ---------- + magic : string + string that will be executed as is when the returned function is called + + Returns + ------- + fun : function + function with no parameters, when called will execute `magic` on the + current active frontend at call time + + See Also + -------- + populate_all_magic_menu : generate the "All Magics..." menu + + Notes + ----- + `fun` execute `magic` an active frontend at the moment it is triggerd, + not the active frontend at the moment it has been created. + + This function is mostly used to create the "All Magics..." Menu at run time. + """ + # need to level nested function to be sure to past magic + # on active frontend **at run time**. + def inner_dynamic_magic(): + self.active_frontend.execute(magic) + inner_dynamic_magic.__name__ = "dynamics_magic_s" + return inner_dynamic_magic + + def populate_all_magic_menu(self, listofmagic=None): + """Clean "All Magics..." menu and repopulate it with `listofmagic` + + Parameters + ---------- + listofmagic : string, + repr() of a list of strings, send back by the kernel + + Notes + ----- + `listofmagic`is a repr() of list because it is fed with the result of + a 'user_expression' + """ + alm_magic_menu = self.all_magic_menu + alm_magic_menu.clear() + + # list of protected magic that don't like to be called without argument + # append '?' to the end to print the docstring when called from the menu + protected_magic = set(["more","less","load_ext","pycat","loadpy","save"]) + magics=re.findall('\w+', listofmagic) + for magic in magics: + if magic in protected_magic: + pmagic = '%s%s%s'%('%',magic,'?') + else: + pmagic = '%s%s'%('%',magic) + xaction = QtGui.QAction(pmagic, + self, + triggered=self._make_dynamic_magic(pmagic) + ) + alm_magic_menu.addAction(xaction) + + def update_all_magic_menu(self): + """ Update the list on magic in the "All Magics..." Menu + + Request the kernel with the list of availlable magic and populate the + menu with the list received back + + """ + # first define a callback which will get the list of all magic and put it in the menu. + self.active_frontend._silent_exec_callback('get_ipython().lsmagic()', self.populate_all_magic_menu) + def init_magic_menu(self): self.magic_menu = self.menuBar().addMenu("&Magic") self.all_magic_menu = self.magic_menu.addMenu("&All Magics") - + + # this action should not appear as it will be cleard when menu + # will be updated at first kernel response. + self.pop = QtGui.QAction("&Update All Magic Menu ", + self, triggered=self.update_all_magic_menu) + self.add_menu_action(self.all_magic_menu, self.pop) + self.reset_action = QtGui.QAction("&Reset", self, statusTip="Clear all varible from workspace", @@ -583,34 +663,7 @@ class MainWindow(QtGui.QMainWindow): statusTip="List interactive variable with detail", triggered=self.whos_magic_active_frontend) self.add_menu_action(self.magic_menu, self.whos_action) - - # allmagics submenu: - - #for now this is just a copy and paste, but we should get this dynamically - magiclist=["%alias", "%autocall", "%automagic", "%bookmark", "%cd", "%clear", - "%colors", "%debug", "%dhist", "%dirs", "%doctest_mode", "%ed", "%edit", "%env", "%gui", - "%guiref", "%hist", "%history", "%install_default_config", "%install_profiles", - "%less", "%load_ext", "%loadpy", "%logoff", "%logon", "%logstart", "%logstate", - "%logstop", "%lsmagic", "%macro", "%magic", "%man", "%more", "%notebook", "%page", - "%pastebin", "%pdb", "%pdef", "%pdoc", "%pfile", "%pinfo", "%pinfo2", "%popd", "%pprint", - "%precision", "%profile", "%prun", "%psearch", "%psource", "%pushd", "%pwd", "%pycat", - "%pylab", "%quickref", "%recall", "%rehashx", "%reload_ext", "%rep", "%rerun", - "%reset", "%reset_selective", "%run", "%save", "%sc", "%sx", "%tb", "%time", "%timeit", - "%unalias", "%unload_ext", "%who", "%who_ls", "%whos", "%xdel", "%xmode"] - - def make_dynamic_magic(i): - def inner_dynamic_magic(): - self.active_frontend.execute(i) - inner_dynamic_magic.__name__ = "dynamics_magic_%s" % i - return inner_dynamic_magic - - for magic in magiclist: - xaction = QtGui.QAction(magic, - self, - triggered=make_dynamic_magic(magic) - ) - self.all_magic_menu.addAction(xaction) - + def init_window_menu(self): self.window_menu = self.menuBar().addMenu("&Window") if sys.platform == 'darwin': diff --git a/IPython/frontend/qt/console/qtconsoleapp.py b/IPython/frontend/qt/console/qtconsoleapp.py index 0a78aae..21594e8 100644 --- a/IPython/frontend/qt/console/qtconsoleapp.py +++ b/IPython/frontend/qt/console/qtconsoleapp.py @@ -451,6 +451,10 @@ class IPythonQtConsoleApp(BaseIPythonApplication): self.window.log = self.log self.window.add_tab_with_frontend(self.widget) self.window.init_menu_bar() + + # we need to populate the 'Magic Menu' once the kernel has answer at least once + self.kernel_manager.shell_channel.first_reply.connect(self.window.pop.trigger) + self.window.setWindowTitle('Python' if self.pure else 'IPython') def init_colors(self):