Skip to content

Commit 0aa82b1

Browse files
committed
fix(doctor): resolve F-01 build venv preflight
Closes #4
1 parent 1def2a3 commit 0aa82b1

4 files changed

Lines changed: 164 additions & 3 deletions

File tree

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,10 @@ Check the local environment:
196196
uvx mcp-server-python-docs doctor
197197
```
198198

199+
This checks the runtime Python version, SQLite FTS5, cache/index paths, disk
200+
space, and whether the current interpreter has the `venv`/`ensurepip` support
201+
needed by `build-index`.
202+
199203
Validate an existing index:
200204

201205
```bash
@@ -222,6 +226,19 @@ Install Python from [python.org](https://www.python.org/) or use:
222226
uv python install
223227
```
224228

229+
### Missing `pythonX.Y-venv` on Debian/Ubuntu
230+
231+
If `doctor` reports that build venv support is unavailable, install the venv
232+
package for the same Python minor version that runs the server:
233+
234+
```bash
235+
sudo apt install python3.12-venv
236+
```
237+
238+
Adjust `3.12` to match the version shown by `doctor`. Without this package,
239+
`build-index` cannot create the disposable Sphinx environment it uses to build
240+
JSON documentation content.
241+
225242
### `uvx` cache stale
226243

227244
If `uvx mcp-server-python-docs` runs an old version:

src/mcp_server_python_docs/__main__.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,7 @@ def doctor() -> None:
434434
import sqlite3
435435
from pathlib import Path
436436

437+
from mcp_server_python_docs.diagnostics import check_build_venv_support
437438
from mcp_server_python_docs.storage.db import get_cache_dir, get_index_path
438439

439440
results: list[tuple[str, bool, str]] = [] # (probe_name, passed, detail)
@@ -470,7 +471,15 @@ def doctor() -> None:
470471
)
471472
results.append(("SQLite FTS5", fts5_ok, fts5_detail))
472473

473-
# 3. Cache directory
474+
# 3. Build-index Sphinx venv support
475+
build_venv_result = check_build_venv_support()
476+
results.append((
477+
"Build venv support",
478+
build_venv_result.passed,
479+
build_venv_result.detail,
480+
))
481+
482+
# 4. Cache directory
474483
cache_dir = get_cache_dir()
475484
cache_exists = cache_dir.exists()
476485
cache_writable = False
@@ -491,7 +500,7 @@ def doctor() -> None:
491500
cache_ok = False
492501
results.append(("Cache directory", cache_ok, cache_detail))
493502

494-
# 4. Index database presence
503+
# 5. Index database presence
495504
index_path = get_index_path()
496505
index_exists = index_path.exists()
497506
index_detail = str(index_path)
@@ -504,7 +513,7 @@ def doctor() -> None:
504513
index_detail += f" ({size_mb:.1f} MB)"
505514
results.append(("Index database", index_exists, index_detail))
506515

507-
# 5. Free disk space
516+
# 6. Free disk space
508517
check_path = cache_dir if cache_exists else cache_dir.parent
509518
# Ensure path exists for disk_usage
510519
if not check_path.exists():
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Environment diagnostics shared by CLI health checks."""
2+
from __future__ import annotations
3+
4+
import subprocess
5+
import sys
6+
from dataclasses import dataclass
7+
8+
9+
@dataclass(frozen=True)
10+
class DiagnosticResult:
11+
"""Result for a single environment diagnostic probe."""
12+
13+
passed: bool
14+
detail: str
15+
16+
17+
def _combined_output_excerpt(stdout: str, stderr: str, limit: int = 500) -> str:
18+
combined = "\n".join(part.strip() for part in (stderr, stdout) if part.strip())
19+
if len(combined) <= limit:
20+
return combined
21+
return combined[-limit:]
22+
23+
24+
def check_build_venv_support(
25+
python_executable: str | None = None,
26+
timeout: float = 10.0,
27+
) -> DiagnosticResult:
28+
"""Check that build-index can create pip-enabled Sphinx environments.
29+
30+
``build-index`` creates a disposable Sphinx virtual environment with pip.
31+
Debian/Ubuntu hosts without the matching ``pythonX.Y-venv`` package can run
32+
the server but fail when that environment needs ``ensurepip``.
33+
"""
34+
executable = python_executable or sys.executable
35+
package_name = f"python{sys.version_info.major}.{sys.version_info.minor}-venv"
36+
command = [executable, "-c", "import ensurepip; import venv"]
37+
38+
try:
39+
result = subprocess.run(
40+
command,
41+
capture_output=True,
42+
text=True,
43+
timeout=timeout,
44+
)
45+
except FileNotFoundError:
46+
return DiagnosticResult(
47+
passed=False,
48+
detail=f"{executable} not found; build-index cannot create Sphinx venvs",
49+
)
50+
except subprocess.TimeoutExpired:
51+
return DiagnosticResult(
52+
passed=False,
53+
detail=(
54+
f"{executable} timed out while checking venv/ensurepip support "
55+
"for build-index"
56+
),
57+
)
58+
59+
if result.returncode == 0:
60+
return DiagnosticResult(
61+
passed=True,
62+
detail=f"{executable} has venv and ensurepip available for build-index",
63+
)
64+
65+
detail = (
66+
f"{executable} cannot import venv/ensurepip; build-index needs them to "
67+
"create Sphinx venvs. On Debian/Ubuntu, install "
68+
f"{package_name} for this interpreter."
69+
)
70+
output = _combined_output_excerpt(result.stdout, result.stderr)
71+
if output:
72+
detail = f"{detail} Output: {output}"
73+
74+
return DiagnosticResult(passed=False, detail=detail)

tests/test_doctor.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,16 @@ def test_doctor_checks_disk_space(self):
8080
)
8181
assert "PASS: Disk space" in result.stderr
8282

83+
def test_doctor_checks_build_venv_support(self):
84+
"""Doctor reports whether build-index can create Sphinx venvs."""
85+
result = subprocess.run(
86+
[sys.executable, "-m", "mcp_server_python_docs", "doctor"],
87+
capture_output=True,
88+
text=True,
89+
timeout=15,
90+
)
91+
assert "Build venv support" in result.stderr
92+
8393
def test_doctor_reports_missing_index(self):
8494
"""Doctor reports FAIL for Index database when pointed at empty dir."""
8595
with tempfile.TemporaryDirectory() as tmpdir:
@@ -115,3 +125,54 @@ def test_doctor_in_help(self):
115125
)
116126
combined = result.stdout + result.stderr
117127
assert "doctor" in combined
128+
129+
130+
class TestBuildVenvSupportProbe:
131+
"""Verify the build-index venv prerequisite probe."""
132+
133+
def test_probe_reports_missing_ensurepip_with_platform_package_hint(self, monkeypatch):
134+
"""Missing ensurepip points users to the versioned Debian/Ubuntu venv package."""
135+
136+
def fake_run(*args, **kwargs):
137+
return subprocess.CompletedProcess(
138+
args=args[0],
139+
returncode=1,
140+
stdout="",
141+
stderr="ModuleNotFoundError: No module named 'ensurepip'",
142+
)
143+
144+
monkeypatch.setattr(subprocess, "run", fake_run)
145+
146+
from mcp_server_python_docs.diagnostics import check_build_venv_support
147+
148+
result = check_build_venv_support()
149+
150+
assert result.passed is False
151+
assert "ensurepip" in result.detail
152+
assert (
153+
f"python{sys.version_info.major}.{sys.version_info.minor}-venv"
154+
in result.detail
155+
)
156+
157+
def test_probe_passes_when_venv_and_ensurepip_are_importable(self, monkeypatch):
158+
"""Available venv and ensurepip support passes the build prerequisite probe."""
159+
160+
def fake_run(*args, **kwargs):
161+
command = args[0]
162+
assert "ensurepip" in command[-1]
163+
assert "venv" in command[-1]
164+
return subprocess.CompletedProcess(
165+
args=command,
166+
returncode=0,
167+
stdout="",
168+
stderr="",
169+
)
170+
171+
monkeypatch.setattr(subprocess, "run", fake_run)
172+
173+
from mcp_server_python_docs.diagnostics import check_build_venv_support
174+
175+
result = check_build_venv_support()
176+
177+
assert result.passed is True
178+
assert "build-index" in result.detail

0 commit comments

Comments
 (0)