diff --git a/rdflib/plugins/sparql/parser.py b/rdflib/plugins/sparql/parser.py index 0db77d8c9..2035b4f08 100644 --- a/rdflib/plugins/sparql/parser.py +++ b/rdflib/plugins/sparql/parser.py @@ -226,13 +226,6 @@ def expandCollection(terms): ) -def _hexExpand(match): - return chr(int(match.group(0)[1:], 16)) - - -PN_LOCAL.setParseAction(lambda x: re.sub("(%s)" % PERCENT_re, _hexExpand, x[0])) - - # [141] PNAME_LN ::= PNAME_NS PN_LOCAL PNAME_LN = PNAME_NS + Param("localname", PN_LOCAL.leaveWhitespace()) diff --git a/test/test_sparql/test_prefixed_name.py b/test/test_sparql/test_prefixed_name.py new file mode 100644 index 000000000..ca793ef77 --- /dev/null +++ b/test/test_sparql/test_prefixed_name.py @@ -0,0 +1,135 @@ +import itertools +import logging +from contextlib import ExitStack +from typing import Type, Union + +import pyparsing +import pytest +from pyparsing import Optional + +import rdflib +from rdflib import Graph +from rdflib.namespace import Namespace +from rdflib.term import URIRef + +RESERVED_PCHARS = [ + "%20", + "%21", + "%23", + "%24", + "%25", + "%26", + "%27", + "%28", + "%29", + "%2A", + "%2B", + "%2C", + "%2F", + "%3A", + "%3B", + "%3D", + "%3F", + "%40", + "%5B", + "%5D", +] + + +@pytest.mark.parametrize( + "reserved_char_percent_encoded", + RESERVED_PCHARS, +) +def test_sparql_parse_reserved_char_percent_encoded(reserved_char_percent_encoded): + data = f"""@prefix : . + + :_id "01G39WKRH76BGY5D3SKDHJP2SX" ; + :transcript{reserved_char_percent_encoded}data [ :_id "01G39WKRH7JYRX78X7FG4RCNYF" ; + :_key "transcript{reserved_char_percent_encoded}data" ; + :value "value" ; + :value_id "01G39WKRH7PVK1DXQHWT08DZA8" ] .""" + + q = f"""PREFIX : + SELECT ?o + WHERE {{ ?s :transcript{reserved_char_percent_encoded}data/:value ?o . }}""" + + g = rdflib.Graph() + g.parse(data=data, format="ttl") + res = g.query(q) + + assert list(res)[0][0] == rdflib.term.Literal("value") + + assert reserved_char_percent_encoded in str( + rdflib.plugins.sparql.parser.parseQuery(q) + ) + + +PNAME_PREFIX = Namespace("https://example.com/test_pnames/") + + +@pytest.fixture(scope="module") +def blank_graph() -> Graph: + return Graph() + + +@pytest.mark.parametrize( + ["pname_ns", "pname", "expected_result"], + itertools.chain( + [ + ("eg", "invalid/PN_PREFIX", pyparsing.exceptions.ParseException), + ("", "eg:a", Exception), + ("", ":invalid PN_LOCAL", pyparsing.exceptions.ParseException), + ("", ":invalid/PN_LOCAL", pyparsing.exceptions.ParseException), + ("", ":a:b:c", PNAME_PREFIX["a:b:c"]), + ("", ":", URIRef(f"{PNAME_PREFIX}")), + ("", ":a", PNAME_PREFIX.a), + ("eg", " eg:obj ", PNAME_PREFIX.obj), + ("", " :obj ", PNAME_PREFIX.obj), + ("eg", " \t eg:obj \t ", PNAME_PREFIX.obj), + ("", " \n :obj \n ", PNAME_PREFIX.obj), + ("eg", "eg:", URIRef(f"{PNAME_PREFIX}")), + ("eg", "eg:a", PNAME_PREFIX.a), + ("", ":transcript%20data", PNAME_PREFIX["transcript%20data"]), + ], + ( + ("", f":aaa{pchar}zzz", PNAME_PREFIX[f"aaa{pchar}zzz"]) + for pchar in RESERVED_PCHARS + ), + ), +) +def test_pnames( + pname_ns: str, + pname: str, + expected_result: Union[URIRef, Type[Exception]], + blank_graph: Graph, +) -> None: + """ + The given pname produces the expected result. + """ + catcher: Optional[pytest.ExceptionInfo[Exception]] = None + + with ExitStack() as xstack: + if isinstance(expected_result, type) and issubclass(expected_result, Exception): + catcher = xstack.enter_context(pytest.raises(expected_result)) + query_string = f"""\ + PREFIX {pname_ns}: <{PNAME_PREFIX}> + + CONSTRUCT {{ + {pname}. + }} WHERE {{}} + """ + query_result = blank_graph.query(query_string) + assert query_result.type == "CONSTRUCT" + assert isinstance(query_result.graph, Graph) + triples = list(query_result.graph.triples((None, None, None))) + assert len(triples) == 1 + triple = triples[0] + result = triple[2] + logging.debug("result = %s", result) + + if catcher is not None: + assert isinstance(catcher, pytest.ExceptionInfo) + assert catcher.value is not None + else: + assert isinstance(expected_result, URIRef) + assert expected_result == result