Skip to content

Commit e30e386

Browse files
authored
Merge pull request RDFLib#1825 from aucampia/iwana-20220415T1648-defined_namespace_partialmethod
fix: DefinedNamespace: fixed handling of control attributes. Merging with only one review as this increases test coverage and is currently blocking documentation builds.
2 parents e79c840 + fdc6479 commit e30e386

File tree

2 files changed

+266
-3
lines changed

2 files changed

+266
-3
lines changed

rdflib/namespace/__init__.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import warnings
44
from functools import lru_cache
55
from pathlib import Path
6-
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Tuple, Union
6+
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Set, Tuple, Union
77
from unicodedata import category
88
from urllib.parse import urldefrag, urljoin
99

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

192192

193+
# _DFNS_RESERVED_ATTRS are attributes for which DefinedNamespaceMeta should
194+
# always raise AttributeError if they are not defined and which should not be
195+
# considered part of __dir__ results. These should be all annotations on
196+
# `DefinedNamespaceMeta`.
197+
_DFNS_RESERVED_ATTRS: Set[str] = {
198+
"_NS",
199+
"_warn",
200+
"_fail",
201+
"_extras",
202+
"_underscore_num",
203+
}
204+
205+
193206
class DefinedNamespaceMeta(type):
194207
"""Utility metaclass for generating URIRefs with a common prefix."""
195208

@@ -202,7 +215,13 @@ class DefinedNamespaceMeta(type):
202215
@lru_cache(maxsize=None)
203216
def __getitem__(cls, name: str, default=None) -> URIRef:
204217
name = str(name)
218+
if name in _DFNS_RESERVED_ATTRS:
219+
raise AttributeError(
220+
f"DefinedNamespace like object has no attribute {name!r}"
221+
)
205222
if str(name).startswith("__"):
223+
# NOTE on type ignore: This seems to be a real bug, super() does not
224+
# implement this method, it will fail if it is ever reached.
206225
return super().__getitem__(name, default) # type: ignore[misc] # undefined in superclass
207226
if (cls._warn or cls._fail) and name not in cls:
208227
if cls._fail:
@@ -218,7 +237,7 @@ def __getattr__(cls, name: str):
218237
return cls.__getitem__(name)
219238

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

223242
def __str__(cls) -> str:
224243
return str(cls._NS)
@@ -230,6 +249,8 @@ def __contains__(cls, item: str) -> bool:
230249
"""Determine whether a URI or an individual item belongs to this namespace"""
231250
item_str = str(item)
232251
if item_str.startswith("__"):
252+
# NOTE on type ignore: This seems to be a real bug, super() does not
253+
# implement this method, it will fail if it is ever reached.
233254
return super().__contains__(item) # type: ignore[misc] # undefined in superclass
234255
if item_str.startswith(str(cls._NS)):
235256
item_str = item_str[len(str(cls._NS)) :]
@@ -242,7 +263,10 @@ def __contains__(cls, item: str) -> bool:
242263
)
243264

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

248272
def as_jsonld_context(self, pfx: str) -> dict:

test/test_namespace/test_definednamespace.py

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
1+
import inspect
12
import json
3+
import logging
24
import subprocess
35
import sys
6+
import warnings
7+
from contextlib import ExitStack
8+
from dataclasses import dataclass
49
from pathlib import Path
510
from test.data import TEST_DATA_DIR
11+
from typing import Optional, Type
12+
13+
import pytest
614

715
from rdflib import RDF, SKOS
16+
from rdflib.namespace import DefinedNamespace, Namespace
17+
from rdflib.term import URIRef
818

919

1020
def test_definednamespace_creator_qb():
@@ -191,3 +201,232 @@ def test_definednamespace_jsonld_context():
191201
actual = SKOS.as_jsonld_context("skos")
192202

