Skip to content

Commit 53405b7

Browse files
nicholascarvemonet
andauthored
Replacement for #3125 (#3146)
* add tests to check SPARQLStore against public endpoints. By default it is disabled (slow, and public endpoints loves to go down whenever they can, so we don't want to have them run as part of the default CI/CD). We need to add the flag --public-endpoints to the pytest command to enable them. The list of tested endpoints is the same as the one used in SPARQLWrapper tests, with a new entry for a Qlever endpoint. Documentation about this have been added to the developers docs page * Improve type annotations for the SPARQLStore, uses Literal, so that people using it will get proper autocomplete suggestion for the returnFormats and methods available. Add better docs on how to use SPARQLStore in its docstring * Fix mypy type checking errors: Unused "type: ignore" comment and Unsupported operand types for + ("str" and "int") * fix doctests * improve mkdocs page linking * add additions to developers.rst to developers.md --------- Co-authored-by: Vincent Emonet <[email protected]>
1 parent a683d19 commit 53405b7

File tree

8 files changed

+298
-24
lines changed

8 files changed

+298
-24
lines changed

docs/developers.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,18 @@ poetry install --all-extras
128128
poetry run pytest
129129
```
130130

131+
By default, tests of the `SPARQLStore` against remote public endpoints are skipped, to enable them add the flag:
132+
133+
```bash
134+
poetry run pytest --public-endpoints
135+
```
136+
137+
Or exclusively run the SPARQLStore tests:
138+
139+
```bash
140+
poetry run pytest test/test_store/test_store_sparqlstore_public.py --public-endpoints
141+
```
142+
131143
### Writing tests
132144

133145
New tests should be written for [pytest](https://docs.pytest.org/en/latest/) instead of for python's built-in `unittest` module as pytest provides advanced features such as parameterization and more flexibility in writing expected failure tests than `unittest`.

examples/custom_eval.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
asking for `rdf:type` triples.
77
88
Here the custom eval function is added manually, normally you would use
9-
entry points to do it. See the [Plugins Usage Documentation](/plugins/).
9+
entry points to do it. See the [Plugins Usage Documentation](../plugins.md).
1010
"""
1111

1212
from pathlib import Path

rdflib/compare.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -466,14 +466,14 @@ def _traces(
466466
best_experimental_score = experimental_score
467467
elif best_score > color_score:
468468
# prune this branch.
469-
if stats is not None:
470-
stats["prunings"] = int(stats.get("prunings", 0)) + 1
469+
if stats is not None and isinstance(stats["prunings"], int):
470+
stats["prunings"] += 1
471471
elif experimental_score != best_experimental_score:
472472
best.append(refined_coloring)
473473
else:
474474
# prune this branch.
475-
if stats is not None:
476-
stats["prunings"] = int(stats.get("prunings", 0)) + 1
475+
if stats is not None and isinstance(stats["prunings"], int):
476+
stats["prunings"] += 1
477477
discrete: list[list[Color]] = [x for x in best if self._discrete(x)]
478478
if len(discrete) == 0:
479479
best_score = None

rdflib/plugin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
either automatically through entry points or by calling
44
rdf.plugin.register directly.
55
6-
For more details, see the [Plugins Usage Documentation](/plugins/).
6+
For more details, see the [Plugins Usage Documentation](../plugins.md).
77
"""
88

99
from __future__ import annotations

rdflib/plugins/stores/sparqlconnector.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
if TYPE_CHECKING:
1818
import typing_extensions as te
1919

20+
SUPPORTED_METHODS = te.Literal["GET", "POST", "POST_FORM"]
21+
SUPPORTED_FORMATS = te.Literal["xml", "json", "csv", "tsv", "application/rdf+xml"]
22+
2023

2124
class SPARQLConnectorException(Exception): # noqa: N818
2225
pass
@@ -41,8 +44,8 @@ def __init__(
4144
self,
4245
query_endpoint: str | None = None,
4346
update_endpoint: str | None = None,
44-
returnFormat: str = "xml", # noqa: N803
45-
method: te.Literal["GET", "POST", "POST_FORM"] = "GET",
47+
returnFormat: SUPPORTED_FORMATS = "xml", # noqa: N803
48+
method: SUPPORTED_METHODS = "GET",
4649
auth: tuple[str, str] | None = None,
4750
**kwargs,
4851
):

rdflib/plugins/stores/sparqlstore.py

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
)
4545
from rdflib.plugins.sparql.sparql import Query, Update
4646
from rdflib.query import Result, ResultRow
47+
from .sparqlconnector import SUPPORTED_FORMATS, SUPPORTED_METHODS
4748

4849
from .sparqlconnector import SPARQLConnector
4950

@@ -67,11 +68,37 @@ def _node_to_sparql(node: Node) -> str:
6768

6869

6970
class SPARQLStore(SPARQLConnector, Store):
70-
"""An RDFLib store around a SPARQL endpoint
71+
"""An RDFLib store around a SPARQL endpoint.
7172
7273
This is context-aware and should work as expected
7374
when a context is specified.
7475
76+
### Usage example
77+
78+
```python
79+
>>> from rdflib import Dataset
80+
>>> from rdflib.plugins.stores.sparqlstore import SPARQLStore
81+
>>>
82+
>>> g = Dataset(
83+
... SPARQLStore("https://query.wikidata.org/sparql", returnFormat="xml"),
84+
... default_union=True
85+
... )
86+
>>>
87+
>>> res = g.query("SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 5")
88+
>>>
89+
>>> # Iterate the results
90+
>>> for row in res:
91+
... pass # but really you'd do something like: print(row)
92+
>>>
93+
>>> # Or serialize the results
94+
>>> # something like: print(res.serialize(format="json").decode())
95+
```
96+
97+
!!! warning "Not all SPARQL endpoints support the same features"
98+
99+
Checkout the `test suite on public endpoints <https://github.com/RDFLib/rdflib/blob/main/test/test_store/test_store_sparqlstore_public.py>`_
100+
for more details on how to successfully query different types of endpoints.
101+
75102
For ConjunctiveGraphs, reading is done from the "default graph". Exactly
76103
what this means depends on your endpoint, because SPARQL does not offer a
77104
simple way to query the union of all graphs as it would be expected for a
@@ -83,7 +110,7 @@ class SPARQLStore(SPARQLConnector, Store):
83110
84111
!!! warning "Blank nodes
85112
86-
By default the SPARQL Store does not support blank-nodes!
113+
By default, the SPARQL Store does not support blank-nodes!
87114
88115
As blank-nodes act as variables in SPARQL queries,
89116
there is no way to query for a particular blank node without
@@ -96,13 +123,15 @@ class SPARQLStore(SPARQLConnector, Store):
96123
"<bnode:b0001>", you can use a function like this:
97124
98125
```python
99-
>>> def my_bnode_ext(node):
100-
... if isinstance(node, BNode):
101-
... return '<bnode:b%s>' % node
102-
... return _node_to_sparql(node)
103-
>>> store = SPARQLStore('http://dbpedia.org/sparql',
104-
... node_to_sparql=my_bnode_ext)
105-
126+
>> def my_bnode_ext(node):
127+
... if isinstance(node, BNode):
128+
... return f"<bnode:b{node}>"
129+
... return _node_to_sparql(node)
130+
...
131+
>> store = SPARQLStore(
132+
... "http://dbpedia.org/sparql",
133+
... node_to_sparql=my_bnode_ext
134+
... )
106135
```
107136
108137
You can request a particular result serialization with the
@@ -115,14 +144,11 @@ class SPARQLStore(SPARQLConnector, Store):
115144
urllib when doing HTTP calls. I.e. you have full control of
116145
cookies/auth/headers.
117146
118-
Form example:
147+
HTTP basic auth is available with:
119148
120149
```python
121-
>>> store = SPARQLStore('...my endpoint ...', auth=('user','pass'))
122-
150+
>> store = SPARQLStore('...my endpoint ...', auth=('user','pass'))
123151
```
124-
125-
will use HTTP basic auth.
126152
"""
127153

128154
formula_aware = False
@@ -136,13 +162,15 @@ def __init__(
136162
sparql11: bool = True,
137163
context_aware: bool = True,
138164
node_to_sparql: _NodeToSparql = _node_to_sparql,
139-
returnFormat: str = "xml", # noqa: N803
165+
returnFormat: SUPPORTED_FORMATS = "xml", # noqa: N803
166+
method: SUPPORTED_METHODS = "GET",
140167
auth: tuple[str, str] | None = None,
141168
**sparqlconnector_kwargs,
142169
):
143170
super(SPARQLStore, self).__init__(
144171
query_endpoint=query_endpoint,
145172
returnFormat=returnFormat,
173+
method=method,
146174
auth=auth,
147175
**sparqlconnector_kwargs,
148176
)

test/conftest.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,15 @@ def exit_stack() -> Generator[ExitStack, None, None]:
104104

105105

106106
@pytest.hookimpl(tryfirst=True)
107-
def pytest_collection_modifyitems(items: Iterable[pytest.Item]):
107+
def pytest_collection_modifyitems(config: pytest.Config, items: Iterable[pytest.Item]):
108108
for item in items:
109+
if config and not config.getoption("--public-endpoints", False):
110+
# Skip tests marked with public_endpoints if the option is not provided
111+
if "public_endpoints" in item.keywords:
112+
item.add_marker(
113+
pytest.mark.skip(reason="need --public-endpoints option to run")
114+
)
115+
109116
parent_name = (
110117
str(Path(item.parent.module.__file__).relative_to(PROJECT_ROOT))
111118
if item.parent is not None
@@ -117,3 +124,19 @@ def pytest_collection_modifyitems(items: Iterable[pytest.Item]):
117124
extra_markers = EXTRA_MARKERS[(parent_name, item.name)]
118125
for extra_marker in extra_markers:
119126
item.add_marker(extra_marker)
127+
128+
129+
def pytest_addoption(parser):
130+
"""Add optional pytest markers to run tests on public endpoints"""
131+
parser.addoption(
132+
"--public-endpoints",
133+
action="store_true",
134+
default=False,
135+
help="run tests that require public SPARQL endpoints",
136+
)
137+
138+
139+
def pytest_configure(config):
140+
config.addinivalue_line(
141+
"markers", "public_endpoints: mark tests that require public SPARQL endpoints"
142+
)

0 commit comments

Comments
 (0)