Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/).
Expand Down
4 changes: 4 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@

::: httpx2.stream

## Compatibility Helpers

::: httpx2.enable_httpx_alias

## `Client`

::: httpx2.Client
Expand Down
18 changes: 18 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
6 changes: 6 additions & 0 deletions src/httpx2/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`. ([#968](https://github.com/pydantic/httpx2/pull/968))

## 2.2.0 (May 16th, 2026)

### Fixed
Expand Down
2 changes: 2 additions & 0 deletions src/httpx2/httpx2/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .__version__ import __description__, __title__, __version__
from ._alias import *
from ._api import *
from ._auth import *
from ._client import *
Expand Down Expand Up @@ -35,6 +36,7 @@
"DecodingError",
"delete",
"DigestAuth",
"enable_httpx_alias",
"FunctionAuth",
"get",
"head",
Expand Down
60 changes: 60 additions & 0 deletions src/httpx2/httpx2/_alias.py
Original file line number Diff line number Diff line change
@@ -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()
118 changes: 118 additions & 0 deletions tests/httpx2/test_alias.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import sys
import types
import typing
from importlib import metadata

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)}
_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]


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_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)

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()