Changeset - 5ff9d9e264bb
[Not reviewed]
default
0 3 0
drewp@bigasterisk.com - 12 years ago 2012-09-29 21:00:34
drewp@bigasterisk.com
Patch.fromDiff and its usage in graphfile changes
Ignore-this: 650eaba5a71a5e838d14af5110b04e34
3 files changed with 111 insertions and 13 deletions:
0 comments (0 inline, 0 general)
light9/rdfdb/graphfile.py
Show inline comments
 
import logging, traceback
 
from twisted.python.filepath import FilePath
 
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()
 

	
 
class GraphFile(object):
 
    """
 
    one rdf file that we read from, write to, and notice external changes to
 
    """
 
    def __init__(self, notifier, path, uri, patch, getSubgraph):
 
        """
 
        this does not include an initial reread() call
 
        """
 
        self.path, self.uri = path, uri
 
@@ -31,26 +32,57 @@ class GraphFile(object):
 

	
 
    def reread(self):
 
        """update the graph with any diffs from this file"""
 
        old = self.getSubgraph(self.uri)
 
        new = Graph()
 
        try:
 
            new.parse(location=self.path, format='n3')
 
        except SyntaxError as e:
 
            print e
 
            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)
 

	
 
    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
 

	
 
        print "file dels"
 
        for s  in dels:
 
            print s
 
        print "file adds"
 
        for s in adds:
 
            print s
 
        print ""
 
        """
 
        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
 
        
 
        if adds or dels:
 
            self.patch(Patch(addQuads=adds, delQuads=dels))
 
        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)
 
        
light9/rdfdb/patch.py
Show inline comments
 
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)
 

	
 
class Patch(object):
 
    """
 
    immutable
 
    
 
    the json representation includes the {"patch":...} wrapper
 
    """
 
    def __init__(self, jsonRepr=None, addQuads=None, delQuads=None,
 
                 addGraph=None, delGraph=None):
 
@@ -19,24 +19,42 @@ class Patch(object):
 
        """
 
        self._jsonRepr = jsonRepr
 
        self._addQuads, self._delQuads = addQuads, delQuads
 
        self._addGraph, self._delGraph = addGraph, delGraph
 

	
 
        if self._jsonRepr is not None:
 
            body = json.loads(self._jsonRepr)
 
            self._delGraph = graphFromNQuad(body['patch']['deletes'])
 
            self._addGraph = graphFromNQuad(body['patch']['adds'])
 
            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:
 
            if self._addGraph is not None:
 
                self._addQuads = list(self._addGraph.quads(ALLSTMTS))
 
            else:
 
                raise
 
        return self._addQuads
 

	
 
    @property
 
    def delQuads(self):
 
        if self._delQuads is None:
 
@@ -88,12 +106,53 @@ class Patch(object):
 
        for p2 in more:
 
            for q in p2.delQuads:
 
                if q in adds:
 
                    adds.remove(q)
 
                else:
 
                    dels.add(q)
 
            for q in p2.addQuads:
 
                if q in dels:
 
                    dels.remove(q)
 
                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])
 
        
 
        
light9/rdfdb/rdflibpatch.py
Show inline comments
 
@@ -58,40 +58,47 @@ def graphFromNQuad(text):
 

	
 
from rdflib.plugins.serializers.nt import _xmlcharref_encode
 
def serializeQuad(g):
 
    """replacement for graph.serialize(format='nquads')"""
 
    out = ""
 
    for s,p,o,c in g.quads((None,None,None)):
 
        out += u"%s %s %s %s .\n" % (s.n3(),
 
                                p.n3(),
 
                                _xmlcharref_encode(o.n3()), 
 
                                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 = '<http://example.com/> <http://example.com/> <http://example.com/> <http://example.com/> .\n'
 
    def testSerializes(self):
 
        n = U("http://example.com/")
 
        g = graphFromQuads([(n,n,n,n)])
 
        out = serializeQuad(g)
 
        self.assertEqual(out.strip(), self.nqOut.strip())
 

	
 
    def testNquadParserSerializes(self):
 
        g = graphFromNQuad(self.nqOut)
 
        self.assertEqual(len(g), 1)
 
        out = serializeQuad(g)
 
        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):
 
    def testAddsToNewContext(self):
 
        g = ConjunctiveGraph()
 
        patchQuads(g, [], [stmt1])
 
        self.assert_(len(g), 1)
 
        quads = list(g.quads((None,None,None)))
 
        self.assertEqual(quads, [stmt1])
 

	
 
    def testDeletes(self):
 
        g = ConjunctiveGraph()
0 comments (0 inline, 0 general)