1# Copyright (C) 2009-2017 Barry Warsaw
2#
3# This file is part of setup_helpers.py
4#
5# setup_helpers.py is free software: you can redistribute it and/or modify it
6# under the terms of the GNU Lesser General Public License as published by the
7# Free Software Foundation, version 3 of the License.
8#
9# setup_helpers.py is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
12# for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with setup_helpers.py.  If not, see <http://www.gnu.org/licenses/>.
16
17"""setup.py helper functions."""
18
19from __future__ import absolute_import, print_function, unicode_literals
20
21
22__metaclass__ = type
23__all__ = [
24    'description',
25    'find_doctests',
26    'get_version',
27    'long_description',
28    'require_python',
29    ]
30
31
32import os
33import re
34import sys
35
36
37DEFAULT_VERSION_RE = re.compile(
38    r'(?P<version>\d+\.\d+(?:\.\d+)?(?:(?:a|b|rc)\d+)?)')
39EMPTYSTRING = ''
40
41__version__ = '3.0'
42
43
44def require_python(minimum):
45    """Require at least a minimum Python version.
46
47    The version number is expressed in terms of `sys.hexversion`.  E.g. to
48    require a minimum of Python 2.6, use::
49
50    >>> require_python(0x206000f0)
51
52    :param minimum: Minimum Python version supported.
53    :type minimum: integer
54    """
55    if sys.hexversion < minimum:
56        hversion = hex(minimum)[2:]
57        if len(hversion) % 2 != 0:
58            hversion = '0' + hversion
59        split = list(hversion)
60        parts = []
61        while split:
62            parts.append(int(''.join((split.pop(0), split.pop(0))), 16))
63        major, minor, micro, release = parts
64        if release == 0xf0:
65            print('Python {0}.{1}.{2} or better is required'.format(
66                major, minor, micro))
67        else:
68            print('Python {0}.{1}.{2} ({3}) or better is required'.format(
69                major, minor, micro, hex(release)[2:]))
70        sys.exit(1)
71
72
73def get_version(filename, pattern=None):
74    """Extract the __version__ from a file without importing it.
75
76    While you could get the __version__ by importing the module, the very act
77    of importing can cause unintended consequences.  For example, Distribute's
78    automatic 2to3 support will break.  Instead, this searches the file for a
79    line that starts with __version__, and extract the version number by
80    regular expression matching.
81
82    By default, two or three dot-separated digits are recognized, but by
83    passing a pattern parameter, you can recognize just about anything.  Use
84    the `version` group name to specify the match group.
85
86    :param filename: The name of the file to search.
87    :type filename: string
88    :param pattern: Optional alternative regular expression pattern to use.
89    :type pattern: string
90    :return: The version that was extracted.
91    :rtype: string
92    """
93    if pattern is None:
94        cre = DEFAULT_VERSION_RE
95    else:
96        cre = re.compile(pattern)
97    with open(filename) as fp:
98        for line in fp:
99            if line.startswith('__version__'):
100                mo = cre.search(line)
101                assert mo, 'No valid __version__ string found'
102                return mo.group('version')
103    raise AssertionError('No __version__ assignment found')
104
105
106def find_doctests(start='.', extension='.rst'):
107    """Find separate-file doctests in the package.
108
109    This is useful for Distribute's automatic 2to3 conversion support.  The
110    `setup()` keyword argument `convert_2to3_doctests` requires file names,
111    which may be difficult to track automatically as you add new doctests.
112
113    :param start: Directory to start searching in (default is cwd)
114    :type start: string
115    :param extension: Doctest file extension (default is .txt)
116    :type extension: string
117    :return: The doctest files found.
118    :rtype: list
119    """
120    doctests = []
121    for dirpath, dirnames, filenames in os.walk(start):
122        doctests.extend(os.path.join(dirpath, filename)
123                        for filename in filenames
124                        if filename.endswith(extension))
125    return doctests
126
127
128def long_description(*filenames):
129    """Provide a long description."""
130    res = ['']
131    for filename in filenames:
132        with open(filename) as fp:
133            for line in fp:
134                res.append('   ' + line)
135            res.append('')
136        res.append('\n')
137    return EMPTYSTRING.join(res)
138
139
140def description(filename):
141    """Provide a short description."""
142    # This ends up in the Summary header for PKG-INFO and it should be a
143    # one-liner.  It will get rendered on the package page just below the
144    # package version header but above the long_description, which ironically
145    # gets stuff into the Description header.  It should not include reST, so
146    # pick out the first single line after the double header.
147    with open(filename) as fp:
148        for lineno, line in enumerate(fp):
149            if lineno < 3:
150                continue
151            line = line.strip()
152            if len(line) > 0:
153                return line
154