# HG changeset patch # User drewp@bigasterisk.com # Date 2012-09-29 21:00:34 # Node ID 5ff9d9e264bb8f5021c74719b9bf578fb9232fa4 # Parent b19cd005a4916aab02a9769d2b6c451f2f29ba52 Patch.fromDiff and its usage in graphfile changes Ignore-this: 650eaba5a71a5e838d14af5110b04e34 diff --git a/light9/rdfdb/graphfile.py b/light9/rdfdb/graphfile.py --- a/light9/rdfdb/graphfile.py +++ b/light9/rdfdb/graphfile.py @@ -3,6 +3,7 @@ from twisted.python.filepath import File from twisted.internet.inotify import IN_CLOSE_WRITE, IN_MOVED_FROM from rdflib import Graph from light9.rdfdb.patch import Patch +from light9.rdfdb.rdflibpatch import inContext log = logging.getLogger() @@ -40,17 +41,48 @@ class GraphFile(object): log.error("syntax error in %s" % self.path) return - adds = [(s, p, o, self.uri) for s, p, o in new - old] - dels = [(s, p, o, self.uri) for s, p, o in old - new] + old = inContext(old, self.uri) + new = inContext(new, self.uri) + print "old %s new %s" % (old, new) + + p = Patch.fromDiff(old, new) + if p: + self.patch(p, dueToFileChange=True) - print "file dels" - for s in dels: - print s - print "file adds" - for s in adds: - print s - print "" + def dirty(self, graph): + """ + there are new contents for our file + + graph is the rdflib.Graph that contains the contents of the + file. It is allowed to change. Note that dirty() will probably + do the save later when the graph might be different. + + after a timer has passed, write it out. Any scheduling issues + between files? i don't think so. the timer might be kind of + huge, and then we might want to take a hint from a client that + it's a good time to save the files that it was editing, like + when the mouse moves out of the client's window and might be + going towards a text file editor + + """ + log.info("%s dirty, %s stmt" % (self.uri, len(graph))) + self.graphToWrite = graph + if self.writeCall: + self.writeCall.reset(self.flushDelay) + else: + self.writeCall = reactor.callLater(self.flushDelay, self.flush) + + def flush(self): + self.writeCall = None + + tmpOut = self.path + ".rdfdb-temp" + f = open(tmpOut, 'w') + t1 = time.time() + self.graphToWrite.serialize(destination=f, format='n3') + serializeTime = time.time() - t1 + f.close() + self.lastWriteTimestamp = os.path.getmtime(tmpOut) + os.rename(tmpOut, self.path) + iolog.info("rewrote %s in %.1f ms", self.path, serializeTime * 1000) - if adds or dels: - self.patch(Patch(addQuads=adds, delQuads=dels)) diff --git a/light9/rdfdb/patch.py b/light9/rdfdb/patch.py --- a/light9/rdfdb/patch.py +++ b/light9/rdfdb/patch.py @@ -1,5 +1,5 @@ import json, unittest -from rdflib import ConjunctiveGraph, URIRef +from rdflib import ConjunctiveGraph, URIRef, URIRef as U from light9.rdfdb.rdflibpatch import graphFromNQuad, graphFromQuads, serializeQuad ALLSTMTS = (None, None, None) @@ -28,6 +28,24 @@ class Patch(object): if 'senderUpdateUri' in body: self.senderUpdateUri = body['senderUpdateUri'] + @classmethod + def fromDiff(cls, oldGraph, newGraph): + """ + make a patch that changes oldGraph to newGraph + """ + old = set(oldGraph.quads(ALLSTMTS)) + new = set(newGraph.quads(ALLSTMTS)) + return cls(addQuads=list(new - old), delQuads=list(old - new)) + + def __nonzero__(self): + """ + does this patch do anything to a graph? + """ + if self._jsonRepr and self._jsonRepr.strip(): + raise NotImplementedError() + return bool(self._addQuads or self._delQuads or + self._addGraph or self._delGraph) + @property def addQuads(self): if self._addQuads is None: @@ -97,3 +115,44 @@ class Patch(object): else: adds.add(q) return Patch(delQuads=dels, addQuads=adds) + + def getContext(self): + """assumes that all the edits are on the same context""" + ctx = None + for q in self.addQuads + self.delQuads: + if ctx is None: + ctx = q[3] + + if ctx != q[3]: + raise ValueError("patch applies to multiple contexts, at least %r and %r" % (ctx, q[3])) + return ctx + +stmt1 = U('http://a'), U('http://b'), U('http://c'), U('http://ctx1') + +class TestPatchFromDiff(unittest.TestCase): + def testEmpty(self): + g = ConjunctiveGraph() + p = Patch.fromDiff(g, g) + self.assert_(not p) + + def testNonEmpty(self): + g1 = ConjunctiveGraph() + g2 = graphFromQuads([stmt1]) + p = Patch.fromDiff(g1, g2) + self.assert_(p) + + def testNoticesAdds(self): + g1 = ConjunctiveGraph() + g2 = graphFromQuads([stmt1]) + p = Patch.fromDiff(g1, g2) + self.assertEqual(p.addQuads, [stmt1]) + self.assertEqual(p.delQuads, []) + + def testNoticesDels(self): + g1 = graphFromQuads([stmt1]) + g2 = ConjunctiveGraph() + p = Patch.fromDiff(g1, g2) + self.assertEqual(p.addQuads, []) + self.assertEqual(p.delQuads, [stmt1]) + + diff --git a/light9/rdfdb/rdflibpatch.py b/light9/rdfdb/rdflibpatch.py --- a/light9/rdfdb/rdflibpatch.py +++ b/light9/rdfdb/rdflibpatch.py @@ -67,6 +67,14 @@ def serializeQuad(g): c.n3()) return out +def inContext(graph, newContext): + """ + make a ConjunctiveGraph where all the triples in the given graph + are in newContext + """ + return graphFromQuads([(s,p,o,newContext) for s,p,o in graph]) + + class TestGraphFromQuads(unittest.TestCase): nqOut = ' .\n' def testSerializes(self): @@ -82,7 +90,6 @@ class TestGraphFromQuads(unittest.TestCa self.assertEqual(out.strip(), self.nqOut.strip()) - stmt1 = U('http://a'), U('http://b'), U('http://c'), U('http://ctx1') stmt2 = U('http://a'), U('http://b'), U('http://c'), U('http://ctx2') class TestPatchQuads(unittest.TestCase):