Skip to content

Commit

Permalink
Merge pull request #1825 from aucampia/iwana-20220415T1648-defined_na…
Browse files Browse the repository at this point in the history
…mespace_partialmethod

fix: DefinedNamespace: fixed handling of control attributes.

Merging with only one review as this increases test coverage and is currently blocking documentation builds.
  • Loading branch information
aucampia committed Apr 19, 2022
2 parents e79c840 + fdc6479 commit e30e386
Show file tree
Hide file tree
Showing 2 changed files with 266 additions and 3 deletions.
30 changes: 27 additions & 3 deletions rdflib/namespace/__init__.py
Expand Up @@ -3,7 +3,7 @@
import warnings
from functools import lru_cache
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Tuple, Union
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Set, Tuple, Union
from unicodedata import category
from urllib.parse import urldefrag, urljoin

Expand Down Expand Up @@ -190,6 +190,19 @@ def __repr__(self) -> str:
return f"URIPattern({super().__repr__()})"


# _DFNS_RESERVED_ATTRS are attributes for which DefinedNamespaceMeta should
# always raise AttributeError if they are not defined and which should not be
# considered part of __dir__ results. These should be all annotations on
# `DefinedNamespaceMeta`.
_DFNS_RESERVED_ATTRS: Set[str] = {
"_NS",
"_warn",
"_fail",
"_extras",
"_underscore_num",
}


class DefinedNamespaceMeta(type):
"""Utility metaclass for generating URIRefs with a common prefix."""

Expand All @@ -202,7 +215,13 @@ class DefinedNamespaceMeta(type):
@lru_cache(maxsize=None)
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}"
)
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:
Expand All @@ -218,7 +237,7 @@ def __getattr__(cls, name: str):
return cls.__getitem__(name)

def __repr__(cls) -> str:
return f'Namespace("{cls._NS}")'
return f'Namespace({str(cls._NS)!r})'

def __str__(cls) -> str:
return str(cls._NS)
Expand All @@ -230,6 +249,8 @@ def __contains__(cls, item: str) -> bool:
"""Determine whether a URI or an individual item belongs to this namespace"""
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)) :]
Expand All @@ -242,7 +263,10 @@ def __contains__(cls, item: str) -> bool:
)

def __dir__(cls) -> Iterable[str]:
values = {cls[str(x)] for x in cls.__annotations__}
attrs = {str(x) for x in cls.__annotations__}
# Removing these as they should not be considered part of the namespace.
attrs.difference_update(_DFNS_RESERVED_ATTRS)
values = {cls[str(x)] for x in attrs}
return values

def as_jsonld_context(self, pfx: str) -> dict:
Expand Down
239 changes: 239 additions & 0 deletions test/test_namespace/test_definednamespace.py
@@ -1,10 +1,20 @@
import inspect
import json
import logging
import subprocess
import sys
import warnings
from contextlib import ExitStack
from dataclasses import dataclass
from pathlib import Path
from test.data import TEST_DATA_DIR
from typing import Optional, Type

import pytest

from rdflib import RDF, SKOS
from rdflib.namespace import DefinedNamespace, Namespace
from rdflib.term import URIRef


def test_definednamespace_creator_qb():
Expand Down Expand Up @@ -191,3 +201,232 @@ def test_definednamespace_jsonld_context():
actual = SKOS.as_jsonld_context("skos")

assert actual == expected


prefix = "http://example.com/"


class DFNSNoNS(DefinedNamespace):
defined: URIRef
_defined: URIRef


class DFNSDefaults(DefinedNamespace):
_NS = Namespace(f"{prefix}DFNSDefaults#")
defined: URIRef
_defined: URIRef


class DFNSDefaultsEmpty(DefinedNamespace):
_NS = Namespace(f"{prefix}DFNSDefaultsEmpty#")


class DFNSWarnFailEmpty(DefinedNamespace):
_NS = Namespace(f"{prefix}DFNSWarnFailEmpty#")
_warn = True
_fail = True


class DFNSNoWarnNoFail(DefinedNamespace):
_NS = Namespace(f"{prefix}DFNSNoWarnNoFail#")
_warn = False
_fail = False
defined: URIRef
_defined: URIRef


class DFNSNoWarnFail(DefinedNamespace):
_NS = Namespace(f"{prefix}DFNSNoWarnFail#")
_warn = False
_fail = True
defined: URIRef
_defined: URIRef


class DFNSWarnNoFail(DefinedNamespace):
_NS = Namespace(f"{prefix}DFNSWarnNoFail#")
_warn = True
_fail = False
defined: URIRef
_defined: URIRef


class DFNSWarnFail(DefinedNamespace):
_NS = Namespace(f"{prefix}DFNSWarnFail#")
_warn = True
_fail = True
defined: URIRef
_defined: URIRef


