ocr update

This commit is contained in:
Pavel committed 2026-08-27 23:46:12 +04:00
1 parent d7a7d4d084
commit d4e92be0ab
27 files changed
+1656 -134

No files matched your search

+164 -64
View File
@@ -1,6 +1,6 @@
"""Comprehensive tests for application/parser/file/docling_parser.py
Covers: DoclingParser (init, _init_parser, _get_ocr_options, _export_content,
Covers: DoclingParser (init, _init_parser, OCR engine selection, _export_content,
parse_file), subclass initialization, error handling.
"""
@@ -27,8 +27,8 @@ class TestDoclingParserInit:
assert parser.ocr_enabled is True
assert parser.table_structure is True
assert parser.export_format == "markdown"
assert parser.use_rapidocr is True
assert parser.ocr_languages == ["english"]
assert parser.ocr_engine is None # None -> settings.OCR_ENGINE at build
assert parser.ocr_languages is None # None -> the engine's own default
assert parser.force_full_page_ocr is False
assert parser._converter is None
@@ -39,14 +39,14 @@ class TestDoclingParserInit:
ocr_enabled=False,
table_structure=False,
export_format="text",
use_rapidocr=False,
ocr_engine="rapidocr",
ocr_languages=["german"],
force_full_page_ocr=True,
)
assert parser.ocr_enabled is False
assert parser.table_structure is False
assert parser.export_format == "text"
assert parser.use_rapidocr is False
assert parser.ocr_engine == "rapidocr"
assert parser.ocr_languages == ["german"]
assert parser.force_full_page_ocr is True
@@ -90,44 +90,169 @@ class TestDoclingParserInitParser:
@pytest.mark.unit
class TestGetOCROptions:
class TestOcrEngineSelection:
"""``OCR_ENGINE`` resolution and per-engine option building."""
def test_returns_none_when_rapidocr_disabled(self):
@pytest.fixture
def settings(self):
from application.core.settings import settings
return settings
def test_default_setting_is_tesseract(self, settings):
assert settings.OCR_ENGINE == "tesseract"
assert settings.OCR_LANGS == "eng"
def test_none_reads_setting(self, settings, monkeypatch):
from application.parser.file.docling_parser import _resolve_ocr_engine
monkeypatch.setattr(settings, "OCR_ENGINE", "auto")
assert _resolve_ocr_engine(None) == "auto"
def test_unknown_engine_degrades_to_auto(self):
from application.parser.file.docling_parser import _resolve_ocr_engine
assert _resolve_ocr_engine("easyocr") == "auto"
def test_tesseract_without_binary_degrades_to_auto(self, monkeypatch):
import application.parser.file.docling_parser as dp
monkeypatch.setattr(dp.shutil, "which", lambda name: None)
assert dp._resolve_ocr_engine("tesseract") == "auto"
def test_tesseract_with_binary_selected(self, monkeypatch):
import application.parser.file.docling_parser as dp
monkeypatch.setattr(dp.shutil, "which", lambda name: "/usr/bin/tesseract")
assert dp._resolve_ocr_engine("tesseract") == "tesseract"
def test_ocrmac_off_darwin_degrades_to_auto(self, monkeypatch):
import application.parser.file.docling_parser as dp
monkeypatch.setattr(dp.sys, "platform", "linux")
assert dp._resolve_ocr_engine("ocrmac") == "auto"
def test_rapidocr_missing_degrades_to_auto(self, monkeypatch):
import sys
import application.parser.file.docling_parser as dp
monkeypatch.setitem(sys.modules, "rapidocr", None)
assert dp._resolve_ocr_engine("rapidocr") == "auto"
def test_deepseek_passes_through(self):
from application.parser.file.docling_parser import _resolve_ocr_engine
assert _resolve_ocr_engine("deepseek") == "deepseek"
def test_build_auto_returns_none(self):
from application.parser.file.docling_parser import _build_ocr_options
assert _build_ocr_options("auto", None, True) is None
def test_build_tesseract_reads_ocr_langs(self, settings, monkeypatch):
pytest.importorskip("docling")
from application.parser.file.docling_parser import _build_ocr_options
monkeypatch.setattr(settings, "OCR_LANGS", "eng+chi_sim")
options = _build_ocr_options("tesseract", None, True)
assert type(options).__name__ == "TesseractCliOcrOptions"
assert options.lang == ["eng", "chi_sim"]
assert options.force_full_page_ocr is True
def test_build_tesseract_explicit_languages_win(self):
pytest.importorskip("docling")
from application.parser.file.docling_parser import _build_ocr_options
options = _build_ocr_options("tesseract", ["deu"], False)
assert options.lang == ["deu"]
def test_build_rapidocr(self):
pytest.importorskip("docling")
from application.parser.file.docling_parser import _build_ocr_options
options = _build_ocr_options("rapidocr", None, False)
assert type(options).__name__ == "RapidOcrOptions"
assert options.lang == ["english"]
def test_build_import_failure_returns_none(self, monkeypatch):
import sys
from application.parser.file.docling_parser import _build_ocr_options
monkeypatch.setitem(sys.modules, "docling.datamodel.pipeline_options", None)
assert _build_ocr_options("tesseract", ["eng"], False) is None
assert _build_ocr_options("ocrmac", None, False) is None
def test_build_generic_failure_returns_none(self, monkeypatch):
import sys
import types
from application.parser.file.docling_parser import _build_ocr_options
fake = types.ModuleType("docling.datamodel.pipeline_options")
def _boom(**kwargs):
raise RuntimeError("bad options")
fake.RapidOcrOptions = _boom
monkeypatch.setitem(sys.modules, "docling.datamodel.pipeline_options", fake)
assert _build_ocr_options("rapidocr", None, False) is None
@pytest.mark.unit
class TestDeepseekVlmConverter:
"""OCR_ENGINE=deepseek swaps the whole pipeline for docling's VLM route."""
@pytest.fixture(autouse=True)
def _requires_docling(self):
pytest.importorskip("docling")
def test_deepseek_builds_vlm_converter(self, monkeypatch):
from application.core.settings import settings
from application.parser.file.docling_parser import DoclingParser
parser = DoclingParser(use_rapidocr=False)
assert parser._get_ocr_options() is None
monkeypatch.setattr(settings, "OCR_DEEPSEEK_URL", "http://gpu-host:8000/v1/chat/completions")
monkeypatch.setattr(settings, "OCR_DEEPSEEK_MODEL", "deepseek-ocr-x")
def test_returns_options_when_available(self):
built = {}
def _capture_converter(format_options):
built["format_options"] = format_options
return MagicMock()
monkeypatch.setattr(
"docling.document_converter.DocumentConverter", _capture_converter
)
parser = DoclingParser(ocr_enabled=True, ocr_engine="deepseek")
parser._create_converter()
from docling.datamodel.base_models import InputFormat
from docling.pipeline.vlm_pipeline import VlmPipeline
pdf_option = built["format_options"][InputFormat.PDF]
image_option = built["format_options"][InputFormat.IMAGE]
assert pdf_option.pipeline_cls is VlmPipeline
assert image_option.pipeline_cls is VlmPipeline
vlm = pdf_option.pipeline_options.vlm_options
assert vlm.url == "http://gpu-host:8000/v1/chat/completions"
assert vlm.params["model"] == "deepseek-ocr-x"
assert pdf_option.pipeline_options.enable_remote_services is True
def test_deepseek_ignored_when_ocr_disabled(self, monkeypatch):
from application.parser.file.docling_parser import DoclingParser
parser = DoclingParser(use_rapidocr=True, ocr_languages=["english"])
vlm_called = []
monkeypatch.setattr(
DoclingParser,
"_create_vlm_converter",
lambda self: vlm_called.append(True),
)
monkeypatch.setattr("docling.document_converter.DocumentConverter", MagicMock())
DoclingParser(ocr_enabled=False, ocr_engine="deepseek")._create_converter()
mock_options = MagicMock()
with patch(
"application.parser.file.docling_parser.DoclingParser._get_ocr_options",
return_value=mock_options,
):
result = parser._get_ocr_options()
assert result is mock_options
def test_returns_none_on_import_error(self):
from application.parser.file.docling_parser import DoclingParser
parser = DoclingParser(use_rapidocr=True)
# Simulate the ImportError path
original = parser._get_ocr_options
def patched_get_ocr():
try:
raise ImportError("No RapidOcrOptions")
except ImportError:
return None
parser._get_ocr_options = patched_get_ocr
assert parser._get_ocr_options() is None
parser._get_ocr_options = original
assert vlm_called == []
# =====================================================================
@@ -431,31 +556,6 @@ class TestDoclingSubclasses:
@pytest.mark.unit
class TestDoclingParserGaps:
def test_get_ocr_options_import_error_returns_none(self):
"""Cover lines 148-150: ImportError returns None."""
from application.parser.file.docling_parser import DoclingParser
parser = DoclingParser(ocr_enabled=True, use_rapidocr=True)
with patch.dict("sys.modules", {"docling.datamodel.pipeline_options": None}):
# Force re-import to trigger ImportError
with patch(
"builtins.__import__", side_effect=ImportError("no module")
):
result = parser._get_ocr_options()
assert result is None
def test_get_ocr_options_generic_error_returns_none(self):
"""Cover lines 151-153: generic Exception returns None."""
from application.parser.file.docling_parser import DoclingParser
parser = DoclingParser(ocr_enabled=True, use_rapidocr=True)
with patch(
"builtins.__import__",
side_effect=RuntimeError("unexpected"),
):
result = parser._get_ocr_options()
assert result is None
def test_csv_parser_init(self):
"""Cover line 289: DoclingCSVParser.__init__ calls super."""
from application.parser.file.docling_parser import DoclingCSVParser
@@ -1442,11 +1542,11 @@ class TestForceFullPageOCRWiring:
DoclingParser(**kwargs)._create_converter()
return built[0]
def test_forced_without_rapidocr(self, monkeypatch):
def test_forced_with_default_auto_options(self, monkeypatch):
options = self._pipeline_options(
monkeypatch,
ocr_enabled=True,
use_rapidocr=False,
ocr_engine="auto",
force_full_page_ocr=True,
)
assert options.ocr_options.force_full_page_ocr is True
@@ -1455,7 +1555,7 @@ class TestForceFullPageOCRWiring:
options = self._pipeline_options(
monkeypatch,
ocr_enabled=True,
use_rapidocr=False,
ocr_engine="auto",
force_full_page_ocr=False,
)
assert options.ocr_options.force_full_page_ocr is False