Skip to content

fix(skills): keep a line break that is inside a quoted CSV cell - #10113

Open
L4XB wants to merge 2 commits into
AstrBotDevs:masterfrom
L4XB:fix/csv-multiline-cell
Open

L4XB wants to merge 2 commits into
AstrBotDevs:masterfrom
L4XB:fix/csv-multiline-cell

Conversation

@L4XB

@L4XB L4XB commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

All three spreadsheet scripts read a delimited file the same way:

rows = list(csv.reader(text.splitlines(), delimiter=delimiter))

csv.reader accepts any iterable of strings and does join a quoted cell that spans several of them — but splitlines() has already removed the break, so what the reader joins is "line one" + "line two" with nothing in between.

On a two-row file whose second column holds a note that spans lines:

ID,Note
1,"line one
line two"
2,plain
script before after
inspect_workbook.py ['1', 'line oneline two'] ['1', 'line one\n line two']
csv_to_xlsx.pyB2 'line oneline two' 'line one\nline two'
validate_workbook.py reads the same glued value unchanged output, correct value

Measured on master (e0aa8d3) by running the three scripts over that file.

A cell that spans lines is ordinary in an exported sheet — a postal address, a product description, a comment column — and csv_to_xlsx.py writes the damage into the .xlsx it produces, so the loss outlives the tool call. Nothing reports it: the row count and the column count stay correct, because the reader did put the two halves in one field. Only the words are wrong.

Modifications / 改动点

io.StringIO(text, newline="") replaces text.splitlines() in inspect_workbook.py, validate_workbook.py and csv_to_xlsx.py. That is the form the csv documentation asks for, and it keeps the break inside the field. import io is added to each script; nothing else changes, and a file without a multi-line cell parses byte-identically.

  • This is NOT a breaking change. / 这不是一个破坏性变更。

三个表格脚本都用 csv.reader(text.splitlines(), ...) 读取分隔文件。splitlines() 已经把换行删掉了,所以跨行的带引号单元格被拼接时,两边的词会粘在一起:"line one\nline two" 变成 line oneline two,并且 csv_to_xlsx.py 会把这个错误写进生成的 .xlsx。改为 io.StringIO(text, newline=""),即 csv 文档推荐的写法。

Screenshots or Test Results / 运行截图或测试结果

test_spreadsheet_skill_keeps_a_line_break_inside_a_quoted_cell added to tests/test_builtin_office_skills.py. It runs inspect_workbook.py and csv_to_xlsx.py over the file above and asserts the sample and cell B2.

Against the unfixed scripts (git stash on the scripts/ directory only):

FAILED tests/test_builtin_office_skills.py::test_spreadsheet_skill_keeps_a_line_break_inside_a_quoted_cell
  At index 1 diff: ['1', 'line oneline two'] != ['1', 'line one\nline two']
1 failed, 4 deselected

With the fix:

$ python -m pytest tests/test_builtin_office_skills.py -q
5 passed

$ python -m pytest tests/ -q
2 failed, 3328 passed, 4 skipped

Both remaining failures are tests/test_fastapi_v1_dashboard.py::test_config_update_revokes_only_affected_shell_sessions; they fail the same way on a clean origin/master checkout and are unrelated to this change.

ruff check and ruff format --check are clean on all four files.


Checklist / 检查清单

  • 😊 If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
    / 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。 (Bug fix, no new feature.)

  • 👀 My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
    / 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”

  • 📚 I checked the affected WebUI instructions and screenshots in docs/zh and docs/en against the changed navigation, page structure, and labels, and updated them in this PR (or explained why no documentation update is needed).
    / 我已对照变化后的 WebUI 入口、页面结构和术语,核对并在本 PR 中更新 docs/zhdocs/en 的相关操作说明与截图(或说明无需更新文档的原因)。 (No WebUI entry point, page or label changes: this is inside three skill scripts.)

  • 🤓 I have ensured that no new dependencies are introduced.
    / 我确保没有引入新依赖库。 (io is stdlib.)

  • 😮 My changes do not introduce malicious code.
    / 我的更改没有引入恶意代码。

Summary by Sourcery

Preserve embedded line breaks when spreadsheet skills parse delimited files.

Bug Fixes:

  • Preserve line breaks in quoted multi-line CSV and TSV cells across spreadsheet inspection, conversion, and validation scripts.

Tests:

  • Add coverage for LF and CRLF line breaks in quoted cells, including inspection output and converted workbook values.

All three spreadsheet scripts read a delimited file with
`csv.reader(text.splitlines(), ...)`. The reader does join a quoted cell that
spans lines, but `splitlines()` has already thrown the break away, so the words
on either side of it are glued together:

    ID,Note
    1,"line one
    line two"

    inspect   sample -> ['1', 'line oneline two']
    csv_to_xlsx   B2 -> 'line oneline two'

A cell that spans lines is ordinary in an exported sheet -- an address, a
description, a note -- and the conversion writes the damage into the .xlsx it
produces.

