fix(xlsx): keep whole numbers and booleans in a column with an empty cell

L4
12d ago
+33-4

Summary

partition_xlsx() reads each worksheet with pd.read_excel() and its default dtype inference, then writes the frame with to_html(na_rep=""). Pandas gives a column that holds a missing value a float dtype. One empty cell therefore turns a column of whole numbers into floats, and a column of booleans into 1.0 / 0.0:

cellselement text on 0.27.28
12, (empty), 7, 312.0, 7.0, 3.0
TRUE, FALSE, (empty)1.0, 0.0

A probe sheet ([12, "pen", True], [None, "ink", False], [7, "pad", None], [3, "007", 1.5]) comes out of main as 12.0 pen 1.0 ink 0.0 7.0 pad 3.0 007 1.5. With this PR it reads 12 pen True ink False 7 pad 3 007 1.5.

Change

Both read paths, the plain pd.read_excel() and the row-compacting reader used for subtable detection, now pass dtype=object (_CELLS_AS_WRITTEN). Each cell keeps the value openpyxl or xlrd gives for it. Empty cells are still missing, so subtable detection sees the same populated cells as before.

Related: #4505 reads cell text such as N/A, NULL or None as content (keep_default_na=False). I first had that here too, then left it out so the two PRs do not overlap. Each one only adds read options, so they combine without trouble.

Out of scope: a date cell still shows its time (2024-09-30 00:00:00).

Testing

  • New test_partition_xlsx_reads_each_cell_as_written, parametrized over find_subtable, checks the text and text_as_html of the probe sheet. Both cases fail on main.
  • The other 142 tests in test_xlsx.py pass unchanged, including the .xls ones.
  • ruff 0.15.10 check and format are clean, and scripts/version-sync.sh -c passes with 0.27.34-dev7.