Skip to content

Commit 5589334

Browse files
committed
Support aiohttp loader in Python 3.14.
- Update aiohttp document loader to work with Python 3.14. - Minimize async related changes to library code in this release. - In sync environment use `asyncio.run`. - In async environment use background thread. - AI pair programming used for this patch. This seemed to be the best of a few alternatives to fix Python 3.14 support while minimizing other changes. Future work could enable better concurrency.
1 parent 01fe0f8 commit 5589334

File tree

2 files changed

+55
-11
lines changed

2 files changed

+55
-11
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
### Changed
66
- **BREAKING**: Require supported Python version >= 3.10.
7+
- Update aiohttp document loader to work with Python 3.14.
8+
- Minimize async related changes to library code in this release.
9+
- In sync environment use `asyncio.run`.
10+
- In async environment use background thread.
711

812
## 2.0.4 - 2024-02-16
913

lib/pyld/documentloader/aiohttp.py

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,33 +7,54 @@
77
.. moduleauthor:: Olaf Conradi <olaf@conradi.org>
88
"""
99

10+
import asyncio
11+
import re
1012
import string
13+
import threading
1114
import urllib.parse as urllib_parse
1215

1316
from pyld.jsonld import (JsonLdError, parse_link_header, LINK_HEADER_REL)
1417

1518

19+
# Background event loop (used when inside an existing async environment)
20+
_background_loop = None
21+
_background_thread = None
22+
23+
24+
def _ensure_background_loop():
25+
"""Start a persistent background event loop if not running."""
26+
global _background_loop, _background_thread
27+
if _background_loop is None:
28+
_background_loop = asyncio.new_event_loop()
29+
30+
def run_loop(loop):
31+
asyncio.set_event_loop(loop)
32+
loop.run_forever()
33+
34+
_background_thread = threading.Thread(
35+
target=run_loop, args=(_background_loop,), daemon=True)
36+
_background_thread.start()
37+
return _background_loop
38+
39+
1640
def aiohttp_document_loader(loop=None, secure=False, **kwargs):
1741
"""
1842
Create an Asynchronous document loader using aiohttp.
1943
20-
:param loop: the event loop used for processing HTTP requests.
44+
:param loop: deprecated / ignored (kept for backward compatibility).
2145
:param secure: require all requests to use HTTPS (default: False).
2246
:param **kwargs: extra keyword args for the aiohttp request get() call.
2347
2448
:return: the RemoteDocument loader function.
2549
"""
26-
import asyncio
2750
import aiohttp
2851

29-
if loop is None:
30-
loop = asyncio.get_event_loop()
31-
3252
async def async_loader(url, headers):
3353
"""
3454
Retrieves JSON-LD at the given URL asynchronously.
3555
3656
:param url: the URL to retrieve.
57+
:param headers: the request headers.
3758
3859
:return: the RemoteDocument.
3960
"""
@@ -56,7 +77,7 @@ async def async_loader(url, headers):
5677
'the URL\'s scheme is not "https".',
5778
'jsonld.InvalidUrl', {'url': url},
5879
code='loading document failed')
59-
async with aiohttp.ClientSession(loop=loop) as session:
80+
async with aiohttp.ClientSession() as session:
6081
async with session.get(url,
6182
headers=headers,
6283
**kwargs) as response:
@@ -104,16 +125,35 @@ async def async_loader(url, headers):
104125
'jsonld.LoadDocumentError', code='loading document failed',
105126
cause=cause)
106127

107-
def loader(url, options={}):
128+
def loader(url, options=None):
108129
"""
109-
Retrieves JSON-LD at the given URL.
130+
Retrieves JSON-LD at the given URL synchronously.
131+
132+
Works safely in both synchronous and asynchronous environments.
110133
111134
:param url: the URL to retrieve.
135+
:param options: the request options.
112136
113137
:return: the RemoteDocument.
114138
"""
115-
return loop.run_until_complete(
116-
async_loader(url,
117-
options.get('headers', {'Accept': 'application/ld+json, application/json'})))
139+
if options is None:
140+
options = {}
141+
headers = options.get(
142+
'headers', {'Accept': 'application/ld+json, application/json'})
143+
144+
# Detect whether we're already in an async environment
145+
try:
146+
running_loop = asyncio.get_running_loop()
147+
except RuntimeError:
148+
running_loop = None
149+
150+
# Sync environment
151+
if not running_loop or not running_loop.is_running():
152+
return asyncio.run(async_loader(url, headers))
153+
154+
# Inside async environment: use background event loop
155+
loop = _ensure_background_loop()
156+
future = asyncio.run_coroutine_threadsafe(async_loader(url, headers), loop)
157+
return future.result()
118158

119159
return loader

0 commit comments

Comments
 (0)