Show a clear message when PyMuPDF is missing
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
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:
|
||||
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.constants import UPLOAD_DIR
|
||||
import fitz
|
||||
|
||||
user = get_current_user(request)
|
||||
db = SessionLocal()
|
||||
@@ -988,6 +996,7 @@ def setup_document_routes(session_manager, upload_handler=None) -> APIRouter:
|
||||
if not pdf_path:
|
||||
raise HTTPException(404, f"Source PDF {upload_id} not found")
|
||||
|
||||
fitz = _load_pdf_viewer_fitz()
|
||||
schema = load_field_sidecar(pdf_path) 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 src.pdf_form_doc import find_source_upload_id
|
||||
from src.constants import UPLOAD_DIR
|
||||
import fitz
|
||||
|
||||
user = get_current_user(request)
|
||||
db = SessionLocal()
|
||||
@@ -1058,6 +1066,7 @@ def setup_document_routes(session_manager, upload_handler=None) -> APIRouter:
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
fitz = _load_pdf_viewer_fitz()
|
||||
pdf_doc = fitz.open(pdf_path)
|
||||
try:
|
||||
if page_no < 1 or page_no > pdf_doc.page_count:
|
||||
|
||||
15
src/pdf_runtime.py
Normal file
15
src/pdf_runtime.py
Normal 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
|
||||
@@ -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() {
|
||||
const pane = document.getElementById('doc-pdf-view');
|
||||
if (!pane || !activeDocId) return;
|
||||
@@ -1118,7 +1128,7 @@ import * as Modals from './modalManager.js';
|
||||
let data;
|
||||
try {
|
||||
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();
|
||||
} catch (e) {
|
||||
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
24
tests/test_pdf_runtime.py
Normal 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
|
||||
Reference in New Issue
Block a user