Skip to content

Commit 6065530

Browse files
committed
Merge branch 'main' into misc-news
2 parents de82a01 + 23b5f53 commit 6065530

30 files changed

Lines changed: 2056 additions & 1627 deletions

.ruff.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
target-version = "py39"
2+
3+
[format]
4+
preview = true
5+
quote-style = "single"
6+
docstring-code-format = true
7+
8+
[lint]
9+
preview = true
10+
select = [
11+
"I", # isort
12+
]
13+
ignore = [
14+
"E501", # Ignore line length errors (we use auto-formatting)
15+
]

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## 2.1.0
4+
5+
- Add the `-i` / `--issue` option to the 'blurb add' command.
6+
This lets you pre-fill the `gh-issue` field in the template.
7+
- Add the `-s` / `--section` option to the 'blurb add' command.
8+
This lets you pre-fill the `section` field in the template.
9+
310
## 2.0.0
411

512
* Move 'blurb test' subcommand into test suite by @hugovk in https://github.com/python/blurb/pull/37

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,25 @@ Here's how you interact with the file:
108108

109109
* Add the GitHub issue number for this commit to the
110110
end of the `.. gh-issue:` line.
111+
The issue can also be specified via the ``-i`` / ``--issue`` option:
112+
113+
```shell
114+
$ blurb add -i 109198
115+
# or equivalently
116+
$ blurb add -i https://github.com/python/cpython/issues/109198
117+
```
111118

112119
* Uncomment the line with the relevant `Misc/NEWS` section for this entry.
113120
For example, if this should go in the `Library` section, uncomment
114121
the line reading `#.. section: Library`. To uncomment, just delete
115122
the `#` at the front of the line.
123+
The section can also be specified via the ``-s`` / ``--section`` option:
124+
125+
```shell
126+
$ blurb add -s Library
127+
# or
128+
$ blurb add -s library
129+
```
116130

