summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNicholas Car <nicholas.car@surroundaustralia.com>2020-05-25 12:44:49 +1000
committerGitHub <noreply@github.com>2020-05-25 12:44:49 +1000
commit9c064ac15fe801a5e5cb2b4ce7b43e087e1be0a6 (patch)
treea4ed2c89e772d911da881d4bb70af43413e83636
parent91037207580838e41c07eb457bd65d7cc6d6ed85 (diff)
parentbc213dc048454ceb2d01865a6111b0766277aa8b (diff)
downloadrdflib-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.py2
-rw-r--r--docs/intro_to_creating_rdf.rst38
-rw-r--r--docs/rdf_terms.rst5
-rw-r--r--rdflib/__init__.py2
-rw-r--r--rdflib/container.py265
-rw-r--r--requirements.dev.txt3
-rw-r--r--test/test_container.py77
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()