From 6465913d36a411c868174b59348c3ac07bdc6753 Mon Sep 17 00:00:00 2001 From: Joel Nothman Date: Thu, 20 Apr 2017 00:26:32 +1000 Subject: [PATCH] More pytest-like test_styler_to_excel; enhancements to xlwt --- pandas/io/excel.py | 38 +++- pandas/io/formats/excel.py | 6 +- pandas/tests/io/formats/test_to_excel.py | 9 +- pandas/tests/io/test_excel.py | 256 ++++++++++++----------- 4 files changed, 178 insertions(+), 131 deletions(-) diff --git a/pandas/io/excel.py b/pandas/io/excel.py index fbb10ebdfc56d..5129c4db171ae 100644 --- a/pandas/io/excel.py +++ b/pandas/io/excel.py @@ -1468,6 +1468,37 @@ def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0, startcol + cell.col, val, style) + @classmethod + def _tweak_style(cls, item): + item = item.copy() + for k in ['top', 'right', 'bottom', 'left']: + if k not in item: + continue + side = item[k] + if not hasattr(side, 'items'): + continue + color = side.get('color') or side.get('colour') + if color is not None: + item[k + '_color'] = color + if side.get('style'): + item[k] = side['style'] + if 'fill' in item and 'pattern' not in item: + fill = item.pop('fill') + item['pattern'] = {} + for k in ['patternType', 'patterntype', 'fill_type']: + if k in fill: + item['pattern']['pattern'] = fill[k] + break + for k in ['fgColor', 'fgcolor', 'start_color']: + if k in fill: + item['pattern']['fore_color'] = fill[k] + break + for k in ['bgColor', 'bgcolor', 'end_color']: + if k in fill: + item['pattern']['back_color'] = fill[k] + break + return item + @classmethod def _style_to_xlwt(cls, item, firstlevel=True, field_sep=',', line_sep=';'): @@ -1476,16 +1507,19 @@ def _style_to_xlwt(cls, item, firstlevel=True, field_sep=',', hstyle = {"font": {"bold": True}, "border": {"top": "thin", - "right": "thin", + "right": {"style": "thin", "color": "red"}, "bottom": "thin", "left": "thin"}, "align": {"horiz": "center"}} will be converted to font: bold on; \ - border: top thin, right thin, bottom thin, left thin; \ + border: top thin, right thin, right_color red, \ + bottom thin, left thin; \ align: horiz center; """ if hasattr(item, 'items'): + item = cls._tweak_style(item) + if firstlevel: it = ["%s: %s" % (key, cls._style_to_xlwt(value, False)) for key, value in item.items()] diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index 00c1926533dc6..80587f9a752c7 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -247,6 +247,8 @@ def build_font(self, props): decoration = props.get('text-decoration') if decoration is not None: decoration = decoration.split() + else: + decoration = () return { 'name': font_names[0] if font_names else None, @@ -255,11 +257,9 @@ def build_font(self, props): 'bold': self.BOLD_MAP.get(props.get('font-weight')), 'italic': self.ITALIC_MAP.get(props.get('font-style')), 'underline': ('single' if - decoration is not None and 'underline' in decoration else None), - 'strike': (None if decoration is None - else 'line-through' in decoration), + 'strike': ('line-through' in decoration) or None, 'color': self.color_to_excel(props.get('color')), # shadow if nonzero digit before shadow colour 'shadow': (bool(re.search('^[^#(]*[1-9]', diff --git a/pandas/tests/io/formats/test_to_excel.py b/pandas/tests/io/formats/test_to_excel.py index 41ad4f63fa129..fff5299921270 100644 --- a/pandas/tests/io/formats/test_to_excel.py +++ b/pandas/tests/io/formats/test_to_excel.py @@ -47,11 +47,11 @@ # - italic # - underline ('text-decoration: underline', - {'font': {'underline': 'single', 'strike': False}}), + {'font': {'underline': 'single'}}), ('text-decoration: overline', - {'font': {'strike': False}}), + {}), ('text-decoration: none', - {'font': {'strike': False}}), + {}), # - strike ('text-decoration: line-through', {'font': {'strike': True}}), @@ -184,8 +184,7 @@ def test_css_to_excel_multiple(): vertical-align: top; unused: something; ''') - assert {"font": {"bold": True, "strike": False, - "underline": "single", "color": "FF0000"}, + assert {"font": {"bold": True, "underline": "single", "color": "FF0000"}, "border": {"top": {"style": "thin"}, "right": {"style": "thin"}, "bottom": {"style": "thin"}, diff --git a/pandas/tests/io/test_excel.py b/pandas/tests/io/test_excel.py index 688ef533152ca..8f754e289091e 100644 --- a/pandas/tests/io/test_excel.py +++ b/pandas/tests/io/test_excel.py @@ -2151,127 +2151,6 @@ def test_write_cells_merge_styled(self): self.assertEqual(xcell_b1.font, openpyxl_sty_merged) self.assertEqual(xcell_a2.font, openpyxl_sty_merged) - def test_styler_to_excel(self): - pytest.importorskip('jinja2') - if not openpyxl_compat.is_compat(major_ver=2): - pytest.skip('incompatible openpyxl version') - - def style(df): - return DataFrame([['font-weight: bold', '', ''], - ['', 'color: blue', ''], - ['', '', 'text-decoration: underline'], - ['border-style: solid', '', ''], - ['', 'font-style: italic', ''], - ['', '', 'text-align: right'], - ['background-color: red', '', ''], - ['', '', ''], - ['', '', ''], - ['', '', '']], - index=df.index, columns=df.columns) - - def assert_equal_style(cell1, cell2): - # XXX: should find a better way to check equality - assert cell1.alignment.__dict__ == cell2.alignment.__dict__ - assert cell1.border.__dict__ == cell2.border.__dict__ - assert cell1.fill.__dict__ == cell2.fill.__dict__ - assert cell1.font.__dict__ == cell2.font.__dict__ - assert cell1.number_format == cell2.number_format - assert cell1.protection.__dict__ == cell2.protection.__dict__ - - def custom_converter(css): - # use bold iff there is custom style attached to the cell - if css.strip(' \n;'): - return {'font': {'bold': True}} - return {} - - # Prepare spreadsheets - - df = DataFrame(np.random.randn(10, 3)) - with ensure_clean('.xlsx') as path: - writer = _Openpyxl22Writer(path) - df.to_excel(writer, engine='openpyxl', sheet_name='frame') - df.style.to_excel(writer, engine='openpyxl', sheet_name='unstyled') - styled = df.style.apply(style, axis=None) - styled.to_excel(writer, engine='openpyxl', sheet_name='styled') - ExcelFormatter(styled, style_converter=custom_converter).write( - writer, engine='openpyxl', sheet_name='custom') - - # (1) compare DataFrame.to_excel and Styler.to_excel when unstyled - n_cells = 0 - for col1, col2 in zip(writer.sheets['frame'], - writer.sheets['unstyled']): - 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'], - writer.sheets['styled']): - 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'], - writer.sheets['custom']): - 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) - class XlwtTests(ExcelWriterBase, tm.TestCase): ext = '.xls' @@ -2473,3 +2352,138 @@ def check_called(func): check_called( lambda: df.to_excel( 'something.xls', engine='dummy')) + + +@pytest.mark.parametrize('engine', [ + 'xlwt', + 'xlsxwriter', + 'openpyxl', +]) +def test_styler_to_excel(engine): + def style(df): + # XXX: RGB colors not supported in xlwt + return DataFrame([['font-weight: bold', '', ''], + (['', '', ''] if engine == 'xlwt' + else ['', 'color: blue', '']), + ['', '', 'text-decoration: underline'], + ['border-style: solid', '', ''], + ['', 'font-style: italic', ''], + ['', '', 'text-align: right'], + (['', '', ''] if engine == 'xlwt' and False + else ['background-color: red', '', '']), + ['', '', ''], + ['', '', ''], + ['', '', '']], + index=df.index, columns=df.columns) + + def assert_equal_style(cell1, cell2): + # XXX: should find a better way to check equality + assert cell1.alignment.__dict__ == cell2.alignment.__dict__ + assert cell1.border.__dict__ == cell2.border.__dict__ + assert cell1.fill.__dict__ == cell2.fill.__dict__ + assert cell1.font.__dict__ == cell2.font.__dict__ + assert cell1.number_format == cell2.number_format + assert cell1.protection.__dict__ == cell2.protection.__dict__ + + def custom_converter(css): + # use bold iff there is custom style attached to the cell + if css.strip(' \n;'): + return {'font': {'bold': True}} + return {} + + pytest.importorskip('jinja2') + pytest.importorskip(engine) + + # Prepare spreadsheets + + df = DataFrame(np.random.randn(10, 3)) + with ensure_clean('.xlsx' if engine != 'xlwt' else '.xls') as path: + writer = ExcelWriter(path, engine=engine) + df.to_excel(writer, sheet_name='frame') + df.style.to_excel(writer, sheet_name='unstyled') + styled = df.style.apply(style, axis=None) + styled.to_excel(writer, sheet_name='styled') + ExcelFormatter(styled, style_converter=custom_converter).write( + writer, sheet_name='custom') + + # 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) + + assert cell1.value == cell2.value + n_cells += 1 + + assert n_cells == (10 + 1) * (3 + 1)