Repository: pandas-dev/pandas

Issue:
Allow ExcelWriter() to add sheets to existing workbook
The ability of ExcelWriter to save different dataframes to different worksheets is great for sharing those dfs with the python-deficient.  But this quickly leads to a need to _add_ worksheets to an existing workbook, not just creating one from scratch; something like:

```
df0=pd.DataFrame(np.arange(3))
df0.to_excel('foo.xlsx','Data 0')

df1=pd.DataFrame(np.arange(2))
df1.to_excel('foo.xlsx','Data 1')
```

The following little diff to io/parsers.py implements this behavior for *.xlsx files:

```
diff --git a/pandas/io/parsers.py b/pandas/io/parsers.py
index 89f892d..7f010ee 100644
--- a/pandas/io/parsers.py
+++ b/pandas/io/parsers.py
@@ -2099,12 +2099,19 @@ class ExcelWriter(object):
             self.fm_date = xlwt.easyxf(num_format_str='YYYY-MM-DD')
         else:
             from openpyxl.workbook import Workbook
-            self.book = Workbook()  # optimized_write=True)
-            # open pyxl 1.6.1 adds a dummy sheet remove it
-            if self.book.worksheets:
-                self.book.remove_sheet(self.book.worksheets[0])
+            from openpyxl.reader.excel import load_workbook
+
+            try:
+               self.book=load_workbook(filename = path)
+               self.sheets={wks.title:wks for wks in self.book.worksheets}
+            except InvalidFileException:
+                self.book = Workbook()  # optimized_write=True)
+                # open pyxl 1.6.1 adds a dummy sheet remove it
+                if self.book.worksheets:
+                    self.book.remove_sheet(self.book.worksheets[0])
+                self.sheets = {}
+
         self.path = path
-        self.sheets = {}
         self.cur_sheet = None
```

Doing this for *.xls files is a little harder.



Discussion:
@jtratner is this still a bug/needed enhancement?

Because of how `to_excel` is set up, this would mean reading in and then writing the file each time (because `to_excel` with a path argument saves the file). The right way to do this is to use `ExcelWriter`:

``` python
import pandas as pd
writer = pd.ExcelWriter('foo.xlsx')
df.to_excel(writer, 'Data 0')
df.to_excel(writer, 'Data 1')
writer.save()
```

I could see (eventually) adding an option to ExcelWriter that doesn't overwrite the file. But, yet again, that may mean writing in the entire file first. I don't know.

I'm going to add something to the docs about this, maybe a test case with this, and I'll look into adding an option to read in the file, but it depends on how xlwt and openpyxl work.

@jtratner what about a context manager `get_excel`?

```
with get_excel('foo.xlsx') as e:
    df.to_excel(e,'Data 0)
    df.to_excel(e,'Data 1)
```

?

how about we just make ExcelWriter into a contextmanager instead?  it'll just call save at the end. Much simpler.


Resolving patch:
diff --git a/pandas/core/frame.py b/pandas/core/frame.py
--- 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
--- 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'


Test patch:
diff --git a/pandas/io/tests/test_excel.py b/pandas/io/tests/test_excel.py
--- 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