diff --git a/.agents/decisions/0004-httpx2-respx-test-aliasing.md b/.agents/decisions/0004-httpx2-respx-test-aliasing.md new file mode 100644 index 00000000..5bc0e60c --- /dev/null +++ b/.agents/decisions/0004-httpx2-respx-test-aliasing.md @@ -0,0 +1,51 @@ +# ADR 0004: httpx2 Integration, Respx Mocking, and Test-Guarded Process Aliasing + +--- +status: accepted +date: 2026-07-25 +decider: Kilo59 / Agent +--- + +## Context & Motivation for Switching to `httpx2` + +`ruff-sync` relies on an asynchronous HTTP client to pull upstream Ruff linter configurations from GitHub and GitLab repositories. + +We migrated from `httpx` to **`httpx2`** based on the official rationale provided by **Pydantic Services**: +- **Active Stewardship & Security Maintenance**: Due to a significant period of reduced maintenance activity and inactivity on the original `httpx` package, Pydantic Services created `httpx2` as an actively maintained successor to guarantee timely security updates, bug fixes, and continuous maintenance for a critical core library in the Python ecosystem. +- **Ecosystem Continuity**: The "2" in `httpx2` functions as a versioning marker for this new era of Pydantic Services maintenance rather than a breaking paradigm rewrite, preserving full compatibility while delivering active upstream support. +- **Strict Typing**: `httpx2` provides first-class typing out of the box, integrating cleanly with our `mypy` strict mode requirements. + +Previously, HTTP test mocking relied on the third-party `pytest-httpx2` plugin. We needed to clean up our HTTP dependencies, eliminate `pytest-httpx2`, and migrate to standard `respx` (`>=0.23.1`) while ensuring zero side-effects on production execution. + +## Decision + +1. **Direct Application Imports**: Application runtime code (`src/ruff_sync/core.py`) imports `httpx2` directly (`import httpx2 as httpx`). +2. **Elimination of `pytest-httpx2`**: We completely removed `pytest-httpx2` from dependencies due to confusion and transport hook coupling. +3. **Test-Guarded Process Aliasing (`src/sitecustomize.py`)**: We place `httpx2.alias_httpx()` inside `src/sitecustomize.py` guarded by a runtime check (`any("pytest" in arg for arg in sys.argv) or "PYTEST_CURRENT_TEST" in os.environ`). + +## Rationale & Technical Tradeoffs + +### 1. Removal of `pytest-httpx2` +Beyond introducing transport hook conflicts and entrypoint ordering bugs, `pytest-httpx2` caused developer confusion: +- `pytest-httpx2` is a wrapper around `respx`, but its name frequently leads developers to the documentation for `pytest-httpx` / `httpx_pytest`. +- This created confusion around fixture names, routing syntax, and unexpected mocking behavior. +Eliminating `pytest-httpx2` removes this confusing wrapper layer and standardizes our test suite directly on `respx`. + +### 2. No Production HTTP Plugin Dependencies +`ruff-sync` runtime code does **not** depend on third-party `httpcore` or `httpx` plugins in production. +If our production runtime relied on external plugins that hard-coded `import httpx` or `import httpcore`, we would require process-wide aliasing at production application startup. Because `ruff-sync` manages its HTTP transport directly via `httpx2`, aliasing is only necessary during test suite execution when `respx` is loaded by Pytest. + +### 3. Packaging Exclusion +`src/sitecustomize.py` lives outside `src/ruff_sync/` and is strictly excluded from wheel packages by Hatchling (`packages = ["src/ruff_sync"]`). Published `.whl` and `sdist` distributions never include or ship `sitecustomize.py` to end users. + +### 4. Why Disabling `respx` Autoloading (`-p no:respx`) Is a Gnarly Upstream Problem +We evaluated disabling `respx` entrypoint autoloading via `addopts = ["-p", "no:respx"]` and manually declaring `pytest_plugins = ["respx"]` in `conftest.py`. However, this reveals a gnarly entrypoint race condition: +- `respx` registers a `pytest11` entrypoint that imports `httpx`/`httpcore` at plugin discovery time. +- Disabling `respx` autoloading and re-enabling it via `pytest_plugins` causes `respx` to initialize its `MockRouter` hooks *after* Python module resolution has settled. In end-to-end CLI tests where `httpx2.AsyncClient` is instantiated across sub-threads or CLI entrypoints, `httpx2` creates fresh `httpcore2` transport instances that bypass `respx`'s delayed transport hooks, causing `RESPX: some routes were not called!` errors. +- **Upstream Resolution Needed**: This conflict needs to be resolved **upstream** — either in `respx` (by deferring transport binding until active router context rather than entrypoint import time) or in `httpx2` (by providing native `httpcore2` transport interceptors for `respx` without requiring `sys.modules` aliasing). Until fixed upstream, `sitecustomize.py` is the only mechanism that forces `alias_httpx()` to run at Python interpreter launch before Pytest's entrypoint scanner initializes `respx`. + +## References + +- [ADR Index](./README.md) +- [httpx2 Migration Guide](https://httpx2.pydantic.dev/migration/) +- [Issue #202](https://github.com/Kilo59/ruff-sync/issues/202) / [PR #203](https://github.com/Kilo59/ruff-sync/pull/203) diff --git a/.agents/decisions/README.md b/.agents/decisions/README.md index 10af0db7..5b2101d9 100644 --- a/.agents/decisions/README.md +++ b/.agents/decisions/README.md @@ -7,6 +7,7 @@ This is an internal record of architectural decisions for `ruff-sync`. These doc | [0001](./0001-type-refactoring-strategy.md) | 2026-04-05 | Accepted | Type Refactoring Strategy | | [0002](./0002-tui-node-ast.md) | 2026-04-05 | Accepted | TUI Node AST Architecture | | [0003](./0003-argument-resolution-layers.md) | 2026-04-10 | Accepted | Two-Layer Argument Resolution (Transport vs Execution) | +| [0004](./0004-httpx2-respx-test-aliasing.md) | 2026-07-25 | Accepted | httpx2 Integration, Respx Mocking, and Test-Guarded Process Aliasing | --- *For instructions on how to create or manage ADRs, see the [ADR Skill](../skills/adr/SKILL.md).* diff --git a/AGENTS.md b/AGENTS.md index e9818feb..4bc01ebd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -191,6 +191,7 @@ uv run coverage run -m pytest -vv - Whitespace/formatting preservation tests go in `tests/test_whitespace.py`. - End-to-end lifecycle tests use fixture triples in `tests/lifecycle_tomls/` (`*_initial.toml`, `*_upstream.toml`, `*_final.toml`). Use the `invoke new-case` task to scaffold them. - Tests should include **both** structural/whitespace assertions and **semantic** assertions (verifying the merged config values are correct). +- **HTTP Mocking & `sitecustomize.py`**: `ruff-sync` uses `httpx2` for HTTP fetching and `respx` for test mocking. `src/sitecustomize.py` contains a test execution guard (`if any("pytest" in arg for arg in sys.argv) or "PYTEST_CURRENT_TEST" in os.environ:`) that calls `httpx2.alias_httpx()` at interpreter launch during pytest runs. `src/sitecustomize.py` lives outside `src/ruff_sync/` and is strictly excluded from wheel packages by Hatchling (`packages = ["src/ruff_sync"]`), so end users never receive it. ## Invoke Tasks diff --git a/README.md b/README.md index f2a89311..08ee9cb7 100644 --- a/README.md +++ b/README.md @@ -453,6 +453,24 @@ flowchart TD style SemanticNode fill:#4b5563,color:#fff,stroke:#374151 ``` +## Development + +For local development and testing: + +```console +# Run test suite +uv run pytest -vv + +# Run linter and formatter +uv run ruff check . --fix +uv run ruff format . + +# Run type checker +uv run mypy . +``` + +> **Note on Test Environment**: Test mocking uses `respx` with `httpx2`. To ensure `respx` intercepts `httpx2` network calls during `pytest` runs before plugin entrypoints load, a guarded `src/sitecustomize.py` script is used in development. This script is strictly excluded from production wheel builds (`packages = ["src/ruff_sync"]`) and is never shipped to end users. + ## Dogfooding To see `ruff-sync` in action, this project automatically "dogfoods" its own configuration. Every pull request runs a `ruff-sync check` against the repository's own `pyproject.toml` using the `--output-format github` flag, providing real-time feedback and inline annotations whenever configuration drift is detected. diff --git a/pyproject.toml b/pyproject.toml index b0aafdea..e0ca02a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ "Topic :: Software Development :: Quality Assurance", ] dependencies = [ - "httpx2>=2.3.0", + "httpx2>=2.9.1", "tomlkit>=0.12.3,<2.0.0", "typing-extensions>=4.5.0", ] @@ -57,7 +57,7 @@ dev = [ "pytest-codspeed>=4.3.0", "pytest-icdiff>=0.9", "pytest-textual-snapshot>=1.0.0", - "pytest-httpx2", + "respx>=0.23.1", "ruamel-yaml>=0.18.6", "ruff>=0.15.0", "textual>=8.2.2", @@ -259,9 +259,8 @@ required-imports = ["from __future__ import annotations"] include = ["src/ruff_sync/*.py"] [tool.pytest] -addopts = ["-p pytest_httpx2"] +pythonpath = ["src", "."] log_level = "INFO" markers = [ "benchmark: marks performance benchmarks (run separately for CodSpeed)", - "httpx2: configure the httpx2_mock fixture", ] diff --git a/src/sitecustomize.py b/src/sitecustomize.py new file mode 100644 index 00000000..1f139936 --- /dev/null +++ b/src/sitecustomize.py @@ -0,0 +1,20 @@ +"""Process startup script to alias httpx to httpx2 during pytest test runs. + +PACKAGING INTENT: +This file must NEVER be included in built wheels (.whl) or release distributions. +It lives outside `src/ruff_sync/` and is excluded from packaging by Hatchling +(`packages = ["src/ruff_sync"]`). If the build backend or target configuration is +ever changed in `pyproject.toml`, ensure this file remains strictly excluded. +""" + +from __future__ import annotations + +import os +import sys + +# Only run alias_httpx() if pytest is running +if any("pytest" in arg for arg in sys.argv) or "PYTEST_CURRENT_TEST" in os.environ: + import httpx2 as httpx + + if "httpx" not in sys.modules: + httpx.alias_httpx() diff --git a/tests/conftest.py b/tests/conftest.py index 99c370df..51a3e4c2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,6 @@ from __future__ import annotations +import contextlib import logging import os import sys @@ -8,20 +9,19 @@ if TYPE_CHECKING: from collections.abc import Generator -import contextlib import pytest -import respx import truststore -from respx.router import DEFAULT as RESPX_DEFAULT -from respx.router import MockRouter from typing_extensions import override +import ruff_sync + with contextlib.suppress(Exception): truststore.SSLContext() -import ruff_sync +pytest_plugins = ["respx"] + LOGGER = logging.getLogger(__name__) @@ -150,43 +150,3 @@ def _run( return exit_code, captured.out, captured.err return _run - - -class HTTPX2MockRouter(MockRouter): - @override - def __call__( - self, - func=None, - *, - assert_all_called=None, - assert_all_mocked=None, - base_url=None, - using=RESPX_DEFAULT, - ): - """Override __call__ to set using='httpcore2' by default for nested mocks.""" - if using is RESPX_DEFAULT: - using = "httpcore2" - return super().__call__( - func=func, - assert_all_called=assert_all_called, - assert_all_mocked=assert_all_mocked, - base_url=base_url, - using=using, - ) - - -@pytest.fixture -def httpx2_mock(request: pytest.FixtureRequest) -> Generator[respx.Router, None, None]: - options = {} - if (marker := request.node.get_closest_marker("httpx2")) is not None: - options.update(marker.kwargs) - options.setdefault("using", "httpcore2") - router = HTTPX2MockRouter(**options) - with router: - yield router - - -@pytest.fixture -def respx_mock(httpx2_mock): - """Fixture that maps respx_mock to httpx2_mock for httpx2 compatibility.""" - return httpx2_mock diff --git a/tests/test_basic.py b/tests/test_basic.py index f55d408f..36f5d47d 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -281,13 +281,13 @@ def test_merge_ruff_toml(source: str, toml_s: str, sep_str: str): @pytest.fixture def mock_http(toml_s: str, respx_mock: respx.MockRouter) -> Generator[respx.MockRouter, None, None]: - with respx_mock(base_url="https://example.com/") as mock: - mock.get("/pyproject.toml").respond( + with respx_mock(base_url="https://example.com/") as http_mock: + http_mock.get("/pyproject.toml").respond( 200, content_type="text/plain", content=toml_s, ) - yield mock + yield http_mock @pytest.fixture @@ -610,8 +610,8 @@ async def test_sync_default_exclude(fs: FakeFilesystem, respx_mock: respx.MockRo ff = fs.create_file("pyproject.toml", contents=source_toml) ff_path = pathlib.Path(ff.path) # type: ignore[arg-type] - with respx_mock(base_url="https://example.com/") as mock: - mock.get("/pyproject.toml").respond( + with respx_mock(base_url="https://example.com/") as http_mock: + http_mock.get("/pyproject.toml").respond( 200, content_type="text/plain", content=upstream_toml, @@ -669,7 +669,7 @@ def test_ruff_config_file_name_equality() -> None: @pytest.mark.asyncio -async def test_merge_multiple_upstreams_preserves_order(respx_mock: respx.Router): +async def test_merge_multiple_upstreams_preserves_order(respx_mock: respx.MockRouter): """Verify that multiple upstreams are merged in the given order.""" # Setup mock data target_doc = document() @@ -701,7 +701,7 @@ async def test_merge_multiple_upstreams_preserves_order(respx_mock: respx.Router @pytest.mark.asyncio -async def test_fetch_upstreams_concurrently_verified(respx_mock: respx.Router): +async def test_fetch_upstreams_concurrently_verified(respx_mock: respx.MockRouter): """Verify that fetch_upstreams_concurrently actually runs tasks in parallel.""" events = [] @@ -754,7 +754,7 @@ async def side_effect_two(request: original_httpx.Request) -> original_httpx.Res @pytest.mark.asyncio -async def test_merge_multiple_upstreams_handles_errors(respx_mock: respx.Router): +async def test_merge_multiple_upstreams_handles_errors(respx_mock: respx.MockRouter): target_doc = document() args = ruff_sync.Arguments( @@ -783,7 +783,7 @@ async def test_merge_multiple_upstreams_handles_errors(respx_mock: respx.Router) def test_cli_surfaces_upstream_error_with_exit_code_and_logs( monkeypatch: pytest.MonkeyPatch, - respx_mock: respx.Router, + respx_mock: respx.MockRouter, capsys: pytest.CaptureFixture[str], configure_logging: logging.Logger, ) -> None: @@ -828,7 +828,7 @@ def test_cli_surfaces_upstream_error_with_exit_code_and_logs( def test_cli_output_format_github( monkeypatch: pytest.MonkeyPatch, - respx_mock: respx.Router, + respx_mock: respx.MockRouter, capsys: pytest.CaptureFixture[str], configure_logging: logging.Logger, ) -> None: @@ -870,7 +870,7 @@ def test_cli_output_format_github( def test_cli_output_format_json( monkeypatch: pytest.MonkeyPatch, - respx_mock: respx.Router, + respx_mock: respx.MockRouter, capsys: pytest.CaptureFixture[str], configure_logging: logging.Logger, ) -> None: @@ -930,7 +930,7 @@ def test_cli_output_format_json( def test_cli_output_format_json_success( monkeypatch: pytest.MonkeyPatch, - respx_mock: respx.Router, + respx_mock: respx.MockRouter, capsys: pytest.CaptureFixture[str], configure_logging: logging.Logger, fs: FakeFilesystem, diff --git a/tests/test_check.py b/tests/test_check.py index aea4fbed..bab9e983 100644 --- a/tests/test_check.py +++ b/tests/test_check.py @@ -9,11 +9,12 @@ import ruff_sync if TYPE_CHECKING: + import respx from pyfakefs.fake_filesystem import FakeFilesystem @pytest.mark.asyncio -async def test_check_in_sync(fs: FakeFilesystem, httpx2_mock): +async def test_check_in_sync(fs: FakeFilesystem, respx_mock: respx.MockRouter): # Setup pyproject_content = """ [tool.ruff] @@ -25,8 +26,8 @@ async def test_check_in_sync(fs: FakeFilesystem, httpx2_mock): upstream_url = URL("https://example.com/pyproject.toml") - with httpx2_mock(base_url="https://example.com") as respx_mock: - respx_mock.get("/pyproject.toml").respond( + with respx_mock(base_url="https://example.com") as http_mock: + http_mock.get("/pyproject.toml").respond( 200, content_type="text/plain", content=pyproject_content, @@ -47,7 +48,9 @@ async def test_check_in_sync(fs: FakeFilesystem, httpx2_mock): @pytest.mark.asyncio -async def test_check_out_of_sync(fs: FakeFilesystem, capsys, configure_logging, httpx2_mock): +async def test_check_out_of_sync( + fs: FakeFilesystem, capsys, configure_logging, respx_mock: respx.MockRouter +): # Setup local_content = """ [tool.ruff] @@ -62,8 +65,8 @@ async def test_check_out_of_sync(fs: FakeFilesystem, capsys, configure_logging, upstream_url = URL("https://example.com/pyproject.toml") - with httpx2_mock(base_url="https://example.com") as respx_mock: - respx_mock.get("/pyproject.toml").respond( + with respx_mock(base_url="https://example.com") as http_mock: + http_mock.get("/pyproject.toml").respond( 200, content_type="text/plain", content=upstream_content, @@ -89,7 +92,9 @@ async def test_check_out_of_sync(fs: FakeFilesystem, capsys, configure_logging, @pytest.mark.asyncio -async def test_check_pre_commit_out_of_sync(fs: FakeFilesystem, caplog, httpx2_mock): +async def test_check_pre_commit_out_of_sync( + fs: FakeFilesystem, caplog, respx_mock: respx.MockRouter +): # Setup local_content = """ [tool.ruff] @@ -110,8 +115,8 @@ async def test_check_pre_commit_out_of_sync(fs: FakeFilesystem, caplog, httpx2_m upstream_url = URL("https://example.com/pyproject.toml") - with httpx2_mock(base_url="https://example.com") as respx_mock: - respx_mock.get("/pyproject.toml").respond( + with respx_mock(base_url="https://example.com") as http_mock: + http_mock.get("/pyproject.toml").respond( 200, content_type="text/plain", content=local_content, @@ -136,7 +141,7 @@ async def test_check_pre_commit_out_of_sync(fs: FakeFilesystem, caplog, httpx2_m @pytest.mark.asyncio -async def test_check_semantic_sync(fs: FakeFilesystem, httpx2_mock): +async def test_check_semantic_sync(fs: FakeFilesystem, respx_mock: respx.MockRouter): # A local comment does NOT make you out of sync — ruff-sync only adds/updates # keys, it never strips local-only additions like comments. local_content = """ @@ -155,8 +160,8 @@ async def test_check_semantic_sync(fs: FakeFilesystem, httpx2_mock): upstream_url = URL("https://example.com/pyproject.toml") - with httpx2_mock(base_url="https://example.com") as respx_mock: - respx_mock.get("/pyproject.toml").respond( + with respx_mock(base_url="https://example.com") as http_mock: + http_mock.get("/pyproject.toml").respond( 200, content_type="text/plain", content=upstream_content, @@ -188,7 +193,7 @@ async def test_check_semantic_sync(fs: FakeFilesystem, httpx2_mock): @pytest.mark.asyncio -async def test_check_semantic_out_of_sync(fs: FakeFilesystem, httpx2_mock): +async def test_check_semantic_out_of_sync(fs: FakeFilesystem, respx_mock: respx.MockRouter): # Setup - actual values differ local_content = """ [tool.ruff] @@ -203,8 +208,8 @@ async def test_check_semantic_out_of_sync(fs: FakeFilesystem, httpx2_mock): upstream_url = URL("https://example.com/pyproject.toml") - with httpx2_mock(base_url="https://example.com") as respx_mock: - respx_mock.get("/pyproject.toml").respond( + with respx_mock(base_url="https://example.com") as http_mock: + http_mock.get("/pyproject.toml").respond( 200, content_type="text/plain", content=upstream_content, @@ -224,7 +229,7 @@ async def test_check_semantic_out_of_sync(fs: FakeFilesystem, httpx2_mock): @pytest.mark.asyncio async def test_check_semantic_diff_output( - fs: FakeFilesystem, capsys, configure_logging, httpx2_mock + fs: FakeFilesystem, capsys, configure_logging, respx_mock: respx.MockRouter ): # Setup - actual values differ local_content = """ @@ -240,8 +245,8 @@ async def test_check_semantic_diff_output( upstream_url = URL("https://example.com/pyproject.toml") - with httpx2_mock(base_url="https://example.com") as respx_mock: - respx_mock.get("/pyproject.toml").respond( + with respx_mock(base_url="https://example.com") as http_mock: + http_mock.get("/pyproject.toml").respond( 200, content_type="text/plain", content=upstream_content, @@ -268,7 +273,9 @@ async def test_check_semantic_diff_output( @pytest.mark.asyncio -async def test_check_multi_upstream(fs: FakeFilesystem, capsys, configure_logging, httpx2_mock): +async def test_check_multi_upstream( + fs: FakeFilesystem, capsys, configure_logging, respx_mock: respx.MockRouter +): """Check supports multiple upstreams and bases status on the fully merged result.""" # Setup local_content = """ @@ -298,9 +305,9 @@ async def test_check_multi_upstream(fs: FakeFilesystem, capsys, configure_loggin u1_url = URL("https://example.com/u1/pyproject.toml") u2_url = URL("https://example.com/u2/pyproject.toml") - with httpx2_mock(base_url="https://example.com") as respx_mock: - respx_mock.get("/u1/pyproject.toml").respond(200, content=upstream1_content) - respx_mock.get("/u2/pyproject.toml").respond(200, content=upstream2_content) + with respx_mock(base_url="https://example.com") as http_mock: + http_mock.get("/u1/pyproject.toml").respond(200, content=upstream1_content) + http_mock.get("/u2/pyproject.toml").respond(200, content=upstream2_content) args = ruff_sync.Arguments( command="check", @@ -328,7 +335,7 @@ async def test_check_multi_upstream(fs: FakeFilesystem, capsys, configure_loggin @pytest.mark.asyncio async def test_check_both_out_of_sync_prioritizes_config_drift( - fs: FakeFilesystem, capsys, configure_logging, httpx2_mock + fs: FakeFilesystem, capsys, configure_logging, respx_mock: respx.MockRouter ): """Verify that Exit 1 is returned when both ruff config AND pre-commit are out of sync.""" # Setup - ruff config drift @@ -347,8 +354,8 @@ async def test_check_both_out_of_sync_prioritizes_config_drift( upstream_url = URL("https://example.com/pyproject.toml") - with httpx2_mock(base_url="https://example.com") as respx_mock: - respx_mock.get("/pyproject.toml").respond(200, content=upstream_content) + with respx_mock(base_url="https://example.com") as http_mock: + http_mock.get("/pyproject.toml").respond(200, content=upstream_content) args = ruff_sync.Arguments( command="check", @@ -375,7 +382,7 @@ async def test_check_both_out_of_sync_prioritizes_config_drift( @pytest.mark.asyncio async def test_check_out_of_sync_json_format( - fs: FakeFilesystem, capsys, configure_logging, httpx2_mock + fs: FakeFilesystem, capsys, configure_logging, respx_mock: respx.MockRouter ): # Setup mirrors the default-format test but uses JSON output_format local_content = """ @@ -391,8 +398,8 @@ async def test_check_out_of_sync_json_format( upstream_url = URL("https://example.com/pyproject.toml") - with httpx2_mock(base_url="https://example.com") as respx_mock: - respx_mock.get("/pyproject.toml").respond( + with respx_mock(base_url="https://example.com") as http_mock: + http_mock.get("/pyproject.toml").respond( 200, content_type="text/plain", content=upstream_content, @@ -440,7 +447,7 @@ async def test_check_out_of_sync_json_format( @pytest.mark.asyncio async def test_check_out_of_sync_github_format( - fs: FakeFilesystem, capsys, configure_logging, httpx2_mock + fs: FakeFilesystem, capsys, configure_logging, respx_mock: respx.MockRouter ): # Setup mirrors the default-format test but uses GITHUB output_format local_content = """ @@ -456,8 +463,8 @@ async def test_check_out_of_sync_github_format( upstream_url = URL("https://example.com/pyproject.toml") - with httpx2_mock(base_url="https://example.com") as respx_mock: - respx_mock.get("/pyproject.toml").respond( + with respx_mock(base_url="https://example.com") as http_mock: + http_mock.get("/pyproject.toml").respond( 200, content_type="text/plain", content=upstream_content, @@ -498,7 +505,7 @@ async def test_check_in_sync_json_format( fs: FakeFilesystem, capsys, configure_logging, - httpx2_mock, + respx_mock: respx.MockRouter, ): """Ensure JSON formatter reports success and no errors when configs are in sync.""" local_content = """ @@ -510,8 +517,8 @@ async def test_check_in_sync_json_format( source_path = pathlib.Path("pyproject.toml") upstream_url = URL("https://example.com/pyproject.toml") - with httpx2_mock(base_url="https://example.com") as respx_mock: - respx_mock.get("/pyproject.toml").respond( + with respx_mock(base_url="https://example.com") as http_mock: + http_mock.get("/pyproject.toml").respond( 200, content_type="text/plain", content=local_content, @@ -555,7 +562,7 @@ async def test_check_in_sync_github_format( fs: FakeFilesystem, capsys, configure_logging, - httpx2_mock, + respx_mock: respx.MockRouter, ): """Ensure GitHub formatter does not emit ::error lines when configs are in sync.""" local_content = """ @@ -567,8 +574,8 @@ async def test_check_in_sync_github_format( source_path = pathlib.Path("pyproject.toml") upstream_url = URL("https://example.com/pyproject.toml") - with httpx2_mock(base_url="https://example.com") as respx_mock: - respx_mock.get("/pyproject.toml").respond( + with respx_mock(base_url="https://example.com") as http_mock: + http_mock.get("/pyproject.toml").respond( 200, content_type="text/plain", content=local_content, @@ -600,7 +607,9 @@ async def test_check_in_sync_github_format( @pytest.mark.asyncio -async def test_check_upstream_error_returns_4(fs: FakeFilesystem, capsys, httpx2_mock): +async def test_check_upstream_error_returns_4( + fs: FakeFilesystem, capsys, respx_mock: respx.MockRouter +): """Verify that an unreachable upstream URL causes check() to raise UpstreamError. The CLI catches UpstreamError and returns exit code 4. Here we test check() @@ -614,8 +623,8 @@ async def test_check_upstream_error_returns_4(fs: FakeFilesystem, capsys, httpx2 upstream_url = URL("https://example.com/pyproject.toml") - with httpx2_mock(base_url="https://example.com") as respx_mock: - respx_mock.get("/pyproject.toml").respond(404) + with respx_mock(base_url="https://example.com") as http_mock: + http_mock.get("/pyproject.toml").respond(404) args = ruff_sync.Arguments( command="check", @@ -632,7 +641,9 @@ async def test_check_upstream_error_returns_4(fs: FakeFilesystem, capsys, httpx2 @pytest.mark.asyncio -async def test_check_sarif_format(fs: FakeFilesystem, capsys, configure_logging, httpx2_mock): +async def test_check_sarif_format( + fs: FakeFilesystem, capsys, configure_logging, respx_mock: respx.MockRouter +): """Verify --output-format sarif produces a valid SARIF v2.1.0 document.""" import json @@ -643,8 +654,8 @@ async def test_check_sarif_format(fs: FakeFilesystem, capsys, configure_logging, upstream_url = URL("https://example.com/pyproject.toml") - with httpx2_mock(base_url="https://example.com") as respx_mock: - respx_mock.get("/pyproject.toml").respond( + with respx_mock(base_url="https://example.com") as http_mock: + http_mock.get("/pyproject.toml").respond( 200, content_type="text/plain", content=upstream_content, @@ -714,7 +725,7 @@ async def test_check_sarif_format(fs: FakeFilesystem, capsys, configure_logging, @pytest.mark.asyncio async def test_check_sarif_format_in_sync( - fs: FakeFilesystem, capsys, configure_logging, httpx2_mock + fs: FakeFilesystem, capsys, configure_logging, respx_mock: respx.MockRouter ): """Verify SARIF formatter emits zero results when configs are in sync.""" import json @@ -725,8 +736,8 @@ async def test_check_sarif_format_in_sync( upstream_url = URL("https://example.com/pyproject.toml") - with httpx2_mock(base_url="https://example.com") as respx_mock: - respx_mock.get("/pyproject.toml").respond( + with respx_mock(base_url="https://example.com") as http_mock: + http_mock.get("/pyproject.toml").respond( 200, content_type="text/plain", content=local_content, @@ -756,7 +767,7 @@ async def test_check_sarif_format_in_sync( @pytest.mark.asyncio async def test_check_sarif_multiple_drifts( - fs: FakeFilesystem, capsys, configure_logging, httpx2_mock + fs: FakeFilesystem, capsys, configure_logging, respx_mock: respx.MockRouter ): """Verify SARIF output includes multiple results for multiple drifted keys.""" import json @@ -768,8 +779,8 @@ async def test_check_sarif_multiple_drifts( upstream_url = URL("https://example.com/pyproject.toml") - with httpx2_mock(base_url="https://example.com") as respx_mock: - respx_mock.get("/pyproject.toml").respond( + with respx_mock(base_url="https://example.com") as http_mock: + http_mock.get("/pyproject.toml").respond( 200, content_type="text/plain", content=upstream_content, diff --git a/tests/test_config_validation.py b/tests/test_config_validation.py index bb1f46ac..0e07a16b 100644 --- a/tests/test_config_validation.py +++ b/tests/test_config_validation.py @@ -28,6 +28,7 @@ ) if TYPE_CHECKING: + import respx from pyfakefs.fake_filesystem import FakeFilesystem from tests.conftest import CLIRunner @@ -518,7 +519,7 @@ def fake_run(cmd: list[str], **kwargs: object) -> subprocess.CompletedProcess[st async def test_pull_aborts_on_invalid_config_when_validate_is_true( fs: FakeFilesystem, monkeypatch: pytest.MonkeyPatch, - httpx2_mock, + respx_mock: respx.MockRouter, ) -> None: """When --validate is passed and ruff rejects the config, pull() returns 1. @@ -537,8 +538,8 @@ def fake_run(cmd: list[str], **kwargs: object) -> subprocess.CompletedProcess[st monkeypatch.setattr("ruff_sync.validation.subprocess.run", fake_run) - with httpx2_mock(base_url="https://example.com") as respx_mock: - respx_mock.get("/pyproject.toml").respond( + with respx_mock(base_url="https://example.com") as http_mock: + http_mock.get("/pyproject.toml").respond( 200, content_type="text/plain", content=_INVALID_UPSTREAM ) @@ -563,7 +564,7 @@ def fake_run(cmd: list[str], **kwargs: object) -> subprocess.CompletedProcess[st async def test_pull_skips_validation_by_default( fs: FakeFilesystem, monkeypatch: pytest.MonkeyPatch, - httpx2_mock, + respx_mock: respx.MockRouter, ) -> None: """Regression guard: without --validate, pull() succeeds even with a bad upstream key. @@ -581,8 +582,8 @@ def fake_run(cmd: list[str], **kwargs: object) -> subprocess.CompletedProcess[st fs.create_file("pyproject.toml", contents=_LOCAL_PYPROJECT) source_path = pathlib.Path("pyproject.toml") - with httpx2_mock(base_url="https://example.com") as respx_mock: - respx_mock.get("/pyproject.toml").respond( + with respx_mock(base_url="https://example.com") as http_mock: + http_mock.get("/pyproject.toml").respond( 200, content_type="text/plain", content=_VALID_UPSTREAM ) @@ -606,7 +607,7 @@ def fake_run(cmd: list[str], **kwargs: object) -> subprocess.CompletedProcess[st async def test_pull_succeeds_when_validate_passes( fs: FakeFilesystem, monkeypatch: pytest.MonkeyPatch, - httpx2_mock, + respx_mock: respx.MockRouter, ) -> None: """When --validate is passed and ruff accepts the config, pull() succeeds (exit 0).""" fs.create_file("pyproject.toml", contents=_LOCAL_PYPROJECT) @@ -618,8 +619,8 @@ def fake_run(cmd: list[str], **kwargs: object) -> subprocess.CompletedProcess[st monkeypatch.setattr("ruff_sync.validation.subprocess.run", fake_run) - with httpx2_mock(base_url="https://example.com") as respx_mock: - respx_mock.get("/pyproject.toml").respond( + with respx_mock(base_url="https://example.com") as http_mock: + http_mock.get("/pyproject.toml").respond( 200, content_type="text/plain", content=_VALID_UPSTREAM ) @@ -915,7 +916,7 @@ def test_pull_uses_persisted_validation_settings( ruff_exit_code: int, expected_exit_code: int, expected_msg: str | None, - httpx2_mock, + respx_mock: respx.MockRouter, ) -> None: """Pull must honor validation settings defined in [tool.ruff-sync].""" fs.create_file( @@ -933,8 +934,8 @@ def fake_run(cmd: list[str], **kwargs: object) -> subprocess.CompletedProcess[st monkeypatch.setattr("ruff_sync.validation.subprocess.run", fake_run) - with httpx2_mock(base_url="https://example.com") as respx_mock: - respx_mock.get("/pyproject.toml").respond( + with respx_mock(base_url="https://example.com") as http_mock: + http_mock.get("/pyproject.toml").respond( 200, content_type="text/plain", content="[tool.ruff]\n" ) @@ -971,7 +972,7 @@ def test_pull_save_persists_validation_flags( cli_flags: list[str], expected_in_toml: list[str], expected_not_in_toml: list[str], - httpx2_mock, + respx_mock: respx.MockRouter, ) -> None: """Pull --save with validation flags must persist them to [tool.ruff-sync].""" fs.create_file( @@ -986,8 +987,8 @@ def fake_run(cmd: list[str], **kwargs: object) -> subprocess.CompletedProcess[st monkeypatch.setattr("ruff_sync.validation.subprocess.run", fake_run) - with httpx2_mock(base_url="https://example.com") as respx_mock: - respx_mock.get("/pyproject.toml").respond( + with respx_mock(base_url="https://example.com") as http_mock: + http_mock.get("/pyproject.toml").respond( 200, content_type="text/plain", content="[tool.ruff]\n" ) @@ -1007,7 +1008,7 @@ def test_pull_save_clears_validation_flags( cli_run: CLIRunner, clean_config_cache: None, monkeypatch: pytest.MonkeyPatch, - httpx2_mock, + respx_mock: respx.MockRouter, ) -> None: """Pull --no-strict --save must persist 'false' to [tool.ruff-sync].""" fs.create_file( @@ -1025,8 +1026,8 @@ def test_pull_save_clears_validation_flags( "ruff_sync.validation.validate_ruff_accepts_config", lambda *args, **kwargs: True ) - with httpx2_mock(base_url="https://example.com") as respx_mock: - respx_mock.get("/pyproject.toml").respond( + with respx_mock(base_url="https://example.com") as http_mock: + http_mock.get("/pyproject.toml").respond( 200, content_type="text/plain", content="[tool.ruff]\n" ) diff --git a/tests/test_deprecation.py b/tests/test_deprecation.py index d8238621..d7a62c98 100644 --- a/tests/test_deprecation.py +++ b/tests/test_deprecation.py @@ -12,6 +12,7 @@ import ruff_sync.cli if TYPE_CHECKING: + import respx from pyfakefs.fake_filesystem import FakeFilesystem @@ -20,7 +21,7 @@ def test_source_cli_deprecation( caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch, clear_ruff_sync_caches, - httpx2_mock, + respx_mock: respx.MockRouter, ): """Test that --source CLI argument emits a deprecation warning and works as --to.""" # Ensure we are in a clean directory @@ -32,8 +33,8 @@ def test_source_cli_deprecation( source_path = test_dir / "pyproject.toml" upstream_url = URL("https://example.com/pyproject.toml") - with httpx2_mock(base_url="https://example.com") as respx_mock: - respx_mock.get("/pyproject.toml").respond( + with respx_mock(base_url="https://example.com") as http_mock: + http_mock.get("/pyproject.toml").respond( 200, text="[tool.ruff]\ntarget-version = 'py310'\n" ) @@ -51,7 +52,7 @@ def test_source_config_deprecation( caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch, clear_ruff_sync_caches, - httpx2_mock, + respx_mock: respx.MockRouter, ): """Test that source in [tool.ruff-sync] emits a deprecation warning and that `to` takes precedence. @@ -112,8 +113,8 @@ def test_source_config_deprecation( ) # Mock the upstream request - with httpx2_mock(base_url="https://example.com") as respx_mock: - respx_mock.get("/pyproject.toml").respond(200, text="[tool.ruff]\n") + with respx_mock(base_url="https://example.com") as http_mock: + http_mock.get("/pyproject.toml").respond(200, text="[tool.ruff]\n") # No --to or --source on CLI, but use --init to allow creating the sub-project file monkeypatch.setattr(sys, "argv", ["ruff-sync", "pull", "--init"]) @@ -141,8 +142,8 @@ def test_source_config_deprecation( ruff_sync.get_config.cache_clear() monkeypatch.setattr(sys, "argv", ["ruff-sync", "pull"]) - with httpx2_mock(base_url="https://example.com") as respx_mock: - respx_mock.get("/pyproject.toml").respond(200, text="[tool.ruff]\n") + with respx_mock(base_url="https://example.com") as http_mock: + http_mock.get("/pyproject.toml").respond(200, text="[tool.ruff]\n") exit_code = ruff_sync.main() assert exit_code == 0 diff --git a/tests/test_e2e.py b/tests/test_e2e.py index 5764e287..ce0512ee 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -33,7 +33,7 @@ TEST_ROOT: Final = pathlib.Path(__file__).parent -def test_manpage(httpx2_mock): +def test_manpage(respx_mock: respx.MockRouter): """Test that the manpage can be generated with ruff-sync --help with no errors.""" completed = subprocess.run( [sys.executable, "-m", "ruff_sync", "--help"], @@ -66,7 +66,7 @@ class _PrepEnv(NamedTuple): def prep_env( fs: FakeFilesystem, request: FixtureRequest, - httpx2_mock, + respx_mock: respx.MockRouter, ) -> Generator[_PrepEnv, None, None]: group_name: str = request.param fs.add_real_directory(LIFECYCLE_TOML_DIR) @@ -80,8 +80,8 @@ def prep_env( base_url = "https://example.com" upstream_url = URL(f"{base_url}/pyproject.toml") - with httpx2_mock(base_url=base_url, assert_all_called=False) as respx_mock: - respx_mock.get(upstream_url.path).respond( + with respx_mock(base_url=base_url, assert_all_called=False) as http_mock: + http_mock.get(upstream_url.path).respond( 200, content_type="text/plain", content=LIFECYCLE_TOML_DIR.joinpath(f"{group_name}_upstream.toml").read_text(), @@ -113,7 +113,7 @@ async def test_ruff_sync(prep_env): @pytest.mark.asyncio -async def test_ruff_check(prep_env, httpx2_mock): +async def test_ruff_check(prep_env: _PrepEnv, respx_mock: respx.MockRouter): # 1. Initially it should be out of sync exit_code = await ruff_sync.check( ruff_sync.Arguments( @@ -178,7 +178,7 @@ async def test_ruff_check(prep_env, httpx2_mock): @pytest.fixture def readme_excludes_env( fs: FakeFilesystem, - httpx2_mock, + respx_mock: respx.MockRouter, ) -> Generator[_PrepEnv, None, None]: group_name = "readme_excludes" fs.add_real_directory(LIFECYCLE_TOML_DIR) @@ -192,8 +192,8 @@ def readme_excludes_env( base_url = "https://example.com" upstream_url = URL(f"{base_url}/pyproject.toml") - with httpx2_mock(base_url=base_url, assert_all_called=False) as respx_mock: - respx_mock.get(upstream_url.path).respond( + with respx_mock(base_url=base_url, assert_all_called=False) as http_mock: + http_mock.get(upstream_url.path).respond( 200, content_type="text/plain", content=LIFECYCLE_TOML_DIR.joinpath(f"{group_name}_upstream.toml").read_text(), @@ -232,7 +232,7 @@ async def test_readme_exclude_examples(readme_excludes_env): @pytest.mark.asyncio -async def test_ruff_sync_multi_upstream(fs: FakeFilesystem, httpx2_mock): +async def test_ruff_sync_multi_upstream(fs: FakeFilesystem, respx_mock: respx.MockRouter): """Test merging of multiple upstreams sequentially.""" fs.add_real_directory(LIFECYCLE_TOML_DIR) @@ -246,8 +246,8 @@ async def test_ruff_sync_multi_upstream(fs: FakeFilesystem, httpx2_mock): u2_url = URL("https://example.com/up2.toml") expected_toml = LIFECYCLE_TOML_DIR.joinpath("multi_upstream_final.toml").read_text() - with httpx2_mock(base_url="https://example.com") as respx_mock: - respx_mock.get("/up1.toml").respond( + with respx_mock(base_url="https://example.com") as http_mock: + http_mock.get("/up1.toml").respond( 200, content_type="text/plain", content=LIFECYCLE_TOML_DIR.joinpath("multi_upstream_up1.toml").read_text(), diff --git a/tests/test_scaffold.py b/tests/test_scaffold.py index 96e1b389..1b864b66 100644 --- a/tests/test_scaffold.py +++ b/tests/test_scaffold.py @@ -16,19 +16,19 @@ @pytest.fixture -def mock_http(toml_s: str, httpx2_mock) -> Generator[respx.MockRouter, None, None]: - with httpx2_mock(base_url="https://example.com/", assert_all_called=False) as respx_mock: - respx_mock.get("/pyproject.toml").respond( +def mock_http(toml_s: str, respx_mock: respx.MockRouter) -> Generator[respx.MockRouter, None, None]: + with respx_mock(base_url="https://example.com/", assert_all_called=False) as http_mock: + http_mock.get("/pyproject.toml").respond( 200, content_type="text/plain", content=toml_s, ) - respx_mock.get("/ruff.toml").respond( + http_mock.get("/ruff.toml").respond( 200, content_type="text/plain", content='target-version = "py310"\nline-length = 99\n', ) - yield respx_mock + yield http_mock @pytest.fixture diff --git a/tests/test_url_handling.py b/tests/test_url_handling.py index 3457a6cb..cb0074ac 100644 --- a/tests/test_url_handling.py +++ b/tests/test_url_handling.py @@ -1,11 +1,16 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import httpx2 as httpx import pytest from httpx2 import URL, AsyncClient from ruff_sync import fetch_upstream_config, is_ruff_toml_file, resolve_raw_url, to_git_url +if TYPE_CHECKING: + import respx + @pytest.mark.parametrize( "path_or_url,expected", @@ -207,9 +212,8 @@ def test_to_git_url(input_url: str, expected_git_url: str | None): assert str(result) == expected_git_url -@pytest.mark.httpx2(assert_all_called=False) @pytest.mark.asyncio -async def test_fetch_upstream_config_with_ruff_toml_fallback(httpx2_mock): +async def test_fetch_upstream_config_with_ruff_toml_fallback(respx_mock: respx.MockRouter): # Given a directory guess result that would normally point to pyproject.toml # If pyproject.toml does not exist but ruff.toml does, it should find ruff.toml base_url = "https://raw.githubusercontent.com/org/repo/main/configs" @@ -217,9 +221,9 @@ async def test_fetch_upstream_config_with_ruff_toml_fallback(httpx2_mock): ruff_url = f"{base_url}/ruff.toml" # Mock: ruff.toml exists, others don't - httpx2_mock.get(ruff_url).respond(200, text="line-length = 100") - httpx2_mock.get(f"{base_url}/.ruff.toml").respond(404) - httpx2_mock.get(pyproject_url).respond(404) + respx_mock.get(ruff_url).respond(200, text="line-length = 100") + respx_mock.get(f"{base_url}/.ruff.toml").respond(404) + respx_mock.get(pyproject_url).respond(404) async with AsyncClient() as client: # The URL passed to fetch_upstream_config is usually the one resolved by resolve_raw_url diff --git a/uv.lock b/uv.lock index 8932a543..14fd4f5f 100644 --- a/uv.lock +++ b/uv.lock @@ -534,15 +534,15 @@ wheels = [ [[package]] name = "httpcore2" -version = "2.3.0" +version = "2.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "h11" }, { name = "truststore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e6/34/18f1c596e677962f040284246f393b10a1f8ce440b3a7e69c637d0f1c7ad/httpcore2-2.3.0.tar.gz", hash = "sha256:07327e251560960eea8e969d92d4c6a325feb13cca39e25340731336c3baf924", size = 64300, upload-time = "2026-06-01T13:15:02.998Z" } +sdist = { url = "https://files.pythonhosted.org/packages/39/a8/20ed1ed79cbc2ecdf5301c0968ab7c85547212e2a7bd126ddd2d986e206e/httpcore2-2.9.1.tar.gz", hash = "sha256:4d8acbf8b306f48c9d6046591fd5ba4037d1b1b1000d140fc2c3eab1e9a0c0e2", size = 67089, upload-time = "2026-07-24T09:21:03.867Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/dd/3357218c69360d1cecc196c230c9a1d5c9afd5dba362056e23e60a5e64e5/httpcore2-2.3.0-py3-none-any.whl", hash = "sha256:477e9e334f74e5240dcac002e890580f36a57d40ff0fb14cc9655731d23b8415", size = 80024, upload-time = "2026-06-01T13:15:00.001Z" }, + { url = "https://files.pythonhosted.org/packages/9f/fb/46c52b781975c335a2bcf1072c7bbc007cbdc8d674217f5ee1daba2c848b/httpcore2-2.9.1-py3-none-any.whl", hash = "sha256:6182472379e855fe4221246a2bb7ecede403bc61c6798062ae1787d051ccde26", size = 82809, upload-time = "2026-07-24T09:21:01.178Z" }, ] [[package]] @@ -562,17 +562,18 @@ wheels = [ [[package]] name = "httpx2" -version = "2.3.0" +version = "2.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "httpcore2" }, { name = "idna" }, { name = "truststore" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/9a/cca0b9145f13d8ae34b885ae28d403a1469a433abc78e0f94f4ce94e650b/httpx2-2.3.0.tar.gz", hash = "sha256:227e7c41d95a76d4077a52640564132777215fc3394e07b66a3116c33d668fa9", size = 81115, upload-time = "2026-06-01T13:15:04.324Z" } +sdist = { url = "https://files.pythonhosted.org/packages/21/14/38128fbafd7e0ed41d874df6c9a653d47c2d111cfe59e2b4ac95161b4abd/httpx2-2.9.1.tar.gz", hash = "sha256:1932a768737e3666291582833da748cc4e563c337cf96706fccc04fa6e58764a", size = 95458, upload-time = "2026-07-24T09:21:04.972Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/ce/ae2911859847f9ba1d6b23027e53481cbeb50b93234f355a968d300ca2cb/httpx2-2.3.0-py3-none-any.whl", hash = "sha256:6f393663bdf6dbe7fe90118e3eb5b2bd024a675cae0390ac08cec9198812d8b7", size = 74538, upload-time = "2026-06-01T13:15:01.566Z" }, + { url = "https://files.pythonhosted.org/packages/13/b8/cfd91c4ab9134d386d48f0b6ac662ff3d4be6efdee59ee1c67ebc3c0487c/httpx2-2.9.1-py3-none-any.whl", hash = "sha256:1820fe14a9ab1107bfeff39259987429450b070ec0ff38cc87eb0d8c97fdc71a", size = 91191, upload-time = "2026-07-24T09:21:02.6Z" }, ] [[package]] @@ -595,11 +596,11 @@ wheels = [ [[package]] name = "idna" -version = "3.15" +version = "3.18" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] [[package]] @@ -1398,18 +1399,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/55/d9/b8a53c20cf5b41042c205bb9d36d37da00418d30fd1a94bf9eb147820720/pytest_codspeed-4.3.0-py3-none-any.whl", hash = "sha256:05baff2a61dc9f3e92b92b9c2ab5fb45d9b802438f5373073f5766a91319ed7a", size = 125224, upload-time = "2026-02-09T15:23:33.774Z" }, ] -[[package]] -name = "pytest-httpx2" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "respx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/d1/70e7edb50679ddd34ba5e13ac1d6e1223bc4e24d0a5fc30587f8fbe884c4/pytest_httpx2-1.0.0.tar.gz", hash = "sha256:d897a14c1341d3f3014e9432c16fed366598aba06a2ba9c08460a51799cb7128", size = 2882, upload-time = "2026-05-20T08:26:41.442Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/e1/5e38e8de42fba9667846a43113469c7481d01db4b6511e42645385bace8f/pytest_httpx2-1.0.0-py3-none-any.whl", hash = "sha256:467dc7f6946854ffc20e1678703266b093037d8f3fb9f1fb523a2c432c64cff0", size = 4504, upload-time = "2026-05-20T08:26:40.219Z" }, -] - [[package]] name = "pytest-icdiff" version = "0.9" @@ -1792,9 +1781,9 @@ dev = [ { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-codspeed" }, - { name = "pytest-httpx2" }, { name = "pytest-icdiff" }, { name = "pytest-textual-snapshot" }, + { name = "respx" }, { name = "ruamel-yaml" }, { name = "ruff" }, { name = "textual" }, @@ -1811,7 +1800,7 @@ docs = [ [package.metadata] requires-dist = [ - { name = "httpx2", specifier = ">=2.3.0" }, + { name = "httpx2", specifier = ">=2.9.1" }, { name = "textual", marker = "extra == 'tui'", specifier = ">=8.2.2" }, { name = "tomlkit", specifier = ">=0.12.3,<2.0.0" }, { name = "typing-extensions", specifier = ">=4.5.0" }, @@ -1830,9 +1819,9 @@ dev = [ { name = "pytest", specifier = ">=8.0.0" }, { name = "pytest-asyncio", specifier = ">=0.23.5" }, { name = "pytest-codspeed", specifier = ">=4.3.0" }, - { name = "pytest-httpx2" }, { name = "pytest-icdiff", specifier = ">=0.9" }, { name = "pytest-textual-snapshot", specifier = ">=1.0.0" }, + { name = "respx", specifier = ">=0.23.1" }, { name = "ruamel-yaml", specifier = ">=0.18.6" }, { name = "ruff", specifier = ">=0.15.0" }, { name = "textual", specifier = ">=8.2.2" },