Skip to content

Commit 30e2c6b

Browse files
committed
Merge pull request #4933 from jtratner/make-ExcelWriter-contextmanager
ENH: Make ExcelWriter & ExcelFile contextmanagers
2 parents e88da53 + 278f726 commit 30e2c6b

File tree

5 files changed

+65
-16
lines changed

5 files changed

+65
-16
lines changed

doc/source/io.rst

+12-14
Original file line numberDiff line numberDiff line change
@@ -1672,14 +1672,13 @@ The Panel class also has a ``to_excel`` instance method,
16721672
which writes each DataFrame in the Panel to a separate sheet.
16731673

16741674
In order to write separate DataFrames to separate sheets in a single Excel file,
1675-
one can use the ExcelWriter class, as in the following example:
1675+
one can pass an :class:`~pandas.io.excel.ExcelWriter`.
16761676

16771677
.. code-block:: python
16781678
1679-
writer = ExcelWriter('path_to_file.xlsx')
1680-
df1.to_excel(writer, sheet_name='Sheet1')
1681-
df2.to_excel(writer, sheet_name='Sheet2')
1682-
writer.save()
1679+
with ExcelWriter('path_to_file.xlsx') as writer:
1680+
df1.to_excel(writer, sheet_name='Sheet1')
1681+
df2.to_excel(writer, sheet_name='Sheet2')
16831682
16841683
.. _io.excel.writers:
16851684

@@ -1693,14 +1692,13 @@ Excel writer engines
16931692
1. the ``engine`` keyword argument
16941693
2. the filename extension (via the default specified in config options)
16951694

1696-
By default ``pandas`` only supports
1697-
`openpyxl <http://packages.python.org/openpyxl/>`__ as a writer for ``.xlsx``
1698-
and ``.xlsm`` files and `xlwt <http://www.python-excel.org/>`__ as a writer for
1699-
``.xls`` files. If you have multiple engines installed, you can change the
1700-
default engine via the ``io.excel.xlsx.writer`` and ``io.excel.xls.writer``
1701-
options.
1695+
By default, ``pandas`` uses `openpyxl <http://packages.python.org/openpyxl/>`__
1696+
for ``.xlsx`` and ``.xlsm`` files and `xlwt <http://www.python-excel.org/>`__
1697+
for ``.xls`` files. If you have multiple engines installed, you can set the
1698+
default engine through :ref:`setting the config options <basics.working_with_options>`
1699+
``io.excel.xlsx.writer`` and ``io.excel.xls.writer``.
17021700

1703-
For example if the optional `XlsxWriter <http://xlsxwriter.readthedocs.org>`__
1701+
For example if the `XlsxWriter <http://xlsxwriter.readthedocs.org>`__
17041702
module is installed you can use it as a xlsx writer engine as follows:
17051703

17061704
.. code-block:: python
@@ -1712,8 +1710,8 @@ module is installed you can use it as a xlsx writer engine as follows:
17121710
writer = ExcelWriter('path_to_file.xlsx', engine='xlsxwriter')
17131711
17141712
# Or via pandas configuration.
1715-
from pandas import set_option
1716-
set_option('io.excel.xlsx.writer', 'xlsxwriter')
1713+
from pandas import options
1714+
options.io.excel.xlsx.writer = 'xlsxwriter'
17171715
17181716
df.to_excel('path_to_file.xlsx', sheet_name='Sheet1')
17191717

doc/source/release.rst

+2
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ Improvements to existing features
126126
- ``read_json`` now raises a (more informative) ``ValueError`` when the dict
127127
contains a bad key and ``orient='split'`` (:issue:`4730`, :issue:`4838`)
128128
- ``read_stata`` now accepts Stata 13 format (:issue:`4291`)
129+
- ``ExcelWriter`` and ``ExcelFile`` can be used as contextmanagers.
130+
(:issue:`3441`, :issue:`4933`)
129131

130132
API Changes
131133
~~~~~~~~~~~

pandas/core/frame.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -3453,7 +3453,7 @@ def unstack(self, level=-1):
34533453
See also
34543454
--------
34553455
DataFrame.pivot : Pivot a table based on column values.
3456-
DataFrame.stack : Pivot a level of the column labels (inverse operation
3456+
DataFrame.stack : Pivot a level of the column labels (inverse operation
34573457
from `unstack`).
34583458
34593459
Examples

pandas/io/excel.py

+23-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from pandas import json
1515
from pandas.compat import map, zip, reduce, range, lrange, u, add_metaclass
1616
from pandas.core import config
17-
from pandas.core.common import pprint_thing, PandasError
17+
from pandas.core.common import pprint_thing
1818
import pandas.compat as compat
1919
from warnings import warn
2020

@@ -260,6 +260,17 @@ def _parse_excel(self, sheetname, header=0, skiprows=None, skip_footer=0,
260260
def sheet_names(self):
261261
return self.book.sheet_names()
262262

263+
def close(self):
264+
"""close path_or_buf if necessary"""
265+
if hasattr(self.path_or_buf, 'close'):
266+
self.path_or_buf.close()
267+
268+
def __enter__(self):
269+
return self
270+
271+
def __exit__(self, exc_type, exc_value, traceback):
272+
self.close()
273+
263274

264275
def _trim_excel_header(row):
265276
# trim header row so auto-index inference works
@@ -408,6 +419,17 @@ def check_extension(cls, ext):
408419
else:
409420
return True
410421

422+
# Allow use as a contextmanager
423+
def __enter__(self):
424+
return self
425+
426+
def __exit__(self, exc_type, exc_value, traceback):
427+
self.close()
428+
429+
def close(self):
430+
"""synonym for save, to make it more file-like"""
431+
return self.save()
432+
411433

412434
class _OpenpyxlWriter(ExcelWriter):
413435
engine = 'openpyxl'

pandas/io/tests/test_excel.py

+27
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,18 @@ def test_xlsx_table(self):
275275
tm.assert_frame_equal(df4, df.ix[:-1])
276276
tm.assert_frame_equal(df4, df5)
277277

278+
def test_reader_closes_file(self):
279+
_skip_if_no_xlrd()
280+
_skip_if_no_openpyxl()
281+
282+
pth = os.path.join(self.dirpath, 'test.xlsx')
283+
f = open(pth, 'rb')
284+
with ExcelFile(f) as xlsx:
285+
# parses okay
286+
df = xlsx.parse('Sheet1', index_col=0)
287+
288+
self.assertTrue(f.closed)
289+
278290

279291
class ExcelWriterBase(SharedItems):
280292
# Base class for test cases to run with different Excel writers.
@@ -310,6 +322,21 @@ def test_excel_sheet_by_name_raise(self):
310322

311323
self.assertRaises(xlrd.XLRDError, xl.parse, '0')
312324

325+
def test_excelwriter_contextmanager(self):
326+
ext = self.ext
327+
pth = os.path.join(self.dirpath, 'testit.{0}'.format(ext))
328+
329+
with ensure_clean(pth) as pth:
330+
with ExcelWriter(pth) as writer:
331+
self.frame.to_excel(writer, 'Data1')
332+
self.frame2.to_excel(writer, 'Data2')
333+
334+
with ExcelFile(pth) as reader:
335+
found_df = reader.parse('Data1')
336+
found_df2 = reader.parse('Data2')
337+
tm.assert_frame_equal(found_df, self.frame)
338+
tm.assert_frame_equal(found_df2, self.frame2)
339+
313340
def test_roundtrip(self):
314341
_skip_if_no_xlrd()
315342
ext = self.ext

0 commit comments

Comments
 (0)