Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 2.1.0

- Add the `-i` / `--issue` option to the 'blurb add' command.
This lets you pre-fill the `gh-issue` field in the template.

## 2.0.0

* Move 'blurb test' subcommand into test suite by @hugovk in https://github.com/python/blurb/pull/37
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,13 @@ Here's how you interact with the file:

* Add the GitHub issue number for this commit to the
end of the `.. gh-issue:` line.
The issue can also be specified via the ``-i`` / ``--issue`` option:

```shell
$ blurb add -i 109198
# or equivalently
$ blurb add -i https://github.com/python/cpython/issues/109198
```

* Uncomment the line with the relevant `Misc/NEWS` section for this entry.
For example, if this should go in the `Library` section, uncomment
Expand Down
85 changes: 75 additions & 10 deletions src/blurb/blurb.py
Original file line number Diff line number Diff line change
Expand Up @@ -766,7 +766,14 @@ def help(subcommand=None):
for name, p in inspect.signature(fn).parameters.items():
if p.kind == inspect.Parameter.KEYWORD_ONLY:
short_option = name[0]
options.append(f" [-{short_option}|--{name}]")
if isinstance(p.default, bool):
options.append(f" [-{short_option}|--{name}]")
else:
if p.default is None:
metavar = f'{name.upper()}'
else:
metavar = f'{name.upper()}[={p.default}]'
options.append(f" [-{short_option}|--{name} {metavar}]")
elif p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
positionals.append(" ")
has_default = (p.default != inspect._empty)
Expand Down Expand Up @@ -817,25 +824,65 @@ def find_editor():
error('Could not find an editor! Set the EDITOR environment variable.')


def _template_text_for_temp_file():
def _extract_issue_number(issue, /):
if issue is None:
return None
issue = issue.strip()

if issue.startswith(('GH-', 'gh-')):
stripped = issue[3:]
else:
stripped = issue.removeprefix('#')
try:
if stripped.isdecimal():
return int(stripped)
except ValueError:
pass

# Allow GitHub URL with or without the scheme
stripped = issue.removeprefix('https://')
stripped = stripped.removeprefix('github.com/python/cpython/issues/')
try:
if stripped.isdecimal():
return int(stripped)
except ValueError:
pass

sys.exit(f"Invalid GitHub issue number: {issue}")


def _blurb_template_text(*, issue):
issue_number = _extract_issue_number(issue)

text = template

# Ensure that there is a trailing space after '.. gh-issue:' to make
# filling in the template easier.
# filling in the template easier, unless an issue number was given
# through the --issue command-line flag.
issue_line = ".. gh-issue:"
without_space = "\n" + issue_line + "\n"
with_space = "\n" + issue_line + " \n"
if without_space not in text:
sys.exit("Can't find gh-issue line to ensure there's a space on the end!")
text = text.replace(without_space, with_space)
sys.exit("Can't find gh-issue line in the template!")
if issue_number is None:
with_space = "\n" + issue_line + " \n"
text = text.replace(without_space, with_space)
else:
with_issue_number = f"\n{issue_line} {issue_number}\n"
text = text.replace(without_space, with_issue_number)

return text


@subcommand
def add():
def add(*, issue=None):
"""
Add a blurb (a Misc/NEWS.d/next entry) to the current CPython repo.

Use -i/--issue to specify a GitHub issue number or link, e.g.:

blurb add -i 12345
# or
blurb add -i https://github.com/python/cpython/issues/12345
"""

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

text = _template_text_for_temp_file()
text = _blurb_template_text(issue=issue)
with open(tmp_path, "w", encoding="utf-8") as file:
file.write(text)

Expand Down Expand Up @@ -1169,22 +1216,37 @@ def main():
kwargs = {}
for name, p in inspect.signature(fn).parameters.items():
if p.kind == inspect.Parameter.KEYWORD_ONLY:
assert isinstance(p.default, bool), "blurb command-line processing only handles boolean options"
if (p.default is not None
and not isinstance(p.default, (bool, str))):
sys.exit("blurb command-line processing cannot handle "
f"options of type {type(p.default).__qualname__}")

