summaryrefslogtreecommitdiffstats
path: root/utils/bump_version.py
diff options
context:
space:
mode:
Diffstat (limited to 'utils/bump_version.py')
-rwxr-xr-xutils/bump_version.py183
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()