mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
fix(proxy): lazy-import server to avoid fastapi crash (#442)
## Summary - Lazy-import `create_app`/`run_server` in `headroom/proxy/__init__.py` via PEP 562 `__getattr__` to prevent CLI crash when `fastapi` is not installed (i.e., installed without `[proxy]` extras) - Fix `.pre-commit-config.yaml` to use `python3` instead of `python` (unavailable on macOS Homebrew) - Add graceful `ImportError` skip in `scripts/sync-plugin-versions.py` for environments without dev dependencies Fixes #441 ## Test plan - [x] `headroom --help` works without `[proxy]` extras installed - [x] `headroom proxy --help` works with `[proxy]` extras installed - [x] `headroom proxy --port 18787` starts and serves traffic - [x] Lazy imports resolve correctly: `from headroom.proxy import create_app, run_server` - [x] `AttributeError` raised for invalid attributes on `headroom.proxy` - [x] Pre-commit hooks pass (ruff, ruff-format, mypy, sync-plugin-versions) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Code Bot <claude-code@smartwatermelon.github> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
74392b238e
commit
93c69372e6
5 changed files with 102 additions and 11 deletions
|
|
@ -3,7 +3,7 @@ repos:
|
|||
hooks:
|
||||
- id: sync-plugin-versions
|
||||
name: Sync plugin versions
|
||||
entry: python scripts/sync-plugin-versions.py
|
||||
entry: python3 scripts/sync-plugin-versions.py
|
||||
language: system
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
|
|
|
|||
|
|
@ -14,6 +14,14 @@ Usage:
|
|||
Set base URL in Cursor settings to http://localhost:8787
|
||||
"""
|
||||
|
||||
from .server import create_app, run_server
|
||||
|
||||
__all__ = ["create_app", "run_server"]
|
||||
|
||||
|
||||
def __getattr__(name: str) -> object:
|
||||
if name in ("create_app", "run_server"):
|
||||
from .server import create_app, run_server # noqa: F811
|
||||
|
||||
globals()["create_app"] = create_app
|
||||
globals()["run_server"] = run_server
|
||||
return globals()[name]
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
|
|
|||
|
|
@ -25,14 +25,18 @@ ROOT = Path(__file__).resolve().parent.parent
|
|||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from headroom.release_version import ( # noqa: E402
|
||||
compute_release_version,
|
||||
determine_bump_level,
|
||||
find_latest_release_tag,
|
||||
get_canonical_version,
|
||||
list_release_commits,
|
||||
list_release_tags,
|
||||
)
|
||||
try:
|
||||
from headroom.release_version import ( # noqa: E402
|
||||
compute_release_version,
|
||||
determine_bump_level,
|
||||
find_latest_release_tag,
|
||||
get_canonical_version,
|
||||
list_release_commits,
|
||||
list_release_tags,
|
||||
)
|
||||
except ImportError:
|
||||
print("skip: headroom deps not installed (run from a dev venv to enable)")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def compute_repo_semver(root: Path) -> str:
|
||||
|
|
|
|||
|
|
@ -74,6 +74,31 @@ def test_version_prefers_source_tree_release_history() -> None:
|
|||
package_version.assert_not_called()
|
||||
|
||||
|
||||
def test_proxy_package_import_does_not_eagerly_load_server() -> None:
|
||||
script = textwrap.dedent(
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
|
||||
import headroom.proxy
|
||||
|
||||
print(json.dumps({
|
||||
"server_loaded": "headroom.proxy.server" in sys.modules,
|
||||
}))
|
||||
"""
|
||||
)
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-c", script],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
data = json.loads(result.stdout.strip())
|
||||
assert data["server_loaded"] is False
|
||||
|
||||
|
||||
def test_proxy_server_import_skips_litellm_backend() -> None:
|
||||
script = textwrap.dedent(
|
||||
"""
|
||||
|
|
|
|||
54
tests/test_proxy_package_init.py
Normal file
54
tests/test_proxy_package_init.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
"""Unit tests for headroom.proxy lazy __getattr__ (PEP 562)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
import types
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def test_proxy_getattr_resolves_create_app_and_caches_it(monkeypatch) -> None:
|
||||
sentinel = object()
|
||||
fake_server = types.SimpleNamespace(create_app=sentinel, run_server=object())
|
||||
monkeypatch.setitem(sys.modules, "headroom.proxy.server", fake_server)
|
||||
|
||||
import headroom.proxy as proxy
|
||||
|
||||
module = importlib.reload(proxy)
|
||||
module.__dict__.pop("create_app", None)
|
||||
module.__dict__.pop("run_server", None)
|
||||
# Register teardown: monkeypatch notes these keys are absent now and will
|
||||
# remove them after the test, preventing sentinel leakage to later tests.
|
||||
monkeypatch.delitem(module.__dict__, "create_app", raising=False)
|
||||
monkeypatch.delitem(module.__dict__, "run_server", raising=False)
|
||||
|
||||
result = module.__getattr__("create_app")
|
||||
assert result is sentinel
|
||||
assert module.__dict__["create_app"] is sentinel
|
||||
|
||||
|
||||
def test_proxy_getattr_resolves_run_server(monkeypatch) -> None:
|
||||
sentinel = object()
|
||||
fake_server = types.SimpleNamespace(create_app=object(), run_server=sentinel)
|
||||
monkeypatch.setitem(sys.modules, "headroom.proxy.server", fake_server)
|
||||
|
||||
import headroom.proxy as proxy
|
||||
|
||||
module = importlib.reload(proxy)
|
||||
module.__dict__.pop("create_app", None)
|
||||
module.__dict__.pop("run_server", None)
|
||||
monkeypatch.delitem(module.__dict__, "create_app", raising=False)
|
||||
monkeypatch.delitem(module.__dict__, "run_server", raising=False)
|
||||
|
||||
result = module.__getattr__("run_server")
|
||||
assert result is sentinel
|
||||
assert module.__dict__["run_server"] is sentinel
|
||||
|
||||
|
||||
def test_proxy_getattr_raises_for_unknown_attribute() -> None:
|
||||
import headroom.proxy as proxy
|
||||
|
||||
with pytest.raises(AttributeError, match="has no attribute 'nonexistent'"):
|
||||
proxy.__getattr__("nonexistent")
|
||||
Loading…
Add table
Add a link
Reference in a new issue