@dataclass
class DFNSInfo:
dfns: Type[DefinedNamespace]
suffix: Optional[str]
has_attrs: bool = True


dfns_infos = [
DFNSInfo(DFNSNoNS, None),
DFNSInfo(DFNSDefaults, "DFNSDefaults#"),
DFNSInfo(DFNSNoWarnNoFail, "DFNSNoWarnNoFail#"),
DFNSInfo(DFNSWarnFail, "DFNSWarnFail#"),
DFNSInfo(DFNSNoWarnFail, "DFNSNoWarnFail#"),
DFNSInfo(DFNSWarnNoFail, "DFNSWarnNoFail#"),
DFNSInfo(DFNSDefaultsEmpty, "DFNSDefaultsEmpty#", False),
DFNSInfo(DFNSWarnFailEmpty, "DFNSWarnFailEmpty#", False),
DFNSInfo(DefinedNamespace, None, False),
]
dfns_list = [item.dfns for item in dfns_infos]


def get_dfns_info(dfns: Type[DefinedNamespace]) -> DFNSInfo:
for dfns_info in dfns_infos:
if dfns_info.dfns is dfns:
return dfns_info
raise ValueError("No DFNSInfo for the DefinedNamespace passed in ...")


@pytest.fixture(
scope="module",
params=[item.dfns for item in dfns_infos],
)
def dfns(request) -> DFNSInfo:
assert issubclass(request.param, DefinedNamespace)
return request.param


def test_repr(dfns: Type[DefinedNamespace]) -> None:
dfns_info = get_dfns_info(dfns)
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}"
if dfns_info.suffix is None:
assert repr_str is None
else:
assert repr_str is not None
repro = eval(repr_str)
assert ns_uri == f"{repro}"


def test_inspect(dfns: Type[DefinedNamespace]) -> None:
"""
`inspect.signature` returns. This is here to check that this does not
trigger infinite recursion.
"""
inspect.signature(dfns, follow_wrapped=True)


@pytest.mark.parametrize(
["attr_name", "is_defined"],
[
("defined", True),
("_defined", True),
("notdefined", False),
("_notdefined", False),
],
)
def test_value(dfns: Type[DefinedNamespace], attr_name: str, is_defined: bool) -> None:
dfns_info = get_dfns_info(dfns)
if dfns_info.has_attrs is False:
is_defined = False
resolved: Optional[str] = None
with ExitStack() as xstack:
warnings_record = xstack.enter_context(warnings.catch_warnings(record=True))
if dfns_info.suffix is None or (not is_defined and dfns._fail is True):
xstack.enter_context(pytest.raises(AttributeError))
resolved = eval(f'dfns.{attr_name}')
if dfns_info.suffix is not None:
if is_defined or dfns._fail is False:
assert f"{prefix}{dfns_info.suffix}{attr_name}" == f"{resolved}"
else:
assert resolved is None
if dfns._warn is False:
assert len(warnings_record) == 0
elif not is_defined and resolved is not None:
assert len(warnings_record) == 1
else:
assert resolved is None


@pytest.mark.parametrize(
["attr_name", "is_defined"],
[
("defined", True),
("_defined", True),
("notdefined", False),
("_notdefined", False),
],
)
def test_contains(
dfns: Type[DefinedNamespace], attr_name: str, is_defined: bool
) -> None:
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:
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
else:
assert does_contain is None


@pytest.mark.parametrize(
["attr_name", "is_defined"],
[
("defined", True),
("_defined", True),
("notdefined", False),
("_notdefined", False),
],
)
def test_hasattr(
dfns: Type[DefinedNamespace], attr_name: str, is_defined: bool
) -> None:
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:
is_defined = False
has_attr: Optional[bool] = None
has_attr = hasattr(dfns, attr_name)
if dfns_info.suffix is not None and (is_defined or dfns._fail is False):
assert has_attr is True
else:
assert has_attr is False


def test_dir(dfns: Type[DefinedNamespace]) -> None:
dfns_info = get_dfns_info(dfns)
does_contain: Optional[bool] = None
with ExitStack() as xstack:
# dir should work for DefinedNamespace as this is called by sphinx to
# document it.
if dfns_info.suffix is None and dfns is not DefinedNamespace:
xstack.enter_context(pytest.raises(AttributeError))
attrs = list(dir(dfns))
if dfns_info.suffix is not None:
if dfns_info.has_attrs:
assert set(attrs) == {
URIRef(f"{prefix}{dfns_info.suffix}defined"),
URIRef(f"{prefix}{dfns_info.suffix}_defined"),
}
else:
assert list(attrs) == []
else:
assert does_contain is None

0 comments on commit e30e386

Please sign in to comment.