diff options
author | Nicholas Car <nicholas.car@surroundaustralia.com> | 2020-05-25 12:44:49 +1000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-05-25 12:44:49 +1000 |
commit | 9c064ac15fe801a5e5cb2b4ce7b43e087e1be0a6 (patch) | |
tree | a4ed2c89e772d911da881d4bb70af43413e83636 | |
parent | 91037207580838e41c07eb457bd65d7cc6d6ed85 (diff) | |
parent | bc213dc048454ceb2d01865a6111b0766277aa8b (diff) | |
download | rdflib-9c064ac15fe801a5e5cb2b4ce7b43e087e1be0a6.tar.gz |
Merge pull request #997 from kushagr08/master
Fix #280: Added container.py for adding container class and seq, alt and bag as it's subclasses
-rw-r--r-- | docs/conf.py | 2 | ||||
-rw-r--r-- | docs/intro_to_creating_rdf.rst | 38 | ||||
-rw-r--r-- | docs/rdf_terms.rst | 5 | ||||
-rw-r--r-- | rdflib/__init__.py | 2 | ||||
-rw-r--r-- | rdflib/container.py | 265 | ||||
-rw-r--r-- | requirements.dev.txt | 3 | ||||
-rw-r--r-- | test/test_container.py | 77 |
7 files changed, 380 insertions, 12 deletions
diff --git a/docs/conf.py b/docs/conf.py index dbecc10b..dd9087c3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -154,7 +154,7 @@ html_logo = "_static/RDFlib.png" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -html_favicon = "_static/logo-rdflib.ico" +html_favicon = "_static/RDFlib.ico" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, diff --git a/docs/intro_to_creating_rdf.rst b/docs/intro_to_creating_rdf.rst index d676a5d5..5d58945b 100644 --- a/docs/intro_to_creating_rdf.rst +++ b/docs/intro_to_creating_rdf.rst @@ -128,24 +128,46 @@ When removing, it is possible to leave parts of the triple unspecified (i.e. pas .. code-block:: python - g.remove((bob, None, None)) # remove all triples about bob + g.remove((bob, None, None)) # remove all triples about bob An example ---------- LiveJournal produces FOAF data for their users, but they seem to use -``foaf:member_name`` for a person's full name. To align with data from -other sources, it would be nice to have ``foaf:name`` act as a synonym -for ``foaf:member_name`` (a poor man's one-way -``owl:equivalentProperty``): +``foaf:member_name`` for a person's full name but ``foaf:member_name`` +isn't in FOAF's namespace and perhaps they should have used ``foaf:name`` + +To retrieve some LiveJournal data, add a ``foaf:name`` for every +``foaf:member_name`` and then remove the ``foaf:member_name`` values to +ensure the data actually aligns with other FOAF data, we could do this: .. code-block:: python + from rdflib import Graph from rdflib.namespace import FOAF - g.parse("http://danbri.livejournal.com/data/foaf") + + g = Graph() + # get the data + g.parse("http://danbri.livejournal.com/data/foaf") + + # for every foaf:member_name, add foaf:name and remove foaf:member_name for s, p, o in g.triples((None, FOAF['member_name'], None)): g.add((s, FOAF['name'], o)) + g.remove((s, FOAF['member_name'], o)) + +.. note:: Since rdflib 5.0.0, using ``foaf:member_name`` is somewhat prevented in RDFlib since FOAF is declared + as a :meth:`~rdflib.namespace.ClosedNamespace` class instance that has a closed set of members and + ``foaf:member_name`` isn't one of them! If LiveJournal used RDFlib 5.0.0, an error would have been raised for + ``foaf:member_name`` when the triple was created. + + +Creating Containers & Collections +--------------------------------- +There are two convenience classes for RDF Containers & Collections which you can use instead of declaring each +triple of a Containers or a Collections individually: + + * :meth:`~rdflib.container.Container` (also ``Bag``, ``Seq`` & ``Alt``) and + * :meth:`~rdflib.collection.Collection` -Note that since rdflib 5.0.0, using ``foaf:member_name`` is somewhat prevented in rdflib since FOAF is declared as a :meth:`~rdflib.namespace.ClosedNamespace` -class instance that has a closed set of members and ``foaf:member_name`` isnt one of them! +See their documentation for how. diff --git a/docs/rdf_terms.rst b/docs/rdf_terms.rst index c16339de..a520bc5c 100644 --- a/docs/rdf_terms.rst +++ b/docs/rdf_terms.rst @@ -20,7 +20,7 @@ matching nodes by term-patterns probably will only be terms and not nodes. BNodes ====== - In RDF, a blank node (also called BNode) is a node in an RDF graph representing a resource for which a URI or literal is not given. The resource represented by a blank node is also called an anonymous resource. By RDF standard a blank node can only be used as subject or object in an RDF triple, although in some syntaxes like Notation 3 [1] it is acceptable to use a blank node as a predicate. If a blank node has a node ID (not all blank nodes are labelled in all RDF serializations), it is limited in scope to a serialization of a particular RDF graph, i.e. the node p1 in the subsequent example does not represent the same node as a node named p1 in any other graph --`wikipedia`__ +In RDF, a blank node (also called BNode) is a node in an RDF graph representing a resource for which a URI or literal is not given. The resource represented by a blank node is also called an anonymous resource. By RDF standard a blank node can only be used as subject or object in an RDF triple, although in some syntaxes like Notation 3 [1] it is acceptable to use a blank node as a predicate. If a blank node has a node ID (not all blank nodes are labelled in all RDF serializations), it is limited in scope to a serialization of a particular RDF graph, i.e. the node p1 in the subsequent example does not represent the same node as a node named p1 in any other graph --`wikipedia`__ .. __: http://en.wikipedia.org/wiki/Blank_node @@ -40,7 +40,7 @@ BNodes URIRefs ======= - A URI reference within an RDF graph is a Unicode string that does not contain any control characters ( #x00 - #x1F, #x7F-#x9F) and would produce a valid URI character sequence representing an absolute URI with optional fragment identifier -- `W3 RDF Concepts`__ +A URI reference within an RDF graph is a Unicode string that does not contain any control characters ( #x00 - #x1F, #x7F-#x9F) and would produce a valid URI character sequence representing an absolute URI with optional fragment identifier -- `W3 RDF Concepts`__ .. __: http://www.w3.org/TR/rdf-concepts/#section-Graph-URIref @@ -159,4 +159,3 @@ All this happens automatically when creating ``Literal`` objects by passing Pyth You can add custom data-types with :func:`rdflib.term.bind`, see also :mod:`examples.custom_datatype` - diff --git a/rdflib/__init__.py b/rdflib/__init__.py index 837764a3..bce8204f 100644 --- a/rdflib/__init__.py +++ b/rdflib/__init__.py @@ -196,3 +196,5 @@ assert plugin assert query from rdflib import util + +from .container import * diff --git a/rdflib/container.py b/rdflib/container.py new file mode 100644 index 00000000..5960d0a7 --- /dev/null +++ b/rdflib/container.py @@ -0,0 +1,265 @@ +from rdflib.namespace import RDF
+from rdflib.term import BNode
+from rdflib import URIRef
+from random import randint
+
+__all__ = ["Container", "Bag", "Seq", "Alt", "NoElementException"]
+
+
+class Container(object):
+ """A class for constructing RDF containers, as per https://www.w3.org/TR/rdf11-mt/#rdf-containers
+
+ Basic usage, creating a ``Bag`` and adding to it::
+
+ >>> from rdflib import Graph, BNode, Literal, Bag
+ >>> g = Graph()
+ >>> b = Bag(g, BNode(), [Literal("One"), Literal("Two"), Literal("Three")])
+ >>> print(g.serialize(format="turtle").decode())
+ @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
+ <BLANKLINE>
+ [] a rdf:Bag ;
+ rdf:_1 "One" ;
+ rdf:_2 "Two" ;
+ rdf:_3 "Three" .
+ <BLANKLINE>
+ <BLANKLINE>
+
+ >>> # print out an item using an index reference
+ >>> print(b[2])
+ Two
+
+ >>> # add a new item
+ >>> b.append(Literal("Hello"))
+ >>> print(g.serialize(format="turtle").decode())
+ @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
+ <BLANKLINE>
+ [] a rdf:Bag ;
+ rdf:_1 "One" ;
+ rdf:_2 "Two" ;
+ rdf:_3 "Three" ;
+ rdf:_4 "Hello" .
+ <BLANKLINE>
+ <BLANKLINE>
+
+ """
+
+ def __init__(self, graph, uri, seq=[], rtype="Bag"):
+ """Creates a Container
+
+ :param graph: a Graph instance
+ :param uri: URI or Blank Node of the Container
+ :param seq: the elements of the Container
+ :param rtype: the type of Container, one of "Bag", "Seq" or "Alt"
+ """
+
+ self.graph = graph
+ self.uri = uri or BNode()
+ self._len = 0
+ self._rtype = rtype # rdf:Bag or rdf:Seq or rdf:Alt
+
+ self.append_multiple(seq)
+
+ # adding triple corresponding to container type
+ self.graph.add((self.uri, RDF.type, RDF[self._rtype]))
+
+ def n3(self):
+
+ items = []
+ for i in range(len(self)):
+
+ v = self[i + 1]
+ items.append(v)
+
+ return "( %s )" % " ".join([a.n3() for a in items])
+
+ def _get_container(self):
+ """Returns the URI of the container"""
+
+ return self.uri
+
+ def __len__(self):
+ """Number of items in container"""
+
+ return self._len
+
+ def type_of_conatiner(self):
+ return self._rtype
+
+ def index(self, item):
+ """Returns the 1-based numerical index of the item in the container"""
+
+ pred = self.graph.predicates(self.uri, item)
+ if not pred:
+ raise ValueError("%s is not in %s" % (item, "container"))
+ LI_INDEX = URIRef(str(RDF) + "_")
+
+ i = None
+ for p in pred:
+ i = int(p.replace(LI_INDEX, ""))
+ return i
+
+ def __getitem__(self, key):
+ """Returns item of the container at index key"""
+
+ c = self._get_container()
+
+ assert isinstance(key, int)
+ elem_uri = str(RDF) + "_" + str(key)
+ if key <= 0 or key > len(self):
+ raise KeyError(key)
+ v = self.graph.value(c, URIRef(elem_uri))
+ if v:
+ return v
+ else:
+ raise KeyError(key)
+
+ def __setitem__(self, key, value):
+ """Sets the item at index key or predicate rdf:_key of the container to value"""
+
+ assert isinstance(key, int)
+
+ c = self._get_container()
+ elem_uri = str(RDF) + "_" + str(key)
+ if key <= 0 or key > len(self):
+ raise KeyError(key)
+
+ self.graph.set((c, URIRef(elem_uri), value))
+
+ def __delitem__(self, key):
+ """Removing the item with index key or predicate rdf:_key"""
+
+ assert isinstance(key, int)
+ if key <= 0 or key > len(self):
+ raise KeyError(key)
+
+ graph = self.graph
+ container = self.uri
+ elem_uri = str(RDF) + "_" + str(key)
+ graph.remove((container, URIRef(elem_uri), None))
+ for j in range(key + 1, len(self) + 1):
+ elem_uri = str(RDF) + "_" + str(j)
+ v = graph.value(container, URIRef(elem_uri))
+ graph.remove((container, URIRef(elem_uri), v))
+ elem_uri = str(RDF) + "_" + str(j - 1)
+ graph.add((container, URIRef(elem_uri), v))
+
+ self._len -= 1
+
+ def items(self):
+ """Returns a list of all items in the container"""
+
+ l_ = []
+ container = self.uri
+ i = 1
+ while True:
+ elem_uri = str(RDF) + "_" + str(i)
+
+ if (container, URIRef(elem_uri), None) in self.graph:
+ i += 1
+ l_.append(self.graph.value(container, URIRef(elem_uri)))
+ else:
+ break
+ return l_
+
+ def end(self): #
+
+ # find end index (1-based) of container
+
+ container = self.uri
+ i = 1
+ while True:
+ elem_uri = str(RDF) + "_" + str(i)
+
+ if (container, URIRef(elem_uri), None) in self.graph:
+ i += 1
+ else:
+ return i - 1
+
+ def append(self, item):
+ """Adding item to the end of the container"""
+
+ end = self.end()
+ elem_uri = str(RDF) + "_" + str(end + 1)
+ container = self.uri
+ self.graph.add((container, URIRef(elem_uri), item))
+ self._len += 1
+
+ def append_multiple(self, other):
+ """Adding multiple elements to the container to the end which are in python list other"""
+
+ end = self.end() # it should return the last index
+
+ container = self.uri
+ for item in other:
+
+ end += 1
+ self._len += 1
+ elem_uri = str(RDF) + "_" + str(end)
+ self.graph.add((container, URIRef(elem_uri), item))
+
+ def clear(self):
+ """Removing all elements from the container"""
+
+ container = self.uri
+ graph = self.graph
+ i = 1
+ while True:
+ elem_uri = str(RDF) + "_" + str(i)
+ if (container, URIRef(elem_uri), None) in self.graph:
+ graph.remove((container, URIRef(elem_uri), None))
+ i += 1
+ else:
+ break
+ self._len = 0
+
+
+class Bag(Container):
+ """Unordered container (no preference order of elements)"""
+
+ def __init__(self, graph, uri, seq=[]):
+ Container.__init__(self, graph, uri, seq, "Bag")
+
+
+class Alt(Container):
+ def __init__(self, graph, uri, seq=[]):
+ Container.__init__(self, graph, uri, seq, "Alt")
+
+ def anyone(self):
+ if len(self) == 0:
+ raise NoElementException()
+ else:
+ p = randint(1, len(self))
+ item = self.__getitem__(p)
+ return item
+
+
+class Seq(Container):
+ def __init__(self, graph, uri, seq=[]):
+ Container.__init__(self, graph, uri, seq, "Seq")
+
+ def add_at_position(self, pos, item):
+ assert isinstance(pos, int)
+ if pos <= 0 or pos > len(self) + 1:
+ raise ValueError("Invalid Position for inserting element in rdf:Seq")
+
+ if pos == len(self) + 1:
+ self.append(item)
+ else:
+ for j in range(len(self), pos - 1, -1):
+ container = self._get_container()
+ elem_uri = str(RDF) + "_" + str(j)
+ v = self.graph.value(container, URIRef(elem_uri))
+ self.graph.remove((container, URIRef(elem_uri), v))
+ elem_uri = str(RDF) + "_" + str(j + 1)
+ self.graph.add((container, URIRef(elem_uri), v))
+ elem_uri_pos = str(RDF) + "_" + str(pos)
+ self.graph.add((container, URIRef(elem_uri_pos), item))
+ self._len += 1
+
+
+class NoElementException(Exception):
+ def __init__(self, message="rdf:Alt Container is empty"):
+ self.message = message
+
+ def __str__(self):
+ return self.message
diff --git a/requirements.dev.txt b/requirements.dev.txt new file mode 100644 index 00000000..7e6aaf68 --- /dev/null +++ b/requirements.dev.txt @@ -0,0 +1,3 @@ +sphinx +sphinxcontrib-apidoc +black diff --git a/test/test_container.py b/test/test_container.py new file mode 100644 index 00000000..ab98114d --- /dev/null +++ b/test/test_container.py @@ -0,0 +1,77 @@ +from rdflib.term import BNode
+from rdflib.term import Literal
+from rdflib import Graph
+from rdflib.container import *
+import unittest
+
+
+class TestContainer(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ cls.g = Graph()
+ cls.c1 = Bag(cls.g, BNode())
+ cls.c2 = Bag(cls.g, BNode(), [Literal("1"), Literal("2"), Literal("3"), Literal("4")])
+ cls.c3 = Alt(cls.g, BNode(), [Literal("1"), Literal("2"), Literal("3"), Literal("4")])
+ cls.c4 = Seq(cls.g, BNode(), [Literal("1"), Literal("2"), Literal("3"), Literal("4")])
+
+ def testA(self):
+ self.assertEqual(len(self.c1) == 0, True)
+
+ def testB(self):
+ self.assertEqual(len(self.c2) == 4, True)
+
+ def testC(self):
+ self.c2.append(Literal("5"))
+ del self.c2[2]
+ self.assertEqual(len(self.c2) == 4, True)
+
+ def testD(self):
+ self.assertEqual(self.c2.index(Literal("5")) == 4, True)
+
+ def testE(self):
+ self.assertEqual(self.c2[2] == Literal("3"), True)
+
+ def testF(self):
+ self.c2[2] = Literal("9")
+ self.assertEqual(self.c2[2] == Literal("9"), True)
+
+ def testG(self):
+ self.c2.clear()
+ self.assertEqual(len(self.c2) == 0, True)
+
+ def testH(self):
+ self.c2.append_multiple([Literal("80"), Literal("90")])
+ self.assertEqual(self.c2[1] == Literal("80"), True)
+
+ def testI(self):
+ self.assertEqual(self.c2[2] == Literal("90"), True)
+
+ def testJ(self):
+ self.assertEqual(len(self.c2) == 2, True)
+
+ def testK(self):
+ self.assertEqual(self.c2.end() == 2, True)
+
+ def testL(self):
+ self.assertEqual(self.c3.anyone() in [Literal("1"), Literal("2"), Literal("3"), Literal("4")], True)
+
+ def testM(self):
+ self.c4.add_at_position(3, Literal("60"))
+ self.assertEqual(len(self.c4) == 5, True)
+
+ def testN(self):
+ self.assertEqual(self.c4.index(Literal("60")) == 3, True)
+
+ def testO(self):
+ self.assertEqual(self.c4.index(Literal("3")) == 4, True)
+
+ def testP(self):
+ self.assertEqual(self.c4.index(Literal("4")) == 5, True)
+
+ def testQ(self):
+ self.assertEqual(self.c2.index(Literal("1000")) == 3, False) # there is no Literal("1000") in the Bag
+
+
+if __name__ == "__main__":
+ unittest.main()
|