Skip to content

Commit

Permalink
More pytest-like test_styler_to_excel; enhancements to xlwt
Browse files Browse the repository at this point in the history
  • Loading branch information
jnothman committed Apr 19, 2017
1 parent 6168765 commit 6465913
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 131 deletions.
38 changes: 36 additions & 2 deletions pandas/io/excel.py
Expand Up @@ -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=';'):
Expand All @@ -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()]
Expand Down
6 changes: 3 additions & 3 deletions pandas/io/formats/excel.py
Expand Up @@ -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,
Expand All @@ -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]',
Expand Down
9 changes: 4 additions & 5 deletions pandas/tests/io/formats/test_to_excel.py
Expand Up @@ -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}}),
Expand Down Expand Up @@ -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"},
Expand Down
256 changes: 135 additions & 121 deletions pandas/tests/io/test_excel.py
Expand Up @@ -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'
Expand Down Expand Up @@ -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)

0 comments on commit 6465913

Please sign in to comment.