diff --git a/extra/release.py b/extra/release.py new file mode 100644 index 000000000..b1c7daa44 --- /dev/null +++ b/extra/release.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +"""A utility script for automating the beets release process. +""" +import click +import os +import re + +BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +@click.group() +def release(): + pass + + +# Locations (filenames and patterns) of the version number. +VERSION_LOCS = [ + ( + os.path.join(BASE, 'beets', '__init__.py'), + [ + ( + r'__version__\s*=\s*[\'"]([0-9\.]+)[\'"]', + "__version__ = '{version}'", + ) + ] + ), + ( + os.path.join(BASE, 'docs', 'conf.py'), + [ + ( + r'version\s*=\s*[\'"]([0-9\.]+)[\'"]', + "version = '{minor}'", + ), + ( + r'release\s*=\s*[\'"]([0-9\.]+)[\'"]', + "release = '{version}'", + ), + ] + ), + ( + os.path.join(BASE, 'setup.py'), + [ + ( + r'version\s*=\s*[\'"]([0-9\.]+)[\'"]', + " version='{minor}',", + ) + ] + ), +] + +@release.command() +@click.argument('version') +def bump(version): + """Update the version number in setup.py, docs config, changelog, + and root module. + """ + version_parts = [int(p) for p in version.split('.')] + assert len(version_parts) == 3, "invalid version number" + minor = '{}.{}'.format(*version_parts) + major = '{}'.format(*version_parts) + + # Replace the version each place where it lives. + for filename, locations in VERSION_LOCS: + # Read and transform the file. + out_lines = [] + with open(filename) as f: + for line in f: + for pattern, template in locations: + match = re.match(pattern, line) + if match: + # Check that this version is actually newer. + old_version = match.group(1) + old_parts = [int(p) for p in old_version.split('.')] + assert version_parts > old_parts, \ + "version must be newer than {}".format(old_version) + + # Insert the new version. + out_lines.append(template.format( + version=version, + major=major, + minor=minor, + ) + '\n') + + break + + else: + # Normal line. + out_lines.append(line) + + # Write the file back. + with open(filename, 'w') as f: + f.write(''.join(out_lines)) + + # Generate bits to insert into changelog. + header_line = '{} (in development)'.format(version) + header = '\n\n' + header_line + '\n' + '-' * len(header_line) + '\n\n' + header += 'Changelog goes here!\n' + + # Insert into the right place. + changelog = os.path.join(BASE, 'docs', 'changelog.rst') + with open(changelog) as f: + contents = f.read() + location = contents.find('\n\n') # First blank line. + contents = contents[:location] + header + contents[location:] + + # Write back. + with open(changelog, 'w') as f: + f.write(contents) + + +if __name__ == '__main__': + release()