Bläddra i källkod

Add graph lookup tests; fix transactions in tests.

Stefano Cossu 5 år sedan
förälder
incheckning
bbcf66da2f
3 ändrade filer med 326 tillägg och 93 borttagningar
  1. 30 30
      lakesuperior/api/resource.py
  2. 32 3
      lakesuperior/model/rdf/graph.pyx
  3. 264 60
      tests/0_data_structures/test_graph.py

+ 30 - 30
lakesuperior/api/resource.py

@@ -23,36 +23,36 @@ logger = logging.getLogger(__name__)
 __doc__ = """
 Primary API for resource manipulation.
 
-Quickstart:
-
->>> # First import default configuration and globals—only done once.
->>> import lakesuperior.default_env
->>> from lakesuperior.api import resource
->>> # Get root resource.
->>> rsrc = resource.get('/')
->>> # Dump graph.
->>> set(rsrc.imr)
-{(rdflib.term.URIRef('info:fcres/'),
-  rdflib.term.URIRef('http://purl.org/dc/terms/title'),
-  rdflib.term.Literal('Repository Root')),
- (rdflib.term.URIRef('info:fcres/'),
-  rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
-  rdflib.term.URIRef('http://fedora.info/definitions/v4/repository#Container')),
- (rdflib.term.URIRef('info:fcres/'),
-  rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
-  rdflib.term.URIRef('http://fedora.info/definitions/v4/repository#RepositoryRoot')),
- (rdflib.term.URIRef('info:fcres/'),
-  rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
-  rdflib.term.URIRef('http://fedora.info/definitions/v4/repository#Resource')),
- (rdflib.term.URIRef('info:fcres/'),
-  rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
-  rdflib.term.URIRef('http://www.w3.org/ns/ldp#BasicContainer')),
- (rdflib.term.URIRef('info:fcres/'),
-  rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
-  rdflib.term.URIRef('http://www.w3.org/ns/ldp#Container')),
- (rdflib.term.URIRef('info:fcres/'),
-  rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
-  rdflib.term.URIRef('http://www.w3.org/ns/ldp#RDFSource'))}
+Quickstart::
+
+    >>> # First import default configuration and globals—only done once.
+    >>> import lakesuperior.default_env
+    >>> from lakesuperior.api import resource
+    >>> # Get root resource.
+    >>> rsrc = resource.get('/')
+    >>> # Dump graph.
+    >>> set(rsrc.imr)
+    {(rdflib.term.URIRef('info:fcres/'),
+      rdflib.term.URIRef('http://purl.org/dc/terms/title'),
+      rdflib.term.Literal('Repository Root')),
+     (rdflib.term.URIRef('info:fcres/'),
+      rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
+      rdflib.term.URIRef('http://fedora.info/definitions/v4/repository#Container')),
+     (rdflib.term.URIRef('info:fcres/'),
+      rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
+      rdflib.term.URIRef('http://fedora.info/definitions/v4/repository#RepositoryRoot')),
+     (rdflib.term.URIRef('info:fcres/'),
+      rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
+      rdflib.term.URIRef('http://fedora.info/definitions/v4/repository#Resource')),
+     (rdflib.term.URIRef('info:fcres/'),
+      rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
+      rdflib.term.URIRef('http://www.w3.org/ns/ldp#BasicContainer')),
+     (rdflib.term.URIRef('info:fcres/'),
+      rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
+      rdflib.term.URIRef('http://www.w3.org/ns/ldp#Container')),
+     (rdflib.term.URIRef('info:fcres/'),
+      rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
+      rdflib.term.URIRef('http://www.w3.org/ns/ldp#RDFSource'))}
 """
 
 def transaction(write=False):

+ 32 - 3
lakesuperior/model/rdf/graph.pyx

@@ -39,7 +39,7 @@ cdef class Graph:
 
     Every time a term is looked up or added to even a temporary graph, that
     term is added to the store and creates a key. This is because in the
-    majority of cases that term is bound to be stored permanently anyway, and
+    majority of cases that term is likely to be stored permanently anyway, and
     it's more efficient to hash it and allocate it immediately. A cleanup
     function to remove all orphaned terms (not in any triple or context index)
     can be later devised to compact the database.
