Skip to content

Commit 8e9d94e

Browse files
authored
feat: support Python 3.10-3.14, drop EOL 3.8/3.9 (#512)
* feat: support Python 3.10-3.14, drop EOL 3.8/3.9 Python 3.8 (EOL 2024-10) and 3.9 (EOL 2025-10) are end-of-life; advertising support for them was a false security claim. Raise the supported floor to 3.10 and declare/test through 3.14. - requires-python / python_requires: >=3.10 - classifiers: 3.10-3.14 - CI test matrix: 3.10, 3.11, 3.12, 3.13, 3.14 - README / ARCHITECTURE: reflect new support range Floor stays at 3.10 (not 3.11) because GPU/ML base images still ship 3.10/3.11; plan a 3.10->3.11 bump after 3.10 EOL (2026-10). Refs SLS-265 * test: don't fail on nest_asyncio deprecation under Python 3.14 Adding 3.14 to the CI matrix surfaced a collection error: nest_asyncio (a test-only dependency) calls the deprecated asyncio.get_event_loop_policy, and pytest's -W error promoted the DeprecationWarning to a hard error, interrupting collection of test_worker.py. The SDK runtime does not use that API, so this is purely test infrastructure. Move the warnings config from addopts (-W error) into a filterwarnings list so an ignore for this specific third-party message takes precedence while all other warnings remain errors. Refs SLS-265 * test: modernize async worker tests for Python 3.14 nest_asyncio.apply() ran at import and globally patched asyncio; on Python 3.14 the patched loop breaks current_task(), so asyncio.timeout (used by asyncio.wait_for in the GPU fitness check) raised "Timeout should be used inside a task" and failed collection/run for any test touching it. The SDK runtime itself is 3.14-clean (plain asyncio.run provides the task context). - Remove nest_asyncio entirely and drop it from test deps. Convert the worker test classes from IsolatedAsyncioTestCase to TestCase: their bodies call runpod.serverless.start() synchronously (as production does), so run_worker's internal asyncio.run() no longer nests inside a running loop and needs no patching. - Stub run_fitness_checks in TestRunWorker.setUp so unit tests don't run real GPU/memory probes (covered by test_modules/test_fitness/). - test_download_files_from_urls: assert the set of requested URLs, not positional call order, since downloads run in parallel threads. Refs SLS-265 * test: ignore backoff's asyncio.iscoroutinefunction warning on 3.14 The backoff dependency calls asyncio.iscoroutinefunction in its on_exception decorator; Python 3.14 deprecates it (removal in 3.16). The SDK's own code uses inspect.iscoroutinefunction, and at runtime this is only a warning. Filter it (alongside the existing get_event_loop_policy ignore) so warnings-as-errors doesn't fail the suite until backoff updates. Refs SLS-265 * docs: bump ARCHITECTURE.md Last Updated for Python support change Addresses Copilot review: the Python support range was changed without updating the document's Last Updated header. Refs SLS-265
1 parent 92239c8 commit 8e9d94e

8 files changed

Lines changed: 52 additions & 35 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
runs-on: ubuntu-latest
2323
strategy:
2424
matrix:
25-
python-version: ["3.10", "3.11", "3.12"]
25+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
2626
steps:
2727
- uses: actions/checkout@v6
2828

ARCHITECTURE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Runpod Serverless Module Architecture
22

3-
**Last Updated**: 2025-12-13
3+
**Last Updated**: 2026-06-18
44
**Module**: `runpod/serverless/`
5-
**Python Support**: 3.8-3.11
5+
**Python Support**: 3.10-3.14
66

77
---
88

@@ -1467,4 +1467,4 @@ stateDiagram-v2
14671467
---
14681468

14691469
**Document Version**: 1.0
1470-
**Last Updated**: 2025-12-13
1470+
**Last Updated**: 2026-06-18

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ cd runpod-python
6363
pip install -e .
6464
```
6565

66-
*Python 3.8 or higher is required to use the latest version of this package.*
66+
*Python 3.10 or higher is required to use the latest version of this package.*
6767

6868
## ⚡ | Serverless Worker (SDK)
6969

pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "runpod"
33
dynamic = ["version", "dependencies"]
44
description = "🐍 | Python library for Runpod API and serverless worker SDK."
55
readme = { file = "README.md", content-type = "text/markdown" }
6-
requires-python = ">=3.8"
6+
requires-python = ">=3.10"
77
license = { text = "MIT License" }
88
authors = [
99
{ name = "Runpod", email = "engineer@runpod.io" },
@@ -25,10 +25,11 @@ classifiers = [
2525
"Operating System :: OS Independent",
2626
"Programming Language :: Python",
2727
"Programming Language :: Python :: 3",
28-
"Programming Language :: Python :: 3.8",
29-
"Programming Language :: Python :: 3.9",
3028
"Programming Language :: Python :: 3.10",
3129
"Programming Language :: Python :: 3.11",
30+
"Programming Language :: Python :: 3.12",
31+
"Programming Language :: Python :: 3.13",
32+
"Programming Language :: Python :: 3.14",
3233
"Topic :: Internet :: WWW/HTTP",
3334
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
3435
]
@@ -69,7 +70,6 @@ dev = [
6970
]
7071
test = [
7172
"asynctest",
72-
"nest_asyncio",
7373
"faker",
7474
"pytest-asyncio",
7575
"pytest-cov",

pytest.ini

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
[pytest]
2-
addopts = --durations=10 --cov-config=.coveragerc --timeout=120 --timeout_method=thread --cov=runpod --cov-report=xml --cov-report=term-missing --cov-fail-under=90 -W error -p no:cacheprovider -p no:unraisableexception
2+
addopts = --durations=10 --cov-config=.coveragerc --timeout=120 --timeout_method=thread --cov=runpod --cov-report=xml --cov-report=term-missing --cov-fail-under=90 -p no:cacheprovider -p no:unraisableexception
3+
filterwarnings =
4+
error
5+
# Third-party deps (e.g. backoff) still call asyncio APIs that 3.14 deprecates
6+
# and slates for removal in 3.16. The SDK's own code does not; at runtime these
7+
# only warn. Don't fail the suite on them until the deps update.
8+
ignore:'asyncio\.get_event_loop_policy' is deprecated:DeprecationWarning
9+
ignore:'asyncio\.iscoroutinefunction' is deprecated:DeprecationWarning
310
python_files = tests.py test_*.py *_test.py
411
norecursedirs = venv *.egg-info .git build tests/e2e
512
asyncio_mode = auto

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
install_requires=install_requires,
3535
extras_require=extras_require,
3636
packages=find_packages(),
37-
python_requires=">=3.8",
37+
python_requires=">=3.10",
3838
description="🐍 | Python library for Runpod API and serverless worker SDK.",
3939
long_description=long_description,
4040
long_description_content_type="text/markdown",

tests/test_serverless/test_utils/test_download.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,14 @@ def test_download_files_from_urls(self, mock_open_file, mock_get, mock_makedirs)
9595

9696
self.assertEqual(len(downloaded_files), len(urls))
9797

98-
for index, url in enumerate(urls):
99-
# Check that the url was called with SyncClientSession.get
100-
self.assertIn(url, mock_get.call_args_list[index][0])
101-
98+
# Downloads run in parallel threads, so the order of get() calls is
99+
# non-deterministic; assert the set of requested URLs instead of order.
100+
requested_urls = {call.args[0] for call in mock_get.call_args_list}
101+
self.assertEqual(requested_urls, set(urls))
102+
103+
# executor.map preserves input order in results, so downloaded_files
104+
# still aligns positionally with urls.
105+
for index in range(len(urls)):
102106
# Check that the file has the correct extension
103107
self.assertTrue(downloaded_files[index].endswith(".jpg"))
104108

tests/test_serverless/test_worker.py

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,19 @@
66
import os
77
import sys
88
from unittest import mock
9-
from unittest.mock import patch, mock_open, Mock, MagicMock
10-
11-
from unittest import IsolatedAsyncioTestCase
12-
import nest_asyncio
9+
from unittest import TestCase
10+
from unittest.mock import patch, mock_open, Mock, MagicMock, AsyncMock
1311

1412
import runpod
1513
from runpod.serverless.modules.rp_logger import RunPodLogger
1614
from runpod.serverless.modules.rp_scale import _handle_uncaught_exception
1715
from runpod.serverless import _signal_handler
1816

19-
nest_asyncio.apply()
20-
2117

22-
class TestWorker(IsolatedAsyncioTestCase):
18+
class TestWorker(TestCase):
2319
"""Tests for Runpod serverless worker."""
2420

25-
async def asyncSetUp(self):
21+
def setUp(self):
2622
self.mock_handler = mock.Mock(return_value="test")
2723
self.mock_config = {
2824
"handler": self.mock_handler,
@@ -105,10 +101,10 @@ def test_signal_handler(self, mock_exit, mock_logger):
105101
assert mock_logger.info.called
106102

107103

108-
class TestWorkerTestInput(IsolatedAsyncioTestCase):
104+
class TestWorkerTestInput(TestCase):
109105
"""Tests for runpod | serverless| worker"""
110106

111-
async def asyncSetUp(self):
107+
def setUp(self):
112108
self.mock_handler = Mock()
113109
self.mock_handler.return_value = {}
114110

@@ -176,28 +172,38 @@ def test_generator_handler_exception():
176172
assert True, "Exception was caught as expected"
177173

178174

179-
class TestRunWorker(IsolatedAsyncioTestCase):
175+
class TestRunWorker(TestCase):
180176
"""Tests for runpod | serverless| worker"""
181177

182-
async def asyncSetUp(self):
178+
def setUp(self):
183179
os.environ["RUNPOD_WEBHOOK_GET_JOB"] = "https://test.com"
184180

181+
# run_worker() runs real fitness checks (GPU/memory probes) that exit the
182+
# process when unmet; they have dedicated coverage in
183+
# test_modules/test_fitness/. Stub them so these tests are deterministic
184+
# regardless of host state.
185+
fitness_patcher = patch(
186+
"runpod.serverless.worker.run_fitness_checks", new=AsyncMock()
187+
)
188+
fitness_patcher.start()
189+
self.addCleanup(fitness_patcher.stop)
190+
185191
# Set up the config
186192
self.config = {
187193
"handler": MagicMock(),
188194
"refresh_worker": True,
189195
"rp_args": {"rp_debugger": True, "rp_log_level": "DEBUG"},
190196
}
191197

192-
async def asyncTearDown(self):
198+
def tearDown(self):
193199
sys.excepthook = sys.__excepthook__
194200

195201
@patch("runpod.serverless.modules.rp_scale.AsyncClientSession")
196202
@patch("runpod.serverless.modules.rp_scale.get_job")
197203
@patch("runpod.serverless.modules.rp_job.run_job")
198204
@patch("runpod.serverless.modules.rp_job.stream_result")
199205
@patch("runpod.serverless.modules.rp_job.send_result")
200-
async def test_run_worker(
206+
def test_run_worker(
201207
self,
202208
mock_send_result,
203209
mock_stream_result,
@@ -228,7 +234,7 @@ async def test_run_worker(
228234
@patch("runpod.serverless.modules.rp_job.run_job")
229235
@patch("runpod.serverless.modules.rp_job.stream_result")
230236
@patch("runpod.serverless.modules.rp_job.send_result")
231-
async def test_run_worker_generator_handler(
237+
def test_run_worker_generator_handler(
232238
self, mock_send_result, mock_stream_result, mock_run_job, mock_get_job
233239
):
234240
"""
@@ -258,7 +264,7 @@ async def test_run_worker_generator_handler(
258264
@patch("runpod.serverless.modules.rp_job.run_job")
259265
@patch("runpod.serverless.modules.rp_job.stream_result")
260266
@patch("runpod.serverless.modules.rp_job.send_result")
261-
async def test_run_worker_generator_handler_exception(
267+
def test_run_worker_generator_handler_exception(
262268
self, mock_send_result, mock_stream_result, mock_run_job, mock_get_job
263269
):
264270
"""
@@ -303,7 +309,7 @@ async def test_run_worker_generator_handler_exception(
303309
@patch("runpod.serverless.modules.rp_job.run_job")
304310
@patch("runpod.serverless.modules.rp_job.stream_result")
305311
@patch("runpod.serverless.modules.rp_job.send_result")
306-
async def test_run_worker_generator_aggregate_handler(
312+
def test_run_worker_generator_aggregate_handler(
307313
self, mock_send_result, mock_stream_result, mock_run_job, mock_get_job
308314
):
309315
"""
@@ -343,7 +349,7 @@ async def test_run_worker_generator_aggregate_handler(
343349
@patch("runpod.serverless.modules.rp_job.run_job")
344350
@patch("runpod.serverless.modules.rp_job.stream_result")
345351
@patch("runpod.serverless.modules.rp_job.send_result")
346-
async def test_run_worker_concurrency(
352+
def test_run_worker_concurrency(
347353
self,
348354
mock_send_result,
349355
mock_stream_result,
@@ -420,7 +426,7 @@ def concurrency_modifier(current_concurrency):
420426
@patch("runpod.serverless.modules.rp_job.run_job")
421427
@patch("runpod.serverless.modules.rp_job.stream_result")
422428
@patch("runpod.serverless.modules.rp_job.send_result")
423-
async def test_run_worker_multi_processing(
429+
def test_run_worker_multi_processing(
424430
self,
425431
mock_send_result,
426432
mock_stream_result,
@@ -480,7 +486,7 @@ async def test_run_worker_multi_processing(
480486

481487
@patch("runpod.serverless.modules.rp_scale.get_job")
482488
@patch("runpod.serverless.modules.rp_job.run_job")
483-
async def test_run_worker_multi_processing_scaling_up(
489+
def test_run_worker_multi_processing_scaling_up(
484490
self, mock_run_job, mock_get_job
485491
):
486492
"""

0 commit comments

Comments
 (0)