Skip to content

Commit 589bfba

Browse files
AA-Turnerpicnixzhugovk
authored
Add an -i / --issue option to blurb add (#48)
Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
1 parent 1fbf587 commit 589bfba

File tree

4 files changed

+174
-10
lines changed

4 files changed

+174
-10
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
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+
38
## 2.0.0
49

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

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,13 @@ 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

src/blurb/blurb.py

Lines changed: 75 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -766,7 +766,14 @@ def help(subcommand=None):
766766
for name, p in inspect.signature(fn).parameters.items():
767767
if p.kind == inspect.Parameter.KEYWORD_ONLY:
768768
short_option = name[0]
769-
options.append(f" [-{short_option}|--{name}]")
769+
if isinstance(p.default, bool):
770+
options.append(f" [-{short_option}|--{name}]")
771+
else:
772+
if p.default is None:
773+
metavar = f'{name.upper()}'
774+
else:
775+
metavar = f'{name.upper()}[={p.default}]'
776+
options.append(f" [-{short_option}|--{name} {metavar}]")
770777
elif p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
771778
positionals.append(" ")
772779
has_default = (p.default != inspect._empty)
@@ -817,25 +824,65 @@ def find_editor():
817824
error('Could not find an editor! Set the EDITOR environment variable.')
818825

819826

820-
def _template_text_for_temp_file():
827+
def _extract_issue_number(issue, /):
828+
if issue is None:
829+
return None
830+
issue = issue.strip()
831+
832+
if issue.startswith(('GH-', 'gh-')):
833+
stripped = issue[3:]
834+
else:
835+
stripped = issue.removeprefix('#')
836+
try:
837+
if stripped.isdecimal():
838+
return int(stripped)
839+
except ValueError:
840+
pass
841+
842+
# Allow GitHub URL with or without the scheme
843+
stripped = issue.removeprefix('https://')
844+
stripped = stripped.removeprefix('github.com/python/cpython/issues/')
845+
try:
846+
if stripped.isdecimal():
847+
return int(stripped)
848+
except ValueError:
849+
pass
850+
851+
sys.exit(f"Invalid GitHub issue number: {issue}")
852+
853+
854+
def _blurb_template_text(*, issue):
855+
issue_number = _extract_issue_number(issue)
856+
821857
text = template
822858

823859
# Ensure that there is a trailing space after '.. gh-issue:' to make
824-
# filling in the template easier.
860+
# filling in the template easier, unless an issue number was given
861+
# through the --issue command-line flag.
825862
issue_line = ".. gh-issue:"
826863
without_space = "\n" + issue_line + "\n"
827-
with_space = "\n" + issue_line + " \n"
828864
if without_space not in text:
829-
sys.exit("Can't find gh-issue line to ensure there's a space on the end!")
830-
text = text.replace(without_space, with_space)
865+
sys.exit("Can't find gh-issue line in the template!")
866+
if issue_number is None:
867+
with_space = "\n" + issue_line + " \n"
868+
text = text.replace(without_space, with_space)
869+
else:
870+
with_issue_number = f"\n{issue_line} {issue_number}\n"
871+
text = text.replace(without_space, with_issue_number)
831872

832873
return text
833874

834875

835876
@subcommand
836-
def add():
877+
def add(*, issue=None):
837878
"""
838879
Add a blurb (a Misc/NEWS.d/next entry) to the current CPython repo.
880+
881+
Use -i/--issue to specify a GitHub issue number or link, e.g.:
882+
883+
blurb add -i 12345
884+
# or
885+
blurb add -i https://github.com/python/cpython/issues/12345
839886
"""
840887

841888
editor = find_editor()
@@ -844,7 +891,7 @@ def add():
844891
os.close(handle)
845892
atexit.register(lambda : os.unlink(tmp_path))
846893

847-
text = _template_text_for_temp_file()
894+
text = _blurb_template_text(issue=issue)
848895
with open(tmp_path, "w", encoding="utf-8") as file:
849896
file.write(text)
850897

@@ -1169,22 +1216,37 @@ def main():
11691216
kwargs = {}
11701217
for name, p in inspect.signature(fn).parameters.items():
11711218
if p.kind == inspect.Parameter.KEYWORD_ONLY:
1172-
assert isinstance(p.default, bool), "blurb command-line processing only handles boolean options"
1219+
if (p.default is not None
1220+
and not isinstance(p.default, (bool, str))):
1221+
sys.exit("blurb command-line processing cannot handle "
1222+
f"options of type {type(p.default).__qualname__}")
1223+
11731224
kwargs[name] = p.default
11741225
short_options[name[0]] = name
11751226
long_options[name] = name
11761227

11771228
filtered_args = []
11781229
done_with_options = False
1230+
consume_after = None
11791231

11801232
def handle_option(s, dict):
1233+
nonlocal consume_after
11811234
name = dict.get(s, None)
11821235
if not name:
11831236
sys.exit(f'blurb: Unknown option for {subcommand}: "{s}"')
1184-
kwargs[name] = not kwargs[name]
1237+
1238+
value = kwargs[name]
1239+
if isinstance(value, bool):
1240+
kwargs[name] = not value
1241+
else:
1242+
consume_after = name
11851243

11861244
# print(f"short_options {short_options} long_options {long_options}")
11871245
for a in args:
1246+
if consume_after:
1247+
kwargs[consume_after] = a
1248+
consume_after = None
1249+
continue
11881250
if done_with_options:
11891251
filtered_args.append(a)
11901252
continue
@@ -1199,6 +1261,9 @@ def handle_option(s, dict):
11991261
continue
12001262
filtered_args.append(a)
12011263

1264+
if consume_after:
1265+
sys.exit(f"Error: blurb: {subcommand} {consume_after} "
1266+
f"must be followed by an option argument")
12021267

12031268
sys.exit(fn(*filtered_args, **kwargs))
12041269
except TypeError as e:

tests/test_blurb_add.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import re
2+
3+
import pytest
4+
5+
from blurb import blurb
6+
7+
8+
def test_valid_no_issue_number():
9+
assert blurb._extract_issue_number(None) is None
10+
res = blurb._blurb_template_text(issue=None)
11+
lines = frozenset(res.splitlines())
12+
assert '.. gh-issue:' not in lines
13+
assert '.. gh-issue: ' in lines
14+
15+
16+
@pytest.mark.parametrize('issue', (
17+
# issue given by their number
18+
'12345',
19+
' 12345 ',
20+
# issue given by their number and a 'GH-' prefix
21+
'GH-12345',
22+
' GH-12345 ',
23+
# issue given by their number and a 'gh-' prefix
24+
'gh-12345',
25+
' gh-12345 ',
26+
# issue given by their number and a '#' prefix
27+
'#12345',
28+
' #12345 ',
29+
# issue given by their URL (no scheme)
30+
'github.com/python/cpython/issues/12345',
31+
' github.com/python/cpython/issues/12345 ',
32+
# issue given by their URL (with scheme)
33+
'https://github.com/python/cpython/issues/12345',
34+
' https://github.com/python/cpython/issues/12345 ',
35+
))
36+
def test_valid_issue_number_12345(issue):
37+
actual = blurb._extract_issue_number(issue)
38+
assert actual == 12345
39+
40+
res = blurb._blurb_template_text(issue=issue)
41+
lines = frozenset(res.splitlines())
42+
assert '.. gh-issue:' not in lines
43+
assert '.. gh-issue: ' not in lines
44+
assert '.. gh-issue: 12345' in lines
45+
46+
47+
@pytest.mark.parametrize('issue', (
48+
'',
49+
'abc',
50+
'Gh-123',
51+
'gh-abc',
52+
'gh- 123',
53+
'gh -123',
54+
'gh-',
55+
'bpo-',
56+
'bpo-12345',
57+
'github.com/python/cpython/issues',
58+
'github.com/python/cpython/issues/',
59+
'github.com/python/cpython/issues/abc',
60+
'github.com/python/cpython/issues/gh-abc',
61+
'github.com/python/cpython/issues/gh-123',
62+
'github.com/python/cpython/issues/1234?param=1',
63+
'https://github.com/python/cpython/issues',
64+
'https://github.com/python/cpython/issues/',
65+
'https://github.com/python/cpython/issues/abc',
66+
'https://github.com/python/cpython/issues/gh-abc',
67+
'https://github.com/python/cpython/issues/gh-123',
68+
'https://github.com/python/cpython/issues/1234?param=1',
69+
))
70+
def test_invalid_issue_number(issue):
71+
error_message = re.escape(f'Invalid GitHub issue number: {issue}')
72+
with pytest.raises(SystemExit, match=error_message):
73+
blurb._blurb_template_text(issue=issue)
74+
75+
76+
@pytest.mark.parametrize('invalid', (
77+
'gh-issue: ',
78+
'gh-issue: 1',
79+
'gh-issue',
80+
))
81+
def test_malformed_gh_issue_line(invalid, monkeypatch):
82+
template = blurb.template.replace('.. gh-issue:', invalid)
83+
error_message = re.escape("Can't find gh-issue line in the template!")
84+
with monkeypatch.context() as cm:
85+
cm.setattr(blurb, 'template', template)
86+
with pytest.raises(SystemExit, match=error_message):
87+
blurb._blurb_template_text(issue='1234')

0 commit comments

Comments
 (0)