# Plan: modernize hxprobe's Python toolchain ## Context `hxprobe` (`hxprobe/`) is a fully standalone Python project (own `pyproject.toml`, own venv, own tests — see `docs/summaries/2026-07-02-09-32-hxprobe-standalone-project.md`). Its toolchain is currently bare-minimum: hand-rolled `python -m venv` + `pip install -e .`, no linter/formatter, no type checker, no dependency lockfile, and stdlib `unittest` with a filename-glob convention (`test_[!i]*.py`/`test_integration.py`) to separate hermetic from live tests. User confirmed direction (via AskUserQuestion): adopt **uv** for env/deps (with a lockfile), add **ruff** for lint+format, **skip mypy** for now, and **swap the test runner to pytest** (existing `unittest.TestCase` classes run unchanged under pytest — no test-code rewrite) using a proper `@pytest.mark.integration` marker instead of the filename-glob trick. Confirmed via inspection: `uv` (v0.11.19) is already installed globally (Homebrew); `ruff`/`mypy`/`pytest` only exist under an unrelated pyenv 3.12.3 shim, not on the 3.14 interpreter this project targets — `uv` sidesteps that mismatch by installing everything into the project's own venv. Git remote is a self-hosted Gitea instance, not GitHub, so no CI is being set up here. ## Changes **`hxprobe/pyproject.toml`:** - Add `[dependency-groups]` with `dev = ["pytest>=8.0", "ruff>=0.8"]` (PEP 735, the current uv-native way to declare dev-only deps — keeps the install-as-a-library `dependencies` list clean). - Add `[tool.pytest.ini_options]`: `testpaths = ["tests"]` and a registered `integration` marker (avoids `PytestUnknownMarkWarning`). - Add `[tool.ruff]`: `target-version = "py311"` (matches `requires-python`) and `line-length = 100` (close to the codebase's existing longest lines, ~103 chars, to minimize reformatting churn). - Leave `[build-system]`/`[tool.setuptools]` untouched — build backend wasn't part of the discussion, setuptools works fine here. **`hxprobe/.python-version`:** new file, `3.14`, so `uv sync`/`uv run` pin the same interpreter the rest of the repo uses without relying on `$PATH` order. **`hxprobe/uv.lock`:** generated by `uv lock` — first real lockfile pinning `httpx`, `httpcore`, `h2`, `certifi`, and friends. Committed (not gitignored; lockfiles belong in version control). **Test changes (runner swap only, no rewrite):** - `hxprobe/tests/test_integration.py`: add `pytestmark = pytest.mark.integration` at module level. This replaces the "named `test_integration.py` so the `test_[!i]*.py` glob skips it" convention — selection becomes `-m "not integration"` / `-m integration`, independent of filename. - `test_probe.py`/`test_cli.py`: no changes — they're already hermetic (local `http.server` fixtures only) and need no marker. - `unittest.TestCase` classes, `_NEEDS_NET = unittest.skipUnless(...)`, and the `if __name__ == "__main__": unittest.main()` guards all stay exactly as-is — pytest natively discovers and runs unittest-style tests and respects `unittest.skip*` decorators with zero changes required. **Lint fixes:** run `ruff check --fix` / `ruff format` over `hxprobe/` and review the diff. One known pre-existing issue it will flag: `import sys` in `cli.py` is unused (inherited from the original `latprobe/cli.py`) — remove it. Otherwise expect mostly whitespace/quote-style normalization. **`Makefile`:** replace the `hx-*` section to run through `uv` instead of a hand-managed venv: ```makefile HX_DIR := hxprobe # (drop HX_VENV / HX_VENV_PYTHON — uv owns this now) hx-deps: cd $(HX_DIR) && uv sync hx-run: (deps: hx-deps) cd $(HX_DIR) && uv run python -m hxprobe $(ARGS) hx-lint: (deps: hx-deps) cd $(HX_DIR) && uv run ruff check . hx-fmt: (deps: hx-deps) cd $(HX_DIR) && uv run ruff format . hx-test: (deps: hx-deps) cd $(HX_DIR) && uv run pytest tests -m "not integration" -v $(ARGS) hx-test-integration: (deps: hx-deps) cd $(HX_DIR) && uv run pytest tests -m integration -v $(ARGS) hx-check: hx-lint hx-test (test gate now includes lint, mirroring go-check's fmt+vet+test bundling) hx-clean: also removes .pytest_cache / .ruff_cache alongside __pycache__/egg-info ``` Umbrella `test`/`check`/`clean` targets keep delegating to `hx-test`/ `hx-check`/`hx-clean` unchanged. **`hxprobe/README.md`:** new, minimal — since this project is meant to be `cp -r`-able to its own repo, it should carry its own quick-start (`uv sync`, `uv run python -m hxprobe `, `uv run pytest`) rather than relying on the monorepo's root docs. **Docs:** update `docs/usage/hxprobe.md`'s "Setup" section (`uv sync` instead of manual venv+pip) and "Makefile targets" section (add `hx-lint`/`hx-fmt`, update test invocation description). New `docs/plans/-hxprobe-toolchain-modernization.md` and `docs/summaries/-hxprobe-toolchain-modernization.md` per CLAUDE.md convention. New `CHANGELOG.md` entry. **`.gitignore`:** already covers `.venv/`/`__pycache__/`/`*.egg-info/` unanchored; add `.pytest_cache/` and `.ruff_cache/` (new caches these tools create). ## Verification 1. `cd hxprobe && uv sync` — creates `.venv`, generates/uses `uv.lock`, installs `httpx[http2]` + dev deps (`pytest`, `ruff`). 2. `make hx-run ARGS="-v http://github.com"` — same output as before (HTTP/2, 1 redirect, IP/TLS/cert shown) — confirms the runtime behavior is untouched by the toolchain swap. 3. `make hx-lint` — clean (after fixing whatever `ruff check` surfaces, including the unused `import sys`). 4. `make hx-test` — all hermetic tests pass under pytest; confirm the `integration`-marked tests are excluded (test count matches the current 28 hermetic tests). 5. `make hx-test-integration` — the 9 live tests run and pass under the `integration` marker selection. 6. `make check` (top-level) — Go + latprobe + hxprobe (lint + test) all green in one gate. 7. `grep -rn "latprobe" hxprobe/` — still zero matches (toolchain change must not reintroduce coupling). 8. Confirm `python/latprobe/` is untouched (`git diff --stat python/` empty) — this is a hxprobe-only change.