diff --git a/src/file_manipulator.py b/src/file_manipulator.py index b7815cc..04ebcb1 100644 --- a/src/file_manipulator.py +++ b/src/file_manipulator.py @@ -1,7 +1,7 @@ import os from src.filler import Filler from src.llm import LLM -from commonforms import prepare_form +from src.pdf_template import prepare_form_safe class FileManipulator: @@ -14,7 +14,7 @@ def create_template(self, pdf_path: str): By using commonforms, we create an editable .pdf template and we store it. """ template_path = pdf_path[:-4] + "_template.pdf" - prepare_form(pdf_path, template_path) + prepare_form_safe(pdf_path, template_path) return template_path def fill_form(self, user_input: str, fields: list, pdf_form_path: str): diff --git a/src/pdf_template.py b/src/pdf_template.py new file mode 100644 index 0000000..ad02aae --- /dev/null +++ b/src/pdf_template.py @@ -0,0 +1,46 @@ +"""PDF template preparation abstraction. + +Why this exists: +- `commonforms` pulls in heavy runtime deps (ultralytics -> cv2 -> numpy). +- In minimal/server environments this can hard-crash the interpreter during import. +- FireForm only needs `commonforms.prepare_form()` for template creation. + +This module provides a safe wrapper that: +- imports `commonforms` lazily (only when template creation is requested) +- raises a clear Python exception instead of crashing at app import time +""" + +from __future__ import annotations + +from typing import Protocol + + +class TemplatePreparer(Protocol): + def __call__(self, input_pdf_path: str, output_pdf_path: str) -> None: + ... + + +def prepare_form_safe(input_pdf_path: str, output_pdf_path: str) -> None: + """Prepare a PDF template using `commonforms` via a lazy import. + + Raises: + RuntimeError: if `commonforms` cannot be imported or fails at runtime. + """ + + try: + from commonforms import prepare_form as _prepare_form # type: ignore + except Exception as e: # pragma: no cover + # Catch broad exceptions because some environments segfault or raise + # low-level import errors when image dependencies are missing. + raise RuntimeError( + "Failed to import `commonforms`. Template creation requires the optional " + "commonforms + OpenCV runtime dependencies. " + "If running in Docker, ensure libgl1/libglib2.0-0 are installed." + ) from e + + try: + _prepare_form(input_pdf_path, output_pdf_path) + except Exception as e: + raise RuntimeError( + f"Template preparation failed for '{input_pdf_path}'." + ) from e diff --git a/tests/test_commonforms_lazy_import.py b/tests/test_commonforms_lazy_import.py new file mode 100644 index 0000000..8078107 --- /dev/null +++ b/tests/test_commonforms_lazy_import.py @@ -0,0 +1,20 @@ +import importlib + + +def test_importing_api_main_does_not_eagerly_import_commonforms(monkeypatch): + """Regression test for environments where commonforms (cv2/numpy) crashes. + + The FastAPI app should be importable without importing `commonforms`. + Template creation should be the only place that tries to import it. + """ + + def _fail_import(name, globals=None, locals=None, fromlist=(), level=0): + if name == "commonforms": + raise ImportError("commonforms should not be imported at app import time") + return original_import(name, globals, locals, fromlist, level) + + original_import = __builtins__["__import__"] + monkeypatch.setitem(__builtins__, "__import__", _fail_import) + + # Should not raise + importlib.import_module("api.main") diff --git a/tests/test_templates.py b/tests/test_templates.py index bbced2b..a3015dc 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -1,4 +1,14 @@ -def test_create_template(client): +import pytest + + +def test_create_template(client, monkeypatch): + # Patch at the usage site: api.routes.templates imports Controller + monkeypatch.setattr( + "api.routes.templates.Controller.create_template", + lambda _self, _pdf_path: "src/inputs/file_template.pdf", + raising=True, + ) + payload = { "name": "Template 1", "pdf_path": "src/inputs/file.pdf",