mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
## Description Base installs without the `proxy` extra crash during CLI command registration because `headroom.proxy.request_scope` imported FastAPI at module import time. That import is only needed for typing on `normalize_request_path`. This change keeps the FastAPI `Request` import under `TYPE_CHECKING` so the CLI path used by `headroom --help` no longer requires FastAPI. Closes #2561 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Documentation update - [ ] Refactoring (no functional changes) - [ ] Performance improvement - [ ] Test update - [ ] Build/CI change - [ ] Other (please describe): ## Changes Made - Make the FastAPI `Request` import type-checking only in `headroom/proxy/request_scope.py` - Add a subprocess regression test that imports `request_scope` and `project_context` with FastAPI blocked and verifies `normalize_scope_path` ## Testing ### Test commands run ```bash PYTHONPATH=. python3 -m pytest tests/test_proxy_request_scope.py tests/test_request_scope_no_fastapi.py -q ruff format --check headroom/proxy/request_scope.py tests/test_request_scope_no_fastapi.py ruff check headroom/proxy/request_scope.py tests/test_request_scope_no_fastapi.py ``` ### Test Output ```text ========================= 5 passed, 1 warning in 0.93s ========================= 2 files already formatted All checks passed! ``` ## Real Behavior Proof ### Environment - Linux x86_64, Python 3.11.15 - Shallow sparse checkout of headroom main at commit parent of this PR - System/Hermes venv Python with pytest and ruff available ### Exact command ```bash PYTHONPATH=. python3 - <<'PY' import builtins, sys real = builtins.__import__ def imp(name, *a, **k): if name == "fastapi" or name.startswith("fastapi."): raise ModuleNotFoundError("No module named 'fastapi'") return real(name, *a, **k) builtins.__import__ = imp import headroom.proxy.request_scope as rs import headroom.proxy.project_context as pc rs.normalize_scope_path({"path": "/a"}, "/b") print("ok", "fastapi" not in sys.modules, hasattr(pc, "with_project_prefix")) PY ``` ### Observed result ```text ok True True ``` Importing the request-scope helpers no longer requires FastAPI, and scope path normalization still works. ### Not tested - Full base `pip install headroom-ai` (no extras) end-to-end on a clean venv without the monorepo source tree - Full monorepo `make ci-precheck` / cargo workspace - Live proxy traffic or FastAPI request path behavior beyond the existing unit test for `normalize_request_path` ## Review Readiness - [x] I have tested these changes locally - [x] I have added/updated tests where applicable - [x] I have updated documentation if needed (N/A) - [x] My code follows the project's style guidelines - [x] I have run linting/formatting checks - [x] I have considered security implications - [x] This PR is ready for review
42 lines
1.2 KiB
Python
42 lines
1.2 KiB
Python
"""Regression tests for importing request-scope helpers without FastAPI."""
|
|
|
|
import subprocess
|
|
import sys
|
|
import textwrap
|
|
|
|
|
|
def test_request_scope_and_project_context_import_without_fastapi() -> None:
|
|
script = textwrap.dedent(
|
|
"""
|
|
import builtins
|
|
import sys
|
|
|
|
real_import = builtins.__import__
|
|
|
|
def import_without_fastapi(name, *args, **kwargs):
|
|
if name == "fastapi" or name.startswith("fastapi."):
|
|
raise ModuleNotFoundError("No module named 'fastapi'")
|
|
return real_import(name, *args, **kwargs)
|
|
|
|
builtins.__import__ = import_without_fastapi
|
|
|
|
import headroom.proxy.request_scope as request_scope
|
|
import headroom.proxy.project_context
|
|
|
|
assert not any(
|
|
name == "fastapi" or name.startswith("fastapi.") for name in sys.modules
|
|
)
|
|
scope = {"path": "/a", "raw_path": b"/a"}
|
|
request_scope.normalize_scope_path(scope, "/b c")
|
|
assert scope == {"path": "/b c", "raw_path": b"/b%20c"}
|
|
"""
|
|
)
|
|
|
|
result = subprocess.run(
|
|
[sys.executable, "-c", script],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
)
|
|
|
|
assert result.returncode == 0, result.stderr
|