Browse Source

Add graph lookup tests; fix transactions in tests.

Stefano Cossu 6 years ago
parent
commit
bbcf66da2f
3 changed files with 326 additions and 93 deletions
  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__ = """
 __doc__ = """
 Primary API for resource manipulation.
 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):
 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
     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
     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
     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)
     function to remove all orphaned terms (not in any triple or context index)
     can be later devised to compact the database.
     can be later devised to compact the database.
@@ -54,6 +54,27 @@ cdef class Graph:
         """
         """
         Initialize the graph, optionally from Python/RDFlib data.
         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
         :type store: lakesuperior.store.ldp_rs.lmdb_triplestore.LmdbTriplestore
         :param store: Triplestore where keys are mapped to terms. By default
         :param store: Triplestore where keys are mapped to terms. By default
             this is the default application store
             this is the default application store
@@ -114,6 +135,12 @@ cdef class Graph:
             return self.keys.capacity
             return self.keys.capacity
 
 
 
 
+    property txn_ctx:
+        def __get__(self):
+            """ Expose underlying store's ``txn_ctx``. """
+            return self.store.txn_ctx
+
+
     ## MAGIC METHODS ##
     ## MAGIC METHODS ##
 
 
     def __len__(self):
     def __len__(self):
@@ -123,6 +150,7 @@ cdef class Graph:
 
 
     def __eq__(self, other):
     def __eq__(self, other):
         """ Equality operator between ``Graph`` instances. """
         """ Equality operator between ``Graph`` instances. """
+        # TODO Use __richcmp__()
         return len(self & other) == 0
         return len(self & other) == 0
 
 
 
 
@@ -474,7 +502,8 @@ cdef class Graph:
 
 
             if self.keys.contains(&spok):
             if self.keys.contains(&spok):
                 callback_fn(gr, &spok, ctx)
                 callback_fn(gr, &spok, ctx)
-                return
+
+            return
 
 
         if s is not None:
         if s is not None:
             k1 = self.store.to_key(s)
             k1 = self.store.to_key(s)
@@ -505,7 +534,7 @@ cdef class Graph:
         # Iterate over serialized triples.
         # Iterate over serialized triples.
         self.keys.seek()
         self.keys.seek()
         while self.keys.get_next(&spok):
         while self.keys.get_next(&spok):
-            logger.info('Verifying spok: {spok}')
+            logger.info(f'Verifying spok: {spok}')
             if cmp_fn(&spok, k1, k2):
             if cmp_fn(&spok, k1, k2):
                 callback_fn(gr, &spok, ctx)
                 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.
         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.
         # len() should not need a DB transaction open.
         assert len(gr) == 0
         assert len(gr) == 0
@@ -61,8 +61,7 @@ class TestGraphInit:
         """
         """
         Test creation using a Python set.
         Test creation using a Python set.
         """
         """
-        with store.txn_ctx(True):
-            pdb.set_trace()
+        with store.txn_ctx():
             gr = Graph(store, data=set(trp))
             gr = Graph(store, data=set(trp))
 
 
             assert len(gr) == 6
             assert len(gr) == 6
@@ -72,19 +71,220 @@ class TestGraphInit:
 
 
 
 
 @pytest.mark.usefixtures('trp')
 @pytest.mark.usefixtures('trp')
+@pytest.mark.usefixtures('store')
 class TestGraphLookup:
 class TestGraphLookup:
     """
     """
     Test triple lookup.
     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')
 @pytest.mark.usefixtures('trp')
@@ -92,94 +292,98 @@ class TestGraphOps:
     """
     """
     Test various graph operations.
     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.
         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.
         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.
         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.
         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.
         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):
     def test_addition(self, trp):