Skip to content

Commit 13b132e

Browse files
authored
BUG: boolean/string value in OdsWriter (#54994) (#54996)
1 parent 4e28925 commit 13b132e

File tree

4 files changed

+72
-29
lines changed

4 files changed

+72
-29
lines changed

doc/source/whatsnew/v2.2.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ I/O
250250
^^^
251251
- Bug in :func:`read_csv` where ``on_bad_lines="warn"`` would write to ``stderr`` instead of raise a Python warning. This now yields a :class:`.errors.ParserWarning` (:issue:`54296`)
252252
- Bug in :func:`read_excel`, with ``engine="xlrd"`` (``xls`` files) erroring when file contains NaNs/Infs (:issue:`54564`)
253+
- Bug in :func:`to_excel`, with ``OdsWriter`` (``ods`` files) writing boolean/string value (:issue:`54994`)
253254

254255
Period
255256
^^^^^^

pandas/io/excel/_odswriter.py

+19-8
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,15 @@ def _make_table_cell(self, cell) -> tuple[object, Any]:
192192
if isinstance(val, bool):
193193
value = str(val).lower()
194194
pvalue = str(val).upper()
195-
if isinstance(val, datetime.datetime):
195+
return (
196+
pvalue,
197+
TableCell(
198+
valuetype="boolean",
199+
booleanvalue=value,
200+
attributes=attributes,
201+
),
202+
)
203+
elif isinstance(val, datetime.datetime):
196204
# Fast formatting
197205
value = val.isoformat()
198206
# Slow but locale-dependent
@@ -210,17 +218,20 @@ def _make_table_cell(self, cell) -> tuple[object, Any]:
210218
pvalue,
211219
TableCell(valuetype="date", datevalue=value, attributes=attributes),
212220
)
221+
elif isinstance(val, str):
222+
return (
223+
pvalue,
224+
TableCell(
225+
valuetype="string",
226+
stringvalue=value,
227+
attributes=attributes,
228+
),
229+
)
213230
else:
214-
class_to_cell_type = {
215-
str: "string",
216-
int: "float",
217-
float: "float",
218-
bool: "boolean",
219-
}
220231
return (
221232
pvalue,
222233
TableCell(
223-
valuetype=class_to_cell_type[type(val)],
234+
valuetype="float",
224235
value=value,
225236
attributes=attributes,
226237
),

pandas/tests/io/excel/test_odswriter.py

+49
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
from datetime import (
2+
date,
3+
datetime,
4+
)
15
import re
26

37
import pytest
48

9+
import pandas as pd
510
import pandas._testing as tm
611

712
from pandas.io.excel import ExcelWriter
@@ -47,3 +52,47 @@ def test_book_and_sheets_consistent(ext):
4752
table = odf.table.Table(name="test_name")
4853
writer.book.spreadsheet.addElement(table)
4954
assert writer.sheets == {"test_name": table}
55+
56+
57+
@pytest.mark.parametrize(
58+
["value", "cell_value_type", "cell_value_attribute", "cell_value"],
59+
argvalues=[
60+
(True, "boolean", "boolean-value", "true"),
61+
("test string", "string", "string-value", "test string"),
62+
(1, "float", "value", "1"),
63+
(1.5, "float", "value", "1.5"),
64+
(
65+
datetime(2010, 10, 10, 10, 10, 10),
66+
"date",
67+
"date-value",
68+
"2010-10-10T10:10:10",
69+
),
70+
(date(2010, 10, 10), "date", "date-value", "2010-10-10"),
71+
],
72+
)
73+
def test_cell_value_type(ext, value, cell_value_type, cell_value_attribute, cell_value):
74+
# GH#54994 ODS: cell attributes should follow specification
75+
# http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#refTable13
76+
from odf.namespaces import OFFICENS
77+
from odf.table import (
78+
TableCell,
79+
TableRow,
80+
)
81+
82+
table_cell_name = TableCell().qname
83+
84+
with tm.ensure_clean(ext) as f:
85+
pd.DataFrame([[value]]).to_excel(f, header=False, index=False)
86+
87+
with pd.ExcelFile(f) as wb:
88+
sheet = wb._reader.get_sheet_by_index(0)
89+
sheet_rows = sheet.getElementsByType(TableRow)
90+
sheet_cells = [
91+
x
92+
for x in sheet_rows[0].childNodes
93+
if hasattr(x, "qname") and x.qname == table_cell_name
94+
]
95+
96+
cell = sheet_cells[0]
97+
assert cell.attributes.get((OFFICENS, "value-type")) == cell_value_type
98+
assert cell.attributes.get((OFFICENS, cell_value_attribute)) == cell_value

pandas/tests/io/excel/test_readers.py

+3-21
Original file line numberDiff line numberDiff line change
@@ -578,17 +578,11 @@ def test_reader_dtype_str(self, read_ext, dtype, expected):
578578
actual = pd.read_excel(basename + read_ext, dtype=dtype)
579579
tm.assert_frame_equal(actual, expected)
580580

581-
def test_dtype_backend(self, request, engine, read_ext, dtype_backend):
581+
def test_dtype_backend(self, read_ext, dtype_backend):
582582
# GH#36712
583583
if read_ext in (".xlsb", ".xls"):
584584
pytest.skip(f"No engine for filetype: '{read_ext}'")
585585

586-
# GH 54994
587-
if engine == "calamine" and read_ext == ".ods":
588-
request.node.add_marker(
589-
pytest.mark.xfail(reason="OdsWriter produces broken file")
590-
)
591-
592586
df = DataFrame(
593587
{
594588
"a": Series([1, 3], dtype="Int64"),
@@ -629,17 +623,11 @@ def test_dtype_backend(self, request, engine, read_ext, dtype_backend):
629623
expected = df
630624
tm.assert_frame_equal(result, expected)
631625

632-
def test_dtype_backend_and_dtype(self, request, engine, read_ext):
626+
def test_dtype_backend_and_dtype(self, read_ext):
633627
# GH#36712
634628
if read_ext in (".xlsb", ".xls"):
635629
pytest.skip(f"No engine for filetype: '{read_ext}'")
636630

637-
# GH 54994
638-
if engine == "calamine" and read_ext == ".ods":
639-
request.node.add_marker(
640-
pytest.mark.xfail(reason="OdsWriter produces broken file")
641-
)
642-
643631
df = DataFrame({"a": [np.nan, 1.0], "b": [2.5, np.nan]})
644632
with tm.ensure_clean(read_ext) as file_path:
645633
df.to_excel(file_path, sheet_name="test", index=False)
@@ -651,17 +639,11 @@ def test_dtype_backend_and_dtype(self, request, engine, read_ext):
651639
)
652640
tm.assert_frame_equal(result, df)
653641

654-
def test_dtype_backend_string(self, request, engine, read_ext, string_storage):
642+
def test_dtype_backend_string(self, read_ext, string_storage):
655643
# GH#36712
656644
if read_ext in (".xlsb", ".xls"):
657645
pytest.skip(f"No engine for filetype: '{read_ext}'")
658646

659-
# GH 54994
660-
if engine == "calamine" and read_ext == ".ods":
661-
request.node.add_marker(
662-
pytest.mark.xfail(reason="OdsWriter produces broken file")
663-
)
664-
665647
pa = pytest.importorskip("pyarrow")
666648

667649
with pd.option_context("mode.string_storage", string_storage):

0 commit comments

Comments
 (0)