From 96ab25983275cfcac8e290f90b3008c9d49ee650 Mon Sep 17 00:00:00 2001 From: Joel Nothman Date: Thu, 27 Apr 2017 02:18:13 +1000 Subject: [PATCH 1/7] Support more styles for xlsxwriter --- doc/source/style.ipynb | 2 +- doc/source/whatsnew/v0.20.0.txt | 2 +- pandas/io/excel.py | 134 ++++++++++++++++++++----- pandas/tests/io/test_excel.py | 173 +++++++++++++++++--------------- 4 files changed, 205 insertions(+), 106 deletions(-) diff --git a/doc/source/style.ipynb b/doc/source/style.ipynb index 2cacbb19d81bb..4958445c3fe26 100644 --- a/doc/source/style.ipynb +++ b/doc/source/style.ipynb @@ -918,7 +918,7 @@ "\n", "*Experimental: This is a new feature and still under development. We'll be adding features and possibly making breaking changes in future releases. We'd love to hear your feedback.*\n", "\n", - "Some support is available for exporting styled `DataFrames` to Excel worksheets using the `OpenPyXL` engine. CSS2.2 properties handled include:\n", + "Some support is available for exporting styled `DataFrames` to Excel worksheets using the `OpenPyXL` or `XlsxWriter` engines. CSS2.2 properties handled include:\n", "\n", "- `background-color`\n", "- `border-style`, `border-width`, `border-color` and their {`top`, `right`, `bottom`, `left` variants}\n", diff --git a/doc/source/whatsnew/v0.20.0.txt b/doc/source/whatsnew/v0.20.0.txt index 0b66b90afec67..2f38355dfc847 100644 --- a/doc/source/whatsnew/v0.20.0.txt +++ b/doc/source/whatsnew/v0.20.0.txt @@ -368,7 +368,7 @@ To convert a ``SparseDataFrame`` back to sparse SciPy matrix in COO format, you Excel output for styled DataFrames ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Experimental support has been added to export ``DataFrame.style`` formats to Excel using the ``openpyxl`` engine. (:issue:`15530`) +Experimental support has been added to export ``DataFrame.style`` formats to Excel using the ``openpyxl`` or ``xlsxwriter`` engines. (:issue:`15530`, :issue:`16149`) For example, after running the following, ``styled.xlsx`` renders as below: diff --git a/pandas/io/excel.py b/pandas/io/excel.py index fbb10ebdfc56d..56b0d6d2da18c 100644 --- a/pandas/io/excel.py +++ b/pandas/io/excel.py @@ -1596,6 +1596,68 @@ def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0, startcol + cell.col, val, style) + # Map from openpyxl-oriented styles to flatter xlsxwriter representation + STYLE_MAPPING = [ + (('font', 'name'), 'font_name'), + (('font', 'sz'), 'font_size'), + (('font', 'size'), 'font_size'), + (('font', 'color', 'rgb'), 'font_color'), + (('font', 'color'), 'font_color'), + (('font', 'b'), 'bold'), + (('font', 'bold'), 'bold'), + (('font', 'i'), 'italic'), + (('font', 'italic'), 'italic'), + (('font', 'u'), 'underline'), + (('font', 'underline'), 'underline'), + (('font', 'strike'), 'font_strikeout'), + (('font', 'vertAlign'), 'font_script'), + (('font', 'vertalign'), 'font_script'), + (('number_format', 'format_code'), 'num_format'), + (('number_format',), 'num_format'), + (('protection', 'locked'), 'locked'), + (('protection', 'hidden'), 'hidden'), + (('alignment', 'horizontal'), 'align'), + (('alignment', 'vertical'), 'valign'), + (('alignment', 'text_rotation'), 'rotation'), + (('alignment', 'wrap_text'), 'text_wrap'), + (('alignment', 'indent'), 'indent'), + (('alignment', 'shrink_to_fit'), 'shrink'), + (('fill', 'patternType'), 'pattern'), + (('fill', 'patterntype'), 'pattern'), + (('fill', 'fill_type'), 'pattern'), + (('fill', 'start_color', 'rgb'), 'fg_color'), + (('fill', 'fgColor', 'rgb'), 'fg_color'), + (('fill', 'fgcolor', 'rgb'), 'fg_color'), + (('fill', 'start_color'), 'fg_color'), + (('fill', 'fgColor'), 'fg_color'), + (('fill', 'fgcolor'), 'fg_color'), + (('fill', 'end_color', 'rgb'), 'bg_color'), + (('fill', 'bgColor', 'rgb'), 'bg_color'), + (('fill', 'bgcolor', 'rgb'), 'bg_color'), + (('fill', 'end_color'), 'bg_color'), + (('fill', 'bgColor'), 'bg_color'), + (('fill', 'bgcolor'), 'bg_color'), + (('border', 'color', 'rgb'), 'border_color'), + (('border', 'color'), 'border_color'), + (('border', 'style'), 'border'), + (('border', 'top', 'color', 'rgb'), 'top_color'), + (('border', 'top', 'color'), 'top_color'), + (('border', 'top', 'style'), 'top'), + (('border', 'top'), 'top'), + (('border', 'right', 'color', 'rgb'), 'right_color'), + (('border', 'right', 'color'), 'right_color'), + (('border', 'right', 'style'), 'right'), + (('border', 'right'), 'right'), + (('border', 'bottom', 'color', 'rgb'), 'bottom_color'), + (('border', 'bottom', 'color'), 'bottom_color'), + (('border', 'bottom', 'style'), 'bottom'), + (('border', 'bottom'), 'bottom'), + (('border', 'left', 'color', 'rgb'), 'left_color'), + (('border', 'left', 'color'), 'left_color'), + (('border', 'left', 'style'), 'left'), + (('border', 'left'), 'left'), + ] + def _convert_to_style(self, style_dict, num_format_str=None): """ converts a style_dict to an xlsxwriter format object @@ -1610,35 +1672,57 @@ def _convert_to_style(self, style_dict, num_format_str=None): return None # Create a XlsxWriter format object. - xl_format = self.book.add_format() + props = {} if num_format_str is not None: - xl_format.set_num_format(num_format_str) + props['num_format'] = num_format_str if style_dict is None: - return xl_format - - # Map the cell font to XlsxWriter font properties. - if style_dict.get('font'): - font = style_dict['font'] - if font.get('bold'): - xl_format.set_bold() - - # Map the alignment to XlsxWriter alignment properties. - alignment = style_dict.get('alignment') - if alignment: - if (alignment.get('horizontal') and - alignment['horizontal'] == 'center'): - xl_format.set_align('center') - if (alignment.get('vertical') and - alignment['vertical'] == 'top'): - xl_format.set_align('top') - - # Map the cell borders to XlsxWriter border properties. - if style_dict.get('borders'): - xl_format.set_border() - - return xl_format + return self.book.add_format(props) + + if 'borders' in style_dict: + style_dict = style_dict.copy() + style_dict['border'] = style_dict.pop('borders') + + for src, dst in self.STYLE_MAPPING: + # src is a sequence of keys into a nested dict + # dst is a flat key + if dst in props: + continue + v = style_dict + for k in src: + try: + v = v[k] + except (KeyError, TypeError): + break + else: + props[dst] = v + + if isinstance(props.get('pattern'), string_types): + # TODO: support other fill patterns + props['pattern'] = 0 if props['pattern'] == 'none' else 1 + + for k in ['border', 'top', 'right', 'bottom', 'left']: + if isinstance(props.get(k), string_types): + try: + props[k] = ['none', 'thin', 'medium', 'dashed', 'dotted', + 'thick', 'double', 'hair', 'mediumDashed', + 'dashDot', 'mediumDashDot', 'dashDotDot', + 'mediumDashDotDot', 'slantDashDot'].\ + index(props[k]) + except ValueError: + props[k] = 2 + + if isinstance(props.get('font_script'), string_types): + props['font_script'] = ['baseline', 'superscript', 'subscript'].\ + index(props['font_script']) + + if isinstance(props.get('underline'), string_types): + props['underline'] = {'none': 0, 'single': 1, 'double': 2, + 'singleAccounting': 33, + 'doubleAccounting': 34}[props['underline']] + + return self.book.add_format(props) register_writer(_XlsxWriter) diff --git a/pandas/tests/io/test_excel.py b/pandas/tests/io/test_excel.py index 02652be2153f1..cbe0e4cb4be8d 100644 --- a/pandas/tests/io/test_excel.py +++ b/pandas/tests/io/test_excel.py @@ -2408,85 +2408,100 @@ def custom_converter(css): styled.to_excel(writer, sheet_name='styled') ExcelFormatter(styled, style_converter=custom_converter).write( writer, sheet_name='custom') + writer.save() - # For engines other than openpyxl 2, we only smoke test - if engine != 'openpyxl': - return - if not openpyxl_compat.is_compat(major_ver=2): - pytest.skip('incompatible openpyxl version') - - # (1) compare DataFrame.to_excel and Styler.to_excel when unstyled - n_cells = 0 - for col1, col2 in zip(writer.sheets['frame'].columns, - writer.sheets['unstyled'].columns): - assert len(col1) == len(col2) - for cell1, cell2 in zip(col1, col2): - assert cell1.value == cell2.value - assert_equal_style(cell1, cell2) - n_cells += 1 - - # ensure iteration actually happened: - assert n_cells == (10 + 1) * (3 + 1) - - # (2) check styling with default converter - n_cells = 0 - for col1, col2 in zip(writer.sheets['frame'].columns, - writer.sheets['styled'].columns): - assert len(col1) == len(col2) - for cell1, cell2 in zip(col1, col2): - ref = '%s%d' % (cell2.column, cell2.row) - # XXX: this isn't as strong a test as ideal; we should - # differences are exclusive - if ref == 'B2': - assert not cell1.font.bold - assert cell2.font.bold - elif ref == 'C3': - assert cell1.font.color.rgb != cell2.font.color.rgb - assert cell2.font.color.rgb == '000000FF' - elif ref == 'D4': - assert cell1.font.underline != cell2.font.underline - assert cell2.font.underline == 'single' - elif ref == 'B5': - assert not cell1.border.left.style - assert (cell2.border.top.style == - cell2.border.right.style == - cell2.border.bottom.style == - cell2.border.left.style == - 'medium') - elif ref == 'C6': - assert not cell1.font.italic - assert cell2.font.italic - elif ref == 'D7': - assert (cell1.alignment.horizontal != - cell2.alignment.horizontal) - assert cell2.alignment.horizontal == 'right' - elif ref == 'B8': - assert cell1.fill.fgColor.rgb != cell2.fill.fgColor.rgb - assert cell1.fill.patternType != cell2.fill.patternType - assert cell2.fill.fgColor.rgb == '00FF0000' - assert cell2.fill.patternType == 'solid' - else: - assert_equal_style(cell1, cell2) - - assert cell1.value == cell2.value - n_cells += 1 - - assert n_cells == (10 + 1) * (3 + 1) - - # (3) check styling with custom converter - n_cells = 0 - for col1, col2 in zip(writer.sheets['frame'].columns, - writer.sheets['custom'].columns): - assert len(col1) == len(col2) - for cell1, cell2 in zip(col1, col2): - ref = '%s%d' % (cell2.column, cell2.row) - if ref in ('B2', 'C3', 'D4', 'B5', 'C6', 'D7', 'B8'): - assert not cell1.font.bold - assert cell2.font.bold - else: - assert_equal_style(cell1, cell2) + if engine not in ('openpyxl', 'xlsxwriter'): + # For other engines, we only smoke test + return + openpyxl = pytest.importorskip('openpyxl') + if not openpyxl_compat.is_compat(major_ver=2): + pytest.skip('incompatible openpyxl version') - assert cell1.value == cell2.value - n_cells += 1 + wb = openpyxl.load_workbook(path) - assert n_cells == (10 + 1) * (3 + 1) + # (1) compare DataFrame.to_excel and Styler.to_excel when unstyled + n_cells = 0 + for col1, col2 in zip(wb['frame'].columns, + wb['unstyled'].columns): + assert len(col1) == len(col2) + for cell1, cell2 in zip(col1, col2): + assert cell1.value == cell2.value + assert_equal_style(cell1, cell2) + n_cells += 1 + + # ensure iteration actually happened: + assert n_cells == (10 + 1) * (3 + 1) + + # (2) check styling with default converter + + # XXX: openpyxl (as at 2.4) prefixes colors with 00, xlsxwriter with FF + alpha = '00' if engine == 'openpyxl' else 'FF' + + n_cells = 0 + for col1, col2 in zip(wb['frame'].columns, + wb['styled'].columns): + assert len(col1) == len(col2) + for cell1, cell2 in zip(col1, col2): + ref = '%s%d' % (cell2.column, cell2.row) + # XXX: this isn't as strong a test as ideal; we should + # confirm that differences are exclusive + if ref == 'B2': + assert not cell1.font.bold + assert cell2.font.bold + elif ref == 'C3': + assert cell1.font.color.rgb != cell2.font.color.rgb + assert cell2.font.color.rgb == alpha + '0000FF' + elif ref == 'D4': + # This fails with engine=xlsxwriter due to + # https://bitbucket.org/openpyxl/openpyxl/issues/800 + if engine == 'xlsxwriter' \ + and (LooseVersion(openpyxl.__version__) < + LooseVersion('2.4.6')): + pass + else: + assert cell1.font.underline != cell2.font.underline + assert cell2.font.underline == 'single' + elif ref == 'B5': + assert not cell1.border.left.style + assert (cell2.border.top.style == + cell2.border.right.style == + cell2.border.bottom.style == + cell2.border.left.style == + 'medium') + elif ref == 'C6': + assert not cell1.font.italic + assert cell2.font.italic + elif ref == 'D7': + assert (cell1.alignment.horizontal != + cell2.alignment.horizontal) + assert cell2.alignment.horizontal == 'right' + elif ref == 'B8': + assert cell1.fill.fgColor.rgb != cell2.fill.fgColor.rgb + assert cell1.fill.patternType != cell2.fill.patternType + assert cell2.fill.fgColor.rgb == alpha + 'FF0000' + assert cell2.fill.patternType == 'solid' + else: + assert_equal_style(cell1, cell2) + + assert cell1.value == cell2.value + n_cells += 1 + + assert n_cells == (10 + 1) * (3 + 1) + + # (3) check styling with custom converter + n_cells = 0 + for col1, col2 in zip(wb['frame'].columns, + wb['custom'].columns): + assert len(col1) == len(col2) + for cell1, cell2 in zip(col1, col2): + ref = '%s%d' % (cell2.column, cell2.row) + if ref in ('B2', 'C3', 'D4', 'B5', 'C6', 'D7', 'B8'): + assert not cell1.font.bold + assert cell2.font.bold + else: + assert_equal_style(cell1, cell2) + + assert cell1.value == cell2.value + n_cells += 1 + + assert n_cells == (10 + 1) * (3 + 1) From 740dca410bbfee3dfd4454e7677f5c186cf871fd Mon Sep 17 00:00:00 2001 From: Joel Nothman Date: Wed, 14 Jun 2017 23:05:41 +1000 Subject: [PATCH 2/7] Move what's new to 0.21 --- doc/source/whatsnew/v0.20.0.txt | 2 +- doc/source/whatsnew/v0.21.0.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v0.20.0.txt b/doc/source/whatsnew/v0.20.0.txt index 02aa863e644ae..9d475390175b2 100644 --- a/doc/source/whatsnew/v0.20.0.txt +++ b/doc/source/whatsnew/v0.20.0.txt @@ -357,7 +357,7 @@ To convert a ``SparseDataFrame`` back to sparse SciPy matrix in COO format, you Excel output for styled DataFrames ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Experimental support has been added to export ``DataFrame.style`` formats to Excel using the ``openpyxl`` or ``xlsxwriter`` engines. (:issue:`15530`, :issue:`16149`) +Experimental support has been added to export ``DataFrame.style`` formats to Excel using the ``openpyxl`` engine. (:issue:`15530`) For example, after running the following, ``styled.xlsx`` renders as below: diff --git a/doc/source/whatsnew/v0.21.0.txt b/doc/source/whatsnew/v0.21.0.txt index 79f2816f43a6f..ed281457d6c02 100644 --- a/doc/source/whatsnew/v0.21.0.txt +++ b/doc/source/whatsnew/v0.21.0.txt @@ -39,6 +39,7 @@ Other Enhancements - :func:`read_feather` has gained the ``nthreads`` parameter for multi-threaded operations (:issue:`16359`) - :func:`DataFrame.clip()` and :func: `Series.cip()` have gained an inplace argument. (:issue: `15388`) - :func:`crosstab` has gained a ``margins_name`` parameter to define the name of the row / column that will contain the totals when margins=True. (:issue:`15972`) +- Better support for ``Dataframe.style.to_excel()`` output with the ``xlsxwriter`` engine. (:issue:`16149`) .. _whatsnew_0210.api_breaking: From 30a8dc482550f6c8e3cf8ff476d9d04f2b6783eb Mon Sep 17 00:00:00 2001 From: Joel Nothman Date: Fri, 18 Aug 2017 00:22:30 +1000 Subject: [PATCH 3/7] ENH More efficient traversal of xlsxwriter styles where few types are in use --- pandas/io/excel.py | 161 +++++++++++++++++++++++++-------------------- 1 file changed, 88 insertions(+), 73 deletions(-) diff --git a/pandas/io/excel.py b/pandas/io/excel.py index a0795e462ff87..a36a555a2c23c 100644 --- a/pandas/io/excel.py +++ b/pandas/io/excel.py @@ -1610,66 +1610,80 @@ def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0, val, style) # Map from openpyxl-oriented styles to flatter xlsxwriter representation - STYLE_MAPPING = [ - (('font', 'name'), 'font_name'), - (('font', 'sz'), 'font_size'), - (('font', 'size'), 'font_size'), - (('font', 'color', 'rgb'), 'font_color'), - (('font', 'color'), 'font_color'), - (('font', 'b'), 'bold'), - (('font', 'bold'), 'bold'), - (('font', 'i'), 'italic'), - (('font', 'italic'), 'italic'), - (('font', 'u'), 'underline'), - (('font', 'underline'), 'underline'), - (('font', 'strike'), 'font_strikeout'), - (('font', 'vertAlign'), 'font_script'), - (('font', 'vertalign'), 'font_script'), - (('number_format', 'format_code'), 'num_format'), - (('number_format',), 'num_format'), - (('protection', 'locked'), 'locked'), - (('protection', 'hidden'), 'hidden'), - (('alignment', 'horizontal'), 'align'), - (('alignment', 'vertical'), 'valign'), - (('alignment', 'text_rotation'), 'rotation'), - (('alignment', 'wrap_text'), 'text_wrap'), - (('alignment', 'indent'), 'indent'), - (('alignment', 'shrink_to_fit'), 'shrink'), - (('fill', 'patternType'), 'pattern'), - (('fill', 'patterntype'), 'pattern'), - (('fill', 'fill_type'), 'pattern'), - (('fill', 'start_color', 'rgb'), 'fg_color'), - (('fill', 'fgColor', 'rgb'), 'fg_color'), - (('fill', 'fgcolor', 'rgb'), 'fg_color'), - (('fill', 'start_color'), 'fg_color'), - (('fill', 'fgColor'), 'fg_color'), - (('fill', 'fgcolor'), 'fg_color'), - (('fill', 'end_color', 'rgb'), 'bg_color'), - (('fill', 'bgColor', 'rgb'), 'bg_color'), - (('fill', 'bgcolor', 'rgb'), 'bg_color'), - (('fill', 'end_color'), 'bg_color'), - (('fill', 'bgColor'), 'bg_color'), - (('fill', 'bgcolor'), 'bg_color'), - (('border', 'color', 'rgb'), 'border_color'), - (('border', 'color'), 'border_color'), - (('border', 'style'), 'border'), - (('border', 'top', 'color', 'rgb'), 'top_color'), - (('border', 'top', 'color'), 'top_color'), - (('border', 'top', 'style'), 'top'), - (('border', 'top'), 'top'), - (('border', 'right', 'color', 'rgb'), 'right_color'), - (('border', 'right', 'color'), 'right_color'), - (('border', 'right', 'style'), 'right'), - (('border', 'right'), 'right'), - (('border', 'bottom', 'color', 'rgb'), 'bottom_color'), - (('border', 'bottom', 'color'), 'bottom_color'), - (('border', 'bottom', 'style'), 'bottom'), - (('border', 'bottom'), 'bottom'), - (('border', 'left', 'color', 'rgb'), 'left_color'), - (('border', 'left', 'color'), 'left_color'), - (('border', 'left', 'style'), 'left'), - (('border', 'left'), 'left'), - ] + # Ordering necessary for both determinism and because some are keyed by + # prefixes of others. + STYLE_MAPPING = { + 'font': [ + (('name',), 'font_name'), + (('sz',), 'font_size'), + (('size',), 'font_size'), + (('color', 'rgb',), 'font_color'), + (('color',), 'font_color'), + (('b',), 'bold'), + (('bold',), 'bold'), + (('i',), 'italic'), + (('italic',), 'italic'), + (('u',), 'underline'), + (('underline',), 'underline'), + (('strike',), 'font_strikeout'), + (('vertAlign',), 'font_script'), + (('vertalign',), 'font_script'), + ], + 'number_format': [ + (('format_code',), 'num_format'), + ((), 'num_format',), + ], + 'protection': [ + (('locked',), 'locked'), + (('hidden',), 'hidden'), + ], + 'alignment': [ + (('horizontal',), 'align'), + (('vertical',), 'valign'), + (('text_rotation',), 'rotation'), + (('wrap_text',), 'text_wrap'), + (('indent',), 'indent'), + (('shrink_to_fit',), 'shrink'), + ], + 'fill': [ + (('patternType',), 'pattern'), + (('patterntype',), 'pattern'), + (('fill_type',), 'pattern'), + (('start_color', 'rgb',), 'fg_color'), + (('fgColor', 'rgb',), 'fg_color'), + (('fgcolor', 'rgb',), 'fg_color'), + (('start_color',), 'fg_color'), + (('fgColor',), 'fg_color'), + (('fgcolor',), 'fg_color'), + (('end_color', 'rgb',), 'bg_color'), + (('bgColor', 'rgb',), 'bg_color'), + (('bgcolor', 'rgb',), 'bg_color'), + (('end_color',), 'bg_color'), + (('bgColor',), 'bg_color'), + (('bgcolor',), 'bg_color'), + ], + 'border': [ + (('color', 'rgb',), 'border_color'), + (('color',), 'border_color'), + (('style',), 'border'), + (('top', 'color', 'rgb',), 'top_color'), + (('top', 'color',), 'top_color'), + (('top', 'style',), 'top'), + (('top',), 'top'), + (('right', 'color', 'rgb',), 'right_color'), + (('right', 'color',), 'right_color'), + (('right', 'style',), 'right'), + (('right',), 'right'), + (('bottom', 'color', 'rgb',), 'bottom_color'), + (('bottom', 'color',), 'bottom_color'), + (('bottom', 'style',), 'bottom'), + (('bottom',), 'bottom'), + (('left', 'color', 'rgb',), 'left_color'), + (('left', 'color',), 'left_color'), + (('left', 'style',), 'left'), + (('left',), 'left'), + ], + } def _convert_to_style(self, style_dict, num_format_str=None): """ @@ -1697,19 +1711,20 @@ def _convert_to_style(self, style_dict, num_format_str=None): style_dict = style_dict.copy() style_dict['border'] = style_dict.pop('borders') - for src, dst in self.STYLE_MAPPING: - # src is a sequence of keys into a nested dict - # dst is a flat key - if dst in props: - continue - v = style_dict - for k in src: - try: - v = v[k] - except (KeyError, TypeError): - break - else: - props[dst] = v + for style_group_key, style_group in style_dict.items(): + for src, dst in self.STYLE_MAPPING.get(style_group_key, []): + # src is a sequence of keys into a nested dict + # dst is a flat key + if dst in props: + continue + v = style_group + for k in src: + try: + v = v[k] + except (KeyError, TypeError): + break + else: + props[dst] = v if isinstance(props.get('pattern'), string_types): # TODO: support other fill patterns From 38db7e68bbff192e74730c118c30393edc108fca Mon Sep 17 00:00:00 2001 From: Joel Nothman Date: Mon, 4 Sep 2017 14:40:38 +1000 Subject: [PATCH 4/7] Empty commit to restart build From d6441ffd71a7413dd69d8fd7d5838f5e5c7c5666 Mon Sep 17 00:00:00 2001 From: Joel Nothman Date: Mon, 16 Oct 2017 12:34:59 +1100 Subject: [PATCH 5/7] Factor out _XlsxStyler.convert method --- pandas/io/excel.py | 156 ++++++++++++++++++++++----------------------- 1 file changed, 78 insertions(+), 78 deletions(-) diff --git a/pandas/io/excel.py b/pandas/io/excel.py index a36a555a2c23c..eebb92651c62d 100644 --- a/pandas/io/excel.py +++ b/pandas/io/excel.py @@ -1540,75 +1540,7 @@ def _convert_to_style(cls, style_dict, num_format_str=None): register_writer(_XlwtWriter) -class _XlsxWriter(ExcelWriter): - engine = 'xlsxwriter' - supported_extensions = ('.xlsx',) - - def __init__(self, path, engine=None, - date_format=None, datetime_format=None, **engine_kwargs): - # Use the xlsxwriter module as the Excel writer. - import xlsxwriter - - super(_XlsxWriter, self).__init__(path, engine=engine, - date_format=date_format, - datetime_format=datetime_format, - **engine_kwargs) - - self.book = xlsxwriter.Workbook(path, **engine_kwargs) - - def save(self): - """ - Save workbook to disk. - """ - - return self.book.close() - - def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0, - freeze_panes=None): - # Write the frame cells using xlsxwriter. - sheet_name = self._get_sheet_name(sheet_name) - - if sheet_name in self.sheets: - wks = self.sheets[sheet_name] - else: - wks = self.book.add_worksheet(sheet_name) - self.sheets[sheet_name] = wks - - style_dict = {} - - if _validate_freeze_panes(freeze_panes): - wks.freeze_panes(*(freeze_panes)) - - for cell in cells: - val = _conv_value(cell.val) - - num_format_str = None - if isinstance(cell.val, datetime): - num_format_str = self.datetime_format - elif isinstance(cell.val, date): - num_format_str = self.date_format - - stylekey = json.dumps(cell.style) - if num_format_str: - stylekey += num_format_str - - if stylekey in style_dict: - style = style_dict[stylekey] - else: - 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: - wks.merge_range(startrow + cell.row, - startcol + cell.col, - startrow + cell.mergestart, - startcol + cell.mergeend, - cell.val, style) - else: - wks.write(startrow + cell.row, - startcol + cell.col, - val, style) - +class _XlsxStyler(object): # Map from openpyxl-oriented styles to flatter xlsxwriter representation # Ordering necessary for both determinism and because some are keyed by # prefixes of others. @@ -1685,19 +1617,17 @@ def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0, ], } - def _convert_to_style(self, style_dict, num_format_str=None): + @classmethod + def convert(cls, style_dict, num_format_str=None): """ - converts a style_dict to an xlsxwriter format object + converts a style_dict to an xlsxwriter format dict + Parameters ---------- style_dict: style dictionary to convert num_format_str: optional number format string """ - # If there is no formatting we don't create a format object. - if num_format_str is None and style_dict is None: - return None - # Create a XlsxWriter format object. props = {} @@ -1705,14 +1635,14 @@ def _convert_to_style(self, style_dict, num_format_str=None): props['num_format'] = num_format_str if style_dict is None: - return self.book.add_format(props) + return props if 'borders' in style_dict: style_dict = style_dict.copy() style_dict['border'] = style_dict.pop('borders') for style_group_key, style_group in style_dict.items(): - for src, dst in self.STYLE_MAPPING.get(style_group_key, []): + for src, dst in cls.STYLE_MAPPING.get(style_group_key, []): # src is a sequence of keys into a nested dict # dst is a flat key if dst in props: @@ -1750,7 +1680,77 @@ def _convert_to_style(self, style_dict, num_format_str=None): 'singleAccounting': 33, 'doubleAccounting': 34}[props['underline']] - return self.book.add_format(props) + return props + + +class _XlsxWriter(ExcelWriter): + engine = 'xlsxwriter' + supported_extensions = ('.xlsx',) + + def __init__(self, path, engine=None, + date_format=None, datetime_format=None, **engine_kwargs): + # Use the xlsxwriter module as the Excel writer. + import xlsxwriter + + super(_XlsxWriter, self).__init__(path, engine=engine, + date_format=date_format, + datetime_format=datetime_format, + **engine_kwargs) + self.book = xlsxwriter.Workbook(path, **engine_kwargs) + + def save(self): + """ + Save workbook to disk. + """ + + return self.book.close() + + def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0, + freeze_panes=None): + # Write the frame cells using xlsxwriter. + sheet_name = self._get_sheet_name(sheet_name) + + if sheet_name in self.sheets: + wks = self.sheets[sheet_name] + else: + wks = self.book.add_worksheet(sheet_name) + self.sheets[sheet_name] = wks + + style_dict = {'null': None} + + if _validate_freeze_panes(freeze_panes): + wks.freeze_panes(*(freeze_panes)) + + for cell in cells: + val = _conv_value(cell.val) + + num_format_str = None + if isinstance(cell.val, datetime): + num_format_str = self.datetime_format + elif isinstance(cell.val, date): + num_format_str = self.date_format + + stylekey = json.dumps(cell.style) + if num_format_str: + stylekey += num_format_str + + if stylekey in style_dict: + style = style_dict[stylekey] + else: + style = self.book.add_format( + _XlsxStyler.convert(cell.style, num_format_str)) + style_dict[stylekey] = style + + if cell.mergestart is not None and cell.mergeend is not None: + wks.merge_range(startrow + cell.row, + startcol + cell.col, + startrow + cell.mergestart, + startcol + cell.mergeend, + cell.val, style) + else: + wks.write(startrow + cell.row, + startcol + cell.col, + val, style) register_writer(_XlsxWriter) From 26728ee2ad44fc629e53b880ce5febcec5c107b5 Mon Sep 17 00:00:00 2001 From: Joel Nothman Date: Sun, 29 Oct 2017 21:14:27 +1100 Subject: [PATCH 6/7] Move what's new to 0.22 --- doc/source/whatsnew/v0.21.0.txt | 1 - doc/source/whatsnew/v0.22.0.txt | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/source/whatsnew/v0.21.0.txt b/doc/source/whatsnew/v0.21.0.txt index bb5cff2ce36b7..4c460eeb85b82 100644 --- a/doc/source/whatsnew/v0.21.0.txt +++ b/doc/source/whatsnew/v0.21.0.txt @@ -330,7 +330,6 @@ Various enhancements - :func:`date_range` now accepts 'YS' in addition to 'AS' as an alias for start of year. (:issue:`9313`) - :func:`date_range` now accepts 'Y' in addition to 'A' as an alias for end of year. (:issue:`9313`) - :func:`DataFrame.add_prefix` and :func:`DataFrame.add_suffix` now accept strings containing the '%' character. (:issue:`17151`) -- Better support for ``Dataframe.style.to_excel()`` output with the ``xlsxwriter`` engine. (:issue:`16149`) - Read/write methods that infer compression (:func:`read_csv`, :func:`read_table`, :func:`read_pickle`, and :meth:`~DataFrame.to_pickle`) can now infer from path-like objects, such as ``pathlib.Path``. (:issue:`17206`) - :func:`read_sas` now recognizes much more of the most frequently used date (datetime) formats in SAS7BDAT files. (:issue:`15871`) - :func:`DataFrame.items` and :func:`Series.items` are now present in both Python 2 and 3 and is lazy in all cases. (:issue:`13918`, :issue:`17213`) diff --git a/doc/source/whatsnew/v0.22.0.txt b/doc/source/whatsnew/v0.22.0.txt index cbd094ec4ef49..a4c7fcb3d29e5 100644 --- a/doc/source/whatsnew/v0.22.0.txt +++ b/doc/source/whatsnew/v0.22.0.txt @@ -22,7 +22,7 @@ New features Other Enhancements ^^^^^^^^^^^^^^^^^^ -- +- Better support for ``Dataframe.style.to_excel()`` output with the ``xlsxwriter`` engine. (:issue:`16149`) - - From 80ed56aac0330b74b9f1b209b52b93a543aa3539 Mon Sep 17 00:00:00 2001 From: Joel Nothman Date: Mon, 30 Oct 2017 10:35:40 +1100 Subject: [PATCH 7/7] PEP8 --- pandas/io/excel.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pandas/io/excel.py b/pandas/io/excel.py index 2664eb67b86fc..fec916dc52d20 100644 --- a/pandas/io/excel.py +++ b/pandas/io/excel.py @@ -1791,4 +1791,5 @@ def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0, startcol + cell.col, val, style) + register_writer(_XlsxWriter)