1
# Licensed under a 3-clause BSD style license - see LICENSE.rst
2

3 20
"""
4
Utilities for generating the version string for Astropy (or an affiliated
5
package) and the version.py module, which contains version info for the
6
package.
7

8
Within the generated astropy.version module, the `major`, `minor`, and `bugfix`
9
variables hold the respective parts of the version number (bugfix is '0' if
10
absent). The `release` variable is True if this is a release, and False if this
11
is a development version of astropy. For the actual version string, use::
12

13
    from astropy.version import version
14

15
or::
16

17
    from astropy import __version__
18

19
"""
20

21 20
import datetime
22 20
import os
23 20
import pkgutil
24 20
import sys
25 20
import time
26 20
import warnings
27

28 20
from distutils import log
29 20
from configparser import ConfigParser
30

31 20
import pkg_resources
32

33 20
from . import git_helpers
34 20
from .distutils_helpers import is_distutils_display_option
35 20
from .git_helpers import get_git_devstr
36 20
from .utils import AstropyDeprecationWarning, import_file
37

38 20
__all__ = ['generate_version_py']
39

40

41 20
def _version_split(version):
42
    """
43
    Split a version string into major, minor, and bugfix numbers.  If any of
44
    those numbers are missing the default is zero.  Any pre/post release
45
    modifiers are ignored.
46

47
    Examples
48
    ========
49
    >>> _version_split('1.2.3')
50
    (1, 2, 3)
51
    >>> _version_split('1.2')
52
    (1, 2, 0)
53
    >>> _version_split('1.2rc1')
54
    (1, 2, 0)
55
    >>> _version_split('1')
56
    (1, 0, 0)
57
    >>> _version_split('')
58
    (0, 0, 0)
59
    """
60

61 20
    parsed_version = pkg_resources.parse_version(version)
62

63 20
    if hasattr(parsed_version, 'base_version'):
64
        # New version parsing for setuptools >= 8.0
65 20
        if parsed_version.base_version:
66 20
            parts = [int(part)
67
                     for part in parsed_version.base_version.split('.')]
68
        else:
69 0
            parts = []
70
    else:
71 0
        parts = []
72 0
        for part in parsed_version:
73 0
            if part.startswith('*'):
74
                # Ignore any .dev, a, b, rc, etc.
75 0
                break
76 0
            parts.append(int(part))
77

78 20
    if len(parts) < 3:
79 20
        parts += [0] * (3 - len(parts))
80

81
    # In principle a version could have more parts (like 1.2.3.4) but we only
82
    # support <major>.<minor>.<micro>
83 20
    return tuple(parts[:3])
84

85

86
# This is used by setup.py to create a new version.py - see that file for
87
# details. Note that the imports have to be absolute, since this is also used
88
# by affiliated packages.
89 20
_FROZEN_VERSION_PY_TEMPLATE = """
90
# Autogenerated by {packagetitle}'s setup.py on {timestamp!s} UTC
91
import datetime
92

93
{header}
94

95
major = {major}
96
minor = {minor}
97
bugfix = {bugfix}
98

99
version_info = (major, minor, bugfix)
100

101
release = {rel}
102
timestamp = {timestamp!r}
103
debug = {debug}
104

105
astropy_helpers_version = "{ahver}"
106
"""[1:]
107

108

109 20
_FROZEN_VERSION_PY_WITH_GIT_HEADER = """
110
{git_helpers}
111

112

113
_packagename = "{packagename}"
114
_last_generated_version = "{verstr}"
115
_last_githash = "{githash}"
116

117
# Determine where the source code for this module
118
# lives.  If __file__ is not a filesystem path then
119
# it is assumed not to live in a git repo at all.
120
if _get_repo_path(__file__, levels=len(_packagename.split('.'))):
121
    version = update_git_devstr(_last_generated_version, path=__file__)
122
    githash = get_git_devstr(sha=True, show_warning=False,
123
                             path=__file__) or _last_githash
124
else:
125
    # The file does not appear to live in a git repo so don't bother
126
    # invoking git
127
    version = _last_generated_version
128
    githash = _last_githash
129
"""[1:]
130

131

132 20
_FROZEN_VERSION_PY_STATIC_HEADER = """
133
version = "{verstr}"
134
githash = "{githash}"
135
"""[1:]
136

137

138 20
def _get_version_py_str(packagename, version, githash, release, debug,
139
                        uses_git=True):
140 20
    try:
141 20
        from astropy_helpers import __version__ as ahver
