diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 167730c0..a9f64528 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v6 diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 16bfffa1..2dbca2ef 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,8 +1,8 @@ # Runpod Serverless Module Architecture -**Last Updated**: 2025-12-13 +**Last Updated**: 2026-06-18 **Module**: `runpod/serverless/` -**Python Support**: 3.8-3.11 +**Python Support**: 3.10-3.14 --- @@ -1467,4 +1467,4 @@ stateDiagram-v2 --- **Document Version**: 1.0 -**Last Updated**: 2025-12-13 +**Last Updated**: 2026-06-18 diff --git a/README.md b/README.md index f8911942..b34014b1 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ cd runpod-python pip install -e . ``` -*Python 3.8 or higher is required to use the latest version of this package.* +*Python 3.10 or higher is required to use the latest version of this package.* ## ⚡ | Serverless Worker (SDK) diff --git a/pyproject.toml b/pyproject.toml index c88c8ec2..d4639a15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "runpod" dynamic = ["version", "dependencies"] description = "🐍 | Python library for Runpod API and serverless worker SDK." readme = { file = "README.md", content-type = "text/markdown" } -requires-python = ">=3.8" +requires-python = ">=3.10" license = { text = "MIT License" } authors = [ { name = "Runpod", email = "engineer@runpod.io" }, @@ -25,10 +25,11 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", ] @@ -69,7 +70,6 @@ dev = [ ] test = [ "asynctest", - "nest_asyncio", "faker", "pytest-asyncio", "pytest-cov", diff --git a/pytest.ini b/pytest.ini index 165c6b91..1c2c135e 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,5 +1,12 @@ [pytest] -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 +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 +filterwarnings = + error + # Third-party deps (e.g. backoff) still call asyncio APIs that 3.14 deprecates + # and slates for removal in 3.16. The SDK's own code does not; at runtime these + # only warn. Don't fail the suite on them until the deps update. + ignore:'asyncio\.get_event_loop_policy' is deprecated:DeprecationWarning + ignore:'asyncio\.iscoroutinefunction' is deprecated:DeprecationWarning python_files = tests.py test_*.py *_test.py norecursedirs = venv *.egg-info .git build tests/e2e asyncio_mode = auto diff --git a/setup.py b/setup.py index ebd4c2d1..6fbfa776 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ install_requires=install_requires, extras_require=extras_require, packages=find_packages(), - python_requires=">=3.8", + python_requires=">=3.10", description="🐍 | Python library for Runpod API and serverless worker SDK.", long_description=long_description, long_description_content_type="text/markdown", diff --git a/tests/test_serverless/test_utils/test_download.py b/tests/test_serverless/test_utils/test_download.py index bc04db95..b7533f5c 100644 --- a/tests/test_serverless/test_utils/test_download.py +++ b/tests/test_serverless/test_utils/test_download.py @@ -95,10 +95,14 @@ def test_download_files_from_urls(self, mock_open_file, mock_get, mock_makedirs) self.assertEqual(len(downloaded_files), len(urls)) - for index, url in enumerate(urls): - # Check that the url was called with SyncClientSession.get - self.assertIn(url, mock_get.call_args_list[index][0]) - + # Downloads run in parallel threads, so the order of get() calls is + # non-deterministic; assert the set of requested URLs instead of order. + requested_urls = {call.args[0] for call in mock_get.call_args_list} + self.assertEqual(requested_urls, set(urls)) + + # executor.map preserves input order in results, so downloaded_files + # still aligns positionally with urls. + for index in range(len(urls)): # Check that the file has the correct extension self.assertTrue(downloaded_files[index].endswith(".jpg")) diff --git a/tests/test_serverless/test_worker.py b/tests/test_serverless/test_worker.py index e1fd743f..88f969ba 100644 --- a/tests/test_serverless/test_worker.py +++ b/tests/test_serverless/test_worker.py @@ -6,23 +6,19 @@ import os import sys from unittest import mock -from unittest.mock import patch, mock_open, Mock, MagicMock - -from unittest import IsolatedAsyncioTestCase -import nest_asyncio +from unittest import TestCase +from unittest.mock import patch, mock_open, Mock, MagicMock, AsyncMock import runpod from runpod.serverless.modules.rp_logger import RunPodLogger from runpod.serverless.modules.rp_scale import _handle_uncaught_exception from runpod.serverless import _signal_handler -nest_asyncio.apply() - -class TestWorker(IsolatedAsyncioTestCase): +class TestWorker(TestCase): """Tests for Runpod serverless worker.""" - async def asyncSetUp(self): + def setUp(self): self.mock_handler = mock.Mock(return_value="test") self.mock_config = { "handler": self.mock_handler, @@ -105,10 +101,10 @@ def test_signal_handler(self, mock_exit, mock_logger): assert mock_logger.info.called -class TestWorkerTestInput(IsolatedAsyncioTestCase): +class TestWorkerTestInput(TestCase): """Tests for runpod | serverless| worker""" - async def asyncSetUp(self): + def setUp(self): self.mock_handler = Mock() self.mock_handler.return_value = {} @@ -176,12 +172,22 @@ def test_generator_handler_exception(): assert True, "Exception was caught as expected" -class TestRunWorker(IsolatedAsyncioTestCase): +class TestRunWorker(TestCase): """Tests for runpod | serverless| worker""" - async def asyncSetUp(self): + def setUp(self): os.environ["RUNPOD_WEBHOOK_GET_JOB"] = "https://test.com" + # run_worker() runs real fitness checks (GPU/memory probes) that exit the + # process when unmet; they have dedicated coverage in + # test_modules/test_fitness/. Stub them so these tests are deterministic + # regardless of host state. + fitness_patcher = patch( + "runpod.serverless.worker.run_fitness_checks", new=AsyncMock() + ) + fitness_patcher.start() + self.addCleanup(fitness_patcher.stop) + # Set up the config self.config = { "handler": MagicMock(), @@ -189,7 +195,7 @@ async def asyncSetUp(self): "rp_args": {"rp_debugger": True, "rp_log_level": "DEBUG"}, } - async def asyncTearDown(self): + def tearDown(self): sys.excepthook = sys.__excepthook__ @patch("runpod.serverless.modules.rp_scale.AsyncClientSession") @@ -197,7 +203,7 @@ async def asyncTearDown(self): @patch("runpod.serverless.modules.rp_job.run_job") @patch("runpod.serverless.modules.rp_job.stream_result") @patch("runpod.serverless.modules.rp_job.send_result") - async def test_run_worker( + def test_run_worker( self, mock_send_result, mock_stream_result, @@ -228,7 +234,7 @@ async def test_run_worker( @patch("runpod.serverless.modules.rp_job.run_job") @patch("runpod.serverless.modules.rp_job.stream_result") @patch("runpod.serverless.modules.rp_job.send_result") - async def test_run_worker_generator_handler( + def test_run_worker_generator_handler( self, mock_send_result, mock_stream_result, mock_run_job, mock_get_job ): """ @@ -258,7 +264,7 @@ async def test_run_worker_generator_handler( @patch("runpod.serverless.modules.rp_job.run_job") @patch("runpod.serverless.modules.rp_job.stream_result") @patch("runpod.serverless.modules.rp_job.send_result") - async def test_run_worker_generator_handler_exception( + def test_run_worker_generator_handler_exception( self, mock_send_result, mock_stream_result, mock_run_job, mock_get_job ): """ @@ -303,7 +309,7 @@ async def test_run_worker_generator_handler_exception( @patch("runpod.serverless.modules.rp_job.run_job") @patch("runpod.serverless.modules.rp_job.stream_result") @patch("runpod.serverless.modules.rp_job.send_result") - async def test_run_worker_generator_aggregate_handler( + def test_run_worker_generator_aggregate_handler( self, mock_send_result, mock_stream_result, mock_run_job, mock_get_job ): """ @@ -343,7 +349,7 @@ async def test_run_worker_generator_aggregate_handler( @patch("runpod.serverless.modules.rp_job.run_job") @patch("runpod.serverless.modules.rp_job.stream_result") @patch("runpod.serverless.modules.rp_job.send_result") - async def test_run_worker_concurrency( + def test_run_worker_concurrency( self, mock_send_result, mock_stream_result, @@ -420,7 +426,7 @@ def concurrency_modifier(current_concurrency): @patch("runpod.serverless.modules.rp_job.run_job") @patch("runpod.serverless.modules.rp_job.stream_result") @patch("runpod.serverless.modules.rp_job.send_result") - async def test_run_worker_multi_processing( + def test_run_worker_multi_processing( self, mock_send_result, mock_stream_result, @@ -480,7 +486,7 @@ async def test_run_worker_multi_processing( @patch("runpod.serverless.modules.rp_scale.get_job") @patch("runpod.serverless.modules.rp_job.run_job") - async def test_run_worker_multi_processing_scaling_up( + def test_run_worker_multi_processing_scaling_up( self, mock_run_job, mock_get_job ): """