|
33 | 33 | from distutils.core import setup
|
34 | 34 |
|
35 | 35 | # 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')} |
39 | 249 |
|
40 | 250 | # Get version and release info, which is all stored in nipype/info.py
|
41 | 251 | ver_file = os.path.join('nipype', 'info.py')
|
|
0 commit comments