Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d409907

Browse files
committed
Update bump_version.py
1 parent 55eddad commit d409907

2 files changed

Lines changed: 107 additions & 103 deletions

File tree

utils/CHANGES_template.rst

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
Release x.y.z (in development)
2-
==============================
3-
41
Dependencies
52
------------
63

utils/bump_version.py

Lines changed: 107 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -3,121 +3,125 @@
33
from __future__ import annotations
44

55
import argparse
6+
import dataclasses
67
import re
78
import sys
89
import time
910
from contextlib import contextmanager
1011
from pathlib import Path
11-
from typing import TYPE_CHECKING
12+
from typing import TYPE_CHECKING, Literal
1213

1314
if TYPE_CHECKING:
1415
from collections.abc import Iterator, Sequence
15-
from typing import TypeAlias
1616

1717
script_dir = Path(__file__).parent
1818
package_dir = script_dir.parent
1919

20-
RELEASE_TYPE = {'a': 'alpha', 'b': 'beta'}
2120

22-
VersionInfo: TypeAlias = tuple[int, int, int, str, int]
21+
@dataclasses.dataclass(frozen=True, slots=True)
22+
class VersionInfo:
23+
major: int
24+
minor: int
25+
micro: int
26+
level: Literal['a', 'b', 'rc', 'final']
27+
serial: int
28+
29+
@property
30+
def releaselevel(self) -> Literal['alpha', 'beta', 'candidate', 'final']:
31+
if self.level == 'final':
32+
return 'final'
33+
if self.level == 'a':
34+
return 'alpha'
35+
if self.level == 'b':
36+
return 'beta'
37+
if self.level == 'rc':
38+
return 'candidate'
39+
msg = f'Unknown release level: {self.level}'
40+
raise RuntimeError(msg)
41+
42+
@property
43+
def is_final(self) -> bool:
44+
return self.level == 'final'
45+
46+
@property
47+
def version(self) -> str:
48+
return f'{self.major}.{self.minor}.{self.micro}'
49+
50+
@property
51+
def release(self) -> str:
52+
return f'{self.major}.{self.minor}.{self.micro}{self.level}{self.serial}'
53+
54+
@property
55+
def version_tuple(self) -> tuple[int, int, int]:
56+
return self.major, self.minor, self.micro
57+
58+
@property
59+
def release_tuple(self) -> tuple[int, int, int, str, int]:
60+
return self.major, self.minor, self.micro, self.releaselevel, self.serial
2361

2462

25-
def stringify_version(version_info: VersionInfo, in_develop: bool = True) -> str:
26-
version = '.'.join(str(v) for v in version_info[:3])
27-
if not in_develop and version_info[3] != 'final':
28-
version += version_info[3][0] + str(version_info[4])
63+
def parse_version(version: str) -> VersionInfo:
64+
# Final version:
65+
# - "X.Y.Z" -> (X, Y, Z, 'final', 0)
66+
# - "X.Y" -> (X, Y, 0, 'final', 0) [shortcut]
67+
if matched := re.fullmatch(r'(\d+)\.(\d+)(?:\.(\d+))?', version):
68+
major, minor, micro = matched.groups(default='0')
69+
return VersionInfo(int(major), int(minor), int(micro), 'final', 0)
70+
71+
# Pre-release versions:
72+
# - "X.Y.ZaN" -> (X, Y, Z, 'alpha', N)
73+
# - "X.Y.ZbN" -> (X, Y, Z, 'beta', N)
74+
# - "X.Y.ZrcN" -> (X, Y, Z, 'candidate', N)
75+
if matched := re.fullmatch(r'(\d+)\.(\d+)\.(\d+)(a|b|rc)(\d+)', version):
76+
major, minor, micro, level, serial = matched.groups()
77+
return VersionInfo(int(major), int(minor), int(micro), level, int(serial)) # type: ignore[arg-type]
2978

30-
return version
79+
msg = f'Unknown version: {version}'
80+
raise RuntimeError(msg)
3181

3282

