From 1639d96e1bd37a38dd17377ff6780d4f1e2359f2 Mon Sep 17 00:00:00 2001 From: Alex Nelson Date: Mon, 27 Mar 2023 08:59:58 -0400 Subject: [PATCH 1/6] Start type hinting mechanisms for Graph.query This patch is known to be an incomplete implementation. Running `mypy test/test_typing.py` before this patch raised 0 type errors. After just adding the two new tests, 3. After starting the subclasses of Query, 30. References: * https://github.com/RDFLib/rdflib/issues/2283 Signed-off-by: Alex Nelson --- rdflib/graph.py | 21 +++++++++++--- rdflib/plugins/sparql/algebra.py | 21 ++++++++++++-- rdflib/plugins/sparql/sparql.py | 16 +++++++++++ test/test_typing.py | 48 ++++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 6 deletions(-) diff --git a/rdflib/graph.py b/rdflib/graph.py index 4a96e6d37..968d2ecba 100644 --- a/rdflib/graph.py +++ b/rdflib/graph.py @@ -57,7 +57,7 @@ import typing_extensions as te import rdflib.query - from rdflib.plugins.sparql.sparql import Query, Update + from rdflib.plugins.sparql.sparql import AskQuery, ConstructQuery, DescribeQuery, Query, SelectQuery, Update _SubjectType = Node _PredicateType = Node @@ -1508,12 +1508,17 @@ def parse( def query( self, - query_object: Union[str, Query], + query_object: Union[None, str, Query], processor: Union[str, query.Processor] = "sparql", result: Union[str, Type[query.Result]] = "sparql", initNs: Optional[Mapping[str, Any]] = None, # noqa: N803 initBindings: Optional[Mapping[str, Identifier]] = None, use_store_provided: bool = True, + *args: Any, + ask_query: Optional[AskQuery] = None, + construct_query: Optional[ConstructQuery] = None, + describe_query: Optional[DescribeQuery] = None, + select_query: Optional[SelectQuery] = None, **kwargs: Any, ) -> query.Result: """ @@ -1543,13 +1548,21 @@ def query( """ + # Requirement: Exactly one of the query arguments is non-null. + populated_query_arguments: List[Union[str, Query]] = [x for x in [query_object, ask_query, construct_query, describe_query, select_query] if x is not None] + if len(populated_query_arguments) == 0: + raise ValueError("No query argument was provided.") + elif len(populated_query_arguments) > 1: + raise ValueError("Multiple query arguments were provided.") + passing_query_object: Union[str, Query] = populated_query_arguments[0] + initBindings = initBindings or {} # noqa: N806 initNs = initNs or dict(self.namespaces()) # noqa: N806 if hasattr(self.store, "query") and use_store_provided: try: return self.store.query( - query_object, + passing_query_object, initNs, initBindings, self.default_union and "__UNION__" or self.identifier, @@ -1564,7 +1577,7 @@ def query( processor = plugin.get(processor, query.Processor)(self) # type error: Argument 1 to "Result" has incompatible type "Mapping[str, Any]"; expected "str" - return result(processor.query(query_object, initBindings, initNs, **kwargs)) # type: ignore[arg-type] + return result(processor.query(passing_query_object, initBindings, initNs, **kwargs)) # type: ignore[arg-type] def update( self, diff --git a/rdflib/plugins/sparql/algebra.py b/rdflib/plugins/sparql/algebra.py index 5fd9e59bc..7dd56f646 100644 --- a/rdflib/plugins/sparql/algebra.py +++ b/rdflib/plugins/sparql/algebra.py @@ -39,7 +39,15 @@ from rdflib.plugins.sparql.operators import TrueFilter, and_ from rdflib.plugins.sparql.operators import simplify as simplifyFilters from rdflib.plugins.sparql.parserutils import CompValue, Expr -from rdflib.plugins.sparql.sparql import Prologue, Query, Update +from rdflib.plugins.sparql.sparql import ( + AskQuery, + ConstructQuery, + DescribeQuery, + Prologue, + SelectQuery, + Query, + Update, +) # --------------------------- # Some convenience methods @@ -948,7 +956,16 @@ def translateQuery( _traverseAgg(res, visitor=analyse) _traverseAgg(res, _addVars) - return Query(prologue, res) + if q[1].name == "AskQuery": + return AskQuery(prologue, res) + elif q[1].name == "ConstructQuery": + return ConstructQuery(prologue, res) + elif q[1].name == "DescribeQuery": + return DescribeQuery(prologue, res) + elif q[1].name == "SelectQuery": + return SelectQuery(prologue, res) + else: + return Query(prologue, res) class ExpressionNotCoveredException(Exception): # noqa: N818 diff --git a/rdflib/plugins/sparql/sparql.py b/rdflib/plugins/sparql/sparql.py index 8f6a002da..d7f516928 100644 --- a/rdflib/plugins/sparql/sparql.py +++ b/rdflib/plugins/sparql/sparql.py @@ -450,6 +450,22 @@ def __init__(self, prologue: Prologue, algebra: CompValue): self._original_args: Tuple[str, Mapping[str, str], Optional[str]] +class AskQuery(Query): + pass + + +class ConstructQuery(Query): + pass + + +class DescribeQuery(Query): + pass + + +class SelectQuery(Query): + pass + + class Update: """ A parsed and translated update diff --git a/test/test_typing.py b/test/test_typing.py index 7bce69840..50be53478 100644 --- a/test/test_typing.py +++ b/test/test_typing.py @@ -133,3 +133,51 @@ def test_rdflib_query_exercise() -> None: python_iri: str = kb_https_uriref.toPython() assert python_iri == "https://example.org/kb/y" + + +def test_rdflib_construct_query_result_exercise() -> None: + """ + This test shows minimally necessary type review runtime statements for a CONSTRUCT query. + """ + + # TODO - Data for these graphs is probably not necessary. Review "probably" in this sentence after supporting implementation is complete. + graph0 = rdflib.Graph() + graph1 = rdflib.Graph() + + construct_query = """\ +PREFIX rdfs: +CONSTRUCT { + ?s rdfs:comment "seen"@en . + ?s rdfs:seeAlso ?s . +} +WHERE { + ?s ?p ?o . +} +""" + for result in graph0.query(construct_query): + graph1.add(result) + + subjects0: Set[rdflib.term.Node] = {x for x in graph0.subjects(None, None)} + subjects1: Set[rdflib.term.Node] = {x for x in graph1.subjects(None, None)} + assert len(subjects0) == len(subjects1) + + +def test_rdflib_select_query_result_exercise() -> None: + """ + This test shows minimally necessary type review runtime statements for a SELECT query. + """ + + # TODO - Data for this graph is probably not necessary. Review "probably" in this sentence after supporting implementation is complete. + graph = rdflib.Graph() + + # Assemble set of all triple-objects (position 2) that are URIRefs. + n_urirefs: Set[rdflib.URIRef] = set() + select_query = """\ +SELECT ?s ?p ?o +WHERE { + ?s ?p ?o . +} +""" + for result in graph.query(select_query): + if isinstance(result[2], rdflib.URIRef): + n_urirefs.add(result[2]) From 7df72d30948c71b6de451679d6afdafc2b0240a7 Mon Sep 17 00:00:00 2001 From: Alex Nelson Date: Mon, 27 Mar 2023 09:00:49 -0400 Subject: [PATCH 2/6] Update disclaimer Signed-off-by: Alex Nelson --- test/test_typing.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/test_typing.py b/test/test_typing.py index 50be53478..b2b49e933 100644 --- a/test/test_typing.py +++ b/test/test_typing.py @@ -1,5 +1,8 @@ #!/usr/bin/env python3 +# Portions of this file contributed by NIST are governed by the following +# statement: +# # This software was developed at the National Institute of Standards # and Technology by employees of the Federal Government in the course # of their official duties. Pursuant to title 17 Section 105 of the From a24df3fcde9b67bfb4563357c3c2253f2ca7f5a5 Mon Sep 17 00:00:00 2001 From: Alex Nelson Date: Tue, 10 Sep 2024 13:23:38 -0400 Subject: [PATCH 3/6] Fix sort order Signed-off-by: Alex Nelson --- rdflib/plugins/sparql/algebra.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rdflib/plugins/sparql/algebra.py b/rdflib/plugins/sparql/algebra.py index 5f97e28b4..5e2e1b982 100644 --- a/rdflib/plugins/sparql/algebra.py +++ b/rdflib/plugins/sparql/algebra.py @@ -44,8 +44,8 @@ ConstructQuery, DescribeQuery, Prologue, - SelectQuery, Query, + SelectQuery, Update, ) From 09f0bc85183451b43380b9ecd2e78ae6250f611b Mon Sep 17 00:00:00 2001 From: Alex Nelson Date: Tue, 10 Sep 2024 13:33:10 -0400 Subject: [PATCH 4/6] Switch to Query.prepare classmethod Potentially, adding an assert() addresses the subclass type review, per `@classmethod` docs: > ... If a class method is called for a derived class, the derived class > object is passed as the implied first argument. This patch also reverts the first-draft change to `Graph.query`. References: * https://docs.python.org/3/library/functions.html#classmethod * https://github.com/RDFLib/rdflib/issues/2283#issuecomment-1485923912 Suggested-by: Iwan Aucamp Signed-off-by: Alex Nelson --- rdflib/graph.py | 18 +++--------------- rdflib/plugins/sparql/sparql.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/rdflib/graph.py b/rdflib/graph.py index 8234f597f..d4f6037a3 100644 --- a/rdflib/graph.py +++ b/rdflib/graph.py @@ -1515,17 +1515,13 @@ def parse( def query( self, - query_object: Union[None, str, Query], + query_object: Union[str, Query], processor: Union[str, query.Processor] = "sparql", result: Union[str, Type[query.Result]] = "sparql", initNs: Optional[Mapping[str, Any]] = None, # noqa: N803 initBindings: Optional[Mapping[str, Identifier]] = None, # noqa: N803 use_store_provided: bool = True, *args: Any, - ask_query: Optional[AskQuery] = None, - construct_query: Optional[ConstructQuery] = None, - describe_query: Optional[DescribeQuery] = None, - select_query: Optional[SelectQuery] = None, **kwargs: Any, ) -> query.Result: """ @@ -1555,14 +1551,6 @@ def query( """ - # Requirement: Exactly one of the query arguments is non-null. - populated_query_arguments: List[Union[str, Query]] = [x for x in [query_object, ask_query, construct_query, describe_query, select_query] if x is not None] - if len(populated_query_arguments) == 0: - raise ValueError("No query argument was provided.") - elif len(populated_query_arguments) > 1: - raise ValueError("Multiple query arguments were provided.") - passing_query_object: Union[str, Query] = populated_query_arguments[0] - initBindings = initBindings or {} # noqa: N806 initNs = initNs or dict(self.namespaces()) # noqa: N806 @@ -1575,7 +1563,7 @@ def query( if hasattr(self.store, "query") and use_store_provided: try: return self.store.query( - passing_query_object, + query_object, initNs, initBindings, query_graph, @@ -1590,7 +1578,7 @@ def query( processor = plugin.get(processor, query.Processor)(self) # type error: Argument 1 to "Result" has incompatible type "Mapping[str, Any]"; expected "str" - return result(processor.query(passing_query_object, initBindings, initNs, **kwargs)) # type: ignore[arg-type] + return result(processor.query(query_object, initBindings, initNs, **kwargs)) # type: ignore[arg-type] def update( self, diff --git a/rdflib/plugins/sparql/sparql.py b/rdflib/plugins/sparql/sparql.py index c466e018a..92cf7684e 100644 --- a/rdflib/plugins/sparql/sparql.py +++ b/rdflib/plugins/sparql/sparql.py @@ -5,6 +5,7 @@ import itertools import typing as t from collections.abc import Mapping, MutableMapping +# TODO - import Self from typing_extensions when Python < 3.11. from typing import ( TYPE_CHECKING, Any, @@ -14,6 +15,7 @@ Iterable, List, Optional, + Self, Tuple, TypeVar, Union, @@ -25,6 +27,7 @@ from rdflib.graph import ConjunctiveGraph, Dataset, Graph from rdflib.namespace import NamespaceManager from rdflib.plugins.sparql.parserutils import CompValue +from rdflib.plugins.sparql.processor import prepareQuery from rdflib.term import BNode, Identifier, Literal, Node, URIRef, Variable if TYPE_CHECKING: @@ -489,6 +492,17 @@ def __init__(self, prologue: Prologue, algebra: CompValue): self.algebra = algebra self._original_args: Tuple[str, Mapping[str, str], Optional[str]] + @classmethod + def prepare( + cls, + queryString: str, + initNs: Optional[Mapping[str, Any]] = None, + base: Optional[str] = None, + ) -> Self: + result = prepareQuery(queryString, initNs, base) + assert isinstance(result, cls) + return cls(result.prologue, result.algebra) + class AskQuery(Query): pass From f521914306eb898a55d0abb3c82df59e97128ba3 Mon Sep 17 00:00:00 2001 From: Alex Nelson Date: Tue, 10 Sep 2024 13:37:26 -0400 Subject: [PATCH 5/6] Format --- rdflib/plugins/sparql/sparql.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rdflib/plugins/sparql/sparql.py b/rdflib/plugins/sparql/sparql.py index 92cf7684e..eab317836 100644 --- a/rdflib/plugins/sparql/sparql.py +++ b/rdflib/plugins/sparql/sparql.py @@ -5,6 +5,7 @@ import itertools import typing as t from collections.abc import Mapping, MutableMapping + # TODO - import Self from typing_extensions when Python < 3.11. from typing import ( TYPE_CHECKING, From a22213cb3bf358460802369c48d568ea51eaa8b6 Mon Sep 17 00:00:00 2001 From: Alex Nelson Date: Tue, 10 Sep 2024 13:38:05 -0400 Subject: [PATCH 6/6] Draft remaining tests References: * https://github.com/RDFLib/rdflib/issues/2283 Signed-off-by: Alex Nelson --- test/test_typing.py | 109 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 102 insertions(+), 7 deletions(-) diff --git a/test/test_typing.py b/test/test_typing.py index 79c7a8508..b1c19c637 100644 --- a/test/test_typing.py +++ b/test/test_typing.py @@ -31,6 +31,12 @@ # TODO Bug - rdflib.plugins.sparql.prepareQuery() will run fine if this # test is run, but mypy can't tell the symbol is exposed. import rdflib.plugins.sparql.processor +from rdflib.plugins.sparql.sparql import ( + AskQuery, + ConstructQuery, + DescribeQuery, + SelectQuery, +) from rdflib.query import ResultRow from rdflib.term import IdentifiedNode, Identifier, Node @@ -139,15 +145,70 @@ def test_rdflib_query_exercise() -> None: assert python_iri == "https://example.org/kb/y" +def _test_rdflib_ask_query_result_graph() -> rdflib.Graph: + graph = rdflib.Graph() + graph.add( + ( + rdflib.URIRef("http://example.org/kb/a"), + rdflib.URIRef("http://example.org/kb/b"), + rdflib.URIRef("http://example.org/kb/c"), + ) + ) + return graph + + +def test_rdflib_ask_query_result_exercise_0() -> None: + """ + This test shows minimally necessary type review runtime statements for an ASK query. + """ + graph = _test_rdflib_ask_query_result_graph() + ask_query_0 = """\ +ASK { ?s ?p ?o . } +""" + result_0 = graph.query(AskQuery.prepare(ask_query_0)) + assert result_0 is True + + +def test_rdflib_ask_query_result_exercise_1() -> None: + """ + This test shows minimally necessary type review runtime statements for an ASK query. + """ + graph = _test_rdflib_ask_query_result_graph() + ask_query_1 = """\ +ASK { . } +""" + result_1 = graph.query(AskQuery.prepare(ask_query_1)) + assert result_1 is True + + +def test_rdflib_ask_query_result_exercise_2() -> None: + """ + This test shows minimally necessary type review runtime statements for an ASK query. + """ + graph = _test_rdflib_ask_query_result_graph() + ask_query_2 = """\ +ASK { . } +""" + result_2 = graph.query(AskQuery.prepare(ask_query_2)) + assert result_2 is False + + def test_rdflib_construct_query_result_exercise() -> None: """ This test shows minimally necessary type review runtime statements for a CONSTRUCT query. """ - # TODO - Data for these graphs is probably not necessary. Review "probably" in this sentence after supporting implementation is complete. graph0 = rdflib.Graph() graph1 = rdflib.Graph() + graph0.add( + ( + rdflib.URIRef("http://example.org/kb/a"), + rdflib.URIRef("http://example.org/kb/b"), + rdflib.URIRef("http://example.org/kb/c"), + ) + ) + construct_query = """\ PREFIX rdfs: CONSTRUCT { @@ -158,12 +219,38 @@ def test_rdflib_construct_query_result_exercise() -> None: ?s ?p ?o . } """ - for result in graph0.query(construct_query): + for result in graph0.query(ConstructQuery.prepare(construct_query)): graph1.add(result) subjects0: Set[rdflib.term.Node] = {x for x in graph0.subjects(None, None)} subjects1: Set[rdflib.term.Node] = {x for x in graph1.subjects(None, None)} - assert len(subjects0) == len(subjects1) + assert len(subjects0) == 1 + assert subjects0 == subjects1 + + +def test_rdflib_describe_query_result_exercise() -> None: + """ + This test shows minimally necessary type review runtime statements for a DESCRIBE query. + """ + + graph = rdflib.Graph() + graph.add( + ( + rdflib.URIRef("http://example.org/kb/a"), + rdflib.URIRef("http://example.org/kb/b"), + rdflib.URIRef("http://example.org/kb/c"), + ) + ) + + expected: Set[rdflib.URIRef] = {rdflib.URIRef("http://example.org/kb/a")} + computed: Set[rdflib.URIRef] = set() + describe_query = """\ +DESCRIBE ?s +""" + for result in graph.query(DescribeQuery.prepare(describe_query)): + if isinstance(result[0], rdflib.URIRef): + computed.add(result[0]) + assert expected == computed def test_rdflib_select_query_result_exercise() -> None: @@ -171,17 +258,25 @@ def test_rdflib_select_query_result_exercise() -> None: This test shows minimally necessary type review runtime statements for a SELECT query. """ - # TODO - Data for this graph is probably not necessary. Review "probably" in this sentence after supporting implementation is complete. graph = rdflib.Graph() + graph.add( + ( + rdflib.URIRef("http://example.org/kb/a"), + rdflib.URIRef("http://example.org/kb/b"), + rdflib.URIRef("http://example.org/kb/c"), + ) + ) # Assemble set of all triple-objects (position 2) that are URIRefs. - n_urirefs: Set[rdflib.URIRef] = set() + expected: Set[rdflib.URIRef] = {rdflib.URIRef("http://example.org/kb/b")} + computed: Set[rdflib.URIRef] = set() select_query = """\ SELECT ?s ?p ?o WHERE { ?s ?p ?o . } """ - for result in graph.query(select_query): + for result in graph.query(SelectQuery.prepare(select_query)): if isinstance(result[2], rdflib.URIRef): - n_urirefs.add(result[2]) + computed.add(result[2]) + assert expected == computed