@@ -54,6 +54,27 @@ cdef class Graph:
         """
         Initialize the graph, optionally from Python/RDFlib data.
 
+        When initializing a non-empty Graph, a store transaction must be
+        opened::
+
+            >>> from rdflib import URIRef
+            >>> from lakesuperior import env_setup, env
+            >>> store = env.app_globals.rdf_store
+            >>> # Or alternatively:
+            >>> # from lakesuperior.store.ldp_rs.lmdb_store import LmdbStore
+            >>> # store = LmdbStore('/tmp/test')
+            >>> trp = {(URIRef('urn:s:0'), URIRef('urn:p:0'), URIRef('urn:o:0'))}
+            >>> with store.txn_ctx():
+            >>>     gr = Graph(store, data=trp)
+
+        Similarly, any operation such as adding, changing or looking up triples
+        needs a store transaction.
+
+        Note that, even though any operation may involve adding new terms to
+        the store, a read-only transaction is sufficient. Lakesuperior will
+        open a write transaction automatically only if necessary and only for
+        the time needed to enter the new terms.
+
         :type store: lakesuperior.store.ldp_rs.lmdb_triplestore.LmdbTriplestore
         :param store: Triplestore where keys are mapped to terms. By default
             this is the default application store
@@ -114,6 +135,12 @@ cdef class Graph:
             return self.keys.capacity
 
 
+    property txn_ctx:
+        def __get__(self):
+            """ Expose underlying store's ``txn_ctx``. """
+            return self.store.txn_ctx
+
+
     ## MAGIC METHODS ##
 
     def __len__(self):
@@ -123,6 +150,7 @@ cdef class Graph:
 
     def __eq__(self, other):
         """ Equality operator between ``Graph`` instances. """
+        # TODO Use __richcmp__()
         return len(self & other) == 0
 
 
@@ -474,7 +502,8 @@ cdef class Graph:
 
             if self.keys.contains(&spok):
                 callback_fn(gr, &spok, ctx)
-                return
+
+            return
 
         if s is not None:
             k1 = self.store.to_key(s)
@@ -505,7 +534,7 @@ cdef class Graph:
         # Iterate over serialized triples.
         self.keys.seek()
         while self.keys.get_next(&spok):
-            logger.info('Verifying spok: {spok}')
+            logger.info(f'Verifying spok: {spok}')
             if cmp_fn(&spok, k1, k2):
                 callback_fn(gr, &spok, ctx)
 

+ 264 - 60
tests/0_data_structures/test_graph.py

@@ -50,8 +50,8 @@ class TestGraphInit:
         """
         Test creation of an empty graph.
         """
-        with store.txn_ctx():
-            gr = Graph(store)
+        # No transaction needed to init an empty graph.
+        gr = Graph(store)
 
         # len() should not need a DB transaction open.
         assert len(gr) == 0
@@ -61,8 +61,7 @@ class TestGraphInit:
         """
         Test creation using a Python set.
         """
-        with store.txn_ctx(True):
-            pdb.set_trace()
+        with store.txn_ctx():
             gr = Graph(store, data=set(trp))
 
             assert len(gr) == 6
@@ -72,19 +71,220 @@ class TestGraphInit:
 
 
 @pytest.mark.usefixtures('trp')
+@pytest.mark.usefixtures('store')
 class TestGraphLookup:
     """
     Test triple lookup.
-
-    TODO
     """
 
