From c0198b40a9b58eebf80ab5c5b0f332eefcc938fc Mon Sep 17 00:00:00 2001 From: Jeffrey Tratner Date: Thu, 5 Sep 2013 21:37:29 -0400 Subject: [PATCH 1/2] ENH: Add 'add_metaclass' class decorator. Taken from a new addition to the six library (again, license is in the LICENSE directory). --- pandas/compat/__init__.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/pandas/compat/__init__.py b/pandas/compat/__init__.py index 494cc5fe9ad29..1b5939eb98417 100644 --- a/pandas/compat/__init__.py +++ b/pandas/compat/__init__.py @@ -17,6 +17,8 @@ * binary_type: str in Python 2, bythes in Python 3 * string_types: basestring in Python 2, str in Python 3 * bind_method: binds functions to classes +* add_metaclass(metaclass) - class decorator that recreates class with with the + given metaclass instead (and avoids intermediary class creation) Python 2.6 compatibility: * OrderedDict @@ -34,7 +36,6 @@ import types PY3 = (sys.version_info[0] >= 3) -# import iterator versions of these functions try: import __builtin__ as builtins @@ -96,6 +97,7 @@ def str_to_bytes(s, encoding='ascii'): def bytes_to_str(b, encoding='ascii'): return b + # import iterator versions of these functions range = xrange zip = itertools.izip filter = itertools.ifilter @@ -196,6 +198,19 @@ def u(s): def callable(obj): return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) + +def add_metaclass(metaclass): + """Class decorator for creating a class with a metaclass.""" + def wrapper(cls): + orig_vars = cls.__dict__.copy() + orig_vars.pop('__dict__', None) + orig_vars.pop('__weakref__', None) + for slots_var in orig_vars.get('__slots__', ()): + orig_vars.pop(slots_var) + return metaclass(cls.__name__, cls.__bases__, orig_vars) + return wrapper + + # ---------------------------------------------------------------------------- # Python 2.6 compatibility shims # From 42e06a78e6639ed5a73035d3890b9c051bf08d1d Mon Sep 17 00:00:00 2001 From: Jeffrey Tratner Date: Wed, 4 Sep 2013 08:09:47 -0400 Subject: [PATCH 2/2] CLN: Make ExcelWriter more pluggable Make ExcelWriter an ABC and add ExcelWriter config to core/config_init ENH: Allow Panel.to_excel to pass keyword arguments --- doc/source/io.rst | 18 ++ doc/source/release.rst | 8 + pandas/core/config.py | 2 +- pandas/core/config_init.py | 21 ++ pandas/core/format.py | 11 +- pandas/core/frame.py | 8 +- pandas/core/panel.py | 39 +++- pandas/io/excel.py | 375 +++++++++++++++++++++++----------- pandas/io/tests/test_excel.py | 72 ++++++- 9 files changed, 415 insertions(+), 139 deletions(-) diff --git a/doc/source/io.rst b/doc/source/io.rst index da611c0375789..9fd2c167fa605 100644 --- a/doc/source/io.rst +++ b/doc/source/io.rst @@ -1681,6 +1681,24 @@ one can use the ExcelWriter class, as in the following example: df2.to_excel(writer, sheet_name='sheet2') writer.save() +.. _io.excel.writers: + +Excel writer engines +~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 0.13 + +``pandas`` chooses an Excel writer via two methods: + +1. the ``engine`` keyword argument +2. the filename extension (via the default specified in config options) + +``pandas`` only supports ``openpyxl`` for ``.xlsx`` and ``.xlsm`` files and +``xlwt`` for ``.xls`` files. If you have multiple engines installed, you can choose the +engine to use by default via the options ``io.excel.xlsx.writer`` and +``io.excel.xls.writer``. + + .. _io.hdf5: HDF5 (PyTables) diff --git a/doc/source/release.rst b/doc/source/release.rst index 101ec290a58cf..c80ddd01cdf07 100644 --- a/doc/source/release.rst +++ b/doc/source/release.rst @@ -106,6 +106,13 @@ Improvements to existing features - Add ``axis`` and ``level`` keywords to ``where``, so that the ``other`` argument can now be an alignable pandas object. - ``to_datetime`` with a format of '%Y%m%d' now parses much faster + - It's now easier to hook new Excel writers into pandas (just subclass + ``ExcelWriter`` and register your engine). You can specify an ``engine`` in + ``to_excel`` or in ``ExcelWriter``. You can also specify which writers you + want to use by default with config options ``io.excel.xlsx.writer`` and + ``io.excel.xls.writer``. (:issue:`4745`, :issue:`4750`) + - ``Panel.to_excel()`` now accepts keyword arguments that will be passed to + its ``DataFrame``'s ``to_excel()`` methods. (:issue:`4750`) API Changes ~~~~~~~~~~~ @@ -194,6 +201,7 @@ API Changes - default for ``tupleize_cols`` is now ``False`` for both ``to_csv`` and ``read_csv``. Fair warning in 0.12 (:issue:`3604`) - moved timedeltas support to pandas.tseries.timedeltas.py; add timedeltas string parsing, add top-level ``to_timedelta`` function + - ``NDFrame`` now is compatible with Python's toplevel ``abs()`` function (:issue:`4821`). Internal Refactoring ~~~~~~~~~~~~~~~~~~~~ diff --git a/pandas/core/config.py b/pandas/core/config.py index a14e8afa21322..f81958a0e58fc 100644 --- a/pandas/core/config.py +++ b/pandas/core/config.py @@ -73,7 +73,7 @@ def _get_single_key(pat, silent): if len(keys) == 0: if not silent: _warn_if_deprecated(pat) - raise KeyError('No such keys(s)') + raise KeyError('No such keys(s): %r' % pat) if len(keys) > 1: raise KeyError('Pattern matched multiple keys') key = keys[0] diff --git a/pandas/core/config_init.py b/pandas/core/config_init.py index 6119de5af19e0..797e979963ae2 100644 --- a/pandas/core/config_init.py +++ b/pandas/core/config_init.py @@ -279,3 +279,24 @@ def use_inf_as_null_cb(key): with cf.config_prefix('mode'): cf.register_option('use_inf_as_null', False, use_inf_as_null_doc, cb=use_inf_as_null_cb) + + +# Set up the io.excel specific configuration. +writer_engine_doc = """ +: string + The default Excel writer engine for '{ext}' files. Available options: '{default}' (the default){others}. +""" + +with cf.config_prefix('io.excel'): + # going forward, will be additional writers + for ext, options in [('xls', ['xlwt']), + ('xlsm', ['openpyxl']), + ('xlsx', ['openpyxl'])]: + default = options.pop(0) + if options: + options = " " + ", ".join(options) + else: + options = "" + doc = writer_engine_doc.format(ext=ext, default=default, + others=options) + cf.register_option(ext + '.writer', default, doc, validator=str) diff --git a/pandas/core/format.py b/pandas/core/format.py index 92fcfaa5f2f9c..28ff12a6e51c4 100644 --- a/pandas/core/format.py +++ b/pandas/core/format.py @@ -1146,15 +1146,8 @@ class ExcelFormatter(object): sequence should be given if the DataFrame uses MultiIndex. """ - def __init__(self, - df, - na_rep='', - float_format=None, - cols=None, - header=True, - index=True, - index_label=None - ): + def __init__(self, df, na_rep='', float_format=None, cols=None, + header=True, index=True, index_label=None): self.df = df self.rowcounter = 0 self.na_rep = na_rep diff --git a/pandas/core/frame.py b/pandas/core/frame.py index 2b0e18c0c5524..bd601c5c8408e 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -1354,7 +1354,7 @@ def to_csv(self, path_or_buf, sep=",", na_rep='', float_format=None, def to_excel(self, excel_writer, sheet_name='sheet1', na_rep='', float_format=None, cols=None, header=True, index=True, - index_label=None, startrow=0, startcol=0): + index_label=None, startrow=0, startcol=0, engine=None): """ Write DataFrame to a excel sheet @@ -1381,6 +1381,10 @@ def to_excel(self, excel_writer, sheet_name='sheet1', na_rep='', sequence should be given if the DataFrame uses MultiIndex. startow : upper left cell row to dump data frame startcol : upper left cell column to dump data frame + engine : string, default None + write engine to use - you can also set this via the options + ``io.excel.xlsx.writer``, ``io.excel.xls.writer``, and + ``io.excel.xlsm.writer``. Notes @@ -1396,7 +1400,7 @@ def to_excel(self, excel_writer, sheet_name='sheet1', na_rep='', from pandas.io.excel import ExcelWriter need_save = False if isinstance(excel_writer, compat.string_types): - excel_writer = ExcelWriter(excel_writer) + excel_writer = ExcelWriter(excel_writer, engine=engine) need_save = True formatter = fmt.ExcelFormatter(self, diff --git a/pandas/core/panel.py b/pandas/core/panel.py index bca6f985ac689..6f02b49326e4d 100644 --- a/pandas/core/panel.py +++ b/pandas/core/panel.py @@ -458,22 +458,53 @@ def to_sparse(self, fill_value=None, kind='block'): default_kind=kind, default_fill_value=fill_value) - def to_excel(self, path, na_rep=''): + def to_excel(self, path, na_rep='', engine=None, **kwargs): """ Write each DataFrame in Panel to a separate excel sheet Parameters ---------- - excel_writer : string or ExcelWriter object + path : string or ExcelWriter object File path or existing ExcelWriter na_rep : string, default '' Missing data representation + engine : string, default None + write engine to use - you can also set this via the options + ``io.excel.xlsx.writer``, ``io.excel.xls.writer``, and + ``io.excel.xlsm.writer``. + + Keyword Arguments + ----------------- + float_format : string, default None + Format string for floating point numbers + cols : sequence, optional + Columns to write + header : boolean or list of string, default True + Write out column names. If a list of string is given it is + assumed to be aliases for the column names + index : boolean, default True + Write row names (index) + index_label : string or sequence, default None + Column label for index column(s) if desired. If None is given, and + `header` and `index` are True, then the index names are used. A + sequence should be given if the DataFrame uses MultiIndex. + startow : upper left cell row to dump data frame + startcol : upper left cell column to dump data frame + + Keyword arguments (and na_rep) are passed to the ``to_excel`` method + for each DataFrame written. """ from pandas.io.excel import ExcelWriter - writer = ExcelWriter(path) + + if isinstance(path, compat.string_types): + writer = ExcelWriter(path, engine=engine) + else: + writer = path + kwargs['na_rep'] = na_rep + for item, df in compat.iteritems(self): name = str(item) - df.to_excel(writer, name, na_rep=na_rep) + df.to_excel(writer, name, **kwargs) writer.save() def as_matrix(self): diff --git a/pandas/io/excel.py b/pandas/io/excel.py index 5ff42c5cd12a6..f34c4f99a856d 100644 --- a/pandas/io/excel.py +++ b/pandas/io/excel.py @@ -4,17 +4,46 @@ #---------------------------------------------------------------------- # ExcelFile class - +import os import datetime +import abc import numpy as np from pandas.io.parsers import TextParser from pandas.tseries.period import Period from pandas import json -from pandas.compat import map, zip, reduce, range, lrange +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 import pandas.compat as compat from warnings import warn +__all__ = ["read_excel", "ExcelWriter", "ExcelFile"] + +_writer_extensions = ["xlsx", "xls", "xlsm"] +_writers = {} + +def register_writer(klass): + """Adds engine to the excel writer registry. You must use this method to + integrate with ``to_excel``. Also adds config options for any new + ``supported_extensions`` defined on the writer.""" + if not compat.callable(klass): + raise ValueError("Can only register callables as engines") + engine_name = klass.engine + _writers[engine_name] = klass + for ext in klass.supported_extensions: + if ext.startswith('.'): + ext = ext[1:] + if ext not in _writer_extensions: + config.register_option("io.excel.%s.writer" % ext, + engine_name, validator=str) + _writer_extensions.append(ext) + +def get_writer(engine_name): + try: + return _writers[engine_name] + except KeyError: + raise ValueError("No Excel writer '%s'" % engine_name) def read_excel(path_or_buf, sheetname, **kwds): """Read an Excel table into a pandas DataFrame @@ -240,85 +269,6 @@ def _trim_excel_header(row): return row -class CellStyleConverter(object): - """ - Utility Class which converts a style dict to xlrd or openpyxl style - """ - - @staticmethod - def to_xls(style_dict, num_format_str=None): - """ - converts a style_dict to an xlwt style object - Parameters - ---------- - style_dict: style dictionary to convert - """ - import xlwt - - def style_to_xlwt(item, firstlevel=True, field_sep=',', line_sep=';'): - """helper wich recursively generate an xlwt easy style string - for example: - - hstyle = {"font": {"bold": True}, - "border": {"top": "thin", - "right": "thin", - "bottom": "thin", - "left": "thin"}, - "align": {"horiz": "center"}} - will be converted to - font: bold on; \ - border: top thin, right thin, bottom thin, left thin; \ - align: horiz center; - """ - if hasattr(item, 'items'): - if firstlevel: - it = ["%s: %s" % (key, style_to_xlwt(value, False)) - for key, value in item.items()] - out = "%s " % (line_sep).join(it) - return out - else: - it = ["%s %s" % (key, style_to_xlwt(value, False)) - for key, value in item.items()] - out = "%s " % (field_sep).join(it) - return out - else: - item = "%s" % item - item = item.replace("True", "on") - item = item.replace("False", "off") - return item - - if style_dict: - xlwt_stylestr = style_to_xlwt(style_dict) - style = xlwt.easyxf(xlwt_stylestr, field_sep=',', line_sep=';') - else: - style = xlwt.XFStyle() - if num_format_str is not None: - style.num_format_str = num_format_str - - return style - - @staticmethod - def to_xlsx(style_dict): - """ - converts a style_dict to an openpyxl style object - Parameters - ---------- - style_dict: style dictionary to convert - """ - - from openpyxl.style import Style - xls_style = Style() - for key, value in style_dict.items(): - for nk, nv in value.items(): - if key == "borders": - (xls_style.borders.__getattribute__(nk) - .__setattr__('border_style', nv)) - else: - xls_style.__getattribute__(key).__setattr__(nk, nv) - - return xls_style - - def _conv_value(val): # convert value for excel dump if isinstance(val, np.int64): @@ -331,41 +281,78 @@ def _conv_value(val): return val +class ExcelWriterMeta(abc.ABCMeta): + """ + Metaclass that dynamically chooses the ExcelWriter to use. + + If you directly instantiate a subclass, it skips the engine lookup. + + Defining an ExcelWriter implementation (see abstract methods on ExcelWriter for more...). + + - Mandatory (but not checked at run time): + - ``write_cells(self, cells, sheet_name=None, startrow=0, startcol=0)`` + --> called to write additional DataFrames to disk + - ``supported_extensions`` (tuple of supported extensions), used to check + that engine supports the given extension. + - ``engine`` - string that gives the engine name. Necessary to + instantiate class directly and bypass ``ExcelWriterMeta`` engine lookup. + - ``save(self)`` --> called to save file to disk + - Optional: + - ``__init__(self, path, **kwargs)`` --> always called with path as first + argument. + + You also need to register the class with ``register_writer()``. + """ + + def __call__(cls, path, **kwargs): + engine = kwargs.pop('engine', None) + # if it's not an ExcelWriter baseclass, dont' do anything (you've + # probably made an explicit choice here) + if not isinstance(getattr(cls, 'engine', None), compat.string_types): + if engine is None: + ext = os.path.splitext(path)[-1][1:] + try: + engine = config.get_option('io.excel.%s.writer' % ext) + except KeyError: + error = ValueError("No engine for filetype: '%s'" % ext) + raise error + cls = get_writer(engine) + writer = cls.__new__(cls, path, **kwargs) + writer.__init__(path, **kwargs) + return writer + + +@add_metaclass(ExcelWriterMeta) class ExcelWriter(object): """ - Class for writing DataFrame objects into excel sheets, uses xlwt for xls, - openpyxl for xlsx. See DataFrame.to_excel for typical usage. + Class for writing DataFrame objects into excel sheets, default is to use + xlwt for xls, openpyxl for xlsx. See DataFrame.to_excel for typical usage. Parameters ---------- path : string - Path to xls file + Path to xls or xlsx file. + engine : string (optional) + Engine to use for writing. If None, defaults to ``io.excel..writer``. + NOTE: can only be passed as a keyword argument. """ - def __init__(self, path): - self.use_xlsx = True - if path.endswith('.xls'): - self.use_xlsx = False - import xlwt - self.book = xlwt.Workbook() - self.fm_datetime = xlwt.easyxf( - num_format_str='YYYY-MM-DD HH:MM:SS') - 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]) - self.path = path - self.sheets = {} - self.cur_sheet = None + # declare external properties you can count on + book = None + curr_sheet = None + path = None - def save(self): - """ - Save workbook to disk - """ - self.book.save(self.path) + @abc.abstractproperty + def supported_extensions(self): + "extensions that writer engine supports" + pass + + @abc.abstractproperty + def engine(self): + "name of engine" + pass + + @abc.abstractmethod def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0): """ Write given formated cells into Excel an excel sheet @@ -379,20 +366,77 @@ def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0): startrow: upper left cell row to dump data frame startcol: upper left cell column to dump data frame """ + pass + + @abc.abstractmethod + def save(self): + """ + Save workbook to disk. + """ + pass + + def __init__(self, path, engine=None, **engine_kwargs): + # note that subclasses will *never* get anything for engine + # included here so that it's visible as part of the public signature. + + # validate that this engine can handle the extnesion + ext = os.path.splitext(path)[-1] + self.check_extension(ext) + + self.path = path + self.sheets = {} + self.cur_sheet = None + + def _get_sheet_name(self, sheet_name): if sheet_name is None: sheet_name = self.cur_sheet if sheet_name is None: # pragma: no cover raise ValueError('Must pass explicit sheet_name or set ' 'cur_sheet property') - if self.use_xlsx: - self._writecells_xlsx(cells, sheet_name, startrow, startcol) + return sheet_name + + @classmethod + def check_extension(cls, ext): + """checks that path's extension against the Writer's supported + extensions. If it isn't supported, raises UnsupportedFiletypeError.""" + if ext.startswith('.'): + ext = ext[1:] + if not any(ext in extension for extension in cls.supported_extensions): + msg = (u("Invalid extension for engine '%s': '%s'") % + (pprint_thing(cls.engine), pprint_thing(ext))) + raise ValueError(msg) else: - self._writecells_xls(cells, sheet_name, startrow, startcol) + return True + + +class _OpenpyxlWriter(ExcelWriter): + engine = 'openpyxl' + supported_extensions = ('.xlsx', '.xlsm') - def _writecells_xlsx(self, cells, sheet_name, startrow, startcol): + def __init__(self, path, **engine_kwargs): + # Use the openpyxl module as the Excel writer. + from openpyxl.workbook import Workbook + super(_OpenpyxlWriter, self).__init__(path, **engine_kwargs) + + # 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: + self.book.remove_sheet(self.book.worksheets[0]) + + def save(self): + """ + Save workbook to disk. + """ + return self.book.save(self.path) + + def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0): + # Write the frame cells using openpyxl. from openpyxl.cell import get_column_letter + sheet_name = self._get_sheet_name(sheet_name) + if sheet_name in self.sheets: wks = self.sheets[sheet_name] else: @@ -405,7 +449,7 @@ def _writecells_xlsx(self, cells, sheet_name, startrow, startcol): xcell = wks.cell("%s%s" % (colletter, startrow + cell.row + 1)) xcell.value = _conv_value(cell.val) if cell.style: - style = CellStyleConverter.to_xlsx(cell.style) + style = self._convert_to_style(cell.style) for field in style.__fields__: xcell.style.__setattr__(field, style.__getattribute__(field)) @@ -425,8 +469,55 @@ def _writecells_xlsx(self, cells, sheet_name, startrow, startcol): startrow + cell.row + 1, cletterend, startrow + cell.mergestart + 1)) + @classmethod + def _convert_to_style(cls, style_dict): + """ + converts a style_dict to an openpyxl style object + Parameters + ---------- + style_dict: style dictionary to convert + """ + + from openpyxl.style import Style + xls_style = Style() + for key, value in style_dict.items(): + for nk, nv in value.items(): + if key == "borders": + (xls_style.borders.__getattribute__(nk) + .__setattr__('border_style', nv)) + else: + xls_style.__getattribute__(key).__setattr__(nk, nv) + + return xls_style + +register_writer(_OpenpyxlWriter) + + +class _XlwtWriter(ExcelWriter): + engine = 'xlwt' + supported_extensions = ('.xls',) + + def __init__(self, path, **engine_kwargs): + # Use the xlwt module as the Excel writer. + import xlwt + + super(_XlwtWriter, self).__init__(path, **engine_kwargs) + + self.book = xlwt.Workbook() + self.fm_datetime = xlwt.easyxf(num_format_str='YYYY-MM-DD HH:MM:SS') + self.fm_date = xlwt.easyxf(num_format_str='YYYY-MM-DD') + + def save(self): + """ + Save workbook to disk. + """ + return self.book.save(self.path) + + def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0): + # Write the frame cells using xlwt. + + sheet_name = self._get_sheet_name(sheet_name) - def _writecells_xls(self, cells, sheet_name, startrow, startcol): if sheet_name in self.sheets: wks = self.sheets[sheet_name] else: @@ -451,7 +542,7 @@ def _writecells_xls(self, cells, sheet_name, startrow, startcol): if stylekey in style_dict: style = style_dict[stylekey] else: - style = CellStyleConverter.to_xls(cell.style, num_format_str) + style = self._convert_to_style(cell.style, num_format_str) style_dict[stylekey] = style if cell.mergestart is not None and cell.mergeend is not None: @@ -464,3 +555,59 @@ def _writecells_xls(self, cells, sheet_name, startrow, startcol): wks.write(startrow + cell.row, startcol + cell.col, val, style) + + @classmethod + def _style_to_xlwt(cls, item, firstlevel=True, field_sep=',', line_sep=';'): + """helper which recursively generate an xlwt easy style string + for example: + + hstyle = {"font": {"bold": True}, + "border": {"top": "thin", + "right": "thin", + "bottom": "thin", + "left": "thin"}, + "align": {"horiz": "center"}} + will be converted to + font: bold on; \ + border: top thin, right thin, bottom thin, left thin; \ + align: horiz center; + """ + if hasattr(item, 'items'): + if firstlevel: + it = ["%s: %s" % (key, cls._style_to_xlwt(value, False)) + for key, value in item.items()] + out = "%s " % (line_sep).join(it) + return out + else: + it = ["%s %s" % (key, cls._style_to_xlwt(value, False)) + for key, value in item.items()] + out = "%s " % (field_sep).join(it) + return out + else: + item = "%s" % item + item = item.replace("True", "on") + item = item.replace("False", "off") + return item + + @classmethod + def _convert_to_style(cls, style_dict, num_format_str=None): + """ + converts a style_dict to an xlwt style object + Parameters + ---------- + style_dict: style dictionary to convert + """ + import xlwt + + if style_dict: + xlwt_stylestr = cls._style_to_xlwt(style_dict) + style = xlwt.easyxf(xlwt_stylestr, field_sep=',', line_sep=';') + else: + style = xlwt.XFStyle() + if num_format_str is not None: + style.num_format_str = num_format_str + + return style + +register_writer(_XlwtWriter) + diff --git a/pandas/io/tests/test_excel.py b/pandas/io/tests/test_excel.py index 3f41be6ae64c6..a9822ea0b46c9 100644 --- a/pandas/io/tests/test_excel.py +++ b/pandas/io/tests/test_excel.py @@ -18,7 +18,10 @@ import pandas.io.parsers as parsers from pandas.io.parsers import (read_csv, read_table, read_fwf, TextParser, TextFileReader) -from pandas.io.excel import ExcelFile, ExcelWriter, read_excel +from pandas.io.excel import ( + ExcelFile, ExcelWriter, read_excel, _XlwtWriter, _OpenpyxlWriter, + register_writer +) from pandas.util.testing import (assert_almost_equal, assert_series_equal, network, @@ -602,6 +605,59 @@ def test_excel_roundtrip_datetime(self): recons = reader.parse('test1') tm.assert_frame_equal(self.tsframe, recons) + def test_ExcelWriter_dispatch(self): + with tm.assertRaisesRegexp(ValueError, 'No engine'): + writer = ExcelWriter('nothing') + + _skip_if_no_openpyxl() + writer = ExcelWriter('apple.xlsx') + tm.assert_isinstance(writer, _OpenpyxlWriter) + + _skip_if_no_xlwt() + writer = ExcelWriter('apple.xls') + tm.assert_isinstance(writer, _XlwtWriter) + + + def test_register_writer(self): + # some awkward mocking to test out dispatch and such actually works + called_save = [] + called_write_cells = [] + class DummyClass(ExcelWriter): + called_save = False + called_write_cells = False + supported_extensions = ['test', 'xlsx', 'xls'] + engine = 'dummy' + + def save(self): + called_save.append(True) + + def write_cells(self, *args, **kwargs): + called_write_cells.append(True) + + def check_called(func): + func() + self.assert_(len(called_save) >= 1) + self.assert_(len(called_write_cells) >= 1) + del called_save[:] + del called_write_cells[:] + + register_writer(DummyClass) + writer = ExcelWriter('something.test') + tm.assert_isinstance(writer, DummyClass) + df = tm.makeCustomDataframe(1, 1) + panel = tm.makePanel() + func = lambda: df.to_excel('something.test') + check_called(func) + check_called(lambda: panel.to_excel('something.test')) + from pandas import set_option, get_option + val = get_option('io.excel.xlsx.writer') + set_option('io.excel.xlsx.writer', 'dummy') + check_called(lambda: df.to_excel('something.xlsx')) + check_called(lambda: df.to_excel('something.xls', engine='dummy')) + set_option('io.excel.xlsx.writer', val) + + + def test_to_excel_periodindex(self): _skip_if_no_excelsuite() @@ -731,13 +787,11 @@ def test_to_excel_unicode_filename(self): tm.assert_frame_equal(rs, xp) def test_to_excel_styleconverter(self): - from pandas.io.excel import CellStyleConverter + _skip_if_no_xlwt() + _skip_if_no_openpyxl() - try: - import xlwt - import openpyxl - except ImportError: - raise nose.SkipTest + import xlwt + import openpyxl hstyle = {"font": {"bold": True}, "borders": {"top": "thin", @@ -745,7 +799,7 @@ def test_to_excel_styleconverter(self): "bottom": "thin", "left": "thin"}, "alignment": {"horizontal": "center"}} - xls_style = CellStyleConverter.to_xls(hstyle) + xls_style = _XlwtWriter._convert_to_style(hstyle) self.assertTrue(xls_style.font.bold) self.assertEquals(xlwt.Borders.THIN, xls_style.borders.top) self.assertEquals(xlwt.Borders.THIN, xls_style.borders.right) @@ -753,7 +807,7 @@ def test_to_excel_styleconverter(self): self.assertEquals(xlwt.Borders.THIN, xls_style.borders.left) self.assertEquals(xlwt.Alignment.HORZ_CENTER, xls_style.alignment.horz) - xlsx_style = CellStyleConverter.to_xlsx(hstyle) + xlsx_style = _OpenpyxlWriter._convert_to_style(hstyle) self.assertTrue(xlsx_style.font.bold) self.assertEquals(openpyxl.style.Border.BORDER_THIN, xlsx_style.borders.top.border_style)