-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathmake-release.py
executable file
·219 lines (171 loc) · 7.58 KB
/
make-release.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
#!/usr/bin/env python3
#
# make-release.py - facilitates releasing and publishing versions of a package
#
# Copyright (c) 2011 by Armin Ronacher.
# Copyright 2014 Jeffrey Finkelstein.
#
# Some rights reserved.
#
# Redistribution and use in source and binary forms of the software as well as
# documentation, with or without modification, are permitted provided that the
# following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# * The names of the contributors may not be used to endorse or promote
# products derived from this software without specific prior written
# permission.
#
# THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
# NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE AND
# DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""Updates versions for, tags, and publishes a release of a Python package.
This script requires Python 3.
To use this script::
$ ./make-release.py
This script assumes your versions conform to `semantic versioning`_. By
default, it increments the patch number in the version number (for example,
2.7.1 to 2.7.2). In order to increment a major or minor version number instead
(for example, 2.7.1 to 3.0.0 or 2.7.1 to 2.8.0, respectively), specify either
``major`` or ``minor`` as the sole argument to the script::
$ ./make-release.py major
$ ./make-release.py minor
.. _semantic versioning: http://semver.org/
"""
import os.path
import re
from subprocess import Popen
from subprocess import PIPE
import sys
#: The name of the top-level Python package containing the code for this
#: project, that is, the name of the directory containing the __init__.py file.
PACKAGE = 'birkhoff'
#: The variable containing the version string in the __init__.py file.
INIT_VERSION_STRING = '__version__'
#: The keyword containing the version string in the setup.py file.
SETUP_VERSION_STRING = 'version'
def bump_version(version, which=None):
"""Returns the result of incrementing `version`.
If `which` is not specified, the "patch" part of the version number will be
incremented. If `which` is specified, it must be ``'major'``, ``'minor'``,
or ``'patch'``. If it is one of these three strings, the corresponding part
of the version number will be incremented instead of the patch number.
Returns a string representing the next version number.
Example::
>>> bump_version('2.7.1')
'2.7.2'
>>> bump_version('2.7.1', 'minor')
'2.8.0'
>>> bump_version('2.7.1', 'major')
'3.0.0'
"""
try:
parts = [int(n) for n in version.split('.')]
except ValueError:
fail('Current version is not numeric')
if len(parts) != 3:
fail('Current version is not semantic versioning')
# Determine where to increment the version number
PARTS = {'major': 0, 'minor': 1, 'patch': 2}
index = PARTS[which] if which in PARTS else 2
# Increment the version number at that index and set the subsequent parts
# to 0.
before, middle, after = parts[:index], parts[index], parts[index + 1:]
middle += 1
return '.'.join(str(n) for n in before + [middle] + after)
def set_version(filename, version_number, pattern):
changed = []
def inject_version(match):
before, old, after = match.groups()
changed.append(True)
return before + version_number + after
with open(filename) as f:
contents = re.sub(r"^(\s*%s\s*=\s*')(.+?)(')(?sm)" % pattern,
inject_version, f.read())
if not changed:
fail('Could not find {} in {}'.format(pattern, filename))
with open(filename, 'w') as f:
f.write(contents)
def get_version(filename, pattern):
"""Gets the current version from the specified file.
This function assumes the file includes a string of the form::
<pattern> = <version>
"""
with open(filename) as f:
match = re.search(r"^(\s*%s\s*=\s*')(.+?)(')(?sm)" % pattern, f.read())
if match:
before, version, after = match.groups()
return version
fail('Could not find {} in {}'.format(pattern, filename))
def build_and_upload():
"""Uses Python's setup.py commands to build the package and upload it to
PyPI.
"""
Popen([sys.executable, 'setup.py', 'egg_info', 'sdist', 'upload',
'--sign']).wait()
#Popen([sys.executable, 'setup.py', 'publish']).wait()
def fail(message=None, exit_status=None):
"""Prints the specified message and exits the program with the specified
exit status.
"""
print('Error:', message, file=sys.stderr)
sys.exit(exit_status or 1)
def git_tags():
"""Returns a list of the git tags."""
process = Popen(['git', 'tag'], stdout=PIPE)
return set(process.communicate()[0].splitlines())
def git_is_clean():
"""Returns ``True`` if and only if there are no uncommitted changes."""
return Popen(['git', 'diff', '--quiet']).wait() == 0
def git_commit(message):
"""Commits all changed files with the specified message."""
Popen(['git', 'commit', '-am', message]).wait()
def git_tag(tag):
"""Tags the current version."""
print('Tagging "{}"'.format(tag))
msg = '"Released version {}"'.format(tag)
Popen(['git', 'tag', '-s', '-m', msg, tag]).wait()
def main():
# Determine the current version and compute the next development version.
init_filename = os.path.join(PACKAGE, '__init__.py')
version = get_version(init_filename, INIT_VERSION_STRING)
if version.endswith('-dev'):
version = version[:-4]
# Check if the current version has already been tagged, or if it it not
# ready to be published.
print('Releasing {}'.format(version))
if version in git_tags():
fail('Version {} is already tagged'.format(version))
if not git_is_clean():
fail('You have uncommitted changes in git')
# Set the version string in __init__ and setup.py to be current version.
set_version(init_filename, version, INIT_VERSION_STRING)
set_version('setup.py', version, SETUP_VERSION_STRING)
# Commit and tag the current version in git.
git_commit('Bump version number to {}'.format(version))
git_tag(version)
# Use Python's setup.py to build and upload the package to PyPI.
build_and_upload()
# Set the version string in __init__ and setup.py to be the next version.
which_part = sys.argv[1] if len(sys.argv) > 1 else None
dev_version = bump_version(version, which_part) + '-dev'
set_version(init_filename, dev_version, INIT_VERSION_STRING)
set_version('setup.py', dev_version, SETUP_VERSION_STRING)
#add_new_changelog_section(version, dev_version)
git_commit('Set development version number to {}'.format(dev_version))
if __name__ == '__main__':
main()