Show a clear message when PyMuPDF is missing

This commit is contained in:
red person
2026-06-01 12:27:17 +03:00
committed by GitHub
parent 5b1e56407b
commit 2f87dbcfbc
4 changed files with 61 additions and 3 deletions

View File

@@ -30,6 +30,15 @@ def _locate_current_user_upload(request: Request, upload_dir: str, upload_id: st
return _locate_upload(upload_dir, upload_id, owner=user, auth_manager=auth_manager) return _locate_upload(upload_dir, upload_id, owner=user, auth_manager=auth_manager)
def _load_pdf_viewer_fitz():
from src.pdf_runtime import load_pymupdf_for_pdf_viewer
try:
return load_pymupdf_for_pdf_viewer()
except RuntimeError as exc:
raise HTTPException(503, str(exc)) from exc
def setup_document_routes(session_manager, upload_handler=None) -> APIRouter: def setup_document_routes(session_manager, upload_handler=None) -> APIRouter:
router = APIRouter(tags=["documents"]) router = APIRouter(tags=["documents"])
@@ -972,7 +981,6 @@ def setup_document_routes(session_manager, upload_handler=None) -> APIRouter:
""" """
from src.pdf_form_doc import find_source_upload_id, parse_markdown_to_values, load_field_sidecar from src.pdf_form_doc import find_source_upload_id, parse_markdown_to_values, load_field_sidecar
from src.constants import UPLOAD_DIR from src.constants import UPLOAD_DIR
import fitz
user = get_current_user(request) user = get_current_user(request)
db = SessionLocal() db = SessionLocal()
@@ -988,6 +996,7 @@ def setup_document_routes(session_manager, upload_handler=None) -> APIRouter:
if not pdf_path: if not pdf_path:
raise HTTPException(404, f"Source PDF {upload_id} not found") raise HTTPException(404, f"Source PDF {upload_id} not found")
fitz = _load_pdf_viewer_fitz()
schema = load_field_sidecar(pdf_path) or [] schema = load_field_sidecar(pdf_path) or []
values = parse_markdown_to_values(doc.current_content or "") values = parse_markdown_to_values(doc.current_content or "")
@@ -1040,7 +1049,6 @@ def setup_document_routes(session_manager, upload_handler=None) -> APIRouter:
from fastapi.responses import Response from fastapi.responses import Response
from src.pdf_form_doc import find_source_upload_id from src.pdf_form_doc import find_source_upload_id
from src.constants import UPLOAD_DIR from src.constants import UPLOAD_DIR
import fitz
user = get_current_user(request) user = get_current_user(request)
db = SessionLocal() db = SessionLocal()
@@ -1058,6 +1066,7 @@ def setup_document_routes(session_manager, upload_handler=None) -> APIRouter:
finally: finally:
db.close() db.close()
fitz = _load_pdf_viewer_fitz()
pdf_doc = fitz.open(pdf_path) pdf_doc = fitz.open(pdf_path)
try: try:
if page_no < 1 or page_no > pdf_doc.page_count: if page_no < 1 or page_no > pdf_doc.page_count:

15
src/pdf_runtime.py Normal file
View File

@@ -0,0 +1,15 @@
"""Small helpers for optional PDF runtime dependencies."""
PDF_VIEWER_PYMUPDF_MISSING = (
"PDF viewer requires PyMuPDF. Install optional PDF dependencies with "
"`pip install -r requirements-optional.txt` (PyMuPDF is AGPL-3.0)."
)
def load_pymupdf_for_pdf_viewer():
"""Return the PyMuPDF module, or raise a user-facing setup hint."""
try:
import fitz # PyMuPDF, optional
except ImportError as exc:
raise RuntimeError(PDF_VIEWER_PYMUPDF_MISSING) from exc
return fitz

View File

@@ -1106,6 +1106,16 @@ import * as Modals from './modalManager.js';
}); });
} }
async function _pdfResponseErrorMessage(res) {
const text = await res.text().catch(() => '');
try {
const data = JSON.parse(text);
if (typeof data?.detail === 'string') return data.detail;
if (data?.detail) return JSON.stringify(data.detail);
} catch (_) {}
return text || res.statusText || `HTTP ${res.status}`;
}
async function _renderPdfPane() { async function _renderPdfPane() {
const pane = document.getElementById('doc-pdf-view'); const pane = document.getElementById('doc-pdf-view');
if (!pane || !activeDocId) return; if (!pane || !activeDocId) return;
@@ -1118,7 +1128,7 @@ import * as Modals from './modalManager.js';
let data; let data;
try { try {
const res = await fetch(`${API_BASE}/api/document/${docId}/render-pages`); const res = await fetch(`${API_BASE}/api/document/${docId}/render-pages`);
if (!res.ok) throw new Error(await res.text()); if (!res.ok) throw new Error(await _pdfResponseErrorMessage(res));
data = await res.json(); data = await res.json();
} catch (e) { } catch (e) {
pane.innerHTML = `<div style="color:#fbb;padding:40px;text-align:center;">Failed to load PDF view: ${_escHtml(e.message || String(e))}</div>`; pane.innerHTML = `<div style="color:#fbb;padding:40px;text-align:center;">Failed to load PDF view: ${_escHtml(e.message || String(e))}</div>`;

24
tests/test_pdf_runtime.py Normal file
View File

@@ -0,0 +1,24 @@
import builtins
import pytest
from src.pdf_runtime import PDF_VIEWER_PYMUPDF_MISSING, load_pymupdf_for_pdf_viewer
def test_pdf_viewer_dependency_error_is_user_actionable(monkeypatch):
real_import = builtins.__import__
def fake_import(name, *args, **kwargs):
if name == "fitz":
raise ImportError("No module named fitz")
return real_import(name, *args, **kwargs)
monkeypatch.setattr(builtins, "__import__", fake_import)
with pytest.raises(RuntimeError) as exc:
load_pymupdf_for_pdf_viewer()
message = str(exc.value)
assert message == PDF_VIEWER_PYMUPDF_MISSING
assert "requirements-optional.txt" in message
assert "PyMuPDF" in message