#@+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