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