Skip to content

Commit

Permalink
Append Mode for ExcelWriter with openpyxl (pandas-dev#21251)
Browse files Browse the repository at this point in the history
  • Loading branch information
WillAyd authored and victor committed Sep 30, 2018
1 parent dae6e4c commit 2dceb23
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 15 deletions.
2 changes: 2 additions & 0 deletions doc/source/whatsnew/v0.24.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ v0.24.0
New features
~~~~~~~~~~~~

- ``ExcelWriter`` now accepts ``mode`` as a keyword argument, enabling append to existing workbooks when using the ``openpyxl`` engine (:issue:`3441`)

.. _whatsnew_0240.enhancements.other:

Other Enhancements
Expand Down
51 changes: 36 additions & 15 deletions pandas/io/excel.py
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,10 @@ class ExcelWriter(object):
datetime_format : string, default None
Format string for datetime objects written into Excel files
(e.g. 'YYYY-MM-DD HH:MM:SS')
mode : {'w' or 'a'}, default 'w'
File mode to use (write or append).
.. versionadded:: 0.24.0
Notes
-----
Expand Down Expand Up @@ -897,7 +901,8 @@ def save(self):
pass

def __init__(self, path, engine=None,
date_format=None, datetime_format=None, **engine_kwargs):
date_format=None, datetime_format=None, mode='w',
**engine_kwargs):
# validate that this engine can handle the extension
if isinstance(path, string_types):
ext = os.path.splitext(path)[-1]
Expand All @@ -919,6 +924,8 @@ def __init__(self, path, engine=None,
else:
self.datetime_format = datetime_format

self.mode = mode

def __fspath__(self):
return _stringify_path(self.path)

Expand Down Expand Up @@ -993,23 +1000,27 @@ class _OpenpyxlWriter(ExcelWriter):
engine = 'openpyxl'
supported_extensions = ('.xlsx', '.xlsm')

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

super(_OpenpyxlWriter, self).__init__(path, **engine_kwargs)
super(_OpenpyxlWriter, self).__init__(path, mode=mode, **engine_kwargs)

# Create workbook object with default optimized_write=True.
self.book = Workbook()
if self.mode == 'a': # Load from existing workbook
from openpyxl import load_workbook
book = load_workbook(self.path)
self.book = book
else:
# Create workbook object with default optimized_write=True.
self.book = Workbook()

# Openpyxl 1.6.1 adds a dummy sheet. We remove it.
if self.book.worksheets:
try:
self.book.remove(self.book.worksheets[0])
except AttributeError:
if self.book.worksheets:
try:
self.book.remove(self.book.worksheets[0])
except AttributeError:

# compat
self.book.remove_sheet(self.book.worksheets[0])
# compat - for openpyxl <= 2.4
self.book.remove_sheet(self.book.worksheets[0])

def save(self):
"""
Expand Down Expand Up @@ -1443,11 +1454,16 @@ class _XlwtWriter(ExcelWriter):
engine = 'xlwt'
supported_extensions = ('.xls',)

def __init__(self, path, engine=None, encoding=None, **engine_kwargs):
def __init__(self, path, engine=None, encoding=None, mode='w',
**engine_kwargs):
# Use the xlwt module as the Excel writer.
import xlwt
engine_kwargs['engine'] = engine
super(_XlwtWriter, self).__init__(path, **engine_kwargs)

if mode == 'a':
raise ValueError('Append mode is not supported with xlwt!')

super(_XlwtWriter, self).__init__(path, mode=mode, **engine_kwargs)

if encoding is None:
encoding = 'ascii'
Expand Down Expand Up @@ -1713,13 +1729,18 @@ class _XlsxWriter(ExcelWriter):
supported_extensions = ('.xlsx',)

def __init__(self, path, engine=None,
date_format=None, datetime_format=None, **engine_kwargs):
date_format=None, datetime_format=None, mode='w',
**engine_kwargs):
# Use the xlsxwriter module as the Excel writer.
import xlsxwriter

if mode == 'a':
raise ValueError('Append mode is not supported with xlsxwriter!')

super(_XlsxWriter, self).__init__(path, engine=engine,
date_format=date_format,
datetime_format=datetime_format,
mode=mode,
**engine_kwargs)

self.book = xlsxwriter.Workbook(path, **engine_kwargs)
Expand Down
39 changes: 39 additions & 0 deletions pandas/tests/io/test_excel.py
Original file line number Diff line number Diff line change
Expand Up @@ -2006,6 +2006,31 @@ def test_write_cells_merge_styled(self, merge_cells, ext, engine):
assert xcell_b1.font == openpyxl_sty_merged
assert xcell_a2.font == openpyxl_sty_merged

@pytest.mark.parametrize("mode,expected", [
('w', ['baz']), ('a', ['foo', 'bar', 'baz'])])
def test_write_append_mode(self, merge_cells, ext, engine, mode, expected):
import openpyxl
df = DataFrame([1], columns=['baz'])

with ensure_clean(ext) as f:
wb = openpyxl.Workbook()
wb.worksheets[0].title = 'foo'
wb.worksheets[0]['A1'].value = 'foo'
wb.create_sheet('bar')
wb.worksheets[1]['A1'].value = 'bar'
wb.save(f)

writer = ExcelWriter(f, engine=engine, mode=mode)
df.to_excel(writer, sheet_name='baz', index=False)
writer.save()

wb2 = openpyxl.load_workbook(f)
result = [sheet.title for sheet in wb2.worksheets]
assert result == expected

for index, cell_value in enumerate(expected):
assert wb2.worksheets[index]['A1'].value == cell_value


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

def test_write_append_mode_raises(self, merge_cells, ext, engine):
msg = "Append mode is not supported with xlwt!"

with ensure_clean(ext) as f:
with tm.assert_raises_regex(ValueError, msg):
ExcelWriter(f, engine=engine, mode='a')


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

assert read_num_format == num_format

def test_write_append_mode_raises(self, merge_cells, ext, engine):
msg = "Append mode is not supported with xlsxwriter!"

with ensure_clean(ext) as f:
with tm.assert_raises_regex(ValueError, msg):
ExcelWriter(f, engine=engine, mode='a')


class TestExcelWriterEngineTests(object):

Expand Down

0 comments on commit 2dceb23

Please sign in to comment.