117131
* Finally, go to the end of the file, and enter your `NEWS` entry.
118132
This should be a single paragraph of English text using

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ optional-dependencies.tests = [
3939
urls.Changelog = "https://github.com/python/blurb/blob/main/CHANGELOG.md"
4040
urls.Homepage = "https://github.com/python/blurb"
4141
urls.Source = "https://github.com/python/blurb"
42-
scripts.blurb = "blurb.blurb:main"
42+
scripts.blurb = "blurb._cli:main"
4343

4444
[tool.hatch]
4545
version.source = "vcs"

src/blurb/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1+
"""Command-line tool to manage CPython Misc/NEWS.d entries."""
2+
13
from ._version import __version__

src/blurb/__main__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""Run blurb using ``python3 -m blurb``."""
2-
from blurb import blurb
32

3+
from __future__ import annotations
4+
5+
from blurb._cli import main
46

57
if __name__ == '__main__':
6-
blurb.main()
8+
main()

src/blurb/_add.py

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
from __future__ import annotations
2+
3+
import atexit
4+
import os
5+
import shlex
6+
import shutil
7+
import subprocess
8+
import sys
9+
import tempfile
10+
11+
from blurb._blurb_file import BlurbError, Blurbs
12+
from blurb._cli import error, prompt
13+
from blurb._git import flush_git_add_files, git_add_files
14+
from blurb._template import sections, template
15+
16+
TYPE_CHECKING = False
17+
if TYPE_CHECKING:
18+
from collections.abc import Sequence
19+
20+
if sys.platform == 'win32':
21+
FALLBACK_EDITORS = ('notepad.exe',)
22+
else:
23+
FALLBACK_EDITORS = ('/etc/alternatives/editor', 'nano')
24+
25+
26+
def add(*, issue: str | None = None, section: str | None = None):
27+
"""Add a blurb (a Misc/NEWS.d/next entry) to the current CPython repo.
28+
29+
Use -i/--issue to specify a GitHub issue number or link, e.g.:
30+
31+
blurb add -i 12345
32+
# or
33+
blurb add -i https://github.com/python/cpython/issues/12345
34+
35+
Use -s/--section to specify the section name (case-insensitive), e.g.:
36+
37+
blurb add -s Library
38+
# or
39+
blurb add -s library
40+
41+
The known sections names are defined as follows and
42+
spaces in names can be substituted for underscores:
43+
44+
{sections}
45+
""" # fmt: skip
46+
47+
handle, tmp_path = tempfile.mkstemp('.rst')
48+
os.close(handle)
49+
atexit.register(lambda: os.unlink(tmp_path))
50+
51+
text = _blurb_template_text(issue=issue, section=section)
52+
with open(tmp_path, 'w', encoding='utf-8') as file:
53+
file.write(text)
54+
55+
args = _editor_args()
56+
args.append(tmp_path)
57+
58+
while True:
59+
blurb = _add_blurb_from_template(args, tmp_path)
60+
if blurb is None:
61+
try:
62+
prompt('Hit return to retry (or Ctrl-C to abort)')
63+
except KeyboardInterrupt:
64+
print()
65+
return
66+
print()
67+
continue
68+
break
69+
70+
path = blurb.save_next()
71+
git_add_files.append(path)
72+
flush_git_add_files()
73+
print('Ready for commit.')
74+
75+
76+
add.__doc__ = add.__doc__.format(sections='\n'.join(f'* {s}' for s in sections))
77+
78+
79+
def _editor_args() -> list[str]:
80+
editor = _find_editor()
81+
82+
# We need to be clever about EDITOR.
83+
# On the one hand, it might be a legitimate path to an
84+
# executable containing spaces.
85+
# On the other hand, it might be a partial command-line
86+
# with options.
87+
if shutil.which(editor):
88+
args = [editor]
89+
else:
90+
args = list(shlex.split(editor))
91+
if not shutil.which(args[0]):
92+
raise SystemExit(f'Invalid GIT_EDITOR / EDITOR value: {editor}')
93+
return args
94+
95+
96+
def _find_editor() -> str:
97+
for var in 'GIT_EDITOR', 'EDITOR':
98+
editor = os.environ.get(var)
99+
if editor is not None:
100+
return editor
101+
for fallback in FALLBACK_EDITORS:
102+
if os.path.isabs(fallback):
103+
found_path = fallback
104+
else:
105+
found_path = shutil.which(fallback)
106+
if found_path and os.path.exists(found_path):
107+
return found_path
108+
error('Could not find an editor! Set the EDITOR environment variable.')
109+
110+
111+
def _blurb_template_text(*, issue: str | None, section: str | None) -> str:
112+
issue_number = _extract_issue_number(issue)
113+
section_name = _extract_section_name(section)
114+
115+
text = template
116+
117+
# Ensure that there is a trailing space after '.. gh-issue:' to make
118+
# filling in the template easier, unless an issue number was given
119+
# through the --issue command-line flag.
120+
issue_line = '.. gh-issue:'
121+
without_space = f'\n{issue_line}\n'
122+
if without_space not in text:
123+
raise SystemExit("Can't find gh-issue line in the template!")
124+
if issue_number is None:
125+
with_space = f'\n{issue_line} \n'
126+
text = text.replace(without_space, with_space)
127+
else:
128+
with_issue_number = f'\n{issue_line} {issue_number}\n'
129+
text = text.replace(without_space, with_issue_number)
130+
131+
# Uncomment the section if needed.
132+
if section_name is not None:
133+
pattern = f'.. section: {section_name}'
134+
text = text.replace(f'#{pattern}', pattern)
135+
136+
return text
137+
138+
139+
def _extract_issue_number(issue: str | None, /) -> int | None:
140+
if issue is None:
141+
return None
142+
issue = issue.strip()
143+
144+
if issue.startswith(('GH-', 'gh-')):
145+
stripped = issue[3:]
146+
else:
147+
stripped = issue.removeprefix('#')
148+
try:
149+
if stripped.isdecimal():
150+
return int(stripped)
151+
except ValueError:
152+
pass
153+
154+
# Allow GitHub URL with or without the scheme
155+
stripped = issue.removeprefix('https://')
156+
stripped = stripped.removeprefix('github.com/python/cpython/issues/')
157+
try:
158+
if stripped.isdecimal():
159+
return int(stripped)
160+
except ValueError:
161+
pass
162+
163+
raise SystemExit(f'Invalid GitHub issue number: {issue}')
164+
165+
166+
def _extract_section_name(section: str | None, /) -> str | None:
167+
if section is None:
168+
return None
169+
170+
section = section.strip()
171+
if not section:
172+
raise SystemExit('Empty section name!')
173+
174+
matches = []
175+
# Try an exact or lowercase match
176+
for section_name in sections:
177+
if section in {section_name, section_name.lower()}:
178+
matches.append(section_name)
179+
180+
if not matches:
181+
section_list = '\n'.join(f'* {s}' for s in sections)
182+
raise SystemExit(
183+
f'Invalid section name: {section!r}\n\nValid names are:\n\n{section_list}'
184+
)
185+
186+
if len(matches) > 1:
187+
multiple_matches = ', '.join(f'* {m}' for m in sorted(matches))
188+
raise SystemExit(f'More than one match for {section!r}:\n\n{multiple_matches}')
189+
190+
return matches[0]
191+
192+
193+
def _add_blurb_from_template(args: Sequence[str], tmp_path: str) -> Blurbs | None:
194+
subprocess.run(args)
195+
196+
failure = ''
197+
blurb = Blurbs()
198+
try:
199+
blurb.load(tmp_path)
200+
except BlurbError as e:
201+
failure = str(e)
202+
203+
if not failure:
204+
assert len(blurb) # if parse_blurb succeeds, we should always have a body
205+
if len(blurb) > 1:
206+
failure = "Too many entries! Don't specify '..' on a line by itself."
207+
208+
if failure:
209+
print()
210+
print(f'Error: {failure}')
211+
print()
212+
return None
213+
return blurb

0 commit comments

Comments
 (0)