-    @pytest.mark.skip(reason='TODO')
-    def test_lookup_pattern(self, trp):
+    def test_lookup_all_unbound(self, trp, store):
+        """
+        Test lookup ? ? ? (all unbound)
+        """
+        with store.txn_ctx():
+            gr = Graph(store, data=set(trp))
+
+            flt_gr = gr.lookup((None, None, None))
+
+            assert len(flt_gr) == 6
+
+            assert trp[0] in flt_gr
+            assert trp[2] in flt_gr
+            assert trp[3] in flt_gr
+            assert trp[4] in flt_gr
+            assert trp[5] in flt_gr
+            assert trp[6] in flt_gr
+
+
+    def test_lookup_s(self, trp, store):
+        """
+        Test lookup s ? ?
+        """
+        with store.txn_ctx():
+            gr = Graph(store, data=set(trp))
+
+            flt_gr = gr.lookup((URIRef('urn:s:0'), None, None))
+
+            assert len(flt_gr) == 3
+
+            assert trp[0] in flt_gr
+            assert trp[3] in flt_gr
+            assert trp[4] in flt_gr
+
+            assert trp[2] not in flt_gr
+            assert trp[5] not in flt_gr
+            assert trp[6] not in flt_gr
+
+            # Test for empty results.
+            empty_flt_gr = gr.lookup((URIRef('urn:s:8'), None, None))
+
+            assert len(empty_flt_gr) == 0
+
+
+    def test_lookup_p(self, trp, store):
+        """
+        Test lookup ? p ?
+        """
+        with store.txn_ctx():
+            gr = Graph(store, data=set(trp))
+
+            flt_gr = gr.lookup((None, URIRef('urn:p:0'), None))
+
+            assert len(flt_gr) == 2
+
+            assert trp[0] in flt_gr
+            assert trp[2] in flt_gr
+
+            assert trp[3] not in flt_gr
+            assert trp[4] not in flt_gr
+            assert trp[5] not in flt_gr
+            assert trp[6] not in flt_gr
+
+            # Test for empty results.
+            empty_flt_gr = gr.lookup((None, URIRef('urn:p:8'), None))
+
+            assert len(empty_flt_gr) == 0
+
+
+    def test_lookup_o(self, trp, store):
+        """
+        Test lookup ? ? o
+        """
+        with store.txn_ctx():
+            gr = Graph(store, data=set(trp))
+
+            flt_gr = gr.lookup((None, None, URIRef('urn:o:1')))
+
+            assert len(flt_gr) == 2
+
+            assert trp[4] in flt_gr
+            assert trp[5] in flt_gr
+
+            assert trp[0] not in flt_gr
+            assert trp[2] not in flt_gr
+            assert trp[3] not in flt_gr
+            assert trp[6] not in flt_gr
+
+            # Test for empty results.
+            empty_flt_gr = gr.lookup((None, None, URIRef('urn:o:8')))
+
+            assert len(empty_flt_gr) == 0
+
+
+    def test_lookup_sp(self, trp, store):
+        """
+        Test lookup s p ?
+        """
+        with store.txn_ctx():
+            gr = Graph(store, data=set(trp))
+
+            flt_gr = gr.lookup((URIRef('urn:s:0'), URIRef('urn:p:1'), None))
+
+            assert len(flt_gr) == 2
+
+            assert trp[3] in flt_gr
+            assert trp[4] in flt_gr
+
+            assert trp[0] not in flt_gr
+            assert trp[2] not in flt_gr
+            assert trp[5] not in flt_gr
+            assert trp[6] not in flt_gr
+
+            # Test for empty results.
+            empty_flt_gr = gr.lookup((URIRef('urn:s:0'), URIRef('urn:p:2'), None))
+
+            assert len(empty_flt_gr) == 0
+
+
+    def test_lookup_so(self, trp, store):
+        """
+        Test lookup s ? o
+        """
+        with store.txn_ctx():
+            gr = Graph(store, data=set(trp))
+
+            flt_gr = gr.lookup((URIRef('urn:s:0'), None, URIRef('urn:o:0')))
+
+            assert len(flt_gr) == 2
+
+            assert trp[0] in flt_gr
+            assert trp[3] in flt_gr
+
+            assert trp[2] not in flt_gr
+            assert trp[4] not in flt_gr
+            assert trp[5] not in flt_gr
+            assert trp[6] not in flt_gr
+
+            # Test for empty results.
+            empty_flt_gr = gr.lookup((URIRef('urn:s:0'), None, URIRef('urn:o:2')))
+
+            assert len(empty_flt_gr) == 0
+
+
+    def test_lookup_po(self, trp, store):
+        """
+        Test lookup ? p o
+        """
+        with store.txn_ctx():
+            gr = Graph(store, data=set(trp))
+
+            flt_gr = gr.lookup((None, URIRef('urn:p:1'), URIRef('urn:o:1')))
+
+            assert len(flt_gr) == 2
+
+            assert trp[4] in flt_gr
+            assert trp[5] in flt_gr
+
+            assert trp[0] not in flt_gr
+            assert trp[2] not in flt_gr
+            assert trp[3] not in flt_gr
+            assert trp[6] not in flt_gr
+
+            # Test for empty results.
+            empty_flt_gr = gr.lookup((None, URIRef('urn:p:1'), URIRef('urn:o:2')))
+
+            assert len(empty_flt_gr) == 0
+
+
+    def test_lookup_spo(self, trp, store):
         """
-        Test lookup by basic pattern.
+        Test lookup s p o
         """
-        pass
+        with store.txn_ctx():
+            gr = Graph(store, data=set(trp))
+
+            flt_gr = gr.lookup(
+                (URIRef('urn:s:1'), URIRef('urn:p:1'), URIRef('urn:o:1'))
+            )
+
+            pdb.set_trace()
+            assert len(flt_gr) == 1
+
+            assert trp[5] in flt_gr
+
+            assert trp[0] not in flt_gr
+            assert trp[2] not in flt_gr
+            assert trp[3] not in flt_gr
+            assert trp[4] not in flt_gr
+            assert trp[6] not in flt_gr
+
+            # Test for empty results.
+            empty_flt_gr = gr.lookup(
+                (URIRef('urn:s:1'), URIRef('urn:p:1'), URIRef('urn:o:2'))
+            )
+
+            assert len(empty_flt_gr) == 0
+
+
+@pytest.mark.usefixtures('trp')
+@pytest.mark.usefixtures('store')
+class TestGraphSlicing:
+    """
+    Test triple lookup.
+    """
+    # TODO
+    pass
+
 
 
 @pytest.mark.usefixtures('trp')