142 0
    except ImportError:
143 0
        ahver = "unknown"
144

145 20
    epoch = int(os.environ.get('SOURCE_DATE_EPOCH', time.time()))
146 20
    timestamp = datetime.datetime.utcfromtimestamp(epoch)
147 20
    major, minor, bugfix = _version_split(version)
148

149 20
    if packagename.lower() == 'astropy':
150 0
        packagetitle = 'Astropy'
151
    else:
152 20
        packagetitle = 'Astropy-affiliated package ' + packagename
153

154 20
    header = ''
155

156 20
    if uses_git:
157 20
        header = _generate_git_header(packagename, version, githash)
158 20
    elif not githash:
159
        # _generate_git_header will already generate a new git has for us, but
160
        # for creating a new version.py for a release (even if uses_git=False)
161
        # we still need to get the githash to include in the version.py
162
        # See https://github.com/astropy/astropy-helpers/issues/141
163 20
        githash = git_helpers.get_git_devstr(sha=True, show_warning=True)
164

165 20
    if not header:  # If _generate_git_header fails it returns an empty string
166 20
        header = _FROZEN_VERSION_PY_STATIC_HEADER.format(verstr=version,
167
                                                         githash=githash)
168

169 20
    return _FROZEN_VERSION_PY_TEMPLATE.format(packagetitle=packagetitle,
170
                                              timestamp=timestamp,
171
                                              header=header,
172
                                              major=major,
173
                                              minor=minor,
174
                                              bugfix=bugfix,
175
                                              ahver=ahver,
176
                                              rel=release, debug=debug)
177

178

179 20
def _generate_git_header(packagename, version, githash):
180
    """
181
    Generates a header to the version.py module that includes utilities for
182
    probing the git repository for updates (to the current git hash, etc.)
183
    These utilities should only be available in development versions, and not
184
    in release builds.
185

186
    If this fails for any reason an empty string is returned.
187
    """
188

189 20
    loader = pkgutil.get_loader(git_helpers)
190 20
    source = loader.get_source(git_helpers.__name__) or ''
191 20
    source_lines = source.splitlines()
192 20
    if not source_lines:
193 0
        log.warn('Cannot get source code for astropy_helpers.git_helpers; '
194
                 'git support disabled.')
195 0
        return ''
196

197 20
    idx = 0
198 20
    for idx, line in enumerate(source_lines):
199 20
        if line.startswith('# BEGIN'):
200 20
            break
201 20
    git_helpers_py = '\n'.join(source_lines[idx + 1:])
202

203 20
    verstr = version
204

205 20
    new_githash = git_helpers.get_git_devstr(sha=True, show_warning=False)
206

207 20
    if new_githash:
208 20
        githash = new_githash
209

210 20
    return _FROZEN_VERSION_PY_WITH_GIT_HEADER.format(
211
                git_helpers=git_helpers_py, packagename=packagename,
212
                verstr=verstr, githash=githash)
213

214

215 20
def generate_version_py(packagename=None, version=None, release=None, debug=None,
216
                        uses_git=None, srcdir='.'):
217
    """
218
    Generate a version.py file in the package with version information, and
219
    update developer version strings.
220

221
    This function should normally be called without any arguments. In this case
222
    the package name and version is read in from the ``setup.cfg`` file (from
223
    the ``name`` or ``package_name`` entry and the ``version`` entry in the
224
    ``[metadata]`` section).
225

226
    If the version is a developer version (of the form ``3.2.dev``), the
227
    version string will automatically be expanded to include a sequential
228
    number as a suffix (e.g. ``3.2.dev13312``), and the updated version string
229
    will be returned by this function.
230

231
    Based on this updated version string, a ``version.py`` file will be
232
    generated inside the package, containing the version string as well as more
233
    detailed information (for example the major, minor, and bugfix version
234
    numbers, a ``release`` flag indicating whether the current version is a
235
    stable or developer version, and so on.
236
    """
237

238 20
    if packagename is not None:
239 20
        warnings.warn('The packagename argument to generate_version_py has '
240
                      'been deprecated and will be removed in future. Specify '
241
                      'the package name in setup.cfg instead', AstropyDeprecationWarning)
242

243 20
    if version is not None:
244 20
        warnings.warn('The version argument to generate_version_py has '
245
                      'been deprecated and will be removed in future. Specify '
246
                      'the version number in setup.cfg instead', AstropyDeprecationWarning)
247

248 20
    if release is not None:
