mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Update `bump_version.py
`
This commit is contained in:
parent
55eddad705
commit
d409907d9e
@ -1,6 +1,3 @@
|
|||||||
Release x.y.z (in development)
|
|
||||||
==============================
|
|
||||||
|
|
||||||
Dependencies
|
Dependencies
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
@ -3,121 +3,125 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import dataclasses
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Literal
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Iterator, Sequence
|
from collections.abc import Iterator, Sequence
|
||||||
from typing import TypeAlias
|
|
||||||
|
|
||||||
script_dir = Path(__file__).parent
|
script_dir = Path(__file__).parent
|
||||||
package_dir = script_dir.parent
|
package_dir = script_dir.parent
|
||||||
|
|
||||||
RELEASE_TYPE = {'a': 'alpha', 'b': 'beta'}
|
|
||||||
|
|
||||||
VersionInfo: TypeAlias = tuple[int, int, int, str, int]
|
@dataclasses.dataclass(frozen=True, slots=True)
|
||||||
|
class VersionInfo:
|
||||||
|
major: int
|
||||||
|
minor: int
|
||||||
|
micro: int
|
||||||
|
level: Literal['a', 'b', 'rc', 'final']
|
||||||
|
serial: int
|
||||||
|
|
||||||
|
@property
|
||||||
|
def releaselevel(self) -> Literal['alpha', 'beta', 'candidate', 'final']:
|
||||||
|
if self.level == 'final':
|
||||||
|
return 'final'
|
||||||
|
if self.level == 'a':
|
||||||
|
return 'alpha'
|
||||||
|
if self.level == 'b':
|
||||||
|
return 'beta'
|
||||||
|
if self.level == 'rc':
|
||||||
|
return 'candidate'
|
||||||
|
msg = f'Unknown release level: {self.level}'
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
def stringify_version(version_info: VersionInfo, in_develop: bool = True) -> str:
|
@property
|
||||||
version = '.'.join(str(v) for v in version_info[:3])
|
def is_final(self) -> bool:
|
||||||
if not in_develop and version_info[3] != 'final':
|
return self.level == 'final'
|
||||||
version += version_info[3][0] + str(version_info[4])
|
|
||||||
|
|
||||||
return version
|
@property
|
||||||
|
def version(self) -> str:
|
||||||
|
return f'{self.major}.{self.minor}.{self.micro}'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def release(self) -> str:
|
||||||
|
return f'{self.major}.{self.minor}.{self.micro}{self.level}{self.serial}'
|
||||||
|
|
||||||
def bump_version(path: Path, version_info: VersionInfo, in_develop: bool = True) -> None:
|
@property
|
||||||
version = stringify_version(version_info, in_develop)
|
def version_tuple(self) -> tuple[int, int, int]:
|
||||||
|
return self.major, self.minor, self.micro
|
||||||
|
|
||||||
with open(path, encoding='utf-8') as f:
|
@property
|
||||||
lines = f.read().splitlines()
|
def release_tuple(self) -> tuple[int, int, int, str, int]:
|
||||||
|
return self.major, self.minor, self.micro, self.releaselevel, self.serial
|
||||||
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: str) -> VersionInfo:
|
def parse_version(version: str) -> VersionInfo:
|
||||||
matched = re.search(r'^(\d+)\.(\d+)$', version)
|
# Final version:
|
||||||
if matched:
|
# - "X.Y.Z" -> (X, Y, Z, 'final', 0)
|
||||||
major, minor = matched.groups()
|
# - "X.Y" -> (X, Y, 0, 'final', 0) [shortcut]
|
||||||
return (int(major), int(minor), 0, 'final', 0)
|
if matched := re.fullmatch(r'(\d+)\.(\d+)(?:\.(\d+))?', version):
|
||||||
|
major, minor, micro = matched.groups(default='0')
|
||||||
|
return VersionInfo(int(major), int(minor), int(micro), 'final', 0)
|
||||||
|
|
||||||
matched = re.search(r'^(\d+)\.(\d+)\.(\d+)$', version)
|
# Pre-release versions:
|
||||||
if matched:
|
# - "X.Y.ZaN" -> (X, Y, Z, 'alpha', N)
|
||||||
major, minor, rev = matched.groups()
|
# - "X.Y.ZbN" -> (X, Y, Z, 'beta', N)
|
||||||
return (int(major), int(minor), int(rev), 'final', 0)
|
# - "X.Y.ZrcN" -> (X, Y, Z, 'candidate', N)
|
||||||
|
if matched := re.fullmatch(r'(\d+)\.(\d+)\.(\d+)(a|b|rc)(\d+)', version):
|
||||||
|
major, minor, micro, level, serial = matched.groups()
|
||||||
|
return VersionInfo(int(major), int(minor), int(micro), level, int(serial)) # type: ignore[arg-type]
|
||||||
|
|
||||||
matched = re.search(r'^(\d+)\.(\d+)\s*(a|b|alpha|beta)(\d+)$', version)
|
msg = f'Unknown version: {version}'
|
||||||
if matched:
|
raise RuntimeError(msg)
|
||||||
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):
|
def bump_version(path: Path, version_info: VersionInfo, in_develop: bool = True) -> None:
|
||||||
pass
|
if in_develop or version_info.is_final:
|
||||||
|
version = version_info.version
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def processing(message: str) -> Iterator[None]:
|
|
||||||
try:
|
|
||||||
print(message + ' ... ', end='')
|
|
||||||
yield
|
|
||||||
except Skip as exc:
|
|
||||||
print('skip: %s' % exc)
|
|
||||||
except Exception:
|
|
||||||
print('error')
|
|
||||||
raise
|
|
||||||
else:
|
else:
|
||||||
print('done')
|
version = version_info.release
|
||||||
|
|
||||||
|
with open(path, encoding='utf-8') as f:
|
||||||
|
lines = f.read().splitlines(keepends=True)
|
||||||
|
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if line.startswith('__version__ = '):
|
||||||
|
lines[i] = f"__version__ = '{version}'\n"
|
||||||
|
continue
|
||||||
|
if line.startswith('version_info = '):
|
||||||
|
lines[i] = f'version_info = {version_info.release_tuple}\n'
|
||||||
|
continue
|
||||||
|
if line.startswith('_in_development = '):
|
||||||
|
lines[i] = f'_in_development = {in_develop}\n'
|
||||||
|
continue
|
||||||
|
|
||||||
|
with open(path, 'w', encoding='utf-8') as f:
|
||||||
|
f.writelines(lines)
|
||||||
|
|
||||||
|
|
||||||
class Changes:
|
class Changes:
|
||||||
def __init__(self, path: Path) -> None:
|
def __init__(self, path: Path) -> None:
|
||||||
self.path = path
|
self.path = path
|
||||||
self.fetch_version()
|
|
||||||
|
|
||||||
def fetch_version(self) -> None:
|
|
||||||
with open(self.path, encoding='utf-8') as f:
|
with open(self.path, encoding='utf-8') as f:
|
||||||
version = f.readline().strip()
|
version = f.readline().strip()
|
||||||
matched = re.search(r'^Release (.*) \((.*)\)$', version)
|
matched = re.fullmatch(r'Release (.*) \((.*)\)', version)
|
||||||
if matched is None:
|
if matched is None:
|
||||||
raise RuntimeError('Unknown CHANGES format: %s' % version)
|
msg = f'Unknown CHANGES format: {version}'
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
self.version, self.release_date = matched.groups()
|
self.version, release_date = matched.groups()
|
||||||
self.version_info = parse_version(self.version)
|
self.in_development = release_date == 'in development'
|
||||||
if self.release_date == 'in development':
|
self.version_tuple = parse_version(self.version).version_tuple
|
||||||
self.in_development = True
|
|
||||||
else:
|
|
||||||
self.in_development = False
|
|
||||||
|
|
||||||
def finalize_release_date(self) -> None:
|
def finalise_release_date(self) -> None:
|
||||||
release_date = time.strftime('%b %d, %Y')
|
release_date = time.strftime('%b %d, %Y')
|
||||||
heading = f'Release {self.version} (released {release_date})'
|
heading = f'Release {self.version} (released {release_date})'
|
||||||
|
|
||||||
with open(self.path, 'r+', encoding='utf-8') as f:
|
with open(self.path, 'r+', encoding='utf-8') as f:
|
||||||
f.readline() # skip first two lines
|
f.readline() # skip first two lines
|
||||||
f.readline()
|
f.readline()
|
||||||
@ -130,21 +134,8 @@ class Changes:
|
|||||||
f.write(self.filter_empty_sections(body))
|
f.write(self.filter_empty_sections(body))
|
||||||
|
|
||||||
def add_release(self, version_info: VersionInfo) -> None:
|
def add_release(self, version_info: VersionInfo) -> None:
|
||||||
if version_info[-2:] in (('beta', 0), ('final', 0)):
|
heading = f'Release {version_info.version} (in development)'
|
||||||
version = stringify_version(version_info)
|
tmpl = (script_dir / 'CHANGES_template.rst').read_text(encoding='utf-8')
|
||||||
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(script_dir / 'CHANGES_template.rst', 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:
|
with open(self.path, 'r+', encoding='utf-8') as f:
|
||||||
body = f.read()
|
body = f.read()
|
||||||
|
|
||||||
@ -156,39 +147,55 @@ class Changes:
|
|||||||
f.write('\n')
|
f.write('\n')
|
||||||
f.write(body)
|
f.write(body)
|
||||||
|
|
||||||
def filter_empty_sections(self, body: str) -> str:
|
@staticmethod
|
||||||
|
def filter_empty_sections(body: str) -> str:
|
||||||
return re.sub('^\n.+\n-{3,}\n+(?=\n.+\n[-=]{3,}\n)', '', body, flags=re.MULTILINE)
|
return re.sub('^\n.+\n-{3,}\n+(?=\n.+\n[-=]{3,}\n)', '', body, flags=re.MULTILINE)
|
||||||
|
|
||||||
|
|
||||||
def parse_options(argv: Sequence[str]) -> argparse.Namespace:
|
class Skip(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def processing(message: str) -> Iterator[None]:
|
||||||
|
try:
|
||||||
|
print(message + ' ... ', end='')
|
||||||
|
yield
|
||||||
|
except Skip as exc:
|
||||||
|
print(f'skip: {exc}')
|
||||||
|
except Exception:
|
||||||
|
print('error')
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
print('done')
|
||||||
|
|
||||||
|
|
||||||
|
def parse_options(argv: Sequence[str]) -> tuple[VersionInfo, bool]:
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('version', help='A version number (cf. 1.6b0)')
|
parser.add_argument('version', help='A version number (cf. 1.6.0b0)')
|
||||||
parser.add_argument('--in-develop', action='store_true')
|
parser.add_argument('--in-develop', action='store_true')
|
||||||
options = parser.parse_args(argv)
|
options = parser.parse_args(argv)
|
||||||
options.version = parse_version(options.version)
|
return parse_version(options.version), options.in_develop
|
||||||
return options
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
options = parse_options(sys.argv[1:])
|
version, in_develop = parse_options(sys.argv[1:])
|
||||||
|
|
||||||
with processing('Rewriting sphinx/__init__.py'):
|
with processing('Rewriting sphinx/__init__.py'):
|
||||||
bump_version(
|
bump_version(package_dir / 'sphinx' / '__init__.py', version, in_develop)
|
||||||
package_dir / 'sphinx' / '__init__.py', options.version, options.in_develop
|
|
||||||
)
|
|
||||||
|
|
||||||
with processing('Rewriting CHANGES'):
|
with processing('Rewriting CHANGES'):
|
||||||
changes = Changes(package_dir / 'CHANGES.rst')
|
changes = Changes(package_dir / 'CHANGES.rst')
|
||||||
if changes.version_info == options.version:
|
if changes.version_tuple == version.version_tuple:
|
||||||
if changes.in_development:
|
if changes.in_development and version.is_final and not in_develop:
|
||||||
changes.finalize_release_date()
|
changes.finalise_release_date()
|
||||||
else:
|
else:
|
||||||
reason = 'version not changed'
|
reason = 'version not changed'
|
||||||
raise Skip(reason)
|
raise Skip(reason)
|
||||||
else:
|
else:
|
||||||
if changes.in_development:
|
if changes.in_development:
|
||||||
print('WARNING: last version is not released yet: %s' % changes.version)
|
print(f'WARNING: last version is not released yet: {changes.version}')
|
||||||
changes.add_release(options.version)
|
changes.add_release(version)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
Loading…
Reference in New Issue
Block a user