diff --git a/ci/requirements-2.7.pip b/ci/requirements-2.7.pip
index eb796368e7820..ec10ee76c01e4 100644
--- a/ci/requirements-2.7.pip
+++ b/ci/requirements-2.7.pip
@@ -6,3 +6,4 @@ py
PyCrypto
mock
ipython
+cssdecl
diff --git a/ci/requirements-2.7_WIN.pip b/ci/requirements-2.7_WIN.pip
new file mode 100644
index 0000000000000..b37e06c6219c9
--- /dev/null
+++ b/ci/requirements-2.7_WIN.pip
@@ -0,0 +1 @@
+cssdecl
diff --git a/ci/requirements-3.5_DOC.pip b/ci/requirements-3.5_DOC.pip
new file mode 100644
index 0000000000000..b37e06c6219c9
--- /dev/null
+++ b/ci/requirements-3.5_DOC.pip
@@ -0,0 +1 @@
+cssdecl
diff --git a/ci/requirements-3.6.pip b/ci/requirements-3.6.pip
index e69de29bb2d1d..b37e06c6219c9 100644
--- a/ci/requirements-3.6.pip
+++ b/ci/requirements-3.6.pip
@@ -0,0 +1 @@
+cssdecl
diff --git a/doc/source/install.rst b/doc/source/install.rst
index 48d51e1200447..5402ca9159977 100644
--- a/doc/source/install.rst
+++ b/doc/source/install.rst
@@ -249,6 +249,7 @@ Optional Dependencies
* `openpyxl `__: openpyxl version 1.6.1
or higher (but lower than 2.0.0), or version 2.2 or higher, for writing .xlsx files (xlrd >= 0.9.0)
* `XlsxWriter `__: Alternative Excel writer
+ * `cssdecl `__: along with one of openpyxl or XlsxWriter for exporting styled DataFrames
* `Jinja2 `__: Template engine for conditional HTML formatting.
* `s3fs `__: necessary for Amazon S3 access (s3fs >= 0.0.7).
diff --git a/doc/source/style.ipynb b/doc/source/style.ipynb
index 4eeda491426b1..d9837d51b5b03 100644
--- a/doc/source/style.ipynb
+++ b/doc/source/style.ipynb
@@ -948,7 +948,9 @@
"- `vertical-align`\n",
"- `white-space: nowrap`\n",
"\n",
- "Only CSS2 named colors and hex colors of the form `#rgb` or `#rrggbb` are currently supported."
+ "Only CSS2 named colors and hex colors of the form `#rgb` or `#rrggbb` are currently supported.\n"
+ "\n",
+ "This feature requires the `cssdecl` package to be installed.",
]
},
{
diff --git a/doc/source/whatsnew/v0.21.0.txt b/doc/source/whatsnew/v0.21.0.txt
index 3df0a21facb02..7a0dab2520404 100644
--- a/doc/source/whatsnew/v0.21.0.txt
+++ b/doc/source/whatsnew/v0.21.0.txt
@@ -35,6 +35,7 @@ Other Enhancements
Backwards incompatible API changes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+- The ``DataFrame.style.to_excel()`` styled Excel export functionality now requires an external dependency, `cssdecl `_ (:issue:`16170`).
.. _whatsnew_0210.api:
diff --git a/pandas/io/formats/css.py b/pandas/io/formats/css.py
deleted file mode 100644
index d12d2373e1190..0000000000000
--- a/pandas/io/formats/css.py
+++ /dev/null
@@ -1,248 +0,0 @@
-"""Utilities for interpreting CSS from Stylers for formatting non-HTML outputs
-"""
-
-import re
-import warnings
-
-
-class CSSWarning(UserWarning):
- """This CSS syntax cannot currently be parsed"""
- pass
-
-
-class CSSResolver(object):
- """A callable for parsing and resolving CSS to atomic properties
-
- """
-
- INITIAL_STYLE = {
- }
-
- def __call__(self, declarations_str, inherited=None):
- """ the given declarations to atomic properties
-
- Parameters
- ----------
- declarations_str : str
- A list of CSS declarations
- inherited : dict, optional
- Atomic properties indicating the inherited style context in which
- declarations_str is to be resolved. ``inherited`` should already
- be resolved, i.e. valid output of this method.
-
- Returns
- -------
- props : dict
- Atomic CSS 2.2 properties
-
- Examples
- --------
- >>> resolve = CSSResolver()
- >>> inherited = {'font-family': 'serif', 'font-weight': 'bold'}
- >>> out = resolve('''
- ... border-color: BLUE RED;
- ... font-size: 1em;
- ... font-size: 2em;
- ... font-weight: normal;
- ... font-weight: inherit;
- ... ''', inherited)
- >>> sorted(out.items()) # doctest: +NORMALIZE_WHITESPACE
- [('border-bottom-color', 'blue'),
- ('border-left-color', 'red'),
- ('border-right-color', 'red'),
- ('border-top-color', 'blue'),
- ('font-family', 'serif'),
- ('font-size', '24pt'),
- ('font-weight', 'bold')]
- """
-
- props = dict(self.atomize(self.parse(declarations_str)))
- if inherited is None:
- inherited = {}
-
- # 1. resolve inherited, initial
- for prop, val in inherited.items():
- if prop not in props:
- props[prop] = val
-
- for prop, val in list(props.items()):
- if val == 'inherit':
- val = inherited.get(prop, 'initial')
- if val == 'initial':
- val = self.INITIAL_STYLE.get(prop)
-
- if val is None:
- # we do not define a complete initial stylesheet
- del props[prop]
- else:
- props[prop] = val
-
- # 2. resolve relative font size
- if props.get('font-size'):
- if 'font-size' in inherited:
- em_pt = inherited['font-size']
- assert em_pt[-2:] == 'pt'
- em_pt = float(em_pt[:-2])
- else:
- em_pt = None
- props['font-size'] = self.size_to_pt(
- props['font-size'], em_pt, conversions=self.FONT_SIZE_RATIOS)
-
- font_size = float(props['font-size'][:-2])
- else:
- font_size = None
-
- # 3. TODO: resolve other font-relative units
- for side in self.SIDES:
- prop = 'border-%s-width' % side
- if prop in props:
- props[prop] = self.size_to_pt(
- props[prop], em_pt=font_size,
- conversions=self.BORDER_WIDTH_RATIOS)
- for prop in ['margin-%s' % side, 'padding-%s' % side]:
- if prop in props:
- # TODO: support %
- props[prop] = self.size_to_pt(
- props[prop], em_pt=font_size,
- conversions=self.MARGIN_RATIOS)
-
- return props
-
- UNIT_RATIOS = {
- 'rem': ('pt', 12),
- 'ex': ('em', .5),
- # 'ch':
- 'px': ('pt', .75),
- 'pc': ('pt', 12),
- 'in': ('pt', 72),
- 'cm': ('in', 1 / 2.54),
- 'mm': ('in', 1 / 25.4),
- 'q': ('mm', .25),
- '!!default': ('em', 0),
- }
-
- FONT_SIZE_RATIOS = UNIT_RATIOS.copy()
- FONT_SIZE_RATIOS.update({
- '%': ('em', .01),
- 'xx-small': ('rem', .5),
- 'x-small': ('rem', .625),
- 'small': ('rem', .8),
- 'medium': ('rem', 1),
- 'large': ('rem', 1.125),
- 'x-large': ('rem', 1.5),
- 'xx-large': ('rem', 2),
- 'smaller': ('em', 1 / 1.2),
- 'larger': ('em', 1.2),
- '!!default': ('em', 1),
- })
-
- MARGIN_RATIOS = UNIT_RATIOS.copy()
- MARGIN_RATIOS.update({
- 'none': ('pt', 0),
- })
-
- BORDER_WIDTH_RATIOS = UNIT_RATIOS.copy()
- BORDER_WIDTH_RATIOS.update({
- 'none': ('pt', 0),
- 'thick': ('px', 4),
- 'medium': ('px', 2),
- 'thin': ('px', 1),
- # Default: medium only if solid
- })
-
- def size_to_pt(self, in_val, em_pt=None, conversions=UNIT_RATIOS):
- def _error():
- warnings.warn('Unhandled size: %r' % in_val, CSSWarning)
- return self.size_to_pt('1!!default', conversions=conversions)
-
- try:
- val, unit = re.match(r'^(\S*?)([a-zA-Z%!].*)', in_val).groups()
- except AttributeError:
- return _error()
- if val == '':
- # hack for 'large' etc.
- val = 1
- else:
- try:
- val = float(val)
- except ValueError:
- return _error()
-
- while unit != 'pt':
- if unit == 'em':
- if em_pt is None:
- unit = 'rem'
- else:
- val *= em_pt
- unit = 'pt'
- continue
-
- try:
- unit, mul = conversions[unit]
- except KeyError:
- return _error()
- val *= mul
-
- val = round(val, 5)
- if int(val) == val:
- size_fmt = '%d'
- else:
- size_fmt = '%f'
- return (size_fmt + 'pt') % val
-
- def atomize(self, declarations):
- for prop, value in declarations:
- attr = 'expand_' + prop.replace('-', '_')
- try:
- expand = getattr(self, attr)
- except AttributeError:
- yield prop, value
- else:
- for prop, value in expand(prop, value):
- yield prop, value
-
- SIDE_SHORTHANDS = {
- 1: [0, 0, 0, 0],
- 2: [0, 1, 0, 1],
- 3: [0, 1, 2, 1],
- 4: [0, 1, 2, 3],
- }
- SIDES = ('top', 'right', 'bottom', 'left')
-
- def _side_expander(prop_fmt):
- def expand(self, prop, value):
- tokens = value.split()
- try:
- mapping = self.SIDE_SHORTHANDS[len(tokens)]
- except KeyError:
- warnings.warn('Could not expand "%s: %s"' % (prop, value),
- CSSWarning)
- return
- for key, idx in zip(self.SIDES, mapping):
- yield prop_fmt % key, tokens[idx]
-
- return expand
-
- expand_border_color = _side_expander('border-%s-color')
- expand_border_style = _side_expander('border-%s-style')
- expand_border_width = _side_expander('border-%s-width')
- expand_margin = _side_expander('margin-%s')
- expand_padding = _side_expander('padding-%s')
-
- def parse(self, declarations_str):
- """Generates (prop, value) pairs from declarations
-
- In a future version may generate parsed tokens from tinycss/tinycss2
- """
- for decl in declarations_str.split(';'):
- if not decl.strip():
- continue
- prop, sep, val = decl.partition(':')
- prop = prop.strip().lower()
- # TODO: don't lowercase case sensitive parts of values (strings)
- val = val.strip().lower()
- if sep:
- yield prop, val
- else:
- warnings.warn('Ill-formatted attribute: expected a colon '
- 'in %r' % decl, CSSWarning)
diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py
index 80587f9a752c7..b9f64f0b833cf 100644
--- a/pandas/io/formats/excel.py
+++ b/pandas/io/formats/excel.py
@@ -8,7 +8,6 @@
import numpy as np
from pandas.compat import reduce, string_types
-from pandas.io.formats.css import CSSResolver, CSSWarning
from pandas.io.formats.printing import pprint_thing
from pandas.core.dtypes.common import is_float
import pandas._libs.lib as lib
@@ -61,14 +60,17 @@ class CSSToExcelConverter(object):
# without monkey-patching.
def __init__(self, inherited=None):
+ try:
+ from cssdecl import CSS22Resolver
+ except ImportError:
+ raise ImportError("cssdecl is not installed and is requied to "
+ "convert styles to Excel")
+ self.compute_css = CSS22Resolver().resolve_string
if inherited is not None:
- inherited = self.compute_css(inherited,
- self.compute_css.INITIAL_STYLE)
+ inherited = self.compute_css(inherited)
self.inherited = inherited
- compute_css = CSSResolver()
-
def __call__(self, declarations_str):
"""Convert CSS declarations to ExcelWriter style
@@ -302,6 +304,7 @@ def color_to_excel(self, val):
try:
return self.NAMED_COLORS[val]
except KeyError:
+ from cssdecl import CSSWarning
warnings.warn('Unhandled colour format: %r' % val, CSSWarning)
@@ -333,7 +336,8 @@ class ExcelFormatter(object):
A `'-'` sign will be added in front of -inf.
style_converter : callable, optional
This translates Styler styles (CSS) into ExcelWriter styles.
- Defaults to ``CSSToExcelConverter()``.
+ Defaults to ``CSSToExcelConverter()`` which requires the ``cssdecl``
+ package installed.
It should have signature css_declarations string -> excel style.
This is only called for body cells.
"""
diff --git a/pandas/tests/io/formats/test_css.py b/pandas/tests/io/formats/test_css.py
deleted file mode 100644
index c07856dc63602..0000000000000
--- a/pandas/tests/io/formats/test_css.py
+++ /dev/null
@@ -1,186 +0,0 @@
-import pytest
-
-from pandas.util import testing as tm
-from pandas.io.formats.css import CSSResolver, CSSWarning
-
-
-def assert_resolves(css, props, inherited=None):
- resolve = CSSResolver()
- actual = resolve(css, inherited=inherited)
- assert props == actual
-
-
-def assert_same_resolution(css1, css2, inherited=None):
- resolve = CSSResolver()
- resolved1 = resolve(css1, inherited=inherited)
- resolved2 = resolve(css2, inherited=inherited)
- assert resolved1 == resolved2
-
-
-@pytest.mark.parametrize('name,norm,abnorm', [
- ('whitespace', 'hello: world; foo: bar',
- ' \t hello \t :\n world \n ; \n foo: \tbar\n\n'),
- ('case', 'hello: world; foo: bar', 'Hello: WORLD; foO: bar'),
- ('empty-decl', 'hello: world; foo: bar',
- '; hello: world;; foo: bar;\n; ;'),
- ('empty-list', '', ';'),
-])
-def test_css_parse_normalisation(name, norm, abnorm):
- assert_same_resolution(norm, abnorm)
-
-
-@pytest.mark.parametrize(
- 'invalid_css,remainder', [
- # No colon
- ('hello-world', ''),
- ('border-style: solid; hello-world', 'border-style: solid'),
- ('border-style: solid; hello-world; font-weight: bold',
- 'border-style: solid; font-weight: bold'),
- # Unclosed string fail
- # Invalid size
- ('font-size: blah', 'font-size: 1em'),
- ('font-size: 1a2b', 'font-size: 1em'),
- ('font-size: 1e5pt', 'font-size: 1em'),
- ('font-size: 1+6pt', 'font-size: 1em'),
- ('font-size: 1unknownunit', 'font-size: 1em'),
- ('font-size: 10', 'font-size: 1em'),
- ('font-size: 10 pt', 'font-size: 1em'),
- ])
-def test_css_parse_invalid(invalid_css, remainder):
- with tm.assert_produces_warning(CSSWarning):
- assert_same_resolution(invalid_css, remainder)
-
- # TODO: we should be checking that in other cases no warnings are raised
-
-
-@pytest.mark.parametrize(
- 'shorthand,expansions',
- [('margin', ['margin-top', 'margin-right',
- 'margin-bottom', 'margin-left']),
- ('padding', ['padding-top', 'padding-right',
- 'padding-bottom', 'padding-left']),
- ('border-width', ['border-top-width', 'border-right-width',
- 'border-bottom-width', 'border-left-width']),
- ('border-color', ['border-top-color', 'border-right-color',
- 'border-bottom-color', 'border-left-color']),
- ('border-style', ['border-top-style', 'border-right-style',
- 'border-bottom-style', 'border-left-style']),
- ])
-def test_css_side_shorthands(shorthand, expansions):
- top, right, bottom, left = expansions
-
- assert_resolves('%s: 1pt' % shorthand,
- {top: '1pt', right: '1pt',
- bottom: '1pt', left: '1pt'})
-
- assert_resolves('%s: 1pt 4pt' % shorthand,
- {top: '1pt', right: '4pt',
- bottom: '1pt', left: '4pt'})
-
- assert_resolves('%s: 1pt 4pt 2pt' % shorthand,
- {top: '1pt', right: '4pt',
- bottom: '2pt', left: '4pt'})
-
- assert_resolves('%s: 1pt 4pt 2pt 0pt' % shorthand,
- {top: '1pt', right: '4pt',
- bottom: '2pt', left: '0pt'})
-
- with tm.assert_produces_warning(CSSWarning):
- assert_resolves('%s: 1pt 1pt 1pt 1pt 1pt' % shorthand,
- {})
-
-
-@pytest.mark.parametrize('style,inherited,equiv', [
- ('margin: 1px; margin: 2px', '',
- 'margin: 2px'),
- ('margin: 1px', 'margin: 2px',
- 'margin: 1px'),
- ('margin: 1px; margin: inherit', 'margin: 2px',
- 'margin: 2px'),
- ('margin: 1px; margin-top: 2px', '',
- 'margin-left: 1px; margin-right: 1px; ' +
- 'margin-bottom: 1px; margin-top: 2px'),
- ('margin-top: 2px', 'margin: 1px',
- 'margin: 1px; margin-top: 2px'),
- ('margin: 1px', 'margin-top: 2px',
- 'margin: 1px'),
- ('margin: 1px; margin-top: inherit', 'margin: 2px',
- 'margin: 1px; margin-top: 2px'),
-])
-def test_css_precedence(style, inherited, equiv):
- resolve = CSSResolver()
- inherited_props = resolve(inherited)
- style_props = resolve(style, inherited=inherited_props)
- equiv_props = resolve(equiv)
- assert style_props == equiv_props
-
-
-@pytest.mark.parametrize('style,equiv', [
- ('margin: 1px; margin-top: inherit',
- 'margin-bottom: 1px; margin-right: 1px; margin-left: 1px'),
- ('margin-top: inherit', ''),
- ('margin-top: initial', ''),
-])
-def test_css_none_absent(style, equiv):
- assert_same_resolution(style, equiv)
-
-
-@pytest.mark.parametrize('size,resolved', [
- ('xx-small', '6pt'),
- ('x-small', '%fpt' % 7.5),
- ('small', '%fpt' % 9.6),
- ('medium', '12pt'),
- ('large', '%fpt' % 13.5),
- ('x-large', '18pt'),
- ('xx-large', '24pt'),
-
- ('8px', '6pt'),
- ('1.25pc', '15pt'),
- ('.25in', '18pt'),
- ('02.54cm', '72pt'),
- ('25.4mm', '72pt'),
- ('101.6q', '72pt'),
- ('101.6q', '72pt'),
-])
-@pytest.mark.parametrize('relative_to', # invariant to inherited size
- [None, '16pt'])
-def test_css_absolute_font_size(size, relative_to, resolved):
- if relative_to is None:
- inherited = None
- else:
- inherited = {'font-size': relative_to}
- assert_resolves('font-size: %s' % size, {'font-size': resolved},
- inherited=inherited)
-
-
-@pytest.mark.parametrize('size,relative_to,resolved', [
- ('1em', None, '12pt'),
- ('1.0em', None, '12pt'),
- ('1.25em', None, '15pt'),
- ('1em', '16pt', '16pt'),
- ('1.0em', '16pt', '16pt'),
- ('1.25em', '16pt', '20pt'),
- ('1rem', '16pt', '12pt'),
- ('1.0rem', '16pt', '12pt'),
- ('1.25rem', '16pt', '15pt'),
- ('100%', None, '12pt'),
- ('125%', None, '15pt'),
- ('100%', '16pt', '16pt'),
- ('125%', '16pt', '20pt'),
- ('2ex', None, '12pt'),
- ('2.0ex', None, '12pt'),
- ('2.50ex', None, '15pt'),
- ('inherit', '16pt', '16pt'),
-
- ('smaller', None, '10pt'),
- ('smaller', '18pt', '15pt'),
- ('larger', None, '%fpt' % 14.4),
- ('larger', '15pt', '18pt'),
-])
-def test_css_relative_font_size(size, relative_to, resolved):
- if relative_to is None:
- inherited = None
- else:
- inherited = {'font-size': relative_to}
- assert_resolves('font-size: %s' % size, {'font-size': resolved},
- inherited=inherited)
diff --git a/pandas/tests/io/formats/test_to_excel.py b/pandas/tests/io/formats/test_to_excel.py
index 26a9bb018f30a..30e93fb6ab45f 100644
--- a/pandas/tests/io/formats/test_to_excel.py
+++ b/pandas/tests/io/formats/test_to_excel.py
@@ -5,7 +5,15 @@
import pytest
+import numpy as np
+
+import pandas as pd
+from pandas.compat import openpyxl_compat
+from pandas.io.excel import ExcelWriter
+from pandas import DataFrame
from pandas.io.formats.excel import CSSToExcelConverter
+from pandas.io.formats.excel import ExcelFormatter
+from pandas.util.testing import ensure_clean
@pytest.mark.parametrize('css,expected', [
@@ -172,11 +180,13 @@
{'alignment': {'wrap_text': True}}),
])
def test_css_to_excel(css, expected):
+ pytest.importorskip('cssdecl')
convert = CSSToExcelConverter()
assert expected == convert(css)
def test_css_to_excel_multiple():
+ pytest.importorskip('cssdecl')
convert = CSSToExcelConverter()
actual = convert('''
font-weight: bold;
@@ -210,5 +220,222 @@ def test_css_to_excel_multiple():
{'font': {'italic': True}}),
])
def test_css_to_excel_inherited(css, inherited, expected):
+ pytest.importorskip('cssdecl')
convert = CSSToExcelConverter(inherited)
assert expected == convert(css)
+
+
+@pytest.fixture
+def styled_dataframe():
+ def style(df):
+ # XXX: RGB colors not supported in xlwt
+ 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)
+
+ pytest.importorskip('jinja2')
+ df = DataFrame(np.random.randn(10, 3))
+ return df.style.apply(style, axis=None)
+
+
+def assert_equal_style(cell1, cell2):
+ # XXX: should find a better way to check equality
+ # Neither OpenPyXl's Cell, nor its style objects have __eq__ defined
+ 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__
+
+
+@pytest.mark.parametrize('engine', [
+ pytest.mark.xfail('xlwt', reason='xlwt does not support '
+ 'openpyxl-compatible style dicts'),
+ 'xlsxwriter',
+ 'openpyxl',
+])
+def test_styler_to_excel(engine, styled_dataframe):
+ 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('cssdecl')
+ pytest.importorskip(engine)
+
+ if engine == 'openpyxl' and openpyxl_compat.is_compat(major_ver=1):
+ pytest.xfail('openpyxl1 does not support some openpyxl2-compatible '
+ 'style dicts')
+
+ styled = styled_dataframe
+ df = styled.data
+
+ # Prepare spreadsheets
+
+ 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.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)
+
+
+@pytest.mark.parametrize('engine', [
+ pytest.mark.xfail('xlwt', reason='xlwt does not support '
+ 'openpyxl-compatible style dicts'),
+ 'xlsxwriter',
+ 'openpyxl',
+])
+def test_styler_to_excel_no_cssdecl(engine, styled_dataframe):
+ def custom_converter(css):
+ # use bold iff there is custom style attached to the cell
+ if css.strip(' \n;'):
+ return {'font': {'bold': True}}
+ return {}
+
+ try:
+ import cssdecl # noqa
+ except ImportError:
+ with pytest.raises(ImportError) as rec:
+ pd.DataFrame({"A": [1, 2]}).style.to_excel(engine)
+ assert rec.match("not installed")
+ else:
+ pytest.skip('Test only run if cssdecl not installed')
+
+ pytest.importorskip(engine)
+
+ styled = styled_dataframe
+ df = styled.data
+
+ # Prepare spreadsheets
+
+ with ensure_clean('.xlsx' if engine != 'xlwt' else '.xls') as path:
+ writer = ExcelWriter(path, engine=engine)
+ df.to_excel(writer, sheet_name='frame')
+ with pytest.raises(ImportError):
+ # default style_converter requires cssdecl
+ df.style.to_excel(writer, sheet_name='unstyled')
+ styled.to_excel(writer, sheet_name='styled')
+
+ ExcelFormatter(styled, style_converter=custom_converter).write(
+ writer, sheet_name='custom')
+
+ writer.save()
+
+ openpyxl = pytest.importorskip('openpyxl')
+ if openpyxl_compat.is_compat(major_ver=1):
+ # smoke test only
+ return
+
+ wb = openpyxl.load_workbook(path)
+
+ # 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)
diff --git a/pandas/tests/io/test_excel.py b/pandas/tests/io/test_excel.py
index b4a5b24616728..1de016aadd358 100644
--- a/pandas/tests/io/test_excel.py
+++ b/pandas/tests/io/test_excel.py
@@ -17,7 +17,6 @@
import pandas as pd
from pandas import DataFrame, Index, MultiIndex
-from pandas.io.formats.excel import ExcelFormatter
from pandas.io.parsers import read_csv
from pandas.io.excel import (
ExcelFile, ExcelWriter, read_excel, _XlwtWriter, _Openpyxl1Writer,
@@ -2361,141 +2360,3 @@ def check_called(func):
check_called(
lambda: df.to_excel(
'something.xls', engine='dummy'))
-
-
-@pytest.mark.parametrize('engine', [
- pytest.mark.xfail('xlwt', reason='xlwt does not support '
- 'openpyxl-compatible style dicts'),
- 'xlsxwriter',
- 'openpyxl',
-])
-def test_styler_to_excel(engine):
- def style(df):
- # XXX: RGB colors not supported in xlwt
- 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 {}
-
- pytest.importorskip('jinja2')
- pytest.importorskip(engine)
-
- if engine == 'openpyxl' and openpyxl_compat.is_compat(major_ver=1):
- pytest.xfail('openpyxl1 does not support some openpyxl2-compatible '
- 'style dicts')
-
- # 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)