Repository: RDFLib/rdflib (Python)

Issue:
Consistency Namespace vs. ClosedNamespace
As I understand rdflib.Namespace and ClosedNamespace, they are both meant as auxiliaries to simplify the creation of URIRefs in Python code, e.g. `RDF.type` or `OWL.Class`. In addition, ClosedNamespace provides an additional vocabulary check, e.g., `RDF.spam` will raise an exception.

However, sometimes other uses of Namespace/ClosedNamespace can be useful, e.g., string comparison. At this stage, it is quite inconvenient that Namespace is derived from unicode, whereas ClosedNamespace is derived from the standard object, and the actual namespace string is an instance attribute (uri). In the interest of consistency, I propose that ClosedNamespace should also be derived from unicode (or even from Namespace).

Regards

closednamespace inheritance fix on 5.0.0-dev
the fix for #542 in #551 was backwards incompatible and thus not fit for the patch release 4.2.2 and reverted in #595

this issue is a reminder to either replay bccc76132a7ca19cfa979026e2d5f9ecd8ca6dcf and ae1f6397fe47be7bd6912dae8b08c4007eb9d2ac on top of 5.0.0-dev or maybe drop the unicode base-class for `Namespace` altogether...

@gromgull any preference?



Discussion:
can you give us a use-case where you can't just compare `str(Namespace(...))` with `str(ClosedNamespace(...))`?

I totally agree that it's weird that they derive from different things. It also looks trivial to fix. I've tried - let me see if Travis agrees :)

Also totally agree, thanks for addressing this! :+1:

the change mentioned in #551 is backwards incompatible and will be moved to rdflib 5.0.0 .


I would drop unicode for namespace, I cannot really see any use-cases why it is useful that it's a subclass at all? 

Then let `ClosedNamespace` inherit from `Namespace`

:+1: Totally agree.

please have a look at #597

>  I cannot really see any use-cases why it is useful that it's a subclass at all?

How does one filter for "properties" from `DCTERMS` namespace if not by:

```py
for p, o in g.predicate_objects(s):
    if p.startswith(DCTERMS):
        ...
```



Patch:
diff --git a/rdflib/namespace.py b/rdflib/namespace.py
index 5068fa162..69bce0f34 100644
--- a/rdflib/namespace.py
+++ b/rdflib/namespace.py
@@ -100,8 +100,7 @@
 
 
 class Namespace(str):
-
-    __doc__ = """
+    """
     Utility class for quickly generating URIRefs with a common prefix
 
     >>> from rdflib import Namespace
@@ -126,31 +125,44 @@ def __new__(cls, value):
 
     @property
     def title(self):
+        # Override for DCTERMS.title to return a URIRef instead of str.title method
         return URIRef(self + "title")
 
     def term(self, name):
         # need to handle slices explicitly because of __getitem__ override
         return URIRef(self + (name if isinstance(name, str) else ""))
 
-    def __getitem__(self, key, default=None):
+    def __getitem__(self, key):
         return self.term(key)
 
     def __getattr__(self, name):
         if name.startswith("__"):  # ignore any special Python names!
             raise AttributeError
-        else:
-            return self.term(name)
+        return self.term(name)
 
     def __repr__(self):
-        return "Namespace(%r)" % str(self)
-    
+        return f"Namespace({super().__repr__()})"
+
     def __contains__(self, ref):
+        """Allows to check if a URI is within (starts with) this Namespace.
+
+        >>> from rdflib import URIRef
+        >>> namespace = Namespace('http://example.org/')
+        >>> uri = URIRef('http://example.org/foo')
+        >>> uri in namespace
+        True
+        >>> person_class = namespace['Person']
+        >>> person_class in namespace
+        True
+        >>> obj = URIRef('http://not.example.org/bar')
+        >>> obj in namespace
+        False
+        """
         return ref.startswith(self) # test namespace membership with "ref in ns" syntax
 
 
 class URIPattern(str):
-
-    __doc__ = """
+    """
     Utility class for creating URIs according to some pattern
     This supports either new style formatting with .format
     or old-style with % operator
@@ -169,35 +181,38 @@ def __new__(cls, value):
         return rt
 
     def __mod__(self, *args, **kwargs):
-        return URIRef(str(self).__mod__(*args, **kwargs))
+        return URIRef(super().__mod__(*args, **kwargs))
 
     def format(self, *args, **kwargs):
