Source code for leo.plugins.valuespace

#@+leo-ver=5-thin
#@+node:ville.20110403115003.10348: * @file valuespace.py
#@+<< docstring >>
#@+node:ville.20110403115003.10349: ** << docstring >>
'''Supports Leo scripting using per-Leo-outline namespaces.

Commands
========

This plugin supports the following commands:


vs-create-tree
--------------

Creates a tree whose root node is named 'valuespace' containing one child node
for every entry in the namespace. The headline of each child is *@@r <key>*,
where *<key>* is one of the keys of the namespace. The body text of the child
node is the value for *<key>*.

vs-dump
-------

Prints key/value pairs of the namespace.

vs-reset
--------

Clears the namespace.

vs-update
---------

Scans the entire Leo outline twice, processing *@=*, *@a* and *@r* nodes.

Pass 1
++++++

Pass 1 evaluates all *@=* and *@a* nodes in the outline as follows:

*@=* (assignment) nodes should have headlines of the the form::

    @= <var>

Pass 1 evaluates the body text and assigns the result to *<var>*.

*@a* (anchor) nodes should have headlines of one of two forms::

    @a
    @a <var>

The first form evaluates the script in the **parent** node of the *@a* node.
Such **bare** @a nodes serve as markers that the parent contains code to be
executed.

The second form evaluates the body of the **parent** of the *@a* node and
assigns the result to *<var>*.

**Important**: Both forms of *@a* nodes support the following **@x convention**
when evaluating the parent's body text. Before evaluating the body text, pass1
scans the body text looking for *@x* lines. Such lines have two forms:

1. *@x <python statement>*

Pass 1 executes *<python statement>*.

2. The second form spans multiple lines of the body text::

    @x {
    python statements
    @x }

Pass 1 executes all the python statements between the *@x {* and the *@x }*

3. Assign block of text to variable::

    @x =<var> {
    Some
    Text
    @x }

Pass 1 assigns the block of text to <var>. The type of value is SList,
a special subclass of standard 'list' that makes operating with string
lists convenient. Notably, you can do <var>.n to get the content as plain
string.

A special case of this is the "list append" notation::

    @x =<var>+ {
    Some
    Text
    @x }

This assumes that <var> is a list, and appends the content as SList to this
list. You will typically do '@x var = []' earlier in the document to make this
construct work.

<var> in all constructs above can be arbitrary expression that can be on left hand
side of assignment. E.g. you can use foo.bar, foo['bar'], foo().bar etc.

Pass 2
++++++

Pass 2 "renders" all *@r* nodes in the outline into body text. *@r* nodes should
have the form::

    @r <expression>

Pass 2 evaluates *<expression>* and places the result in the body pane.

**TODO**: discuss SList expressions.

Evaluating expressions
======================

All expression are evaluated in a context that predefines Leo's *c*, *g* and *p*
vars. In addition, *g.vs* is a dictionary whose keys are *c.hash()* and whose
values are the namespaces for each commander. This allows communication between
different namespaces, while keeping namespaces generally separate.

'''

# SList docs: http://ipython.scipy.org/moin/Cookbook/StringListProcessing
#@-<< docstring >>
# By Ville M. Vainio and Terry N. Brown.

#@+<< imports >>
#@+node:ville.20110403115003.10351: ** << imports >>
import leo.core.leoGlobals as g
import leo.core.leoPlugins as leoPlugins
from leo.external.stringlist import SList
    # Uses leoPlugins.TryNext.

import pprint
import os
import re
import json
from io import BytesIO
try:
    import yaml
except ImportError:
    yaml = None
#@-<< imports >>
controllers = {}
    # Keys are c.hash(), values are ValueSpaceControllers.

# pylint: disable=eval-used
# Eval is essential to this plugin.

