1"""
2Finalize the repo for a release. Invokes towncrier and bumpversion.
3"""
4
5__requires__ = ['bump2version', 'towncrier']
6
7
8import subprocess
9import pathlib
10import re
11import sys
12
13
14def release_kind():
15    """
16    Determine which release to make based on the files in the
17    changelog.
18    """
19    # use min here as 'major' < 'minor' < 'patch'
20    return min(
21        'major' if 'breaking' in file.name else
22        'minor' if 'change' in file.name else
23        'patch'
24        for file in pathlib.Path('changelog.d').iterdir()
25    )
26
27
28bump_version_command = [
29    sys.executable,
30    '-m', 'bumpversion',
31    release_kind(),
32]
33
34
35def get_version():
36    cmd = bump_version_command + ['--dry-run', '--verbose']
37    out = subprocess.check_output(cmd, text=True)
38    return re.search('^new_version=(.*)', out, re.MULTILINE).group(1)
39
40
41def update_changelog():
42    cmd = [
43        sys.executable, '-m',
44        'towncrier',
45        'build',
46        '--version', get_version(),
47        '--yes',
48    ]
49    subprocess.check_call(cmd)
50    _repair_changelog()
51
52
53def _repair_changelog():
54    """
55    Workaround for #2666
56    """
57    changelog_fn = pathlib.Path('CHANGES.rst')
58    changelog = changelog_fn.read_text()
59    fixed = re.sub(r'^(v[0-9.]+)v[0-9.]+$', r'\1', changelog, flags=re.M)
60    changelog_fn.write_text(fixed)
61    subprocess.check_output(['git', 'add', changelog_fn])
62
63
64def bump_version():
65    cmd = bump_version_command + ['--allow-dirty']
66    subprocess.check_call(cmd)
67
68
69def ensure_config():
70    """
71    Double-check that Git has an e-mail configured.
72    """
73    subprocess.check_output(['git', 'config', 'user.email'])
74
75
76def check_changes():
77    """
78    Verify that all of the files in changelog.d have the appropriate
79    names.
80    """
81    allowed = 'deprecation', 'breaking', 'change', 'doc', 'misc'
82    except_ = 'README.rst', '.gitignore'
83    news_fragments = (
84        file
85        for file in pathlib.Path('changelog.d').iterdir()
86        if file.name not in except_
87    )
88    unrecognized = [
89        str(file)
90        for file in news_fragments
91        if not any(f".{key}" in file.suffixes for key in allowed)
92    ]
93    if unrecognized:
94        raise ValueError(f"Some news fragments have invalid names: {unrecognized}")
95
96
97if __name__ == '__main__':
98    print("Cutting release at", get_version())
99    ensure_config()
100    check_changes()
101    update_changelog()
102    bump_version()
103