249 20
        warnings.warn('The release argument to generate_version_py has '
250
                      'been deprecated and will be removed in future. We now '
251
                      'use the presence of the "dev" string in the version to '
252
                      'determine whether this is a release', AstropyDeprecationWarning)
253

254
    # We use ConfigParser instead of read_configuration here because the latter
255
    # only reads in keys recognized by setuptools, but we need to access
256
    # package_name below.
257 20
    conf = ConfigParser()
258 20
    conf.read('setup.cfg')
259

260 20
    if conf.has_option('metadata', 'name'):
261 20
        packagename = conf.get('metadata', 'name')
262 20
    elif conf.has_option('metadata', 'package_name'):
263
        # The package-template used package_name instead of name for a while
264 0
        warnings.warn('Specifying the package name using the "package_name" '
265
                      'option in setup.cfg is deprecated - use the "name" '
266
                      'option instead.', AstropyDeprecationWarning)
267 0
        packagename = conf.get('metadata', 'package_name')
268 20
    elif packagename is not None:  # deprecated
269 20
        pass
270
    else:
271 0
        sys.stderr.write('ERROR: Could not read package name from setup.cfg\n')
272 0
        sys.exit(1)
273

274 20
    if conf.has_option('metadata', 'version'):
275 20
        version = conf.get('metadata', 'version')
276 20
        add_git_devstr = True
277 20
    elif version is not None:  # deprecated
278 20
        add_git_devstr = False
279
    else:
280 0
        sys.stderr.write('ERROR: Could not read package version from setup.cfg\n')
281 0
        sys.exit(1)
282

283 20
    if release is None:
284 20
        release = 'dev' not in version
285

286 20
    if not release and add_git_devstr:
287 20
        version += get_git_devstr(False)
288

289 20
    if uses_git is None:
290 20
        uses_git = not release
291

292
    # In some cases, packages have a - but this is a _ in the module. Since we
293
    # are only interested in the module here, we replace - by _
294 20
    packagename = packagename.replace('-', '_')
295

296 20
    try:
297 20
        version_module = get_pkg_version_module(packagename)
298

299 20
        try:
300 20
            last_generated_version = version_module._last_generated_version
301 20
        except AttributeError:
302 20
            last_generated_version = version_module.version
303

304 20
        try:
305 20
            last_githash = version_module._last_githash
306 20
        except AttributeError:
307 20
            last_githash = version_module.githash
308

309 20
        current_release = version_module.release
310 20
        current_debug = version_module.debug
311 20
    except ImportError:
312 20
        version_module = None
313 20
        last_generated_version = None
314 20
        last_githash = None
315 20
        current_release = None
316 20
        current_debug = None
317

318 20
    if release is None:
319
        # Keep whatever the current value is, if it exists
320 0
        release = bool(current_release)
321

322 20
    if debug is None:
323
        # Likewise, keep whatever the current value is, if it exists
324 20
        debug = bool(current_debug)
325

326 20
    package_srcdir = os.path.join(srcdir, *packagename.split('.'))
327 20
    version_py = os.path.join(package_srcdir, 'version.py')
328

329 20
    if (last_generated_version != version or current_release != release or
330
            current_debug != debug):
331 20
        if '-q' not in sys.argv and '--quiet' not in sys.argv:
332 20
            log.set_threshold(log.INFO)
333

334 20
        if is_distutils_display_option():
335
            # Always silence unnecessary log messages when display options are
336
            # being used
337 20
            log.set_threshold(log.WARN)
338

339 20
        log.info('Freezing version number to {0}'.format(version_py))
340

341 20
        with open(version_py, 'w') as f:
342
            # This overwrites the actual version.py
343 20
            f.write(_get_version_py_str(packagename, version, last_githash,
344
                                        release, debug, uses_git=uses_git))
345

346 20
    return version
347

348

349 20
def get_pkg_version_module(packagename, fromlist=None):
350
    """Returns the package's .version module generated by
351
    `astropy_helpers.version_helpers.generate_version_py`.  Raises an
352
    ImportError if the version module is not found.
353

354
    If ``fromlist`` is an iterable, return a tuple of the members of the
355
    version module corresponding to the member names given in ``fromlist``.
356
    Raises an `AttributeError` if any of these module members are not found.
357
    """
358

359 20
    version = import_file(os.path.join(packagename, 'version.py'), name='version')
360

361 20
    if fromlist:
362 0
        return tuple(getattr(version, member) for member in fromlist)
363
    else:
364 20
        return version

Read our documentation on viewing source code .

Loading