-        return URIRef(str.format(self, *args, **kwargs))
+        return URIRef(super().format(*args, **kwargs))
 
     def __repr__(self):
-        return "URIPattern(%r)" % str(self)
+        return f"URIPattern({super().__repr__()})"
 
 
-class ClosedNamespace(object):
+class ClosedNamespace(Namespace):
     """
     A namespace with a closed list of members
 
     Trying to create terms not listed is an error
     """
 
-    def __init__(self, uri, terms):
-        self.uri = uri
-        self.__uris = {}
-        for t in terms:
-            self.__uris[t] = URIRef(self.uri + t)
+    def __new__(cls, uri, terms):
+        rt = super().__new__(cls, uri)
+        rt.__uris = {t: URIRef(rt + t) for t in terms}
+        return rt
+
+    @property
+    def uri(self):  # Back-compat
+        return str(self)
 
     def term(self, name):
-        if name not in self.__uris:
-            raise KeyError("term '{}' not in namespace '{}'".format(name, self.uri))
-        else:
-            return self.__uris[name]
+        uri = self.__uris.get(name)
+        if uri is None:
+            raise KeyError(f"term '{name}' not in namespace '{self}'")
+        return uri
 
-    def __getitem__(self, key, default=None):
+    def __getitem__(self, key):
         return self.term(key)
 
     def __getattr__(self, name):
@@ -209,20 +224,17 @@ def __getattr__(self, name):
             except KeyError as e:
                 raise AttributeError(e)
 
-    def __str__(self):
-        return str(self.uri)
-
     def __repr__(self):
-        return "rdf.namespace.ClosedNamespace(%r)" % str(self.uri)
+        return f"{self.__module__}.{self.__class__.__name__}({str(self)!r})"
 
     def __dir__(self):
-        return list(self._ClosedNamespace__uris)
-    
+        return list(self.__uris)
+
     def __contains__(self, ref):
         return ref in self.__uris.values() # test namespace membership with "ref in ns" syntax
 
     def _ipython_key_completions_(self):
-        return dir(self.uri)
+        return dir(self)
 
 
 class _RDFNamespace(ClosedNamespace):