193203
assert actual == expected
204+
205+
206+
prefix = "http://example.com/"
207+
208+
209+
class DFNSNoNS(DefinedNamespace):
210+
defined: URIRef
211+
_defined: URIRef
212+
213+
214+
class DFNSDefaults(DefinedNamespace):
215+
_NS = Namespace(f"{prefix}DFNSDefaults#")
216+
defined: URIRef
217+
_defined: URIRef
218+
219+
220+
class DFNSDefaultsEmpty(DefinedNamespace):
221+
_NS = Namespace(f"{prefix}DFNSDefaultsEmpty#")
222+
223+
224+
class DFNSWarnFailEmpty(DefinedNamespace):
225+
_NS = Namespace(f"{prefix}DFNSWarnFailEmpty#")
226+
_warn = True
227+
_fail = True
228+
229+
230+
class DFNSNoWarnNoFail(DefinedNamespace):
231+
_NS = Namespace(f"{prefix}DFNSNoWarnNoFail#")
232+
_warn = False
233+
_fail = False
234+
defined: URIRef
235+
_defined: URIRef
236+
237+
238+
class DFNSNoWarnFail(DefinedNamespace):
239+
_NS = Namespace(f"{prefix}DFNSNoWarnFail#")
240+
_warn = False
241+
_fail = True
242+
defined: URIRef
243+
_defined: URIRef
244+
245+
246+
class DFNSWarnNoFail(DefinedNamespace):
247+
_NS = Namespace(f"{prefix}DFNSWarnNoFail#")
248+
_warn = True
249+
_fail = False
250+
defined: URIRef
251+
_defined: URIRef
252+
253+
254+
class DFNSWarnFail(DefinedNamespace):
255+
_NS = Namespace(f"{prefix}DFNSWarnFail#")
256+
_warn = True
257+
_fail = True
258+
defined: URIRef
259+
_defined: URIRef
260+
261+
262+
@dataclass
263+
class DFNSInfo:
264+
dfns: Type[DefinedNamespace]
265+
suffix: Optional[str]
266+
has_attrs: bool = True
267+
268+
269+
dfns_infos = [
270+
DFNSInfo(DFNSNoNS, None),
271+
DFNSInfo(DFNSDefaults, "DFNSDefaults#"),
272+
DFNSInfo(DFNSNoWarnNoFail, "DFNSNoWarnNoFail#"),
273+
DFNSInfo(DFNSWarnFail, "DFNSWarnFail#"),
274+
DFNSInfo(DFNSNoWarnFail, "DFNSNoWarnFail#"),
275+
DFNSInfo(DFNSWarnNoFail, "DFNSWarnNoFail#"),
276+
DFNSInfo(DFNSDefaultsEmpty, "DFNSDefaultsEmpty#", False),
277+
DFNSInfo(DFNSWarnFailEmpty, "DFNSWarnFailEmpty#", False),
278+
DFNSInfo(DefinedNamespace, None, False),
279+
]
280+
dfns_list = [item.dfns for item in dfns_infos]
281+
282+
283+
def get_dfns_info(dfns: Type[DefinedNamespace]) -> DFNSInfo:
284+
for dfns_info in dfns_infos:
285+
if dfns_info.dfns is dfns:
286+
return dfns_info
287+
raise ValueError("No DFNSInfo for the DefinedNamespace passed in ...")
288+
289+
290+
@pytest.fixture(
291+
scope="module",
292+
params=[item.dfns for item in dfns_infos],
293+
)
294+
def dfns(request) -> DFNSInfo:
295+
assert issubclass(request.param, DefinedNamespace)
296+
return request.param
297+
298+
299+
def test_repr(dfns: Type[DefinedNamespace]) -> None:
300+
dfns_info = get_dfns_info(dfns)
301+
ns_uri = f"{prefix}{dfns_info.suffix}"
302+
logging.debug("ns_uri = %s", ns_uri)
303+
304+
repr_str: Optional[str] = None
305+
306+
with ExitStack() as xstack:
307+
if dfns_info.suffix is None:
308+
xstack.enter_context(pytest.raises(AttributeError))
309+
repr_str = f"{dfns_info.dfns!r}"
310+
if dfns_info.suffix is None:
311+
assert repr_str is None
312+
else:
313+
assert repr_str is not None
314+
repro = eval(repr_str)
315+
assert ns_uri == f"{repro}"
316+
317+
318+
def test_inspect(dfns: Type[DefinedNamespace]) -> None:
319+
"""
320+
`inspect.signature` returns. This is here to check that this does not
321+
trigger infinite recursion.
322+
"""
323+
inspect.signature(dfns, follow_wrapped=True)
324+
325+
326+
@pytest.mark.parametrize(
327+
["attr_name", "is_defined"],
328+
[
329+
("defined", True),
330+
("_defined", True),
331+
("notdefined", False),
332+
("_notdefined", False),
333+
],
334+
)
335+
def test_value(dfns: Type[DefinedNamespace], attr_name: str, is_defined: bool) -> None:
336+
dfns_info = get_dfns_info(dfns)
337+
if dfns_info.has_attrs is False:
338+
is_defined = False
339+
resolved: Optional[str] = None
340+
with ExitStack() as xstack:
341+
warnings_record = xstack.enter_context(warnings.catch_warnings(record=True))
342+
if dfns_info.suffix is None or (not is_defined and dfns._fail is True):
343+
xstack.enter_context(pytest.raises(AttributeError))
344+
resolved = eval(f'dfns.{attr_name}')
345+
if dfns_info.suffix is not None:
346+
if is_defined or dfns._fail is False:
347+
assert f"{prefix}{dfns_info.suffix}{attr_name}" == f"{resolved}"
348+
else:
349+
assert resolved is None
350+
if dfns._warn is False:
351+
assert len(warnings_record) == 0
352+
elif not is_defined and resolved is not None:
353+
assert len(warnings_record) == 1
354+
else:
355+
assert resolved is None
356+
357+
358+
@pytest.mark.parametrize(
359+
["attr_name", "is_defined"],
360+
[
361+
("defined", True),
362+
("_defined", True),
363+
("notdefined", False),
364+
("_notdefined", False),
365+
],
366+
)
367+
def test_contains(
368+
dfns: Type[DefinedNamespace], attr_name: str, is_defined: bool
369+
) -> None:
370+
dfns_info = get_dfns_info(dfns)
371+
if dfns_info.suffix is not None:
372+
logging.debug("dfns_info = %s", dfns_info)
373+
if dfns_info.has_attrs is False:
374+
is_defined = False
375+
does_contain: Optional[bool] = None
376+
with ExitStack() as xstack:
377+
if dfns_info.suffix is None:
378+
xstack.enter_context(pytest.raises(AttributeError))
379+
does_contain = attr_name in dfns
380+
if dfns_info.suffix is not None:
381+
if is_defined:
382+
assert does_contain is True
383+
else:
384+
assert does_contain is False
385+
else:
386+
assert does_contain is None
387+
388+
389+
@pytest.mark.parametrize(
390+
["attr_name", "is_defined"],
391+
[
392+
("defined", True),
393+
("_defined", True),
394+
("notdefined", False),
395+
("_notdefined", False),
396+
],
397+
)
398+
def test_hasattr(
399+
dfns: Type[DefinedNamespace], attr_name: str, is_defined: bool
400+
) -> None:
401+
dfns_info = get_dfns_info(dfns)
402+
if dfns_info.suffix is not None:
403+
logging.debug("dfns_info = %s", dfns_info)
404+
if dfns_info.has_attrs is False:
405+
is_defined = False
406+
has_attr: Optional[bool] = None
407+
has_attr = hasattr(dfns, attr_name)
408+
if dfns_info.suffix is not None and (is_defined or dfns._fail is False):
409+
assert has_attr is True
410+
else:
411+
assert has_attr is False
412+
413+
414+
def test_dir(dfns: Type[DefinedNamespace]) -> None:
415+
dfns_info = get_dfns_info(dfns)
416+
does_contain: Optional[bool] = None
417+
with ExitStack() as xstack:
418+
# dir should work for DefinedNamespace as this is called by sphinx to
419+
# document it.
420+
if dfns_info.suffix is None and dfns is not DefinedNamespace:
421+
xstack.enter_context(pytest.raises(AttributeError))
422+
attrs = list(dir(dfns))
423+
if dfns_info.suffix is not None:
424+
if dfns_info.has_attrs:
425+
assert set(attrs) == {
426+
URIRef(f"{prefix}{dfns_info.suffix}defined"),
427+
URIRef(f"{prefix}{dfns_info.suffix}_defined"),
428+
}
429+
else:
430+
assert list(attrs) == []
431+
else:
432+
assert does_contain is None

0 commit comments

Comments
 (0)