diff --git a/ci/requirements-2.7.run b/ci/requirements-2.7.run
index 3a1e91e4246b5d..aa87ce0749f08f 100644
--- a/ci/requirements-2.7.run
+++ b/ci/requirements-2.7.run
@@ -5,7 +5,7 @@ xlwt=0.7.5
numexpr
pytables
matplotlib
-openpyxl=1.6.2
+openpyxl=2.3.2
xlrd=0.9.2
sqlalchemy=0.9.6
lxml
diff --git a/ci/requirements-2.7_LOCALE.run b/ci/requirements-2.7_LOCALE.run
index 978bbf6a051c51..0eed62843d7445 100644
--- a/ci/requirements-2.7_LOCALE.run
+++ b/ci/requirements-2.7_LOCALE.run
@@ -2,7 +2,7 @@ python-dateutil
pytz=2013b
numpy=1.9.2
xlwt=0.7.5
-openpyxl=1.6.2
+openpyxl=2.3.2
xlsxwriter=0.5.2
xlrd=0.9.2
bottleneck=1.0.0
diff --git a/doc/source/install.rst b/doc/source/install.rst
index b8cc40f9616b3f..9b491af4480315 100644
--- a/doc/source/install.rst
+++ b/doc/source/install.rst
@@ -244,8 +244,8 @@ Optional Dependencies
* For Excel I/O:
* `xlrd/xlwt `__: Excel reading (xlrd) and writing (xlwt)
- * `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)
+ * `openpyxl `__: openpyxl version 2.3.0
+ for writing .xlsx files (xlrd >= 0.9.0)
* `XlsxWriter `__: Alternative Excel writer
* `Jinja2 `__: Template engine for conditional HTML formatting.
diff --git a/doc/source/whatsnew/v0.22.0.txt b/doc/source/whatsnew/v0.22.0.txt
index fde1e9e0bcb567..e03b89102fa2d1 100644
--- a/doc/source/whatsnew/v0.22.0.txt
+++ b/doc/source/whatsnew/v0.22.0.txt
@@ -34,7 +34,7 @@ Backwards incompatible API changes
Dependencies have increased minimum versions
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-We have updated our minimum supported versions of dependencies ().
+We have updated our minimum supported versions of dependencies (:issue:`15184`).
If installed, we now require:
+-----------------+-----------------+----------+
@@ -42,6 +42,8 @@ If installed, we now require:
+=================+=================+==========+
| python-dateutil | 2.5.0 | X |
+-----------------+-----------------+----------+
+ | openpyxl | 2.3.2 | |
+ +-----------------+-----------------+----------+
-
-
diff --git a/pandas/compat/openpyxl_compat.py b/pandas/compat/openpyxl_compat.py
deleted file mode 100644
index 87cf52cf00fef0..00000000000000
--- a/pandas/compat/openpyxl_compat.py
+++ /dev/null
@@ -1,35 +0,0 @@
-"""
-Detect incompatible version of OpenPyXL
-
-GH7169
-"""
-
-from distutils.version import LooseVersion
-
-start_ver = '1.6.1'
-stop_ver = '2.0.0'
-
-
-def is_compat(major_ver=1):
- """Detect whether the installed version of openpyxl is supported
-
- Parameters
- ----------
- ver : int
- 1 requests compatibility status among the 1.x.y series
- 2 requests compatibility status of 2.0.0 and later
- Returns
- -------
- compat : bool
- ``True`` if openpyxl is installed and is a compatible version.
- ``False`` otherwise.
- """
- import openpyxl
- ver = LooseVersion(openpyxl.__version__)
- if major_ver == 1:
- return LooseVersion(start_ver) <= ver < LooseVersion(stop_ver)
- elif major_ver == 2:
- return LooseVersion(stop_ver) <= ver
- else:
- raise ValueError('cannot test for openpyxl compatibility with ver {0}'
- .format(major_ver))
diff --git a/pandas/io/excel.py b/pandas/io/excel.py
index fec916dc52d20c..882130bedcbf00 100644
--- a/pandas/io/excel.py
+++ b/pandas/io/excel.py
@@ -28,7 +28,6 @@
from pandas.core import config
from pandas.io.formats.printing import pprint_thing
import pandas.compat as compat
-import pandas.compat.openpyxl_compat as openpyxl_compat
from warnings import warn
from distutils.version import LooseVersion
from pandas.util._decorators import Appender, deprecate_kwarg
@@ -185,22 +184,6 @@ def _get_default_writer(ext):
def get_writer(engine_name):
- if engine_name == 'openpyxl':
- try:
- import openpyxl
-
- # with version-less openpyxl engine
- # make sure we make the intelligent choice for the user
- if LooseVersion(openpyxl.__version__) < '2.0.0':
- return _writers['openpyxl1']
- elif LooseVersion(openpyxl.__version__) < '2.2.0':
- return _writers['openpyxl20']
- else:
- return _writers['openpyxl22']
- except ImportError:
- # fall through to normal exception handling below
- pass
-
try:
return _writers[engine_name]
except KeyError:
@@ -828,20 +811,15 @@ def close(self):
return self.save()
-class _Openpyxl1Writer(ExcelWriter):
- engine = 'openpyxl1'
+class _OpenpyxlWriter(ExcelWriter):
+ engine = 'openpyxl'
supported_extensions = ('.xlsx', '.xlsm')
- openpyxl_majorver = 1
def __init__(self, path, engine=None, **engine_kwargs):
- if not openpyxl_compat.is_compat(major_ver=self.openpyxl_majorver):
- raise ValueError('Installed openpyxl is not supported at this '
- 'time. Use {majorver}.x.y.'
- .format(majorver=self.openpyxl_majorver))
# Use the openpyxl module as the Excel writer.
from openpyxl.workbook import Workbook
- super(_Openpyxl1Writer, self).__init__(path, **engine_kwargs)
+ super(_OpenpyxlWriter, self).__init__(path, **engine_kwargs)
# Create workbook object with default optimized_write=True.
self.book = Workbook()
@@ -861,72 +839,6 @@ def save(self):
"""
return self.book.save(self.path)
- def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0,
- freeze_panes=None):
- # Write the frame cells using openpyxl.
- from openpyxl.cell import get_column_letter
-
- sheet_name = self._get_sheet_name(sheet_name)
-
- if sheet_name in self.sheets:
- wks = self.sheets[sheet_name]
- else:
- wks = self.book.create_sheet()
- wks.title = sheet_name
- self.sheets[sheet_name] = wks
-
- for cell in cells:
- colletter = get_column_letter(startcol + cell.col + 1)
- xcell = wks.cell("{col}{row}".format(col=colletter,
- row=startrow + cell.row + 1))
- if (isinstance(cell.val, compat.string_types) and
- xcell.data_type_for_value(cell.val) != xcell.TYPE_STRING):
- xcell.set_value_explicit(cell.val)
- else:
- xcell.value = _conv_value(cell.val)
- style = None
- if cell.style:
- style = self._convert_to_style(cell.style)
- for field in style.__fields__:
- xcell.style.__setattr__(field,
- style.__getattribute__(field))
-
- if isinstance(cell.val, datetime):
- xcell.style.number_format.format_code = self.datetime_format
- elif isinstance(cell.val, date):
- xcell.style.number_format.format_code = self.date_format
-
- if cell.mergestart is not None and cell.mergeend is not None:
- cletterstart = get_column_letter(startcol + cell.col + 1)
- cletterend = get_column_letter(startcol + cell.mergeend + 1)
-
- wks.merge_cells('{start}{row}:{end}{mergestart}'
- .format(start=cletterstart,
- row=startrow + cell.row + 1,
- end=cletterend,
- mergestart=startrow +
- cell.mergestart + 1))
-
- # Excel requires that the format of the first cell in a merged
- # range is repeated in the rest of the merged range.
- if style:
- first_row = startrow + cell.row + 1
- last_row = startrow + cell.mergestart + 1
- first_col = startcol + cell.col + 1
- last_col = startcol + cell.mergeend + 1
-
- for row in range(first_row, last_row + 1):
- for col in range(first_col, last_col + 1):
- if row == first_row and col == first_col:
- # Ignore first cell. It is already handled.
- continue
- colletter = get_column_letter(col)
- xcell = wks.cell("{col}{row}"
- .format(col=colletter, row=row))
- for field in style.__fields__:
- xcell.style.__setattr__(
- field, style.__getattribute__(field))
-
@classmethod
def _convert_to_style(cls, style_dict):
"""
@@ -948,88 +860,6 @@ def _convert_to_style(cls, style_dict):
return xls_style
-
-register_writer(_Openpyxl1Writer)
-
-
-class _OpenpyxlWriter(_Openpyxl1Writer):
- engine = 'openpyxl'
-
-
-register_writer(_OpenpyxlWriter)
-
-
-class _Openpyxl20Writer(_Openpyxl1Writer):
- """
- Note: Support for OpenPyxl v2 is currently EXPERIMENTAL (GH7565).
- """
- engine = 'openpyxl20'
- openpyxl_majorver = 2
-
- def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0,
- freeze_panes=None):
- # Write the frame cells using openpyxl.
- from openpyxl.cell import get_column_letter
-
- sheet_name = self._get_sheet_name(sheet_name)
-
- if sheet_name in self.sheets:
- wks = self.sheets[sheet_name]
- else:
- wks = self.book.create_sheet()
- wks.title = sheet_name
- self.sheets[sheet_name] = wks
-
- for cell in cells:
- colletter = get_column_letter(startcol + cell.col + 1)
- xcell = wks["{col}{row}"
- .format(col=colletter, row=startrow + cell.row + 1)]
- xcell.value = _conv_value(cell.val)
- style_kwargs = {}
-
- # Apply format codes before cell.style to allow override
- if isinstance(cell.val, datetime):
- style_kwargs.update(self._convert_to_style_kwargs({
- 'number_format': {'format_code': self.datetime_format}}))
- elif isinstance(cell.val, date):
- style_kwargs.update(self._convert_to_style_kwargs({
- 'number_format': {'format_code': self.date_format}}))
-
- if cell.style:
- style_kwargs.update(self._convert_to_style_kwargs(cell.style))
-
- if style_kwargs:
- xcell.style = xcell.style.copy(**style_kwargs)
-
- if cell.mergestart is not None and cell.mergeend is not None:
- cletterstart = get_column_letter(startcol + cell.col + 1)
- cletterend = get_column_letter(startcol + cell.mergeend + 1)
-
- wks.merge_cells('{start}{row}:{end}{mergestart}'
- .format(start=cletterstart,
- row=startrow + cell.row + 1,
- end=cletterend,
- mergestart=startrow +
- cell.mergestart + 1))
-
- # Excel requires that the format of the first cell in a merged
- # range is repeated in the rest of the merged range.
- if style_kwargs:
- first_row = startrow + cell.row + 1
- last_row = startrow + cell.mergestart + 1
- first_col = startcol + cell.col + 1
- last_col = startcol + cell.mergeend + 1
-
- for row in range(first_row, last_row + 1):
- for col in range(first_col, last_col + 1):
- if row == first_row and col == first_col:
- # Ignore first cell. It is already handled.
- continue
- colletter = get_column_letter(col)
- xcell = wks["{col}{row}"
- .format(col=colletter, row=row)]
- xcell.style = xcell.style.copy(**style_kwargs)
-
@classmethod
def _convert_to_style_kwargs(cls, style_dict):
"""
@@ -1341,13 +1171,7 @@ def _convert_to_number_format(cls, number_format_dict):
-------
number_format : str
"""
- try:
- # >= 2.0.0 < 2.1.0
- from openpyxl.styles import NumberFormat
- return NumberFormat(**number_format_dict)
- except:
- # >= 2.1.0
- return number_format_dict['format_code']
+ return number_format_dict['format_code']
@classmethod
def _convert_to_protection(cls, protection_dict):
@@ -1367,17 +1191,6 @@ def _convert_to_protection(cls, protection_dict):
return Protection(**protection_dict)
-
-register_writer(_Openpyxl20Writer)
-
-
-class _Openpyxl22Writer(_Openpyxl20Writer):
- """
- Note: Support for OpenPyxl v2.2 is currently EXPERIMENTAL (GH7565).
- """
- engine = 'openpyxl22'
- openpyxl_majorver = 2
-
def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0,
freeze_panes=None):
# Write the frame cells using openpyxl.
@@ -1443,7 +1256,7 @@ def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0,
setattr(xcell, k, v)
-register_writer(_Openpyxl22Writer)
+register_writer(_OpenpyxlWriter)
class _XlwtWriter(ExcelWriter):
diff --git a/pandas/tests/io/test_excel.py b/pandas/tests/io/test_excel.py
index d33136a86faadc..96117b3c21a9b3 100644
--- a/pandas/tests/io/test_excel.py
+++ b/pandas/tests/io/test_excel.py
@@ -1,6 +1,4 @@
# pylint: disable=E1101
-import functools
-import operator
import os
import sys
import warnings
@@ -17,12 +15,12 @@
import pandas as pd
import pandas.util.testing as tm
from pandas import DataFrame, Index, MultiIndex
-from pandas.compat import u, range, map, openpyxl_compat, BytesIO, iteritems
+from pandas.compat import u, range, map, BytesIO, iteritems
from pandas.core.config import set_option, get_option
from pandas.io.common import URLError
from pandas.io.excel import (
- ExcelFile, ExcelWriter, read_excel, _XlwtWriter, _Openpyxl1Writer,
- _Openpyxl20Writer, _Openpyxl22Writer, register_writer, _XlsxWriter
+ ExcelFile, ExcelWriter, read_excel, _XlwtWriter, _OpenpyxlWriter,
+ register_writer, _XlsxWriter
)
from pandas.io.formats.excel import ExcelFormatter
from pandas.io.parsers import read_csv
@@ -1926,207 +1924,10 @@ def test_path_localpath(self):
tm.assert_frame_equal(df, result)
-def raise_wrapper(major_ver):
- def versioned_raise_wrapper(orig_method):
- @functools.wraps(orig_method)
- def wrapped(self, *args, **kwargs):
- _skip_if_no_openpyxl()
- if openpyxl_compat.is_compat(major_ver=major_ver):
- orig_method(self, *args, **kwargs)
- else:
- msg = (r'Installed openpyxl is not supported at this '
- r'time\. Use.+')
- with tm.assert_raises_regex(ValueError, msg):
- orig_method(self, *args, **kwargs)
- return wrapped
- return versioned_raise_wrapper
-
-
-def raise_on_incompat_version(major_ver):
- def versioned_raise_on_incompat_version(cls):
- methods = filter(operator.methodcaller(
- 'startswith', 'test_'), dir(cls))
- for method in methods:
- setattr(cls, method, raise_wrapper(
- major_ver)(getattr(cls, method)))
- return cls
- return versioned_raise_on_incompat_version
-
-
-@raise_on_incompat_version(1)
class TestOpenpyxlTests(ExcelWriterBase):
+ engine_name = 'openpyxl'
ext = '.xlsx'
- engine_name = 'openpyxl1'
- check_skip = staticmethod(lambda *args, **kwargs: None)
-
- def test_to_excel_styleconverter(self):
- _skip_if_no_openpyxl()
- if not openpyxl_compat.is_compat(major_ver=1):
- pytest.skip('incompatible openpyxl version')
-
- import openpyxl
-
- hstyle = {"font": {"bold": True},
- "borders": {"top": "thin",
- "right": "thin",
- "bottom": "thin",
- "left": "thin"},
- "alignment": {"horizontal": "center", "vertical": "top"}}
-
- xlsx_style = _Openpyxl1Writer._convert_to_style(hstyle)
- assert xlsx_style.font.bold
- assert (openpyxl.style.Border.BORDER_THIN ==
- xlsx_style.borders.top.border_style)
- assert (openpyxl.style.Border.BORDER_THIN ==
- xlsx_style.borders.right.border_style)
- assert (openpyxl.style.Border.BORDER_THIN ==
- xlsx_style.borders.bottom.border_style)
- assert (openpyxl.style.Border.BORDER_THIN ==
- xlsx_style.borders.left.border_style)
- assert (openpyxl.style.Alignment.HORIZONTAL_CENTER ==
- xlsx_style.alignment.horizontal)
- assert (openpyxl.style.Alignment.VERTICAL_TOP ==
- xlsx_style.alignment.vertical)
-
-
-def skip_openpyxl_gt21(cls):
- """Skip test case if openpyxl >= 2.2"""
-
- @classmethod
- def setup_class(cls):
- _skip_if_no_openpyxl()
- import openpyxl
- ver = openpyxl.__version__
- if (not (LooseVersion(ver) >= LooseVersion('2.0.0') and
- LooseVersion(ver) < LooseVersion('2.2.0'))):
- pytest.skip("openpyxl %s >= 2.2" % str(ver))
-
- cls.setup_class = setup_class
- return cls
-
-
-@raise_on_incompat_version(2)
-@skip_openpyxl_gt21
-class TestOpenpyxl20Tests(ExcelWriterBase):
- ext = '.xlsx'
- engine_name = 'openpyxl20'
- check_skip = staticmethod(lambda *args, **kwargs: None)
-
- def test_to_excel_styleconverter(self):
- import openpyxl
- from openpyxl import styles
-
- hstyle = {
- "font": {
- "color": '00FF0000',
- "bold": True,
- },
- "borders": {
- "top": "thin",
- "right": "thin",
- "bottom": "thin",
- "left": "thin",
- },
- "alignment": {
- "horizontal": "center",
- "vertical": "top",
- },
- "fill": {
- "patternType": 'solid',
- 'fgColor': {
- 'rgb': '006666FF',
- 'tint': 0.3,
- },
- },
- "number_format": {
- "format_code": "0.00"
- },
- "protection": {
- "locked": True,
- "hidden": False,
- },
- }
-
- font_color = styles.Color('00FF0000')
- font = styles.Font(bold=True, color=font_color)
- side = styles.Side(style=styles.borders.BORDER_THIN)
- border = styles.Border(top=side, right=side, bottom=side, left=side)
- alignment = styles.Alignment(horizontal='center', vertical='top')
- fill_color = styles.Color(rgb='006666FF', tint=0.3)
- fill = styles.PatternFill(patternType='solid', fgColor=fill_color)
-
- # ahh openpyxl API changes
- ver = openpyxl.__version__
- if ver >= LooseVersion('2.0.0') and ver < LooseVersion('2.1.0'):
- number_format = styles.NumberFormat(format_code='0.00')
- else:
- number_format = '0.00' # XXX: Only works with openpyxl-2.1.0
-
- protection = styles.Protection(locked=True, hidden=False)
-
- kw = _Openpyxl20Writer._convert_to_style_kwargs(hstyle)
- assert kw['font'] == font
- assert kw['border'] == border
- assert kw['alignment'] == alignment
- assert kw['fill'] == fill
- assert kw['number_format'] == number_format
- assert kw['protection'] == protection
-
- def test_write_cells_merge_styled(self):
- from pandas.io.formats.excel import ExcelCell
- from openpyxl import styles
-
- sheet_name = 'merge_styled'
-
- sty_b1 = {'font': {'color': '00FF0000'}}
- sty_a2 = {'font': {'color': '0000FF00'}}
-
- initial_cells = [
- ExcelCell(col=1, row=0, val=42, style=sty_b1),
- ExcelCell(col=0, row=1, val=99, style=sty_a2),
- ]
-
- sty_merged = {'font': {'color': '000000FF', 'bold': True}}
- sty_kwargs = _Openpyxl20Writer._convert_to_style_kwargs(sty_merged)
- openpyxl_sty_merged = styles.Style(**sty_kwargs)
- merge_cells = [
- ExcelCell(col=0, row=0, val='pandas',
- mergestart=1, mergeend=1, style=sty_merged),
- ]
-
- with ensure_clean('.xlsx') as path:
- writer = _Openpyxl20Writer(path)
- writer.write_cells(initial_cells, sheet_name=sheet_name)
- writer.write_cells(merge_cells, sheet_name=sheet_name)
-
- wks = writer.sheets[sheet_name]
- xcell_b1 = wks['B1']
- xcell_a2 = wks['A2']
- assert xcell_b1.style == openpyxl_sty_merged
- assert xcell_a2.style == openpyxl_sty_merged
-
-
-def skip_openpyxl_lt22(cls):
- """Skip test case if openpyxl < 2.2"""
-
- @classmethod
- def setup_class(cls):
- _skip_if_no_openpyxl()
- import openpyxl
- ver = openpyxl.__version__
- if LooseVersion(ver) < LooseVersion('2.2.0'):
- pytest.skip("openpyxl %s < 2.2" % str(ver))
-
- cls.setup_class = setup_class
- return cls
-
-
-@raise_on_incompat_version(2)
-@skip_openpyxl_lt22
-class TestOpenpyxl22Tests(ExcelWriterBase):
- ext = '.xlsx'
- engine_name = 'openpyxl22'
- check_skip = staticmethod(lambda *args, **kwargs: None)
+ check_skip = staticmethod(_skip_if_no_openpyxl)
def test_to_excel_styleconverter(self):
from openpyxl import styles
@@ -2174,7 +1975,7 @@ def test_to_excel_styleconverter(self):
protection = styles.Protection(locked=True, hidden=False)
- kw = _Openpyxl22Writer._convert_to_style_kwargs(hstyle)
+ kw = _OpenpyxlWriter._convert_to_style_kwargs(hstyle)
assert kw['font'] == font
assert kw['border'] == border
assert kw['alignment'] == alignment
@@ -2183,9 +1984,6 @@ def test_to_excel_styleconverter(self):
assert kw['protection'] == protection
def test_write_cells_merge_styled(self):
- if not openpyxl_compat.is_compat(major_ver=2):
- pytest.skip('incompatible openpyxl version')
-
from pandas.io.formats.excel import ExcelCell
sheet_name = 'merge_styled'
@@ -2199,7 +1997,7 @@ def test_write_cells_merge_styled(self):
]
sty_merged = {'font': {'color': '000000FF', 'bold': True}}
- sty_kwargs = _Openpyxl22Writer._convert_to_style_kwargs(sty_merged)
+ sty_kwargs = _OpenpyxlWriter._convert_to_style_kwargs(sty_merged)
openpyxl_sty_merged = sty_kwargs['font']
merge_cells = [
ExcelCell(col=0, row=0, val='pandas',
@@ -2207,7 +2005,7 @@ def test_write_cells_merge_styled(self):
]
with ensure_clean('.xlsx') as path:
- writer = _Openpyxl22Writer(path)
+ writer = _OpenpyxlWriter(path)
writer.write_cells(initial_cells, sheet_name=sheet_name)
writer.write_cells(merge_cells, sheet_name=sheet_name)
@@ -2322,7 +2120,7 @@ def test_column_format(self):
try:
read_num_format = cell.number_format
- except:
+ except Exception:
read_num_format = cell.style.number_format._format_code
assert read_num_format == num_format
@@ -2366,9 +2164,7 @@ def test_ExcelWriter_dispatch(self):
writer_klass = _XlsxWriter
except ImportError:
_skip_if_no_openpyxl()
- if not openpyxl_compat.is_compat(major_ver=1):
- pytest.skip('incompatible openpyxl version')
- writer_klass = _Openpyxl1Writer
+ writer_klass = _OpenpyxlWriter
with ensure_clean('.xlsx') as path:
writer = ExcelWriter(path)
@@ -2461,10 +2257,6 @@ def custom_converter(css):
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))
@@ -2482,9 +2274,6 @@ def custom_converter(css):
# For other engines, we only smoke test
return
openpyxl = pytest.importorskip('openpyxl')
- if not openpyxl_compat.is_compat(major_ver=2):
- pytest.skip('incompatible openpyxl version')
-
wb = openpyxl.load_workbook(path)
# (1) compare DataFrame.to_excel and Styler.to_excel when unstyled