Skip to content

Commit cd77498

Browse files
committed
Address PR review cleanup
1 parent ee9e3c1 commit cd77498

5 files changed

Lines changed: 69 additions & 12 deletions

File tree

src/mcp_server_python_docs/__main__.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -232,15 +232,20 @@ def build_index(versions: str, skip_content: bool) -> None:
232232
pip_path = os.path.join(scripts_dir, "pip")
233233

234234
# Install Sphinx with the version pin for this CPython branch.
235-
# Older Sphinx releases still import pkg_resources, which
236-
# modern venvs do not always seed by default.
235+
bootstrap_requirements = build_sphinx_bootstrap_requirements(
236+
config["sphinx_pin"]
237+
)
238+
if len(bootstrap_requirements) > 1:
239+
logger.info(
240+
"Installing Sphinx bootstrap packages for Python %s: %s",
241+
version,
242+
", ".join(bootstrap_requirements[:-1]),
243+
)
237244
subprocess.run(
238245
[
239246
pip_path,
240247
"install",
241-
*build_sphinx_bootstrap_requirements(
242-
config["sphinx_pin"]
243-
),
248+
*bootstrap_requirements,
244249
],
245250
check=True,
246251
capture_output=True,

src/mcp_server_python_docs/ingestion/publish.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from collections.abc import Iterable
1515
from datetime import datetime
1616
from pathlib import Path
17+
from typing import Final
1718

1819
from mcp_server_python_docs.storage.db import (
1920
get_cache_dir,
@@ -23,7 +24,7 @@
2324

2425
logger = logging.getLogger(__name__)
2526

26-
SMOKE_SENTINEL_SYMBOL = "asyncio.run"
27+
SMOKE_SENTINEL_SYMBOL: Final[str] = "asyncio.run"
2728

2829

2930
def _version_sort_key(version: str) -> tuple[int, ...]:

src/mcp_server_python_docs/ingestion/sphinx_json.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,14 @@ def _mcp_json_default(self, obj):
6363
jsonimpl.SphinxJSONEncoder.default = _mcp_json_default
6464
'''
6565

66-
_IMGHDR_COMPAT_MODULE = '''"""Compatibility shim for old Sphinx on Python 3.13+."""
66+
_IMGHDR_COMPAT_MODULE = '''"""Compatibility shim for Sphinx builds that import imghdr."""
6767
6868
from __future__ import annotations
6969
7070
import os
7171
7272
73+
# Preserve the stdlib imghdr extension hook for old Sphinx-related extensions.
7374
tests = []
7475
7576
@@ -144,6 +145,8 @@ def write_sphinx_json_sitecustomize(output_dir: Path) -> Path:
144145
output_dir.mkdir(parents=True, exist_ok=True)
145146
sitecustomize_path = output_dir / "sitecustomize.py"
146147
sitecustomize_path.write_text(_SPHINX_JSON_SITECUSTOMIZE, encoding="utf-8")
148+
# PYTHONPATH prepending makes this shim shadow stdlib imghdr on Python 3.12.
149+
# The detected formats match the Sphinx usage we need for CPython docs builds.
147150
imghdr_path = output_dir / "imghdr.py"
148151
imghdr_path.write_text(_IMGHDR_COMPAT_MODULE, encoding="utf-8")
149152
return sitecustomize_path
@@ -182,17 +185,30 @@ def build_sphinx_json_command(
182185
]
183186

184187

188+
def _sphinx_pin_needs_pkg_resources(sphinx_pin: str) -> bool:
189+
normalized = sphinx_pin.strip().lower().replace(" ", "")
190+
return normalized.startswith(
191+
(
192+
"sphinx==3.",
193+
"sphinx==4.",
194+
"sphinx~=3.",
195+
"sphinx~=4.",
196+
"sphinx<5",
197+
"sphinx<=4",
198+
)
199+
)
200+
201+
185202
def build_sphinx_bootstrap_requirements(sphinx_pin: str) -> list[str]:
186203
"""Return packages needed before installing CPython Doc requirements.
187204
188-
setuptools<70 keeps ``pkg_resources`` available, which old Sphinx
205+
setuptools<70 keeps ``pkg_resources`` available when old Sphinx
189206
releases (e.g. the 3.4.x line pinned for the Python 3.10 docs build)
190207
still import at startup.
191208
"""
192-
return [
193-
"setuptools<70",
194-
sphinx_pin,
195-
]
209+
if _sphinx_pin_needs_pkg_resources(sphinx_pin):
210+
return ["setuptools<70", sphinx_pin]
211+
return [sphinx_pin]
196212

197213

198214
def parse_fjson(filepath: Path) -> dict:

tests/test_ci_workflows.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from pathlib import Path
55

66
PROJECT_ROOT = Path(__file__).parent.parent
7+
# Keep this literal independent of SUPPORTED_DOC_VERSIONS_CSV so the test catches
8+
# workflow drift even if the application constant changes at the same time.
79
SUPPORTED_VERSION_ARGS = "3.10,3.11,3.12,3.13,3.14"
810

911

tests/test_ingestion.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"""
88
from __future__ import annotations
99

10+
import io
1011
import os
1112
import runpy
1213
import shutil
@@ -117,9 +118,31 @@ def test_writes_imghdr_compat_module(self, tmp_path):
117118

118119
content = (output_dir / "imghdr.py").read_text(encoding="utf-8")
119120
assert "tests = []" in content
121+
assert "stdlib imghdr extension hook" in content
120122
assert "def what" in content
121123
assert "jpeg" in content
122124

125+
def test_imghdr_compat_module_detects_sphinx_image_formats(self, tmp_path):
126+
output_dir = tmp_path / "compat"
127+
write_sphinx_json_sitecustomize(output_dir)
128+
namespace = runpy.run_path(str(output_dir / "imghdr.py"))
129+
130+
what = namespace["what"]
131+
132+
assert what(io.BytesIO(b"\xff\xd8\xff\xe0")) == "jpeg"
133+
assert what(io.BytesIO(b"\x89PNG\r\n\x1a\nextra")) == "png"
134+
assert what(io.BytesIO(b"GIF89aextra")) == "gif"
135+
136+
def test_imghdr_compat_module_preserves_tests_hook(self, tmp_path):
137+
output_dir = tmp_path / "compat"
138+
write_sphinx_json_sitecustomize(output_dir)
139+
namespace = runpy.run_path(str(output_dir / "imghdr.py"))
140+
141+
tests = namespace["tests"]
142+
tests.append(lambda header, _file: "bmp" if header.startswith(b"BM") else None)
143+
144+
assert namespace["what"](io.BytesIO(b"BMfake")) == "bmp"
145+
123146
def test_translation_proxy_patch_stringifies_proxy_objects(
124147
self, tmp_path, monkeypatch
125148
):
@@ -180,6 +203,16 @@ def test_bootstrap_requirements_include_setuptools_before_sphinx(self):
180203

181204
assert requirements == ["setuptools<70", "sphinx==3.4.3"]
182205

206+
def test_bootstrap_requirements_include_setuptools_for_sphinx_4(self):
207+
requirements = build_sphinx_bootstrap_requirements("Sphinx < 5")
208+
209+
assert requirements == ["setuptools<70", "Sphinx < 5"]
210+
211+
def test_bootstrap_requirements_skip_setuptools_for_modern_sphinx(self):
212+
requirements = build_sphinx_bootstrap_requirements("sphinx~=8.2.0")
213+
214+
assert requirements == ["sphinx~=8.2.0"]
215+
183216
def test_build_command_uses_json_builder_and_classic_theme(self, tmp_path):
184217
sphinx_build = tmp_path / "bin" / "sphinx-build"
185218
doc_dir = tmp_path / "cpython" / "Doc"

0 commit comments

Comments
 (0)