#@+others
#@+node:ekr.20110408065137.14221: ** Top level
#@+node:ville.20110403115003.10353: *3* colorize_headlines_visitor
[docs]def colorize_headlines_visitor(c,p, item): """ Changes @thin, @auto, @shadow to bold """ if p.h.startswith("!= "): f = item.font(0) f.setBold(True) item.setFont(0,f) raise leoPlugins.TryNext
#@+node:ville.20110403115003.10352: *3* init
[docs]def init (): '''Return True if the plugin has loaded successfully.''' # vs_reset(None) global controllers # create global valuaspace controller for ipython g.visit_tree_item.add(colorize_headlines_visitor) g.registerHandler('after-create-leo-frame',onCreate) g.plugin_signon(__name__) return True
#@+node:ekr.20110408065137.14222: *3* onCreate
[docs]def onCreate (tag,key): global controllers c = key.get('c') if c: h = c.hash() vc = controllers.get(h) if not vc: controllers [h] = vc = ValueSpaceController(c)
#@+node:ville.20110403115003.10355: ** Commands #@+node:ville.20130127115643.3695: *3* get_vs
[docs]def get_vs(c): '''deal with singleton "ipython" controller''' if g.app.ipk: vsc = controllers.get('ipython') if not vsc: controllers['ipython'] = vsc = ValueSpaceController(c = None, ns = g.app.ipk.namespace) vsc.set_c(c) return vsc return controllers[c.hash()]
#@+node:ekr.20180328055151.1: *3* Ville's original commands #@+node:ville.20110407210441.5691: *4* vs-create-tree
[docs]@g.command('vs-create-tree') def vs_create_tree(event): """Create tree from all variables.""" get_vs(event['c']).create_tree()
#@+node:ekr.20110408065137.14227: *4* vs-dump
[docs]@g.command('vs-dump') def vs_dump(event): """Dump the valuespace for this commander.""" get_vs(event['c']).dump()
#@+node:ekr.20110408065137.14220: *4* vs-reset
[docs]@g.command('vs-reset') def vs_reset(event): get_vs(event['c']).reset()
#@+node:ville.20110403115003.10356: *4* vs-update
[docs]@g.command('vs-update') def vs_update(event): get_vs(event['c']).update()
#@+node:ekr.20110408065137.14219: ** class ValueSpaceController
[docs]class ValueSpaceController(object): '''A class supporting per-commander evaluation spaces containing @a, @r and @= nodes. ''' #@+others #@+node:ekr.20110408065137.14223: *3* ctor def __init__(self, c=None, ns=None): self.c = c self.d = {} if ns is None else ns self.reset() if c: # This must come after self.reset() c.keyHandler.autoCompleter.namespaces.append(self.d) #@+node:ekr.20110408065137.14224: *3* create_tree
[docs] def create_tree (self): '''The vs-create-tree command.''' c = self.c ; p = c.p ; tag = 'valuespace' # Create a 'valuespace' node if p's headline is not 'valuespace'. if p.h == tag: r = p else: r = p.insertAsLastChild() r.h = tag # Create a child of r for all items of self.d for k,v in self.d.items(): if not k.startswith('__'): child = r.insertAsLastChild() child.h = '@@r ' + k self.render_value(child,v) # Create child.b from child.h c.bodyWantsFocus() c.redraw()
#@+node:ekr.20110408065137.14228: *3* dump
[docs] def dump (self): c,d = self.c,self.d exclude = ( '__builtins__', # 'c','g','p', ) print('Valuespace for %s...' % c.shortFileName()) keys = list(d.keys()) keys = [z for z in keys if z not in exclude] keys.sort() max_s = 5 for key in keys: max_s = max(max_s,len(key)) for key in keys: val = d.get(key) pad = max(0,max_s-len(key))*' ' print('%s%s = %s' % (pad,key,val)) c.bodyWantsFocus()
#@+node:ekr.20110408065137.14225: *3* reset
[docs] def reset (self): '''The vs-reset command.''' # do not allow resetting the dict if using ipython if not g.app.ipk: self.d = {} self.c.vs = self.d self.init_ns(self.d)
#@+node:ville.20110409221110.5755: *3* init_ns
[docs] def init_ns(self,ns): """ Add 'builtin' methods to namespace """ def slist(body): """ Return body as SList (string list) """ return SList(body.split("\n")) ns['slist'] = slist
# xxx todo perhaps add more? #@+node:ville.20130127122722.3696: *3* set_c
[docs] def set_c(self,c): """ reconfigure vsc for new c Needed by ipython integration """ self.c = c
#@+node:ekr.20110408065137.14226: *3* update & helpers
[docs] def update (self): '''The vs-update command.''' # names are reversed, xxx TODO fix later self.render_phase() # Pass 1 self.update_vs() # Pass 2 self.c.bodyWantsFocus()
#@+node:ekr.20110407174428.5781: *4* render_phase (pass 1) & helpers
[docs] def render_phase(self): '''Update p's tree (or the entire tree) as follows: - Evaluate all @= nodes and assign them to variables - Evaluate the body of the *parent* nodes for all @a nodes. - Read in @vsi nodes and assign to variables ''' c = self.c self.d['c'] = c # g.vs.c = c self.d['g'] = g # g.vs.g = g for p in c.all_unique_positions(): h = p.h.strip() if h.startswith('@= '): self.d['p'] = p.copy() # g.vs.p = p.copy() var = h[3:].strip() self.let_body(var,self.untangle(p)) elif h.startswith("@vsi "): fname = h[5:] bname, ext = os.path.splitext(fname) g.es("@vsi " + bname +" " + ext) if ext.lower() == '.json': pth = c.getNodePath(p) fn = os.path.join(pth, fname) g.es("vsi read from " + fn) if os.path.isfile(fn): cont = open(fn).read() val = json.loads(cont) self.let(bname, val) self.render_value(p, cont) elif h == '@a' or h.startswith('@a '): tail = h[2:].strip() parent = p.parent() if tail: self.let_body(tail,self.untangle(parent)) try: self.parse_body(parent) except Exception: g.es_exception() g.es("Error parsing " + parent.h)
#@+node:ekr.20110407174428.5777: *5* let & let_body
[docs] def let(self,var,val): '''Enter var into self.d with the given value. Both var and val must be strings.''' self.d ['__vstemp'] = val if var.endswith('+'): rvar = var.rstrip('+') # .. obj = eval(rvar,self.d) exec("%s.append(__vstemp)" % rvar,self.d) else: exec(var + " = __vstemp",self.d) del self.d ['__vstemp']
[docs] def let_cl(self, var, body): """ handle @cl node """ lend = body.find('\n') firstline = body[0:lend] rest = firstline[4:].strip() print("rest",rest) try: translator = eval(rest, self.d) except Exception: g.es_exception() g.es("Can't instantate @cl xlator: " + rest) translated = translator(body[lend+1:]) self.let(var, translated)
[docs] def let_body(self,var,val): if var.endswith(".yaml"): if yaml: #print "set to yaml", `val` sio = BytesIO(val) try: d = yaml.load(sio) except Exception: g.es_exception() g.es("yaml error for: " + var) return parts = os.path.splitext(var) self.let(parts[0], d) else: g.es("did not import yaml") return if val.startswith('@cl '): self.let_cl(var, val) return self.let(var,val)
#@+node:ekr.20110407174428.5780: *5* parse_body & helpers
[docs] def parse_body(self,p): body = self.untangle(p) # body is the script in p's body. self.d ['p'] = p.copy() backop = [] segs = re.finditer('^(@x (.*))$',body,re.MULTILINE) for mo in segs: op = mo.group(2).strip() # print("Oper",op) if op.startswith('='): # print("Assign", op) backop = ('=', op.rstrip('{').lstrip('='), mo.end(1)) elif op == '{': backop = ('runblock', mo.end(1)) elif op == '}': bo = backop[0] # print("backop",bo) if bo == '=': self.let_body(backop[1].strip(), body[backop[2] : mo.start(1)]) elif bo == 'runblock': self.runblock(body[backop[1] : mo.start(1)]) else: self.runblock(op)
#@+node:ekr.20110407174428.5779: *6* runblock
[docs] def runblock(self,block): exec(block,self.d)
#@+node:ekr.20110407174428.5778: *6* untangle (getScript)
[docs] def untangle(self,p): return g.getScript(self.c,p, useSelectedText=False, useSentinels=False)
#@+node:ekr.20110407174428.5782: *4* update_vs (pass 2) & helper
[docs] def update_vs(self): ''' Evaluate @r <expr> nodes, puting the result in their body text. Output @vso nodes, based on file extension ''' c = self.c for p in c.all_unique_positions(): h = p.h.strip() if h.startswith('@r '): expr = h[3:].strip() try: result = eval(expr,self.d) except Exception: g.es_exception() g.es("Failed to render " + h) continue self.render_value(p,result) if h.startswith("@vso "): expr = h[5:].strip() bname, ext = os.path.splitext(expr) try: result = eval(bname,self.d) except Exception: g.es_exception() g.es("@vso failed: " + h) continue if ext.lower() == '.json': cnt = json.dumps(result, indent = 2) pth = os.path.join(c.getNodePath(p), expr) self.render_value(p, cnt) g.es("Writing @vso: " + pth) open(pth, "w").write(cnt) else: g.es_error("Unknown vso extension (should be .json, ...): " + ext)
#@+node:ekr.20110407174428.5784: *5* render_value
[docs] def render_value(self,p,value): '''Put the rendered value in p's body pane.''' if isinstance(value, SList): p.b = value.n elif g.isString(value): # Works with Python 3.x. p.b = value else: p.b = pprint.pformat(value)
#@+node:ekr.20110407174428.5783: *3* test
[docs] def test(self): self.update()
# test() #@-others #@-others #@-leo