diff --git a/rdflib/namespace/__init__.py b/rdflib/namespace/__init__.py index 4077b0be3..eb8e2eeed 100644 --- a/rdflib/namespace/__init__.py +++ b/rdflib/namespace/__init__.py @@ -226,6 +226,7 @@ def __repr__(self) -> 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 @@ def __repr__(self) -> 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 @@ def __getitem__(cls, name: str, default=None) -> URIRef: 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 @@ def __getitem__(cls, name: str, default=None) -> URIRef: 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 = "" + return f"Namespace({ns_repr})" def __str__(cls) -> str: - return str(cls._NS) + try: + return str(cls._NS) + except AttributeError: + return "" 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 @@ def __dir__(cls) -> Iterable[str]: 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 ea8e12969..5860e8eb2 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 "" 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(