1from __future__ import with_statement
2
3import datetime
4import glob
5import os
6import string
7import sys
8
9from distutils import log
10
11
12try:
13    from packaging.util import split_multiline
14except ImportError:
15    try:
16        from distutils2.util import split_multiline
17    except ImportError:
18        from d2to1.util import split_multiline
19
20
21try:
22    # python 2
23    reload
24except NameError:
25    # python 3
26    from imp import reload
27
28from .svnutils import get_svn_info, get_svn_version
29from .versionutils import (package_uses_version_py, clean_version_py,
30                           update_setup_datetime, VERSION_PY_TEMPLATE)
31
32# For each version.py that we create, we will note the version of
33# stsci.distutils (this package) that created it.  The problem is
34# when the package being installed is stsci.distutils -- we don't
35# know the version yet.  So, if we can't import the version (because
36# it does not exist yet), we declare it to be None and special case
37# it later.
38try :
39    from . import version as my_version
40except ImportError :
41    my_version = None
42
43
44def is_display_option(ignore=None):
45    """A hack to test if one of the arguments passed to setup.py is a display
46    argument that should just display a value and exit.  If so, don't bother
47    running this hook (this capability really ought to be included with
48    distutils2).
49
50    Optionally, ignore may contain a list of display options to ignore in this
51    check.  Each option in the ignore list must contain the correct number of
52    dashes.
53    """
54
55    from setuptools.dist import Distribution
56
57    # If there were no arguments in argv (aside from the script name) then this
58    # is an implied display opt
59    if len(sys.argv) < 2:
60        return True
61
62    display_opts = ['--command-packages', '--help', '-h']
63
64    for opt in Distribution.display_options:
65        display_opts.append('--' + opt[0])
66
67    for arg in sys.argv:
68        if arg in display_opts and arg not in ignore:
69            return True
70
71    return False
72
73
74# TODO: With luck this can go away soon--packaging now supports adding the cwd
75# to sys.path for running setup_hooks.  But it also needs to support adding
76# packages_root.  Also, it currently does not support adding cwd/packages_root
77# to sys.path for pre/post-command hooks, so that needs to be fixed.
78def use_packages_root(config):
79    """
80    Adds the path specified by the 'packages_root' option, or the current path
81    if 'packages_root' is not specified, to sys.path.  This is particularly
82    useful, for example, to run setup_hooks or add custom commands that are in
83    your package's source tree.
84
85    Use this when the root of your package source tree is not in
86    the same directory with the setup.py
87
88    Config Usage::
89
90        [files]
91        packages_root = lib
92        # for installing pkgname from lib/pkgname/*.py
93
94        [global]
95        setup_hooks = stsci.distutils.hooks.use_packages_root
96
97    """
98
99    if 'files' in config and 'packages_root' in config['files']:
100        root = config['files']['packages_root']
101    else:
102        root = ''
103
104    if root not in sys.path:
105        if root and sys.path[0] == '':
106            sys.path.insert(1, root)
107        else:
108            sys.path.insert(0, root)
109
110    # Reload the stsci namespace package in case any new paths can be added to
111    # it from the new sys.path entry; depending on how the namespace packages
112    # are installed this may fail--specifically if it's using the old
113    # setuptools-based .pth approach there is not importable package called
114    # 'stsci' that can be imported by itself.
115    if 'stsci' in sys.modules:
116        mod = sys.modules['stsci']
117        if sys.version_info[:2] >= (3, 3) and not hasattr(mod, '__loader__'):
118            # Workaround for Python bug #17099 on Python 3.3, where reload()
119            # crashes if a module doesn't have an __loader__ attribute
120            del sys.modules['stsci']
121            try:
122                import stsci
123            except ImportError:
124                pass
125        else:
126            try :
127                reload(sys.modules['stsci'])
128            except ImportError:
129                # doesn't seem to bother anything when this reload() fails
130                pass
131
132
133def tag_svn_revision(config):
134    """
135    A setup_hook to add the SVN revision of the current working copy path to
136    the package version string, but only if the version ends in .dev.
137
138    For example, ``mypackage-1.0.dev`` becomes ``mypackage-1.0.dev1234``.  This
139    is in accordance with the version string format standardized by PEP 386.
140
141    This should be used as a replacement for the ``tag_svn_revision`` option to
142    the egg_info command.  This hook is more compatible with
143    packaging/distutils2, which does not include any VCS support.  This hook is
144    also more flexible in that it turns the revision number on/off depending on
145    the presence of ``.dev`` in the version string, so that it's not
146    automatically added to the version in final releases.
147
148    This hook does require the ``svnversion`` command to be available in order
149    to work.  It does not examine the working copy metadata directly.
150
151
152    Config Usage::
153
154        [global]
155        setup_hooks = stsci.distutils.hooks.tag_svn_revision
156
157    You should write exactly this in your package's ``__init__.py``::
158
159        from .version import *
160
161    """
162
163    if 'metadata' in config and 'version' in config['metadata']:
164        metadata = config['metadata']
165        version = metadata['version']
166
167        # Don't add an svn revision unless the version ends with .dev
168        if not version.endswith('.dev'):
169            return
170
171        # First try to get the revision by checking for it in an existing
172        # .version module
173        package_dir = config.get('files', {}).get('packages_root', '')
174        packages = config.get('files', {}).get('packages', '')
175        packages = split_multiline(packages)
176        rev = None
177        for package in packages:
178            version_py = package_uses_version_py(package_dir, package)
179            if not version_py:
180                continue
181            try:
182                mod = __import__(package + '.version',
183                                 fromlist='__svn_revision__')
184            except ImportError:
185                mod = None
186            if mod is not None and hasattr(mod, '__svn_revision__'):
187                rev = mod.__svn_revision__
188                break
189
190        # Cleanup
191        names = set([package, package + '.'])
192        for modname in list(sys.modules):
193            if modname == package or modname.startswith(package + '.'):
194                del sys.modules[modname]
195
196        if rev is None:
197            # A .version module didn't exist or was incomplete; try calling
198            # svnversion directly
199            rev = get_svn_version()
200
201        if not rev:
202            return
203        if ':' in rev:
204            rev, _ = rev.split(':', 1)
205        while rev and rev[-1] not in string.digits:
206            rev = rev[:-1]
207        try:
208            rev = int(rev)
209        except (ValueError, TypeError):
210            return
211
212        metadata['version'] ='%s%d' % (version, rev)
213
214
215def _version_hook(function_name, package_dir, packages, name, version, vdate):
216    """This command hook creates an version.py file in each package that
217    requires it.  This is by determining if the package's ``__init__.py`` tries
218    to import or import from the version module.
219
220    version.py will not be created in packages that don't use it.  It should
221    only be used by the top-level package of the project.
222
223    Don't use this function directly--instead use :func:`version_setup_hook` or
224    :func:`version_pre_command_hook` which know how to retrieve the required
225    metadata depending on the context they are run in.
226
227    Not called directly from the config file.  See :func:`version_setup_hook`.
228
229    """
230
231    # Strip any revision info from version; that will be handled separately
232    if '-' in version:
233        version = version.split('-', 1)[0]
234
235    for package in packages:
236        version_py = package_uses_version_py(package_dir, package)
237        if not version_py:
238            continue
239
240        rev = get_svn_version()
241        if ((not rev or not rev[0] in string.digits) and
242                os.path.exists(version_py)):
243            # If were unable to determine an SVN revision and the version.py
244            # already exists, just update the __setup_datetime__ and leave the
245            # rest of the file untouched
246            update_setup_datetime(version_py)
247            return
248        elif rev is None:
249            rev = 'Unable to determine SVN revision'
250
251        svn_info = get_svn_info()
252
253        # Wrap version, rev, and svn_info in str() to ensure that Python 2
254        # unicode literals aren't written, which will break things in Python 3
255        template_variables = {
256                'hook_function': function_name,
257                'name': name,
258                'version': str(version),
259                'vdate': str(vdate),
260                'svn_revision': str(rev),
261                'svn_full_info': str(svn_info),
262                'setup_datetime': datetime.datetime.now(),
263        }
264
265        # my_version is version.py for the stsci.distutils package.
266        # It can be None if we are called during the install of
267        # stsci.distutils; we are creating the version.py, so it was
268        # not available to import yet.  If this is what is happening,
269        # we construct it specially.
270        if my_version is None :
271            if  package == 'stsci.distutils' :
272                template_variables['stsci_distutils_version'] = version
273            else:
274                # It should never happen that version.py does not
275                # exist when we are installing any other package.
276                raise RuntimeError('Internal consistency error')
277        else :
278            template_variables['stsci_distutils_version'] = \
279                    my_version.__version__
280
281        with open(version_py, 'w') as f:
282            f.write(VERSION_PY_TEMPLATE % template_variables)
283
284
285def version_setup_hook(config):
286    """Creates a Python module called version.py which contains these variables:
287
288    * ``__version__`` (the release version)
289    * ``__svn_revision__`` (the SVN revision info as returned by the ``svnversion``
290      command)
291    * ``__svn_full_info__`` (as returned by the ``svn info`` command)
292    * ``__setup_datetime__`` (the date and time that setup.py was last run).
293    * ``__vdate__`` (the release date)
294
295    These variables can be imported in the package's ``__init__.py`` for
296    degugging purposes.  The version.py module will *only* be created in a
297    package that imports from the version module in its ``__init__.py``.  It
298    should be noted that this is generally preferable to writing these
299    variables directly into ``__init__.py``, since this provides more control
300    and is less likely to unexpectedly break things in ``__init__.py``.
301
302    Config Usage::
303
304        [global]
305        setup-hooks = stsci.distutils.hooks.version_setup_hook
306
307    You should write exactly this in your package's ``__init__.py``::
308
309        from .version import *
310
311    """
312
313    if is_display_option(ignore=['--version']):
314        return
315
316    name = config['metadata'].get('name')
317    version = config['metadata'].get('version', '0.0.0')
318    vdate = config['metadata'].get('vdate', 'unspecified')
319    package_dir = config.get('files', {}).get('packages_root', '')
320    packages = config.get('files', {}).get('packages', '')
321
322    packages = split_multiline(packages)
323
324    _version_hook(__name__ + '.version_setup_hook', package_dir, packages,
325                 name, version, vdate)
326
327
328def version_pre_command_hook(command_obj):
329    """
330    .. deprecated:: 0.3
331        Use :func:`version_setup_hook` instead; it's generally safer to
332        check/update the version.py module on every setup.py run instead of on
333        specific commands.
334
335    This command hook creates an version.py file in each package that requires
336    it.  This is by determining if the package's ``__init__.py`` tries to
337    import or import from the version module.
338
339    version.py will not be created in packages that don't use it.  It should
340    only be used by the top-level package of the project.
341    """
342
343    if is_display_option():
344        return
345
346    package_dir = command_obj.distribution.package_dir.get('', '.')
347    packages = command_obj.distribution.packages
348    name = command_obj.distribution.metadata.name
349    version = command_obj.distribution.metadata.version
350
351    _version_hook(__name__ + '.version_pre_command_hook',package_dir, packages,
352                 name, version, vdate=None)
353
354
355def version_post_command_hook(command_obj):
356    """
357    .. deprecated:: 0.3
358        This hook was meant to complement :func:`version_pre_command_hook`,
359        also deprecated.
360
361    Cleans up a previously generated version.py in order to avoid
362    clutter.
363
364    Only removes the file if we're in an SVN working copy and the file is not
365    already under version control.
366    """
367
368    package_dir = command_obj.distribution.package_dir.get('', '.')
369    packages = command_obj.distribution.packages
370
371    for package in packages:
372        clean_version_py(package_dir, package)
373
374
375def numpy_extension_hook(command_obj):
376    """A distutils2 pre-command hook for the build_ext command needed for
377    building extension modules that use NumPy.
378
379    To use this hook, add 'numpy' to the list of include_dirs in setup.cfg
380    section for an extension module.  This hook will replace 'numpy' with the
381    necessary numpy header paths in the include_dirs option for that extension.
382
383    Note: Although this function uses numpy, stsci.distutils does not depend on
384    numpy.  It is up to the distribution that uses this hook to require numpy
385    as a dependency.
386
387    Config Usage::
388
389        [extension=mypackage.extmod]
390        sources =
391            foo.c
392            bar.c
393        include_dirs = numpy
394
395        [build_ext]
396        pre-hook.numpy-extension = stsci.distutils.hooks.numpy_extension_hook
397    """
398
399    command_name = command_obj.get_command_name()
400    if command_name != 'build_ext':
401        log.warn('%s is meant to be used with the build_ext command only; '
402                 'it is not for use with the %s command.' %
403                 (__name__, command_name))
404    try:
405        import numpy
406    except ImportError:
407        # It's virtually impossible to automatically install numpy through
408        # setuptools; I've tried.  It's not pretty.
409        # Besides, we don't want users complaining that our software doesn't
410        # work just because numpy didn't build on their system.
411        sys.stderr.write('\n\nNumpy is required to build this package.\n'
412                         'Please install Numpy on your system first.\n\n')
413        sys.exit(1)
414
415    includes = [numpy.get_include()]
416    #includes = [numpy.get_numarray_include(), numpy.get_include()]
417    for extension in command_obj.extensions:
418        if 'numpy' not in extension.include_dirs:
419            continue
420        idx = extension.include_dirs.index('numpy')
421        for inc in includes:
422            extension.include_dirs.insert(idx, inc)
423        extension.include_dirs.remove('numpy')
424
425
426def glob_data_files(command_obj):
427    """A pre-command hook for the install_data command allowing wildcard
428    patterns to be used in the data_files option.
429
430    Also ensures that data files with relative paths as their targets are
431    installed relative install_lib.
432
433    Config Usage::
434
435        [files]
436        data_files =
437            target_directory = source_directory/*.foo
438            other_target_directory = other_source_directory/*
439
440        [install_data]
441        pre-hook.glob-data-files = stsci.distutils.hooks.glob_data_files
442
443
444    """
445
446    command_name = command_obj.get_command_name()
447    if command_name != 'install_data':
448        log.warn('%s is meant to be used with the install_data command only; '
449                 'it is not for use with the %s command.' %
450                 (__name__, command_name))
451
452    data_files = command_obj.data_files
453
454    for idx, val in enumerate(data_files[:]):
455        if isinstance(val, basestring):
456            # Support the rare, deprecated case where just a filename is given
457            filenames = glob.glob(val)
458            del data_files[idx]
459            data_files.extend(filenames)
460            continue
461
462        dest, filenames = val
463        filenames = sum((glob.glob(item) for item in filenames), [])
464        data_files[idx] = (dest, filenames)
465
466    # Ensure the correct install dir; this is the default behavior for
467    # installing with distribute, but when using
468    # --single-version-externally-managed we need to to tweak this
469    install_cmd = command_obj.get_finalized_command('install')
470    if command_obj.install_dir == install_cmd.install_data:
471        install_lib_cmd = command_obj.get_finalized_command('install_lib')
472        command_obj.install_dir = install_lib_cmd.install_dir
473