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

Skip to content

Commit de10fe8

Browse files
authored
Move 'blurb add' to blurb._add (#52)
1 parent 9c9ccc6 commit de10fe8

File tree

4 files changed

+238
-205
lines changed

4 files changed

+238
-205
lines changed

src/blurb/_add.py

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
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._cli import subcommand,error,prompt
12+
from blurb._template import sections, template
13+
from blurb.blurb import Blurbs, BlurbError, flush_git_add_files, git_add_files
14+
15+
TYPE_CHECKING = False
16+
if TYPE_CHECKING:
17+
from collections.abc import Sequence
18+
19+
if sys.platform == 'win32':
20+
FALLBACK_EDITORS = ('notepad.exe',)
21+
else:
22+
FALLBACK_EDITORS = ('/etc/alternatives/editor', 'nano')
23+
24+
25+
@subcommand
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+
"""
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+
add.__doc__ = add.__doc__.format(sections='\n'.join(f'* {s}' for s in sections))
75+
76+
77+
def _editor_args() -> list[str]:
78+
editor = _find_editor()
79+
80+
# We need to be clever about EDITOR.
81+
# On the one hand, it might be a legitimate path to an
82+
# executable containing spaces.
83+
# On the other hand, it might be a partial command-line
84+
# with options.
85+
if shutil.which(editor):
86+
args = [editor]
87+
else:
88+
args = list(shlex.split(editor))
89+
if not shutil.which(args[0]):
90+
raise SystemExit(f'Invalid GIT_EDITOR / EDITOR value: {editor}')
91+
return args
92+
93+
94+
def _find_editor() -> str:
95+
for var in 'GIT_EDITOR', 'EDITOR':
96+
editor = os.environ.get(var)
97+
if editor is not None:
98+
return editor
99+
for fallback in FALLBACK_EDITORS:
100+
if os.path.isabs(fallback):
101+
found_path = fallback
102+
else:
103+
found_path = shutil.which(fallback)
104+
if found_path and os.path.exists(found_path):
105+
return found_path
106+
error('Could not find an editor! Set the EDITOR environment variable.')
107+
108+
109+
def _blurb_template_text(*, issue: str | None, section: str | None) -> str:
110+
issue_number = _extract_issue_number(issue)
111+
section_name = _extract_section_name(section)
112+
113+
text = template
114+
115+
# Ensure that there is a trailing space after '.. gh-issue:' to make
116+
# filling in the template easier, unless an issue number was given
117+
# through the --issue command-line flag.
118+
issue_line = '.. gh-issue:'
119+
without_space = f'\n{issue_line}\n'
120+
if without_space not in text:
121+
raise SystemExit("Can't find gh-issue line in the template!")
122+
if issue_number is None:
123+
with_space = f'\n{issue_line} \n'
124+
text = text.replace(without_space, with_space)
125+
else:
126+
with_issue_number = f'\n{issue_line} {issue_number}\n'
127+
text = text.replace(without_space, with_issue_number)
128+
129+
# Uncomment the section if needed.
130+
if section_name is not None:
131+
pattern = f'.. section: {section_name}'
132+
text = text.replace(f'#{pattern}', pattern)
133+
134+
return text
135+
136+
137+
def _extract_issue_number(issue: str | None, /) -> int | None:
138+
if issue is None:
139+
return None
140+
issue = issue.strip()
141+
142+
if issue.startswith(('GH-', 'gh-')):
143+
stripped = issue[3:]
144+
else:
145+
stripped = issue.removeprefix('#')
146+
try:
147+
if stripped.isdecimal():
148+
return int(stripped)
149+
except ValueError:
150+
pass
151+
152+
# Allow GitHub URL with or without the scheme
153+
stripped = issue.removeprefix('https://')
154+
stripped = stripped.removeprefix('github.com/python/cpython/issues/')
155+
try:
156+
if stripped.isdecimal():
157+
return int(stripped)
158+
except ValueError:
159+
pass
160+
161+
raise SystemExit(f'Invalid GitHub issue number: {issue}')
162+
163+
164+
def _extract_section_name(section: str | None, /) -> str | None:
165+
if section is None:
166+
return None
167+
168+
section = section.strip()
169+
if not section:
170+
raise SystemExit('Empty section name!')
171+
172+
matches = []
173+
# Try an exact or lowercase match
174+
for section_name in sections:
175+
if section in {section_name, section_name.lower()}:
176+
matches.append(section_name)
177+
178+
if not matches:
179+
section_list = '\n'.join(f'* {s}' for s in sections)
180+
raise SystemExit(f'Invalid section name: {section!r}\n\n'
181+
f'Valid names are:\n\n{section_list}')
182+
183+
if len(matches) > 1:
184+
multiple_matches = ', '.join(f'* {m}' for m in sorted(matches))
185+
raise SystemExit(f'More than one match for {section!r}:\n\n'
186+
f'{multiple_matches}')
187+
188+
return matches[0]
189+
190+
191+
def _add_blurb_from_template(args: Sequence[str], tmp_path: str) -> Blurbs | None:
192+
subprocess.run(args)
193+
194+
failure = ''
195+
blurb = Blurbs()
196+
try:
197+
blurb.load(tmp_path)
198+
except BlurbError as e:
199+
failure = str(e)
200+
201+
if not failure:
202+
assert len(blurb) # if parse_blurb succeeds, we should always have a body
203+
if len(blurb) > 1:
204+
failure = "Too many entries! Don't specify '..' on a line by itself."
205+
206+
if failure:
207+
print()
208+
print(f'Error: {failure}')
209+
print()
210+
return None
211+
return blurb

src/blurb/_cli.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
TYPE_CHECKING = False
1111
if TYPE_CHECKING:
1212
from collections.abc import Callable
13-
from typing import TypeAlias
13+
from typing import NoReturn, TypeAlias
1414

1515
CommandFunc: TypeAlias = Callable[..., None]
1616

@@ -19,10 +19,14 @@
1919
readme_re = re.compile(r'This is \w+ version \d+\.\d+').match
2020

2121

22-
def error(msg: str, /):
22+
def error(msg: str, /) -> NoReturn:
2323
raise SystemExit(f'Error: {msg}')
2424

2525

26+
def prompt(prompt: str, /) -> str:
27+
return input(f'[{prompt}> ')
28+
29+
2630
def subcommand(fn: CommandFunc):
2731
global subcommands
2832
subcommands[fn.__name__] = fn

0 commit comments

Comments
 (0)