diff --git a/light9/collector/collector.py b/light9/collector/collector.py
--- a/light9/collector/collector.py
+++ b/light9/collector/collector.py
@@ -2,13 +2,22 @@ from __future__ import division
import time
import logging
from rdflib import Literal
-from light9.namespaces import L9, RDF, DEV
+from light9.namespaces import L9, RDF
from light9.collector.output import setListElem
from light9.collector.device import toOutputAttrs, resolve
+# types only
+from rdflib import Graph, URIRef
+from typing import List, Dict, Tuple, Any, TypeVar, Generic
+from light9.collector.output import Output
+
+ClientType = TypeVar('ClientType')
+ClientSessionType = TypeVar('ClientSessionType')
+
log = logging.getLogger('collector')
def outputMap(graph, outputs):
+ # type: (Graph, List[Output]) -> Dict[Tuple[URIRef, URIRef], Tuple[Output, int]]
"""From rdf config graph, compute a map of
(device, outputattr) : (output, index)
that explains which output index to set for any device update.
@@ -20,26 +29,38 @@ def outputMap(graph, outputs):
outputByUri[out.uri] = out
for dc in graph.subjects(RDF.type, L9['DeviceClass']):
+ log.info('mapping DeviceClass %s', dc)
for dev in graph.subjects(RDF.type, dc):
- output = outputByUri[graph.value(dev, L9['dmxUniverse'])]
+ log.info(' mapping device %s', dev)
+ universe = graph.value(dev, L9['dmxUniverse'])
+ try:
+ output = outputByUri[universe]
+ except Exception:
+ log.warn('dev %r :dmxUniverse %r', dev, universe)
+ raise
dmxBase = int(graph.value(dev, L9['dmxBase']).toPython())
for row in graph.objects(dc, L9['attr']):
outputAttr = graph.value(row, L9['outputAttr'])
offset = int(graph.value(row, L9['dmxOffset']).toPython())
index = dmxBase + offset - 1
ret[(dev, outputAttr)] = (output, index)
- log.info('map %s,%s to %s,%s', dev, outputAttr, output, index)
+ log.info(' map %s to %s,%s', outputAttr, output, index)
return ret
-class Collector(object):
+class Collector(Generic[ClientType, ClientSessionType]):
def __init__(self, graph, outputs, clientTimeoutSec=10):
+ # type: (Graph, List[Output], float) -> None
self.graph = graph
self.outputs = outputs
self.clientTimeoutSec = clientTimeoutSec
self.graph.addHandler(self.rebuildOutputMap)
- self.lastRequest = {} # client : (session, time, {(dev,devattr): latestValue})
- self.stickyAttrs = {} # (dev, devattr): value to use instead of 0
+
+ # client : (session, time, {(dev,devattr): latestValue})
+ self.lastRequest = {} # type: Dict[ClientType, Tuple[ClientSessionType, float, Dict[Tuple[URIRef, URIRef], float]]]
+
+ # (dev, devAttr): value to use instead of 0
+ self.stickyAttrs = {} # type: Dict[Tuple[URIRef, URIRef], float]
def rebuildOutputMap(self):
self.outputMap = outputMap(self.graph, self.outputs) # (device, outputattr) : (output, index)
@@ -56,15 +77,18 @@ class Collector(object):
self.remapOut[(dev, attr)] = start, end
def _forgetStaleClients(self, now):
+ # type: (float) -> None
staleClients = []
- for c, (_, t, _) in self.lastRequest.iteritems():
+ for c, (_, t, _2) in self.lastRequest.iteritems():
if t < now - self.clientTimeoutSec:
staleClients.append(c)
for c in staleClients:
+ log.info('forgetting stale client %r', c)
del self.lastRequest[c]
def resolvedSettingsDict(self, settingsList):
- out = {}
+ # type: (List[Tuple[URIRef, URIRef, float]]) -> Dict[Tuple[URIRef, URIRef], float]
+ out = {} # type: Dict[Tuple[URIRef, URIRef], float]
for d, da, v in settingsList:
if (d, da) in out:
out[(d, da)] = resolve(d, da, [out[(d, da)], v])
@@ -72,6 +96,12 @@ class Collector(object):
out[(d, da)] = v
return out
+ def _warnOnLateRequests(self, client, now, sendTime):
+ requestLag = now - sendTime
+ if requestLag > .1:
+ log.warn('collector.setAttrs from %s is running %.1fms after the request was made',
+ client, requestLag * 1000)
+
def setAttrs(self, client, clientSession, settings, sendTime):
"""
settings is a list of (device, attr, value). These attrs are
@@ -82,10 +112,7 @@ class Collector(object):
Call with settings=[] to ping us that your session isn't dead.
"""
now = time.time()
- requestLag = now - sendTime
- if requestLag > .1:
- log.warn('collector.setAttrs from %s is running %.1fms after the request was made',
- client, requestLag * 1000)
+ self._warnOnLateRequests(client, now, sendTime)
self._forgetStaleClients(now)
@@ -128,6 +155,7 @@ class Collector(object):
pendingOut = {} # output : values
for out in self.outputs:
pendingOut[out] = [0] * out.numChannels
+
for device, attrs in outputAttrs.iteritems():
for outputAttr, value in attrs.iteritems():
self.setAttr(device, outputAttr, value, pendingOut)
diff --git a/light9/collector/collector_test.py b/light9/collector/collector_test.py
--- a/light9/collector/collector_test.py
+++ b/light9/collector/collector_test.py
@@ -1,7 +1,7 @@
import unittest
-import datetime
+import datetime, time
from freezegun import freeze_time
-from rdflib import Namespace
+from rdflib import Namespace, URIRef
from light9.namespaces import L9, DEV
from light9.collector.collector import Collector, outputMap
@@ -17,10 +17,32 @@ PREFIX = '''
@prefix dmx0: .
'''
+THEATER = '''
+ :brightness a :DeviceAttr; :dataType :scalar .
+
+ :SimpleDimmer a :DeviceClass;
+ :deviceAttr :brightness;
+ :attr
+ [ :outputAttr :level; :dmxOffset 0 ] .
+
+ :ChauvetColorStrip a :DeviceClass;
+ :deviceAttr :color;
+ :attr
+ [ :outputAttr :mode; :dmxOffset 0 ],
+ [ :outputAttr :red; :dmxOffset 1 ],
+ [ :outputAttr :green; :dmxOffset 2 ],
+ [ :outputAttr :blue; :dmxOffset 3 ] .
+
+'''
+
+t0 = 0 # time
+
class MockOutput(object):
- def __init__(self, connections):
+ def __init__(self, uri, connections):
self.connections = connections
self.updates = []
+ self.uri = uri
+ self.numChannels = 4
def allConnections(self):
return self.connections
@@ -31,29 +53,30 @@ class MockOutput(object):
def flush(self):
self.updates.append('flush')
+@unittest.skip("outputMap got rewritten and mostly doesn't raise on these cases")
class TestOutputMap(unittest.TestCase):
def testWorking(self):
- out0 = MockOutput([(0, DMX0['c1'])])
+ out0 = MockOutput(UDMX, [(0, DMX0['c1'])])
m = outputMap(MockSyncedGraph(PREFIX + '''
dmx0:c1 :connectedTo dev:inst1Brightness .
dev:inst1 a :Device; :brightness dev:inst1Brightness .
'''), [out0])
self.assertEqual({(DEV['inst1'], L9['brightness']): (out0, 0)}, m)
+
def testMissingOutput(self):
- out0 = MockOutput([(0, DMX0['c1'])])
+ out0 = MockOutput(UDMX, [(0, DMX0['c1'])])
self.assertRaises(KeyError, outputMap, MockSyncedGraph(PREFIX + '''
- dmx0:c2 :connectedTo dev:inst1Brightness .
dev:inst1 a :Device; :brightness dev:inst1Brightness .
'''), [out0])
def testMissingOutputConnection(self):
- out0 = MockOutput([(0, DMX0['c1'])])
+ out0 = MockOutput(UDMX, [(0, DMX0['c1'])])
self.assertRaises(ValueError, outputMap, MockSyncedGraph(PREFIX + '''
dev:inst1 a :Device; :brightness dev:inst1Brightness .
'''), [out0])
def testMultipleOutputConnections(self):
- out0 = MockOutput([(0, DMX0['c1'])])
+ out0 = MockOutput(UDMX, [(0, DMX0['c1'])])
self.assertRaises(ValueError, outputMap, MockSyncedGraph(PREFIX + '''
dmx0:c1 :connectedTo dev:inst1Brightness .
dmx0:c2 :connectedTo dev:inst1Brightness .
@@ -64,137 +87,147 @@ class TestOutputMap(unittest.TestCase):
class TestCollector(unittest.TestCase):
def setUp(self):
- self.config = MockSyncedGraph(PREFIX + '''
-
- udmx:c1 :connectedTo dev:colorStripRed .
- udmx:c2 :connectedTo dev:colorStripGreen .
- udmx:c3 :connectedTo dev:colorStripBlue .
- udmx:c4 :connectedTo dev:colorStripMode .
+ self.config = MockSyncedGraph(PREFIX + THEATER + '''
dev:colorStrip a :Device, :ChauvetColorStrip;
+ :dmxUniverse udmx:; :dmxBase 1;
:red dev:colorStripRed;
:green dev:colorStripGreen;
:blue dev:colorStripBlue;
:mode dev:colorStripMode .
- dmx0:c1 :connectedTo dev:inst1Brightness .
- dev:inst1 a :Device, :Dimmer;
- :brightness dev:inst1Brightness .
+ dev:inst1 a :Device, :SimpleDimmer;
+ :dmxUniverse dmx0:; :dmxBase 1;
+ :level dev:inst1Brightness .
''')
- self.dmx0 = MockOutput([(0, DMX0['c1'])])
- self.udmx = MockOutput([(0, UDMX['c1']),
- (1, UDMX['c2']),
- (2, UDMX['c3']),
- (3, UDMX['c4'])])
+ self.dmx0 = MockOutput(DMX0[None], [(0, DMX0['c1'])])
+ self.udmx = MockOutput(UDMX[None], [(0, UDMX['c1']),
+ (1, UDMX['c2']),
+ (2, UDMX['c3']),
+ (3, UDMX['c4'])])
def testRoutesColorOutput(self):
c = Collector(self.config, outputs=[self.dmx0, self.udmx])
c.setAttrs('client', 'sess1',
- [(DEV['colorStrip'], L9['color'], '#00ff00')])
+ [(DEV['colorStrip'], L9['color'], '#00ff00')], t0)
- self.assertEqual([[0, 255, 0, 215], 'flush'], self.udmx.updates)
- self.assertEqual([], self.dmx0.updates)
+ self.assertEqual([[215, 0, 255, 0], 'flush'], self.udmx.updates)
+ self.assertEqual([[0, 0, 0, 0], 'flush'], self.dmx0.updates)
def testOutputMaxOfTwoClients(self):
c = Collector(self.config, outputs=[self.dmx0, self.udmx])
c.setAttrs('client1', 'sess1',
- [(DEV['colorStrip'], L9['color'], '#ff0000')])
+ [(DEV['colorStrip'], L9['color'], '#ff0000')], t0)
c.setAttrs('client2', 'sess1',
- [(DEV['colorStrip'], L9['color'], '#333333')])
+ [(DEV['colorStrip'], L9['color'], '#333333')], t0)
- self.assertEqual([[255, 0, 0, 215], 'flush',
- [255, 51, 51, 215], 'flush'],
+ self.assertEqual([[215, 255, 0, 0], 'flush',
+ [215, 255, 51, 51], 'flush'],
self.udmx.updates)
- self.assertEqual([], self.dmx0.updates)
+ self.assertEqual([[0, 0, 0, 0], 'flush', [0, 0, 0, 0], 'flush'],
+ self.dmx0.updates)
def testClientOnSameOutputIsRememberedOverCalls(self):
c = Collector(self.config, outputs=[self.dmx0, self.udmx])
c.setAttrs('client1', 'sess1',
- [(DEV['colorStrip'], L9['color'], '#080000')])
+ [(DEV['colorStrip'], L9['color'], '#080000')], t0)
c.setAttrs('client2', 'sess1',
- [(DEV['colorStrip'], L9['color'], '#060000')])
+ [(DEV['colorStrip'], L9['color'], '#060000')], t0)
c.setAttrs('client1', 'sess1',
- [(DEV['colorStrip'], L9['color'], '#050000')])
+ [(DEV['colorStrip'], L9['color'], '#050000')], t0)
- self.assertEqual([[8, 0, 0, 215], 'flush',
- [8, 0, 0, 215], 'flush',
- [6, 0, 0, 215], 'flush'],
+ self.assertEqual([[215, 8, 0, 0], 'flush',
+ [215, 8, 0, 0], 'flush',
+ [215, 6, 0, 0], 'flush'],
self.udmx.updates)
- self.assertEqual([], self.dmx0.updates)
+ self.assertEqual([[0, 0, 0, 0], 'flush',
+ [0, 0, 0, 0], 'flush',
+ [0, 0, 0, 0], 'flush'],
+ self.dmx0.updates)
def testClientsOnDifferentOutputs(self):
c = Collector(self.config, outputs=[self.dmx0, self.udmx])
- c.setAttrs('client1', 'sess1', [(DEV['colorStrip'], L9['color'], '#aa0000')])
- c.setAttrs('client2', 'sess1', [(DEV['inst1'], L9['brightness'], .5)])
+ c.setAttrs('client1', 'sess1', [(DEV['colorStrip'], L9['color'], '#aa0000')], t0)
+ c.setAttrs('client2', 'sess1', [(DEV['inst1'], L9['brightness'], .5)], t0)
# ok that udmx is flushed twice- it can screen out its own duplicates
- self.assertEqual([[170, 0, 0, 215], 'flush',
- [170, 0, 0, 215], 'flush'], self.udmx.updates)
- self.assertEqual([[127], 'flush'], self.dmx0.updates)
+ self.assertEqual([[215, 170, 0, 0], 'flush',
+ [215, 170, 0, 0], 'flush'], self.udmx.updates)
+ self.assertEqual([[0, 0, 0, 0], 'flush',
+ [127, 0, 0, 0], 'flush'], self.dmx0.updates)
def testNewSessionReplacesPreviousOutput(self):
# ..as opposed to getting max'd with it
c = Collector(self.config, outputs=[self.dmx0, self.udmx])
- c.setAttrs('client1', 'sess1', [(DEV['inst1'], L9['brightness'], .8)])
- c.setAttrs('client1', 'sess2', [(DEV['inst1'], L9['brightness'], .5)])
+ c.setAttrs('client1', 'sess1', [(DEV['inst1'], L9['brightness'], .8)], t0)
+ c.setAttrs('client1', 'sess2', [(DEV['inst1'], L9['brightness'], .5)], t0)
- self.assertEqual([[204], 'flush', [127], 'flush'], self.dmx0.updates)
+ self.assertEqual([[204, 0, 0, 0], 'flush',
+ [127, 0, 0, 0], 'flush'], self.dmx0.updates)
def testNewSessionDropsPreviousSettingsOfOtherAttrs(self):
-
- c = Collector(MockSyncedGraph(PREFIX + '''
-
- udmx:c1 :connectedTo dev:colorStripRed .
- udmx:c2 :connectedTo dev:colorStripGreen .
- udmx:c3 :connectedTo dev:colorStripBlue .
- udmx:c4 :connectedTo dev:colorStripMode .
+ c = Collector(MockSyncedGraph(PREFIX + THEATER + '''
dev:colorStrip a :Device, :ChauvetColorStrip;
+ :dmxUniverse udmx:; :dmxBase 1;
:red dev:colorStripRed;
:green dev:colorStripGreen;
:blue dev:colorStripBlue;
:mode dev:colorStripMode .
- dmx0:c1 :connectedTo dev:inst1Brightness .
- dev:inst1 a :Device, :Dimmer;
- :brightness dev:inst1Brightness .
+ dev:inst1 a :Device, :SimpleDimmer;
+ :dmxUniverse dmx0:; :dmxBase 0;
+ :level dev:inst1Brightness .
'''), outputs=[self.dmx0, self.udmx])
c.setAttrs('client1', 'sess1',
- [(DEV['colorStrip'], L9['color'], '#ff0000')])
+ [(DEV['colorStrip'], L9['color'], '#ff0000')], t0)
c.setAttrs('client1', 'sess2',
- [(DEV['colorStrip'], L9['color'], '#00ff00')])
+ [(DEV['colorStrip'], L9['color'], '#00ff00')], t0)
- self.assertEqual([[255, 0, 0, 215], 'flush',
- [0, 255, 0, 215], 'flush'], self.udmx.updates)
+ self.assertEqual([[215, 255, 0, 0], 'flush',
+ [215, 0, 255, 0], 'flush'], self.udmx.updates)
def testClientIsForgottenAfterAWhile(self):
with freeze_time(datetime.datetime.now()) as ft:
c = Collector(self.config, outputs=[self.dmx0, self.udmx])
- c.setAttrs('cli1', 'sess1', [(DEV['inst1'], L9['brightness'], .5)])
+ c.setAttrs('cli1', 'sess1', [(DEV['inst1'], L9['brightness'], .5)],
+ time.time())
ft.tick(delta=datetime.timedelta(seconds=1))
- c.setAttrs('cli2', 'sess1', [(DEV['inst1'], L9['brightness'], .2)])
+ # this max's with cli1's value so we still see .5
+ c.setAttrs('cli2', 'sess1', [(DEV['inst1'], L9['brightness'], .2)],
+ time.time())
ft.tick(delta=datetime.timedelta(seconds=9.1))
- c.setAttrs('cli2', 'sess1', [(DEV['inst1'], L9['brightness'], .4)])
- self.assertEqual([[127], 'flush', [127], 'flush', [102], 'flush'],
+ # now cli1 is forgotten, so our value appears
+ c.setAttrs('cli2', 'sess1', [(DEV['inst1'], L9['brightness'], .4)],
+ time.time())
+ self.assertEqual([[127, 0, 0, 0], 'flush',
+ [127, 0, 0, 0], 'flush',
+ [102, 0, 0, 0], 'flush'],
self.dmx0.updates)
- def testClientUpdatesAreCollected(self):
- # second call to setAttrs doesn't forget the first
+ def testClientUpdatesAreNotMerged(self):
+ # second call to setAttrs forgets the first
c = Collector(self.config, outputs=[self.dmx0, self.udmx])
+ t0 = time.time()
+ c.setAttrs('client1', 'sess1', [(DEV['inst1'], L9['brightness'], .5)], t0)
+ c.setAttrs('client1', 'sess1', [(DEV['inst1'], L9['brightness'], 1)], t0)
+ c.setAttrs('client1', 'sess1', [(DEV['colorStrip'], L9['color'], '#00ff00')], t0)
- c.setAttrs('client1', 'sess1', [(DEV['inst1'], L9['brightness'], .5)])
- c.setAttrs('client1', 'sess1', [(DEV['inst1'], L9['brightness'], 1)])
- c.setAttrs('client1', 'sess1', [(DEV['colorStrip'], L9['color'], '#00ff00')])
-
- self.assertEqual([[0, 255, 0, 215], 'flush'], self.udmx.updates)
- self.assertEqual([[127], 'flush', [255], 'flush', [255], 'flush'], self.dmx0.updates)
+ self.assertEqual([[0, 0, 0, 0], 'flush',
+ [0, 0, 0, 0], 'flush',
+ [215, 0, 255, 0], 'flush'],
+ self.udmx.updates)
+ self.assertEqual([[127, 0, 0, 0], 'flush',
+ [255, 0, 0, 0], 'flush',
+ [0, 0, 0, 0], 'flush'],
+ self.dmx0.updates)
def testRepeatedAttributesInOneRequestGetResolved(self):
c = Collector(self.config, outputs=[self.dmx0, self.udmx])
@@ -202,12 +235,13 @@ class TestCollector(unittest.TestCase):
c.setAttrs('client1', 'sess1', [
(DEV['inst1'], L9['brightness'], .5),
(DEV['inst1'], L9['brightness'], .3),
- ])
- self.assertEqual([[127], 'flush'], self.dmx0.updates)
+ ], t0)
+ self.assertEqual([[127, 0, 0, 0], 'flush'], self.dmx0.updates)
c.setAttrs('client1', 'sess1', [
(DEV['inst1'], L9['brightness'], .3),
(DEV['inst1'], L9['brightness'], .5),
- ])
- self.assertEqual([[127], 'flush', [127], 'flush'], self.dmx0.updates)
+ ], t0)
+ self.assertEqual([[127, 0, 0, 0], 'flush',
+ [127, 0, 0, 0], 'flush'], self.dmx0.updates)
diff --git a/light9/collector/device.py b/light9/collector/device.py
--- a/light9/collector/device.py
+++ b/light9/collector/device.py
@@ -76,7 +76,7 @@ def toOutputAttrs(deviceType, deviceAttr
out = deviceAttrSettings.get(attr)
if out is None:
return default
- return float(out.toPython())
+ return float(out.toPython()) if isinstance(out, Literal) else out
def rgbAttr(attr):
color = deviceAttrSettings.get(attr, '#000000')
diff --git a/light9/collector/device_test.py b/light9/collector/device_test.py
--- a/light9/collector/device_test.py
+++ b/light9/collector/device_test.py
@@ -20,8 +20,8 @@ class TestColorStrip(unittest.TestCase):
class TestDimmer(unittest.TestCase):
def testConvert(self):
- self.assertEqual({L9['brightness']: 127},
- toOutputAttrs(L9['Dimmer'], {L9['brightness']: .5}))
+ self.assertEqual({L9['level']: 127},
+ toOutputAttrs(L9['SimpleDimmer'], {L9['brightness']: .5}))
class TestMini15(unittest.TestCase):
def testConvertColor(self):
diff --git a/light9/collector/output.py b/light9/collector/output.py
--- a/light9/collector/output.py
+++ b/light9/collector/output.py
@@ -23,6 +23,8 @@ class Output(object):
often as you want- the result will be sent as soon as possible,
and with repeats as needed to outlast hardware timeouts.
"""
+ uri = None # type: URIRef
+ numChannels = None # type: int
def __init__(self):
raise NotImplementedError
diff --git a/light9/collector/output_test.py b/light9/collector/output_test.py
--- a/light9/collector/output_test.py
+++ b/light9/collector/output_test.py
@@ -33,14 +33,6 @@ class TestSetListElem(unittest.TestCase)
self.assertEqual([0, 1, 1], x)
class TestDmxOutput(unittest.TestCase):
- def testGeneratesConnectionList(self):
- out = DmxOutput(L9['output/udmx/'], 3)
- self.assertEqual([
- (0, L9['output/udmx/c1']),
- (1, L9['output/udmx/c2']),
- (2, L9['output/udmx/c3']),
- ], list(out.allConnections()))
-
def testFlushIsNoop(self):
out = DmxOutput(L9['output/udmx/'], 3)
out.flush()
diff --git a/light9/io/__init__.py b/light9/io/__init__.py
--- a/light9/io/__init__.py
+++ b/light9/io/__init__.py
@@ -1,7 +1,7 @@
from __future__ import division
import sys
-class BaseIO:
+class BaseIO(object):
def __init__(self):
self.dummy=1
self.__name__ = 'BaseIO'
@@ -83,57 +83,3 @@ class UsbDMX(BaseIO):
for lev in levels]) + "\x55"
self._dmx().write(packet)
-class SerialPots(BaseIO):
- """
- this is a dummy object (that returns zeros forever) until you call startup()
- which makes it bind to the port, etc
-
- """
- def __init__(self):
- # no init here- call getport() to actually initialize
- self.dummy=1
- self.__name__='SerialPots' # i thought this was automatic!
-
- def golive(self):
- """
- ls -l /dev/i2c-0
- crw-rw-rw- 1 root root 89, 0 Jul 11 12:27 /dev/i2c-0
- """
- import serport
- self.serport = serport
-
- self.f = open("/dev/i2c-0","rw")
-
- # this is for a chip with A0,A1,A2 lines all low:
- port = 72
-
- from fcntl import *
-
- I2C_SLAVE = 0x0703 #/* Change slave address */
- ioctl(self.f,I2C_SLAVE,port)
- self.dummy=0
-
- def godummy(self):
- BaseIO.godummy(self)
- self.f.close()
-
- def getlevels(self):
- if self.dummy:
- return (0,0,0,0)
- else:
- return self.serport.read_all_adc(self.f.fileno())
-
-
-if __name__=='__main__':
-
- """ tester program that just dumps levels for a while """
- from time import sleep
- from serport import *
-
- i=0
- while i<100:
- sleep(.033)
- i=i+1
-
- print read_all_adc(f.fileno())
-
diff --git a/light9/rdfdb/graphfile_test.py b/light9/rdfdb/graphfile_test.py
--- a/light9/rdfdb/graphfile_test.py
+++ b/light9/rdfdb/graphfile_test.py
@@ -27,6 +27,8 @@ class TestGraphFileOutput(unittest.TestC
gf.flush()
wroteContent = open(tf.name).read()
self.assertEqual('''@prefix : .
+@prefix dev: .
+@prefix effect: .
@prefix n: .
@prefix rdf: .
@prefix rdfs: .
@@ -34,6 +36,4 @@ class TestGraphFileOutput(unittest.TestC
@prefix xsd: .
:boo n:two .
-
''', wroteContent)
-
diff --git a/makefile b/makefile
--- a/makefile
+++ b/makefile
@@ -102,3 +102,11 @@ effect_node_setup: create_virtualenv pac
coffee:
zsh -c 'coffee -cw light9/web/{.,live,timeline}/*.coffee'
+
+env-mypy/bin/mypy:
+ mkdir -p env-mypy
+ virtualenv -p /usr/bin/python3 env-mypy/
+ env-mypy/bin/pip install mypy==0.501 lxml==3.7.3
+
+mypy-collector: env-mypy/bin/mypy
+ env-mypy/bin/mypy --py2 --ignore-missing-imports --strict-optional --custom-typeshed-dir stubs --html-report /tmp/rep bin/collector light9/collector/*.py
diff --git a/requirements.txt b/requirements.txt
--- a/requirements.txt
+++ b/requirements.txt
@@ -34,3 +34,5 @@ klein==15.3.1
git+http://github.com/drewp/scales.git@448d59fb491b7631877528e7695a93553bfaaa93#egg=scales
colormath==2.1.1
noise==1.2.2
+
+typing==3.6.1