3383
def bump_version(path: Path, version_info: VersionInfo, in_develop: bool = True) -> None:
34-
version = stringify_version(version_info, in_develop)
84+
if in_develop or version_info.is_final:
85+
version = version_info.version
86+
else:
87+
version = version_info.release
3588

3689
with open(path, encoding='utf-8') as f:
37-
lines = f.read().splitlines()
90+
lines = f.read().splitlines(keepends=True)
3891

3992
for i, line in enumerate(lines):
4093
if line.startswith('__version__ = '):
41-
lines[i] = f"__version__ = '{version}'"
94+
lines[i] = f"__version__ = '{version}'\n"
4295
continue
4396
if line.startswith('version_info = '):
44-
lines[i] = f'version_info = {version_info}'
97+
lines[i] = f'version_info = {version_info.release_tuple}\n'
4598
continue
4699
if line.startswith('_in_development = '):
47-
lines[i] = f'_in_development = {in_develop}'
100+
lines[i] = f'_in_development = {in_develop}\n'
48101
continue
49102

50103
with open(path, 'w', encoding='utf-8') as f:
51-
f.write('\n'.join(lines) + '\n')
52-
53-
54-
def parse_version(version: str) -> VersionInfo:
55-
matched = re.search(r'^(\d+)\.(\d+)$', version)
56-
if matched:
57-
major, minor = matched.groups()
58-
return (int(major), int(minor), 0, 'final', 0)
59-
60-
matched = re.search(r'^(\d+)\.(\d+)\.(\d+)$', version)
61-
if matched:
62-
major, minor, rev = matched.groups()
63-
return (int(major), int(minor), int(rev), 'final', 0)
64-
65-
matched = re.search(r'^(\d+)\.(\d+)\s*(a|b|alpha|beta)(\d+)$', version)
66-
if matched:
67-
major, minor, typ, relver = matched.groups()
68-
release = RELEASE_TYPE.get(typ, typ)
69-
return (int(major), int(minor), 0, release, int(relver))
70-
71-
matched = re.search(r'^(\d+)\.(\d+)\.(\d+)\s*(a|b|alpha|beta)(\d+)$', version)
72-
if matched:
73-
major, minor, rev, typ, relver = matched.groups()
74-
release = RELEASE_TYPE.get(typ, typ)
75-
return (int(major), int(minor), int(rev), release, int(relver))
76-
77-
raise RuntimeError('Unknown version: %s' % version)
78-
79-
80-
class Skip(Exception):
81-
pass
82-
83-
84-
@contextmanager
85-
def processing(message: str) -> Iterator[None]:
86-
try:
87-
print(message + ' ... ', end='')
88-
yield
89-
except Skip as exc:
90-
print('skip: %s' % exc)
91-
except Exception:
92-
print('error')
93-
raise
94-
else:
95-
print('done')
104+
f.writelines(lines)
96105

97106

98107
class Changes:
99108
def __init__(self, path: Path) -> None:
100109
self.path = path
101-
self.fetch_version()
102110

103-
def fetch_version(self) -> None:
104111
with open(self.path, encoding='utf-8') as f:
105112
version = f.readline().strip()
106-
matched = re.search(r'^Release (.*) \((.*)\)$', version)
113+
matched = re.fullmatch(r'Release (.*) \((.*)\)', version)
107114
if matched is None:
108-
raise RuntimeError('Unknown CHANGES format: %s' % version)
115+
msg = f'Unknown CHANGES format: {version}'
116+
raise RuntimeError(msg)
109117

110-
self.version, self.release_date = matched.groups()
111-
self.version_info = parse_version(self.version)
112-
if self.release_date == 'in development':
113-
self.in_development = True
114-
else:
115-
self.in_development = False
118+
self.version, release_date = matched.groups()
119+
self.in_development = release_date == 'in development'
120+
self.version_tuple = parse_version(self.version).version_tuple
116121

