From 0ea760ca8de96246c21369cde53f6976a23d0cd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 25 Jun 2024 13:53:14 +0200 Subject: [PATCH 1/4] Parse str-flag options in ``blurb`` Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Co-authored-by: Adam Turner <9087854+aa-turner@users.noreply.github.com> --- src/blurb/blurb.py | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/blurb/blurb.py b/src/blurb/blurb.py index 7be4474..dda6088 100755 --- a/src/blurb/blurb.py +++ b/src/blurb/blurb.py @@ -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) @@ -1169,22 +1176,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 @@ -1199,6 +1221,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: From f3c94f0184afd8092fe5ef0885c8ecb458520692 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Mon, 11 Aug 2025 21:09:43 +0100 Subject: [PATCH 2/4] Add ```-i/--issue`` option to ``blurb add`` --- CHANGELOG.md | 5 +++ README.md | 7 ++++ src/blurb/blurb.py | 52 ++++++++++++++++++++---- tests/test_blurb_add.py | 87 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 144 insertions(+), 7 deletions(-) create mode 100644 tests/test_blurb_add.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b810a9..680ad31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index c5e6abc..f415378 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/blurb/blurb.py b/src/blurb/blurb.py index dda6088..24275af 100755 --- a/src/blurb/blurb.py +++ b/src/blurb/blurb.py @@ -824,25 +824,63 @@ 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: + 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: + 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() @@ -851,7 +889,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) diff --git a/tests/test_blurb_add.py b/tests/test_blurb_add.py new file mode 100644 index 0000000..1433b7e --- /dev/null +++ b/tests/test_blurb_add.py @@ -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: {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') From e71c5a212b9d5d148aa9aacbb7edb3b44983710f Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Mon, 11 Aug 2025 22:09:25 +0100 Subject: [PATCH 3/4] fixup! Add ```-i/--issue`` option to ``blurb add`` --- tests/test_blurb_add.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_blurb_add.py b/tests/test_blurb_add.py index 1433b7e..814d7b2 100644 --- a/tests/test_blurb_add.py +++ b/tests/test_blurb_add.py @@ -68,7 +68,7 @@ def test_valid_issue_number_12345(issue): 'https://github.com/python/cpython/issues/1234?param=1', )) def test_invalid_issue_number(issue): - error_message = re.escape(f'Invalid GitHub issue: {issue}') + error_message = re.escape(f'Invalid GitHub issue number: {issue}') with pytest.raises(SystemExit, match=error_message): blurb._blurb_template_text(issue=issue) From 47a3f4cccd60293f4ed1a34f5327fb532cd5ead4 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Mon, 11 Aug 2025 22:12:22 +0100 Subject: [PATCH 4/4] fixup! Add ```-i/--issue`` option to ``blurb add`` --- src/blurb/blurb.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/blurb/blurb.py b/src/blurb/blurb.py index 24275af..5e5eb75 100755 --- a/src/blurb/blurb.py +++ b/src/blurb/blurb.py @@ -834,7 +834,8 @@ def _extract_issue_number(issue, /): else: stripped = issue.removeprefix('#') try: - return int(stripped) + if stripped.isdecimal(): + return int(stripped) except ValueError: pass @@ -842,7 +843,8 @@ def _extract_issue_number(issue, /): stripped = issue.removeprefix('https://') stripped = stripped.removeprefix('github.com/python/cpython/issues/') try: - return int(stripped) + if stripped.isdecimal(): + return int(stripped) except ValueError: pass