diff options
Diffstat (limited to '')
-rwxr-xr-x | utils/bump_version.py | 183 |
1 files changed, 183 insertions, 0 deletions
diff --git a/utils/bump_version.py b/utils/bump_version.py new file mode 100755 index 0000000..6e50755 --- /dev/null +++ b/utils/bump_version.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 + +import argparse +import os +import re +import sys +import time +from contextlib import contextmanager + +script_dir = os.path.dirname(__file__) +package_dir = os.path.abspath(os.path.join(script_dir, '..')) + +RELEASE_TYPE = {'a': 'alpha', 'b': 'beta'} + + +def stringify_version(version_info, in_develop=True): + version = '.'.join(str(v) for v in version_info[:3]) + if not in_develop and version_info[3] != 'final': + version += version_info[3][0] + str(version_info[4]) + + return version + + +def bump_version(path, version_info, in_develop=True): + version = stringify_version(version_info, in_develop) + + with open(path, encoding='utf-8') as f: + lines = f.read().splitlines() + + for i, line in enumerate(lines): + if line.startswith('__version__ = '): + lines[i] = f"__version__ = '{version}'" + continue + if line.startswith('version_info = '): + lines[i] = f'version_info = {version_info}' + continue + if line.startswith('_in_development = '): + lines[i] = f'_in_development = {in_develop}' + continue + + with open(path, 'w', encoding='utf-8') as f: + f.write('\n'.join(lines) + '\n') + + +def parse_version(version): + matched = re.search(r'^(\d+)\.(\d+)$', version) + if matched: + major, minor = matched.groups() + return (int(major), int(minor), 0, 'final', 0) + + matched = re.search(r'^(\d+)\.(\d+)\.(\d+)$', version) + if matched: + major, minor, rev = matched.groups() + return (int(major), int(minor), int(rev), 'final', 0) + + matched = re.search(r'^(\d+)\.(\d+)\s*(a|b|alpha|beta)(\d+)$', version) + if matched: + major, minor, typ, relver = matched.groups() + release = RELEASE_TYPE.get(typ, typ) + return (int(major), int(minor), 0, release, int(relver)) + + matched = re.search(r'^(\d+)\.(\d+)\.(\d+)\s*(a|b|alpha|beta)(\d+)$', version) + if matched: + major, minor, rev, typ, relver = matched.groups() + release = RELEASE_TYPE.get(typ, typ) + return (int(major), int(minor), int(rev), release, int(relver)) + + raise RuntimeError('Unknown version: %s' % version) + + +class Skip(Exception): + pass + + +@contextmanager +def processing(message): + try: + print(message + ' ... ', end='') + yield + except Skip as exc: + print('skip: %s' % exc) + except Exception: + print('error') + raise + else: + print('done') + + +class Changes: + def __init__(self, path): + self.path = path + self.fetch_version() + + def fetch_version(self): + with open(self.path, encoding='utf-8') as f: + version = f.readline().strip() + matched = re.search(r'^Release (.*) \((.*)\)$', version) + if matched is None: + raise RuntimeError('Unknown CHANGES format: %s' % version) + + self.version, self.release_date = matched.groups() + self.version_info = parse_version(self.version) + if self.release_date == 'in development': + self.in_development = True + else: + self.in_development = False + + def finalize_release_date(self): + release_date = time.strftime('%b %d, %Y') + heading = f'Release {self.version} (released {release_date})' + + with open(self.path, 'r+', encoding='utf-8') as f: + f.readline() # skip first two lines + f.readline() + body = f.read() + + f.seek(0) + f.truncate(0) + f.write(heading + '\n') + f.write('=' * len(heading) + '\n') + f.write(self.filter_empty_sections(body)) + + def add_release(self, version_info): + if version_info[-2:] in (('beta', 0), ('final', 0)): + version = stringify_version(version_info) + else: + reltype = version_info[3] + version = (f'{stringify_version(version_info)} ' + f'{RELEASE_TYPE.get(reltype, reltype)}{version_info[4] or ""}') + heading = 'Release %s (in development)' % version + + with open(os.path.join(script_dir, 'CHANGES_template'), encoding='utf-8') as f: + f.readline() # skip first two lines + f.readline() + tmpl = f.read() + + with open(self.path, 'r+', encoding='utf-8') as f: + body = f.read() + + f.seek(0) + f.truncate(0) + f.write(heading + '\n') + f.write('=' * len(heading) + '\n') + f.write(tmpl) + f.write('\n') + f.write(body) + + def filter_empty_sections(self, body): + return re.sub('^\n.+\n-{3,}\n+(?=\n.+\n[-=]{3,}\n)', '', body, flags=re.M) + + +def parse_options(argv): + parser = argparse.ArgumentParser() + parser.add_argument('version', help='A version number (cf. 1.6b0)') + parser.add_argument('--in-develop', action='store_true') + options = parser.parse_args(argv) + options.version = parse_version(options.version) + return options + + +def main(): + options = parse_options(sys.argv[1:]) + + with processing("Rewriting sphinx/__init__.py"): + bump_version(os.path.join(package_dir, 'sphinx/__init__.py'), + options.version, options.in_develop) + + with processing('Rewriting CHANGES'): + changes = Changes(os.path.join(package_dir, 'CHANGES')) + if changes.version_info == options.version: + if changes.in_development: + changes.finalize_release_date() + else: + reason = 'version not changed' + raise Skip(reason) + else: + if changes.in_development: + print('WARNING: last version is not released yet: %s' % changes.version) + changes.add_release(options.version) + + +if __name__ == '__main__': + main() |