Repository: pandas-dev/pandas

Issue:
COMPAT: Break in openpyxl 
- [ ] 3.4 build is now using current version of `openpyxl` (2.0.2), fix for this, see here: https://travis-ci.org/pydata/pandas/jobs/25510797
- [ ] change ci/requirements-2.7.txt to use last version (1.8.6); will need same import fixes

When I try to use     `DataFrame` `to_excel`, I was given the following error. I am using `Pandas 0.13.1`, `openpyxl 2.0.2`

```
from openpyxl.style import Style
ImportError: No module named style
```



Discussion:
I think that `openpyxl` broke back-compat AFAICT, here's my message to them: https://groups.google.com/forum/#!msg/openpyxl-users/oSNDZe6_YLg/gOXBHifN-C8J

I would suggest for now downgrade to the 1.x series (1.8.6)

it didn't seem to be trvial to fix pandas, though a PR would be welcom

you can also install `xlsxwriter` which is a _faster_ writer for excel

cc @jmcnamara @jtratner @cpcloud @TomAugspurger ?

I'm [starting to dig into this now](https://github.com/neirbowj/pandas/tree/issue7169_openpyxl). First stumbling block: I want to get Travis-CI to start testing against both versions of OpenPyXL, but I just learned about how `install.sh` grabs a bunch of things from http://pandas.pydata.org/pandas-build/dev/wheels/. Any suggestions how to handle this?

don't worry about the wheels
simply change ci/requirements-2.7.txt and change the version (the 3.4 build doesn't have a version because it will always pull the latest), maybe change 2.7 and 3.3 if u want (leave some of the old ones too)

u can do several versions of u want (in different builds)

I build the wheels - but since these r not compiled modules it doesn't make much difference (if it can't pull the wheel it will just get from pypi)

@jreback: Thank you for the fast response.

Sorry if I'm being dense. Are you suggesting I test against both versions of OpenPyXL by changing some reqt's but not all, and thereby having something like:
-   python-2.7 + openpyxl-2.0.2
-   python-3.3 + openpyxl-1.6.2

...but not...
-   python-2.7 + openpyxl-1.6.2
-   python-3.3 + openpyxl-2.0.2

?

This was my initial attempt: neirbowj/pandas@3b36527c

no don't add any builds (eg don't modify Travis.yml)
just edit ci/requirements-2.7.txt (and 3.3) (one line each - just change the version)
for say 2.02 and 1.86 (u can pick relevant ones)

pandas takes so long to build/run (even with a lot of caching) that we limit to 5 builds
plus a couple of more that run after (but don't affect the fail/ success)

Got it. Onward I merrily stumble.

haha np

that said if I think we ought to be testing different versions of other libs

pls say so 

On a different tack I've started to look at putting in a `openpyxl` backward compatible fix for this. It isn't trivial but it is doable.

The main issue is that the style objects used by `openpyxl` are now immutable after creation. However, that just means that we will have to implement a solutions similar to the ones already used by the `xlwt` and `xlsxwriter` engines.

I'll post a separate PR when I have something working.

@neirbowj any luck with this?

@jreback A little. I didn't have any time to work on it yesterday, and I won't today either. Tomorrow is looking promising.

I've gotten as far as teaching the unit test to be version aware, and teaching `pandas.io.excel._OpenpyxlWriter` to choose the best implementation of `_convert_to_style()` at class definition time. However, I haven't actually implemented the v2 flavor of that method, and am getting a little wrapped around the axle trying to figure out the best way to do it, and how to handle the style merging behavior inside the `write_excel` caller.

ok great

at least for 0.14.0 i would like it to not break
would be ok with raising a helpful error message as well
(in case the fix is going to take too long)

lmk on progress

I'll keep that in mind. Thank you.

@neirbowj not harassing, but .... :)

Today is still looking promising. When I start on it after $WORK, the first thing I'll do is make a simple patch that detects OpenPyXL 2, fails the unit test, and informatively disables `_OpenpyxlWriter`.

that sounds good thanks


Resolving patch:
diff --git a/pandas/compat/openpyxl_compat.py b/pandas/compat/openpyxl_compat.py
new file mode 100644
--- /dev/null
+++ b/pandas/compat/openpyxl_compat.py
@@ -0,0 +1,26 @@
+"""
+Detect incompatible version of OpenPyXL
+
+GH7169
+"""
+
+from distutils.version import LooseVersion
+
+start_ver = '1.6.1'
+stop_ver = '2.0.0'
+
+def is_compat():
+    """
+    Detect whether the installed version of openpyxl is supported
+    Returns True/False if openpyxl is installed, None otherwise
+    """
+    try:
+        import openpyxl
+    except ImportError:
+        return None
+
+    ver = LooseVersion(openpyxl.__version__)
+    if ver < LooseVersion(start_ver) or LooseVersion(stop_ver) <= ver:
+        return False
+
+    return True
diff --git a/pandas/io/excel.py b/pandas/io/excel.py
--- a/pandas/io/excel.py
+++ b/pandas/io/excel.py
@@ -16,6 +16,7 @@
 from pandas.core import config
 from pandas.core.common import pprint_thing
 import pandas.compat as compat
+import pandas.compat.openpyxl_compat as openpyxl_compat
 import pandas.core.common as com
 from warnings import warn
 from distutils.version import LooseVersion
@@ -617,7 +618,12 @@ def _convert_to_style(cls, style_dict):
 
         return xls_style
 
-register_writer(_OpenpyxlWriter)
+
+if openpyxl_compat.is_compat():
+    register_writer(_OpenpyxlWriter)
+else:
+    warn('Installed openpyxl is not supported at this time. Use >={} and <{}.'
+            .format(openpyxl_compat.start_ver, openpyxl_compat.stop_ver))
 
 
 class _XlwtWriter(ExcelWriter):


Test patch:
diff --git a/pandas/io/tests/test_excel.py b/pandas/io/tests/test_excel.py
--- a/pandas/io/tests/test_excel.py
+++ b/pandas/io/tests/test_excel.py
@@ -1,6 +1,6 @@
 # pylint: disable=E1101
 
-from pandas.compat import u, range, map
+from pandas.compat import u, range, map, openpyxl_compat
 from datetime import datetime, date, time
 import os
 from distutils.version import LooseVersion
@@ -45,6 +45,10 @@ def _skip_if_no_openpyxl():
     except ImportError:
         raise nose.SkipTest('openpyxl not installed, skipping')
 
+    if not openpyxl_compat.is_compat():
+        raise nose.SkipTest('need %s <= openpyxl < %s, skipping' %
+                (openpyxl_compat.start_ver, openpyxl_compat.stop_ver))
+
 
 def _skip_if_no_xlsxwriter():
     try: