##// END OF EJS Templates
ipy_leo: implement %lee hist (open input + output history)
Ville M. Vainio -
Show More
@@ -1,508 +1,541 b''
1 1 """ ILeo - Leo plugin for IPython
2 2
3 3
4 4 """
5 5 import IPython.ipapi
6 6 import IPython.genutils
7 7 import IPython.generics
8 8 from IPython.hooks import CommandChainDispatcher
9 9 import re
10 10 import UserDict
11 11 from IPython.ipapi import TryNext
12 12 import IPython.macro
13 13
14 14 def init_ipython(ipy):
15 15 """ This will be run by _ip.load('ipy_leo')
16 16
17 17 Leo still needs to run update_commander() after this.
18 18
19 19 """
20 20 global ip
21 21 ip = ipy
22 22 ip.set_hook('complete_command', mb_completer, str_key = 'mb')
23 23 ip.expose_magic('mb',mb_f)
24 24 ip.expose_magic('lee',lee_f)
25 25 ip.expose_magic('leoref',leoref_f)
26 26 expose_ileo_push(push_cl_node,100)
27 27 # this should be the LAST one that will be executed, and it will never raise TryNext
28 28 expose_ileo_push(push_ipython_script, 1000)
29 29 expose_ileo_push(push_plain_python, 100)
30 30 expose_ileo_push(push_ev_node, 100)
31 ip.user_ns['wb'] = LeoWorkbook()
31 global wb
32 wb = LeoWorkbook()
33 ip.user_ns['wb'] = wb
32 34
33 35 show_welcome()
34 36
35 37
36 38 def update_commander(new_leox):
37 39 """ Set the Leo commander to use
38 40
39 41 This will be run every time Leo does ipython-launch; basically,
40 42 when the user switches the document he is focusing on, he should do
41 43 ipython-launch to tell ILeo what document the commands apply to.
42 44
43 45 """
44 46
45 47 global c,g
46 48 c,g = new_leox.c, new_leox.g
47 49 print "Set Leo Commander:",c.frame.getTitle()
48 50
49 51 # will probably be overwritten by user, but handy for experimentation early on
50 52 ip.user_ns['c'] = c
51 53 ip.user_ns['g'] = g
52 54 ip.user_ns['_leo'] = new_leox
53 55
54 56 new_leox.push = push_position_from_leo
55 57 run_leo_startup_node()
56 58
57 59 from IPython.external.simplegeneric import generic
58 60 import pprint
59 61
60 62 def es(s):
61 63 g.es(s, tabName = 'IPython')
62 64 pass
63 65
64 66 @generic
65 67 def format_for_leo(obj):
66 68 """ Convert obj to string representiation (for editing in Leo)"""
67 69 return pprint.pformat(obj)
68 70
69 71 @format_for_leo.when_type(list)
70 72 def format_list(obj):
71 73 return "\n".join(str(s) for s in obj)
72 74
73 75
74 76 attribute_re = re.compile('^[a-zA-Z_][a-zA-Z0-9_]*$')
75 77 def valid_attribute(s):
76 78 return attribute_re.match(s)
77 79
78 80 def all_cells():
79 81 d = {}
80 82 for p in c.allNodes_iter():
81 83 h = p.headString()
82 84 if h.startswith('@a '):
83 85 d[h.lstrip('@a ').strip()] = p.parent().copy()
84 86 elif not valid_attribute(h):
85 87 continue
86 88 d[h] = p.copy()
87 89 return d
88 90
89 91 def eval_node(n):
90 92 body = n.b
91 93 if not body.startswith('@cl'):
92 94 # plain python repr node, just eval it
93 95 return ip.ev(n.b)
94 96 # @cl nodes deserve special treatment - first eval the first line (minus cl), then use it to call the rest of body
95 97 first, rest = body.split('\n',1)
96 98 tup = first.split(None, 1)
97 99 # @cl alone SPECIAL USE-> dump var to user_ns
98 100 if len(tup) == 1:
99 101 val = ip.ev(rest)
100 102 ip.user_ns[n.h] = val
101 103 es("%s = %s" % (n.h, repr(val)[:20] ))
102 104 return val
103 105
104 106 cl, hd = tup
105 107
106 108 xformer = ip.ev(hd.strip())
107 109 es('Transform w/ %s' % repr(xformer))
108 110 return xformer(rest, n)
109 111
110 112 class LeoNode(object, UserDict.DictMixin):
111 113 """ Node in Leo outline
112 114
113 115 Most important attributes (getters/setters available:
114 116 .v - evaluate node, can also be alligned
115 117 .b, .h - body string, headline string
116 118 .l - value as string list
117 119
118 120 Also supports iteration,
119 121
120 122 setitem / getitem (indexing):
121 123 wb.foo['key'] = 12
122 124 assert wb.foo['key'].v == 12
123 125
124 126 Note the asymmetry on setitem and getitem! Also other
125 127 dict methods are available.
126 128
127 129 .ipush() - run push-to-ipython
128 130
129 131 Minibuffer command access (tab completion works):
130 132
131 133 mb save-to-file
132 134
133 135 """
134 136 def __init__(self,p):
135 137 self.p = p.copy()
136 138
137 139 def __str__(self):
138 140 return "<LeoNode %s>" % str(self.p)
139 141
140 142 __repr__ = __str__
141 143
142 144 def __get_h(self): return self.p.headString()
143 145 def __set_h(self,val):
144 146 print "set head",val
145 147 c.beginUpdate()
146 148 try:
147 149 c.setHeadString(self.p,val)
148 150 finally:
149 151 c.endUpdate()
150 152
151 153 h = property( __get_h, __set_h, doc = "Node headline string")
152 154
153 155 def __get_b(self): return self.p.bodyString()
154 156 def __set_b(self,val):
155 157 print "set body",val
156 158 c.beginUpdate()
157 159 try:
158 160 c.setBodyString(self.p, val)
159 161 finally:
160 162 c.endUpdate()
161 163
162 164 b = property(__get_b, __set_b, doc = "Nody body string")
163 165
164 166 def __set_val(self, val):
165 167 self.b = format_for_leo(val)
166 168
167 169 v = property(lambda self: eval_node(self), __set_val, doc = "Node evaluated value")
168 170
169 171 def __set_l(self,val):
170 172 self.b = '\n'.join(val )
171 173 l = property(lambda self : IPython.genutils.SList(self.b.splitlines()),
172 174 __set_l, doc = "Node value as string list")
173 175
174 176 def __iter__(self):
175 177 """ Iterate through nodes direct children """
176 178
177 179 return (LeoNode(p) for p in self.p.children_iter())
178 180
179 181 def __children(self):
180 182 d = {}
181 183 for child in self:
182 184 head = child.h
183 185 tup = head.split(None,1)
184 186 if len(tup) > 1 and tup[0] == '@k':
185 187 d[tup[1]] = child
186 188 continue
187 189
188 190 if not valid_attribute(head):
189 191 d[head] = child
190 192 continue
191 193 return d
192 194 def keys(self):
193 195 d = self.__children()
194 196 return d.keys()
195 197 def __getitem__(self, key):
196 198 """ wb.foo['Some stuff'] Return a child node with headline 'Some stuff'
197 199
198 200 If key is a valid python name (e.g. 'foo'), look for headline '@k foo' as well
199 201 """
200 202 key = str(key)
201 203 d = self.__children()
202 204 return d[key]
203 205 def __setitem__(self, key, val):
204 206 """ You can do wb.foo['My Stuff'] = 12 to create children
205 207
206 208 This will create 'My Stuff' as a child of foo (if it does not exist), and
207 209 do .v = 12 assignment.
208 210
209 211 Exception:
210 212
211 213 wb.foo['bar'] = 12
212 214
213 215 will create a child with headline '@k bar', because bar is a valid python name
214 216 and we don't want to crowd the WorkBook namespace with (possibly numerous) entries
215 217 """
216 218 key = str(key)
217 219 d = self.__children()
218 220 if key in d:
219 221 d[key].v = val
220 222 return
221 223
222 224 if not valid_attribute(key):
223 225 head = key
224 226 else:
225 227 head = '@k ' + key
226 228 p = c.createLastChildNode(self.p, head, '')
227 229 LeoNode(p).v = val
228 230
229 231 def ipush(self):
230 232 """ Does push-to-ipython on the node """
231 233 push_from_leo(self)
232 234
233 235 def go(self):
234 236 """ Set node as current node (to quickly see it in Outline) """
235 237 c.beginUpdate()
236 238 try:
237 239 c.setCurrentPosition(self.p)
238 240 finally:
239 241 c.endUpdate()
240 242
241 243 def script(self):
242 244 """ Method to get the 'tangled' contents of the node
243 245
244 246 (parse @others, << section >> references etc.)
245 247 """
246 248 return g.getScript(c,self.p,useSelectedText=False,useSentinels=False)
247 249
248 250 def __get_uA(self):
249 251 p = self.p
250 252 # Create the uA if necessary.
251 253 if not hasattr(p.v.t,'unknownAttributes'):
252 254 p.v.t.unknownAttributes = {}
253 255
254 256 d = p.v.t.unknownAttributes.setdefault('ipython', {})
255 257 return d
256 258
257 259 uA = property(__get_uA, doc = "Access persistent unknownAttributes of node")
258 260
259 261
260 262 class LeoWorkbook:
261 263 """ class for 'advanced' node access
262 264
263 265 Has attributes for all "discoverable" nodes. Node is discoverable if it
264 266 either
265 267
266 268 - has a valid python name (Foo, bar_12)
267 269 - is a parent of an anchor node (if it has a child '@a foo', it is visible as foo)
268 270
269 271 """
270 272 def __getattr__(self, key):
271 273 if key.startswith('_') or key == 'trait_names' or not valid_attribute(key):
272 274 raise AttributeError
273 275 cells = all_cells()
274 276 p = cells.get(key, None)
275 277 if p is None:
276 278 return add_var(key)
277 279
278 280 return LeoNode(p)
279 281
280 282 def __str__(self):
281 283 return "<LeoWorkbook>"
282 284 def __setattr__(self,key, val):
283 285 raise AttributeError("Direct assignment to workbook denied, try wb.%s.v = %s" % (key,val))
284 286
285 287 __repr__ = __str__
286 288
287 289 def __iter__(self):
288 290 """ Iterate all (even non-exposed) nodes """
289 291 cells = all_cells()
290 292 return (LeoNode(p) for p in c.allNodes_iter())
291 293
292 294 current = property(lambda self: LeoNode(c.currentPosition()), doc = "Currently selected node")
293 295
294 296 def match_h(self, regex):
295 297 cmp = re.compile(regex)
296 298 for node in self:
297 299 if re.match(cmp, node.h, re.IGNORECASE):
298 300 yield node
299 301 return
300 302
301 303 @IPython.generics.complete_object.when_type(LeoWorkbook)
302 304 def workbook_complete(obj, prev):
303 305 return all_cells().keys() + [s for s in prev if not s.startswith('_')]
304 306
305 307
306 308 def add_var(varname):
307 309 c.beginUpdate()
308 310 try:
309 311 p2 = g.findNodeAnywhere(c,varname)
310 312 if p2:
311 313 return LeoNode(p2)
312 314
313 315 rootpos = g.findNodeAnywhere(c,'@ipy-results')
314 if not rootpos:
315 rootpos = c.currentPosition()
316 p2 = rootpos.insertAsLastChild()
316 if rootpos:
317 p2 = rootpos.insertAsLastChild()
318
319 else:
320 p2 = c.currentPosition().insertAfter()
321
317 322 c.setHeadString(p2,varname)
318 323 return LeoNode(p2)
319 324 finally:
320 325 c.endUpdate()
321 326
322 327 def add_file(self,fname):
323 328 p2 = c.currentPosition().insertAfter()
324 329
325 330 push_from_leo = CommandChainDispatcher()
326 331
327 332 def expose_ileo_push(f, prio = 0):
328 333 push_from_leo.add(f, prio)
329 334
330 335 def push_ipython_script(node):
331 336 """ Execute the node body in IPython, as if it was entered in interactive prompt """
332 337 c.beginUpdate()
333 338 try:
334 339 ohist = ip.IP.output_hist
335 340 hstart = len(ip.IP.input_hist)
336 341 script = node.script()
337 342
338 343 script = g.splitLines(script + '\n')
339 344 ip.user_ns['_p'] = node
340 345 ip.runlines(script)
341 346 del ip.user_ns['_p']
342 347
343 348 has_output = False
344 349 for idx in range(hstart,len(ip.IP.input_hist)):
345 350 val = ohist.get(idx,None)
346 351 if val is None:
347 352 continue
348 353 has_output = True
349 354 inp = ip.IP.input_hist[idx]
350 355 if inp.strip():
351 356 es('In: %s' % (inp[:40], ))
352 357
353 358 es('<%d> %s' % (idx, pprint.pformat(ohist[idx],width = 40)))
354 359
355 360 if not has_output:
356 361 es('ipy run: %s (%d LL)' %( node.h,len(script)))
357 362 finally:
358 363 c.endUpdate()
359 364
360 365
361 366 def eval_body(body):
362 367 try:
363 368 val = ip.ev(body)
364 369 except:
365 370 # just use stringlist if it's not completely legal python expression
366 371 val = IPython.genutils.SList(body.splitlines())
367 372 return val
368 373
369 374 def push_plain_python(node):
370 375 if not node.h.endswith('P'):
371 376 raise TryNext
372 377 script = node.script()
373 378 lines = script.count('\n')
374 379 try:
375 380 exec script in ip.user_ns
376 381 except:
377 382 print " -- Exception in script:\n"+script + "\n --"
378 383 raise
379 384 es('ipy plain: %s (%d LL)' % (node.h,lines))
380 385
381 386
382 387 def push_cl_node(node):
383 388 """ If node starts with @cl, eval it
384 389
385 390 The result is put to root @ipy-results node
386 391 """
387 392 if not node.b.startswith('@cl'):
388 393 raise TryNext
389 394
390 395 p2 = g.findNodeAnywhere(c,'@ipy-results')
391 396 val = node.v
392 397 if p2:
393 398 es("=> @ipy-results")
394 399 LeoNode(p2).v = val
395 400 es(val)
396 401
397 402 def push_ev_node(node):
398 403 """ If headline starts with @ev, eval it and put result in body """
399 404 if not node.h.startswith('@ev '):
400 405 raise TryNext
401 406 expr = node.h.lstrip('@ev ')
402 407 es('ipy eval ' + expr)
403 408 res = ip.ev(expr)
404 409 node.v = res
405 410
406 411
407 412 def push_position_from_leo(p):
408 413 push_from_leo(LeoNode(p))
409 414
410 415 @generic
411 416 def edit_object_in_leo(obj, varname):
412 417 """ Make it @cl node so it can be pushed back directly by alt+I """
413 418 node = add_var(varname)
414 node.b = '@cl\n' + format_for_leo(obj)
419 formatted = format_for_leo(obj)
420 if not formatted.startswith('@cl'):
421 formatted = '@cl\n' + formatted
422 node.b = formatted
415 423 node.go()
416 424
417 425 @edit_object_in_leo.when_type(IPython.macro.Macro)
418 426 def edit_macro(obj,varname):
419 427 bod = '_ip.defmacro("""\\\n' + obj.value + '""")'
420 428 node = add_var('Macro_' + varname)
421 429 node.b = bod
422 430 node.go()
431
432 def get_history(hstart = 0):
433 res = []
434 ohist = ip.IP.output_hist
435
436 for idx in range(hstart, len(ip.IP.input_hist)):
437 val = ohist.get(idx,None)
438 has_output = True
439 inp = ip.IP.input_hist_raw[idx]
440 if inp.strip():
441 res.append('In [%d]: %s' % (idx, inp))
442 if val:
443 res.append(pprint.pformat(val))
444 res.append('\n')
445 return ''.join(res)
446
423 447
424 448 def lee_f(self,s):
425 449 """ Open file(s)/objects in Leo
426 450
427 Takes an mglob pattern, e.g. '%lee *.cpp' or %leo 'rec:*.cpp'
451 - %lee hist -> open full session history in leo
452 - Takes an object
453 - Takes an mglob pattern, e.g. '%lee *.cpp' or %leo 'rec:*.cpp'
428 454 """
429 455 import os
430 456
431 457 c.beginUpdate()
432 458 try:
433
459 if s == 'hist':
460 wb.ipython_history.b = get_history()
461 wb.ipython_history.go()
462 return
463
464
465
434 466 # try editing the object directly
435 467 obj = ip.user_ns.get(s, None)
436 468 if obj is not None:
437 469 edit_object_in_leo(obj,s)
438 470 return
439 471
440 472 # if it's not object, it's a file name / mglob pattern
441 473 from IPython.external import mglob
442 474
443 files = mglob.expand(s)
475 files = (os.path.abspath(f) for f in mglob.expand(s))
444 476 for fname in files:
445 477 p = g.findNodeAnywhere(c,'@auto ' + fname)
446 478 if not p:
447 479 p = c.currentPosition().insertAfter()
448 480
449 481 p.setHeadString('@auto ' + fname)
450 482 if os.path.isfile(fname):
451 483 c.setBodyString(p,open(fname).read())
452 484 c.selectPosition(p)
485 print "Editing file(s), press ctrl+shift+w in Leo to write @auto nodes"
453 486 finally:
454 487 c.endUpdate()
455 488
456 489
457 490
458 491 def leoref_f(self,s):
459 492 """ Quick reference for ILeo """
460 493 import textwrap
461 494 print textwrap.dedent("""\
462 495 %leoe file/object - open file / object in leo
463 496 wb.foo.v - eval node foo (i.e. headstring is 'foo' or '@ipy foo')
464 497 wb.foo.v = 12 - assign to body of node foo
465 498 wb.foo.b - read or write the body of node foo
466 499 wb.foo.l - body of node foo as string list
467 500
468 501 for el in wb.foo:
469 502 print el.v
470 503
471 504 """
472 505 )
473 506
474 507
475 508
476 509 def mb_f(self, arg):
477 510 """ Execute leo minibuffer commands
478 511
479 512 Example:
480 513 mb save-to-file
481 514 """
482 515 c.executeMinibufferCommand(arg)
483 516
484 517 def mb_completer(self,event):
485 518 """ Custom completer for minibuffer """
486 519 cmd_param = event.line.split()
487 520 if event.line.endswith(' '):
488 521 cmd_param.append('')
489 522 if len(cmd_param) > 2:
490 523 return ip.IP.Completer.file_matches(event.symbol)
491 524 cmds = c.commandsDict.keys()
492 525 cmds.sort()
493 526 return cmds
494 527
495 528 def show_welcome():
496 529 print "------------------"
497 530 print "Welcome to Leo-enabled IPython session!"
498 531 print "Try %leoref for quick reference."
499 532 import IPython.platutils
500 533 IPython.platutils.set_term_title('ILeo')
501 534 IPython.platutils.freeze_term_title()
502 535
503 536 def run_leo_startup_node():
504 537 p = g.findNodeAnywhere(c,'@ipy-startup')
505 538 if p:
506 539 print "Running @ipy-startup nodes"
507 540 for n in LeoNode(p):
508 541 push_from_leo(n)
General Comments 0
You need to be logged in to leave comments. Login now