diff --git a/pyproject.toml b/pyproject.toml index 7fa2954d18c..190d90dc275 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ dependencies = [ "click >=8.2", "granian[reload] >=2.7.4", "httpx >=0.26,<1.0", + "httpx2 >=2.0; python_version >= '3.10'", "packaging >=24.2,<27", "psutil >=7.0.0,<8.0; sys_platform == 'win32'", "python-multipart >=0.0.32,<1.0", diff --git a/reflex/custom_components/custom_components.py b/reflex/custom_components/custom_components.py index 35dd3bcb4da..2e97bb799e6 100644 --- a/reflex/custom_components/custom_components.py +++ b/reflex/custom_components/custom_components.py @@ -634,7 +634,10 @@ def _collect_details_for_gallery(): Raises: SystemExit: If pyproject.toml file is ill-formed or the request to the backend services fails. """ - import httpx + try: + import httpx2 as httpx + except ModuleNotFoundError: + import httpx from reflex_cli.utils import hosting console.rule("[bold]Authentication with Reflex Services") diff --git a/reflex/utils/frontend_skeleton.py b/reflex/utils/frontend_skeleton.py index 9a5fa3d9ee3..55590de7897 100644 --- a/reflex/utils/frontend_skeleton.py +++ b/reflex/utils/frontend_skeleton.py @@ -141,7 +141,10 @@ def initialize_agents_md( """ plan = _plan_agents_md(agents_file, claude_file) - import httpx + try: + import httpx2 as httpx + except ModuleNotFoundError: + import httpx logger.debug(f"Fetching {url}") try: diff --git a/reflex/utils/js_runtimes.py b/reflex/utils/js_runtimes.py index 2f5b9a5188d..469fdd7ddce 100644 --- a/reflex/utils/js_runtimes.py +++ b/reflex/utils/js_runtimes.py @@ -234,7 +234,10 @@ def download_and_run(url: str, *args, show_status: bool = False, **env): Raises: SystemExit: If the script fails to download. """ - import httpx + try: + import httpx2 as httpx + except ModuleNotFoundError: + import httpx # Download the script logger.debug(f"Downloading {url}") diff --git a/reflex/utils/net.py b/reflex/utils/net.py index ffa96b95ce3..4d83372ba53 100644 --- a/reflex/utils/net.py +++ b/reflex/utils/net.py @@ -41,7 +41,10 @@ def _wrap_https_func( @functools.wraps(func) def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _T: - import httpx + try: + import httpx2 as httpx + except ModuleNotFoundError: + import httpx url = args[0] logger.debug(f"Sending HTTPS request to {args[0]}") @@ -95,7 +98,10 @@ def _is_ipv4_supported() -> bool: Returns: True if the system supports IPv4, False otherwise. """ - import httpx + try: + import httpx2 as httpx + except ModuleNotFoundError: + import httpx try: httpx.head("http://1.1.1.1", timeout=3) @@ -111,7 +117,10 @@ def _is_ipv6_supported() -> bool: Returns: True if the system supports IPv6, False otherwise. """ - import httpx + try: + import httpx2 as httpx + except ModuleNotFoundError: + import httpx try: httpx.head("http://[2606:4700:4700::1111]", timeout=3) @@ -150,12 +159,29 @@ def _httpx_client(): Returns: An HTTPX client. """ - import httpx - from httpx._utils import get_environment_proxies + # Resolve the active HTTP library at call time. Prefer httpx2 when + # available, fall back to real httpx on Python 3.8/3.9 (which httpx2 + # cannot run on). Bind the classes to local names so pyright does not + # infer a union of `httpx2.HTTPTransport | httpx.HTTPTransport` — + # that union is not assignable to `Client(mounts=...)` because the + # two transport classes are unrelated. + try: + import httpx2 + from httpx2._utils import get_environment_proxies + except ModuleNotFoundError: + import httpx as httpx2 # noqa: F401 — local name `httpx2` bound to the real httpx + from httpx._utils import get_environment_proxies # noqa: F811 verify_setting = _httpx_verify_kwarg() - return httpx.Client( - transport=httpx.HTTPTransport( + # `httpx2` is a union of the two modules here (httpx2 in the try + # branch, real httpx in the except branch). The two HTTPTransport + # / Proxy / Client classes share compatible shapes but pyright in + # min-version mode still infers a union and rejects the assignment + # to `BaseTransport` / `ProxyTypes`. In practice only one branch + # runs per process; the `# type: ignore` below is the smallest way + # to tell pyright that, suppressing the no-real-error warnings. + return httpx2.Client( # type: ignore[call-overload] + transport=httpx2.HTTPTransport( # type: ignore[arg-type] local_address=_httpx_local_address_kwarg(), verify=verify_setting, ), @@ -163,8 +189,9 @@ def _httpx_client(): key: ( None if url is None - else httpx.HTTPTransport( - proxy=httpx.Proxy(url=url), verify=verify_setting + else httpx2.HTTPTransport( # type: ignore[arg-type] + proxy=httpx2.Proxy(url=url), # type: ignore[arg-type] + verify=verify_setting, ) ) for key, url in get_environment_proxies().items() @@ -172,4 +199,4 @@ def _httpx_client(): ) -get = _wrap_https_lazy_func(lambda: _httpx_client().get) +get = _wrap_https_lazy_func(lambda: _httpx_client().get) # type: ignore[arg-type] diff --git a/reflex/utils/registry.py b/reflex/utils/registry.py index 994f7ea1ae0..b34dbc16b9d 100644 --- a/reflex/utils/registry.py +++ b/reflex/utils/registry.py @@ -20,7 +20,10 @@ def latency(registry: str) -> int: Returns: int: The latency of the registry in microseconds. """ - import httpx + try: + import httpx2 as httpx + except ModuleNotFoundError: + import httpx try: time_to_respond = net.get(registry, timeout=2).elapsed.microseconds diff --git a/reflex/utils/telemetry.py b/reflex/utils/telemetry.py index a1d76d2e8b5..334b15cebf9 100644 --- a/reflex/utils/telemetry.py +++ b/reflex/utils/telemetry.py @@ -435,7 +435,10 @@ def _prepare_event( def _send_event(event_data: _Event) -> bool: - import httpx + try: + import httpx2 as httpx + except ModuleNotFoundError: + import httpx try: httpx.post(POSTHOG_API_URL, json=event_data) diff --git a/reflex/utils/templates.py b/reflex/utils/templates.py index f47976e28ad..e0203ec4b3e 100644 --- a/reflex/utils/templates.py +++ b/reflex/utils/templates.py @@ -122,7 +122,10 @@ def create_config_init_app_from_remote_template(app_name: str, template_url: str SystemExit: If any download, file operations fail or unexpected zip file format. """ - import httpx + try: + import httpx2 as httpx + except ModuleNotFoundError: + import httpx # Create a temp directory for the zip download. try: diff --git a/tests/units/test_telemetry.py b/tests/units/test_telemetry.py index aa530e39ccf..7ee3c8c0985 100644 --- a/tests/units/test_telemetry.py +++ b/tests/units/test_telemetry.py @@ -66,7 +66,11 @@ def httpx_post(mocker: MockerFixture): Returns: The mock for ``httpx.post`` so tests can assert on the posted payload. """ - return mocker.patch("httpx.post") + try: + import httpx2 as httpx + except ModuleNotFoundError: + import httpx + return mocker.patch.object(httpx, "post") def test_telemetry(): diff --git a/tests/units/utils/test_utils.py b/tests/units/utils/test_utils.py index 98ebe80475b..d643124c5a4 100644 --- a/tests/units/utils/test_utils.py +++ b/tests/units/utils/test_utils.py @@ -537,7 +537,10 @@ def test_initialize_agents_md_refreshes_managed_section(tmp_path, mocker): def test_initialize_agents_md_warns_on_fetch_failure(tmp_path, mocker, caplog): """Test that a failed fetch warns without writing AGENTS.md or the bridge.""" - import httpx + try: + import httpx2 as httpx + except ModuleNotFoundError: + import httpx agents_file = tmp_path / "AGENTS.md" claude_file = tmp_path / "CLAUDE.md" diff --git a/uv.lock b/uv.lock index 43f7d18e4d3..034b43b0cf9 100644 --- a/uv.lock +++ b/uv.lock @@ -15,7 +15,7 @@ resolution-markers = [ ] [options] -exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer = "2026-08-27T03:10:58.998873Z" exclude-newer-span = "P7D" [options.exclude-newer-package] @@ -659,7 +659,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } wheels = [ @@ -1068,7 +1068,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -1993,15 +1993,15 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" } }, - { name = "cycler" }, - { name = "fonttools" }, - { name = "kiwisolver" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, - { name = "packaging" }, - { name = "pillow" }, - { name = "pyparsing" }, - { name = "python-dateutil" }, + { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "cycler", marker = "python_full_version < '3.11'" }, + { name = "fonttools", marker = "python_full_version < '3.11'" }, + { name = "kiwisolver", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "pillow", marker = "python_full_version < '3.11'" }, + { name = "pyparsing", marker = "python_full_version < '3.11'" }, + { name = "python-dateutil", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233, upload-time = "2026-04-24T00:14:13.539Z" } wheels = [ @@ -2659,10 +2659,10 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, - { name = "python-dateutil" }, - { name = "pytz" }, - { name = "tzdata" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "python-dateutil", marker = "python_full_version < '3.11'" }, + { name = "pytz", marker = "python_full_version < '3.11'" }, + { name = "tzdata", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } wheels = [ @@ -3767,6 +3767,7 @@ dependencies = [ { name = "click" }, { name = "granian", extra = ["reload"] }, { name = "httpx" }, + { name = "httpx2" }, { name = "packaging" }, { name = "psutil", marker = "sys_platform == 'win32'" }, { name = "python-multipart" }, @@ -3858,6 +3859,7 @@ requires-dist = [ { name = "click", specifier = ">=8.2" }, { name = "granian", extras = ["reload"], specifier = ">=2.7.4" }, { name = "httpx", specifier = ">=0.26,<1.0" }, + { name = "httpx2", marker = "python_full_version >= '3.10'", specifier = ">=2.0" }, { name = "packaging", specifier = ">=24.2,<27" }, { name = "psutil", marker = "sys_platform == 'win32'", specifier = ">=7.0.0,<8.0" }, { name = "pydantic", marker = "extra == 'db'", specifier = ">=2.12.0,<3.0" }, @@ -4431,7 +4433,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } wheels = [ @@ -4492,7 +4494,7 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } wheels = [ @@ -4997,6 +4999,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, ] +[[package]] +name = "ty" +version = "0.0.69" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, +] + [[package]] name = "ty" version = "0.0.73"