Skip to content

Commit 125245e

Browse files
committed
Merge pull request #377 from matthew-brett/refactor-setup.py
MRG: refactoring setup.py to fix sdist etc python setup.py sdist was broken, because I wasn't adding some files to the MANIFEST.in, and because I wasn't correctly using the numpy build_py command. In other news, it was annoying to have to install sympy etc to run python setup.py egg_info, which was in turn required when installing via pip - see gh-320. Fix both of these by dropping checks for Mayavi and Cython, dropping nibabel/nisext dependency (by copying code and refactoring), and letting setuptools take care of dependency checking for missing packages.
2 parents 82e2216 + 72df175 commit 125245e

File tree

7 files changed

+182
-51
lines changed

7 files changed

+182
-51
lines changed

.travis.yml

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ cache:
1717
env:
1818
global:
1919
- DEPENDS="numpy scipy sympy matplotlib nibabel"
20+
- INSTALL_TYPE="setup"
2021
python:
2122
- 2.6
2223
- 3.2
@@ -32,11 +33,25 @@ matrix:
3233
# Absolute minimum dependencies
3334
- python: 2.7
3435
env:
36+
# Definitive source for these in nipy/info.py
3537
- DEPENDS="numpy==1.6.0 scipy==0.9.0 sympy==0.7.0 nibabel==1.2.0"
3638
# Test compiling against external lapack
3739
- python: 3.4
3840
env:
3941
- NIPY_EXTERNAL_LAPACK=1
42+
- python: 2.7
43+
env:
44+
- INSTALL_TYPE=sdist
45+
- DEPENDS="numpy==1.6.0"
46+
- python: 2.7
47+
env:
48+
- INSTALL_TYPE=wheel
49+
- DEPENDS="numpy==1.6.0"
50+
- python: 2.7
51+
env:
52+
- INSTALL_TYPE=requirements
53+
- DEPENDS=
54+
4055
before_install:
4156
- source tools/travis_tools.sh
4257
- virtualenv --python=python venv
@@ -52,7 +67,21 @@ before_install:
5267
# command to install dependencies
5368
# e.g. pip install -r requirements.txt # --use-mirrors
5469
install:
55-
- python setup.py install
70+
- |
71+
if [ "$INSTALL_TYPE" == "setup" ]; then
72+
python setup.py install
73+
elif [ "$INSTALL_TYPE" == "sdist" ]; then
74+
python setup_egg.py egg_info # check egg_info while we're here
75+
python setup_egg.py sdist
76+
wheelhouse_pip_install dist/*.tar.gz
77+
elif [ "$INSTALL_TYPE" == "wheel" ]; then
78+
pip install wheel
79+
python setup_egg.py bdist_wheel
80+
wheelhouse_pip_install dist/*.whl
81+
elif [ "$INSTALL_TYPE" == "requirements" ]; then
82+
wheelhouse_pip_install -r requirements.txt
83+
python setup.py install
84+
fi
5685
# command to run tests, e.g. python setup.py test
5786
script:
5887
# Change into an innocuous directory and find tests from installation

MANIFEST.in

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,17 @@ include Changelog TODO
33
include *.py
44
include site.*
55
recursive-include nipy *.c *.h *.pyx *.pxd
6-
recursive-include libcstat *.c *.h *.pyx *.pxd
6+
recursive-include lib *.c *.h *.pyx *.pxd remake
77
recursive-include scripts *
88
recursive-include tools *
99
# put this stuff back into setup.py (package_data) once I'm enlightened
1010
# enough to accomplish this herculean task
1111
recursive-include nipy/algorithms/tests/data *
12+
include nipy/testing/*.nii.gz
13+
include nipy/algorithms/diagnostics/tests/data/*.mat
14+
include nipy/algorithms/statistics/models/tests/*.bin
15+
include nipy/modalities/fmri/tests/*.npz
16+
include nipy/modalities/fmri/tests/*.mat
1217
include nipy/COMMIT_INFO.txt
1318
include LICENSE
1419
graft examples

nipy/info.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@
131131

132132
# minimum versions
133133
# Update in readme text above
134+
# Update in .travis.yml
135+
# Update in requirements.txt
134136
NUMPY_MIN_VERSION='1.6.0'
135137
SCIPY_MIN_VERSION = '0.9.0'
136138
NIBABEL_MIN_VERSION = '1.2'

nipy/setup.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
1-
from __future__ import absolute_import
21
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
32
# vi: set ft=python sts=4 ts=4 sw=4 et:
3+
from __future__ import absolute_import
44

55
import os
6+
import sys
67

7-
from nipy.externals.six import string_types
8-
from nipy.externals.six.moves.configparser import ConfigParser
8+
# Cannot use internal copy of six because can't import from nipy tree
9+
# This is to allow setup.py to run without a full nipy
10+
PY3 = sys.version_info[0] == 3
11+
if PY3:
12+
string_types = str,
13+
from configparser import ConfigParser
14+
else:
15+
string_types = basestring,
16+
from ConfigParser import ConfigParser
917

1018
NIPY_DEFAULTS = dict()
1119

requirements.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# See nipy/info.py for requirement definitions
2+
numpy>=1.6.0
3+
scipy>=0.9.0
4+
sympy>=0.7.0
5+
nibabel>=1.2.0

setup.py

Lines changed: 32 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,17 @@
1111
# update it when the contents of directories change.
1212
if exists('MANIFEST'): os.remove('MANIFEST')
1313

14-
# Import build helpers
15-
try:
16-
from nisext.sexts import package_check, get_comrec_build
17-
except ImportError:
18-
raise RuntimeError('Need nisext package from nibabel installation'
19-
' - please install nibabel first')
20-
21-
from setup_helpers import (generate_a_pyrex_source,
22-
cmdclass, INFO_VARS)
14+
from setup_helpers import (generate_a_pyrex_source, get_comrec_build,
15+
cmdclass, INFO_VARS, get_pkg_version,
16+
version_error_msg)
2317

2418
# monkey-patch numpy distutils to use Cython instead of Pyrex
2519
from numpy.distutils.command.build_src import build_src
2620
build_src.generate_a_pyrex_source = generate_a_pyrex_source
2721

2822
# Add custom commit-recording build command
29-
cmdclass['build_py'] = get_comrec_build('nipy')
23+
from numpy.distutils.command.build_py import build_py as _build_py
24+
cmdclass['build_py'] = get_comrec_build('nipy', _build_py)
3025

3126
def configuration(parent_package='',top_path=None):
3227
from numpy.distutils.misc_util import Configuration
@@ -59,42 +54,28 @@ def configuration(parent_package='',top_path=None):
5954
if not 'extra_setuptools_args' in globals():
6055
extra_setuptools_args = dict()
6156

62-
6357
# Hard and soft dependency checking
64-
package_check('numpy', INFO_VARS['NUMPY_MIN_VERSION'])
65-
package_check('scipy', INFO_VARS['SCIPY_MIN_VERSION'])
66-
package_check('nibabel', INFO_VARS['NIBABEL_MIN_VERSION'])
67-
package_check('sympy', INFO_VARS['SYMPY_MIN_VERSION'])
68-
def _mayavi_version(pkg_name):
69-
"""Mayavi2 pruned enthought. namespace at 4.0.0
70-
"""
71-
v = ''
72-
try:
73-
from mayavi import version
74-
v = version.version
75-
if v == '':
76-
v = '4.0.0' # must be the one in Debian
77-
except ImportError:
78-
from enthought.mayavi import version
79-
v = version.version
80-
return v
81-
package_check('mayavi',
82-
INFO_VARS['MAYAVI_MIN_VERSION'],
83-
optional=True,
84-
version_getter=_mayavi_version)
85-
# Cython can be a build dependency
86-
def _cython_version(pkg_name):
87-
from Cython.Compiler.Version import version
88-
return version
89-
package_check('cython',
90-
INFO_VARS['CYTHON_MIN_VERSION'],
91-
optional=True,
92-
version_getter=_cython_version,
93-
messages={'opt suffix': ' - you will not be able '
94-
'to rebuild Cython source files into C files',
95-
'missing opt': 'Missing optional build-time '
96-
'package "%s"'}
97-
)
58+
DEPS = (
59+
('numpy', INFO_VARS['NUMPY_MIN_VERSION'], 'setup_requires', True),
60+
('scipy', INFO_VARS['SCIPY_MIN_VERSION'], 'install_requires', True),
61+
('nibabel', INFO_VARS['NIBABEL_MIN_VERSION'], 'install_requires', False),
62+
('sympy', INFO_VARS['SYMPY_MIN_VERSION'], 'install_requires', False))
63+
64+
using_setuptools = 'setuptools' in sys.modules
65+
66+
for name, min_ver, req_type, heavy in DEPS:
67+
found_ver = get_pkg_version(name)
68+
ver_err_msg = version_error_msg(name, found_ver, min_ver)
69+
if not using_setuptools:
70+
if ver_err_msg != None:
71+
raise RuntimeError(ver_err_msg)
72+
else: # Using setuptools
73+
# Add packages to given section of setup/install_requires
74+
if ver_err_msg != None or not heavy:
75+
new_req = '{0}>={1}'.format(name, min_ver)
76+
old_reqs = extra_setuptools_args.get(req_type, [])
77+
extra_setuptools_args[req_type] = old_reqs + [new_req]
78+
9879

9980
################################################################################
10081
# commands for installing the data
@@ -103,7 +84,12 @@ def _cython_version(pkg_name):
10384

10485
def data_install_msgs():
10586
# Check whether we have data packages
106-
from nibabel.data import datasource_or_bomber
87+
try: # Allow setup.py to run without nibabel
88+
from nibabel.data import datasource_or_bomber
89+
except ImportError:
90+
log.warn('Cannot check for optional data packages: see: '
91+
'http://nipy.org/nipy/stable/users/install_data.html')
92+
return
10793
DATA_PKGS = INFO_VARS['DATA_PKGS']
10894
templates = datasource_or_bomber(DATA_PKGS['nipy-templates'])
10995
example_data = datasource_or_bomber(DATA_PKGS['nipy-data'])

setup_helpers.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,16 @@
2323
from distutils.cmd import Command
2424
from distutils.command.clean import clean
2525
from distutils.command.install_scripts import install_scripts
26+
from distutils.command.build_py import build_py
2627
from distutils.version import LooseVersion
2728
from distutils.dep_util import newer_group
2829
from distutils.errors import DistutilsError
2930

31+
try:
32+
from ConfigParser import ConfigParser
33+
except ImportError: # Python 3
34+
from configparser import ConfigParser
35+
3036
from numpy.distutils.misc_util import appendpath
3137
from numpy.distutils import log
3238

@@ -308,6 +314,96 @@ def run(self):
308314
with open(bat_file, 'wt') as fobj:
309315
fobj.write(bat_contents)
310316

317+
# This copied from nibabel/nisext/sexts.py
318+
# We'll probably drop this way of doing versioning soon
319+
def get_comrec_build(pkg_dir, build_cmd=build_py):
320+
""" Return extended build command class for recording commit
321+
322+
The extended command tries to run git to find the current commit, getting
323+
the empty string if it fails. It then writes the commit hash into a file
324+
in the `pkg_dir` path, named ``COMMIT_INFO.txt``.
325+
326+
In due course this information can be used by the package after it is
327+
installed, to tell you what commit it was installed from if known.
328+
329+
To make use of this system, you need a package with a COMMIT_INFO.txt file -
330+
e.g. ``myproject/COMMIT_INFO.txt`` - that might well look like this::
331+
332+
# This is an ini file that may contain information about the code state
333+
[commit hash]
334+
# The line below may contain a valid hash if it has been substituted during 'git archive'
335+
archive_subst_hash=$Format:%h$
336+
# This line may be modified by the install process
337+
install_hash=
338+
339+
The COMMIT_INFO file above is also designed to be used with git substitution
340+
- so you probably also want a ``.gitattributes`` file in the root directory
341+
of your working tree that contains something like this::
342+
343+
myproject/COMMIT_INFO.txt export-subst
344+
345+
That will cause the ``COMMIT_INFO.txt`` file to get filled in by ``git
346+
archive`` - useful in case someone makes such an archive - for example with
347+
via the github 'download source' button.
348+
349+
Although all the above will work as is, you might consider having something
350+
like a ``get_info()`` function in your package to display the commit
351+
information at the terminal. See the ``pkg_info.py`` module in the nipy
352+
package for an example.
353+
"""
354+
class MyBuildPy(build_cmd):
355+
''' Subclass to write commit data into installation tree '''
356+
def run(self):
357+
build_cmd.run(self)
358+
import subprocess
359+
proc = subprocess.Popen('git rev-parse --short HEAD',
360+
stdout=subprocess.PIPE,
361+
stderr=subprocess.PIPE,
362+
shell=True)
363+
repo_commit, _ = proc.communicate()
364+
# Fix for python 3
365+
repo_commit = str(repo_commit)
366+
# We write the installation commit even if it's empty
367+
cfg_parser = ConfigParser()
368+
cfg_parser.read(pjoin(pkg_dir, 'COMMIT_INFO.txt'))
369+
cfg_parser.set('commit hash', 'install_hash', repo_commit)
370+
out_pth = pjoin(self.build_lib, pkg_dir, 'COMMIT_INFO.txt')
371+
cfg_parser.write(open(out_pth, 'wt'))
372+
return MyBuildPy
373+
374+
375+
def get_pkg_version(pkg_name):
376+
""" Return package version for `pkg_name` if installed
377+
378+
Returns
379+
-------
380+
pkg_version : str or None
381+
Return None if package not importable. Return 'unknown' if standard
382+
``__version__`` string not present. Otherwise return version string.
383+
"""
384+
try:
385+
pkg = __import__(pkg_name)
386+
except ImportError:
387+
return None
388+
try:
389+
return pkg.__version__
390+
except AttributeError:
391+
return 'unknown'
392+
393+
394+
def version_error_msg(pkg_name, found_ver, min_ver):
395+
""" Return informative error message for version or None
396+
"""
397+
if found_ver is None:
398+
return 'We need package {0}, but not importable'.format(pkg_name)
399+
if found_ver == 'unknown':
400+
msg = 'We need {0} version {1}, but cannot get version'.format(
401+
pkg_name, min_ver)
402+
if LooseVersion(found_ver) >= LooseVersion(min_ver):
403+
return None
404+
return 'We need {0} version {1}, but found version {2}'.format(
405+
pkg_name, found_ver, min_ver)
406+
311407

312408
# The command classes for distutils, used by setup.py
313409
cmdclass = {'api_docs': APIDocs,

0 commit comments

Comments
 (0)