Skip to content

Commit c6bcc99

Browse files
committed
Merge branch 'Themanwithoutaplan-openpyxl22'
2 parents 1e84f1d + 6a86bad commit c6bcc99

File tree

10 files changed

+246
-26
lines changed

10 files changed

+246
-26
lines changed

ci/requirements-2.6.pip

+2
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
blosc
2+
openpyxl
3+
argparse

ci/requirements-2.6.run

-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ pytz=2013b
44
scipy=0.11.0
55
xlwt=0.7.5
66
xlrd=0.9.2
7-
openpyxl=2.0.3
87
statsmodels=0.4.3
98
html5lib=1.0b2
109
beautiful-soup=4.2.0

ci/requirements-3.3.pip

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
blosc
2+
openpyxl

ci/requirements-3.3.run

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
python-dateutil
22
pytz=2013b
33
numpy=1.8.0
4-
openpyxl=1.6.2
54
xlsxwriter=0.4.6
65
xlrd=0.9.2
6+
xlwt
77
html5lib=1.0b2
88
numexpr
99
pytables

doc/source/io.rst

+8-3
Original file line numberDiff line numberDiff line change
@@ -2230,6 +2230,10 @@ Writing Excel Files to Memory
22302230
Pandas supports writing Excel files to buffer-like objects such as ``StringIO`` or
22312231
``BytesIO`` using :class:`~pandas.io.excel.ExcelWriter`.
22322232

