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