Skip to content

Commit e09b80c

Browse files
committed
feat: suppress open client warnings in SPARQLWrapper.queries
The `SPARQLWrapper.queries` method is a sync wrapper around async code and intended to be a convenience method for users wishing to execute multiple operations concurrently but do not want to write async code themselves. `SPARQLWrapper.queries` internally creates a `SPARQLWrapper` instance with a shared `httpx.AsyncClient` that is then used in an async context manger for calls to `SPARQLWrapper.aquery`. > `SPARQLWrapper.queries` ergo is a managed application of `SPARQLWrapper.aquery` that uses the context manager API of `SPARQLWrapper` internally. This application causes the sparqlx `ClientManager` to emit a warning about an open client though. While this is technically correct and intended behavior, it is superfluos and potentially misleading for `SPARQLWrapper.queries` which actually manages the client through the context manager. It is therefore preferable to suppress client warnings for `SPARQLWrapper.queries`. The change uses a PEP-806-inspired async wrapper around the sync `warnings.catch_warnings` context manager to suppress `Userwarnings` coming from `sparqlx.utils.client_manager` within `SPARQLWrapper.queries`. Note: `warnings.catch_warnings` is not concurrency-safe, see [Concurrent safety of Context Managers](https://docs.python.org/3/library/warnings.html#concurrent-safety-of-context-managers) in the `warnings` docs. Although the implemented change uses a `contextlib.contextmanager` which closures context state and can ergo mitigate concurrency-safety problems, `warnings` filters are global and would actually need explicit locking or protection from a `ContextVar`. In the case at hand, this is of no concern however, since `SPARQLWrapper.queries` controls its own encapsulated event loop. Closes #124.
1 parent 0edc822 commit e09b80c

2 files changed

Lines changed: 38 additions & 3 deletions

File tree

src/sparqlx/sparqlwrapper.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from contextlib import AbstractAsyncContextManager, AbstractContextManager
66
import functools
77
from typing import Literal as TLiteral, Self, overload
8+
import warnings
89

910
import httpx
1011
from rdflib import Dataset, Graph
@@ -32,7 +33,12 @@
3233
RDFLibQueryTransport,
3334
RDFLibUpdateTransport,
3435
)
35-
from sparqlx.utils.utils import Endpoint, _get_query_type, _get_response_converter
36+
from sparqlx.utils.utils import (
37+
Endpoint,
38+
_get_query_type,
39+
_get_response_converter,
40+
as_async_context,
41+
)
3642

3743

3844
class SPARQLWrapper(AbstractContextManager, AbstractAsyncContextManager):
@@ -486,7 +492,21 @@ def queries(
486492
)
487493

488494
async def _runner() -> Iterator[httpx.Response]:
489-
async with query_component, asyncio.TaskGroup() as tg:
495+
acatch_warnings: AbstractAsyncContextManager[None] = as_async_context(
496+
warnings.catch_warnings()
497+
)
498+
499+
async with (
500+
acatch_warnings,
501+
query_component,
502+
asyncio.TaskGroup() as tg,
503+
):
504+
warnings.filterwarnings(
505+
action="ignore",
506+
category=UserWarning,
507+
module="sparqlx.utils.client_manager",
508+
)
509+
490510
tasks = [
491511
tg.create_task(
492512
query_component.aquery(

src/sparqlx/utils/utils.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import asyncio
2+
from collections.abc import AsyncIterator
3+
from contextlib import AbstractContextManager, asynccontextmanager
14
from typing import cast
25

36
from rdflib import BNode, Graph
47
from rdflib.plugins.sparql import prepareQuery
58
from rdflib.plugins.sparql.sparql import Query
6-
79
from sparqlx.types import SPARQLQuery, SPARQLQueryTypeLiteral, SPARQLResponseFormat
810
from sparqlx.utils.converters import _convert_ask, _convert_bindings, _convert_graph
911

@@ -71,3 +73,16 @@ def url(self) -> str:
7173
@property
7274
def graph(self) -> Graph | None:
7375
return self._endpoint if isinstance(self._endpoint, Graph) else None
76+
77+
78+
@asynccontextmanager
79+
async def as_async_context[T](sync_cm: AbstractContextManager[T]) -> AsyncIterator[T]:
80+
"""Async context wrapper around a sync context manager.
81+
82+
The async context manager allows to call a sync context manager
83+
from an async context statement. This workaround is mentioned in PEP 806,
84+
see https://peps.python.org/pep-0806/#workaround-an-as-acm-wrapper.
85+
"""
86+
with sync_cm as result:
87+
await asyncio.sleep(0)
88+
yield result

0 commit comments

Comments
 (0)