Skip to content

Commit 6131a59

Browse files
WillAydjreback
authored andcommitted
Append Mode for ExcelWriter with openpyxl (#21251)
1 parent 91451cb commit 6131a59

File tree

3 files changed

+77
-15
lines changed

3 files changed

+77
-15
lines changed

doc/source/whatsnew/v0.24.0.txt

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ v0.24.0
88
New features
99
~~~~~~~~~~~~
1010

11+
- ``ExcelWriter`` now accepts ``mode`` as a keyword argument, enabling append to existing workbooks when using the ``openpyxl`` engine (:issue:`3441`)
12+
1113
.. _whatsnew_0240.enhancements.other:
1214

1315
Other Enhancements

pandas/io/excel.py

+36-15
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,10 @@ class ExcelWriter(object):
804804
datetime_format : string, default None
805805
Format string for datetime objects written into Excel files
806806
(e.g. 'YYYY-MM-DD HH:MM:SS')
807+
mode : {'w' or 'a'}, default 'w'
808+
File mode to use (write or append).
809+
810+
.. versionadded:: 0.24.0
807811
808812
Notes
809813
-----
@@ -897,7 +901,8 @@ def save(self):
897901
pass
898902

899903
def __init__(self, path, engine=None,
900-
date_format=None, datetime_format=None, **engine_kwargs):
904+
date_format=None, datetime_format=None, mode='w',
905+
**engine_kwargs):
901906
# validate that this engine can handle the extension
902907
if isinstance(path, string_types):
903908
ext = os.path.splitext(path)[-1]
@@ -919,6 +924,8 @@ def __init__(self, path, engine=None,
919924
else:
920925
self.datetime_format = datetime_format
921926

927+
self.mode = mode
928+
922929
def __fspath__(self):
923930
return _stringify_path(self.path)
924931

@@ -993,23 +1000,27 @@ class _OpenpyxlWriter(ExcelWriter):
9931000
engine = 'openpyxl'
9941001
supported_extensions = ('.xlsx', '.xlsm')
9951002

996-
def __init__(self, path, engine=None, **engine_kwargs):
1003+
def __init__(self, path, engine=None, mode='w', **engine_kwargs):
9971004
# Use the openpyxl module as the Excel writer.
9981005
from openpyxl.workbook import Workbook
9991006

1000-
super(_OpenpyxlWriter, self).__init__(path, **engine_kwargs)
1007+
super(_OpenpyxlWriter, self).__init__(path, mode=mode, **engine_kwargs)
10011008

1002-
# Create workbook object with default optimized_write=True.
1003-
self.book = Workbook()
1009+
if self.mode == 'a': # Load from existing workbook
1010+
from openpyxl import load_workbook
1011+
book = load_workbook(self.path)
1012+
self.book = book
1013+
else:
1014+
# Create workbook object with default optimized_write=True.
1015+
self.book = Workbook()
10041016

1005-
# Openpyxl 1.6.1 adds a dummy sheet. We remove it.
1006-
if self.book.worksheets:
1007-
try:
1008-
self.book.remove(self.book.worksheets[0])
1009-
except AttributeError:
1017+
if self.book.worksheets:
1018+
try:
1019+
self.book.remove(self.book.worksheets[0])
1020+
except AttributeError:
10101021

1011-
# compat
1012-
self.book.remove_sheet(self.book.worksheets[0])
1022+
# compat - for openpyxl <= 2.4
1023+
self.book.remove_sheet(self.book.worksheets[0])
10131024

10141025
def save(self):
10151026
"""
@@ -1443,11 +1454,16 @@ class _XlwtWriter(ExcelWriter):
14431454
engine = 'xlwt'
14441455
supported_extensions = ('.xls',)
14451456

1446-
def __init__(self, path, engine=None, encoding=None, **engine_kwargs):
1457+
def __init__(self, path, engine=None, encoding=None, mode='w',
1458+
**engine_kwargs):
14471459
# Use the xlwt module as the Excel writer.
14481460
import xlwt
14491461
engine_kwargs['engine'] = engine
1450-
super(_XlwtWriter, self).__init__(path, **engine_kwargs)
1462+
1463+
if mode == 'a':
1464+
raise ValueError('Append mode is not supported with xlwt!')
1465+
1466+
super(_XlwtWriter, self).__init__(path, mode=mode, **engine_kwargs)
14511467

14521468
if encoding is None:
14531469
encoding = 'ascii'
@@ -1713,13 +1729,18 @@ class _XlsxWriter(ExcelWriter):
17131729
supported_extensions = ('.xlsx',)
17141730

17151731
def __init__(self, path, engine=None,
1716-
date_format=None, datetime_format=None, **engine_kwargs):
1732+
date_format=None, datetime_format=None, mode='w',
1733+
**engine_kwargs):
17171734
# Use the xlsxwriter module as the Excel writer.
17181735
import xlsxwriter
17191736

1737+
if mode == 'a':
1738+
raise ValueError('Append mode is not supported with xlsxwriter!')
1739+
17201740
super(_XlsxWriter, self).__init__(path, engine=engine,
17211741
date_format=date_format,
17221742
datetime_format=datetime_format,
1743+
mode=mode,
17231744
**engine_kwargs)
17241745

17251746
self.book = xlsxwriter.Workbook(path, **engine_kwargs)

pandas/tests/io/test_excel.py

+39
Original file line numberDiff line numberDiff line change
@@ -2006,6 +2006,31 @@ def test_write_cells_merge_styled(self, merge_cells, ext, engine):
20062006
assert xcell_b1.font == openpyxl_sty_merged
20072007
assert xcell_a2.font == openpyxl_sty_merged
20082008

2009+
@pytest.mark.parametrize("mode,expected", [
2010+
('w', ['baz']), ('a', ['foo', 'bar', 'baz'])])
2011+
def test_write_append_mode(self, merge_cells, ext, engine, mode, expected):
2012+
import openpyxl
2013+
df = DataFrame([1], columns=['baz'])
2014+
2015+
with ensure_clean(ext) as f:
2016+
wb = openpyxl.Workbook()
2017+
wb.worksheets[0].title = 'foo'
2018+
wb.worksheets[0]['A1'].value = 'foo'
2019+
wb.create_sheet('bar')
2020+
wb.worksheets[1]['A1'].value = 'bar'
2021+
wb.save(f)
2022+
2023+
writer = ExcelWriter(f, engine=engine, mode=mode)
2024+
df.to_excel(writer, sheet_name='baz', index=False)
2025+
writer.save()
2026+
2027+
wb2 = openpyxl.load_workbook(f)
2028+
result = [sheet.title for sheet in wb2.worksheets]
2029+
assert result == expected
2030+
2031+
for index, cell_value in enumerate(expected):
2032+
assert wb2.worksheets[index]['A1'].value == cell_value
2033+
20092034

20102035
@td.skip_if_no('xlwt')
20112036
@pytest.mark.parametrize("merge_cells,ext,engine", [
@@ -2060,6 +2085,13 @@ def test_to_excel_styleconverter(self, merge_cells, ext, engine):
20602085
assert xlwt.Alignment.HORZ_CENTER == xls_style.alignment.horz
20612086
assert xlwt.Alignment.VERT_TOP == xls_style.alignment.vert
20622087

2088+
def test_write_append_mode_raises(self, merge_cells, ext, engine):
2089+
msg = "Append mode is not supported with xlwt!"
2090+
2091+
with ensure_clean(ext) as f:
2092+
with tm.assert_raises_regex(ValueError, msg):
2093+
ExcelWriter(f, engine=engine, mode='a')
2094+
20632095

20642096
@td.skip_if_no('xlsxwriter')
20652097
@pytest.mark.parametrize("merge_cells,ext,engine", [
@@ -2111,6 +2143,13 @@ def test_column_format(self, merge_cells, ext, engine):
21112143

21122144
assert read_num_format == num_format
21132145

2146+
def test_write_append_mode_raises(self, merge_cells, ext, engine):
2147+
msg = "Append mode is not supported with xlsxwriter!"
2148+
2149+
with ensure_clean(ext) as f:
2150+
with tm.assert_raises_regex(ValueError, msg):
2151+
ExcelWriter(f, engine=engine, mode='a')
2152+
21142153

21152154
class TestExcelWriterEngineTests(object):
21162155

0 commit comments

Comments
 (0)