Repository: nilearn/nilearn

Issue:
Add package dependencies to a requirements.txt file
Great package here!

Not sure if I've done something improperly, but during both `pip install` and the development cloning process, I had dependency errors.  I came back and saw the dependencies and resolved them manually.

Rather than declaring dependencies in the `README.md` file, what about declaring in a `requirements.txt` file? ([documentation](https://pip.pypa.io/en/latest/user_guide.html#requirements-files)).

This should allow `pip` to resolve dependencies when installing the package, and should make getting the development version after cloning `nilearn` up and running more easily.

I'd be happy to do this, if it sounds good! 



Discussion:
Thanks for the kind words!

We could also have defined the dependencies in setup.py. The thing is that for example numpy needs to be compiled from source when using pip install. This compilation step is likely to fail (for example because you don't have the package with the required C headers on Linux, or even more likely on Windows where most of our users don't have any C compiler installed) 

Basically we are strongly encouraging users to install numpy, scipy and scikit-learn via their OS package managers on Linux or Anaconda on Windows and OSX.

@GaelVaroquaux @AlexandreAbraham any other opinions?

> @GaelVaroquaux @AlexandreAbraham any other opinions?

Not much to add to what you said. That said, we should note that since
the alpha release, we have added the nibabel dependency in the
requirement file. So things will be a bit more streamlined in the next
release.

Time for an beta?

Ok.  Perhaps having an explicit way to (a) install dependencies that play nice and (b) a way to error out the `pip install` of `nilearn` when they don't have `numpy`, `scipy`, or `scikit-learn` will help more novice  (installation-wise) users understand what's expected of them.

Glad to look into how to accomplish this in a `setup.py` file (sounds like a `requirements.txt` file may not be flexible enough to accomplish the above), if that sounds like a reasonable direction.

> Ok. Perhaps having an explicit way to (a) install dependencies that play nice
> and (b) a way to error out the pip install of nilearn when they don't have
> numpy, scipy, or scikit-learn will help more novice (installation-wise) users
> understand what's expected of them.

Maybe shooting a warning (or an error) in the setup.py would be a good
way to do this.

To do this, we should probably move the '_check_dependencies' out from
nilearn/__init__.py to nilearn.version and 'import' it in the setup.py as
the version number is currently imported.

I think that we would accept a PR (pull request) doing that.
Unfortunately, I'll be offline for 2 weeks starting tomorrow. So I cannot
review it, but maybe other people of the nilearn team will be able to.

> To do this, we should probably move the '_check_dependencies' out from nilearn/__init__.py to nilearn.version and 'import' it in the setup.py as the version number is currently imported. 

I think the general idea has legs but needs to be tweaked a bit. The caveat is that you don't want to throw when nibabel is not installed in setup.py because setup.py is supposed to install it.

Also it'd be good to check what setup.py install error exactly looks like, to make sure we get an error that is actually helpful.

> I think the general idea has legs but needs to be tweaked a bit. The caveat is
> that you don't want to throw when nibabel is not installed in setup.py because
> setup.py is supposed to install it.

Good point. That means that you probably need to have a keyword argument
in the dependency checker.

Also, we should raise an error only if an install is asked, and not other
options.

@bcipolli: did you have a look at how this could be done in the setup.py? Are you planning to send us a pull request?

@GaelVaroquaux I was planning to take a look at this after Jan. 15 (I have two things due then); would that work for you?

We are battling hard to release the 0.1 of nilearn, and I don't want to
let the release date slip, as I will need it for teaching.

OK.  If this is something you'd like in the release, just give me an idea of your timelines and I'll make time to do it!

I will take a look at this now.

> I will take a look at this now.

Thanks!


Resolving patch:
diff --git a/nilearn/__init__.py b/nilearn/__init__.py
--- a/nilearn/__init__.py
+++ b/nilearn/__init__.py
@@ -9,66 +9,18 @@
 See http://nilearn.github.com for complete documentation.
 """
 
-from version import __version__
+from .version import _check_module_dependencies, __version__
 
+_check_module_dependencies()
 
-def _import_module_with_version_check(module_name, minimum_version):
-    """Check that module is installed with a recent enough version
-    """
-    from distutils.version import LooseVersion
 
-    try:
-        module = __import__(module_name)
-    except ImportError as exc:
-        user_friendly_info = ('{} could not be found, '
-                              'please install it properly to use nilearn'
-                              ).format(module_name)
-        exc.args += (user_friendly_info,)
-        raise
+# Monkey-patch gzip to have faster reads on large gzip files
+#   Note: import must occur AFTER dependency checks occur above.
+import gzip
 
-    module_version = module.__version__
+if hasattr(gzip.GzipFile, 'max_read_chunk'):
+    gzip.GzipFile.max_read_chunk = 100 * 1024 * 1024  # 100Mb
 
-    version_too_old = (not LooseVersion(module_version) >=
-                       LooseVersion(minimum_version))
-
-    if version_too_old:
-        message = (
-            'A {module_name} version of at least {minimum_version} '
-            'is required to use nilearn. {module_version} was found. '
-            'Please upgrade {module_name}').format(
-                module_name=module_name,
-                minimum_version=minimum_version,
-                module_version=module_version)
-
-        raise ImportError(message)
-
-    return module
-
-
-def _check_dependencies():
-    _required_module_versions = [('numpy', '1.6.0'),
-                                 ('scipy', '0.9.0'),
-                                 ('sklearn', '0.10'),
-                                 ('nibabel', '1.1.0')]
-
-    for module_name, minimum_version in _required_module_versions:
-        _import_module_with_version_check(module_name, minimum_version)
-
-    try:
-        import gzip
-        if hasattr(gzip.GzipFile, 'max_read_chunk'):
-            # Monkey-patch gzip to have faster reads on large
-            # gzip files
-            gzip.GzipFile.max_read_chunk = 100 * 1024 * 1024  # 100Mb
-    except ImportError as exc:
-        exc.args += ('Python has been compiled without gzip, '
-                     'reading nii.gz files will be impossible.',)
-        raise
-
-_check_dependencies()
-
-del _import_module_with_version_check
-del _check_dependencies
 
 # Boolean controling whether the joblib caches should be
 # flushed if the version of certain modules changes (eg nibabel, as it
diff --git a/nilearn/version.py b/nilearn/version.py
--- a/nilearn/version.py
+++ b/nilearn/version.py
@@ -1 +1,94 @@
+# *- encoding: utf-8 -*-
+"""
+nilearn version, required package versions, and utilities for checking
+"""
+# Author: Loïc Estève, Ben Cipollini
+# License: simplified BSD
+
 __version__ = '0.1b1'
+
+_NILEARN_INSTALL_MSG = 'See %s for installation information.' % (
+    'http://nilearn.github.io/introduction.html#installation')
+
+# This is a tuple to preserve order, so that dependencies are checked
+#   in some meaningful order (more => less 'core').  We avoid using
+#   collections.OrderedDict to preserve Python 2.6 compatibility.
+REQUIRED_MODULE_METADATA = (
+    ('numpy', {
+        'min_version': '1.6.0',
+        'required_at_installation': True,
+        'install_info': _NILEARN_INSTALL_MSG}),
+    ('scipy', {
+        'min_version': '0.9.0',
+        'required_at_installation': True,
+        'install_info': _NILEARN_INSTALL_MSG}),
+    ('sklearn', {
+        'min_version': '0.10',
+        'required_at_installation': True,
+        'install_info': _NILEARN_INSTALL_MSG}),
+    ('nibabel', {
+        'min_version': '1.1.0',
+        'required_at_installation': False}),)
+
+
+def _import_module_with_version_check(
+        module_name,
+        minimum_version,
+        install_info=None):
+    """Check that module is installed with a recent enough version
+    """
+    from distutils.version import LooseVersion
+
+    try:
+        module = __import__(module_name)
+    except ImportError as exc:
+        user_friendly_info = ('Module "{}" could not be found.  {}').format(
+            module_name,
+            install_info or 'Please install it properly to use nilearn.')
+        exc.args += (user_friendly_info,)
+        raise
+
+    # Avoid choking on modules with no __version__ attribute
+    module_version = getattr(module, '__version__', '0.0.0')
+
+    version_too_old = (not LooseVersion(module_version) >=
+                       LooseVersion(minimum_version))
+
+    if version_too_old:
+        message = (
+            'A {module_name} version of at least {minimum_version} '
+            'is required to use nilearn. {module_version} was found. '
+            'Please upgrade {module_name}').format(
+                module_name=module_name,
+                minimum_version=minimum_version,
+                module_version=module_version)
+
+        raise ImportError(message)
+
+    return module
+
+
+def _check_module_dependencies(is_nilearn_installing=False):
+    """Throw an exception if nilearn dependencies are not installed.
+
+    Parameters
+    ----------
+    is_nilearn_installing: boolean
+        if True, only error on missing packages that cannot be auto-installed.
+        if False, error on any missing package.
+
+    Throws
+    -------
+    ImportError
+    """
+
+    for (module_name, module_metadata) in REQUIRED_MODULE_METADATA:
+        if not (is_nilearn_installing and
+                not module_metadata['required_at_installation']):
+            # Skip check only when installing and it's a module that
+            # will be auto-installed.
+            _import_module_with_version_check(
+                module_name=module_name,
+                minimum_version=module_metadata['min_version'],
+                install_info=module_metadata.get('install_info'))
+    
\ No newline at end of file
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -8,20 +8,31 @@
 from setuptools import setup, find_packages
 
 
-def get_version():
-    """Returns the version found in nilearn/version.py
+def load_version():
+    """Executes nilearn/version.py in a globals dictionary and return it.
 
-    Importing nilearn is not an option because there may dependencies
-    like nibabel which are not installed and setup.py is supposed to
-    install them.
+    Note: importing nilearn is not an option because there may be
+    dependencies like nibabel which are not installed and
+    setup.py is supposed to install them.
     """
-    locals_dict = {}
+    # load all vars into globals, otherwise
+    #   the later function call using global vars doesn't work.
     globals_dict = {}
     with open(os.path.join('nilearn', 'version.py')) as fp:
-        exec(fp.read(), globals_dict, locals_dict)
+        exec(fp.read(), globals_dict)
 
-    return locals_dict['__version__']
+    return globals_dict
 
+
+def is_installing():
+    # Allow command-lines such as "python setup.py build install"
+    return 'install' in sys.argv
+
+
+# Make sources available using relative paths from this file's directory.
+os.chdir(os.path.dirname(os.path.abspath(__file__)))
+
+_VERSION_GLOBALS = load_version()
 DISTNAME = 'nilearn'
 DESCRIPTION = 'Statistical learning for neuroimaging in Python'
 LONG_DESCRIPTION = open('README.rst').read()
@@ -30,13 +41,18 @@ def get_version():
 URL = 'http://nilearn.github.com'
 LICENSE = 'new BSD'
 DOWNLOAD_URL = 'http://nilearn.github.com'
-VERSION = get_version()
+VERSION = _VERSION_GLOBALS['__version__']
+
 
 if __name__ == "__main__":
-    old_path = os.getcwd()
-    local_path = os.path.dirname(os.path.abspath(sys.argv[0]))
-    os.chdir(local_path)
-    sys.path.insert(0, local_path)
+    if is_installing():
+        module_check_fn = _VERSION_GLOBALS['_check_module_dependencies']
+        module_check_fn(is_nilearn_installing=True)
+
+    install_requires = \
+        ['%s>=%s' % (mod, meta['min_version'])
+            for mod, meta in _VERSION_GLOBALS['REQUIRED_MODULE_METADATA']
+            if not meta['required_at_installation']]
 
     setup(name=DISTNAME,
           maintainer=MAINTAINER,
@@ -68,5 +84,4 @@ def get_version():
           package_data={'nilearn.data': ['*.nii.gz'],
                         'nilearn.plotting.glass_brain_files': ['*.json'],
                         'nilearn.tests.data': ['*']},
-          install_requires=['nibabel>=1.1.0'],
-    )
+          install_requires=install_requires,)


Test patch: