""" ILeo - Leo plugin for IPython """ import IPython.ipapi import IPython.genutils import IPython.generics from IPython.hooks import CommandChainDispatcher import re import UserDict from IPython.ipapi import TryNext import IPython.macro def init_ipython(ipy): """ This will be run by _ip.load('ipy_leo') Leo still needs to run update_commander() after this. """ global ip ip = ipy ip.set_hook('complete_command', mb_completer, str_key = '%mb') ip.expose_magic('mb',mb_f) ip.expose_magic('lee',lee_f) ip.expose_magic('leoref',leoref_f) expose_ileo_push(push_cl_node,100) # this should be the LAST one that will be executed, and it will never raise TryNext expose_ileo_push(push_ipython_script, 1000) expose_ileo_push(push_plain_python, 100) expose_ileo_push(push_ev_node, 100) global wb wb = LeoWorkbook() ip.user_ns['wb'] = wb show_welcome() def update_commander(new_leox): """ Set the Leo commander to use This will be run every time Leo does ipython-launch; basically, when the user switches the document he is focusing on, he should do ipython-launch to tell ILeo what document the commands apply to. """ global c,g c,g = new_leox.c, new_leox.g print "Set Leo Commander:",c.frame.getTitle() # will probably be overwritten by user, but handy for experimentation early on ip.user_ns['c'] = c ip.user_ns['g'] = g ip.user_ns['_leo'] = new_leox new_leox.push = push_position_from_leo run_leo_startup_node() from IPython.external.simplegeneric import generic import pprint def es(s): g.es(s, tabName = 'IPython') pass @generic def format_for_leo(obj): """ Convert obj to string representiation (for editing in Leo)""" return pprint.pformat(obj) @format_for_leo.when_type(list) def format_list(obj): return "\n".join(str(s) for s in obj) attribute_re = re.compile('^[a-zA-Z_][a-zA-Z0-9_]*$') def valid_attribute(s): return attribute_re.match(s) _rootnode = None def rootnode(): """ Get ileo root node (@ipy-root) if node has become invalid or has not been set, return None Note that the root is the *first* @ipy-root item found """ global _rootnode if _rootnode is None: return None if c.positionExists(_rootnode.p): return _rootnode _rootnode = None return None def all_cells(): global _rootnode d = {} r = rootnode() if r is not None: nodes = r.p.children_iter() else: nodes = c.allNodes_iter() for p in nodes: h = p.headString() if h.strip() == '@ipy-root': # update root node (found it for the first time) _rootnode = LeoNode(p) # the next recursive call will use the children of new root return all_cells() if h.startswith('@a '): d[h.lstrip('@a ').strip()] = p.parent().copy() elif not valid_attribute(h): continue d[h] = p.copy() return d def eval_node(n): body = n.b if not body.startswith('@cl'): # plain python repr node, just eval it return ip.ev(n.b) # @cl nodes deserve special treatment - first eval the first line (minus cl), then use it to call the rest of body first, rest = body.split('\n',1) tup = first.split(None, 1) # @cl alone SPECIAL USE-> dump var to user_ns if len(tup) == 1: val = ip.ev(rest) ip.user_ns[n.h] = val es("%s = %s" % (n.h, repr(val)[:20] )) return val cl, hd = tup xformer = ip.ev(hd.strip()) es('Transform w/ %s' % repr(xformer)) return xformer(rest, n) class LeoNode(object, UserDict.DictMixin): """ Node in Leo outline Most important attributes (getters/setters available: .v - evaluate node, can also be alligned .b, .h - body string, headline string .l - value as string list Also supports iteration, setitem / getitem (indexing): wb.foo['key'] = 12 assert wb.foo['key'].v == 12 Note the asymmetry on setitem and getitem! Also other dict methods are available. .ipush() - run push-to-ipython Minibuffer command access (tab completion works): mb save-to-file """ def __init__(self,p): self.p = p.copy() def __str__(self): return "" % str(self.p) __repr__ = __str__ def __get_h(self): return self.p.headString() def __set_h(self,val): print "set head",val c.beginUpdate() try: c.setHeadString(self.p,val) finally: c.endUpdate() h = property( __get_h, __set_h, doc = "Node headline string") def __get_b(self): return self.p.bodyString() def __set_b(self,val): print "set body",val c.beginUpdate() try: c.setBodyString(self.p, val) finally: c.endUpdate() b = property(__get_b, __set_b, doc = "Nody body string") def __set_val(self, val): self.b = format_for_leo(val) v = property(lambda self: eval_node(self), __set_val, doc = "Node evaluated value") def __set_l(self,val): self.b = '\n'.join(val ) l = property(lambda self : IPython.genutils.SList(self.b.splitlines()), __set_l, doc = "Node value as string list") def __iter__(self): """ Iterate through nodes direct children """ return (LeoNode(p) for p in self.p.children_iter()) def __children(self): d = {} for child in self: head = child.h tup = head.split(None,1) if len(tup) > 1 and tup[0] == '@k': d[tup[1]] = child continue if not valid_attribute(head): d[head] = child continue return d def keys(self): d = self.__children() return d.keys() def __getitem__(self, key): """ wb.foo['Some stuff'] Return a child node with headline 'Some stuff' If key is a valid python name (e.g. 'foo'), look for headline '@k foo' as well """ key = str(key) d = self.__children() return d[key] def __setitem__(self, key, val): """ You can do wb.foo['My Stuff'] = 12 to create children This will create 'My Stuff' as a child of foo (if it does not exist), and do .v = 12 assignment. Exception: wb.foo['bar'] = 12 will create a child with headline '@k bar', because bar is a valid python name and we don't want to crowd the WorkBook namespace with (possibly numerous) entries """ key = str(key) d = self.__children() if key in d: d[key].v = val return if not valid_attribute(key): head = key else: head = '@k ' + key p = c.createLastChildNode(self.p, head, '') LeoNode(p).v = val def ipush(self): """ Does push-to-ipython on the node """ push_from_leo(self) def go(self): """ Set node as current node (to quickly see it in Outline) """ c.beginUpdate() try: c.setCurrentPosition(self.p) finally: c.endUpdate() def script(self): """ Method to get the 'tangled' contents of the node (parse @others, << section >> references etc.) """ return g.getScript(c,self.p,useSelectedText=False,useSentinels=False) def __get_uA(self): p = self.p # Create the uA if necessary. if not hasattr(p.v.t,'unknownAttributes'): p.v.t.unknownAttributes = {} d = p.v.t.unknownAttributes.setdefault('ipython', {}) return d uA = property(__get_uA, doc = "Access persistent unknownAttributes of node") class LeoWorkbook: """ class for 'advanced' node access Has attributes for all "discoverable" nodes. Node is discoverable if it either - has a valid python name (Foo, bar_12) - is a parent of an anchor node (if it has a child '@a foo', it is visible as foo) """ def __getattr__(self, key): if key.startswith('_') or key == 'trait_names' or not valid_attribute(key): raise AttributeError cells = all_cells() p = cells.get(key, None) if p is None: return add_var(key) return LeoNode(p) def __str__(self): return "" def __setattr__(self,key, val): raise AttributeError("Direct assignment to workbook denied, try wb.%s.v = %s" % (key,val)) __repr__ = __str__ def __iter__(self): """ Iterate all (even non-exposed) nodes """ cells = all_cells() return (LeoNode(p) for p in c.allNodes_iter()) current = property(lambda self: LeoNode(c.currentPosition()), doc = "Currently selected node") def match_h(self, regex): cmp = re.compile(regex) for node in self: if re.match(cmp, node.h, re.IGNORECASE): yield node return @IPython.generics.complete_object.when_type(LeoWorkbook) def workbook_complete(obj, prev): return all_cells().keys() + [s for s in prev if not s.startswith('_')] def add_var(varname): c.beginUpdate() r = rootnode() try: if r is None: p2 = g.findNodeAnywhere(c,varname) else: p2 = g.findNodeInChildren(c, r.p, varname) if p2: return LeoNode(p2) if r is not None: p2 = r.p.insertAsLastChild() else: p2 = c.currentPosition().insertAfter() c.setHeadString(p2,varname) return LeoNode(p2) finally: c.endUpdate() def add_file(self,fname): p2 = c.currentPosition().insertAfter() push_from_leo = CommandChainDispatcher() def expose_ileo_push(f, prio = 0): push_from_leo.add(f, prio) def push_ipython_script(node): """ Execute the node body in IPython, as if it was entered in interactive prompt """ c.beginUpdate() try: ohist = ip.IP.output_hist hstart = len(ip.IP.input_hist) script = node.script() script = g.splitLines(script + '\n') ip.user_ns['_p'] = node ip.runlines(script) ip.user_ns.pop('_p',None) has_output = False for idx in range(hstart,len(ip.IP.input_hist)): val = ohist.get(idx,None) if val is None: continue has_output = True inp = ip.IP.input_hist[idx] if inp.strip(): es('In: %s' % (inp[:40], )) es('<%d> %s' % (idx, pprint.pformat(ohist[idx],width = 40))) if not has_output: es('ipy run: %s (%d LL)' %( node.h,len(script))) finally: c.endUpdate() def eval_body(body): try: val = ip.ev(body) except: # just use stringlist if it's not completely legal python expression val = IPython.genutils.SList(body.splitlines()) return val def push_plain_python(node): if not node.h.endswith('P'): raise TryNext script = node.script() lines = script.count('\n') try: exec script in ip.user_ns except: print " -- Exception in script:\n"+script + "\n --" raise es('ipy plain: %s (%d LL)' % (node.h,lines)) def push_cl_node(node): """ If node starts with @cl, eval it The result is put as last child of @ipy-results node, if it exists """ if not node.b.startswith('@cl'): raise TryNext p2 = g.findNodeAnywhere(c,'@ipy-results') val = node.v if p2: es("=> @ipy-results") LeoNode(p2).v = val es(val) def push_ev_node(node): """ If headline starts with @ev, eval it and put result in body """ if not node.h.startswith('@ev '): raise TryNext expr = node.h.lstrip('@ev ') es('ipy eval ' + expr) res = ip.ev(expr) node.v = res def push_position_from_leo(p): push_from_leo(LeoNode(p)) @generic def edit_object_in_leo(obj, varname): """ Make it @cl node so it can be pushed back directly by alt+I """ node = add_var(varname) formatted = format_for_leo(obj) if not formatted.startswith('@cl'): formatted = '@cl\n' + formatted node.b = formatted node.go() @edit_object_in_leo.when_type(IPython.macro.Macro) def edit_macro(obj,varname): bod = '_ip.defmacro("""\\\n' + obj.value + '""")' node = add_var('Macro_' + varname) node.b = bod node.go() def get_history(hstart = 0): res = [] ohist = ip.IP.output_hist for idx in range(hstart, len(ip.IP.input_hist)): val = ohist.get(idx,None) has_output = True inp = ip.IP.input_hist_raw[idx] if inp.strip(): res.append('In [%d]: %s' % (idx, inp)) if val: res.append(pprint.pformat(val)) res.append('\n') return ''.join(res) def lee_f(self,s): """ Open file(s)/objects in Leo - %lee hist -> open full session history in leo - Takes an object - Takes an mglob pattern, e.g. '%lee *.cpp' or %leo 'rec:*.cpp' """ import os c.beginUpdate() try: if s == 'hist': wb.ipython_history.b = get_history() wb.ipython_history.go() return # try editing the object directly obj = ip.user_ns.get(s, None) if obj is not None: edit_object_in_leo(obj,s) return # if it's not object, it's a file name / mglob pattern from IPython.external import mglob files = (os.path.abspath(f) for f in mglob.expand(s)) for fname in files: p = g.findNodeAnywhere(c,'@auto ' + fname) if not p: p = c.currentPosition().insertAfter() p.setHeadString('@auto ' + fname) if os.path.isfile(fname): c.setBodyString(p,open(fname).read()) c.selectPosition(p) print "Editing file(s), press ctrl+shift+w in Leo to write @auto nodes" finally: c.endUpdate() def leoref_f(self,s): """ Quick reference for ILeo """ import textwrap print textwrap.dedent("""\ %leoe file/object - open file / object in leo wb.foo.v - eval node foo (i.e. headstring is 'foo' or '@ipy foo') wb.foo.v = 12 - assign to body of node foo wb.foo.b - read or write the body of node foo wb.foo.l - body of node foo as string list for el in wb.foo: print el.v """ ) def mb_f(self, arg): """ Execute leo minibuffer commands Example: mb save-to-file """ c.executeMinibufferCommand(arg) def mb_completer(self,event): """ Custom completer for minibuffer """ cmd_param = event.line.split() if event.line.endswith(' '): cmd_param.append('') if len(cmd_param) > 2: return ip.IP.Completer.file_matches(event.symbol) cmds = c.commandsDict.keys() cmds.sort() return cmds def show_welcome(): print "------------------" print "Welcome to Leo-enabled IPython session!" print "Try %leoref for quick reference." import IPython.platutils IPython.platutils.set_term_title('ILeo') IPython.platutils.freeze_term_title() def run_leo_startup_node(): p = g.findNodeAnywhere(c,'@ipy-startup') if p: print "Running @ipy-startup nodes" for n in LeoNode(p): push_from_leo(n)