117-
def finalize_release_date(self) -> None:
122+
def finalise_release_date(self) -> None:
118123
release_date = time.strftime('%b %d, %Y')
119124
heading = f'Release {self.version} (released {release_date})'
120-
121125
with open(self.path, 'r+', encoding='utf-8') as f:
122126
f.readline() # skip first two lines
123127
f.readline()
@@ -130,21 +134,8 @@ def finalize_release_date(self) -> None:
130134
f.write(self.filter_empty_sections(body))
131135

132136
def add_release(self, version_info: VersionInfo) -> None:
133-
if version_info[-2:] in (('beta', 0), ('final', 0)):
134-
version = stringify_version(version_info)
135-
else:
136-
reltype = version_info[3]
137-
version = (
138-
f'{stringify_version(version_info)} '
139-
f'{RELEASE_TYPE.get(reltype, reltype)}{version_info[4] or ""}'
140-
)
141-
heading = 'Release %s (in development)' % version
142-
143-
with open(script_dir / 'CHANGES_template.rst', encoding='utf-8') as f:
144-
f.readline() # skip first two lines
145-
f.readline()
146-
tmpl = f.read()
147-
137+
heading = f'Release {version_info.version} (in development)'
138+
tmpl = (script_dir / 'CHANGES_template.rst').read_text(encoding='utf-8')
148139
with open(self.path, 'r+', encoding='utf-8') as f:
149140
body = f.read()
150141

@@ -156,39 +147,55 @@ def add_release(self, version_info: VersionInfo) -> None:
156147
f.write('\n')
157148
f.write(body)
158149

159-
def filter_empty_sections(self, body: str) -> str:
150+
@staticmethod
151+
def filter_empty_sections(body: str) -> str:
160152
return re.sub('^\n.+\n-{3,}\n+(?=\n.+\n[-=]{3,}\n)', '', body, flags=re.MULTILINE)
161153

162154

163-
def parse_options(argv: Sequence[str]) -> argparse.Namespace:
155+
class Skip(Exception):
156+
pass
157+
158+
159+
@contextmanager
160+
def processing(message: str) -> Iterator[None]:
161+
try:
162+
print(message + ' ... ', end='')
163+
yield
164+
except Skip as exc:
165+
print(f'skip: {exc}')
166+
except Exception:
167+
print('error')
168+
raise
169+
else:
170+
print('done')
171+
172+
173+
def parse_options(argv: Sequence[str]) -> tuple[VersionInfo, bool]:
164174
parser = argparse.ArgumentParser()
165-
parser.add_argument('version', help='A version number (cf. 1.6b0)')
175+
parser.add_argument('version', help='A version number (cf. 1.6.0b0)')
166176
parser.add_argument('--in-develop', action='store_true')
167177
options = parser.parse_args(argv)
168-
options.version = parse_version(options.version)
169-
return options
178+
return parse_version(options.version), options.in_develop
170179

171180

172181
def main() -> None:
173-
options = parse_options(sys.argv[1:])
182+
version, in_develop = parse_options(sys.argv[1:])
174183

175184
with processing('Rewriting sphinx/__init__.py'):
176-
bump_version(
177-
package_dir / 'sphinx' / '__init__.py', options.version, options.in_develop
178-
)
185+
bump_version(package_dir / 'sphinx' / '__init__.py', version, in_develop)
179186

180187
with processing('Rewriting CHANGES'):
181188
changes = Changes(package_dir / 'CHANGES.rst')
182-
if changes.version_info == options.version:
183-
if changes.in_development:
184-
changes.finalize_release_date()
189+
if changes.version_tuple == version.version_tuple:
190+
if changes.in_development and version.is_final and not in_develop:
191+
changes.finalise_release_date()
185192
else:
186193
reason = 'version not changed'
187194
raise Skip(reason)
188195
else:
189196
if changes.in_development:
190-
print('WARNING: last version is not released yet: %s' % changes.version)
191-
changes.add_release(options.version)
197+
print(f'WARNING: last version is not released yet: {changes.version}')
198+
changes.add_release(version)
192199

193200

194201
if __name__ == '__main__':

0 commit comments

Comments
 (0)