diff --git a/bin/effecteval b/bin/effecteval --- a/bin/effecteval +++ b/bin/effecteval @@ -104,8 +104,12 @@ class EffectUpdates(cyclone.websocket.We def updateClient(self): # todo: if client has dropped, abort and don't get any more # graph updates - self.sendMessage({'codeLines': - list(self.graph.objects(self.uri, L9['code']))}) + + # EffectNode knows how to put them in order. Somehow this is + # not triggering an update when the order changes. + en = EffectNode(self.graph, self.uri) + codeLines = [c.code for c in en.codes] + self.sendMessage({'codeLines': codeLines}) def connectionLost(self, reason): log.info("websocket closed") diff --git a/light9/effecteval/effect.py b/light9/effecteval/effect.py --- a/light9/effecteval/effect.py +++ b/light9/effecteval/effect.py @@ -1,4 +1,5 @@ import re, logging +import toposort from rdflib import URIRef from light9.namespaces import L9, RDF from light9.curvecalc.curve import CurveResource @@ -22,8 +23,9 @@ class CodeLine(object): def __init__(self, graph, code): self.graph, self.code = graph, code - self.outName, self.expr, self.resources = self._asPython() + self.outName, self.inExpr, self.expr, self.resources = self._asPython() self.pyResources = self._resourcesAsPython(self.resources) + self.possibleVars = self.findVars(self.inExpr) def _asPython(self): """ @@ -50,8 +52,15 @@ class CodeLine(object): return 'curve(%s, t)' % v return v outExpr = re.sub(r'<(http\S*?)>', repl, expr) - return lname, outExpr, resources + return lname, expr, outExpr, resources + def findVars(self, expr): + """may return some more strings than just the vars""" + withoutUris = re.sub(r'<(http\S*?)>', 'None', expr) + tokens = set(re.findall(r'\b([a-zA-Z_]\w*)\b', withoutUris)) + tokens.discard('None') + return tokens + def _uriIsCurve(self, uri): # this result could vary with graph changes (rare) return self.graph.contains((uri, RDF.type, L9['Curve'])) @@ -92,8 +101,21 @@ class EffectNode(object): self.codes = [CodeLine(self.graph, s) for s in codeStrs] + self.sortCodes() + self.otherFuncs = Effects.configExprGlobals() - + + def sortCodes(self): + """put self.codes in a working evaluation order""" + codeFromOutput = dict((c.outName, c) for c in self.codes) + deps = {} + for c in self.codes: + outName = c.outName + inNames = c.possibleVars.intersection(codeFromOutput.keys()) + inNames.discard(outName) + deps[outName] = inNames + self.codes = [codeFromOutput[n] for n in toposort.toposort_flatten(deps)] + def eval(self, songTime): ns = {'t': songTime} ns.update(self.otherFuncs) @@ -101,13 +123,13 @@ class EffectNode(object): ns.update(dict( curve=lambda c, t: c.eval(t), )) - # loop over lines in order, merging in outputs - # merge in named outputs from previous lines for c in self.codes: codeNs = ns.copy() codeNs.update(c.pyResources) - if c.outName == 'out': - out = eval(c.expr, codeNs) - return out + lineOut = eval(c.expr, codeNs) + ns[c.outName] = lineOut + if 'out' not in ns: + log.error("ran code for %s, didn't make an 'out' value", self.uri) + return ns['out'] diff --git a/light9/effecteval/effectloop.py b/light9/effecteval/effectloop.py --- a/light9/effecteval/effectloop.py +++ b/light9/effecteval/effectloop.py @@ -78,9 +78,6 @@ class EffectLoop(object): now = time.time() if now > self.lastErrorLog + 5: log.error("effect %s: %s" % (e.uri, exc)) - log.error(" expr: %s", e.code.expr) - log.error(" resources: %r", - getattr(e, 'resourceMap', '?')) self.lastErrorLog = now out = Submaster.sub_maxes(*outSubs) diff --git a/light9/effecteval/test_effect.py b/light9/effecteval/test_effect.py --- a/light9/effecteval/test_effect.py +++ b/light9/effecteval/test_effect.py @@ -10,43 +10,56 @@ def isCurve(self, uri): return 'curve' in uri @mock.patch('light9.effecteval.effect.CodeLine._uriIsCurve', new=isCurve) +@mock.patch('light9.effecteval.effect.CodeLine._resourcesAsPython', + new=lambda self, r: self.expr) class TestAsPython(unittest.TestCase): def test_gets_lname(self): ec = CodeLine(graph=None, code='x = y+1') - self.assertEqual('x', ec._asPython()[0]) + self.assertEqual('x', ec.outName) def test_gets_simple_code(self): ec = CodeLine(graph=None, code='x = y+1') - self.assertEqual('y+1', ec._asPython()[1]) - self.assertEqual({}, ec._asPython()[2]) + self.assertEqual('y+1', ec._asPython()[2]) + self.assertEqual({}, ec._asPython()[3]) def test_converts_uri_to_var(self): ec = CodeLine(graph=None, code='x = ') - _, expr, uris = ec._asPython() + _, inExpr, expr, uris = ec._asPython() self.assertEqual('_res0', expr) self.assertEqual({'_res0': URIRef('http://example.com/')}, uris) def test_converts_multiple_uris(self): ec = CodeLine(graph=None, code='x = + ') - _, expr, uris = ec._asPython() + _, inExpr, expr, uris = ec._asPython() self.assertEqual('_res0 + _res1', expr) self.assertEqual({'_res0': URIRef('http://example.com/'), '_res1': URIRef('http://other')}, uris) def test_doesnt_fall_for_brackets(self): ec = CodeLine(graph=None, code='x = 1<2>3< h') - _, expr, uris = ec._asPython() + _, inExpr, expr, uris = ec._asPython() self.assertEqual('1<2>3< h', expr) self.assertEqual({}, uris) def test_curve_uri_expands_to_curve_eval_func(self): ec = CodeLine(graph=None, code='x = ') - _, expr, uris = ec._asPython() + _, inExpr, expr, uris = ec._asPython() self.assertEqual('curve(_res0, t)', expr) self.assertEqual({'_res0': URIRef('http://example/curve1')}, uris) def test_curve_doesnt_double_wrap(self): ec = CodeLine(graph=None, code='x = curve(, t+.01)') - _, expr, uris = ec._asPython() + _, inExpr, expr, uris = ec._asPython() self.assertEqual('curve(_res0, t+.01)', expr) self.assertEqual({'_res0': URIRef('http://example/curve1')}, uris) + +@mock.patch('light9.effecteval.effect.CodeLine._uriIsCurve', new=isCurve) +@mock.patch('light9.effecteval.effect.CodeLine._resourcesAsPython', + new=lambda self, r: self.expr) +class TestPossibleVars(unittest.TestCase): + def test1(self): + self.assertEqual(set([]), CodeLine(None, 'a1 = 1').possibleVars) + def test2(self): + self.assertEqual(set(['a2']), CodeLine(None, 'a1 = a2').possibleVars) + def test3(self): + self.assertEqual(set(['a2', 'a3']), CodeLine(None, 'a1 = a2 + a3').possibleVars) diff --git a/pydeps b/pydeps --- a/pydeps +++ b/pydeps @@ -20,4 +20,6 @@ service_identity==0.2 Pillow==2.4.0 faulthandler==2.3 treq==0.2.1 -mock==1.0.1 \ No newline at end of file +mock==1.0.1 +toposort==1.0 +