Skip to content

Commit f91f68d

Browse files
committed
Merge pull request #1172 from nipy/test_setup
Test setup
2 parents 5a2b9f1 + 7af8a0b commit f91f68d

File tree

1 file changed

+213
-3
lines changed

1 file changed

+213
-3
lines changed

setup.py

Lines changed: 213 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,219 @@
3333
from distutils.core import setup
3434

3535
# Commit hash writing, and dependency checking
36-
from nisext.sexts import get_comrec_build, package_check, install_scripts_bat
37-
cmdclass = {'build_py': get_comrec_build('nipype'),
38-
'install_scripts': install_scripts_bat}
36+
''' Distutils / setuptools helpers from nibabel.nisext'''
37+
38+
import os
39+
from os.path import join as pjoin, split as psplit, splitext
40+
import sys
41+
PY3 = sys.version_info[0] >= 3
42+
if PY3:
43+
string_types = str,
44+
else:
45+
string_types = basestring,
46+
try:
47+
from ConfigParser import ConfigParser
48+
except ImportError:
49+
from configparser import ConfigParser
50+
51+
from distutils.version import LooseVersion
52+
from distutils.command.build_py import build_py
53+
from distutils.command.install_scripts import install_scripts
54+
55+
from distutils import log
56+
57+
def get_comrec_build(pkg_dir, build_cmd=build_py):
58+
""" Return extended build command class for recording commit
59+
60+
The extended command tries to run git to find the current commit, getting
61+
the empty string if it fails. It then writes the commit hash into a file
62+
in the `pkg_dir` path, named ``COMMIT_INFO.txt``.
63+
64+
In due course this information can be used by the package after it is
65+
installed, to tell you what commit it was installed from if known.
66+
67+
To make use of this system, you need a package with a COMMIT_INFO.txt file -
68+
e.g. ``myproject/COMMIT_INFO.txt`` - that might well look like this::
69+
70+
# This is an ini file that may contain information about the code state
71+
[commit hash]
72+
# The line below may contain a valid hash if it has been substituted during 'git archive'
73+
archive_subst_hash=$Format:%h$
74+
# This line may be modified by the install process
75+
install_hash=
76+
77+
The COMMIT_INFO file above is also designed to be used with git substitution
78+
- so you probably also want a ``.gitattributes`` file in the root directory
79+
of your working tree that contains something like this::
80+
81+
myproject/COMMIT_INFO.txt export-subst
82+
83+
That will cause the ``COMMIT_INFO.txt`` file to get filled in by ``git
84+
archive`` - useful in case someone makes such an archive - for example with
85+
via the github 'download source' button.
86+
87+
Although all the above will work as is, you might consider having something
88+
like a ``get_info()`` function in your package to display the commit
89+
information at the terminal. See the ``pkg_info.py`` module in the nipy
90+
package for an example.
91+
"""
92+
class MyBuildPy(build_cmd):
93+
''' Subclass to write commit data into installation tree '''
94+
def run(self):
95+
build_cmd.run(self)
96+
import subprocess
97+
proc = subprocess.Popen('git rev-parse --short HEAD',
98+
stdout=subprocess.PIPE,
99+
stderr=subprocess.PIPE,
100+
shell=True)
101+
repo_commit, _ = proc.communicate()
102+
# Fix for python 3
103+
repo_commit = str(repo_commit)
104+
# We write the installation commit even if it's empty
105+
cfg_parser = ConfigParser()
106+
cfg_parser.read(pjoin(pkg_dir, 'COMMIT_INFO.txt'))
107+
cfg_parser.set('commit hash', 'install_hash', repo_commit)
108+
out_pth = pjoin(self.build_lib, pkg_dir, 'COMMIT_INFO.txt')
109+
cfg_parser.write(open(out_pth, 'wt'))
110+
return MyBuildPy
111+
112+
113+
def _add_append_key(in_dict, key, value):
114+
""" Helper for appending dependencies to setuptools args """
115+
# If in_dict[key] does not exist, create it
116+
# If in_dict[key] is a string, make it len 1 list of strings
117+
# Append value to in_dict[key] list
118+
if key not in in_dict:
119+
in_dict[key] = []
120+
elif isinstance(in_dict[key], string_types):
121+
in_dict[key] = [in_dict[key]]
122+
in_dict[key].append(value)
123+
124+
125+
# Dependency checks
126+
def package_check(pkg_name, version=None,
127+
optional=False,
128+
checker=LooseVersion,
129+
version_getter=None,
130+
messages=None,
131+
setuptools_args=None
132+
):
133+
''' Check if package `pkg_name` is present and has good enough version
134+
135+
Has two modes of operation. If `setuptools_args` is None (the default),
136+
raise an error for missing non-optional dependencies and log warnings for
137+
missing optional dependencies. If `setuptools_args` is a dict, then fill
138+
``install_requires`` key value with any missing non-optional dependencies,
139+
and the ``extras_requires`` key value with optional dependencies.
140+
141+
This allows us to work with and without setuptools. It also means we can
142+
check for packages that have not been installed with setuptools to avoid
143+
installing them again.
144+
145+
Parameters
146+
----------
147+
pkg_name : str
148+
name of package as imported into python
149+
version : {None, str}, optional
150+
minimum version of the package that we require. If None, we don't
151+
check the version. Default is None
152+
optional : bool or str, optional
153+
If ``bool(optional)`` is False, raise error for absent package or wrong
154+
version; otherwise warn. If ``setuptools_args`` is not None, and
155+
``bool(optional)`` is not False, then `optional` should be a string
156+
giving the feature name for the ``extras_require`` argument to setup.
157+
checker : callable, optional
158+
callable with which to return comparable thing from version
159+
string. Default is ``distutils.version.LooseVersion``
160+
version_getter : {None, callable}:
161+
Callable that takes `pkg_name` as argument, and returns the
162+
package version string - as in::
163+
164+
``version = version_getter(pkg_name)``
165+
166+
If None, equivalent to::
167+
168+
mod = __import__(pkg_name); version = mod.__version__``
169+
messages : None or dict, optional
170+
dictionary giving output messages
171+
setuptools_args : None or dict
172+
If None, raise errors / warnings for missing non-optional / optional
173+
dependencies. If dict fill key values ``install_requires`` and
174+
``extras_require`` for non-optional and optional dependencies.
175+
'''
176+
setuptools_mode = not setuptools_args is None
177+
optional_tf = bool(optional)
178+
if version_getter is None:
179+
def version_getter(pkg_name):
180+
mod = __import__(pkg_name)
181+
return mod.__version__
182+
if messages is None:
183+
messages = {}
184+
msgs = {
185+
'missing': 'Cannot import package "%s" - is it installed?',
186+
'missing opt': 'Missing optional package "%s"',
187+
'opt suffix' : '; you may get run-time errors',
188+
'version too old': 'You have version %s of package "%s"'
189+
' but we need version >= %s', }
190+
msgs.update(messages)
191+
status, have_version = _package_status(pkg_name,
192+
version,
193+
version_getter,
194+
checker)
195+
if status == 'satisfied':
196+
return
197+
if not setuptools_mode:
198+
if status == 'missing':
199+
if not optional_tf:
200+
raise RuntimeError(msgs['missing'] % pkg_name)
201+
log.warn(msgs['missing opt'] % pkg_name +
202+
msgs['opt suffix'])
203+
return
204+
elif status == 'no-version':
205+
raise RuntimeError('Cannot find version for %s' % pkg_name)
206+
assert status == 'low-version'
207+
if not optional_tf:
208+
raise RuntimeError(msgs['version too old'] % (have_version,
209+
pkg_name,
210+
version))
211+
log.warn(msgs['version too old'] % (have_version,
212+
pkg_name,
213+
version)
214+
+ msgs['opt suffix'])
215+
return
216+
# setuptools mode
217+
if optional_tf and not isinstance(optional, string_types):
218+
raise RuntimeError('Not-False optional arg should be string')
219+
dependency = pkg_name
220+
if version:
221+
dependency += '>=' + version
222+
if optional_tf:
223+
if not 'extras_require' in setuptools_args:
224+
setuptools_args['extras_require'] = {}
225+
_add_append_key(setuptools_args['extras_require'],
226+
optional,
227+
dependency)
228+
return
229+
#_add_append_key(setuptools_args, 'install_requires', dependency)
230+
return
231+
232+
233+
def _package_status(pkg_name, version, version_getter, checker):
234+
try:
235+
__import__(pkg_name)
236+
except ImportError:
237+
return 'missing', None
238+
if not version:
239+
return 'satisfied', None
240+
try:
241+
have_version = version_getter(pkg_name)
242+
except AttributeError:
243+
return 'no-version', None
244+
if checker(have_version) < checker(version):
245+
return 'low-version', have_version
246+
return 'satisfied', have_version
247+
248+
cmdclass = {'build_py': get_comrec_build('nipype')}
39249

40250
# Get version and release info, which is all stored in nipype/info.py
41251
ver_file = os.path.join('nipype', 'info.py')

0 commit comments

Comments
 (0)