Read from `io.StringIO(text, newline="")` instead, which is the form the csv
docs ask for and which keeps the break inside the field.

@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 reviewed your changes and they look great!

Sourcery assessment

Needs a human reviewer. If the CSV parsing change is wrong, conversion could write incorrect cell values into generated XLSX files, and inspection or validation could report misleading results. Reverting stops future bad outputs, while existing files can be regenerated from the original CSV source.


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

buyun14 pushed a commit to buyun14/AstrBot that referenced this pull request Sep 17, 2026
buyun14 pushed a commit to buyun14/AstrBot that referenced this pull request Sep 17, 2026

@kilisamemarisaaa kilisamemarisaaa 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.

Make the new fixture's newline encoding explicit on Windows

At head d6bdc8ee6d9323c57878078a94f44dbd37147e36, the new test fails on Windows (Python 3.12.6):

python -m pytest tests/test_builtin_office_skills.py -q
1 failed, 4 passed
At index 1 diff: ['1', 'line one\r\nline two'] != ['1', 'line one\nline two']

Path.write_text(..., encoding="utf-8") uses default newline translation, so the fixture at tests/test_builtin_office_skills.py:82 contains CRLF on Windows. The scripts read bytes and the new StringIO(..., newline="") correctly preserves those CRLF characters, but both assertions require LF.

Please write the fixture with newline="" (or explicit bytes) so its contents match the LF assertions on every platform. Changing only that write locally made this test file pass: 5 passed. Product scripts were unchanged. An explicit CRLF fixture expecting CRLF would also cover the intended preservation behavior, without normalizing away the data being tested.

This is a fixture portability issue, not evidence against the parser fix. Validation was limited to this test file; I did not rerun the full AstrBot suite. The temporary fixture change was removed from my isolated review checkout.

Path.write_text without a newline argument translates "\n" to os.linesep,
so on Windows the fixture held CRLF while both assertions required LF, and
the test failed there although the parser was doing the right thing:
preserving the bytes it was given.

The fixture now writes with newline="" so its contents are the same on
every platform, and a companion case covers a genuinely CRLF file end to
end, from the reader through the workbook cell.
@L4XB

L4XB commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

You are right, and thank you for running it on Windows. Fixed in fcf78d0.

Path.write_text(..., encoding="utf-8") leaves newline=None, which translates "\n" to os.linesep on write, so the fixture held CRLF on Windows while both assertions required LF. The parser was doing the right thing there: it preserved the bytes it was given, and the test punished it for that. The fixture now writes with newline="".

I measured the mechanism rather than assuming it, by writing the same string both ways on this machine:

win.csv    CR bytes: 4  LF bytes: 4      # write_text(..., newline="\r\n"), what Windows does by default
fixed.csv  CR bytes: 0  LF bytes: 4      # write_text(..., newline="")

I also took your second suggestion and added test_spreadsheet_skill_keeps_a_crlf_line_break_inside_a_quoted_cell: a genuinely CRLF file (4 CR bytes in the fixture, verified), asserting "line one\r\nline two" from the inspector's sample and from the workbook cell. tests/test_builtin_office_skills.py is 6 passed.

One correction to something I would otherwise have implied. That CRLF case does not discriminate the newline="" argument to io.StringIO, and I checked before claiming it did:

with newline=''    -> [['ID', 'Note'], ['1', 'line one\r\nline two'], ['2', 'plain']]
default newline    -> [['ID', 'Note'], ['1', 'line one\r\nline two'], ['2', 'plain']]

io.StringIO's default is newline='\n', not None, so it already leaves \r alone; the argument is explicitness, not behaviour. What both new tests do discriminate is the change this PR is actually about. Restoring text.splitlines() in all three scripts:

FAILED tests/test_builtin_office_skills.py::test_spreadsheet_skill_keeps_a_line_break_inside_a_quoted_cell
FAILED tests/test_builtin_office_skills.py::test_spreadsheet_skill_keeps_a_crlf_line_break_inside_a_quoted_cell
2 failed, 4 passed

and with the fix back in place, 6 passed.

Run on macOS with Python 3.12.11 in an isolated venv (pytest, pytest-asyncio, openpyxl, python-docx, chardet, xlrd, pandas). I have no Windows host, so the CRLF case is exercised there through an explicitly CRLF fixture rather than through the platform, which is the part your run covered and mine cannot.

@kilisamemarisaaa kilisamemarisaaa 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.

Rechecked head fcf78d0 in a fresh detached worktree on Windows / Python 3.12.6. python -m pytest tests/test_builtin_office_skills.py -q: 6 passed. The explicit newline setting fixes the Windows fixture failure, and both LF and CRLF quoted-cell preservation cases pass. Reviewed the updated diff; my fixture-portability concern is resolved. This is focused validation, not a full-suite run. Ruff was unavailable in this Python environment.

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.

2 participants