diff --git a/bin/keyboardcomposer b/bin/keyboardcomposer --- a/bin/keyboardcomposer +++ b/bin/keyboardcomposer @@ -4,7 +4,7 @@ from __future__ import division, nested_ import cgi, os, sys, time, subprocess, logging from optparse import OptionParser import webcolors, colorsys - +from louie import dispatcher from twisted.internet import reactor, tksupport from twisted.web import xmlrpc, server, resource from Tix import * @@ -19,6 +19,8 @@ from light9 import dmxclient, showconfig from light9.uihelpers import toplevelat, bindkeys from light9.namespaces import L9 from light9.tkdnd import initTkdnd, dragSourceRegister +from light9.rdfdb.syncedgraph import SyncedGraph + from bcf2000 import BCF2000 nudge_keys = { @@ -55,6 +57,7 @@ class SubScale(Scale, Fadable): class SubmasterTk(Frame): def __init__(self, master, sub, current_level): + self.sub = sub bg = sub.graph.value(sub.uri, L9.color, default='#000000') rgb = webcolors.hex_to_rgb(bg) hsv = colorsys.rgb_to_hsv(*[x/255 for x in rgb]) @@ -65,28 +68,34 @@ class SubmasterTk(Frame): self.slider_var = DoubleVar() self.slider_var.set(current_level) self.scale = SubScale(self, variable=self.slider_var, width=20) - namelabel = Label(self, text=sub.name, font="Arial 7", bg=darkBg, + + self.namelabel = Label(self, font="Arial 7", bg=darkBg, fg='white', pady=0) - namelabel.pack(side=TOP) + self.sub.graph.addHandler(self.updateName) + + self.namelabel.pack(side=TOP) levellabel = Label(self, textvariable=self.slider_var, font="Arial 7", bg='black', fg='white', pady=0) levellabel.pack(side=TOP) self.scale.pack(side=BOTTOM, expand=1, fill=BOTH) bindkeys(self, "", self.launch_subcomposer) - for w in [self, namelabel, levellabel]: + for w in [self, self.namelabel, levellabel]: dragSourceRegister(w, 'copy', 'text/uri-list', sub.uri) + def updateName(self): + self.namelabel.config(text=self.sub.graph.label(self.sub.uri)) + def launch_subcomposer(self, *args): subprocess.Popen(["bin/subcomposer", "--no-geometry", self.name]) class KeyboardComposer(Frame, SubClient): - def __init__(self, root, graph, submasters, current_sub_levels=None, + def __init__(self, root, graph, current_sub_levels=None, hw_sliders=True): Frame.__init__(self, root, bg='black') SubClient.__init__(self) self.graph = graph - self.submasters = submasters + self.submasters = Submasters(graph) self.name_to_subtk = {} self.current_sub_levels = {} self.current_row = 0 @@ -101,21 +110,14 @@ class KeyboardComposer(Frame, SubClient) self.use_hw_sliders = hw_sliders self.connect_to_hw(hw_sliders) - self.draw_ui() + + self.make_key_hints() + self.make_buttons() + + self.graph.addHandler(self.redraw_sliders) self.send_levels_loop() - def draw_ui(self): - self.rows = [] # this holds Tk Frames for each row - self.slider_vars = {} # this holds subname:sub Tk vars - self.slider_table = {} # this holds coords:sub Tk vars - self.name_to_subtk.clear() # subname : SubmasterTk instance - - self.make_key_hints() - self.draw_sliders() - if len(self.rows): - self.change_row(self.current_row) - self.rows[self.current_row].focus() - + def make_buttons(self): self.buttonframe = Frame(self, bg='black') self.buttonframe.pack(side=BOTTOM) @@ -131,10 +133,6 @@ class KeyboardComposer(Frame, SubClient) command=self.alltozero, bg='black', fg='white') self.alltozerobutton.pack(side='left') - self.refreshbutton = Button(self.buttonframe, text="Refresh", - command=self.refresh, bg='black', fg='white') - self.refreshbutton.pack(side=LEFT) - self.save_stage_button = Button(self.buttonframe, text="Save", command=lambda: self.save_current_stage(self.sub_name.get()), bg='black', fg='white') @@ -142,18 +140,53 @@ class KeyboardComposer(Frame, SubClient) self.sub_name = Entry(self.buttonframe, bg='black', fg='white') self.sub_name.pack(side=LEFT) + + def redraw_sliders(self): + self.slider_vars = {} # this holds subname:sub Tk vars + self.slider_table = {} # this holds coords:sub Tk vars + self.name_to_subtk.clear() # subname : SubmasterTk instance + + self.graph.addHandler(self.draw_sliders) + if len(self.rows): + self.change_row(self.current_row) + self.rows[self.current_row].focus() + self.stop_frequent_update_time = 0 - + + def onNewSub(self, sub): + log.info("new %s", sub) + self.graph.addHandler(self.draw_sliders) + + def onLostSub(self, subUri): + log.info("lost %s", subUri) + self.graph.addHandler(self.draw_sliders) + def draw_sliders(self): + + + if hasattr(self, 'rows'): + for r in self.rows: + r.destroy() + self.rows = [] # this holds Tk Frames for each row + + self.tk_focusFollowsMouse() rowcount = -1 col = 0 last_group = None + + # there are unlikely to be any subs at startup because we + # probably haven't been called back with the graph data yet + + #read get_all_subs then watch 'new submaster' 'lost submaster' signals withgroups = sorted((self.graph.value(sub.uri, L9['group']), self.graph.value(sub.uri, L9['order']), sub) for sub in self.submasters.get_all_subs()) + dispatcher.connect(self.onNewSub, "new submaster") + dispatcher.connect(self.onLostSub, "lost submaster") + log.info("withgroups %s", withgroups) for group, order, sub in withgroups: group = self.graph.value(sub.uri, L9['group']) @@ -203,7 +236,7 @@ class KeyboardComposer(Frame, SubClient) try: self.sliders = Sliders(self) except IOError: - print "Couldn't actually find any sliders (but really, it's no problem)" + log.info("no hardware sliders") self.sliders = DummySliders() self.use_hw_sliders = False else: @@ -256,6 +289,7 @@ class KeyboardComposer(Frame, SubClient) if event.keysym in ('Prior', 'p', 'bracketright'): diff = -1 self.change_row(self.current_row + diff) + def change_row(self, row): old_row = self.current_row self.current_row = row @@ -328,20 +362,24 @@ class KeyboardComposer(Frame, SubClient) def highlight_row(self, row): row = self.rows[row] row['bg'] = 'red' + def unhighlight_row(self, row): row = self.rows[row] row['bg'] = 'black' + def get_levels(self): return dict([(name, slidervar.get()) for name, slidervar in self.slider_vars.items()]) + def get_levels_as_sub(self): scaledsubs = [self.submasters.get_sub_by_name(sub) * level \ for sub, level in self.get_levels().items() if level > 0.0] maxes = sub_maxes(*scaledsubs) return maxes + def save_current_stage(self, subname): - print "saving current levels as", subname + log.info("saving current levels as %s", subname) sub = self.get_levels_as_sub() sub.name = subname sub.temporary = 0 @@ -356,20 +394,6 @@ class KeyboardComposer(Frame, SubClient) self.send_levels() self.after(10, self.send_frequent_updates) - def refresh(self): - self.save() - graph = showconfig.getGraph() - self.submasters = Submasters(graph) - self.current_sub_levels, self.current_row = \ - pickle.load(file('.keyboardcomposer.savedlevels')) - for r in self.rows: - r.destroy() - self.keyhints.destroy() - self.buttonframe.destroy() - self.draw_ui() - # possibly paranoia (but possibly not) - self.change_row(self.current_row) - def alltozero(self): for name, subtk in self.name_to_subtk.items(): if subtk.scale.scale_var.get() != 0: @@ -463,10 +487,9 @@ if __name__ == "__main__": opts, args = parser.parse_args() logging.basicConfig(level=logging.INFO if opts.v else logging.WARN) - log = logging.getLogger() + log = logging.getLogger('keyboardcomposer') - graph = showconfig.getGraph() - s = Submasters(graph) + graph = SyncedGraph("keyboardcomposer") root = Tk() initTkdnd(root.tk, 'tkdnd/trunk/') @@ -476,7 +499,7 @@ if __name__ == "__main__": startLevels = None if opts.nonpersistent: startLevels = {} - kc = KeyboardComposer(tl, graph, s, startLevels, + kc = KeyboardComposer(tl, graph, startLevels, hw_sliders=not opts.no_sliders) kc.pack(fill=BOTH, expand=1) @@ -489,8 +512,8 @@ if __name__ == "__main__": reactor.listenTCP(networking.keyboardComposer.port, server.Site(LevelServerHttp(kc.name_to_subtk))) except twisted.internet.error.CannotListenError, e: - print "Can't (and won't!) start level server:" - print e + log.warn("Can't (and won't!) start level server:") + log.warn(e) root.protocol('WM_DELETE_WINDOW', reactor.stop) if not opts.nonpersistent: @@ -500,5 +523,5 @@ if __name__ == "__main__": # prof.watchPoint("/usr/lib/python2.4/site-packages/rdflib-2.3.3-py2.4-linux-i686.egg/rdflib/Graph.py", 615) - + prof.run(reactor.run, profile=False) diff --git a/bin/rdfdb b/bin/rdfdb --- a/bin/rdfdb +++ b/bin/rdfdb @@ -70,7 +70,9 @@ Our web ui: registered clients recent edits, each one says what client it came from. You can reverse -them here. +them here. We should be able to take patches that are close in time +and keep updating the same data (e.g. a stream of changes as the user +drags a slider) and collapse them into a single edit for clarity. """ from twisted.internet import reactor @@ -131,7 +133,9 @@ class Db(object): for inFile in [#"show/dance2012/config.n3", "show/dance2012/subs/bcools", - #"demo.n3", + "show/dance2012/subs/bwarm", + "show/dance2012/subs/house", + "demo.n3", ]: self.g = GraphFile(notifier, inFile, diff --git a/light9/Submaster.py b/light9/Submaster.py --- a/light9/Submaster.py +++ b/light9/Submaster.py @@ -1,100 +1,166 @@ from __future__ import division import os, logging, time -from rdflib import Graph +from rdflib import Graph, RDF from rdflib import RDFS, Literal, BNode from light9.namespaces import L9, XSD from light9.TLUtility import dict_scale, dict_max from light9 import Patch, showconfig -try: - import dispatch.dispatcher as dispatcher -except ImportError: - from louie import dispatcher -log = logging.getLogger() +from louie import dispatcher +log = logging.getLogger('submaster') -class Submaster: +class Submaster(object): "Contain a dictionary of levels, but you didn't need to know that" - def __init__(self, - name=None, - graph=None, sub=None, - leveldict=None, temporary=False): - """sub is the URI for this submaster, graph is a graph where - we can learn about the sub. If graph is not provided, we look - in a file named name. + def __init__(self, name, levels): + """this sub has a name just for debugging. It doesn't get persisted. + See PersistentSubmaster. + + levels is a dict + """ + self.name = name + self.levels = levels + + self.temporary = True + + if not self.temporary: + # obsolete + dispatcher.connect(log.error, 'reload all subs') + + log.debug("%s initial levels %s", self.name, self.levels) + + def _editedLevels(self): + pass + + def set_level(self, channelname, level, save=True): + self.levels[Patch.resolve_name(channelname)] = level + self._editedLevels() + + def set_all_levels(self, leveldict): + self.levels.clear() + for k, v in leveldict.items(): + # this may call _editedLevels too many times + self.set_level(k, v, save=0) - name is the filename where we can load a graph about this URI - (see showconfig.subFile) + def get_levels(self): + return self.levels + + def no_nonzero(self): + return (not self.levels.values()) or not (max(self.levels.values()) > 0) + + def __mul__(self, scalar): + return Submaster("%s*%s" % (self.name, scalar), + levels=dict_scale(self.levels, scalar)) + __rmul__ = __mul__ + def max(self, *othersubs): + return sub_maxes(self, *othersubs) + + def __add__(self, other): + return self.max(other) - passing name alone makes a new empty sub + def ident(self): + return (self.name, tuple(sorted(self.levels.items()))) - temporary means the sub won't get saved or loaded + def __repr__(self): + items = getattr(self, 'levels', {}).items() + items.sort() + levels = ' '.join(["%s:%.2f" % item for item in items]) + return "<'%s': [%s]>" % (getattr(self, 'name', 'no name yet'), levels) + + def __cmp__(self, other): + # not sure how useful this is + return cmp(self.ident(), other.ident()) + + def __hash__(self): + return hash(self.ident()) + + def get_dmx_list(self): + leveldict = self.get_levels() # gets levels of sub contents + levels = [] + for k, v in leveldict.items(): + if v == 0: + continue + try: + dmxchan = Patch.get_dmx_channel(k) - 1 + except ValueError: + log.error("error trying to compute dmx levels for submaster %s" % self.name) + raise + if dmxchan >= len(levels): + levels.extend([0] * (dmxchan - len(levels) + 1)) + levels[dmxchan] = max(v, levels[dmxchan]) - pass: - name, temporary=True - no rdf involved - sub, filename - read sub URI from graph at filename - - name - new sub - sub - n - name, sub - new + return levels + + def normalize_patch_names(self): + """Use only the primary patch names.""" + # possibly busted -- don't use unless you know what you're doing + self.set_all_levels(self.levels.copy()) + + def get_normalized_copy(self): + """Get a copy of this sumbaster that only uses the primary patch + names. The levels will be the same.""" + newsub = Submaster("%s (normalized)" % self.name, {}) + newsub.set_all_levels(self.levels) + return newsub + + def crossfade(self, othersub, amount): + """Returns a new sub that is a crossfade between this sub and + another submaster. - """ - if name is sub is leveldict is None: - raise TypeError("more args are needed") - if sub is not None and name is None: - name = graph.label(sub) - if graph is not None: - # old code was passing leveldict as second positional arg - assert isinstance(graph, Graph) - self.name = name - self.uri = sub + NOTE: You should only crossfade between normalized submasters.""" + otherlevels = othersub.get_levels() + keys_set = {} + for k in self.levels.keys() + otherlevels.keys(): + keys_set[k] = 1 + all_keys = keys_set.keys() + + xfaded_sub = Submaster("xfade", {}) + for k in all_keys: + xfaded_sub.set_level(k, + linear_fade(self.levels.get(k, 0), + otherlevels.get(k, 0), + amount)) + + return xfaded_sub + +class PersistentSubmaster(Submaster): + def __init__(self, graph, uri): + self.graph, self.uri = graph, uri + self.graph.addHandler(self.setName) + self.graph.addHandler(self.setLevels) + Submaster.__init__(self, self.name, self.levels) self.graph = graph - self.temporary = temporary - if leveldict: - self.levels = leveldict + self.uri = uri + self.temporary = False + + def ident(self): + return self.uri + + def _editedLevels(self): + self.save() + + def setName(self): + log.info("sub update name %s %s", self.uri, self.graph.label(self.uri)) + self.name = self.graph.label(self.uri) + + def setLevels(self): + log.info("sub update levels") + oldLevels = getattr(self, 'levels', {}).copy() + self.setLevelsFromGraph() + if oldLevels != self.levels: + log.info("sub %s changed" % self.name) + + def setLevelsFromGraph(self): + patchGraph = showconfig.getGraph() # going away + if hasattr(self, 'levels'): + self.levels.clear() else: self.levels = {} - self.reload(quiet=True, graph=graph) - if not self.temporary: - dispatcher.connect(self.reload, 'reload all subs') - log.debug("%s initial levels %s", self.name, self.levels) - - def reload(self, quiet=False, graph=None): - if self.temporary: - return - try: - oldlevels = self.levels.copy() - self.levels.clear() - patchGraph = showconfig.getGraph() - if 1 or graph is None: - # need to read the sub graph to build the levels, not - # use the main one! The sub graphs will eventually - # just be part of the one and only shared graph - graph = Graph() - if not self.name: - # anon sub, maybe for a chase - pass - else: - inFile = showconfig.subFile(self.name) - - t1 = time.time() - graph.parse(inFile, format="n3") - log.info("reading %s in %.1fms", inFile, 1000 * (time.time() - t1)) - - self.setLevelsFromGraph(graph, patchGraph) - - if (not quiet) and (oldlevels != self.levels): - log.info("sub %s changed" % self.name) - except IOError, e: - log.error("Can't read file for sub: %r (%s)" % (self.name, e)) - - def setLevelsFromGraph(self, graph, patchGraph): - self.uri = L9['sub/%s' % self.name] - for lev in graph.objects(self.uri, L9['lightLevel']): - chan = graph.value(lev, L9['channel']) - val = graph.value(lev, L9['level']) + for lev in self.graph.objects(self.uri, L9['lightLevel']): + chan = self.graph.value(lev, L9['channel']) + val = self.graph.value(lev, L9['level']) name = patchGraph.label(chan) if not name: - log.error("sub %r has channel %r with no name- leaving out that channel" % (self.name, chan)) + #log.error("sub %r has channel %r with no name- leaving out that channel" % (self.name, chan)) continue self.levels[name] = float(val) @@ -120,87 +186,6 @@ class Submaster: graph.serialize(showconfig.subFile(self.name), format="nt") - def set_level(self, channelname, level, save=True): - self.levels[Patch.resolve_name(channelname)] = level - if save: - self.save() - def set_all_levels(self, leveldict): - self.levels.clear() - for k, v in leveldict.items(): - self.set_level(k, v, save=0) - self.save() - def get_levels(self): - return self.levels - def no_nonzero(self): - return (not self.levels.values()) or not (max(self.levels.values()) > 0) - def __mul__(self, scalar): - return Submaster("%s*%s" % (self.name, scalar), - leveldict=dict_scale(self.levels, scalar), - temporary=True) - __rmul__ = __mul__ - def max(self, *othersubs): - return sub_maxes(self, *othersubs) - - def __add__(self, other): - return self.max(other) - - def __repr__(self): - items = self.levels.items() - items.sort() - levels = ' '.join(["%s:%.2f" % item for item in items]) - return "<'%s': [%s]>" % (self.name, levels) - def get_dmx_list(self): - leveldict = self.get_levels() # gets levels of sub contents - - levels = [] - for k, v in leveldict.items(): - if v == 0: - continue - try: - dmxchan = Patch.get_dmx_channel(k) - 1 - except ValueError: - log.error("error trying to compute dmx levels for submaster %s" % self.name) - raise - if dmxchan >= len(levels): - levels.extend([0] * (dmxchan - len(levels) + 1)) - levels[dmxchan] = max(v, levels[dmxchan]) - - return levels - def normalize_patch_names(self): - """Use only the primary patch names.""" - # possibly busted -- don't use unless you know what you're doing - self.set_all_levels(self.levels.copy()) - def get_normalized_copy(self): - """Get a copy of this sumbaster that only uses the primary patch - names. The levels will be the same.""" - newsub = Submaster("%s (normalized)" % self.name, temporary=1) - newsub.set_all_levels(self.levels) - return newsub - def crossfade(self, othersub, amount): - """Returns a new sub that is a crossfade between this sub and - another submaster. - - NOTE: You should only crossfade between normalized submasters.""" - otherlevels = othersub.get_levels() - keys_set = {} - for k in self.levels.keys() + otherlevels.keys(): - keys_set[k] = 1 - all_keys = keys_set.keys() - - xfaded_sub = Submaster("xfade", temporary=1) - for k in all_keys: - xfaded_sub.set_level(k, - linear_fade(self.levels.get(k, 0), - otherlevels.get(k, 0), - amount)) - - return xfaded_sub - def __cmp__(self, other): - """Compare by sub repr (name, hopefully)""" - return cmp(repr(self), repr(other)) - def __hash__(self): - raise NotImplementedError - return hash(repr(self)) def linear_fade(start, end, amount): """Fades between two floats by an amount. amount is a float between @@ -213,8 +198,7 @@ def sub_maxes(*subs): nonzero_subs = [s for s in subs if not s.no_nonzero()] name = "max(%s)" % ", ".join([repr(s) for s in nonzero_subs]) return Submaster(name, - leveldict=dict_max(*[sub.levels for sub in nonzero_subs]), - temporary=1) + levels=dict_max(*[sub.levels for sub in nonzero_subs])) def combine_subdict(subdict, name=None, permanent=False): """A subdict is { Submaster objects : levels }. We combine all @@ -236,17 +220,24 @@ class Submasters: "Collection o' Submaster objects" def __init__(self, graph): self.submasters = {} + self.graph = graph + + graph.addHandler(self.findSubs) - files = os.listdir(showconfig.subsDir()) - t1 = time.time() - for filename in files: - # we don't want these files - if filename.startswith('.') or filename.endswith('~') or \ - filename.startswith('CVS'): - continue - self.submasters[filename] = Submaster(filename, graph=graph) - log.info("loaded all submasters in %.1fms" % ((time.time() - t1) * 1000)) - + def findSubs(self): + current = set() + + for s in self.graph.subjects(RDF.type, L9['Submaster']): + log.info("found sub %s", s) + if s not in self.submasters: + sub = self.submasters[s] = PersistentSubmaster(self.graph, s) + dispatcher.send("new submaster", sub=sub) + current.add(s) + for s in set(self.submasters.keys()) - current: + del self.submasters[s] + dispatcher.send("lost submaster", subUri=s) + log.info("findSubs finished %s", self.submasters) + def get_all_subs(self): "All Submaster objects" l = self.submasters.items() @@ -255,7 +246,7 @@ class Submasters: songs = [] notsongs = [] for s in l: - if s.name.startswith('song'): + if s.name and s.name.startswith('song'): songs.append(s) else: notsongs.append(s) diff --git a/light9/rdfdb/graphfile.py b/light9/rdfdb/graphfile.py --- a/light9/rdfdb/graphfile.py +++ b/light9/rdfdb/graphfile.py @@ -1,4 +1,4 @@ -import logging +import logging, traceback from twisted.python.filepath import FilePath from rdflib import Graph from light9.rdfdb.patch import Patch @@ -18,7 +18,10 @@ class GraphFile(object): def notify(self, notifier, filepath, mask): log.info("file %s changed" % filepath) - self.reread() + try: + self.reread() + except Exception: + traceback.print_exc() def reread(self): """update the graph with any diffs from this file""" diff --git a/light9/rdfdb/syncedgraph.py b/light9/rdfdb/syncedgraph.py --- a/light9/rdfdb/syncedgraph.py +++ b/light9/rdfdb/syncedgraph.py @@ -1,7 +1,7 @@ -from rdflib import ConjunctiveGraph, RDFS +from rdflib import ConjunctiveGraph, RDFS, RDF import logging, cyclone.httpclient, traceback, urllib from twisted.internet import reactor -log = logging.getLogger() +log = logging.getLogger('syncedgraph') from light9.rdfdb.patch import Patch, ALLSTMTS from light9.rdfdb.rdflibpatch import patchQuads @@ -44,6 +44,7 @@ class GraphWatchers(object): """ def __init__(self): self._handlersSp = {} # (s,p): set(handlers) + self._handlersPo = {} # (p,o): set(handlers) def addSubjPredWatcher(self, func, s, p): if func is None: @@ -52,19 +53,34 @@ class GraphWatchers(object): try: self._handlersSp.setdefault(key, set()).add(func) except Exception: - print "with key %r and func %r" % (key, func) + log.error("with key %r and func %r" % (key, func)) raise + def addPredObjWatcher(self, func, p, o): + self._handlersPo.setdefault((p, o), set()).add(func) + def whoCares(self, patch): - """what handler functions would care about the changes in this patch""" + """what handler functions would care about the changes in this patch? + + this removes the handlers that it gives you + """ self.dependencies() affectedSubjPreds = set([(s, p) for s, p, o, c in patch.addQuads]+ [(s, p) for s, p, o, c in patch.delQuads]) + affectedPredObjs = set([(p, o) for s, p, o, c in patch.addQuads]+ + [(p, o) for s, p, o, c in patch.delQuads]) ret = set() - for (s,p), func in self._handlersSp.iteritems(): - if (s,p) in affectedSubjPreds: - ret.update(func) + for (s, p), funcs in self._handlersSp.iteritems(): + if (s, p) in affectedSubjPreds: + ret.update(funcs) + funcs.clear() + + for (p, o), funcs in self._handlersPo.iteritems(): + if (p, o) in affectedPredObjs: + ret.update(funcs) + funcs.clear() + return ret def dependencies(self): @@ -73,7 +89,7 @@ class GraphWatchers(object): data they depend on. This is meant for showing on the web ui for browsing. """ - print "whocares" + log.info("whocares:") from pprint import pprint pprint(self._handlersSp) @@ -111,12 +127,12 @@ class SyncedGraph(object): self.updateResource = 'http://localhost:%s/update' % port log.info("listening on %s" % port) self.register(label) - self.currentFunc = None + self.currentFuncs = [] # stack of addHandler callers def register(self, label): def done(x): - print "registered", x.body + log.debug("registered with rdfdb") cyclone.httpclient.fetch( url='http://localhost:8051/graphClients', @@ -152,12 +168,12 @@ class SyncedGraph(object): # reveals all their statement fetches? Just make them always # new? Cache their results, so if i make the query again and # it gives the same result, I don't call the handler? - - self.currentFunc = func + + self.currentFuncs.append(func) try: func() finally: - self.currentFunc = None + self.currentFuncs.pop() def updateOnPatch(self, p): """ @@ -165,26 +181,52 @@ class SyncedGraph(object): might care, and then notice what data they depend on now """ for func in self._watchers.whoCares(p): - # and forget the old one! + # todo: forget the old handlers for this func self.addHandler(func) - def _assertCurrent(self): - if self.currentFunc is None: + def _getCurrentFunc(self): + if not self.currentFuncs: # this may become a warning later raise ValueError("asked for graph data outside of a handler") + # we add the watcher to the deepest function, since that + # should be the cheapest way to update when this part of the + # data changes + return self.currentFuncs[-1] + # these just call through to triples() so it might be possible to - # watch just that one - def value(self, subj, pred): - self._assertCurrent() - self._watchers.addSubjPredWatcher(self.currentFunc, subj, pred) - return self._graph.value(subj, pred) + # watch just that one. + + # if you get a bnode in your response, maybe the answer to + # dependency tracking is to say that you depended on the triple + # that got you that bnode, since it is likely to change to another + # bnode later. This won't work if the receiver stores bnodes + # between calls, but probably most of them don't do that (they + # work from a starting uri) + + def value(self, subject=None, predicate=RDF.value, object=None, + default=None, any=True): + if object is not None: + raise NotImplementedError() + func = self._getCurrentFunc() + self._watchers.addSubjPredWatcher(func, subject, predicate) + return self._graph.value(subject, predicate, object=object, + default=default, any=any) def objects(self, subject=None, predicate=None): - self._assertCurrent() - self._watchers.addSubjPredWatcher(self.currentFunc, subject, predicate) + func = self._getCurrentFunc() + self._watchers.addSubjPredWatcher(func, subject, predicate) return self._graph.objects(subject, predicate) def label(self, uri): - self._assertCurrent() return self.value(uri, RDFS.label) + + def subjects(self, predicate=None, object=None): + func = self._getCurrentFunc() + self._watchers.addPredObjWatcher(func, predicate, object) + return self._graph.subjects(predicate, object) + + # i find myself wanting 'patch' versions of these calls that tell + # you only what results have just appeared or disappeared. I think + # I'm going to be repeating that logic a lot. Maybe just for the + # subjects(RDF.type, t) call