|
1 | 1 | """Tests for the `httpx` → `httpx2` migration shim in `mcp.shared._httpx`. |
2 | 2 |
|
3 | 3 | `mcp` prefers `httpx2` and falls back to `httpx` with an `MCPDeprecationWarning` emitted at |
4 | | -the shim's import time. Today the lockfile pins `httpx` (not `httpx2`), so importing the shim |
5 | | -exercises the fallback. |
| 4 | +the shim's import time. The lockfile pins `httpx` (not `httpx2`), so the canonical state of |
| 5 | +the shim is the fallback path. |
6 | 6 | """ |
7 | 7 |
|
8 | 8 | from __future__ import annotations |
9 | 9 |
|
10 | 10 | import importlib |
11 | | -import sys |
12 | 11 | import warnings |
| 12 | +from unittest import mock |
13 | 13 |
|
14 | 14 | import pytest |
15 | 15 |
|
| 16 | +import mcp.shared._httpx |
16 | 17 | from mcp.shared.exceptions import MCPDeprecationWarning |
17 | 18 |
|
18 | 19 |
|
19 | | -def _force_reimport_shim() -> None: |
20 | | - """Drop the cached shim module so the next import re-runs its top-level code.""" |
21 | | - sys.modules.pop("mcp.shared._httpx", None) |
| 20 | +@pytest.fixture(autouse=True) |
| 21 | +def _restore_shim_state(): |
| 22 | + """Reload the shim after each test so a simulated `httpx2` doesn't leak into later tests.""" |
| 23 | + yield |
| 24 | + importlib.reload(mcp.shared._httpx) |
22 | 25 |
|
23 | 26 |
|
24 | | -def test_fallback_emits_warning_at_import(monkeypatch: pytest.MonkeyPatch) -> None: |
25 | | - """With only `httpx` installed, importing the shim emits `MCPDeprecationWarning`.""" |
26 | | - monkeypatch.delitem(sys.modules, "httpx2", raising=False) |
27 | | - _force_reimport_shim() |
| 27 | +def test_fallback_emits_warning() -> None: |
| 28 | + with mock.patch.dict("sys.modules", {"httpx2": None}): |
| 29 | + with pytest.warns(MCPDeprecationWarning, match=r"install `httpx2` instead"): |
| 30 | + importlib.reload(mcp.shared._httpx) |
28 | 31 |
|
29 | | - from collections.abc import Mapping, Sequence |
30 | 32 |
|
31 | | - real_import = __import__ |
| 33 | +def test_httpx2_present_is_silent() -> None: |
| 34 | + import httpx |
32 | 35 |
|
33 | | - def fake_import( |
34 | | - name: str, |
35 | | - globals: Mapping[str, object] | None = None, |
36 | | - locals: Mapping[str, object] | None = None, |
37 | | - fromlist: Sequence[str] = (), |
38 | | - level: int = 0, |
39 | | - ) -> object: |
40 | | - if name == "httpx2": |
41 | | - raise ImportError("simulated: httpx2 not installed") |
42 | | - return real_import(name, globals, locals, fromlist, level) |
43 | | - |
44 | | - monkeypatch.setattr("builtins.__import__", fake_import) |
45 | | - with pytest.warns(MCPDeprecationWarning, match=r"install `httpx2` instead"): |
46 | | - importlib.import_module("mcp.shared._httpx") |
47 | | - |
48 | | - |
49 | | -def test_httpx2_present_is_silent(monkeypatch: pytest.MonkeyPatch) -> None: |
50 | | - """When `httpx2` is importable, the shim selects it and emits no warning.""" |
51 | | - import httpx as real_httpx |
52 | | - |
53 | | - monkeypatch.setitem(sys.modules, "httpx2", real_httpx) |
54 | | - _force_reimport_shim() |
55 | | - |
56 | | - with warnings.catch_warnings(record=True) as caught: |
57 | | - warnings.simplefilter("always", MCPDeprecationWarning) |
58 | | - reloaded = importlib.import_module("mcp.shared._httpx") |
59 | | - |
60 | | - assert reloaded.httpx is real_httpx |
61 | | - assert [w for w in caught if issubclass(w.category, MCPDeprecationWarning)] == [] |
| 36 | + with mock.patch.dict("sys.modules", {"httpx2": httpx}): |
| 37 | + with warnings.catch_warnings(): |
| 38 | + warnings.simplefilter("error", MCPDeprecationWarning) |
| 39 | + importlib.reload(mcp.shared._httpx) |
| 40 | + assert mcp.shared._httpx.httpx is httpx |
0 commit comments