From b5e9a729acd4c78d21b44ed2ec6fc664cd33ae8f 2010-08-20 16:36:29
From: Brian Granger <ellisonbg@gmail.com>
Date: 2010-08-20 16:36:29
Subject: [PATCH] Started %edit magic.

---

diff --git a/IPython/zmq/zmqshell.py b/IPython/zmq/zmqshell.py
index fb7b88f..c3789b0 100644
--- a/IPython/zmq/zmqshell.py
+++ b/IPython/zmq/zmqshell.py
@@ -66,6 +66,319 @@ class ZMQInteractiveShell(InteractiveShell):
         Term = IPython.utils.io.IOTerm()
         IPython.utils.io.Term = Term
 
+    def magic_edit(self,parameter_s='',last_call=['','']):
+        """Bring up an editor and execute the resulting code.
+
+        Usage:
+          %edit [options] [args]
+
+        %edit runs IPython's editor hook.  The default version of this hook is
+        set to call the __IPYTHON__.rc.editor command.  This is read from your
+        environment variable $EDITOR.  If this isn't found, it will default to
+        vi under Linux/Unix and to notepad under Windows.  See the end of this
+        docstring for how to change the editor hook.
+
+        You can also set the value of this editor via the command line option
+        '-editor' or in your ipythonrc file. This is useful if you wish to use
+        specifically for IPython an editor different from your typical default
+        (and for Windows users who typically don't set environment variables).
+
+        This command allows you to conveniently edit multi-line code right in
+        your IPython session.
+        
+        If called without arguments, %edit opens up an empty editor with a
+        temporary file and will execute the contents of this file when you
+        close it (don't forget to save it!).
+
+
+        Options:
+
+        -n <number>: open the editor at a specified line number.  By default,
+        the IPython editor hook uses the unix syntax 'editor +N filename', but
+        you can configure this by providing your own modified hook if your
+        favorite editor supports line-number specifications with a different
+        syntax.
+        
+        -p: this will call the editor with the same data as the previous time
+        it was used, regardless of how long ago (in your current session) it
+        was.
+
+        -r: use 'raw' input.  This option only applies to input taken from the
+        user's history.  By default, the 'processed' history is used, so that
+        magics are loaded in their transformed version to valid Python.  If
+        this option is given, the raw input as typed as the command line is
+        used instead.  When you exit the editor, it will be executed by
+        IPython's own processor.
+        
+        -x: do not execute the edited code immediately upon exit. This is
+        mainly useful if you are editing programs which need to be called with
+        command line arguments, which you can then do using %run.
+
+
+        Arguments:
+
+        If arguments are given, the following possibilites exist:
+
+        - The arguments are numbers or pairs of colon-separated numbers (like
+        1 4:8 9). These are interpreted as lines of previous input to be
+        loaded into the editor. The syntax is the same of the %macro command.
+
+        - If the argument doesn't start with a number, it is evaluated as a
+        variable and its contents loaded into the editor. You can thus edit
+        any string which contains python code (including the result of
+        previous edits).
+
+        - If the argument is the name of an object (other than a string),
+        IPython will try to locate the file where it was defined and open the
+        editor at the point where it is defined. You can use `%edit function`
+        to load an editor exactly at the point where 'function' is defined,
+        edit it and have the file be executed automatically.
+
+        If the object is a macro (see %macro for details), this opens up your
+        specified editor with a temporary file containing the macro's data.
+        Upon exit, the macro is reloaded with the contents of the file.
+
+        Note: opening at an exact line is only supported under Unix, and some
+        editors (like kedit and gedit up to Gnome 2.8) do not understand the
+        '+NUMBER' parameter necessary for this feature. Good editors like
+        (X)Emacs, vi, jed, pico and joe all do.
+
+        - If the argument is not found as a variable, IPython will look for a
+        file with that name (adding .py if necessary) and load it into the
+        editor. It will execute its contents with execfile() when you exit,
+        loading any code in the file into your interactive namespace.
+
+        After executing your code, %edit will return as output the code you
+        typed in the editor (except when it was an existing file). This way
+        you can reload the code in further invocations of %edit as a variable,
+        via _<NUMBER> or Out[<NUMBER>], where <NUMBER> is the prompt number of
+        the output.
+
+        Note that %edit is also available through the alias %ed.
+
+        This is an example of creating a simple function inside the editor and
+        then modifying it. First, start up the editor:
+
+        In [1]: ed
+        Editing... done. Executing edited code...
+        Out[1]: 'def foo():n    print "foo() was defined in an editing session"n'
+
+        We can then call the function foo():
+        
+        In [2]: foo()
+        foo() was defined in an editing session
+
+        Now we edit foo.  IPython automatically loads the editor with the
+        (temporary) file where foo() was previously defined:
+        
+        In [3]: ed foo
+        Editing... done. Executing edited code...
+
+        And if we call foo() again we get the modified version:
+        
+        In [4]: foo()
+        foo() has now been changed!
+
+        Here is an example of how to edit a code snippet successive
+        times. First we call the editor:
+
+        In [5]: ed
+        Editing... done. Executing edited code...
+        hello
+        Out[5]: "print 'hello'n"
+
+        Now we call it again with the previous output (stored in _):
+
+        In [6]: ed _
+        Editing... done. Executing edited code...
+        hello world
+        Out[6]: "print 'hello world'n"
+
+        Now we call it with the output #8 (stored in _8, also as Out[8]):
+
+        In [7]: ed _8
+        Editing... done. Executing edited code...
+        hello again
+        Out[7]: "print 'hello again'n"
+
+
+        Changing the default editor hook:
+
+        If you wish to write your own editor hook, you can put it in a
+        configuration file which you load at startup time.  The default hook
+        is defined in the IPython.core.hooks module, and you can use that as a
+        starting example for further modifications.  That file also has
+        general instructions on how to set a new hook for use once you've
+        defined it."""
+        
+        # FIXME: This function has become a convoluted mess.  It needs a
+        # ground-up rewrite with clean, simple logic.
+
+        def make_filename(arg):
+            "Make a filename from the given args"
+            try:
+                filename = get_py_filename(arg)
+            except IOError:
+                if args.endswith('.py'):
+                    filename = arg
+                else:
+                    filename = None
+            return filename
+
+        # custom exceptions
+        class DataIsObject(Exception): pass
+
+        opts,args = self.parse_options(parameter_s,'prn:')
+        # Set a few locals from the options for convenience:
+        opts_p = opts.has_key('p')
+        opts_r = opts.has_key('r')
+        
+        # Default line number value
+        lineno = opts.get('n',None)
+
+        if opts_p:
+            args = '_%s' % last_call[0]
+            if not self.shell.user_ns.has_key(args):
+                args = last_call[1]
+            
+        # use last_call to remember the state of the previous call, but don't
+        # let it be clobbered by successive '-p' calls.
+        try:
+            last_call[0] = self.shell.displayhook.prompt_count
+            if not opts_p:
+                last_call[1] = parameter_s
+        except:
+            pass
+
+        # by default this is done with temp files, except when the given
+        # arg is a filename
+        use_temp = 1
+
+        if re.match(r'\d',args):
+            # Mode where user specifies ranges of lines, like in %macro.
+            # This means that you can't edit files whose names begin with
+            # numbers this way. Tough.
+            ranges = args.split()
+            data = ''.join(self.extract_input_slices(ranges,opts_r))
+        elif args.endswith('.py'):
+            filename = make_filename(args)
+            data = ''
+            use_temp = 0
+        elif args:
+            try:
+                # Load the parameter given as a variable. If not a string,
+                # process it as an object instead (below)
+
+                #print '*** args',args,'type',type(args)  # dbg
+                data = eval(args,self.shell.user_ns)
+                if not type(data) in StringTypes:
+                    raise DataIsObject
+
+            except (NameError,SyntaxError):
+                # given argument is not a variable, try as a filename
+                filename = make_filename(args)
+                if filename is None:
+                    warn("Argument given (%s) can't be found as a variable "
+                         "or as a filename." % args)
+                    return
+
+                data = ''
+                use_temp = 0
+            except DataIsObject:
+
+                # macros have a special edit function
+                if isinstance(data,Macro):
+                    self._edit_macro(args,data)
+                    return
+                                
+                # For objects, try to edit the file where they are defined
+                try:
+                    filename = inspect.getabsfile(data)
+                    if 'fakemodule' in filename.lower() and inspect.isclass(data):                     
+                        # class created by %edit? Try to find source
+                        # by looking for method definitions instead, the
+                        # __module__ in those classes is FakeModule.
+                        attrs = [getattr(data, aname) for aname in dir(data)]
+                        for attr in attrs:
+                            if not inspect.ismethod(attr):
+                                continue
+                            filename = inspect.getabsfile(attr)
+                            if filename and 'fakemodule' not in filename.lower():
+                                # change the attribute to be the edit target instead
+                                data = attr 
+                                break
+                    
+                    datafile = 1
+                except TypeError:
+                    filename = make_filename(args)
+                    datafile = 1
+                    warn('Could not find file where `%s` is defined.\n'
+                         'Opening a file named `%s`' % (args,filename))
+                # Now, make sure we can actually read the source (if it was in
+                # a temp file it's gone by now).
+                if datafile:
+                    try:
+                        if lineno is None:
+                            lineno = inspect.getsourcelines(data)[1]
+                    except IOError:
+                        filename = make_filename(args)
+                        if filename is None:
+                            warn('The file `%s` where `%s` was defined cannot '
+                                 'be read.' % (filename,data))
+                            return
+                use_temp = 0
+        else:
+            data = ''
+
+        if use_temp:
+            filename = self.shell.mktempfile(data)
+            print 'IPython will make a temporary file named:',filename
+
+        payload = {
+            'source' : 'IPython.zmq.zmqshell.ZMQInteractiveShell.edit_magic',
+            'filename' : filename,
+            'line_number' : lineno
+        }
+        self.payload_manager.write_payload(payload)
+
+        # # do actual editing here
+        # print 'Editing...',
+        # sys.stdout.flush()
+        # try:
+        #     # Quote filenames that may have spaces in them
+        #     if ' ' in filename:
+        #         filename = "%s" % filename
+        #     self.shell.hooks.editor(filename,lineno)
+        # except TryNext:
+        #     warn('Could not open editor')
+        #     return
+        # 
+        # # XXX TODO: should this be generalized for all string vars?
+        # # For now, this is special-cased to blocks created by cpaste
+        # if args.strip() == 'pasted_block':
+        #     self.shell.user_ns['pasted_block'] = file_read(filename)
+        # 
+        # if opts.has_key('x'):  # -x prevents actual execution
+        #     print
+        # else:
+        #     print 'done. Executing edited code...'
+        #     if opts_r:
+        #         self.shell.runlines(file_read(filename))
+        #     else:
+        #         self.shell.safe_execfile(filename,self.shell.user_ns,
+        #                                  self.shell.user_ns)
+        # 
+        #                                              
+        # if use_temp:
+        #     try:
+        #         return open(filename).read()
+        #     except IOError,msg:
+        #         if msg.filename == filename:
+        #             warn('File not found. Did you forget to save?')
+        #             return
+        #         else:
+        #             self.shell.showtraceback()
+
 InteractiveShellABC.register(ZMQInteractiveShell)