From 1ed4feb63219badbe910b8b402377f011a5853e7 Mon Sep 17 00:00:00 2001 From: Gunnar Aastrand Grimnes Date: Wed, 26 Jun 2013 16:10:41 +0200 Subject: [PATCH 1/5] Cleaning up Dataset class, adding graph tracking to store API, as discussed in #307 Summary of changes: * added methods ```add_graph``` and ```remove_graph``` to the Store API, implemented these for Sleepycat and IOMemory. A flag, ```graph_awareness``` is set on the store if they methods are supported, default implementations will raise an exception. * made the dataset require a store with the ```graph_awareness``` flag set. * removed the graph-state kept in the ```Dataset``` class directly. * removed ```dataset.add_quads```, ```remove_quads``` methods. The ```add/remove``` methods of ```ConjunctiveGraph``` are smart enough to work with triples or quads. * removed the ```dataset.graphs``` method - it now does exactly the same as ```contexts``` * cleaned up a bit more confusion of whether Graph instance or the Graph identifiers are passed to store methods. (#225) --- rdflib/graph.py | 139 ++++++++------------------- rdflib/plugins/memory.py | 38 ++++++-- rdflib/plugins/sleepycat.py | 7 ++ rdflib/plugins/stores/sparqlstore.py | 1 - rdflib/store.py | 35 ++++++- test/test_conjunctive_graph.py | 4 + test/test_dataset.py | 131 +++++++++++++++++++++++++ test/test_graph.py | 14 +-- test/test_graph_context.py | 21 ++-- 9 files changed, 256 insertions(+), 134 deletions(-) create mode 100644 test/test_dataset.py diff --git a/rdflib/graph.py b/rdflib/graph.py index 699625609..089f6885b 100644 --- a/rdflib/graph.py +++ b/rdflib/graph.py @@ -1246,6 +1246,8 @@ def _spoc(self, triple_or_quad, default=False): helper method for having methods that support either triples or quads """ + if triple_or_quad is None: + return (None, None, None, self.default_context if default else None) if len(triple_or_quad) == 3: c = self.default_context if default else None (s, p, o) = triple_or_quad @@ -1329,13 +1331,12 @@ def triples(self, triple_or_quad, context=None): for (s, p, o), cg in self.store.triples((s, p, o), context=context): yield s, p, o - def quads(self, pattern=None): + def quads(self, triple_or_quad=None): """Iterate over all the quads in the entire conjunctive graph""" - if pattern is None: - s, p, o = (None, None, None) - else: - s, p, o = pattern - for (s, p, o), cg in self.store.triples((s, p, o), context=None): + + s,p,o,c = self._spoc(triple_or_quad) + + for (s, p, o), cg in self.store.triples((s, p, o), context=c): for ctx in cg: yield s, p, o, ctx @@ -1356,11 +1357,11 @@ def contexts(self, triple=None): """ for context in self.store.contexts(triple): if isinstance(context, Graph): + # TODO: One of these should never happen and probably + # should raise an exception rather than smoothing over + # the weirdness - see #225 yield context else: - # TODO: This should never happen and probably should - # raise an exception rather than smoothing over the - # weirdness - see #225 yield self.get_context(context) def get_context(self, identifier, quoted=False): @@ -1415,7 +1416,7 @@ def __reduce__(self): - +DATASET_DEFAULT_GRAPH_ID = URIRef('urn:x-rdflib:default') class Dataset(ConjunctiveGraph): __doc__ = format_doctest_out(""" @@ -1438,8 +1439,6 @@ class Dataset(ConjunctiveGraph): >>> # Create a graph in the dataset, if the graph name has already been >>> # used, the corresponding graph will be returned >>> # (ie, the Dataset keeps track of the constituent graphs) - >>> # The special argument Dataset.DEFAULT can be used to return the - >>> # default graph >>> g = ds.graph(URIRef('http://www.example.com/gr')) >>> >>> # add triples to the new graph as usual @@ -1448,7 +1447,7 @@ class Dataset(ConjunctiveGraph): ... URIRef('http://example.org/y'), ... Literal('bar')) ) >>> # alternatively: add a quad to the dataset -> goes to the graph - >>> ds.add_quad( + >>> ds.add( ... (URIRef('http://example.org/x'), ... URIRef('http://example.org/z'), ... Literal('foo-bar'),g) ) @@ -1522,11 +1521,12 @@ class Dataset(ConjunctiveGraph): .. versionadded:: 4.0 """) - DEFAULT = "DEFAULT" - def __init__(self, store='default'): super(Dataset, self).__init__(store=store, identifier=None) - self.graph_names = {Dataset.DEFAULT: self} + + if not self.store.graph_aware: + raise Exception("DataSet must be backed by a graph-aware store!") + self.default_context = Graph(store=self.store, identifier=DATASET_DEFAULT_GRAPH_ID) def __str__(self): pattern = ("[a rdflib:Dataset;rdflib:storage " @@ -1540,102 +1540,41 @@ def graph(self, identifier=None): "genid", "http://rdflib.net" + rdflib_skolem_genid, override=False) identifier = BNode().skolemize() - elif identifier == Dataset.DEFAULT: - return self else: if isinstance(identifier, BNode): raise Exception( "Blank nodes cannot be Graph identifiers in RDF Datasets") if not isinstance(identifier, URIRef): identifier = URIRef(identifier) - - if identifier in self.graph_names.keys(): - return self.graph_names[identifier] - else: - retval = Graph(store=self.store, identifier=identifier) - self.graph_names[identifier] = retval - return retval + + g = self.get_context(identifier) + self.store.add_graph(g) + return g def remove_graph(self, g): - if g is None or g == Dataset.DEFAULT: - # default graph cannot be removed - return - else: - if isinstance(g, Graph): - try: - del self.graph_names[g.identifier] - self.remove_context(g.identifier) - except KeyError: - pass - else: - try: - del self.graph_names[URIRef(g)] - self.remove_context(g) - except KeyError: - pass + if not isinstance(g, Graph): + g = self.get_context(g) - def graphs(self, empty=True): - if empty: - # All graphs should be returned, including the empty ones: - for n in self.graph_names.keys(): - yield n - else: - # Only non-empty graphs should be returned; the contexts() call of - # the conjunctive graph does the job - for c in self.contexts(): - if isinstance(c.identifier, BNode): - yield Dataset.DEFAULT - else: - yield c.identifier - - def add_quad(self, quad): - (s, p, o, g) = quad - if g is None: - self.add((s, p, o)) - else: - if isinstance(g, Graph): - try: - self.graph_names[g.identifier].add((s, p, o)) - except KeyError: - pass - else: - try: - self.graph_names[URIRef(g)].add((s, p, o)) - except KeyError: - pass - - def remove_quad(self, (s, p, o, g)): - if g is None: - self.remove((s, p, o)) - else: - if isinstance(g, Graph): - try: - self.graph_names[g.identifier].remove((s, p, o)) - except KeyError: - pass - else: - try: - self.graph_names[URIRef(g)].remove((s, p, o)) - except KeyError: - pass + self.store.remove_graph(g) + if g is None or g == self.default_context: + # default graph cannot be removed + # only triples deleted, so add it back in + self.store.add_graph(self.default_context) + + def contexts(self, triple=None): + default = False + for c in super(Dataset, self).contexts(triple): + default|=c.identifier == DATASET_DEFAULT_GRAPH_ID + yield c + if not default: yield self.graph(DATASET_DEFAULT_GRAPH_ID) + def quads(self, quad): - (s, p, o, g) = quad - for s, p, o, c in super(Dataset, self).quads((s, p, o)): - if g is None: - # all quads have to be returned. However, the blank node name - # for the default graph should be removed - if isinstance(c.identifier, BNode): - yield (s, p, o, None) - else: - yield (s, p, o, c.identifier) - elif isinstance(g, Graph): - # only quads of a specific graph should be returned: - if g.identifier == c.identifier: - yield (s, p, o, c.identifier) + for s, p, o, c in super(Dataset, self).quads(quad): + if c.identifier==self.default_context: + yield (s, p, o, None) else: - if ("%s" % g) == ("%s" % c.identifier): - yield (s, p, o, c.identifier) + yield (s, p, o, c.identifier) class QuotedGraph(Graph): diff --git a/rdflib/plugins/memory.py b/rdflib/plugins/memory.py index 307b554ed..18a520b24 100644 --- a/rdflib/plugins/memory.py +++ b/rdflib/plugins/memory.py @@ -191,6 +191,7 @@ class IOMemory(Store): """ context_aware = True formula_aware = True + graph_aware = True # The following variable name conventions are used in this class: # @@ -242,7 +243,7 @@ def namespaces(self): def add(self, triple, context, quoted=False): Store.add(self, triple, context, quoted) - if context is not None and context not in self.__all_contexts: + if context is not None: self.__all_contexts.add(context) enctriple = self.__encodeTriple(triple) @@ -285,7 +286,9 @@ def remove(self, triplepat, context=None): del self.__tripleContexts[enctriple] - if triplepat == (None, None, None) and context in self.__all_contexts: + if triplepat == (None, None, None) and \ + context in self.__all_contexts and \ + not self.graph_aware: # remove the whole context self.__all_contexts.remove(context) @@ -340,7 +343,7 @@ def triples(self, triplein, context=None): if self.__tripleHasContext(enctriple, cid)) def contexts(self, triple=None): - if triple is None: + if triple is None or triple is (None,None,None): return (context for context in self.__all_contexts) enctriple = self.__encodeTriple(triple) @@ -354,6 +357,20 @@ def __len__(self, context=None): cid = self.__obj2id(context) return sum(1 for enctriple, contexts in self.__all_triples(cid)) + def add_graph(self, graph): + if not self.graph_aware: + Store.add_graph(self, graph) + else: + self.__all_contexts.add(graph) + + def remove_graph(self, graph): + if not self.graph_aware: + Store.remove_graph(self, graph) + else: + self.remove((None,None,None), graph) + self.__all_contexts.remove(graph) + + # internal utility methods below def __addTripleContext(self, enctriple, context, quoted): @@ -414,8 +431,8 @@ def __removeTripleContext(self, enctriple, cid): self.__tripleContexts[enctriple] = ctxs def __obj2id(self, obj): - """encode object, storing it in the encoding map if necessary, and - return the integer key""" + """encode object, storing it in the encoding map if necessary, + and return the integer key""" if obj not in self.__obj2int: id = randid() while id in self.__int2obj: @@ -430,20 +447,21 @@ def __encodeTriple(self, triple): return tuple(map(self.__obj2id, triple)) def __decodeTriple(self, enctriple): - """decode a whole encoded triple, returning the original triple""" + """decode a whole encoded triple, returning the original + triple""" return tuple(map(self.__int2obj.get, enctriple)) def __all_triples(self, cid): - """return a generator which yields all the triples (unencoded) of - the given context""" + """return a generator which yields all the triples (unencoded) + of the given context""" for tset in self.__subjectIndex.values(): for enctriple in tset.copy(): if self.__tripleHasContext(enctriple, cid): yield self.__decodeTriple(enctriple), self.__contexts(enctriple) def __contexts(self, enctriple): - """return a generator for all the non-quoted contexts (unencoded) - the encoded triple appears in""" + """return a generator for all the non-quoted contexts + (unencoded) the encoded triple appears in""" return (self.__int2obj.get(cid) for cid in self.__getTripleContexts(enctriple, skipQuoted=True) if cid is not None) def __emptygen(self): diff --git a/rdflib/plugins/sleepycat.py b/rdflib/plugins/sleepycat.py index 9c5d9e617..1591bf25a 100644 --- a/rdflib/plugins/sleepycat.py +++ b/rdflib/plugins/sleepycat.py @@ -43,6 +43,7 @@ class Sleepycat(Store): context_aware = True formula_aware = True transaction_aware = False + graph_aware = True db_env = None def __init__(self, configuration=None, identifier=None): @@ -495,6 +496,12 @@ def contexts(self, triple=None): current = None cursor.close() + def add_graph(self, graph): + self.__contexts.put(bb(self._to_string(graph)), "") + + def remove_graph(self, graph): + self.remove((None, None, None), graph) + def _from_string(self, i): k = self.__i2k.get(int(i)) return self._loads(k) diff --git a/rdflib/plugins/stores/sparqlstore.py b/rdflib/plugins/stores/sparqlstore.py index 23225e1de..97837f5b2 100644 --- a/rdflib/plugins/stores/sparqlstore.py +++ b/rdflib/plugins/stores/sparqlstore.py @@ -178,7 +178,6 @@ class SPARQLStore(NSSPARQLWrapper, Store): formula_aware = False transaction_aware = False regex_matching = NATIVE_REGEX - batch_unification = False def __init__(self, endpoint=None, bNodeAsURI=False, diff --git a/rdflib/store.py b/rdflib/store.py index 82ef74050..f4f4aa21e 100644 --- a/rdflib/store.py +++ b/rdflib/store.py @@ -20,6 +20,8 @@ ``Transaction-capable``: capable of providing transactional integrity to the RDF operations performed on it. +``Graph-aware``: capable of keeping track of empty graphs. + ------ """ @@ -111,7 +113,7 @@ class Store(object): context_aware = False formula_aware = False transaction_aware = False - batch_unification = False + graph_aware = False def __init__(self, configuration=None, identifier=None): """ @@ -268,9 +270,9 @@ def triples(self, triple_pattern, context=None): for example, REGEXTerm, URIRef, Literal, BNode, Variable, Graph, QuotedGraph, Date? DateRange? - A conjunctive query can be indicated by either providing a value of - None for the context or the identifier associated with the Conjunctive - Graph (if it is context aware). + :param context: A conjunctive query can be indicated by either + providing a value of None, or a specific context can be + queries by passing a Graph instance (if store is context aware). """ subject, predicate, object = triple_pattern @@ -282,12 +284,18 @@ def __len__(self, context=None): quoted (asserted) statements if the context is not specified, otherwise it should return the number of statements in the formula or context given. + + :param context: a graph instance to query or None """ def contexts(self, triple=None): """ Generator over all contexts in the graph. If triple is specified, a generator over all contexts the triple is in. + + if store is graph_aware, may also return empty contexts + + :returns: a generator over Nodes """ def query(self, query, initNs, initBindings, queryGraph, **kwargs): @@ -347,3 +355,22 @@ def commit(self): def rollback(self): """ """ + + # Optional graph methods + + def add_graph(self, graph): + """ + Add a graph to the store, no effect if the graph already + exists. + :param graph: a Graph instance + """ + raise Exception("Graph method called on non-graph_aware store") + + def remove_graph(self, graph): + """ + Remove a graph from the store, this shoud also remove all + triples in the graph + + :param graphid: a Graph instance + """ + raise Exception("Graph method called on non-graph_aware store") diff --git a/test/test_conjunctive_graph.py b/test/test_conjunctive_graph.py index 6014383d6..8bd7d2f8f 100644 --- a/test/test_conjunctive_graph.py +++ b/test/test_conjunctive_graph.py @@ -1,3 +1,7 @@ +""" +Tests for ConjunctiveGraph that do not depend on the underlying store +""" + from rdflib import ConjunctiveGraph, Graph from rdflib.term import Identifier, URIRef, BNode from rdflib.parser import StringInputSource diff --git a/test/test_dataset.py b/test/test_dataset.py new file mode 100644 index 000000000..f769d49e5 --- /dev/null +++ b/test/test_dataset.py @@ -0,0 +1,131 @@ +import sys +import os +import unittest + +from tempfile import mkdtemp, mkstemp +import shutil +from rdflib import Graph, Dataset, URIRef, BNode, plugin +from rdflib.graph import DATASET_DEFAULT_GRAPH_ID + +from nose.exc import SkipTest + + +class DatasetTestCase(unittest.TestCase): + store = 'default' + slow = True + tmppath = None + + def setUp(self): + try: + self.graph = Dataset(store=self.store) + except ImportError: + raise SkipTest( + "Dependencies for store '%s' not available!" % self.store) + if self.store == "SQLite": + _, self.tmppath = mkstemp( + prefix='test', dir='/tmp', suffix='.sqlite') + else: + self.tmppath = mkdtemp() + self.graph.open(self.tmppath, create=True) + self.michel = URIRef(u'michel') + self.tarek = URIRef(u'tarek') + self.bob = URIRef(u'bob') + self.likes = URIRef(u'likes') + self.hates = URIRef(u'hates') + self.pizza = URIRef(u'pizza') + self.cheese = URIRef(u'cheese') + + self.c1 = URIRef(u'context-1') + self.c2 = URIRef(u'context-2') + + # delete the graph for each test! + self.graph.remove((None, None, None)) + + def tearDown(self): + self.graph.close() + if os.path.isdir(self.tmppath): + shutil.rmtree(self.tmppath) + else: + os.remove(self.tmppath) + + + def testGraphAware(self): + if not self.graph.store.graph_aware: return + + g = self.graph + g1 = g.graph(self.c1) + + + # added graph exists + self.assertEquals(set(x.identifier for x in self.graph.contexts()), + set([self.c1, DATASET_DEFAULT_GRAPH_ID])) + + # added graph is empty + self.assertEquals(len(g1), 0) + + g1.add( (self.tarek, self.likes, self.pizza) ) + + # added graph still exists + self.assertEquals(set(x.identifier for x in self.graph.contexts()), + set([self.c1, DATASET_DEFAULT_GRAPH_ID])) + + # added graph contains one triple + self.assertEquals(len(g1), 1) + + g1.remove( (self.tarek, self.likes, self.pizza) ) + + # added graph is empty + self.assertEquals(len(g1), 0) + + # graph still exists, although empty + self.assertEquals(set(x.identifier for x in self.graph.contexts()), + set([self.c1, DATASET_DEFAULT_GRAPH_ID])) + + g.remove_graph(self.c1) + + # graph is gone + self.assertEquals(set(x.identifier for x in self.graph.contexts()), + set([DATASET_DEFAULT_GRAPH_ID])) + + def testDefaultGraph(self): + + self.graph.add(( self.tarek, self.likes, self.pizza)) + self.assertEquals(len(self.graph), 1) + # only default exists + self.assertEquals(set(x.identifier for x in self.graph.contexts()), + set([DATASET_DEFAULT_GRAPH_ID])) + + # removing default graph removes triples but not actual graph + self.graph.remove_graph(DATASET_DEFAULT_GRAPH_ID) + + self.assertEquals(len(self.graph), 0) + # default still exists + self.assertEquals(set(x.identifier for x in self.graph.contexts()), + set([DATASET_DEFAULT_GRAPH_ID])) + + + + + +# dynamically create classes for each registered Store + +pluginname = None +if __name__ == '__main__': + if len(sys.argv) > 1: + pluginname = sys.argv[1] + +tests = 0 +for s in plugin.plugins(pluginname, plugin.Store): + if s.name in ('default', 'IOMemory', 'Auditable', + 'Concurrent', 'SPARQLStore', 'SPARQLUpdateStore'): + continue # these are tested by default + if not s.getClass().graph_aware: + continue + + locals()["t%d" % tests] = type("%sContextTestCase" % s.name, ( + DatasetTestCase,), {"store": s.name}) + tests += 1 + + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_graph.py b/test/test_graph.py index d35c0a090..46aaf9fd3 100644 --- a/test/test_graph.py +++ b/test/test_graph.py @@ -177,8 +177,8 @@ def testConnected(self): self.assertEquals(False, graph.connected()) def testSub(self): - g1 = Graph() - g2 = Graph() + g1 = self.graph + g2 = Graph(store=g2.store) tarek = self.tarek # michel = self.michel @@ -210,8 +210,8 @@ def testSub(self): self.assertEquals((bob, likes, cheese) in g1, False) def testGraphAdd(self): - g1 = Graph() - g2 = Graph() + g1 = self.graph + g2 = Graph(store=g1.store) tarek = self.tarek # michel = self.michel @@ -242,8 +242,8 @@ def testGraphAdd(self): self.assertEquals((bob, likes, cheese) in g1, True) def testGraphIntersection(self): - g1 = Graph() - g2 = Graph() + g1 = self.graph + g2 = Graph(store=g1.store) tarek = self.tarek michel = self.michel @@ -279,7 +279,7 @@ def testGraphIntersection(self): self.assertEquals((bob, likes, cheese) in g1, False) self.assertEquals((michel, likes, cheese) in g1, True) - + # dynamically create classes for each registered Store diff --git a/test/test_graph_context.py b/test/test_graph_context.py index 5f3673c4f..fce1626d6 100644 --- a/test/test_graph_context.py +++ b/test/test_graph_context.py @@ -47,12 +47,6 @@ def tearDown(self): else: os.remove(self.tmppath) - def get_context(self, identifier): - assert isinstance(identifier, URIRef) or \ - isinstance(identifier, BNode), type(identifier) - return Graph(store=self.graph.store, identifier=identifier, - namespace_manager=self) - def addStuff(self): tarek = self.tarek michel = self.michel @@ -126,15 +120,15 @@ def testLenInOneContext(self): c1 = self.c1 # make sure context is empty - self.graph.remove_context(self.get_context(c1)) + self.graph.remove_context(self.graph.get_context(c1)) graph = Graph(self.graph.store, c1) oldLen = len(self.graph) for i in range(0, 10): graph.add((BNode(), self.hates, self.hates)) self.assertEquals(len(graph), oldLen + 10) - self.assertEquals(len(self.get_context(c1)), oldLen + 10) - self.graph.remove_context(self.get_context(c1)) + self.assertEquals(len(self.graph.get_context(c1)), oldLen + 10) + self.graph.remove_context(self.graph.get_context(c1)) self.assertEquals(len(self.graph), oldLen) self.assertEquals(len(graph), 0) @@ -194,9 +188,9 @@ def testRemoveContext(self): self.addStuffInMultipleContexts() self.assertEquals(len(Graph(self.graph.store, c1)), 1) - self.assertEquals(len(self.get_context(c1)), 1) + self.assertEquals(len(self.graph.get_context(c1)), 1) - self.graph.remove_context(self.get_context(c1)) + self.graph.remove_context(self.graph.get_context(c1)) self.assert_(self.c1 not in self.graph.contexts()) def testRemoveAny(self): @@ -292,7 +286,7 @@ def testTriples(self): # all unbound without context, same result! asserte(len(list(triples((Any, Any, Any)))), 7) - for c in [graph, self.get_context(c1)]: + for c in [graph, self.graph.get_context(c1)]: # unbound subjects asserte(set(c.subjects(likes, pizza)), set((michel, tarek))) asserte(set(c.subjects(hates, pizza)), set((bob,))) @@ -343,6 +337,9 @@ def testTriples(self): asserte(len(list(c1triples((Any, Any, Any)))), 0) asserte(len(list(triples((Any, Any, Any)))), 0) + + + # dynamically create classes for each registered Store pluginname = None From 06be678ab4ffa0cf8ea8adf264146f9d1caa23cf Mon Sep 17 00:00:00 2001 From: Gunnar Aastrand Grimnes Date: Wed, 26 Jun 2013 19:12:39 +0200 Subject: [PATCH 2/5] added support for toggleable union of default graph --- rdflib/graph.py | 32 ++++++++++++++++++++++---------- test/test_dataset.py | 7 ++++++- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/rdflib/graph.py b/rdflib/graph.py index 089f6885b..75b07475b 100644 --- a/rdflib/graph.py +++ b/rdflib/graph.py @@ -281,6 +281,7 @@ def __init__(self, store='default', identifier=None, self.__namespace_manager = namespace_manager self.context_aware = False self.formula_aware = False + self.default_union = False def __get_store(self): return self.__store @@ -1025,7 +1026,7 @@ def query(self, query_object, processor='sparql', try: return self.store.query( query_object, initNs, initBindings, - self.context_aware + self.default_union and '__UNION__' or self.identifier, **kwargs) @@ -1049,7 +1050,7 @@ def update(self, update_object, processor='sparql', try: return self.store.update( update_object, initNs, initBindings, - self.context_aware + self.default_union and '__UNION__' or self.identifier, **kwargs) @@ -1233,6 +1234,7 @@ def __init__(self, store='default', identifier=None): assert self.store.context_aware, ("ConjunctiveGraph must be backed by" " a context aware store.") self.context_aware = True + self.default_union = True # Conjunctive! self.default_context = Graph(store=self.store, identifier=identifier or BNode()) @@ -1317,15 +1319,16 @@ def triples(self, triple_or_quad, context=None): """ s,p,o,c = self._spoc(triple_or_quad) - context = context or c + context = self._graph(context or c) + + if not self.default_union and context is None: + context=self.default_context if isinstance(p, Path): if context is None: context = self - else: - context = self._graph(context) - for s, o in p.eval(self.get_context(context), s, o): + for s, o in p.eval(context, s, o): yield (s, p, o) else: for (s, p, o), cg in self.store.triples((s, p, o), context=context): @@ -1340,10 +1343,17 @@ def quads(self, triple_or_quad=None): for ctx in cg: yield s, p, o, ctx - def triples_choices(self, (s, p, o)): + def triples_choices(self, (s, p, o), context=None): """Iterate over all the triples in the entire conjunctive graph""" + + if context is None: + if not self.default_union: + context=self.default_context + else: + context = self._graph(context) + for (s1, p1, o1), cg in self.store.triples_choices((s, p, o), - context=None): + context=context): yield (s1, p1, o1) def __len__(self): @@ -1521,13 +1531,16 @@ class Dataset(ConjunctiveGraph): .. versionadded:: 4.0 """) - def __init__(self, store='default'): + def __init__(self, store='default', default_union=False): super(Dataset, self).__init__(store=store, identifier=None) if not self.store.graph_aware: raise Exception("DataSet must be backed by a graph-aware store!") self.default_context = Graph(store=self.store, identifier=DATASET_DEFAULT_GRAPH_ID) + self.default_union = default_union + + def __str__(self): pattern = ("[a rdflib:Dataset;rdflib:storage " "[a rdflib:Store;rdfs:label '%s']]") @@ -1567,7 +1580,6 @@ def contexts(self, triple=None): default|=c.identifier == DATASET_DEFAULT_GRAPH_ID yield c if not default: yield self.graph(DATASET_DEFAULT_GRAPH_ID) - def quads(self, quad): for s, p, o, c in super(Dataset, self).quads(quad): diff --git a/test/test_dataset.py b/test/test_dataset.py index f769d49e5..0559364fb 100644 --- a/test/test_dataset.py +++ b/test/test_dataset.py @@ -103,8 +103,13 @@ def testDefaultGraph(self): self.assertEquals(set(x.identifier for x in self.graph.contexts()), set([DATASET_DEFAULT_GRAPH_ID])) + def testNotUnion(self): + g1 = self.graph.graph(self.c1) + g1.add((self.tarek, self.likes, self.pizza)) - + self.assertEqual(list(self.graph.objects(self.tarek, None)), + []) + self.assertEqual(list(g1.objects(self.tarek, None)), [self.pizza]) # dynamically create classes for each registered Store From 8b110ca785d9457e2a61057f8e21c529f26e9f02 Mon Sep 17 00:00:00 2001 From: Gunnar Aastrand Grimnes Date: Mon, 29 Jul 2013 11:30:37 +0200 Subject: [PATCH 3/5] whitespace gardening --- rdflib/graph.py | 157 +++++++++++++++++++++++---------------- rdflib/plugins/memory.py | 6 +- test/test_dawg.py | 6 +- test/test_graph.py | 2 +- 4 files changed, 99 insertions(+), 72 deletions(-) diff --git a/rdflib/graph.py b/rdflib/graph.py index 75b07475b..232e601be 100644 --- a/rdflib/graph.py +++ b/rdflib/graph.py @@ -17,25 +17,50 @@ Graph ----- -An RDF graph is a set of RDF triples. Graphs support the python ``in`` operator, as well as iteration and some operations like union, difference and intersection. + +An RDF graph is a set of RDF triples. Graphs support the python ``in`` +operator, as well as iteration and some operations like union, +difference and intersection. Conjunctive Graph ----------------- -A Conjunctive Graph is the most relevant collection of graphs that are considered to be the boundary for closed world assumptions. This boundary is equivalent to that of the store instance (which is itself uniquely identified and distinct from other instances of :class:`Store` that signify other Conjunctive Graphs). It is equivalent to all the named graphs within it and associated with a ``_default_`` graph which is automatically assigned a :class:`BNode` for an identifier - if one isn't given. +A Conjunctive Graph is the most relevant collection of graphs that are +considered to be the boundary for closed world assumptions. This +boundary is equivalent to that of the store instance (which is itself +uniquely identified and distinct from other instances of +:class:`Store` that signify other Conjunctive Graphs). It is +equivalent to all the named graphs within it and associated with a +``_default_`` graph which is automatically assigned a :class:`BNode` +for an identifier - if one isn't given. Quoted graph ------------ -The notion of an RDF graph [14] is extended to include the concept of a formula node. A formula node may occur wherever any other kind of node can appear. Associated with a formula node is an RDF graph that is completely disjoint from all other graphs; i.e. has no nodes in common with any other graph. (It may contain the same labels as other RDF graphs; because this is, by definition, a separate graph, considerations of tidiness do not apply between the graph at a formula node and any other graph.) +The notion of an RDF graph [14] is extended to include the concept of +a formula node. A formula node may occur wherever any other kind of +node can appear. Associated with a formula node is an RDF graph that +is completely disjoint from all other graphs; i.e. has no nodes in +common with any other graph. (It may contain the same labels as other +RDF graphs; because this is, by definition, a separate graph, +considerations of tidiness do not apply between the graph at a formula +node and any other graph.) -This is intended to map the idea of "{ N3-expression }" that is used by N3 into an RDF graph upon which RDF semantics is defined. +This is intended to map the idea of "{ N3-expression }" that is used +by N3 into an RDF graph upon which RDF semantics is defined. Dataset ------- -The RDF 1.1 Dataset, a small extension to the Conjunctive Graph. The primary term is "graphs in the datasets" and not "contexts with quads" so there is a separate method to set/retrieve a graph in a dataset and to operate with dataset graphs. As a consequence of this approach, dataset graphs cannot be identified with blank nodes, a name is always required (RDFLib will automatically add a name if one is not provided at creation time). This implementation includes a convenience method to directly add a single quad to a dataset graph. +The RDF 1.1 Dataset, a small extension to the Conjunctive Graph. The +primary term is "graphs in the datasets" and not "contexts with quads" +so there is a separate method to set/retrieve a graph in a dataset and +to operate with dataset graphs. As a consequence of this approach, +dataset graphs cannot be identified with blank nodes, a name is always +required (RDFLib will automatically add a name if one is not provided +at creation time). This implementation includes a convenience method +to directly add a single quad to a dataset graph. Instantiating Graphs with default store (IOMemory) and default identifier @@ -97,13 +122,15 @@ ``None`` terms in calls to :meth:`~rdflib.graph.Graph.triples` can be thought of as "open variables". -Graph support set-theoretic operators, you can add/subtract graphs, as well as -intersection (with multiplication operator g1*g2) and xor (g1 ^ g2). +Graph support set-theoretic operators, you can add/subtract graphs, as +well as intersection (with multiplication operator g1*g2) and xor (g1 +^ g2). -Note that BNode IDs are kept when doing set-theoretic operations, this may or -may not be what you want. Two named graphs within the same application probably -want share BNode IDs, two graphs with data from different sources probably not. -If your BNode IDs are all generated by RDFLib they are UUIDs and unique. +Note that BNode IDs are kept when doing set-theoretic operations, this +may or may not be what you want. Two named graphs within the same +application probably want share BNode IDs, two graphs with data from +different sources probably not. If your BNode IDs are all generated +by RDFLib they are UUIDs and unique. >>> g1 = Graph() >>> g2 = Graph() @@ -299,8 +326,8 @@ def _get_namespace_manager(self): def _set_namespace_manager(self, nm): self.__namespace_manager = nm - namespace_manager = property(_get_namespace_manager, - _set_namespace_manager, + namespace_manager = property(_get_namespace_manager, + _set_namespace_manager, doc="this graph's namespace-manager") def __repr__(self): @@ -383,7 +410,7 @@ def triples(self, (s, p, o)): Returns triples that match the given triple pattern. If triple pattern does not provide a context, all contexts will be searched. """ - if isinstance(p, Path): + if isinstance(p, Path): for _s, _o in p.eval(self, s, o): yield (_s, p, _o) else: @@ -391,11 +418,11 @@ def triples(self, (s, p, o)): yield (s, p, o) @py3compat.format_doctest_out - def __getitem__(self, item): + def __getitem__(self, item): """ A graph can be "sliced" as a shortcut for the triples method - The python slice syntax is (ab)used for specifying triples. - A generator over matches is returned, + The python slice syntax is (ab)used for specifying triples. + A generator over matches is returned, the returned tuples include only the parts not given >>> import rdflib @@ -404,7 +431,7 @@ def __getitem__(self, item): >>> list(g[rdflib.URIRef('urn:bob')]) # all triples about bob [(rdflib.term.URIRef(%(u)s'http://www.w3.org/2000/01/rdf-schema#label'), rdflib.term.Literal(%(u)s'Bob'))] - + >>> list(g[:rdflib.RDFS.label]) # all label triples [(rdflib.term.URIRef(%(u)s'urn:bob'), rdflib.term.Literal(%(u)s'Bob'))] @@ -414,7 +441,7 @@ def __getitem__(self, item): Combined with SPARQL paths, more complex queries can be written concisely: - Name of all Bobs friends: + Name of all Bobs friends: g[bob : FOAF.knows/FOAF.name ] @@ -432,32 +459,32 @@ def __getitem__(self, item): """ - if isinstance(item, slice): + if isinstance(item, slice): s,p,o=item.start,item.stop,item.step if s is None and p is None and o is None: return self.triples((s,p,o)) - elif s is None and p is None: + elif s is None and p is None: return self.subject_predicates(o) - elif s is None and o is None: + elif s is None and o is None: return self.subject_objects(p) - elif p is None and o is None: + elif p is None and o is None: return self.predicate_objects(s) - elif s is None: + elif s is None: return self.subjects(p,o) - elif p is None: + elif p is None: return self.predicates(s,o) - elif o is None: + elif o is None: return self.objects(s,p) - else: - # all given + else: + # all given return (s,p,o) in self elif isinstance(item, (Path,Node)): return self.predicate_objects(item) - - else: + + else: raise TypeError("You can only index a graph by a single rdflib term or path, or a slice of rdflib terms.") def __len__(self): @@ -870,7 +897,7 @@ def bind(self, prefix, namespace, override=True): was already bound to a different prefix. for example: graph.bind('foaf', 'http://xmlns.com/foaf/0.1/') - + """ return self.namespace_manager.bind( prefix, namespace, override=override) @@ -1006,17 +1033,17 @@ def query(self, query_object, processor='sparql', result='sparql', initNs=None, initBindings=None, use_store_provided=True, **kwargs): """ - Query this graph. - + Query this graph. + A type of 'prepared queries' can be realised by providing initial variable bindings with initBindings - Initial namespaces are used to resolve prefixes used in the query, + Initial namespaces are used to resolve prefixes used in the query, if none are given, the namespaces from the graph's namespace manager - are used. + are used. :returntype: rdflib.query.QueryResult - + """ initBindings = initBindings or {} @@ -1150,7 +1177,7 @@ def resource(self, identifier): >>> assert resource.graph is graph """ - if not isinstance(identifier, Node): + if not isinstance(identifier, Node): identifier = URIRef(identifier) return Resource(self, identifier) @@ -1212,7 +1239,7 @@ def do_de_skolemize2(t): return retval -class ConjunctiveGraph(Graph): +class ConjunctiveGraph(Graph): """ A ConjunctiveGraph is an (unamed) aggregation of all the named @@ -1223,7 +1250,7 @@ class ConjunctiveGraph(Graph): to use as the name of this default graph or it will assign a BNode. - All methods that add triples work against this default graph. + All methods that add triples work against this default graph. All queries are carried out against the union of all graphs. @@ -1243,17 +1270,17 @@ def __str__(self): "[a rdflib:Store;rdfs:label '%s']]") return pattern % self.store.__class__.__name__ - def _spoc(self, triple_or_quad, default=False): + def _spoc(self, triple_or_quad, default=False): """ - helper method for having methods that support + helper method for having methods that support either triples or quads """ - if triple_or_quad is None: + if triple_or_quad is None: return (None, None, None, self.default_context if default else None) - if len(triple_or_quad) == 3: + if len(triple_or_quad) == 3: c = self.default_context if default else None (s, p, o) = triple_or_quad - elif len(triple_or_quad) == 4: + elif len(triple_or_quad) == 4: (s, p, o, c) = triple_or_quad c = self._graph(c) return s,p,o,c @@ -1270,13 +1297,13 @@ def __contains__(self, triple_or_quad): def add(self, triple_or_quad): """ - Add a triple or quad to the store. + Add a triple or quad to the store. if a triple is given it is added to the default context """ - + s,p,o,c = self._spoc(triple_or_quad, default=True) - + _assertnode(s,p,o) self.store.add((s, p, o), context=c, quoted=False) @@ -1284,7 +1311,7 @@ def add(self, triple_or_quad): def _graph(self, c): if not isinstance(c, Graph): return self.get_context(c) - else: + else: return c @@ -1299,20 +1326,20 @@ def addN(self, quads): def remove(self, triple_or_quad): """ Removes a triple or quads - + if a triple is given it is removed from all contexts - + a quad is removed from the given context only """ s,p,o,c = self._spoc(triple_or_quad) - + self.store.remove((s, p, o), context=c) - + def triples(self, triple_or_quad, context=None): """ Iterate over all the triples in the entire conjunctive graph - + For legacy reasons, this can take the context to query either as a fourth element of the quad, or as the explicit context keyword paramater. The kw param takes precedence. @@ -1336,9 +1363,9 @@ def triples(self, triple_or_quad, context=None): def quads(self, triple_or_quad=None): """Iterate over all the quads in the entire conjunctive graph""" - + s,p,o,c = self._spoc(triple_or_quad) - + for (s, p, o), cg in self.store.triples((s, p, o), context=c): for ctx in cg: yield s, p, o, ctx @@ -1346,10 +1373,10 @@ def quads(self, triple_or_quad=None): def triples_choices(self, (s, p, o), context=None): """Iterate over all the triples in the entire conjunctive graph""" - if context is None: + if context is None: if not self.default_union: context=self.default_context - else: + else: context = self._graph(context) for (s1, p1, o1), cg in self.store.triples_choices((s, p, o), @@ -1412,7 +1439,7 @@ def parse(self, source=None, publicID=None, format="xml", file=file, data=data, format=format) g_id = publicID and publicID or source.getPublicId() - if not isinstance(g_id, Node): + if not isinstance(g_id, Node): g_id = URIRef(g_id) context = Graph(store=self.store, identifier=g_id) @@ -1534,7 +1561,7 @@ class Dataset(ConjunctiveGraph): def __init__(self, store='default', default_union=False): super(Dataset, self).__init__(store=store, identifier=None) - if not self.store.graph_aware: + if not self.store.graph_aware: raise Exception("DataSet must be backed by a graph-aware store!") self.default_context = Graph(store=self.store, identifier=DATASET_DEFAULT_GRAPH_ID) @@ -1574,9 +1601,9 @@ def remove_graph(self, g): # only triples deleted, so add it back in self.store.add_graph(self.default_context) - def contexts(self, triple=None): + def contexts(self, triple=None): default = False - for c in super(Dataset, self).contexts(triple): + for c in super(Dataset, self).contexts(triple): default|=c.identifier == DATASET_DEFAULT_GRAPH_ID yield c if not default: yield self.graph(DATASET_DEFAULT_GRAPH_ID) @@ -1660,9 +1687,9 @@ def remove(self, triple): def __reduce__(self): return (GraphValue, (self.store, self.identifier,)) -# Make sure QuotedGraph and GraphValues are ordered correctly -# wrt to other Terms. -# this must be done here, as the QuotedGraph cannot be +# Make sure QuotedGraph and GraphValues are ordered correctly +# wrt to other Terms. +# this must be done here, as the QuotedGraph cannot be # circularily imported in term.py rdflib.term._ORDERING[QuotedGraph]=11 rdflib.term._ORDERING[GraphValue]=12 @@ -1916,7 +1943,7 @@ def remove(self, (s, p, o)): def triples(self, (s, p, o)): for graph in self.graphs: - if isinstance(p, Path): + if isinstance(p, Path): for s, o in p.eval(self, s, o): yield s, p, o else: diff --git a/rdflib/plugins/memory.py b/rdflib/plugins/memory.py index 18a520b24..085c0d880 100644 --- a/rdflib/plugins/memory.py +++ b/rdflib/plugins/memory.py @@ -288,7 +288,7 @@ def remove(self, triplepat, context=None): if triplepat == (None, None, None) and \ context in self.__all_contexts and \ - not self.graph_aware: + not self.graph_aware: # remove the whole context self.__all_contexts.remove(context) @@ -357,13 +357,13 @@ def __len__(self, context=None): cid = self.__obj2id(context) return sum(1 for enctriple, contexts in self.__all_triples(cid)) - def add_graph(self, graph): + def add_graph(self, graph): if not self.graph_aware: Store.add_graph(self, graph) else: self.__all_contexts.add(graph) - def remove_graph(self, graph): + def remove_graph(self, graph): if not self.graph_aware: Store.remove_graph(self, graph) else: diff --git a/test/test_dawg.py b/test/test_dawg.py index e4aacb674..4810226aa 100644 --- a/test/test_dawg.py +++ b/test/test_dawg.py @@ -67,7 +67,7 @@ def setFlags(): # we obviously need this rdflib.DAWG_LITERAL_COLLATION = True - + def resetFlags(): import rdflib # Several tests rely on lexical form of literals being kept! @@ -598,7 +598,7 @@ def _str(x): def test_dawg(): - + setFlags() if SPARQL10Tests: @@ -609,7 +609,7 @@ def test_dawg(): for t in read_manifest("test/DAWG/data-sparql11/manifest-all.ttl"): yield t - if RDFLibTests: + if RDFLibTests: for t in read_manifest("test/DAWG/rdflib/manifest.ttl"): yield t diff --git a/test/test_graph.py b/test/test_graph.py index 46aaf9fd3..3dfd4442b 100644 --- a/test/test_graph.py +++ b/test/test_graph.py @@ -279,7 +279,7 @@ def testGraphIntersection(self): self.assertEquals((bob, likes, cheese) in g1, False) self.assertEquals((michel, likes, cheese) in g1, True) - + # dynamically create classes for each registered Store From 5aaa6f4f582ccb9c9f92a945ac4cd4095276348e Mon Sep 17 00:00:00 2001 From: Gunnar Aastrand Grimnes Date: Mon, 29 Jul 2013 11:31:05 +0200 Subject: [PATCH 4/5] made dawg tests use dataset class --- test/test_dawg.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/test/test_dawg.py b/test/test_dawg.py index 4810226aa..6b1b1f06a 100644 --- a/test/test_dawg.py +++ b/test/test_dawg.py @@ -30,7 +30,7 @@ def most_common(self, N): from rdflib import ( - ConjunctiveGraph, Graph, Namespace, RDF, RDFS, URIRef, BNode, Literal) + Dataset, Graph, Namespace, RDF, RDFS, URIRef, BNode, Literal) from rdflib.query import Result from rdflib.compare import isomorphic @@ -43,7 +43,7 @@ def most_common(self, N): from rdflib.py3compat import decodeStringEscape -from nose.tools import nottest, eq_ as eq +from nose.tools import nottest, eq_ from nose import SkipTest from urlparse import urljoin @@ -56,6 +56,8 @@ def most_common(self, N): else: from io import BytesIO +def eq(a,b,msg): + return eq_(a,b,msg+': (%r!=%r)'%(a,b)) def setFlags(): import rdflib @@ -78,13 +80,13 @@ def resetFlags(): # we obviously need this rdflib.DAWG_LITERAL_COLLATION = False - + DEBUG_FAIL = True -DEBUG_FAIL = False +#DEBUG_FAIL = False DEBUG_ERROR = True -DEBUG_ERROR = False +#DEBUG_ERROR = False SPARQL10Tests = True # SPARQL10Tests = False @@ -226,7 +228,7 @@ def update_test(t): raise SkipTest() try: - g = ConjunctiveGraph() + g = Dataset() if not res: if syntax: @@ -253,7 +255,7 @@ def update_test(t): evalUpdate(g, req) # read expected results - resg = ConjunctiveGraph() + resg = Dataset() if resdata: resg.default_context.load(resdata, format=_fmt(resdata)) @@ -263,7 +265,7 @@ def update_test(t): eq(set(x.identifier for x in g.contexts() if x != g.default_context), set(x.identifier for x in resg.contexts() - if x != resg.default_context)) + if x != resg.default_context), 'named graphs in datasets do not match') assert isomorphic(g.default_context, resg.default_context), \ 'Default graphs are not isomorphic' @@ -354,7 +356,7 @@ def skip(reason='(none)'): f.close() try: - g = ConjunctiveGraph() + g = Dataset() if data: g.default_context.load(data, format=_fmt(data)) From 6c026d0922a392efdc1434122c081a383d9c415f Mon Sep 17 00:00:00 2001 From: Gunnar Aastrand Grimnes Date: Mon, 29 Jul 2013 11:31:20 +0200 Subject: [PATCH 5/5] Dataset and graph_aware store fixes Add graphs when parsing, so also empty graphs are added. --- rdflib/graph.py | 39 ++++++++++++++++++++------------- rdflib/plugins/memory.py | 6 ++++- rdflib/plugins/sparql/update.py | 18 ++++++++++++--- test/test_graph.py | 2 +- 4 files changed, 45 insertions(+), 20 deletions(-) diff --git a/rdflib/graph.py b/rdflib/graph.py index 232e601be..42bb7cac5 100644 --- a/rdflib/graph.py +++ b/rdflib/graph.py @@ -1308,8 +1308,9 @@ def add(self, triple_or_quad): self.store.add((s, p, o), context=c, quoted=False) - def _graph(self, c): - if not isinstance(c, Graph): + def _graph(self, c): + if c is None: return None + if not isinstance(c, Graph): return self.get_context(c) else: return c @@ -1348,10 +1349,14 @@ def triples(self, triple_or_quad, context=None): s,p,o,c = self._spoc(triple_or_quad) context = self._graph(context or c) - if not self.default_union and context is None: - context=self.default_context - - if isinstance(p, Path): + if self.default_union: + if context==self.default_context: + context = None + else: + if context is None: + context = self.default_context + + if isinstance(p, Path): if context is None: context = self @@ -1443,7 +1448,7 @@ def parse(self, source=None, publicID=None, format="xml", g_id = URIRef(g_id) context = Graph(store=self.store, identifier=g_id) - context.remove((None, None, None)) + context.remove((None, None, None)) # hmm ? context.parse(source, publicID=publicID, format=format, location=location, file=file, data=data, **args) return context @@ -1580,17 +1585,21 @@ def graph(self, identifier=None): "genid", "http://rdflib.net" + rdflib_skolem_genid, override=False) identifier = BNode().skolemize() - else: - if isinstance(identifier, BNode): - raise Exception( - "Blank nodes cannot be Graph identifiers in RDF Datasets") - if not isinstance(identifier, URIRef): - identifier = URIRef(identifier) - - g = self.get_context(identifier) + + g = self._graph(identifier) + self.store.add_graph(g) return g + def parse(self, source=None, publicID=None, format="xml", + location=None, file=None, data=None, **args): + c = ConjunctiveGraph.parse(self, source, publicID, format, location, file, data, **args) + self.graph(c) + + def add_graph(self, g): + """alias of graph for consistency""" + return self.graph(g) + def remove_graph(self, g): if not isinstance(g, Graph): g = self.get_context(g) diff --git a/rdflib/plugins/memory.py b/rdflib/plugins/memory.py index 085c0d880..cbabad097 100644 --- a/rdflib/plugins/memory.py +++ b/rdflib/plugins/memory.py @@ -368,7 +368,11 @@ def remove_graph(self, graph): Store.remove_graph(self, graph) else: self.remove((None,None,None), graph) - self.__all_contexts.remove(graph) + try: + self.__all_contexts.remove(graph) + except KeyError: + pass # we didn't know this graph, no problem + # internal utility methods below diff --git a/rdflib/plugins/sparql/update.py b/rdflib/plugins/sparql/update.py index a4607d69d..57c5bf461 100644 --- a/rdflib/plugins/sparql/update.py +++ b/rdflib/plugins/sparql/update.py @@ -62,6 +62,16 @@ def evalClear(ctx, u): for g in _graphAll(ctx, u.graphiri): g.remove((None, None, None)) +def evalDrop(ctx, u): + """ + http://www.w3.org/TR/sparql11-update/#drop + """ + if ctx.dataset.store.graph_aware: + for g in _graphAll(ctx, u.graphiri): + ctx.dataset.store.remove_graph(g) + else: + evalClear(ctx, u) + def evalInsertData(ctx, u): """ @@ -214,7 +224,10 @@ def evalMove(ctx, u): dstg += srcg - srcg.remove((None, None, None)) + if ctx.dataset.store.graph_aware: + ctx.dataset.store.remove_graph(srcg) + else: + srcg.remove((None, None, None)) def evalCopy(ctx, u): @@ -277,8 +290,7 @@ def evalUpdate(graph, update, initBindings=None): elif u.name == 'Clear': evalClear(ctx, u) elif u.name == 'Drop': - # rdflib does not record empty graphs, so clear == drop - evalClear(ctx, u) + evalDrop(ctx, u) elif u.name == 'Create': evalCreate(ctx, u) elif u.name == 'Add': diff --git a/test/test_graph.py b/test/test_graph.py index 3dfd4442b..c459694dd 100644 --- a/test/test_graph.py +++ b/test/test_graph.py @@ -178,7 +178,7 @@ def testConnected(self): def testSub(self): g1 = self.graph - g2 = Graph(store=g2.store) + g2 = Graph(store=g1.store) tarek = self.tarek # michel = self.michel