@@ -230,9 +242,10 @@ class _RDFNamespace(ClosedNamespace):
     Closed namespace for RDF terms
     """
 
-    def __init__(self):
-        super(_RDFNamespace, self).__init__(
-            URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#"),
+    def __new__(cls):
+        return super().__new__(
+            cls,
+            "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
             terms=[
                 # Syntax Names
                 "RDF",
@@ -284,9 +297,9 @@ def term(self, name):
                 pass
             else:
                 if i > 0:
-                    return URIRef("%s_%s" % (self.uri, i))
+                    return URIRef(f"{self}_{i}")
 
-        return super(_RDFNamespace, self).term(name)
+        return super().term(name)
 
 
 CSVW = Namespace("http://www.w3.org/ns/csvw#")
@@ -640,7 +653,7 @@ def __init__(self, graph):
 
     def __contains__(self, ref):
         # checks if a reference is in any of the managed namespaces with syntax
-        # "ref in manager". Note that we don't use "ref in ns", as 
+        # "ref in manager". Note that we don't use "ref in ns", as
         # NamespaceManager.namespaces() returns Iterator[Tuple[str, URIRef]]
         # rather than Iterator[Tuple[str, Namespace]]
         return any(ref.startswith(ns) for prefix, ns in self.namespaces())
@@ -652,11 +665,10 @@ def reset(self):
         for p, n in self.namespaces():  # repopulate the trie
             insert_trie(self.__trie, str(n))
 
-    def __get_store(self):
+    @property
+    def store(self):
         return self.graph.store
 
-    store = property(__get_store)
-
     def qname(self, uri):
         prefix, namespace, name = self.compute_qname(uri)
         if prefix == "":
@@ -812,7 +824,7 @@ def bind(self, prefix, namespace, override=True, replace=False):
             prefix = ""
         elif " " in prefix:
             raise KeyError("Prefixes may not contain spaces.")
-            
+
         bound_namespace = self.store.namespace(prefix)
         # Check if the bound_namespace contains a URI
         # and if so convert it into a URIRef for comparison
diff --git a/rdflib/plugins/sparql/operators.py b/rdflib/plugins/sparql/operators.py
index b00056d21..9bf8b19ed 100644
--- a/rdflib/plugins/sparql/operators.py
+++ b/rdflib/plugins/sparql/operators.py
@@ -36,7 +36,7 @@
 
 
 # closed namespace, langString isn't in it
-RDF_langString = URIRef(RDF.uri + "langString")
+RDF_langString = RDF.langString
 
 
 def Builtin_IRI(expr, ctx):


Test patch:
diff --git a/test/test_namespace.py b/test/test_namespace.py
index 3c321f8cb..87b7f046a 100644
--- a/test/test_namespace.py
+++ b/test/test_namespace.py
@@ -1,10 +1,90 @@
 import unittest
+from unittest.case import expectedFailure
 
 from rdflib.graph import Graph
-from rdflib.namespace import Namespace, FOAF, RDF, RDFS, SH
+from rdflib.namespace import (
+    ClosedNamespace,
+    Namespace,
+    FOAF,
+    RDF,
+    RDFS,
+    SH,
+    DCTERMS,
+    URIPattern,
+)
 from rdflib.term import URIRef
 
 
+class NamespaceTest(unittest.TestCase):
+    def setUp(self):
+        self.ns_str = "http://example.com/name/space/"
+        self.ns = Namespace(self.ns_str)
+
+    def test_repr(self):
+        # NOTE: this assumes ns_str has no characthers that need escaping
+        self.assertIn(self.ns_str, f"{self.ns!r}")
+        self.assertEqual(self.ns, eval(f"{self.ns!r}"))
+
+    def test_str(self):
+        self.assertEqual(self.ns_str, f"{self.ns}")
+
+    def test_member(self):
+        self.assertEqual(f"{self.ns_str}a", f"{self.ns.a}")
+
+    def test_dcterms_title(self):
+        self.assertEqual(DCTERMS.title, URIRef(DCTERMS + 'title'))
+
+    def test_iri(self):
+        prefix = u'http://jörn.loves.encoding.problems/'
+        ns = Namespace(prefix)
+        self.assertEqual(ns, str(prefix))
+        self.assert_(ns[u'jörn'].startswith(ns))
+
+
+class ClosedNamespaceTest(unittest.TestCase):
+    def setUp(self):
+        self.ns_str = "http://example.com/name/space/"
+        self.ns = ClosedNamespace(self.ns_str, ["a", "b", "c"])
+
+    def test_repr(self):
+        # NOTE: this assumes ns_str has no characthers that need escaping
+        self.assertIn(self.ns_str, f"{self.ns!r}")
+
+    @expectedFailure
+    def test_repr_ef(self):
+        """
+        This fails because ClosedNamespace repr does not represent the second argument
+        """
+        self.assertEqual(self.ns, eval(f"{self.ns!r}"))
+
+    def test_str(self):
+        self.assertEqual(self.ns_str, f"{self.ns}")
+
+    def test_member(self):
+        self.assertEqual(f"{self.ns_str}a", f"{self.ns.a}")
+
+    def test_missing_member(self):
+        with self.assertRaises(AttributeError) as context:
+            f"{self.ns.missing}"
+        self.assertIn("missing", f"{context.exception}")
+
+
+class URIPatternTest(unittest.TestCase):
+    def setUp(self):
+        self.pattern_str = "http://example.org/%s/%d/resource"
+        self.pattern = URIPattern(self.pattern_str)
+
+    def test_repr(self):
+        # NOTE: this assumes pattern_str has no characthers that need escaping
+        self.assertIn(self.pattern_str, f"{self.pattern!r}")
+        self.assertEqual(self.pattern, eval(f"{self.pattern!r}"))
+
+    def test_format(self):
+        self.assertEqual(
+            "http://example.org/foo/100/resource", str(self.pattern % ("foo", 100))
+        )
+
+
 class NamespacePrefixTest(unittest.TestCase):
     def test_compute_qname(self):
         """Test sequential assignment of unknown prefixes"""
@@ -123,10 +203,13 @@ def add_not_in_namespace(s):
 
         # a property name within the FOAF namespace
         self.assertEqual(
-            add_not_in_namespace("givenName"),
+            FOAF.givenName,
             URIRef("http://xmlns.com/foaf/0.1/givenName"),
         )
 
+        # namescape can be used as str
+        self.assertTrue(FOAF.givenName.startswith(FOAF))
+
     def test_contains_method(self):
         """Tests for Namespace.__contains__() methods."""