Skip to content

refactor: clean up httpx2 integration and remove pytest-httpx2 (#202) - #203

Merged
Kilo59 merged 13 commits into
mainfrom
issue-202-migrate-httpx2
Jul 25, 2026
Merged

refactor: clean up httpx2 integration and remove pytest-httpx2 (#202)#203
Kilo59 merged 13 commits into
mainfrom
issue-202-migrate-httpx2

Conversation

@Kilo59

@Kilo59 Kilo59 commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #202.

This PR cleans up and standardizes our httpx2 integration by:

  • Upgrading to httpx2>=2.9.1 (which introduces httpx2.alias_httpx()) and respx>=0.23.1.
  • Completely removing the pytest-httpx2 plugin dependency.
  • Adding a test-guarded src/sitecustomize.py module to handle respx entrypoint aliasing during test execution.

Architectural Decisions & Rationale

1. Removal of pytest-httpx2

We deliberately eliminated the pytest-httpx2 package dependency. Beyond introducing transport hook conflicts and obscure entrypoint ordering issues, pytest-httpx2 caused significant developer confusion:

  • pytest-httpx2 is actually just a wrapper around respx, but its naming causes developers to repeatedly land on documentation for pytest-httpx / httpx_pytest.
  • This led to endless confusion around fixture names, routing syntax, and unexpected mocking behavior.

Eliminating pytest-httpx2 removes this confusing wrapper layer, standardizes our test suite on respx directly, and simplifies our dependency tree.

2. No Third-Party HTTP Plugin Dependencies in Production

ruff-sync runtime code (src/ruff_sync/core.py) imports httpx2 directly (import httpx2 as httpx) and does not rely on any third-party httpcore or httpx plugins at application runtime.

Rationale: If our production runtime relied on third-party libraries or plugins that hard-coded import httpx or import httpcore, we would require process-wide aliasing at application startup in production. 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)

src/sitecustomize.py lives outside src/ruff_sync/ and is strictly excluded from wheel packages by Hatchling (packages = ["src/ruff_sync"]).

  • Production Wheels: Published wheel packages (.whl) only include ruff_sync/* files. sitecustomize.py is never included or shipped to end users.
  • Execution Guard: Inside src/sitecustomize.py, an explicit runtime check (if any("pytest" in arg for arg in sys.argv) or "PYTEST_CURRENT_TEST" in os.environ:) guarantees that alias_httpx() only fires during Pytest executions.

Why Disabling respx Autoloading (-p no:respx) Is a Gnarly Issue (Upstream Problem)

We thoroughly evaluated attempting to disable respx entrypoint autoloading (addopts = ["-p", "no:respx"]) and manually declaring pytest_plugins = ["respx"] in conftest.py. However, this approach reveals a gnarly entrypoint race condition:

  1. Early Entrypoint Import: respx registers a pytest11 entrypoint that imports httpx/httpcore at plugin discovery time.
  2. Subprocess & CLI Transport Bypassing: When respx is disabled via -p no:respx and re-enabled via pytest_plugins, respx initializes 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.
  3. Upstream Solution Needed: Ideally, this issue 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.

Technical Tradeoffs & Alternatives Evaluated

Approach A (Selected): Test-Guarded src/sitecustomize.py

Python's built-in site module automatically executes sitecustomize.py at interpreter initialization time before Pytest loads entrypoints.

  • Pros: Guarantees sys.modules["httpcore"] points to httpcore2 when respx initializes during test runs, without touching production execution. Excluded from wheel builds via Hatchling (packages = ["src/ruff_sync"]).
  • Cons: Introduces a sitecustomize.py file in src/.

Approach B: Explicit Transport Mocking (respx.MockRouter(using="httpcore2"))

Explicitly pass using="httpcore2" to respx router fixtures.

  • Pros: No process-level aliasing required.
  • Cons: Fails to intercept network calls in CLI/e2e subprocess tests where httpx2 creates fresh AsyncClient instances without inherited router options.

Verification

  • uv run ruff check . --fix: Passed (0 errors)
  • uv run ruff format .: Passed (0 errors)
  • uv run mypy .: Passed (0 errors across 53 source files)
  • uv run pytest -vv: Passed (413 passed, 1 xfailed)

@sourcery-ai

sourcery-ai Bot commented Jul 25, 2026

Copy link
Copy Markdown

🧙 Sourcery has finished reviewing your pull request!


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@Kilo59 Kilo59 added the dependencies Pull requests that update a dependency file label Jul 25, 2026
@codecov

codecov Bot commented Jul 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.06%. Comparing base (2fdf0f2) to head (c773860).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #203   +/-   ##
=======================================
  Coverage   94.06%   94.06%           
=======================================
  Files          10       10           
  Lines        1602     1602           
=======================================
  Hits         1507     1507           
  Misses         95       95           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • The httpx2.alias_httpx() invocation is duplicated in __init__.py, __main__.py, and sitecustomize.py; consider centralizing this in a single helper to avoid divergence and make the aliasing behavior easier to reason about.
  • Given that sitecustomize already aliases httpx/httpcore at process startup, it may be worth reviewing whether the additional guarded alias calls in src/ruff_sync/__init__.py and src/ruff_sync/__main__.py are still needed, or if they can be removed to reduce side effects on import.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `httpx2.alias_httpx()` invocation is duplicated in `__init__.py`, `__main__.py`, and `sitecustomize.py`; consider centralizing this in a single helper to avoid divergence and make the aliasing behavior easier to reason about.
- Given that `sitecustomize` already aliases `httpx`/`httpcore` at process startup, it may be worth reviewing whether the additional guarded alias calls in `src/ruff_sync/__init__.py` and `src/ruff_sync/__main__.py` are still needed, or if they can be removed to reduce side effects on import.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@codspeed-hq

codspeed-hq Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 15 untouched benchmarks


Comparing issue-202-migrate-httpx2 (c773860) with main (2fdf0f2)

Open in CodSpeed

@Kilo59

Kilo59 commented Jul 25, 2026

Copy link
Copy Markdown
Owner Author

@sourcery-ai review

@Kilo59 Kilo59 added the dev-ex Developer Experiance related improvements label Jul 25, 2026
@Kilo59 Kilo59 self-assigned this Jul 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor
╒═══════════════╤═════════════════╤═════════════════╤══════════════╤════════════╕
│ File          │ Maintainabili   │ Lines of Code   │ Cyclomatic   │ Unique     │
│               │ ty Index        │                 │ Complexity   │ Operands   │
╞═══════════════╪═════════════════╪═════════════════╪══════════════╪════════════╡
│ tests/test_de │ 58.7012 ->      │ 216 -> 217      │ 10 -> 10     │ 5 -> 5     │
│ precation.py  │ 58.5343         │                 │              │            │
├───────────────┼─────────────────┼─────────────────┼──────────────┼────────────┤
│ tests/test_ch │ 27.1825 ->      │ 813 -> 824      │ 38 -> 38     │ 8 -> 8     │
│ eck.py        │ 27.0203         │                 │              │            │
├───────────────┼─────────────────┼─────────────────┼──────────────┼────────────┤
│ tests/conftes │ 62.3678 ->      │ 192 -> 152      │ 22 -> 14     │ 4 -> 3     │
│ t.py          │ 69.1874         │                 │              │            │
├───────────────┼─────────────────┼─────────────────┼──────────────┼────────────┤
│ tests/test_co │ 30.5839 ->      │ 1051 -> 1052    │ 40 -> 40     │ 8 -> 8     │
│ nfig_validati │ 30.5468         │                 │              │            │
│ on.py         │                 │                 │              │            │
├───────────────┼─────────────────┼─────────────────┼──────────────┼────────────┤
│ tests/test_ur │ 64.7737 ->      │ 254 -> 258      │ 7 -> 7       │ 3 -> 3     │
│ l_handling.py │ 64.2308         │                 │              │            │
╘═══════════════╧═════════════════╧═════════════════╧══════════════╧════════════╛

@Kilo59
Kilo59 merged commit b4bcf42 into main Jul 25, 2026
20 checks passed
@Kilo59
Kilo59 deleted the issue-202-migrate-httpx2 branch July 25, 2026 19:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file dev-ex Developer Experiance related improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate from httpx to httpx2

1 participant