chore: import upstream snapshot with attribution
CI / lint (3.11) (push) Has been cancelled
CI / lint (3.12) (push) Has been cancelled
CI / lint (3.13) (push) Has been cancelled
CI / shellcheck (push) Has been cancelled
CI / shfmt (push) Has been cancelled
CI / setup (3.11) (push) Has been cancelled
CI / setup (3.12) (push) Has been cancelled
CI / setup (3.13) (push) Has been cancelled
CI / check-licenses (3.12) (push) Has been cancelled
CI / test_unit (3.11) (push) Has been cancelled
CI / test_unit (3.12) (push) Has been cancelled
CI / test_unit (3.13) (push) Has been cancelled
CI / test_unit_no_extras (3.11) (push) Has been cancelled
CI / test_unit_no_extras (3.12) (push) Has been cancelled
CI / test_json_to_html (3.12) (push) Has been cancelled
CI / test_unit_no_extras (3.13) (push) Has been cancelled
CI / test_unit_dependency_extras (csv, 3.12, --extra csv) (push) Has been cancelled
CI / test_unit_dependency_extras (xlsx, 3.11, --extra xlsx) (push) Has been cancelled
CI / test_unit_dependency_extras (xlsx, 3.12, --extra xlsx) (push) Has been cancelled
CI / test_unit_dependency_extras (csv, 3.11, --extra csv) (push) Has been cancelled
CI / test_unit_dependency_extras (csv, 3.13, --extra csv) (push) Has been cancelled
CI / test_unit_dependency_extras (docx, 3.11, --extra docx) (push) Has been cancelled
CI / test_unit_dependency_extras (docx, 3.12, --extra docx) (push) Has been cancelled
CI / test_unit_dependency_extras (docx, 3.13, --extra docx) (push) Has been cancelled
CI / test_unit_dependency_extras (markdown, 3.11, --extra md) (push) Has been cancelled
CI / test_unit_dependency_extras (markdown, 3.12, --extra md) (push) Has been cancelled
CI / test_unit_dependency_extras (markdown, 3.13, --extra md) (push) Has been cancelled
CI / test_unit_dependency_extras (odt, 3.11, --extra odt) (push) Has been cancelled
CI / test_unit_dependency_extras (odt, 3.12, --extra odt) (push) Has been cancelled
CI / test_unit_dependency_extras (odt, 3.13, --extra odt) (push) Has been cancelled
CI / test_unit_dependency_extras (pdf-image, 3.11, --extra pdf --extra image --extra paddleocr) (push) Has been cancelled
CI / test_unit_dependency_extras (pdf-image, 3.12, --extra pdf --extra image --extra paddleocr) (push) Has been cancelled
CI / test_unit_dependency_extras (pdf-image, 3.13, --extra pdf --extra image --extra paddleocr) (push) Has been cancelled
CI / test_unit_dependency_extras (pptx, 3.11, --extra pptx) (push) Has been cancelled
CI / test_unit_dependency_extras (pptx, 3.12, --extra pptx) (push) Has been cancelled
CI / test_unit_dependency_extras (pptx, 3.13, --extra pptx) (push) Has been cancelled
CI / test_unit_dependency_extras (pypandoc, 3.11, --extra epub --extra org --extra rtf --extra rst) (push) Has been cancelled
CI / test_unit_dependency_extras (pypandoc, 3.12, --extra epub --extra org --extra rtf --extra rst) (push) Has been cancelled
CI / test_unit_dependency_extras (pypandoc, 3.13, --extra epub --extra org --extra rtf --extra rst) (push) Has been cancelled
Build And Push Docker Image / set-short-sha (push) Has been cancelled
Partition Benchmark / setup (push) Has been cancelled
Partition Benchmark / Measure and compare partition() runtime (push) Has been cancelled
CI / test_unit_dependency_extras (xlsx, 3.13, --extra xlsx) (push) Has been cancelled
CI / test_ingest_src (3.12) (push) Has been cancelled
CI / test_json_to_markdown (3.12) (push) Has been cancelled
CI / changelog (push) Has been cancelled
CI / test_dockerfile (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Build And Push Docker Image / build-images (linux/amd64, opensource-linux-8core) (push) Has been cancelled
Build And Push Docker Image / build-images (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build And Push Docker Image / publish-images (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:33:56 +08:00
commit 461bf6fd40
1313 changed files with 1079898 additions and 0 deletions
@@ -0,0 +1,114 @@
# pyright: reportPrivateUsage=false
"""Unit-test suite for the `unstructured.partition.utils.ocr_models.ocr_interface` module."""
from __future__ import annotations
from unittest.mock import patch
import pytest
from test_unstructured.unit_utils import (
FixtureRequest,
LogCaptureFixture,
Mock,
instance_mock,
method_mock,
property_mock,
)
from unstructured.partition.utils.config import ENVConfig
from unstructured.partition.utils.constants import (
OCR_AGENT_PADDLE,
OCR_AGENT_PADDLE_OLD,
OCR_AGENT_TESSERACT,
OCR_AGENT_TESSERACT_OLD,
)
from unstructured.partition.utils.ocr_models.ocr_interface import OCRAgent
class DescribeOCRAgent:
"""Unit-test suite for `unstructured.partition.utils...ocr_interface.OCRAgent` class."""
def it_provides_access_to_the_configured_OCR_agent(
self, _get_ocr_agent_cls_qname_: Mock, get_instance_: Mock, ocr_agent_: Mock
):
_get_ocr_agent_cls_qname_.return_value = OCR_AGENT_TESSERACT
get_instance_.return_value = ocr_agent_
ocr_agent = OCRAgent.get_agent(language="eng")
_get_ocr_agent_cls_qname_.assert_called_once_with()
get_instance_.assert_called_once_with(OCR_AGENT_TESSERACT, "eng")
assert ocr_agent is ocr_agent_
def but_it_raises_when_the_requested_agent_is_not_whitelisted(
self, _get_ocr_agent_cls_qname_: Mock
):
_get_ocr_agent_cls_qname_.return_value = "Invalid.Ocr.Agent.Qname"
with pytest.raises(ValueError, match="must be set to a whitelisted module"):
OCRAgent.get_agent(language="eng")
@pytest.mark.parametrize("exception_cls", [ImportError, AttributeError])
def and_it_raises_when_the_requested_agent_cannot_be_loaded(
self, _get_ocr_agent_cls_qname_: Mock, exception_cls: type[Exception], _clear_cache
):
_get_ocr_agent_cls_qname_.return_value = OCR_AGENT_TESSERACT
with (
patch(
"unstructured.partition.utils.ocr_models.ocr_interface.importlib.import_module",
side_effect=exception_cls,
),
pytest.raises(RuntimeError, match="Could not get the OCRAgent instance"),
):
OCRAgent.get_agent(language="eng")
@pytest.mark.parametrize(
("OCR_AGENT", "expected_value"),
[
(OCR_AGENT_PADDLE, OCR_AGENT_PADDLE),
(OCR_AGENT_PADDLE_OLD, OCR_AGENT_PADDLE),
(OCR_AGENT_TESSERACT, OCR_AGENT_TESSERACT),
(OCR_AGENT_TESSERACT_OLD, OCR_AGENT_TESSERACT),
],
)
def it_computes_the_OCR_agent_qualified_module_name(
self, OCR_AGENT: str, expected_value: str, OCR_AGENT_prop_: Mock
):
OCR_AGENT_prop_.return_value = OCR_AGENT
assert OCRAgent._get_ocr_agent_cls_qname() == expected_value
@pytest.mark.parametrize("OCR_AGENT", [OCR_AGENT_PADDLE_OLD, OCR_AGENT_TESSERACT_OLD])
def and_it_logs_a_warning_when_the_OCR_AGENT_module_name_is_obsolete(
self, caplog: LogCaptureFixture, OCR_AGENT: str, OCR_AGENT_prop_: Mock
):
OCR_AGENT_prop_.return_value = OCR_AGENT
OCRAgent._get_ocr_agent_cls_qname()
assert f"OCR agent name {OCR_AGENT} is outdated " in caplog.text
# -- fixtures --------------------------------------------------------------------------------
@pytest.fixture()
def _clear_cache(self):
# Clear the cache created by @functools.lru_cache(maxsize=None) on OCRAgent.get_instance()
# before each test
OCRAgent.get_instance.cache_clear()
yield
# Clear the cache created by @functools.lru_cache(maxsize=None) on OCRAgent.get_instance()
# after each test (just in case)
OCRAgent.get_instance.cache_clear()
@pytest.fixture()
def get_instance_(self, request: FixtureRequest):
return method_mock(request, OCRAgent, "get_instance")
@pytest.fixture()
def _get_ocr_agent_cls_qname_(self, request: FixtureRequest):
return method_mock(request, OCRAgent, "_get_ocr_agent_cls_qname")
@pytest.fixture()
def ocr_agent_(self, request: FixtureRequest):
return instance_mock(request, OCRAgent)
@pytest.fixture()
def OCR_AGENT_prop_(self, request: FixtureRequest):
return property_mock(request, ENVConfig, "OCR_AGENT")
@@ -0,0 +1,58 @@
import shutil
import tempfile
from pathlib import Path
import pytest
def test_default_config():
from unstructured.partition.utils.config import env_config
assert env_config.IMAGE_CROP_PAD == 0
def test_env_override(monkeypatch):
monkeypatch.setenv("IMAGE_CROP_PAD", str(1))
from unstructured.partition.utils.config import env_config
assert env_config.IMAGE_CROP_PAD == 1
@pytest.fixture()
def _setup_tmpdir():
from unstructured.partition.utils.config import env_config
_tmpdir = tempfile.tempdir
_storage_tmpdir = env_config.GLOBAL_WORKING_PROCESS_DIR
_storage_tmpdir_bak = f"{env_config.GLOBAL_WORKING_PROCESS_DIR}_bak"
if Path(_storage_tmpdir).is_dir():
shutil.move(_storage_tmpdir, _storage_tmpdir_bak)
tempfile.tempdir = None
yield
if Path(_storage_tmpdir_bak).is_dir():
if Path(_storage_tmpdir).is_dir():
shutil.rmtree(_storage_tmpdir)
shutil.move(_storage_tmpdir_bak, _storage_tmpdir)
tempfile.tempdir = _tmpdir
@pytest.mark.usefixtures("_setup_tmpdir")
def test_env_storage_disabled(monkeypatch):
monkeypatch.setenv("GLOBAL_WORKING_DIR_ENABLED", "false")
from unstructured.partition.utils.config import env_config
assert not env_config.GLOBAL_WORKING_DIR_ENABLED
assert str(Path.home() / ".cache/unstructured") == env_config.GLOBAL_WORKING_DIR
assert not Path(env_config.GLOBAL_WORKING_PROCESS_DIR).is_dir()
assert tempfile.gettempdir() != env_config.GLOBAL_WORKING_PROCESS_DIR
@pytest.mark.usefixtures("_setup_tmpdir")
def test_env_storage_enabled(monkeypatch):
monkeypatch.setenv("GLOBAL_WORKING_DIR_ENABLED", "true")
from unstructured.partition.utils.config import env_config
assert env_config.GLOBAL_WORKING_DIR_ENABLED
assert str(Path.home() / ".cache/unstructured") == env_config.GLOBAL_WORKING_DIR
assert Path(env_config.GLOBAL_WORKING_PROCESS_DIR).is_dir()
assert tempfile.gettempdir() == env_config.GLOBAL_WORKING_PROCESS_DIR
@@ -0,0 +1,157 @@
import numpy as np
import pytest
from unstructured_inference.inference.elements import TextRegions
from unstructured.documents.coordinates import PixelSpace
from unstructured.documents.elements import CoordinatesMetadata, Element, Text
from unstructured.partition.utils.constants import SORT_MODE_BASIC, SORT_MODE_XY_CUT
from unstructured.partition.utils.sorting import (
coord_has_valid_points,
coordinates_to_bbox,
shrink_bbox,
sort_page_elements,
sort_text_regions,
)
class MockCoordinatesMetadata(CoordinatesMetadata):
def __init__(self, points):
system = PixelSpace(width=300, height=500)
super().__init__(points, system)
def test_coord_valid_coordinates():
coordinates = CoordinatesMetadata([(1, 2), (3, 4), (5, 6), (7, 8)], PixelSpace)
assert coord_has_valid_points(coordinates) is True
def test_coord_missing_incomplete_point():
coordinates = CoordinatesMetadata([(1, 2), (3, 4), (5, 6)], PixelSpace)
assert coord_has_valid_points(coordinates) is False
def test_coord_negative_values():
coordinates = CoordinatesMetadata([(1, 2), (3, 4), (5, -6), (7, 8)], PixelSpace)
assert coord_has_valid_points(coordinates) is False
def test_coord_weird_values():
coordinates = CoordinatesMetadata([(1, 2), ("3", 4), (5, 6), (7, 8)], PixelSpace)
assert coord_has_valid_points(coordinates) is False
def test_coord_invalid_point_structure():
coordinates = CoordinatesMetadata([(1, 2), (3, 4, 5), (6, 7), (8, 9)], PixelSpace)
assert coord_has_valid_points(coordinates) is False
@pytest.mark.parametrize("sort_mode", ["xy-cut", "basic"])
def test_sort_page_elements_without_coordinates(sort_mode):
elements = [Element(str(idx)) for idx in range(5)]
assert sort_page_elements(elements) == elements
def test_sort_xycut_neg_coordinates():
elements = []
for idx in range(2):
elem = Text(str(idx))
elem.metadata.coordinates = CoordinatesMetadata(
[(0, idx), (3, 4), (6, 7), (8, 9)],
PixelSpace,
)
elements.append(elem)
# NOTE(crag): xycut not attempted, sort_page_elements returns original list
assert sort_page_elements(elements, sort_mode=SORT_MODE_XY_CUT) is not elements
def test_sort_xycut_pos_coordinates():
elements = []
for idx in range(2):
elem = Text(str(idx))
elem.metadata.coordinates = CoordinatesMetadata(
[(1, 2), (3, 4), (6, 7), (8, 9)],
PixelSpace,
)
elements.append(elem)
# NOTE(crag): xycut ran, so different list reference returned from input list
assert sort_page_elements(elements, sort_mode=SORT_MODE_XY_CUT) is not elements
def test_sort_basic_neg_coordinates():
elements = []
for idx in range(3):
elem = Text(str(idx))
elem.metadata.coordinates = CoordinatesMetadata(
[(1, -idx), (3, 4), (6, 7), (8, 9)],
PixelSpace,
)
elements.append(elem)
sorted_page_elements = sort_page_elements(elements, sort_mode=SORT_MODE_BASIC)
sorted_elem_text = " ".join([str(elem.text) for elem in sorted_page_elements])
assert sorted_elem_text == "2 1 0"
def test_sort_basic_pos_coordinates():
elements = []
for idx in range(3):
elem = Text(str(9 - idx))
elem.metadata.coordinates = CoordinatesMetadata(
[(1, 9 - idx), (3, 4), (6, 7), (8, 9)],
PixelSpace,
)
elements.append(elem)
sorted_page_elements = sort_page_elements(elements, sort_mode=SORT_MODE_BASIC)
assert sorted_page_elements is not elements
sorted_elem_text = " ".join([str(elem.text) for elem in sorted_page_elements])
assert sorted_elem_text == "7 8 9"
def test_sort_text_regions():
unsorted = TextRegions(
element_coords=np.array(
[[1, 2, 2, 2], [1, 1, 2, 2], [3, 1, 4, 4]],
),
texts=np.array(["1", "2", "3"]),
sources=np.array(["foo"] * 3),
)
assert sort_text_regions(unsorted, sort_mode=SORT_MODE_BASIC).texts.tolist() == ["2", "3", "1"]
@pytest.mark.parametrize(
"coords",
[
[[1, 2, 2, 2], [1, 1, 2, 2], [3, -1, 4, 4]],
[[1, 2, 2, 2], [1, 1, 2, 2], [3, None, 4, 4]],
],
)
def test_sort_text_regions_with_invalid_coords_using_xy_cut_does_no_ops(coords):
unsorted = TextRegions(
element_coords=np.array(coords).astype(float),
texts=np.array(["1", "2", "3"]),
sources=np.array(["foo"] * 3),
)
assert sort_text_regions(unsorted).texts.tolist() == ["1", "2", "3"]
def test_coordinates_to_bbox():
coordinates_data = MockCoordinatesMetadata([(10, 20), (10, 200), (100, 200), (100, 20)])
expected_result = (10, 20, 100, 200)
assert coordinates_to_bbox(coordinates_data) == expected_result
def test_shrink_bbox():
bbox = (0, 0, 200, 100)
shrink_factor = 0.9
expected_result = (0, 0, 180, 90)
assert shrink_bbox(bbox, shrink_factor) == expected_result
bbox = (20, 20, 320, 120)
shrink_factor = 0.9
expected_result = (20, 20, 290, 110)
assert shrink_bbox(bbox, shrink_factor) == expected_result
@@ -0,0 +1,181 @@
from unittest.mock import patch
import cv2
import numpy as np
import pytest
from unstructured.partition.utils import xycut
def test_projection_by_bboxes():
boxes = np.array([[10, 20, 50, 60], [30, 40, 70, 80]])
# Test case 1: Horizontal projection
result_horizontal = xycut.projection_by_bboxes(boxes, 0)
expected_result_horizontal = np.array(
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
)
assert np.array_equal(result_horizontal[:30], expected_result_horizontal)
# Test case 2: Vertical projection
result_vertical = xycut.projection_by_bboxes(boxes, 1)
expected_result_vertical = np.array(
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
)
assert np.array_equal(result_vertical[:30], expected_result_vertical)
def test_split_projection_profile():
# Test case 1: Sample projection profile with given min_value and min_gap
arr_values = np.array([0, 0, 3, 4, 0, 0, 2, 0, 0, 0, 5, 6, 7, 0, 0, 0])
min_value = 0
min_gap = 1
result = xycut.split_projection_profile(arr_values, min_value, min_gap)
expected_result = (np.array([2, 6, 10]), np.array([4, 7, 13]))
assert np.array_equal(result, expected_result)
# Test case 2: Another sample projection profile with different parameters
arr_values = np.array([0, 2, 0, 0, 0, 3, 0, 0, 4, 5, 6, 0, 0, 0])
min_value = 1
min_gap = 2
result = xycut.split_projection_profile(arr_values, min_value, min_gap)
expected_result = (np.array([1, 5, 8]), np.array([2, 6, 11]))
assert np.array_equal(result, expected_result)
@pytest.mark.parametrize(
("recursive_func", "expected"),
[
(xycut.recursive_xy_cut, [0, 1, 2]),
(xycut.recursive_xy_cut_swapped, [0, 2, 1]),
],
)
def test_recursive_xy_cut(recursive_func, expected):
boxes = np.array([[0, 0, 20, 20], [200, 0, 230, 30], [0, 40, 50, 50]])
indices = np.array([0, 1, 2])
res = []
recursive_func(boxes, indices, res)
assert res == expected
def test_points_to_bbox():
# Test a valid case
points = [10, 20, 30, 40, 50, 60, 70, 80]
result = xycut.points_to_bbox(points)
assert result == [10, 20, 70, 80]
# Test a case where points are unordered
points = [30, 40, 10, 20, 70, 80, 50, 60]
result = xycut.points_to_bbox(points)
assert result == [10, 20, 70, 80]
# Test a case where all points are negative
points = [-10, -20, -30, -40, -50, -60, -70, -80]
result = xycut.points_to_bbox(points)
assert result == [0, 0, 0, 0]
# Test a case with invalid number of points
with pytest.raises(AssertionError):
points = [10, 20, 30, 40, 50, 60] # Missing two points
xycut.points_to_bbox(points)
def test_bbox2points():
# Test a valid case
bbox = [10, 20, 70, 80]
result = xycut.bbox2points(bbox)
assert result == [10, 20, 70, 20, 70, 80, 10, 80]
# Test a case where the top and bottom are the same
bbox = [10, 20, 70, 20]
result = xycut.bbox2points(bbox)
assert result == [10, 20, 70, 20, 70, 20, 10, 20]
# Test a case where left and right are the same
bbox = [10, 20, 10, 80]
result = xycut.bbox2points(bbox)
assert result == [10, 20, 10, 20, 10, 80, 10, 80]
# Test a case where the bbox is a point (left and right are the same,
# top and bottom are the same)
bbox = [10, 20, 10, 20]
result = xycut.bbox2points(bbox)
assert result == [10, 20, 10, 20, 10, 20, 10, 20]
def test_vis_polygon():
img = np.ones((200, 200, 3), dtype=np.uint8) * 255
points = [(50, 50), (150, 50), (150, 150), (50, 150)]
color = (0, 0, 255) # Red color
thickness = 2
result_img = xycut.vis_polygon(img, points, thickness, color)
# Define the expected image with the square drawn
expected_img = np.copy(img)
cv2.line(expected_img, points[0], points[1], color, thickness)
cv2.line(expected_img, points[1], points[2], color, thickness)
cv2.line(expected_img, points[2], points[3], color, thickness)
cv2.line(expected_img, points[3], points[0], color, thickness)
assert np.array_equal(result_img, expected_img)
def test_vis_points():
img = np.ones((200, 200, 3), dtype=np.uint8) * 255
points = [[10, 20, 30, 20, 30, 40, 10, 40], [50, 60, 70, 60, 70, 80, 50, 80]]
texts = ["Label1", "Label2"]
color = (0, 200, 0)
result_img = xycut.vis_points(img, points, texts, color)
# Check if the resulting image contains the expected shapes and labels
expected_img = np.copy(img)
# Draw polygons and labels for each set of points
for i, _points in enumerate(points):
xycut.vis_polygon(expected_img, np.array(_points).reshape(-1, 2), thickness=2, color=color)
bbox = xycut.points_to_bbox(_points)
left, top, right, bottom = bbox
cx = (left + right) // 2
cy = (top + bottom) // 2
txt = texts[i]
# Draw a filled rectangle for the label background
cat_size = cv2.getTextSize(txt, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 2)[0]
expected_img = cv2.rectangle(
expected_img,
(cx - 5 * len(txt), cy - cat_size[1] - 5),
(cx - 5 * len(txt) + cat_size[0], cy - 5),
color,
-1,
)
# Draw the label text
expected_img = cv2.putText(
expected_img,
txt,
(cx - 5 * len(txt), cy - 5),
cv2.FONT_HERSHEY_SIMPLEX,
0.5,
(255, 255, 255),
thickness=1,
lineType=cv2.LINE_AA,
)
assert np.array_equal(result_img, expected_img)
def test_vis_polygons_with_index():
img = np.ones((200, 200, 3), dtype=np.uint8) * 255
points = [[10, 20, 30, 20, 30, 40, 10, 40], [50, 60, 70, 60, 70, 80, 50, 80]]
with patch(
"unstructured.partition.utils.xycut.vis_points", return_value=img
) as mock_vis_points:
result_img = xycut.vis_polygons_with_index(img, points)
# Check if vis_points was called with the correct arguments
mock_vis_points.assert_called_once()
assert np.array_equal(result_img, img)