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):