changeset 1103:771f50f19b4b

single-line effect code now evals by changing <uri> into a suitable python object Ignore-this: cde829f021be54bc7cfd63bddde43aa9
author Drew Perttula <drewp@bigasterisk.com>
date Mon, 09 Jun 2014 07:10:39 +0000
parents d588211a0b4e
children 448fe9f81501
files bin/effecteval light9/curvecalc/curve.py light9/effecteval/effect.py light9/effecteval/effectloop.py light9/effecteval/test_effect.py makefile pydeps
diffstat 7 files changed, 165 insertions(+), 34 deletions(-) [+]
line wrap: on
line diff
--- a/bin/effecteval	Mon Jun 09 03:56:44 2014 +0000
+++ b/bin/effecteval	Mon Jun 09 07:10:39 2014 +0000
@@ -53,7 +53,7 @@
             (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 @@
     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):
--- a/light9/curvecalc/curve.py	Mon Jun 09 03:56:44 2014 +0000
+++ b/light9/curvecalc/curve.py	Mon Jun 09 07:10:39 2014 +0000
@@ -22,7 +22,8 @@
         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 @@
         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 @@
         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'])
--- a/light9/effecteval/effect.py	Mon Jun 09 03:56:44 2014 +0000
+++ b/light9/effecteval/effect.py	Mon Jun 09 07:10:39 2014 +0000
@@ -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 @@
         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(<uri1>, intensity=<curveuri2>)
+        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 @@
         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
+
--- a/light9/effecteval/effectloop.py	Mon Jun 09 03:56:44 2014 +0000
+++ b/light9/effecteval/effectloop.py	Mon Jun 09 07:10:39 2014 +0000
@@ -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 @@
         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 @@
                     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)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/light9/effecteval/test_effect.py	Mon Jun 09 07:10:39 2014 +0000
@@ -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 = <http://example.com/>')
+        _, 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 = <http://example.com/> + <http://other>')
+        _, 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 = <http://example/curve1>')
+        _, 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(<http://example/curve1>, t+.01)')
+        _, expr, uris = ec._asPython()
+        self.assertEqual('curve(_res0, t+.01)', expr)
+        self.assertEqual({'_res0': URIRef('http://example/curve1')}, uris)
--- a/makefile	Mon Jun 09 03:56:44 2014 +0000
+++ b/makefile	Mon Jun 09 07:10:39 2014 +0000
@@ -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
--- a/pydeps	Mon Jun 09 03:56:44 2014 +0000
+++ b/pydeps	Mon Jun 09 07:10:39 2014 +0000
@@ -8,7 +8,8 @@
 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 @@
 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