@@ -92,94 +292,98 @@ class TestGraphOps:
     """
     Test various graph operations.
     """
-    def test_len(self, trp):
+    def test_len(self, trp, store):
         """
         Test the length of a graph with and without duplicates.
         """
-        gr = Graph()
-        assert len(gr) == 0
+        with store.txn_ctx():
+            gr = Graph(store)
+            assert len(gr) == 0
 
-        gr.add((trp[0],))
-        assert len(gr) == 1
+            gr.add((trp[0],))
+            assert len(gr) == 1
 
-        gr.add((trp[1],)) # Same values
-        assert len(gr) == 1
+            gr.add((trp[1],)) # Same values
+            assert len(gr) == 1
 
-        gr.add((trp[2],))
-        assert len(gr) == 2
+            gr.add((trp[2],))
+            assert len(gr) == 2
 
-        gr.add(trp)
-        assert len(gr) == 6
+            gr.add(trp)
+            assert len(gr) == 6
 
 
-    def test_dup(self, trp):
+    def test_dup(self, trp, store):
         """
         Test operations with duplicate triples.
         """
-        gr = Graph()
-        #import pdb; pdb.set_trace()
+        with store.txn_ctx():
+            gr = Graph(store)
 
-        gr.add((trp[0],))
-        assert trp[1] in gr
-        assert trp[2] not in gr
+            gr.add((trp[0],))
+            assert trp[1] in gr
+            assert trp[2] not in gr
 
 
-    def test_remove(self, trp):
+    def test_remove(self, trp, store):
         """
         Test adding and removing triples.
         """
-        gr = Graph()
+        with store.txn_ctx():
+            gr = Graph(store)
 
-        gr.add(trp)
-        gr.remove(trp[0])
-        assert len(gr) == 5
-        assert trp[0] not in gr
-        assert trp[1] not in gr
+            gr.add(trp)
+            gr.remove(trp[0])
+            assert len(gr) == 5
+            assert trp[0] not in gr
+            assert trp[1] not in gr
 
-        # This is the duplicate triple.
-        gr.remove(trp[1])
-        assert len(gr) == 5
+            # This is the duplicate triple.
+            gr.remove(trp[1])
+            assert len(gr) == 5
 
-        # This is the triple in reverse order.
-        gr.remove(trp[2])
-        assert len(gr) == 4
+            # This is the triple in reverse order.
+            gr.remove(trp[2])
+            assert len(gr) == 4
 
-        gr.remove(trp[4])
-        assert len(gr) == 3
+            gr.remove(trp[4])
+            assert len(gr) == 3
 
 
-    def test_union(self, trp):
+    def test_union(self, trp, store):
         """
         Test graph union.
         """
-        gr1 = Graph()
-        gr2 = Graph()
+        with store.txn_ctx():
+            gr1 = Graph(store)
+            gr2 = Graph(store)
 
-        gr1.add(trp[0:3])
-        gr2.add(trp[2:6])
+            gr1.add(trp[0:3])
+            gr2.add(trp[2:6])
 
-        gr3 = gr1 | gr2
+            gr3 = gr1 | gr2
 
-        assert len(gr3) == 5
-        assert trp[0] in gr3
-        assert trp[4] in gr3
+            assert len(gr3) == 5
+            assert trp[0] in gr3
+            assert trp[4] in gr3
 
 
-    def test_ip_union(self, trp):
+    def test_ip_union(self, trp, store):
         """
         Test graph in-place union.
         """
-        gr1 = Graph()
-        gr2 = Graph()
+        with store.txn_ctx():
+            gr1 = Graph(store)
+            gr2 = Graph(store)
 
-        gr1.add(trp[0:3])
-        gr2.add(trp[2:6])
+            gr1.add(trp[0:3])
+            gr2.add(trp[2:6])
 
-        gr1 |= gr2
+            gr1 |= gr2
 
-        assert len(gr1) == 5
-        assert trp[0] in gr1
-        assert trp[4] in gr1
+            assert len(gr1) == 5
+            assert trp[0] in gr1
+            assert trp[4] in gr1
 
 
     def test_addition(self, trp):