# HG changeset patch # User Drew Perttula # Date 2014-06-09 07:10:39 # Node ID 771f50f19b4ba634589cc1d6852007d521dd726e # Parent d588211a0b4ee1f84187096029c4f81c35e6b9b6 single-line effect code now evals by changing into a suitable python object Ignore-this: cde829f021be54bc7cfd63bddde43aa9 diff --git a/bin/effecteval b/bin/effecteval --- a/bin/effecteval +++ b/bin/effecteval @@ -53,7 +53,7 @@ class SongEffects(PrettyErrorHandler, cy (song, L9['effect'], effect, ctx), (effect, RDF.type, L9['Effect'], ctx), (effect, L9['code'], - Literal('out = sub(%s, intensity=%s)' % (dropped.n3(), curve.n3())), + Literal('out = %s * %s' % (dropped.n3(), curve.n3())), ctx), ])) @@ -102,13 +102,15 @@ class Code(PrettyErrorHandler, cyclone.w def put(self): effect = URIRef(self.get_argument('uri')) code = Literal(self.get_argument('code')) - with self.settings.graph.currentState(tripleFilter=(None, L9['effect'], effect)) as g: + with self.settings.graph.currentState( + tripleFilter=(None, L9['effect'], effect)) as g: song = g.subjects(L9['effect'], effect).next() self.settings.graph.patchObject( context=song, subject=effect, predicate=L9['code'], newObject=code) + # right here we could tell if the code has a python error and return it self.send_error(202) class EffectEval(PrettyErrorHandler, cyclone.web.RequestHandler): diff --git a/light9/curvecalc/curve.py b/light9/curvecalc/curve.py --- a/light9/curvecalc/curve.py +++ b/light9/curvecalc/curve.py @@ -22,7 +22,8 @@ class Curve(object): self._muted = False def __repr__(self): - return "<%s (%s points)>" % (self.__class__.__name__, len(self.points)) + return "<%s %s (%s points)>" % (self.__class__.__name__, self.uri, + len(self.points)) def muted(): doc = "Whether to currently send levels (boolean, obviously)" @@ -183,7 +184,7 @@ class CurveResource(object): self.curve = Curve(self.uri) self.curve.points.extend([(0, 0)]) self.saveCurve() - self.watchChanges() + self.watchCurvePointChanges() def loadCurve(self): if hasattr(self, 'curve'): @@ -192,7 +193,11 @@ class CurveResource(object): pointsFile = self.graph.value(self.uri, L9['pointsFile']) self.curve = Curve(self.uri, pointsStorage='file' if pointsFile else 'graph') - self.graph.addHandler(self.pointsFromGraph) + if hasattr(self.graph, 'addHandler'): + self.graph.addHandler(self.pointsFromGraph) + else: + # given a currentState graph + self.pointsFromGraph() def pointsFromGraph(self): pts = self.graph.value(self.uri, L9['points']) diff --git a/light9/effecteval/effect.py b/light9/effecteval/effect.py --- a/light9/effecteval/effect.py +++ b/light9/effecteval/effect.py @@ -1,9 +1,9 @@ -from run_local import log -import re +import re, logging from rdflib import URIRef -from light9.namespaces import L9 -from light9.curvecalc.curve import Curve +from light9.namespaces import L9, RDF +from light9.curvecalc.curve import CurveResource from light9 import Submaster +log = logging.getLogger('effect') def uriFromCode(s): # i thought this was something a graph could do with its namespace manager @@ -15,6 +15,46 @@ def uriFromCode(s): return URIRef(s[1:-1]) raise NotImplementedError +# consider http://waxeye.org/ for a parser that can be used in py and js + +class CodeLine(object): + """code string is immutable""" + def __init__(self, graph, code): + self.graph, self.code = graph, code + + self.outName, self.expr, self.resources = self._asPython() + + def _asPython(self): + """ + out = sub(, intensity=) + becomes + 'out', + 'sub(_u1, intensity=curve(_u2, t))', + {'_u1': URIRef('uri1'), '_u2': URIRef('uri2')} + """ + lname, expr = [s.strip() for s in self.code.split('=', 1)] + self.uriCounter = 0 + resources = {} + + def alreadyInCurveFunc(s, i): + prefix = 'curve(' + return i >= len(prefix) and s[i-len(prefix):i] == prefix + + def repl(m): + v = '_res%s' % self.uriCounter + self.uriCounter += 1 + r = resources[v] = URIRef(m.group(1)) + if self._uriIsCurve(r): + if not alreadyInCurveFunc(m.string, m.start()): + return 'curve(%s, t)' % v + return v + outExpr = re.sub(r'<(http\S*?)>', repl, expr) + return lname, outExpr, resources + + def _uriIsCurve(self, uri): + # this result could vary with graph changes (rare) + return self.graph.contains((uri, RDF.type, L9['Curve'])) + class EffectNode(object): def __init__(self, graph, uri): self.graph, self.uri = graph, uri @@ -22,28 +62,51 @@ class EffectNode(object): self.graph.addHandler(self.prepare) def prepare(self): - self.code = self.graph.value(self.uri, L9['code']) - if self.code is None: + log.info("prepare effect %s", self.uri) + # maybe there can be multiple lines of code as multiple + # objects here, and we sort them by dependencies + codeStr = self.graph.value(self.uri, L9['code']) + if codeStr is None: raise ValueError("effect %s has no code" % self.uri) - m = re.match(r'^out = sub\((.*?), intensity=(.*?)\)', self.code) - if not m: - raise NotImplementedError - subUri = uriFromCode(m.group(1)) - subs = Submaster.get_global_submasters(self.graph) - self.sub = subs.get_sub_by_uri(subUri) + + self.code = CodeLine(self.graph, codeStr) + + self.resourceMap = self.resourcesAsPython() - intensityCurve = uriFromCode(m.group(2)) - self.curve = Curve(uri=intensityCurve) + def resourcesAsPython(self): + """ + mapping of the local names for uris in the code to high-level + objects (Submaster, Curve) + """ + out = {} + subs = Submaster.get_global_submasters(self.graph) + for localVar, uri in self.code.resources.items(): + for rdfClass in self.graph.objects(uri, RDF.type): + if rdfClass == L9['Curve']: + cr = CurveResource(self.graph, uri) + cr.loadCurve() + out[localVar] = cr.curve + elif rdfClass == L9['Submaster']: + out[localVar] = subs.get_sub_by_uri(uri) + else: + out[localVar] = uri - pts = self.graph.value(intensityCurve, L9['points']) - if pts is None: - log.info("curve %r has no points" % intensityCurve) - else: - self.curve.set_from_string(pts) - + return out def eval(self, songTime): - # consider http://waxeye.org/ for a parser that can be used in py and js - level = self.curve.eval(songTime) - scaledSubs = self.sub * level - return scaledSubs + ns = {'t': songTime} + + 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 + + ns.update(self.resourceMap) + return eval(self.code.expr, ns) + + +class GraphAwareFunction(object): + def __init__(self, graph): + self.graph = graph + diff --git a/light9/effecteval/effectloop.py b/light9/effecteval/effectloop.py --- a/light9/effecteval/effectloop.py +++ b/light9/effecteval/effectloop.py @@ -1,3 +1,4 @@ +from __future__ import division import time, json, logging, traceback from twisted.internet import reactor from twisted.internet.defer import inlineCallbacks, returnValue @@ -19,6 +20,7 @@ class EffectLoop(object): self.currentEffects = [] self.lastLogTime = 0 self.lastLogMsg = "" + self.lastErrorLog = 0 self.graph.addHandler(self.setEffects) self.period = 1 / 30 self.coastSecs = .3 # main reason to keep this low is to notice play/pause @@ -73,7 +75,12 @@ class EffectLoop(object): try: outSubs.append(e.eval(songTime)) except Exception as exc: - log.error("effect %s: %s" % (e.uri, exc)) + 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", e.resourcesAsPython()) + self.lastErrorLog = now out = Submaster.sub_maxes(*outSubs) self.logLevels(t1, out) diff --git a/light9/effecteval/test_effect.py b/light9/effecteval/test_effect.py new file mode 100644 --- /dev/null +++ b/light9/effecteval/test_effect.py @@ -0,0 +1,52 @@ +import unittest +import mock +import sys +sys.path.insert(0, 'bin') # for run_local + +from effect import CodeLine +from rdflib import URIRef + +def isCurve(self, uri): + return 'curve' in uri + +@mock.patch('light9.effecteval.effect.CodeLine._uriIsCurve', new=isCurve) +class TestAsPython(unittest.TestCase): + def test_gets_lname(self): + ec = CodeLine(graph=None, code='x = y+1') + self.assertEqual('x', ec._asPython()[0]) + + 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]) + + def test_converts_uri_to_var(self): + ec = CodeLine(graph=None, code='x = ') + _, 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() + 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() + 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() + 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() + self.assertEqual('curve(_res0, t+.01)', expr) + self.assertEqual({'_res0': URIRef('http://example/curve1')}, uris) diff --git a/makefile b/makefile --- a/makefile +++ b/makefile @@ -1,10 +1,10 @@ -NOSEARGS="--no-path-adjustment light9.rdfdb.rdflibpatch light9.rdfdb.patch" +NOSEARGS="--no-path-adjustment light9.rdfdb.rdflibpatch light9.rdfdb.patch light9.effecteval.test_effect" tests: eval env/bin/nosetests -x $(NOSEARGS) tests_watch: - eval env/bin/nosetests --with-watch $(NOSEARGS) + eval env/bin/nosetests --with-watcher $(NOSEARGS) # needed packages: python-gtk2 python-imaging diff --git a/pydeps b/pydeps --- a/pydeps +++ b/pydeps @@ -8,7 +8,8 @@ web.py==0.37 restkit==4.2.2 ipython==2.1.0 nose==1.3.3 -nose-alert==0.9.0 +nose-watcher==0.1.2 +watchdog==0.7.1 ipdb==0.8 coloredlogs==0.5 genshi==0.7 @@ -18,4 +19,5 @@ txosc==0.2.0 service_identity==0.2 Pillow==2.4.0 faulthandler==2.3 -treq==0.2.1 \ No newline at end of file +treq==0.2.1 +mock==1.0.1 \ No newline at end of file