2233+
.. versionadded:: 0.17
2234+
2235+
Added support for Openpyxl >= 2.2
2236+
22332237
.. code-block:: python
22342238
22352239
# Safe import for either Python 2.x or 3.x
@@ -2279,14 +2283,15 @@ config options <options>` ``io.excel.xlsx.writer`` and
22792283
files if `Xlsxwriter`_ is not available.
22802284

22812285
.. _XlsxWriter: http://xlsxwriter.readthedocs.org
2282-
.. _openpyxl: http://packages.python.org/openpyxl/
2286+
.. _openpyxl: http://openpyxl.readthedocs.org/
22832287
.. _xlwt: http://www.python-excel.org
22842288

22852289
To specify which writer you want to use, you can pass an engine keyword
22862290
argument to ``to_excel`` and to ``ExcelWriter``. The built-in engines are:
22872291

2288-
- ``openpyxl``: This includes stable support for OpenPyxl 1.6.1 up to but
2289-
not including 2.0.0, and experimental support for OpenPyxl 2.0.0 and later.
2292+
- ``openpyxl``: This includes stable support for Openpyxl from 1.6.1. However,
2293+
it is advised to use version 2.2 and higher, especially when working with
2294+
styles.
22902295
- ``xlsxwriter``
22912296
- ``xlwt``
22922297

doc/source/release.rst

+2-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ Highlights include:
6464
- Support for reading SAS xport files, see :ref:`here <whatsnew_0170.enhancements.sas_xport>`
6565
- Documentation comparing SAS to *pandas*, see :ref:`here <compare_with_sas>`
6666
- Removal of the automatic TimeSeries broadcasting, deprecated since 0.8.0, see :ref:`here <whatsnew_0170.prior_deprecations>`
67-
- Compatibility with Python 3.5
67+
- Compatibility with Python 3.5 (:issue:`11097`)
68+
- Compatibility with matplotlib 1.5.0 (:issue:`11111`)
6869

6970
See the :ref:`v0.17.0 Whatsnew <whatsnew_0170>` overview for an extensive list
7071
of all enhancements and bugs that have been fixed in 0.17.0.

doc/source/whatsnew/v0.17.0.txt

+2
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ engine, they are mapped to NumPy calls.
278278

279279
Changes to Excel with ``MultiIndex``
280280
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
281+
281282
In version 0.16.2 a ``DataFrame`` with ``MultiIndex`` columns could not be written to Excel via ``to_excel``.
282283
That functionality has been added (:issue:`10564`), along with updating ``read_excel`` so that the data can
283284
be read back with no loss of information by specifying which columns/rows make up the ``MultiIndex``
@@ -336,6 +337,7 @@ Google BigQuery Enhancements
336337
Other enhancements
337338
^^^^^^^^^^^^^^^^^^
338339

340+
- Support for ``openpyxl`` >= 2.2. The API for style support is now stable (:issue:`10125`)
339341
- ``merge`` now accepts the argument ``indicator`` which adds a Categorical-type column (by default called ``_merge``) to the output object that takes on the values (:issue:`8790`)
340342

341343
=================================== ================

pandas/io/excel.py

+74-4
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,10 @@ def get_writer(engine_name):
5757
# make sure we make the intelligent choice for the user
5858
if LooseVersion(openpyxl.__version__) < '2.0.0':
5959
return _writers['openpyxl1']
60+
elif LooseVersion(openpyxl.__version__) < '2.2.0':
61+
return _writers['openpyxl20']
6062
else:
61-
return _writers['openpyxl2']
63+
return _writers['openpyxl22']
6264
except ImportError:
6365
# fall through to normal exception handling below
6466
pass
@@ -760,11 +762,11 @@ class _OpenpyxlWriter(_Openpyxl1Writer):
760762
register_writer(_OpenpyxlWriter)
761763

762764

763-
class _Openpyxl2Writer(_Openpyxl1Writer):
765+
class _Openpyxl20Writer(_Openpyxl1Writer):
764766
"""
765767
Note: Support for OpenPyxl v2 is currently EXPERIMENTAL (GH7565).
766768
"""
767-
engine = 'openpyxl2'
769+
engine = 'openpyxl20'
768770
openpyxl_majorver = 2
769771

770772
def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0):
@@ -1172,8 +1174,76 @@ def _convert_to_protection(cls, protection_dict):
11721174
return Protection(**protection_dict)
11731175

11741176

1175-
register_writer(_Openpyxl2Writer)
1177+
register_writer(_Openpyxl20Writer)
11761178

1179+
class _Openpyxl22Writer(_Openpyxl20Writer):
1180+
"""
1181+
Note: Support for OpenPyxl v2.2 is currently EXPERIMENTAL (GH7565).
1182+
"""
1183+
engine = 'openpyxl22'
1184+
openpyxl_majorver = 2
1185+
1186+
def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0):
1187+
# Write the frame cells using openpyxl.
1188+
from openpyxl import styles
1189+
1190+
sheet_name = self._get_sheet_name(sheet_name)
1191+
1192+
_style_cache = {}
1193+
1194+
if sheet_name in self.sheets:
1195+
wks = self.sheets[sheet_name]
1196+
else:
1197+
wks = self.book.create_sheet()
1198+
wks.title = sheet_name
1199+
self.sheets[sheet_name] = wks
1200+
1201+
for cell in cells:
1202+
xcell = wks.cell(
1203+
row=startrow + cell.row + 1,
1204+
column=startcol + cell.col + 1
1205+
)
1206+
xcell.value = _conv_value(cell.val)
1207+
1208+
style_kwargs = {}
1209+
if cell.style:
1210+
key = str(cell.style)
1211+
style_kwargs = _style_cache.get(key)
1212+
if style_kwargs is None:
1213+
style_kwargs = self._convert_to_style_kwargs(cell.style)
1214+
_style_cache[key] = style_kwargs
1215+
1216+
if style_kwargs:
1217+
for k, v in style_kwargs.items():
1218+
setattr(xcell, k, v)
1219+
1220+
if cell.mergestart is not None and cell.mergeend is not None:
1221+
1222+
wks.merge_cells(
1223+
start_row=startrow + cell.row + 1,
1224+
start_column=startcol + cell.col + 1,
1225+
end_column=startcol + cell.mergeend + 1,
1226+
end_row=startrow + cell.mergeend + 1
1227+
)
1228+
1229+
# When cells are merged only the top-left cell is preserved
1230+
# The behaviour of the other cells in a merged range is undefined
1231+
if style_kwargs:
1232+
first_row = startrow + cell.row + 1
1233+
last_row = startrow + cell.mergestart + 1
1234+
first_col = startcol + cell.col + 1
1235+
last_col = startcol + cell.mergeend + 1
1236+
1237+
for row in range(first_row, last_row + 1):
1238+
for col in range(first_col, last_col + 1):
1239+
if row == first_row and col == first_col:
1240+
# Ignore first cell. It is already handled.
1241+
continue
1242+
xcell = wks.cell(column=col, row=row)
1243+
for k, v in style_kwargs.items():
1244+
setattr(xcell, k, v)
1245+
1246+
register_writer(_Openpyxl22Writer)
11771247

11781248
class _XlwtWriter(ExcelWriter):
11791249
engine = 'xlwt'

pandas/io/tests/test_excel.py

+135-15
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from pandas.io.parsers import read_csv
2020
from pandas.io.excel import (
2121
ExcelFile, ExcelWriter, read_excel, _XlwtWriter, _Openpyxl1Writer,
22-
_Openpyxl2Writer, register_writer, _XlsxWriter
22+
_Openpyxl20Writer, _Openpyxl22Writer, register_writer, _XlsxWriter
2323
)
2424
from pandas.io.common import URLError
2525
from pandas.util.testing import ensure_clean, makeCustomDataframe as mkdf
@@ -1470,17 +1470,28 @@ def test_to_excel_styleconverter(self):
14701470
xlsx_style.alignment.vertical)
14711471

14721472

1473+
def skip_openpyxl_gt21(cls):
1474+
"""Skip a TestCase instance if openpyxl >= 2.2"""
1475+
1476+
@classmethod
1477+
def setUpClass(cls):
1478+
_skip_if_no_openpyxl()
1479+
import openpyxl
1480+
ver = openpyxl.__version__
1481+
if not (ver >= LooseVersion('2.0.0') and ver < LooseVersion('2.2.0')):
1482+
raise nose.SkipTest("openpyxl >= 2.2")
1483+
1484+
cls.setUpClass = setUpClass
1485+
return cls
1486+
14731487
@raise_on_incompat_version(2)
1474-
class Openpyxl2Tests(ExcelWriterBase, tm.TestCase):
1488+
@skip_openpyxl_gt21
1489+
class Openpyxl20Tests(ExcelWriterBase, tm.TestCase):
14751490
ext = '.xlsx'
1476-
engine_name = 'openpyxl2'
1491+
engine_name = 'openpyxl20'
14771492
check_skip = staticmethod(lambda *args, **kwargs: None)
14781493

14791494
def test_to_excel_styleconverter(self):
1480-
_skip_if_no_openpyxl()
1481-
if not openpyxl_compat.is_compat(major_ver=2):
1482-
raise nose.SkipTest('incompatiable openpyxl version')
1483-
14841495
import openpyxl
14851496
from openpyxl import styles
14861497

@@ -1532,7 +1543,7 @@ def test_to_excel_styleconverter(self):
15321543

15331544
protection = styles.Protection(locked=True, hidden=False)
15341545

1535-
kw = _Openpyxl2Writer._convert_to_style_kwargs(hstyle)
1546+
kw = _Openpyxl20Writer._convert_to_style_kwargs(hstyle)
15361547
self.assertEqual(kw['font'], font)
15371548
self.assertEqual(kw['border'], border)
15381549
self.assertEqual(kw['alignment'], alignment)
@@ -1542,7 +1553,116 @@ def test_to_excel_styleconverter(self):
15421553

15431554

15441555
def test_write_cells_merge_styled(self):
1556+
from pandas.core.format import ExcelCell
1557+
from openpyxl import styles
1558+
1559+
sheet_name='merge_styled'
1560+
1561+
sty_b1 = {'font': {'color': '00FF0000'}}
1562+
sty_a2 = {'font': {'color': '0000FF00'}}
1563+
1564+
initial_cells = [
1565+
ExcelCell(col=1, row=0, val=42, style=sty_b1),
1566+
ExcelCell(col=0, row=1, val=99, style=sty_a2),
1567+
]
1568+
1569+
sty_merged = {'font': { 'color': '000000FF', 'bold': True }}
1570+
sty_kwargs = _Openpyxl20Writer._convert_to_style_kwargs(sty_merged)
1571+
openpyxl_sty_merged = styles.Style(**sty_kwargs)
1572+
merge_cells = [
1573+
ExcelCell(col=0, row=0, val='pandas',
1574+
mergestart=1, mergeend=1, style=sty_merged),
1575+
]
1576+
1577+
with ensure_clean('.xlsx') as path:
1578+
writer = _Openpyxl20Writer(path)
1579+
writer.write_cells(initial_cells, sheet_name=sheet_name)
1580+
writer.write_cells(merge_cells, sheet_name=sheet_name)
1581+
1582+
wks = writer.sheets[sheet_name]
1583+
xcell_b1 = wks.cell('B1')
1584+
xcell_a2 = wks.cell('A2')
1585+
self.assertEqual(xcell_b1.style, openpyxl_sty_merged)
1586+
self.assertEqual(xcell_a2.style, openpyxl_sty_merged)
1587+
1588+
def skip_openpyxl_lt22(cls):
1589+
"""Skip a TestCase instance if openpyxl < 2.2"""
1590+
1591+
@classmethod
1592+
def setUpClass(cls):
15451593
_skip_if_no_openpyxl()
1594+
import openpyxl
1595+
ver = openpyxl.__version__
1596+
if ver < LooseVersion('2.2.0'):
1597+
raise nose.SkipTest("openpyxl < 2.2")
1598+
1599+
cls.setUpClass = setUpClass
1600+
return cls
1601+
1602+
@raise_on_incompat_version(2)
1603+
@skip_openpyxl_lt22
1604+
class Openpyxl22Tests(ExcelWriterBase, tm.TestCase):
1605+
ext = '.xlsx'
1606+
engine_name = 'openpyxl22'
1607+
check_skip = staticmethod(lambda *args, **kwargs: None)
1608+
1609+
def test_to_excel_styleconverter(self):
1610+
import openpyxl
1611+
from openpyxl import styles
1612+
1613+
hstyle = {
1614+
"font": {
1615+
"color": '00FF0000',
1616+
"bold": True,
1617+
},
1618+
"borders": {
1619+
"top": "thin",
1620+
"right": "thin",
1621+
"bottom": "thin",
1622+
"left": "thin",
1623+
},
1624+
"alignment": {
1625+
"horizontal": "center",
1626+
"vertical": "top",
1627+
},
1628+
"fill": {
1629+
"patternType": 'solid',
1630+
'fgColor': {
1631+
'rgb': '006666FF',
1632+
'tint': 0.3,
1633+
},
1634+
},
1635+
"number_format": {
1636+
"format_code": "0.00"
1637+
},
1638+
"protection": {
1639+
"locked": True,
1640+
"hidden": False,
1641+
},
1642+
}
1643+
1644+
font_color = styles.Color('00FF0000')
1645+
font = styles.Font(bold=True, color=font_color)
1646+
side = styles.Side(style=styles.borders.BORDER_THIN)
1647+
border = styles.Border(top=side, right=side, bottom=side, left=side)
1648+
alignment = styles.Alignment(horizontal='center', vertical='top')
1649+
fill_color = styles.Color(rgb='006666FF', tint=0.3)
1650+
fill = styles.PatternFill(patternType='solid', fgColor=fill_color)
1651+
1652+
number_format = '0.00'
1653+
1654+
protection = styles.Protection(locked=True, hidden=False)
1655+
1656+
kw = _Openpyxl22Writer._convert_to_style_kwargs(hstyle)
1657+
self.assertEqual(kw['font'], font)
1658+
self.assertEqual(kw['border'], border)
1659+
self.assertEqual(kw['alignment'], alignment)
1660+
self.assertEqual(kw['fill'], fill)
1661+
self.assertEqual(kw['number_format'], number_format)
1662+
self.assertEqual(kw['protection'], protection)
1663+
1664+
1665+
def test_write_cells_merge_styled(self):
15461666
if not openpyxl_compat.is_compat(major_ver=2):
15471667
raise nose.SkipTest('incompatiable openpyxl version')
15481668

@@ -1560,23 +1680,23 @@ def test_write_cells_merge_styled(self):
15601680
]
15611681

15621682
sty_merged = {'font': { 'color': '000000FF', 'bold': True }}
1563-
sty_kwargs = _Openpyxl2Writer._convert_to_style_kwargs(sty_merged)
1564-
openpyxl_sty_merged = styles.Style(**sty_kwargs)
1683+
sty_kwargs = _Openpyxl22Writer._convert_to_style_kwargs(sty_merged)
1684+
openpyxl_sty_merged = sty_kwargs['font']
15651685
merge_cells = [
15661686
ExcelCell(col=0, row=0, val='pandas',
15671687
mergestart=1, mergeend=1, style=sty_merged),
15681688
]
15691689

15701690
with ensure_clean('.xlsx') as path:
1571-
writer = _Openpyxl2Writer(path)
1691+
writer = _Openpyxl22Writer(path)
15721692
writer.write_cells(initial_cells, sheet_name=sheet_name)
15731693
writer.write_cells(merge_cells, sheet_name=sheet_name)
15741694

15751695
wks = writer.sheets[sheet_name]
15761696
xcell_b1 = wks.cell('B1')
15771697
xcell_a2 = wks.cell('A2')
1578-
self.assertEqual(xcell_b1.style, openpyxl_sty_merged)
1579-
self.assertEqual(xcell_a2.style, openpyxl_sty_merged)
1698+
self.assertEqual(xcell_b1.font, openpyxl_sty_merged)
1699+
self.assertEqual(xcell_a2.font, openpyxl_sty_merged)
15801700

15811701

15821702
class XlwtTests(ExcelWriterBase, tm.TestCase):
@@ -1676,9 +1796,9 @@ def test_column_format(self):
16761796
cell = read_worksheet.cell('B2')
16771797

16781798
try:
1679-
read_num_format = cell.style.number_format._format_code
1799+
read_num_format = cell.number_format
16801800
except:
1681-
read_num_format = cell.style.number_format
1801+
read_num_format = cell.style.number_format._format_code
16821802

16831803
self.assertEqual(read_num_format, num_format)
16841804

0 commit comments

Comments
 (0)