Skip to content

Commit

Permalink
Logging exceptions from Literal value converters
Browse files Browse the repository at this point in the history
- Also, adding a function to reset bindings so tests are less
  order-dependent
  • Loading branch information
mwatts15 committed May 20, 2022
1 parent ba855c2 commit bb152a7
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 17 deletions.
49 changes: 35 additions & 14 deletions rdflib/term.py
Original file line number Diff line number Diff line change
Expand Up @@ -1962,6 +1962,8 @@ def _castPythonToLiteral( # noqa: N802
(Fraction, (None, _OWL_RATIONAL)),
]

_OriginalGenericPythonToXSDRules = list(_GenericPythonToXSDRules)

_SpecificPythonToXSDRules: List[
Tuple[Tuple[Type[Any], str], Optional[Callable[[Any], Union[str, bytes]]]]
] = [
Expand All @@ -1973,6 +1975,8 @@ def _castPythonToLiteral( # noqa: N802
((bytes, _XSD_B64BINARY), b64encode),
]

_OriginalSpecificPythonToXSDRules = list(_SpecificPythonToXSDRules)

XSDToPython: Dict[Optional[str], Optional[Callable[[str], Any]]] = {
None: None, # plain literals map directly to value space
URIRef(_XSD_PFX + "time"): parse_time,
Expand Down Expand Up @@ -2031,32 +2035,52 @@ def _castPythonToLiteral( # noqa: N802
_toPythonMapping.update(XSDToPython)


def _reset_bindings() -> None:
"""
Reset lexical<->value space binding for `Literal`
"""
_toPythonMapping.clear()
_toPythonMapping.update(XSDToPython)

_GenericPythonToXSDRules.clear()
_GenericPythonToXSDRules.extend(_OriginalGenericPythonToXSDRules)

_SpecificPythonToXSDRules.clear()
_SpecificPythonToXSDRules.extend(_OriginalSpecificPythonToXSDRules)


def _castLexicalToPython( # noqa: N802
lexical: Union[str, bytes], datatype: Optional[str]
) -> Any:
"""
Map a lexical form to the value-space for the given datatype
:returns: a python object for the value or ``None``
"""
convFunc = _toPythonMapping.get(datatype, False) # noqa: N806
if convFunc:
if TYPE_CHECKING:
# NOTE: This is here because convFunc is seen as
# Union[Callable[[str], Any], bool, None]
# even though it will never have value of True.
assert not isinstance(convFunc, bool)
convFunc
try:
conv_func = _toPythonMapping[datatype]
except KeyError:
# no conv_func -> unknown data-type
return None

if conv_func is not None:
try:
# type error: Argument 1 has incompatible type "Union[str, bytes]"; expected "str"
# NOTE for type ignore: various functions in _toPythonMapping will
# only work for str, so there is some inconsistency here, the right
# approach may be to change lexical to be of str type but this will
# require runtime changes.
return convFunc(lexical) # type: ignore[arg-type]
except:
return conv_func(lexical) # type: ignore[arg-type]
except Exception:
logger.warning(
"Failed to convert Literal lexical form to value. Datatype=%s, "
"Converter=%s",
datatype,
conv_func,
exc_info=True,
)
# not a valid lexical representation for this dt
return None
elif convFunc is None:
else:
# no conv func means 1-1 lexical<->value-space mapping
try:
return str(lexical)
Expand All @@ -2065,9 +2089,6 @@ def _castLexicalToPython( # noqa: N802
# NOTE for type ignore: code assumes that lexical is of type bytes
# at this point.
return str(lexical, "utf-8") # type: ignore[arg-type]
else:
# no convFunc - unknown data-type
return None


_AnyT = TypeVar("_AnyT", bound=Any)
Expand Down
41 changes: 38 additions & 3 deletions test/test_literal/test_literal.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from contextlib import ExitStack
from decimal import Decimal
from test.utils import affix_tuples
from typing import Any, Optional, Type, Union
from typing import Any, Generator, Optional, Type, Union

import isodate
import pytest
Expand All @@ -33,12 +33,21 @@
_XSD_TIME,
Literal,
URIRef,
_reset_bindings,
bind,
)

EGNS = Namespace("http://example.com/")


@pytest.fixture()
def clear_bindings() -> Generator[None, None, None]:
try:
yield
finally:
_reset_bindings()


class TestLiteral:
def test_repr_apostrophe(self) -> None:
a = rdflib.Literal("'")
Expand Down Expand Up @@ -779,7 +788,7 @@ def test_non_false_boolean(self) -> None:


class TestBindings:
def test_binding(self) -> None:
def test_binding(self, clear_bindings: None) -> None:
class a:
def __init__(self, v: str) -> None:
self.v = v[3:-3]
Expand Down Expand Up @@ -814,7 +823,7 @@ def __str__(self) -> str:
assert lb.value == vb
assert lb.datatype == dtB

def test_specific_binding(self) -> None:
def test_specific_binding(self, clear_bindings: None) -> None:
def lexify(s: str) -> str:
return "--%s--" % s

Expand Down Expand Up @@ -927,3 +936,29 @@ def check_make_literals(
else:
assert literal.value is None
assert lexical == f"{literal}"


def test_exception_in_converter(
caplog: pytest.LogCaptureFixture, clear_bindings: None
) -> None:
def lexify(s: str) -> str:
return "--%s--" % s

def unlexify(s: str) -> str:
raise Exception("TEST_EXCEPTION")

datatype = rdflib.URIRef("urn:dt:mystring")

# Datatype-specific rule
bind(datatype, str, unlexify, lexify, datatype_specific=True)

s = "Hello"

Literal("--%s--" % s, datatype=datatype)

assert (
caplog.record_tuples[0][1] == logging.WARNING
and caplog.record_tuples[0][2].startswith("Failed to convert")
and caplog.records[0].exc_info
and str(caplog.records[0].exc_info[1]) == "TEST_EXCEPTION"
)

0 comments on commit bb152a7

Please sign in to comment.