kwargs[name] = p.default
short_options[name[0]] = name
long_options[name] = name

filtered_args = []
done_with_options = False
consume_after = None

def handle_option(s, dict):
nonlocal consume_after
name = dict.get(s, None)
if not name:
sys.exit(f'blurb: Unknown option for {subcommand}: "{s}"')
kwargs[name] = not kwargs[name]

value = kwargs[name]
if isinstance(value, bool):
kwargs[name] = not value
else:
consume_after = name

# print(f"short_options {short_options} long_options {long_options}")
for a in args:
if consume_after:
kwargs[consume_after] = a
consume_after = None
continue
if done_with_options:
filtered_args.append(a)
continue
Expand All @@ -1199,6 +1261,9 @@ def handle_option(s, dict):
continue
filtered_args.append(a)

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

sys.exit(fn(*filtered_args, **kwargs))
except TypeError as e:
Expand Down
87 changes: 87 additions & 0 deletions tests/test_blurb_add.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import re

import pytest

from blurb import blurb


def test_valid_no_issue_number():
assert blurb._extract_issue_number(None) is None
res = blurb._blurb_template_text(issue=None)
lines = frozenset(res.splitlines())
assert '.. gh-issue:' not in lines
assert '.. gh-issue: ' in lines


@pytest.mark.parametrize('issue', (
# issue given by their number
'12345',
' 12345 ',
# issue given by their number and a 'GH-' prefix
'GH-12345',
' GH-12345 ',
# issue given by their number and a 'gh-' prefix
'gh-12345',
' gh-12345 ',
# issue given by their number and a '#' prefix
'#12345',
' #12345 ',
# issue given by their URL (no scheme)
'github.com/python/cpython/issues/12345',
' github.com/python/cpython/issues/12345 ',
# issue given by their URL (with scheme)
'https://github.com/python/cpython/issues/12345',
' https://github.com/python/cpython/issues/12345 ',
))
def test_valid_issue_number_12345(issue):
actual = blurb._extract_issue_number(issue)
assert actual == 12345

res = blurb._blurb_template_text(issue=issue)
lines = frozenset(res.splitlines())
assert '.. gh-issue:' not in lines
assert '.. gh-issue: ' not in lines
assert '.. gh-issue: 12345' in lines


@pytest.mark.parametrize('issue', (
'',
'abc',
'Gh-123',
'gh-abc',
'gh- 123',
'gh -123',
'gh-',
'bpo-',
'bpo-12345',
'github.com/python/cpython/issues',
'github.com/python/cpython/issues/',
'github.com/python/cpython/issues/abc',
'github.com/python/cpython/issues/gh-abc',
'github.com/python/cpython/issues/gh-123',
'github.com/python/cpython/issues/1234?param=1',
'https://github.com/python/cpython/issues',
'https://github.com/python/cpython/issues/',
'https://github.com/python/cpython/issues/abc',
'https://github.com/python/cpython/issues/gh-abc',
'https://github.com/python/cpython/issues/gh-123',
'https://github.com/python/cpython/issues/1234?param=1',
))
def test_invalid_issue_number(issue):
error_message = re.escape(f'Invalid GitHub issue number: {issue}')
with pytest.raises(SystemExit, match=error_message):
blurb._blurb_template_text(issue=issue)


@pytest.mark.parametrize('invalid', (
'gh-issue: ',
'gh-issue: 1',
'gh-issue',
))
def test_malformed_gh_issue_line(invalid, monkeypatch):
template = blurb.template.replace('.. gh-issue:', invalid)
error_message = re.escape("Can't find gh-issue line in the template!")
with monkeypatch.context() as cm:
cm.setattr(blurb, 'template', template)
with pytest.raises(SystemExit, match=error_message):
blurb._blurb_template_text(issue='1234')