Repository: chainer/chainer

Issue:
Make h5py optional
h5py is a little difficult to install as it depends on Cython and llibhdf5. We decided to make serialization to hdf file optional. Only when users have h5py library it works. And, show installation method when users who don't have h5py try to use it.



Discussion:

Resolving patch:
diff --git a/chainer/serializers/hdf5.py b/chainer/serializers/hdf5.py
--- a/chainer/serializers/hdf5.py
+++ b/chainer/serializers/hdf5.py
@@ -1,5 +1,3 @@
-import os
-
 import numpy
 
 from chainer import cuda
@@ -8,9 +6,18 @@
 
 try:
     import h5py
+    _available = True
 except ImportError:
-    if not os.environ.get('READTHEDOCS', None) == 'True':
-        raise
+    _available = False
+
+
+def _check_available():
+    if not _available:
+        msg = '''h5py is not installed on your environment.
+Please install h5py to activate hdf5 serializers.
+
+  $ pip install h5py'''
+        raise RuntimeError(msg)
 
 
 class HDF5Serializer(serializer.Serializer):
@@ -26,6 +33,8 @@ class HDF5Serializer(serializer.Serializer):
 
     """
     def __init__(self, group, compression=4):
+        _check_available()
+
         self.group = group
         self.compression = compression
 
@@ -57,6 +66,7 @@ def save_hdf5(filename, obj, compression=4):
         compression (int): Gzip compression level.
 
     """
+    _check_available()
     with h5py.File(filename, 'w') as f:
         s = HDF5Serializer(f, compression=compression)
         s.save(obj)
@@ -74,6 +84,7 @@ class HDF5Deserializer(serializer.Deserializer):
 
     """
     def __init__(self, group):
+        _check_available()
         self.group = group
 
     def __getitem__(self, key):
@@ -103,6 +114,7 @@ def load_hdf5(filename, obj):
         obj: Object to be deserialized. It must support serialization protocol.
 
     """
+    _check_available()
     with h5py.File(filename, 'r') as f:
         d = HDF5Deserializer(f)
         d.load(obj)
diff --git a/chainer_setup_build.py b/chainer_setup_build.py
--- a/chainer_setup_build.py
+++ b/chainer_setup_build.py
@@ -352,7 +352,10 @@ def parse_args():
 
 
 def get_cython_pkg():
-    return pkg_resources.get_distribution('cython')
+    try:
+        return pkg_resources.get_distribution('cython')
+    except pkg_resources.DistributionNotFound:
+        return None
 
 
 def run_command(cmd):
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -7,6 +7,9 @@
 import chainer_setup_build
 
 
+setup_requires = [
+    'numpy>=1.9.0',
+]
 install_requires = [
     'filelock',
     'nose',
@@ -17,12 +20,10 @@
 
 # Hack for Read the Docs
 on_rtd = chainer_setup_build.check_readthedocs_environment()
-if not on_rtd:
-    install_requires.append('h5py>=2.5.0')
-
 if on_rtd:
     print('Add develop command for Read the Docs')
     sys.argv.insert(1, 'develop')
+    setup_requires = ['Cython>=0.23'] + setup_requires
 
 chainer_setup_build.parse_args()
 
@@ -77,8 +78,7 @@
     },
     zip_safe=False,
     install_requires=install_requires,
-    setup_requires=['Cython>=0.19',  # for h5py
-                    'numpy>=1.9.0'],
+    setup_requires=setup_requires,
     tests_require=['mock',
                    'nose'],
     # To trick build into running build_ext


Test patch:
diff --git a/tests/chainer_tests/serializers_tests/test_hdf5.py b/tests/chainer_tests/serializers_tests/test_hdf5.py
--- a/tests/chainer_tests/serializers_tests/test_hdf5.py
+++ b/tests/chainer_tests/serializers_tests/test_hdf5.py
@@ -1,4 +1,5 @@
 import os
+import sys
 import tempfile
 import unittest
 
@@ -223,4 +224,37 @@ def test_save_optimizer2(self):
             self._check_group(h5, ('Wp', 'epoch', 't'))
 
 
+original_import = __import__
+
+
+def no_h5py(name, _globals=None, _locals=None, fromlist=(), level=0):
+    if name == 'h5py':
+        raise ImportError()
+    else:
+        return original_import(name, _globals, _locals, fromlist, level)
+
+
+class TestNoH5py(unittest.TestCase):
+
+    def setUp(self):
+        __builtins__['__import__'] = no_h5py
+
+    def tearDown(self):
+        __builtins__['__import__'] = original_import
+
+    def test_raise(self):
+        del sys.modules['chainer.serializers.hdf5']
+        del sys.modules['chainer.serializers']
+
+        import chainer.serializers
+        with self.assertRaises(RuntimeError):
+            chainer.serializers.save_hdf5(None, None, None)
+        with self.assertRaises(RuntimeError):
+            chainer.serializers.load_hdf5(None, None)
+        with self.assertRaises(RuntimeError):
+            chainer.serializers.HDF5Serializer(None)
+        with self.assertRaises(RuntimeError):
+            chainer.serializers.HDF5Deserializer(None)
+
+
 testing.run_module(__name__, __file__)