From 534a67de77e0a445d3637673a780d005b92e5ac4 Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Mon, 31 Aug 2026 11:01:06 +0530 Subject: [PATCH 1/2] Make AppHarness dependencies installable --- news/6974.bugfix.md | 1 + pyproject.toml | 5 +++++ reflex/testing.py | 21 +++++++++++++++++++-- tests/units/test_testing.py | 5 +++++ uv.lock | 5 +++++ 5 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 news/6974.bugfix.md diff --git a/news/6974.bugfix.md b/news/6974.bugfix.md new file mode 100644 index 00000000000..687d3cf59a6 --- /dev/null +++ b/news/6974.bugfix.md @@ -0,0 +1 @@ +Make `reflex.testing` importable without test-only dependencies and provide a `testing` extra for `AppHarness`. diff --git a/pyproject.toml b/pyproject.toml index 959df1a5633..244ba684b92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,6 +66,11 @@ db = [ "sqlmodel >=0.0.24,<0.1", ] pydantic = ["reflex-base[pydantic]"] +testing = [ + "psutil >=7.0.0,<8.0", + "selenium >=4.0.0,<5.0", + "uvicorn >=0.34.0,<1.0", +] [project.urls] homepage = "https://reflex.dev" diff --git a/reflex/testing.py b/reflex/testing.py index 51c950e637a..c66bdbba2c7 100644 --- a/reflex/testing.py +++ b/reflex/testing.py @@ -25,7 +25,6 @@ from pathlib import Path from typing import TYPE_CHECKING, Any, ClassVar, Literal, TypeVar -import uvicorn from reflex_base.components.memo import MEMOS from reflex_base.config import get_config, reload_config from reflex_base.environment import environment @@ -61,6 +60,9 @@ except ImportError: has_selenium = False +if TYPE_CHECKING: + import uvicorn + # The timeout (minutes) to check for the port. DEFAULT_TIMEOUT = 15 POLL_INTERVAL = 0.25 @@ -341,6 +343,14 @@ def _start_backend(self, port: int = 0): if self.app_asgi is None: msg = "App was not initialized." raise RuntimeError(msg) + try: + import uvicorn + except ImportError as exc: + msg = ( + "AppHarness backend support requires `uvicorn`. Install it with " + "`pip install 'reflex[testing]'`." + ) + raise ImportError(msg) from exc self.backend = uvicorn.Server( uvicorn.Config( app=self.app_asgi, @@ -470,7 +480,14 @@ def __enter__(self) -> Self: def stop(self) -> None: """Stop the frontend and backend servers.""" - import psutil + try: + import psutil + except ImportError as exc: + msg = ( + "AppHarness cleanup requires `psutil`. Install it with " + "`pip install 'reflex[testing]'`." + ) + raise ImportError(msg) from exc # Quit browsers first to avoid any lingering events being sent during shutdown. for driver in self._frontends: diff --git a/tests/units/test_testing.py b/tests/units/test_testing.py index a38682c06af..917b4e52050 100644 --- a/tests/units/test_testing.py +++ b/tests/units/test_testing.py @@ -19,6 +19,11 @@ from reflex.utils.exec import should_prerender_routes +def test_testing_module_does_not_import_uvicorn_at_module_load(): + """Importing reflex.testing does not require the AppHarness backend runtime.""" + assert "uvicorn" not in reflex_testing.__dict__ + + @pytest.mark.skip("Slow test that makes network requests.") def test_app_harness(tmp_path): """Ensure that AppHarness can compile and start an app. diff --git a/uv.lock b/uv.lock index 4212881b6da..27c78760b53 100644 --- a/uv.lock +++ b/uv.lock @@ -3676,6 +3676,11 @@ db = [ pydantic = [ { name = "reflex-base", extra = ["pydantic"] }, ] +testing = [ + { name = "psutil" }, + { name = "selenium" }, + { name = "uvicorn" }, +] [package.dev-dependencies] dev = [ From d26555a598237f41070ffe92fe6a6d3bc817774f Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Mon, 31 Aug 2026 11:14:11 +0530 Subject: [PATCH 2/2] Fix lazy uvicorn loading for AppHarnessProd --- reflex/testing.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/reflex/testing.py b/reflex/testing.py index c66bdbba2c7..660f89b81bf 100644 --- a/reflex/testing.py +++ b/reflex/testing.py @@ -69,6 +69,25 @@ FRONTEND_POPEN_ARGS = {} T = TypeVar("T") TimeoutType = int | float | None + + +def _get_uvicorn(): + """Import uvicorn for an AppHarness server. + + Returns: + The imported uvicorn module. + """ + try: + import uvicorn + except ImportError as exc: + msg = ( + "AppHarness backend support requires `uvicorn`. Install it with " + "`pip install 'reflex[testing]'`." + ) + raise ImportError(msg) from exc + return uvicorn + + if platform.system() == "Windows": FRONTEND_POPEN_ARGS["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP # pyright: ignore [reportAttributeAccessIssue] FRONTEND_POPEN_ARGS["shell"] = True @@ -343,14 +362,7 @@ def _start_backend(self, port: int = 0): if self.app_asgi is None: msg = "App was not initialized." raise RuntimeError(msg) - try: - import uvicorn - except ImportError as exc: - msg = ( - "AppHarness backend support requires `uvicorn`. Install it with " - "`pip install 'reflex[testing]'`." - ) - raise ImportError(msg) from exc + uvicorn = _get_uvicorn() self.backend = uvicorn.Server( uvicorn.Config( app=self.app_asgi, @@ -842,6 +854,7 @@ class AppHarnessProd(AppHarness): frontend_server: uvicorn.Server | None = None def _run_frontend(self): + uvicorn = _get_uvicorn() with chdir(self.app_path): frontend_app = reflex.utils.exec._frontend_prod_app() self.frontend_server = uvicorn.Server( @@ -910,6 +923,7 @@ def _start_backend(self): msg = "App was not initialized." raise RuntimeError(msg) environment.REFLEX_SKIP_COMPILE.set(True) + uvicorn = _get_uvicorn() self.backend = uvicorn.Server( uvicorn.Config( app=self.app_asgi,