feat(xlsx): expose a guarded worksheet-at-a-time workbook reader

Merged
CY
13d ago
+142-42

What

Adds a public way to read a workbook one worksheet at a time with the same protections partition_xlsx() uses:

from unstructured.partition.xlsx import open_excel_workbook

with open_excel_workbook(file_bytes, compact_empty_rows=True) as workbook:
    for name in workbook.sheet_names:
        frame = workbook.parse(name, header=None)
        ...  # release `frame` before parsing the next sheet

open_excel_workbook():

  • rejects non-Office and password-protected files with UnprocessableEntityError, as partition_xlsx() does;
  • picks the engine from the file signature (openpyxl for XLSX, xlrd for XLS) and rejects anything else with UnprocessableEntityError;
  • runs the existing XLSX_MAX_CELLS worksheet cell-count check before any worksheet is read;
  • optionally reads with runs of empty rows compacted (XLSX only), as subtable detection already does;
  • closes the reader when the block exits, including on errors.

partition_xlsx() now reads through the same function, so its behavior is unchanged.

Why

Integrations that need their own per-worksheet processing currently have two options: call pandas.read_excel directly, which skips the cell-count limit and reads every sheet into memory at once, or import private helpers (_check_worksheet_cell_count, _RowCompactingOpenpyxlReader) that can change without notice. A tiny file with one far-away cell can still expand into a huge dense data-frame through the first route. This gives such callers a supported, guarded entry point.

Validation

  • New test_xlsx_reader.py: engine selection, password-protected rejection, cell-limit enforcement before reading (independent of the ambient XLSX_MAX_CELLS), empty-row compaction, repeated worksheet reads, and reader cleanup on success and on a consumer error.
  • XLSX partitioner suite passes unchanged; baseline-vs-branch workbook comparisons produce identical elements.
  • Ruff check/format pass.

Release

Targets 0.27.32 as 0.27.32-dev5 (0.27.31 was taken by #4538, which merged first.) Released together with #4419, #4516, #4517, #4524 and #4523, and followed by the stacked dense-worksheet PR. Whichever merges after another rebases and renumbers the CHANGELOG heading, as usual for this repo.