Skip to content

Commit c9662f0

Browse files
committed
Move 'blurb merge' to blurb._merge
1 parent 5dd62ee commit c9662f0

File tree

4 files changed

+224
-227
lines changed

4 files changed

+224
-227
lines changed

src/blurb/_cli.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ def prompt(prompt: str, /) -> str:
2727
return input(f'[{prompt}> ')
2828

2929

30+
def require_ok(prompt: str, /) -> str:
31+
prompt = f"[{prompt}> "
32+
while True:
33+
s = input(prompt).strip()
34+
if s == 'ok':
35+
return s
36+
37+
3038
def subcommand(fn: CommandFunc):
3139
global subcommands
3240
subcommands[fn.__name__] = fn
@@ -138,7 +146,6 @@ def _blurb_help() -> None:
138146

139147

140148
def main() -> None:
141-
global original_dir
142149

143150
args = sys.argv[1:]
144151

@@ -157,8 +164,9 @@ def main() -> None:
157164
if fn in (help, version):
158165
raise SystemExit(fn(*args))
159166

167+
import blurb._merge
168+
blurb._merge.original_dir = os.getcwd()
160169
try:
161-
original_dir = os.getcwd()
162170
chdir_to_repo_root()
163171

164172
# map keyword arguments to options

src/blurb/_merge.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import glob
2+
import os
3+
import sys
4+
from pathlib import Path
5+
6+
from blurb._cli import require_ok, subcommand
7+
from blurb._template import (
8+
next_filename_unsanitize_sections, sanitize_section,
9+
sanitize_section_legacy, sections,
10+
)
11+
from blurb._versions import glob_versions, printable_version
12+
from blurb.blurb import Blurbs, textwrap_body
13+
14+
original_dir: str = os.getcwd()
15+
16+
17+
@subcommand
18+
def merge(output: str | None = None, *, forced: bool = False) -> None:
19+
"""Merge all blurbs together into a single Misc/NEWS file.
20+
21+
Optional output argument specifies where to write to.
22+
Default is <cpython-root>/Misc/NEWS.
23+
24+
If overwriting, blurb merge will prompt you to make sure it's okay.
25+
To force it to overwrite, use -f.
26+
"""
27+
if output:
28+
output = os.path.join(original_dir, output)
29+
else:
30+
output = 'Misc/NEWS'
31+
32+
versions = glob_versions()
33+
if not versions:
34+
sys.exit("You literally don't have ANY blurbs to merge together!")
35+
36+
if os.path.exists(output) and not forced:
37+
print(f'You already have a {output!r} file.')
38+
require_ok('Type ok to overwrite')
39+
40+
write_news(output, versions=versions)
41+
42+
43+
def write_news(output: str, *, versions: list[str]) -> None:
44+
buff = []
45+
46+
def prnt(msg: str = '', /):
47+
buff.append(msg)
48+
49+
prnt("""
50+
+++++++++++
51+
Python News
52+
+++++++++++
53+
54+
""".strip())
55+
56+
for version in versions:
57+
filenames = glob_blurbs(version)
58+
59+
blurbs = Blurbs()
60+
if version == 'next':
61+
for filename in filenames:
62+
if os.path.basename(filename) == 'README.rst':
63+
continue
64+
blurbs.load_next(filename)
65+
if not blurbs:
66+
continue
67+
metadata = blurbs[0][0]
68+
metadata['release date'] = 'XXXX-XX-XX'
69+
else:
70+
assert len(filenames) == 1
71+
blurbs.load(filenames[0])
72+
73+
header = f"What's New in Python {printable_version(version)}?"
74+
prnt()
75+
prnt(header)
76+
prnt('=' * len(header))
77+
prnt()
78+
79+
metadata, body = blurbs[0]
80+
release_date = metadata['release date']
81+
82+
prnt(f'*Release date: {release_date}*')
83+
prnt()
84+
85+
if 'no changes' in metadata:
86+
prnt(body)
87+
prnt()
88+
continue
89+
90+
last_section = None
91+
for metadata, body in blurbs:
92+
section = metadata['section']
93+
if last_section != section:
94+
last_section = section
95+
prnt(section)
96+
prnt('-' * len(section))
97+
prnt()
98+
if metadata.get('gh-issue'):
99+
issue_number = metadata['gh-issue']
100+
if int(issue_number):
101+
body = f'gh-{issue_number}: {body}'
102+
elif metadata.get('bpo'):
103+
issue_number = metadata['bpo']
104+
if int(issue_number):
105+
body = f'bpo-{issue_number}: {body}'
106+
107+
body = f'- {body}'
108+
text = textwrap_body(body, subsequent_indent=' ')
109+
prnt(text)
110+
prnt()
111+
prnt('**(For information about older versions, consult the HISTORY file.)**')
112+
113+
new_contents = '\n'.join(buff)
114+
115+
# Only write in `output` if the contents are different
116+
# This speeds up subsequent Sphinx builds
117+
try:
118+
previous_contents = Path(output).read_text(encoding='utf-8')
119+
except (FileNotFoundError, UnicodeError):
120+
previous_contents = None
121+
if new_contents != previous_contents:
122+
Path(output).write_text(new_contents, encoding='utf-8')
123+
else:
124+
print(output, 'is already up to date')
125+
126+
127+
def glob_blurbs(version: str) -> list[str]:
128+
filenames = []
129+
base = os.path.join('Misc', 'NEWS.d', version)
130+
if version != 'next':
131+
wildcard = f'{base}.rst'
132+
filenames.extend(glob.glob(wildcard))
133+
else:
134+
sanitized_sections = (
135+
{sanitize_section(section) for section in sections} |
136+
{sanitize_section_legacy(section) for section in sections}
137+
)
138+
for section in sanitized_sections:
139+
wildcard = os.path.join(base, section, '*.rst')
140+
entries = glob.glob(wildcard)
141+
deletables = [x for x in entries if x.endswith('/README.rst')]
142+
for filename in deletables:
143+
entries.remove(filename)
144+
filenames.extend(entries)
145+
filenames.sort(reverse=True, key=next_filename_unsanitize_sections)
146+
return filenames

