Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import argparse
import csv
import io
import re
import unicodedata
from datetime import date, datetime
Expand Down Expand Up @@ -121,7 +122,10 @@ def convert_csv_to_xlsx(
except csv.Error:
delimiter = "\t" if input_path.suffix.lower() == ".tsv" else ","

rows = list(csv.reader(text.splitlines(), delimiter=delimiter))
# Not text.splitlines(): the reader joins a quoted cell that spans
# lines, but the line break itself is gone with the split, so the
# words on either side of it are glued together.
rows = list(csv.reader(io.StringIO(text, newline=""), delimiter=delimiter))
if not rows or not any(rows):
raise ValueError("Delimited input contains no cells.")
column_count = max(len(row) for row in rows)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import argparse
import csv
import io
import json
import zipfile
from pathlib import Path
Expand Down Expand Up @@ -45,7 +46,10 @@ def _inspect_delimited(path: Path, sample_rows: int, sample_cols: int) -> dict:
delimiter = dialect.delimiter
except csv.Error:
delimiter = "\t" if path.suffix.lower() == ".tsv" else ","
rows = list(csv.reader(text.splitlines(), delimiter=delimiter))
# Not text.splitlines(): the reader joins a quoted cell that spans
# lines, but the line break itself is gone with the split, so the
# words on either side of it are glued together.
rows = list(csv.reader(io.StringIO(text, newline=""), delimiter=delimiter))
return {
"kind": "delimited",
"encoding": encoding,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import argparse
import csv
import io
import json
import zipfile
from pathlib import Path
Expand Down Expand Up @@ -42,7 +43,10 @@ def _validate_delimited(path: Path, errors: list[str], warnings: list[str]) -> d
delimiter = dialect.delimiter
except csv.Error:
delimiter = "\t" if path.suffix.lower() == ".tsv" else ","
rows = list(csv.reader(text.splitlines(), delimiter=delimiter))
# Not text.splitlines(): the reader joins a quoted cell that spans
# lines, but the line break itself is gone with the split, so the
# words on either side of it are glued together.
rows = list(csv.reader(io.StringIO(text, newline=""), delimiter=delimiter))
if not rows:
errors.append("The file contains no rows.")
return {"rows": 0, "columns": 0}
Expand Down
25 changes: 25 additions & 0 deletions tests/test_builtin_office_skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,31 @@ def test_spreadsheet_skill_converts_inspects_and_validates_csv(tmp_path: Path) -
assert json.loads(validated.stdout)["valid"] is True


def test_spreadsheet_skill_keeps_a_line_break_inside_a_quoted_cell(
tmp_path: Path,
) -> None:
"""A cell may span lines, and the break is part of the value."""
source = tmp_path / "notes.csv"
source.write_text('ID,Note\n1,"line one\nline two"\n2,plain\n', encoding="utf-8")
output = tmp_path / "notes.xlsx"

inspected = _run_script(SPREADSHEET_SCRIPTS / "inspect_workbook.py", source)
converted = _run_script(SPREADSHEET_SCRIPTS / "csv_to_xlsx.py", source, output)

assert inspected.returncode == 0, inspected.stderr
assert converted.returncode == 0, converted.stderr

# "line oneline two" before this, in the sample and in the workbook.
assert json.loads(inspected.stdout)["sample"] == [
["ID", "Note"],
["1", "line one\nline two"],
["2", "plain"],
]
workbook = load_workbook(output)
assert workbook.active["B2"].value == "line one\nline two"
workbook.close()


def test_spreadsheet_skill_rejects_broken_formula_reference(tmp_path: Path) -> None:
path = tmp_path / "broken.xlsx"
workbook = Workbook()
Expand Down
Loading