Skip to content

Commit 6465913

Browse files
committed
More pytest-like test_styler_to_excel; enhancements to xlwt
1 parent 6168765 commit 6465913

File tree

4 files changed

+178
-131
lines changed

4 files changed

+178
-131
lines changed

pandas/io/excel.py

+36-2
Original file line numberDiff line numberDiff line change
@@ -1468,6 +1468,37 @@ def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0,
14681468
startcol + cell.col,
14691469
val, style)
14701470

1471+
@classmethod
1472+
def _tweak_style(cls, item):
1473+
item = item.copy()
1474+
for k in ['top', 'right', 'bottom', 'left']:
1475+
if k not in item:
1476+
continue
1477+
side = item[k]
1478+
if not hasattr(side, 'items'):
1479+
continue
1480+
color = side.get('color') or side.get('colour')
1481+
if color is not None:
1482+
item[k + '_color'] = color
1483+
if side.get('style'):
1484+
item[k] = side['style']
1485+
if 'fill' in item and 'pattern' not in item:
1486+
fill = item.pop('fill')
1487+
item['pattern'] = {}
1488+
for k in ['patternType', 'patterntype', 'fill_type']:
1489+
if k in fill:
1490+
item['pattern']['pattern'] = fill[k]
1491+
break
1492+
for k in ['fgColor', 'fgcolor', 'start_color']:
1493+
if k in fill:
1494+
item['pattern']['fore_color'] = fill[k]
1495+
break
1496+
for k in ['bgColor', 'bgcolor', 'end_color']:
1497+
if k in fill:
1498+
item['pattern']['back_color'] = fill[k]
1499+
break
1500+
return item
1501+
14711502
@classmethod
14721503
def _style_to_xlwt(cls, item, firstlevel=True, field_sep=',',
14731504
line_sep=';'):
@@ -1476,16 +1507,19 @@ def _style_to_xlwt(cls, item, firstlevel=True, field_sep=',',
14761507
14771508
hstyle = {"font": {"bold": True},
14781509
"border": {"top": "thin",
1479-
"right": "thin",
1510+
"right": {"style": "thin", "color": "red"},
14801511
"bottom": "thin",
14811512
"left": "thin"},
14821513
"align": {"horiz": "center"}}
14831514
will be converted to
14841515
font: bold on; \
1485-
border: top thin, right thin, bottom thin, left thin; \
1516+
border: top thin, right thin, right_color red, \
1517+
bottom thin, left thin; \
14861518
align: horiz center;
14871519
"""
14881520
if hasattr(item, 'items'):
1521+
item = cls._tweak_style(item)
1522+
14891523
if firstlevel:
14901524
it = ["%s: %s" % (key, cls._style_to_xlwt(value, False))
14911525
for key, value in item.items()]

pandas/io/formats/excel.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,8 @@ def build_font(self, props):
247247
decoration = props.get('text-decoration')
248248
if decoration is not None:
249249
decoration = decoration.split()
250+
else:
251+
decoration = ()
250252

251253
return {
252254
'name': font_names[0] if font_names else None,
@@ -255,11 +257,9 @@ def build_font(self, props):
255257
'bold': self.BOLD_MAP.get(props.get('font-weight')),
256258
'italic': self.ITALIC_MAP.get(props.get('font-style')),
257259
'underline': ('single' if
258-
decoration is not None and
259260
'underline' in decoration
260261
else None),
261-
'strike': (None if decoration is None
262-
else 'line-through' in decoration),
262+
'strike': ('line-through' in decoration) or None,
263263
'color': self.color_to_excel(props.get('color')),
264264
# shadow if nonzero digit before shadow colour
265265
'shadow': (bool(re.search('^[^#(]*[1-9]',

pandas/tests/io/formats/test_to_excel.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,11 @@
4747
# - italic
4848
# - underline
4949
('text-decoration: underline',
50-
{'font': {'underline': 'single', 'strike': False}}),
50+
{'font': {'underline': 'single'}}),
5151
('text-decoration: overline',
52-
{'font': {'strike': False}}),
52+
{}),
5353
('text-decoration: none',
54-
{'font': {'strike': False}}),
54+
{}),
5555
# - strike
5656
('text-decoration: line-through',
5757
{'font': {'strike': True}}),
@@ -184,8 +184,7 @@ def test_css_to_excel_multiple():
184184
vertical-align: top;
185185
unused: something;
186186
''')
187-
assert {"font": {"bold": True, "strike": False,
188-
"underline": "single", "color": "FF0000"},
187+
assert {"font": {"bold": True, "underline": "single", "color": "FF0000"},
189188
"border": {"top": {"style": "thin"},
190189
"right": {"style": "thin"},
191190
"bottom": {"style": "thin"},

pandas/tests/io/test_excel.py

+135-121
Original file line numberDiff line numberDiff line change
@@ -2151,127 +2151,6 @@ def test_write_cells_merge_styled(self):
21512151
self.assertEqual(xcell_b1.font, openpyxl_sty_merged)
21522152
self.assertEqual(xcell_a2.font, openpyxl_sty_merged)
21532153

2154-
def test_styler_to_excel(self):
2155-
pytest.importorskip('jinja2')
2156-
if not openpyxl_compat.is_compat(major_ver=2):
2157-
pytest.skip('incompatible openpyxl version')
2158-
2159-
def style(df):
2160-
return DataFrame([['font-weight: bold', '', ''],
2161-
['', 'color: blue', ''],
2162-
['', '', 'text-decoration: underline'],
2163-
['border-style: solid', '', ''],
2164-
['', 'font-style: italic', ''],
2165-
['', '', 'text-align: right'],
2166-
['background-color: red', '', ''],
2167-
['', '', ''],
2168-
['', '', ''],
2169-
['', '', '']],
2170-
index=df.index, columns=df.columns)
2171-
2172-
def assert_equal_style(cell1, cell2):
2173-
# XXX: should find a better way to check equality
2174-
assert cell1.alignment.__dict__ == cell2.alignment.__dict__
2175-
assert cell1.border.__dict__ == cell2.border.__dict__
2176-
assert cell1.fill.__dict__ == cell2.fill.__dict__
2177-
assert cell1.font.__dict__ == cell2.font.__dict__
2178-
assert cell1.number_format == cell2.number_format
2179-
assert cell1.protection.__dict__ == cell2.protection.__dict__
2180-
2181-
def custom_converter(css):
2182-
# use bold iff there is custom style attached to the cell
2183-
if css.strip(' \n;'):
2184-
return {'font': {'bold': True}}
2185-
return {}
2186-
2187-
# Prepare spreadsheets
2188-
2189-
df = DataFrame(np.random.randn(10, 3))
2190-
with ensure_clean('.xlsx') as path:
2191-
writer = _Openpyxl22Writer(path)
2192-
df.to_excel(writer, engine='openpyxl', sheet_name='frame')
2193-
df.style.to_excel(writer, engine='openpyxl', sheet_name='unstyled')
2194-
styled = df.style.apply(style, axis=None)
2195-
styled.to_excel(writer, engine='openpyxl', sheet_name='styled')
2196-
ExcelFormatter(styled, style_converter=custom_converter).write(
2197-
writer, engine='openpyxl', sheet_name='custom')
2198-
2199-
# (1) compare DataFrame.to_excel and Styler.to_excel when unstyled
2200-
n_cells = 0
2201-
for col1, col2 in zip(writer.sheets['frame'],
2202-
writer.sheets['unstyled']):
2203-
assert len(col1) == len(col2)
2204-
for cell1, cell2 in zip(col1, col2):
2205-
assert cell1.value == cell2.value
2206-
assert_equal_style(cell1, cell2)
2207-
n_cells += 1
2208-
2209-
# ensure iteration actually happened:
2210-
assert n_cells == (10 + 1) * (3 + 1)
2211-
2212-
# (2) check styling with default converter
2213-
n_cells = 0
2214-
for col1, col2 in zip(writer.sheets['frame'],
2215-
writer.sheets['styled']):
2216-
assert len(col1) == len(col2)
2217-
for cell1, cell2 in zip(col1, col2):
2218-
ref = '%s%d' % (cell2.column, cell2.row)
2219-
# XXX: this isn't as strong a test as ideal; we should
2220-
# differences are exclusive
2221-
if ref == 'B2':
2222-
assert not cell1.font.bold
2223-
assert cell2.font.bold
2224-
elif ref == 'C3':
2225-
assert cell1.font.color.rgb != cell2.font.color.rgb
2226-
assert cell2.font.color.rgb == '000000FF'
2227-
elif ref == 'D4':
2228-
assert cell1.font.underline != cell2.font.underline
2229-
assert cell2.font.underline == 'single'
2230-
elif ref == 'B5':
2231-
assert not cell1.border.left.style
2232-
assert (cell2.border.top.style ==
2233-
cell2.border.right.style ==
2234-
cell2.border.bottom.style ==
2235-
cell2.border.left.style ==
2236-
'medium')
2237-
elif ref == 'C6':
2238-
assert not cell1.font.italic
2239-
assert cell2.font.italic
2240-
elif ref == 'D7':
2241-
assert (cell1.alignment.horizontal !=
2242-
cell2.alignment.horizontal)
2243-
assert cell2.alignment.horizontal == 'right'
2244-
elif ref == 'B8':
2245-
assert cell1.fill.fgColor.rgb != cell2.fill.fgColor.rgb
2246-
assert cell1.fill.patternType != cell2.fill.patternType
2247-
assert cell2.fill.fgColor.rgb == '00FF0000'
2248-
assert cell2.fill.patternType == 'solid'
2249-
else:
2250-
assert_equal_style(cell1, cell2)
2251-
2252-
assert cell1.value == cell2.value
2253-
n_cells += 1
2254-
2255-
assert n_cells == (10 + 1) * (3 + 1)
2256-
2257-
# (3) check styling with custom converter
2258-
n_cells = 0
2259-
for col1, col2 in zip(writer.sheets['frame'],
2260-
writer.sheets['custom']):
2261-
assert len(col1) == len(col2)
2262-
for cell1, cell2 in zip(col1, col2):
2263-
ref = '%s%d' % (cell2.column, cell2.row)
2264-
if ref in ('B2', 'C3', 'D4', 'B5', 'C6', 'D7', 'B8'):
2265-
assert not cell1.font.bold
2266-
assert cell2.font.bold
2267-
else:
2268-
assert_equal_style(cell1, cell2)
2269-
2270-
assert cell1.value == cell2.value
2271-
n_cells += 1
2272-
2273-
assert n_cells == (10 + 1) * (3 + 1)
2274-
22752154

22762155
class XlwtTests(ExcelWriterBase, tm.TestCase):
22772156
ext = '.xls'
@@ -2473,3 +2352,138 @@ def check_called(func):
24732352
check_called(
24742353
lambda: df.to_excel(
24752354
'something.xls', engine='dummy'))
2355+
2356+
2357+
@pytest.mark.parametrize('engine', [
2358+
'xlwt',
2359+
'xlsxwriter',
2360+
'openpyxl',
2361+
])
2362+
def test_styler_to_excel(engine):
2363+
def style(df):
2364+
# XXX: RGB colors not supported in xlwt
2365+
return DataFrame([['font-weight: bold', '', ''],
2366+
(['', '', ''] if engine == 'xlwt'
2367+
else ['', 'color: blue', '']),
2368+
['', '', 'text-decoration: underline'],
2369+
['border-style: solid', '', ''],
2370+
['', 'font-style: italic', ''],
2371+
['', '', 'text-align: right'],
2372+
(['', '', ''] if engine == 'xlwt' and False
2373+
else ['background-color: red', '', '']),
2374+
['', '', ''],
2375+
['', '', ''],
2376+
['', '', '']],
2377+
index=df.index, columns=df.columns)
2378+
2379+
def assert_equal_style(cell1, cell2):
2380+
# XXX: should find a better way to check equality
2381+
assert cell1.alignment.__dict__ == cell2.alignment.__dict__
2382+
assert cell1.border.__dict__ == cell2.border.__dict__
2383+
assert cell1.fill.__dict__ == cell2.fill.__dict__
2384+
assert cell1.font.__dict__ == cell2.font.__dict__
2385+
assert cell1.number_format == cell2.number_format
2386+
assert cell1.protection.__dict__ == cell2.protection.__dict__
2387+
2388+
def custom_converter(css):
2389+
# use bold iff there is custom style attached to the cell
2390+
if css.strip(' \n;'):
2391+
return {'font': {'bold': True}}
2392+
return {}
2393+
2394+
pytest.importorskip('jinja2')
2395+
pytest.importorskip(engine)
2396+
2397+
# Prepare spreadsheets
2398+
2399+
df = DataFrame(np.random.randn(10, 3))
2400+
with ensure_clean('.xlsx' if engine != 'xlwt' else '.xls') as path:
2401+
writer = ExcelWriter(path, engine=engine)
2402+
df.to_excel(writer, sheet_name='frame')
2403+
df.style.to_excel(writer, sheet_name='unstyled')
2404+
styled = df.style.apply(style, axis=None)
2405+
styled.to_excel(writer, sheet_name='styled')
2406+
ExcelFormatter(styled, style_converter=custom_converter).write(
2407+
writer, sheet_name='custom')
2408+
2409+
# For engines other than openpyxl 2, we only smoke test
2410+
if engine != 'openpyxl':
2411+
return
2412+
if not openpyxl_compat.is_compat(major_ver=2):
2413+
pytest.skip('incompatible openpyxl version')
2414+
2415+
# (1) compare DataFrame.to_excel and Styler.to_excel when unstyled
2416+
n_cells = 0
2417+
for col1, col2 in zip(writer.sheets['frame'].columns,
2418+
writer.sheets['unstyled'].columns):
2419+
assert len(col1) == len(col2)
2420+
for cell1, cell2 in zip(col1, col2):
2421+
assert cell1.value == cell2.value
2422+
assert_equal_style(cell1, cell2)
2423+
n_cells += 1
2424+
2425+
# ensure iteration actually happened:
2426+
assert n_cells == (10 + 1) * (3 + 1)
2427+
2428+
# (2) check styling with default converter
2429+
n_cells = 0
2430+
for col1, col2 in zip(writer.sheets['frame'].columns,
2431+
writer.sheets['styled'].columns):
2432+
assert len(col1) == len(col2)
2433+
for cell1, cell2 in zip(col1, col2):
2434+
ref = '%s%d' % (cell2.column, cell2.row)
2435+
# XXX: this isn't as strong a test as ideal; we should
2436+
# differences are exclusive
2437+
if ref == 'B2':
2438+
assert not cell1.font.bold
2439+
assert cell2.font.bold
2440+
elif ref == 'C3':
2441+
assert cell1.font.color.rgb != cell2.font.color.rgb
2442+
assert cell2.font.color.rgb == '000000FF'
2443+
elif ref == 'D4':
2444+
assert cell1.font.underline != cell2.font.underline
2445+
assert cell2.font.underline == 'single'
2446+
elif ref == 'B5':
2447+
assert not cell1.border.left.style
2448+
assert (cell2.border.top.style ==
2449+
cell2.border.right.style ==
2450+
cell2.border.bottom.style ==
2451+
cell2.border.left.style ==
2452+
'medium')
2453+
elif ref == 'C6':
2454+
assert not cell1.font.italic
2455+
assert cell2.font.italic
2456+
elif ref == 'D7':
2457+
assert (cell1.alignment.horizontal !=
2458+
cell2.alignment.horizontal)
2459+
assert cell2.alignment.horizontal == 'right'
2460+
elif ref == 'B8':
2461+
assert cell1.fill.fgColor.rgb != cell2.fill.fgColor.rgb
2462+
assert cell1.fill.patternType != cell2.fill.patternType
2463+
assert cell2.fill.fgColor.rgb == '00FF0000'
2464+
assert cell2.fill.patternType == 'solid'
2465+
else:
2466+
assert_equal_style(cell1, cell2)
2467+
2468+
assert cell1.value == cell2.value
2469+
n_cells += 1
2470+
2471+
assert n_cells == (10 + 1) * (3 + 1)
2472+
2473+
# (3) check styling with custom converter
2474+
n_cells = 0
2475+
for col1, col2 in zip(writer.sheets['frame'].columns,
2476+
writer.sheets['custom'].columns):
2477+
assert len(col1) == len(col2)
2478+
for cell1, cell2 in zip(col1, col2):
2479+
ref = '%s%d' % (cell2.column, cell2.row)
2480+
if ref in ('B2', 'C3', 'D4', 'B5', 'C6', 'D7', 'B8'):
2481+
assert not cell1.font.bold
2482+
assert cell2.font.bold
2483+
else:
2484+
assert_equal_style(cell1, cell2)
2485+
2486+
assert cell1.value == cell2.value
2487+
n_cells += 1
2488+
2489+
assert n_cells == (10 + 1) * (3 + 1)

0 commit comments

Comments
 (0)