From: Ashley Sommer <ashleysommer@gmail.com>
Date: Fri, 1 Nov 2024 14:24:41 +1000
Subject: Fix defined namespace warnings (#2964) (#2965)

* Fix defined namespace warnings

Current docs-generation tests are polluted by lots of warnings that occur when Sphinx tries to read various parts of DefinedNamespace.

* Fix tests that no longer need incorrect exceptions handled.

* fix black formatting in test file

* Undo typing changes, so this works on current pre-3.9 branch

* better handling for any/all double-underscore properties

* Don't include __slots__ in dir().
---
 rdflib/namespace/__init__.py                 | 44 ++++++++++++++++++----------
 test/test_namespace/test_definednamespace.py | 28 ++++++------------
 2 files changed, 38 insertions(+), 34 deletions(-)

diff --git a/rdflib/namespace/__init__.py b/rdflib/namespace/__init__.py
index 4077b0b..eb8e2ee 100644
--- a/rdflib/namespace/__init__.py
+++ b/rdflib/namespace/__init__.py
@@ -226,6 +226,7 @@ class URIPattern(str):
 # considered part of __dir__ results. These should be all annotations on
 # `DefinedNamespaceMeta`.
 _DFNS_RESERVED_ATTRS: Set[str] = {
+    "__slots__",
     "_NS",
     "_warn",
     "_fail",
@@ -244,6 +245,8 @@ _IGNORED_ATTR_LOOKUP: Set[str] = {
 class DefinedNamespaceMeta(type):
     """Utility metaclass for generating URIRefs with a common prefix."""
 
+    __slots__: Tuple[str, ...] = tuple()
+
     _NS: Namespace
     _warn: bool = True
     _fail: bool = False  # True means mimic ClosedNamespace
@@ -255,15 +258,11 @@ class DefinedNamespaceMeta(type):
         name = str(name)
 
         if name in _DFNS_RESERVED_ATTRS:
-            raise AttributeError(
-                f"DefinedNamespace like object has no attribute {name!r}"
+            raise KeyError(
+                f"DefinedNamespace like object has no access item named {name!r}"
             )
         elif name in _IGNORED_ATTR_LOOKUP:
             raise KeyError()
-        if str(name).startswith("__"):
-            # NOTE on type ignore: This seems to be a real bug, super() does not
-            # implement this method, it will fail if it is ever reached.
-            return super().__getitem__(name, default)  # type: ignore[misc] # undefined in superclass
         if (cls._warn or cls._fail) and name not in cls:
             if cls._fail:
                 raise AttributeError(f"term '{name}' not in namespace '{cls._NS}'")
@@ -277,26 +276,39 @@ class DefinedNamespaceMeta(type):
     def __getattr__(cls, name: str):
         if name in _IGNORED_ATTR_LOOKUP:
             raise AttributeError()
+        elif name in _DFNS_RESERVED_ATTRS:
+            raise AttributeError(
+                f"DefinedNamespace like object has no attribute {name!r}"
+            )
+        elif name.startswith("__"):
+            return super(DefinedNamespaceMeta, cls).__getattribute__(name)
         return cls.__getitem__(name)
 
     def __repr__(cls) -> str:
-        return f"Namespace({str(cls._NS)!r})"
+        try:
+            ns_repr = repr(cls._NS)
+        except AttributeError:
+            ns_repr = "<DefinedNamespace>"
+        return f"Namespace({ns_repr})"
 
     def __str__(cls) -> str:
-        return str(cls._NS)
+        try:
+            return str(cls._NS)
+        except AttributeError:
+            return "<DefinedNamespace>"
 
     def __add__(cls, other: str) -> URIRef:
         return cls.__getitem__(other)
 
     def __contains__(cls, item: str) -> bool:
         """Determine whether a URI or an individual item belongs to this namespace"""
+        try:
+            this_ns = cls._NS
+        except AttributeError:
+            return False
         item_str = str(item)
-        if item_str.startswith("__"):
-            # NOTE on type ignore: This seems to be a real bug, super() does not
-            # implement this method, it will fail if it is ever reached.
-            return super().__contains__(item)  # type: ignore[misc] # undefined in superclass
-        if item_str.startswith(str(cls._NS)):
-            item_str = item_str[len(str(cls._NS)) :]
+        if item_str.startswith(str(this_ns)):
+            item_str = item_str[len(str(this_ns)) :]
         return any(
             item_str in c.__annotations__
             or item_str in c._extras
@@ -313,7 +325,7 @@ class DefinedNamespaceMeta(type):
         return values
 
     def as_jsonld_context(self, pfx: str) -> dict:  # noqa: N804
-        """Returns this DefinedNamespace as a a JSON-LD 'context' object"""
+        """Returns this DefinedNamespace as a JSON-LD 'context' object"""
         terms = {pfx: str(self._NS)}
         for key, term in self.__annotations__.items():
             if issubclass(term, URIRef):
@@ -328,6 +340,8 @@ class DefinedNamespace(metaclass=DefinedNamespaceMeta):
     Warnings are emitted if unknown members are referenced if _warn is True
     """
 
+    __slots__: Tuple[str, ...] = tuple()
+
     def __init__(self):
         raise TypeError("namespace may not be instantiated")
 
diff --git a/test/test_namespace/test_definednamespace.py b/test/test_namespace/test_definednamespace.py
index ea8e129..5860e8e 100644
--- a/test/test_namespace/test_definednamespace.py
+++ b/test/test_namespace/test_definednamespace.py
@@ -299,14 +299,9 @@ def test_repr(dfns: Type[DefinedNamespace]) -> None:
     ns_uri = f"{prefix}{dfns_info.suffix}"
     logging.debug("ns_uri = %s", ns_uri)
 
-    repr_str: Optional[str] = None
-
-    with ExitStack() as xstack:
-        if dfns_info.suffix is None:
-            xstack.enter_context(pytest.raises(AttributeError))
-        repr_str = f"{dfns_info.dfns!r}"
+    repr_str: str = f"{dfns_info.dfns!r}"
     if dfns_info.suffix is None:
-        assert repr_str is None
+        assert "<DefinedNamespace>" in repr_str
     else:
         assert repr_str is not None
         repro = eval(repr_str)
@@ -368,20 +363,15 @@ def test_contains(
     dfns_info = get_dfns_info(dfns)
     if dfns_info.suffix is not None:
         logging.debug("dfns_info = %s", dfns_info)
-    if dfns_info.has_attrs is False:
+    if dfns_info.has_attrs is False or dfns_info.suffix is None:
         is_defined = False
-    does_contain: Optional[bool] = None
-    with ExitStack() as xstack:
-        if dfns_info.suffix is None:
-            xstack.enter_context(pytest.raises(AttributeError))
-        does_contain = attr_name in dfns
-    if dfns_info.suffix is not None:
-        if is_defined:
-            assert does_contain is True
-        else:
-            assert does_contain is False
+
+    does_contain: bool = attr_name in dfns
+
+    if is_defined:
+        assert does_contain is True
     else:
-        assert does_contain is None
+        assert does_contain is False
 
 
 @pytest.mark.parametrize(
