Skip to content

fix(skills): inspect a legacy .xls by its values, not by its storage - #10111

Open
L4XB wants to merge 2 commits into
AstrBotDevs:masterfrom
L4XB:fix/xls-inspect-cell-types
Open

L4XB wants to merge 2 commits into
AstrBotDevs:masterfrom
L4XB:fix/xls-inspect-cell-types

Conversation

@L4XB

@L4XB L4XB commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

What this fixes

_inspect_xls reads the sample with sheet.row_values(...), which hands back xlrd's raw storage rather than the cell's value. _inspect_xlsx goes through openpyxl and hands back the value. So the same sheet is described two different ways depending on which format it is saved in.

Measured on master (e0aa8d3), the same four cells written to both formats and run through inspect_workbook.py:

.xls   ->  ["When", "Active", "Units", "Rate"]
           [45292.0, 1, 12.0, 1.5]

.xlsx  ->  ["When", "Active", "Units", "Rate"]
           ["2024-01-01 00:00:00", true, 12, 1.5]

The date is the one that matters: 45292.0 is Excel's serial number, and nothing in the JSON marks it as a date, so a model reading the inspection has no way to recover it. 1 for a boolean and 12.0 for a whole number are the same drift in smaller form.

The change

_xls_cell_value converts each cell by its xlrd type, to match what _inspect_xlsx already produces:

  • XL_CELL_DATEdatetime, via xldate_as_tuple with the workbook's datemode (the 1900/1904 epoch matters and only the book knows which it is);
  • XL_CELL_BOOLEANbool;
  • XL_CELL_NUMBER that is whole → int, since xlrd stores every number as a double;
  • XL_CELL_ERROR → the text Excel shows (#DIV/0!), which is what openpyxl reports.

A float that is not whole, a string and an empty cell are untouched. row_values becomes row_slice, because the conversion needs the cell type and row_values throws it away; the column limit is unchanged.

validate_workbook.py only counts sheets for .xls and needs nothing.

Tests

One case in tests/test_builtin_office_skills.py, in the house style — it runs both scripts as subprocesses over a real .xls (written with xlwt) and the equivalent .xlsx:

assert xls_sample[1] == ["2024-01-01 00:00:00", True, 12, 1.5]
assert xls_sample == xlsx_sample

The second assertion is the property the first is really about: the two formats have to describe the same sheet the same way.

xlwt joins the dev dependency group — it is the only way to write a legacy .xls fixture — and the test also importorskips it so the suite still runs without it.

Measured:

result
with the change 5 passed (the whole file)
source change stashed, test kept 1 failed

ruff check and ruff format clean with the pinned 0.15.22.

Summary by Sourcery

Align legacy .xls inspection with .xlsx value semantics so equivalent workbooks produce consistent samples.

Bug Fixes:

  • Make legacy .xls workbook inspection return cell values consistent with .xlsx inspection, including dates, booleans, whole numbers, errors, and time-only cells.

Build:

  • Add xlwt to the development dependencies for generating legacy .xls test fixtures.

Tests:

  • Add subprocess-based coverage comparing equivalent .xls and .xlsx samples, including date, boolean, numeric, and time-only values.

xlrd hands back what Excel stores rather than what the cell says, so the
spreadsheet skill described the same sheet two different ways:

            .xls        .xlsx
    date    45292.0     2024-01-01 00:00:00
    bool    1           True
    int     12.0        12

The date is the one that cannot be recovered afterwards: the sample the model
reads says 45292.0 and nothing marks it as a date.

Convert the date, the boolean and the whole number the way openpyxl already
delivers them, and report an error cell as the text Excel shows. A float that
is not whole and every string are untouched.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="astrbot/builtin_stars/astrbot/skills/spreadsheets/scripts/inspect_workbook.py" line_range="75" />
<code_context>
+            )
+        except (ValueError, xlrd.XLDateError):
+            return cell.value
+        return datetime(year, month, day, hour, minute, second)
+    if cell.ctype == xlrd.XL_CELL_BOOLEAN:
+        return bool(cell.value)
</code_context>
<issue_to_address>
**issue (bug_risk):** `datetime(year, month, day, hour, minute, second)` raises `ValueError` for valid Excel date-formatted cells representing a time-only value or serial zero, because `xldate_as_tuple` returns zero month/day components for those values. The inspector then exits with an error instead of returning the sample.

**Triggers:** When an XLS contains a date-formatted time-only cell, such as `0.5` for noon, or a date-formatted zero value.

**Suggested fix:** Handle zero month/day tuples as `datetime.time` or preserve the raw value, matching openpyxl's handling of time-only cells, and include the constructor in the conversion error handling.
</issue_to_address>

Sourcery assessment

Approval pending. 1 finding to address first.

Blocking findings: astrbot/builtin_stars/astrbot/skills/spreadsheets/scripts/inspect_workbook.py:75


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

`xldate_as_tuple` reports a cell that carries only a time as
`(0, 0, 0, hour, minute, second)`, so `datetime(...)` raised
`ValueError: year 0 is out of range` and the inspector exited with an
error instead of printing the sample. openpyxl reads the same cell in an
.xlsx as a `datetime.time`, so return one here too.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sourcery assessment

Approved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant