Repository: pandas-dev/pandas

Issue:
BLD: add xlwt py3 version to build matrix
apparently we are using the regular `xlwt` on the 3.3 build, which doesn't (it skips everything), so need 

still in dev on xlwt master branch



Discussion:
https://github.com/python-excel/xlwt/pull/32

cc @jmcnamara 

is this correct?

@jreback 
That is correct. Currently `xlwt` doesn't support Python 3. However, `xlrd`, `OpenPyXL` and `XlsxWriter` do (i,e., all the other Excel modules that Pandas uses).

Note, the `xlwt-future` PR has been there for a while and looks unlikely to be accepted without a lot of rework.

You could also drop `xlwt` support from the Pandas 3.x dependencies until it is Py3 compatible.


Resolving patch:
diff --git a/pandas/core/frame.py b/pandas/core/frame.py
--- a/pandas/core/frame.py
+++ b/pandas/core/frame.py
@@ -1246,6 +1246,9 @@ def to_excel(self, excel_writer, sheet_name='Sheet1', na_rep='',
         >>> df1.to_excel(writer,'Sheet1')
         >>> df2.to_excel(writer,'Sheet2')
         >>> writer.save()
+
+        For compatibility with to_csv, to_excel serializes lists and dicts to
+        strings before writing.
         """
         from pandas.io.excel import ExcelWriter
         if self.columns.nlevels > 1:
diff --git a/pandas/io/excel.py b/pandas/io/excel.py
--- a/pandas/io/excel.py
+++ b/pandas/io/excel.py
@@ -9,11 +9,13 @@
 import abc
 import numpy as np
 
+from pandas.core.frame import DataFrame
 from pandas.io.parsers import TextParser
 from pandas.io.common import _is_url, _urlopen
 from pandas.tseries.period import Period
 from pandas import json
-from pandas.compat import map, zip, reduce, range, lrange, u, add_metaclass
+from pandas.compat import (map, zip, reduce, range, lrange, u, add_metaclass,
+                           BytesIO, string_types)
 from pandas.core import config
 from pandas.core.common import pprint_thing
 import pandas.compat as compat
@@ -417,10 +419,13 @@ def _parse_cell(cell_contents,cell_typ):
                     if parse_cols is None or should_parse[j]:
                         row.append(_parse_cell(value,typ))
                 data.append(row)
-    
+
+            if sheet.nrows == 0:
+                return DataFrame()
+
             if header is not None:
                 data[header] = _trim_excel_header(data[header])
-    
+
             parser = TextParser(data, header=header, index_col=index_col,
                                 has_index_names=has_index_names,
                                 na_values=na_values,
@@ -474,6 +479,8 @@ def _conv_value(val):
         val = bool(val)
     elif isinstance(val, Period):
         val = "%s" % val
+    elif com.is_list_like(val):
+        val = str(val)
 
     return val
 
@@ -497,6 +504,11 @@ 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')
+
+    Notes
+    -----
+    For compatibility with CSV writers, ExcelWriter serializes lists
+    and dicts to strings before writing.
     """
     # Defining an ExcelWriter implementation (see abstract methods for more...)
 
@@ -521,9 +533,13 @@ class ExcelWriter(object):
     # ExcelWriter.
     def __new__(cls, path, engine=None, **kwargs):
         # only switch class if generic(ExcelWriter)
-        if cls == ExcelWriter:
+        if issubclass(cls, ExcelWriter):
             if engine is None:
-                ext = os.path.splitext(path)[-1][1:]
+                if isinstance(path, string_types):
+                    ext = os.path.splitext(path)[-1][1:]
+                else:
+                    ext = 'xlsx'
+
                 try:
                     engine = config.get_option('io.excel.%s.writer' % ext)
                 except KeyError:
@@ -574,7 +590,11 @@ def save(self):
     def __init__(self, path, engine=None,
                  date_format=None, datetime_format=None, **engine_kwargs):
         # validate that this engine can handle the extension
-        ext = os.path.splitext(path)[-1]
+        if isinstance(path, string_types):
+            ext = os.path.splitext(path)[-1]
+        else:
+            ext = 'xls' if engine == 'xlwt' else 'xlsx'
+
         self.check_extension(ext)
 
         self.path = path
@@ -1159,7 +1179,7 @@ class _XlwtWriter(ExcelWriter):
     def __init__(self, path, engine=None, encoding=None, **engine_kwargs):
         # Use the xlwt module as the Excel writer.
         import xlwt
-
+        engine_kwargs['engine'] = engine
         super(_XlwtWriter, self).__init__(path, **engine_kwargs)
 
         if encoding is None:
@@ -1311,6 +1331,8 @@ def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0):
         style_dict = {}
 
         for cell in cells:
+            val = _conv_value(cell.val)
+
             num_format_str = None
             if isinstance(cell.val, datetime.datetime):
                 num_format_str = self.datetime_format
@@ -1336,7 +1358,7 @@ def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0):
             else:
                 wks.write(startrow + cell.row,
                           startcol + cell.col,
-                          cell.val, style)
+                          val, style)
 
     def _convert_to_style(self, style_dict, num_format_str=None):
         """
diff --git a/vb_suite/packers.py b/vb_suite/packers.py
--- a/vb_suite/packers.py
+++ b/vb_suite/packers.py
@@ -7,6 +7,7 @@
 import os
 import pandas as pd
 from pandas.core import common as com
+from pandas.compat import BytesIO
 from random import randrange
 
 f = '__test__.msg'
@@ -206,3 +207,46 @@ def remove(f):
 packers_read_stata_with_validation = Benchmark("pd.read_stata(f)", setup, start_date=start_date)
 
 packers_write_stata_with_validation = Benchmark("df.to_stata(f, {'index': 'tc'})", setup, cleanup="remove(f)", start_date=start_date)
+
+#----------------------------------------------------------------------
+# Excel - alternative writers
+setup = common_setup + """
+bio = BytesIO()
+"""
+
+excel_writer_bench = """
+bio.seek(0)
+writer = pd.io.excel.ExcelWriter(bio, engine='{engine}')
+df[:2000].to_excel(writer)
+writer.save()
+"""
+
+benchmark_xlsxwriter = excel_writer_bench.format(engine='xlsxwriter')
+
+packers_write_excel_xlsxwriter = Benchmark(benchmark_xlsxwriter, setup)
+
+benchmark_openpyxl = excel_writer_bench.format(engine='openpyxl')
+
+packers_write_excel_openpyxl = Benchmark(benchmark_openpyxl, setup)
+
+benchmark_xlwt = excel_writer_bench.format(engine='xlwt')
+
+packers_write_excel_xlwt = Benchmark(benchmark_xlwt, setup)
+
+
+#----------------------------------------------------------------------
+# Excel - reader
+
+setup = common_setup + """
+bio = BytesIO()
+writer = pd.io.excel.ExcelWriter(bio, engine='xlsxwriter')
+df[:2000].to_excel(writer)
+writer.save()
+"""
+
+benchmark_read_excel="""
+bio.seek(0)
+pd.read_excel(bio)
+"""
+
+packers_read_excel = Benchmark(benchmark_read_excel, setup)


Test patch:
diff --git a/pandas/io/tests/data/blank.xls b/pandas/io/tests/data/blank.xls
new file mode 100755
Binary files /dev/null and b/pandas/io/tests/data/blank.xls differ
diff --git a/pandas/io/tests/data/blank.xlsx b/pandas/io/tests/data/blank.xlsx
new file mode 100755
Binary files /dev/null and b/pandas/io/tests/data/blank.xlsx differ
diff --git a/pandas/io/tests/data/blank_with_header.xls b/pandas/io/tests/data/blank_with_header.xls
new file mode 100755
Binary files /dev/null and b/pandas/io/tests/data/blank_with_header.xls differ
diff --git a/pandas/io/tests/data/blank_with_header.xlsx b/pandas/io/tests/data/blank_with_header.xlsx
new file mode 100755
Binary files /dev/null and b/pandas/io/tests/data/blank_with_header.xlsx differ
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
@@ -1,6 +1,6 @@
 # pylint: disable=E1101
 
-from pandas.compat import u, range, map, openpyxl_compat
+from pandas.compat import u, range, map, openpyxl_compat, BytesIO, iteritems
 from datetime import datetime, date, time
 import sys
 import os
@@ -455,7 +455,7 @@ def test_reading_multiple_specific_sheets(self):
     def test_creating_and_reading_multiple_sheets(self):
         # Test reading multiple sheets, from a runtime created excel file
         # with multiple sheets.
-         # See PR #9450       
+        # See PR #9450
     
         _skip_if_no_xlrd()
         _skip_if_no_xlwt()
@@ -471,7 +471,7 @@ def tdf(sheetname):
   
         with ensure_clean('.xlsx') as pth:
             with ExcelWriter(pth) as ew:
-                for sheetname, df in dfs.iteritems():
+                for sheetname, df in iteritems(dfs):
                     df.to_excel(ew,sheetname)
             dfs_returned = pd.read_excel(pth,sheetname=sheets)
             for s in sheets:
@@ -520,6 +520,29 @@ def test_reader_seconds(self):
         actual = read_excel(epoch_1904, 'Sheet1')
         tm.assert_frame_equal(actual, expected)
 
+    # GH6403
+    def test_read_excel_blank(self):
+        _skip_if_no_xlrd()
+
+        blank = os.path.join(self.dirpath, 'blank.xls')
+        actual = read_excel(blank, 'Sheet1')
+        tm.assert_frame_equal(actual, DataFrame())
+
+        blank = os.path.join(self.dirpath, 'blank.xlsx')
+        actual = read_excel(blank, 'Sheet1')
+        tm.assert_frame_equal(actual, DataFrame())
+
+    def test_read_excel_blank_with_header(self):
+        _skip_if_no_xlrd()
+
+        expected = DataFrame(columns=['col_1', 'col_2'])
+        blank = os.path.join(self.dirpath, 'blank_with_header.xls')
+        actual = read_excel(blank, 'Sheet1')
+        tm.assert_frame_equal(actual, expected)
+
+        blank = os.path.join(self.dirpath, 'blank_with_header.xlsx')
+        actual = read_excel(blank, 'Sheet1')
+        tm.assert_frame_equal(actual, expected)
 
 class ExcelWriterBase(SharedItems):
     # Base class for test cases to run with different Excel writers.
@@ -1218,6 +1241,30 @@ def test_datetimes(self):
 
             tm.assert_series_equal(write_frame['A'], read_frame['A'])
 
+    # GH7074
+    def test_bytes_io(self):
+        bio = BytesIO()
+        df = DataFrame(np.random.randn(10, 2))
+        writer = ExcelWriter(bio)
+        df.to_excel(writer)
+        writer.save()
+        bio.seek(0)
+        reread_df = pd.read_excel(bio)
+        tm.assert_frame_equal(df, reread_df)
+
+    # GH8188
+    def test_write_lists_dict(self):
+        df = pd.DataFrame({'mixed': ['a', ['b', 'c'], {'d': 'e', 'f': 2}],
+                           'numeric': [1, 2, 3.0],
+                           'str': ['apple', 'banana', 'cherry']})
+        expected = df.copy()
+        expected.mixed = expected.mixed.apply(str)
+        expected.numeric = expected.numeric.astype('int64')
+        with ensure_clean(self.ext) as path:
+            df.to_excel(path, 'Sheet1')
+            read = read_excel(path, 'Sheet1', header=0)
+            tm.assert_frame_equal(read, expected)
+
 def raise_wrapper(major_ver):
     def versioned_raise_wrapper(orig_method):
         @functools.wraps(orig_method)
@@ -1512,6 +1559,7 @@ class XlsxWriterTests_NoMerge(ExcelWriterBase, tm.TestCase):
 
 
 class ExcelWriterEngineTests(tm.TestCase):
+
     def test_ExcelWriter_dispatch(self):
         with tm.assertRaisesRegexp(ValueError, 'No engine'):
             ExcelWriter('nothing')