From 5222a85b2c29e6c3f37410555810f1f940be3f66 Mon Sep 17 00:00:00 2001 From: jonathan343 Date: Tue, 19 May 2026 13:57:11 -0400 Subject: [PATCH 1/3] Add opt-in httpx import alias helper --- README.md | 18 +++++++ docs/api.md | 4 ++ docs/index.md | 18 +++++++ src/httpx2/CHANGELOG.md | 6 +++ src/httpx2/httpx2/__init__.py | 2 + src/httpx2/httpx2/_alias.py | 60 ++++++++++++++++++++++ tests/httpx2/test_alias.py | 94 +++++++++++++++++++++++++++++++++++ 7 files changed, 202 insertions(+) create mode 100644 src/httpx2/httpx2/_alias.py create mode 100644 tests/httpx2/test_alias.py diff --git a/README.md b/README.md index 0f707b32..64700597 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,24 @@ Or, to include the optional HTTP/2 support, use: pip install httpx2[http2] ``` +### Migrating existing `httpx` imports + +HTTPX2 can be explicitly aliased as `httpx` for applications that need a staged +migration path. This is opt-in, process-local, and must run before anything +imports `httpx`: + +```python +import httpx2 + +httpx2.enable_httpx_alias() + +import httpx +``` + +This will fail if the real `httpx` package is installed in the environment, or +if another `httpx` module has already been imported. Uninstall `httpx` first, or +use `import httpx2` directly. + ## Documentation Project documentation is available at [https://httpx2.pydantic.dev/](https://httpx2.pydantic.dev/). diff --git a/docs/api.md b/docs/api.md index 6ef4ac4b..9f9cde5b 100644 --- a/docs/api.md +++ b/docs/api.md @@ -26,6 +26,10 @@ ::: httpx2.stream +## Compatibility Helpers + +::: httpx2.enable_httpx_alias + ## `Client` ::: httpx2.Client diff --git a/docs/index.md b/docs/index.md index 7ceb670f..a8b270c1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -143,3 +143,21 @@ To include the optional brotli and zstandard decoders support, use: ```shell pip install 'httpx2[brotli,zstd]' ``` + +## Migrating existing `httpx` imports + +HTTPX2 can be explicitly aliased as `httpx` for applications that need a staged +migration path. This is opt-in, process-local, and must run before anything +imports `httpx`: + +```python +import httpx2 + +httpx2.enable_httpx_alias() + +import httpx +``` + +This will fail if the real `httpx` package is installed in the environment, or +if another `httpx` module has already been imported. Uninstall `httpx` first, or +use `import httpx2` directly. diff --git a/src/httpx2/CHANGELOG.md b/src/httpx2/CHANGELOG.md index 873e203a..685061a1 100644 --- a/src/httpx2/CHANGELOG.md +++ b/src/httpx2/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## Unreleased + +### Added + +* Add `httpx2.enable_httpx_alias()` as an opt-in migration helper for applications that need `import httpx` to resolve to `httpx2`. + ## 2.2.0 (May 16th, 2026) ### Fixed diff --git a/src/httpx2/httpx2/__init__.py b/src/httpx2/httpx2/__init__.py index 068e0a25..e7b222d5 100644 --- a/src/httpx2/httpx2/__init__.py +++ b/src/httpx2/httpx2/__init__.py @@ -1,4 +1,5 @@ from .__version__ import __description__, __title__, __version__ +from ._alias import * from ._api import * from ._auth import * from ._client import * @@ -35,6 +36,7 @@ "DecodingError", "delete", "DigestAuth", + "enable_httpx_alias", "FunctionAuth", "get", "head", diff --git a/src/httpx2/httpx2/_alias.py b/src/httpx2/httpx2/_alias.py new file mode 100644 index 00000000..9f80368f --- /dev/null +++ b/src/httpx2/httpx2/_alias.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +import sys +from importlib import metadata + +__all__ = ["enable_httpx_alias"] + + +def _httpx_distribution_installed() -> bool: + try: + metadata.distribution("httpx") + except metadata.PackageNotFoundError: + return False + + return True + + +def _alias_loaded_httpx2_modules() -> None: + for name, module in list(sys.modules.items()): + if name == "httpx2" or name.startswith("httpx2."): + alias_name = f"httpx{name.removeprefix('httpx2')}" + existing_module = sys.modules.get(alias_name) + if existing_module is not None and existing_module is not module: + raise RuntimeError( + "Cannot alias httpx2 as httpx because another httpx module is already loaded. " + "Call httpx2.enable_httpx_alias() before anything imports httpx." + ) + sys.modules[alias_name] = module + + +def enable_httpx_alias() -> None: + """ + Alias `httpx2` as `httpx` for compatibility with existing code. + + This must be called before anything imports `httpx`. It will fail if the + real `httpx` package is installed or if another `httpx` module has already + been imported in the current process. + """ + httpx2_module = sys.modules["httpx2"] + + existing_httpx_module = sys.modules.get("httpx") + if existing_httpx_module is httpx2_module: + _alias_loaded_httpx2_modules() + return + + loaded_httpx_modules = [name for name in sys.modules if name == "httpx" or name.startswith("httpx.")] + if loaded_httpx_modules: + raise RuntimeError( + "Cannot alias httpx2 as httpx because another httpx module is already loaded. " + "Call httpx2.enable_httpx_alias() before anything imports httpx." + ) + + if _httpx_distribution_installed(): + raise RuntimeError( + "Cannot alias httpx2 as httpx because the httpx distribution is installed. " + "Uninstall httpx or use import httpx2 directly." + ) + + sys.modules["httpx"] = httpx2_module + _alias_loaded_httpx2_modules() diff --git a/tests/httpx2/test_alias.py b/tests/httpx2/test_alias.py new file mode 100644 index 00000000..f3324cfb --- /dev/null +++ b/tests/httpx2/test_alias.py @@ -0,0 +1,94 @@ +import sys +import types +import typing + +import pytest + +import httpx2 +from httpx2 import _alias + + +@pytest.fixture(autouse=True) +def restore_httpx_modules() -> typing.Iterator[None]: + original_modules = {name: module for name, module in sys.modules.items() if _is_httpx_module(name)} + for name in original_modules: + del sys.modules[name] + + yield + + for name in [name for name in sys.modules if _is_httpx_module(name)]: + del sys.modules[name] + sys.modules.update(original_modules) + + +def _is_httpx_module(name: str) -> bool: + return name == "httpx" or name.startswith("httpx.") + + +def _httpx_distribution_missing() -> bool: + return False + + +def _httpx_distribution_installed() -> bool: + return True + + +def test_enable_httpx_alias(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(_alias, "_httpx_distribution_installed", _httpx_distribution_missing) + + httpx2.enable_httpx_alias() + + import httpx + + assert httpx is httpx2 + assert httpx.Client is httpx2.Client + + +def test_enable_httpx_alias_is_idempotent(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(_alias, "_httpx_distribution_installed", _httpx_distribution_missing) + + httpx2.enable_httpx_alias() + httpx2.enable_httpx_alias() + + assert sys.modules["httpx"] is httpx2 + + +def test_enable_httpx_alias_aliases_loaded_submodules(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(_alias, "_httpx_distribution_installed", _httpx_distribution_missing) + + httpx2.enable_httpx_alias() + + import httpx._client as httpx_client + + assert httpx_client is sys.modules["httpx2._client"] + + +def test_enable_httpx_alias_fails_when_httpx_distribution_is_installed(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(_alias, "_httpx_distribution_installed", _httpx_distribution_installed) + + with pytest.raises(RuntimeError, match="httpx distribution is installed"): + httpx2.enable_httpx_alias() + + +def test_enable_httpx_alias_fails_when_httpx_is_loaded() -> None: + sys.modules["httpx"] = types.ModuleType("httpx") + + with pytest.raises(RuntimeError, match="another httpx module is already loaded"): + httpx2.enable_httpx_alias() + + +def test_enable_httpx_alias_fails_when_httpx_submodule_is_loaded() -> None: + sys.modules["httpx._client"] = types.ModuleType("httpx._client") + + with pytest.raises(RuntimeError, match="another httpx module is already loaded"): + httpx2.enable_httpx_alias() + + +def test_enable_httpx_alias_fails_when_alias_submodule_is_replaced(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(_alias, "_httpx_distribution_installed", _httpx_distribution_missing) + + httpx2.enable_httpx_alias() + sys.modules["httpx._client"] = types.ModuleType("httpx._client") + + with pytest.raises(RuntimeError, match="another httpx module is already loaded"): + httpx2.enable_httpx_alias() From 8fd99f4814a7b246ff8e002a5532124ffa2864ed Mon Sep 17 00:00:00 2001 From: jonathan343 Date: Tue, 19 May 2026 14:25:29 -0400 Subject: [PATCH 2/3] Cover httpx alias distribution checks --- tests/httpx2/test_alias.py | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/tests/httpx2/test_alias.py b/tests/httpx2/test_alias.py index f3324cfb..13e31fb8 100644 --- a/tests/httpx2/test_alias.py +++ b/tests/httpx2/test_alias.py @@ -1,6 +1,7 @@ import sys import types import typing +from importlib import metadata import pytest @@ -11,14 +12,17 @@ @pytest.fixture(autouse=True) def restore_httpx_modules() -> typing.Iterator[None]: original_modules = {name: module for name, module in sys.modules.items() if _is_httpx_module(name)} - for name in original_modules: - del sys.modules[name] + _remove_httpx_modules() yield + _remove_httpx_modules() + sys.modules.update(original_modules) + + +def _remove_httpx_modules() -> None: for name in [name for name in sys.modules if _is_httpx_module(name)]: del sys.modules[name] - sys.modules.update(original_modules) def _is_httpx_module(name: str) -> bool: @@ -33,6 +37,26 @@ def _httpx_distribution_installed() -> bool: return True +def test_httpx_distribution_installed_returns_false_when_missing(monkeypatch: pytest.MonkeyPatch) -> None: + def distribution(name: str) -> object: + assert name == "httpx" + raise metadata.PackageNotFoundError + + monkeypatch.setattr(metadata, "distribution", distribution) + + assert not _alias._httpx_distribution_installed() + + +def test_httpx_distribution_installed_returns_true_when_found(monkeypatch: pytest.MonkeyPatch) -> None: + def distribution(name: str) -> object: + assert name == "httpx" + return object() + + monkeypatch.setattr(metadata, "distribution", distribution) + + assert _alias._httpx_distribution_installed() + + def test_enable_httpx_alias(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(_alias, "_httpx_distribution_installed", _httpx_distribution_missing) From 2d0fe19bb3649afa43e4cb07d9e009fb88c73df4 Mon Sep 17 00:00:00 2001 From: jonathan343 Date: Tue, 19 May 2026 14:32:03 -0400 Subject: [PATCH 3/3] Add PR link to changelog entry --- src/httpx2/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/httpx2/CHANGELOG.md b/src/httpx2/CHANGELOG.md index 685061a1..5cec6e0b 100644 --- a/src/httpx2/CHANGELOG.md +++ b/src/httpx2/CHANGELOG.md @@ -8,7 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added -* Add `httpx2.enable_httpx_alias()` as an opt-in migration helper for applications that need `import httpx` to resolve to `httpx2`. +* Add `httpx2.enable_httpx_alias()` as an opt-in migration helper for applications that need `import httpx` to resolve to `httpx2`. ([#968](https://github.com/pydantic/httpx2/pull/968)) ## 2.2.0 (May 16th, 2026)