diff --git a/doc/source/io.rst b/doc/source/io.rst
index 951960493975e..a0e41a96181a2 100644
--- a/doc/source/io.rst
+++ b/doc/source/io.rst
@@ -1672,14 +1672,13 @@ The Panel class also has a ``to_excel`` instance method,
which writes each DataFrame in the Panel to a separate sheet.
In order to write separate DataFrames to separate sheets in a single Excel file,
-one can use the ExcelWriter class, as in the following example:
+one can pass an :class:`~pandas.io.excel.ExcelWriter`.
.. code-block:: python
- writer = ExcelWriter('path_to_file.xlsx')
- df1.to_excel(writer, sheet_name='Sheet1')
- df2.to_excel(writer, sheet_name='Sheet2')
- writer.save()
+ with ExcelWriter('path_to_file.xlsx') as writer:
+ df1.to_excel(writer, sheet_name='Sheet1')
+ df2.to_excel(writer, sheet_name='Sheet2')
.. _io.excel.writers:
@@ -1693,14 +1692,13 @@ Excel writer engines
1. the ``engine`` keyword argument
2. the filename extension (via the default specified in config options)
-By default ``pandas`` only supports
-`openpyxl `__ as a writer for ``.xlsx``
-and ``.xlsm`` files and `xlwt `__ as a writer for
-``.xls`` files. If you have multiple engines installed, you can change the
-default engine via the ``io.excel.xlsx.writer`` and ``io.excel.xls.writer``
-options.
+By default, ``pandas`` uses `openpyxl `__
+for ``.xlsx`` and ``.xlsm`` files and `xlwt `__
+for ``.xls`` files. If you have multiple engines installed, you can set the
+default engine through :ref:`setting the config options `
+``io.excel.xlsx.writer`` and ``io.excel.xls.writer``.
-For example if the optional `XlsxWriter `__
+For example if the `XlsxWriter `__
module is installed you can use it as a xlsx writer engine as follows:
.. code-block:: python
@@ -1712,8 +1710,8 @@ module is installed you can use it as a xlsx writer engine as follows:
writer = ExcelWriter('path_to_file.xlsx', engine='xlsxwriter')
# Or via pandas configuration.
- from pandas import set_option
- set_option('io.excel.xlsx.writer', 'xlsxwriter')
+ from pandas import options
+ options.io.excel.xlsx.writer = 'xlsxwriter'
df.to_excel('path_to_file.xlsx', sheet_name='Sheet1')
diff --git a/doc/source/release.rst b/doc/source/release.rst
index 75097ee50e8c1..444f25e8662bd 100644
--- a/doc/source/release.rst
+++ b/doc/source/release.rst
@@ -126,6 +126,8 @@ Improvements to existing features
- ``read_json`` now raises a (more informative) ``ValueError`` when the dict
contains a bad key and ``orient='split'`` (:issue:`4730`, :issue:`4838`)
- ``read_stata`` now accepts Stata 13 format (:issue:`4291`)
+ - ``ExcelWriter`` and ``ExcelFile`` can be used as contextmanagers.
+ (:issue:`3441`, :issue:`4933`)
API Changes
~~~~~~~~~~~
diff --git a/pandas/core/frame.py b/pandas/core/frame.py
index 0482312a71547..799d96f46a15b 100644
--- a/pandas/core/frame.py
+++ b/pandas/core/frame.py
@@ -3453,7 +3453,7 @@ def unstack(self, level=-1):
See also
--------
DataFrame.pivot : Pivot a table based on column values.
- DataFrame.stack : Pivot a level of the column labels (inverse operation
+ DataFrame.stack : Pivot a level of the column labels (inverse operation
from `unstack`).
Examples
diff --git a/pandas/io/excel.py b/pandas/io/excel.py
index 6ce8eb697268b..02dbc381a10be 100644
--- a/pandas/io/excel.py
+++ b/pandas/io/excel.py
@@ -14,7 +14,7 @@
from pandas import json
from pandas.compat import map, zip, reduce, range, lrange, u, add_metaclass
from pandas.core import config
-from pandas.core.common import pprint_thing, PandasError
+from pandas.core.common import pprint_thing
import pandas.compat as compat
from warnings import warn
@@ -260,6 +260,17 @@ def _parse_excel(self, sheetname, header=0, skiprows=None, skip_footer=0,
def sheet_names(self):
return self.book.sheet_names()
+ def close(self):
+ """close path_or_buf if necessary"""
+ if hasattr(self.path_or_buf, 'close'):
+ self.path_or_buf.close()
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ self.close()
+
def _trim_excel_header(row):
# trim header row so auto-index inference works
@@ -408,6 +419,17 @@ def check_extension(cls, ext):
else:
return True
+ # Allow use as a contextmanager
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ self.close()
+
+ def close(self):
+ """synonym for save, to make it more file-like"""
+ return self.save()
+
class _OpenpyxlWriter(ExcelWriter):
engine = 'openpyxl'
diff --git a/pandas/io/tests/test_excel.py b/pandas/io/tests/test_excel.py
index 94f3e5a8cf746..2bcf4789412f6 100644
--- a/pandas/io/tests/test_excel.py
+++ b/pandas/io/tests/test_excel.py
@@ -275,6 +275,18 @@ def test_xlsx_table(self):
tm.assert_frame_equal(df4, df.ix[:-1])
tm.assert_frame_equal(df4, df5)
+ def test_reader_closes_file(self):
+ _skip_if_no_xlrd()
+ _skip_if_no_openpyxl()
+
+ pth = os.path.join(self.dirpath, 'test.xlsx')
+ f = open(pth, 'rb')
+ with ExcelFile(f) as xlsx:
+ # parses okay
+ df = xlsx.parse('Sheet1', index_col=0)
+
+ self.assertTrue(f.closed)
+
class ExcelWriterBase(SharedItems):
# Base class for test cases to run with different Excel writers.
@@ -310,6 +322,21 @@ def test_excel_sheet_by_name_raise(self):
self.assertRaises(xlrd.XLRDError, xl.parse, '0')
+ def test_excelwriter_contextmanager(self):
+ ext = self.ext
+ pth = os.path.join(self.dirpath, 'testit.{0}'.format(ext))
+
+ with ensure_clean(pth) as pth:
+ with ExcelWriter(pth) as writer:
+ self.frame.to_excel(writer, 'Data1')
+ self.frame2.to_excel(writer, 'Data2')
+
+ with ExcelFile(pth) as reader:
+ found_df = reader.parse('Data1')
+ found_df2 = reader.parse('Data2')
+ tm.assert_frame_equal(found_df, self.frame)
+ tm.assert_frame_equal(found_df2, self.frame2)
+
def test_roundtrip(self):
_skip_if_no_xlrd()
ext = self.ext