view rdfdb/grapheditapi.py @ 86:5b6e90a708ce

some weak file_vs_uri coverage
author drewp@bigasterisk.com
date Mon, 04 Apr 2022 23:00:42 -0700
parents 22c9679dbf67
children 1120c6489888
line wrap: on
line source

import logging
import random
from itertools import chain

from rdflib import RDF, URIRef
from rdflib.term import Node

from rdfdb.patch import Patch, quadsWithContextUris

log = logging.getLogger('graphedit')


class GraphEditApi(object):
    """
    fancier graph edits
    
    mixin for SyncedGraph, separated here because these methods work together
    """

    def getObjectPatch(self, context, subject, predicate, newObject):
        """send a patch which removes existing values for (s,p,*,c)
        and adds (s,p,newObject,c). Values in other graphs are not affected.

        newObject can be None, which will remove all (subj,pred,*) statements.
        """

        existing = []
        for spoc in quadsWithContextUris(self._graph.quads((subject, predicate, None, context))):
            existing.append(spoc)
        toAdd = ([(subject, predicate, newObject, context)] if newObject is not None else [])
        return Patch(delQuads=existing, addQuads=toAdd).simplify()

    def patchObject(self, context: URIRef, subject: Node, predicate: URIRef, newObject: Node):
        p = self.getObjectPatch(context, subject, predicate, newObject)
        if not p.isNoop():
            log.debug("patchObject %r" % p.jsonRepr)
        self.patch(p)  # type: ignore

    def patchSubgraph(self, context, newGraph):
        """
        replace all statements in 'context' with the quads in newGraph.
        This is not cooperating with currentState.
        """
        old = set(quadsWithContextUris(self._graph.quads((None, None, None, context))))
        new = set(quadsWithContextUris(newGraph))
        p = Patch(delQuads=old - new, addQuads=new - old)
        self.patch(p)
        return p  # for debugging

    def patchMapping(self, context, subject, predicate, nodeClass, keyPred, valuePred, newKey, newValue):
        """
        creates/updates a structure like this:

           ?subject ?predicate [
             a ?nodeClass;
             ?keyPred ?newKey;
             ?valuePred ?newValue ] .

        There should be a complementary readMapping that gets you a
        value since that's tricky too
        """

        # as long as currentState is expensive and has the
        # tripleFilter optimization, this looks like a mess. If
        # currentState became cheap, a lot of code here could go away.

        with self.currentState(tripleFilter=(subject, predicate, None)) as current:
            adds = set([])
            for setting in current.objects(subject, predicate):
                with self.currentState(tripleFilter=(setting, keyPred, None)) as current2:

                    match = current2.value(setting, keyPred) == newKey
                if match:
                    break
            else:
                setting = URIRef(subject + "/map/%s" % random.randrange(999999999))
                adds.update([
                    (subject, predicate, setting, context),
                    (setting, RDF.type, nodeClass, context),
                    (setting, keyPred, newKey, context),
                ])

        with self.currentState(tripleFilter=(setting, valuePred, None)) as current:
            dels = set([])
            for prev in current.objects(setting, valuePred):
                dels.add((setting, valuePred, prev, context))
            adds.add((setting, valuePred, newValue, context))

            if adds != dels:
                self.patch(Patch(delQuads=dels, addQuads=adds))

    def removeMappingNode(self, context, node):
        """
        removes the statements with this node as subject or object, which
        is the right amount of statements to remove a node that
        patchMapping made.
        """
        p = Patch(delQuads=[
            spo + (context,)
            for spo in chain(self._graph.triples((None, None, node), context=context), self._graph.triples((node, None, None), context=context))
        ])
        self.patch(p)


import unittest

from rdflib import ConjunctiveGraph


class TestPatchSubgraph(unittest.TestCase):

    def testCollapsesIdenticalQuads(self):
        appliedPatches = []

        class Obj(GraphEditApi):

            def patch(self, p):
                appliedPatches.append(p)

            _graph: ConjunctiveGraph

        obj = Obj()
        obj._graph = ConjunctiveGraph()
        stmt1 = (URIRef('s'), URIRef('p'), URIRef('o'), URIRef('g'))
        obj._graph.addN([stmt1])
        obj.patchSubgraph(URIRef('g'), [stmt1])
        self.assertEqual(len(appliedPatches), 1)
        p = appliedPatches[0]
        self.assertTrue(p.isNoop())
        self.assertEqual(p.jsonRepr, '{"patch": {"adds": "", "deletes": ""}}')