src/blurb/_versions.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import glob
2+
import sys
3+
4+
if sys.version_info[:2] >= (3, 11):
5+
from contextlib import chdir
6+
else:
7+
import os
8+
9+
class chdir:
10+
def __init__(self, path: str, /) -> None:
11+
self.path = path
12+
13+
def __enter__(self) -> None:
14+
self.previous_cwd = os.getcwd()
15+
os.chdir(self.path)
16+
17+
def __exit__(self, *args) -> None:
18+
os.chdir(self.previous_cwd)
19+
20+
21+
def glob_versions() -> list[str]:
22+
versions = []
23+
with chdir('Misc/NEWS.d'):
24+
for wildcard in ('2.*.rst', '3.*.rst', 'next'):
25+
versions += [x.partition('.rst')[0] for x in glob.glob(wildcard)]
26+
versions.sort(key=version_key, reverse=True)
27+
return versions
28+
29+
30+
def version_key(element: str, /) -> str:
31+
fields = list(element.split('.'))
32+
if len(fields) == 1:
33+
return element
34+
35+
# in sorted order,
36+
# 3.5.0a1 < 3.5.0b1 < 3.5.0rc1 < 3.5.0
37+
# so for sorting purposes we transform
38+
# "3.5." and "3.5.0" into "3.5.0zz0"
39+
last = fields.pop()
40+
for s in ('a', 'b', 'rc'):
41+
if s in last:
42+
last, stage, stage_version = last.partition(s)
43+
break
44+
else:
45+
stage = 'zz'
46+
stage_version = '0'
47+
48+
fields.append(last)
49+
while len(fields) < 3:
50+
fields.append('0')
51+
52+
fields.extend([stage, stage_version])
53+
fields = [s.rjust(6, '0') for s in fields]
54+
55+
return '.'.join(fields)
56+
57+
58+
def printable_version(version: str, /) -> str:
59+
if version == 'next':
60+
return version
61+
if 'a' in version:
62+
return version.replace('a', ' alpha ')
63+
if 'b' in version:
64+
return version.replace('b', ' beta ')
65+
if 'rc' in version:
66+
return version.replace('rc', ' release candidate ')
67+
return version + ' final'

0 commit comments

Comments
 (0)