From cdc62a4acb360d100325090abb524b5ee7eb6a1b 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] Add a ``-s`` / ```--section`` option to ``blurb add`` Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- CHANGELOG.md | 2 + README.md | 7 ++++ src/blurb/blurb.py | 52 +++++++++++++++++++++++-- tests/test_blurb_add.py | 84 +++++++++++++++++++++++++++++++++++++++-- 4 files changed, 137 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 680ad31..04d4ee9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - Add the `-i` / `--issue` option to the 'blurb add' command. This lets you pre-fill the `gh-issue` field in the template. +- Add the `-s` / `--section` option to the 'blurb add' command. + This lets you pre-fill the `section` field in the template. ## 2.0.0 diff --git a/README.md b/README.md index f415378..ff23efc 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,13 @@ Here's how you interact with the file: For example, if this should go in the `Library` section, uncomment the line reading `#.. section: Library`. To uncomment, just delete the `#` at the front of the line. + The section can also be specified via the ``-s`` / ``--section`` option: + + ```shell + $ blurb add -s Library + # or + $ blurb add -s library + ``` * Finally, go to the end of the file, and enter your `NEWS` entry. This should be a single paragraph of English text using diff --git a/src/blurb/blurb.py b/src/blurb/blurb.py index 5e5eb75..3758af2 100755 --- a/src/blurb/blurb.py +++ b/src/blurb/blurb.py @@ -851,8 +851,36 @@ def _extract_issue_number(issue, /): sys.exit(f"Invalid GitHub issue number: {issue}") -def _blurb_template_text(*, issue): +def _extract_section_name(section, /): + if section is None: + return None + + section = section.strip() + if not section: + sys.exit("Empty section name!") + + matches = [] + # Try an exact or lowercase match + for section_name in sections: + if section in {section_name, section_name.lower()}: + matches.append(section_name) + + if not matches: + section_list = '\n'.join(f'* {s}' for s in sections) + sys.exit(f"Invalid section name: {section!r}\n\n" + f"Valid names are:\n\n{section_list}") + + if len(matches) > 1: + multiple_matches = ', '.join(f'* {m}' for m in sorted(matches)) + sys.exit(f"More than one match for {section!r}:\n\n" + f"{multiple_matches}") + + return matches[0] + + +def _blurb_template_text(*, issue, section): issue_number = _extract_issue_number(issue) + section_name = _extract_section_name(section) text = template @@ -870,11 +898,16 @@ def _blurb_template_text(*, issue): with_issue_number = f"\n{issue_line} {issue_number}\n" text = text.replace(without_space, with_issue_number) + # Uncomment the section if needed. + if section_name is not None: + pattern = f'.. section: {section_name}' + text = text.replace(f'#{pattern}', pattern) + return text @subcommand -def add(*, issue=None): +def add(*, issue=None, section=None): """ Add a blurb (a Misc/NEWS.d/next entry) to the current CPython repo. @@ -883,6 +916,17 @@ def add(*, issue=None): blurb add -i 12345 # or blurb add -i https://github.com/python/cpython/issues/12345 + +Use -s/--section to specify the section name (case-insensitive), e.g.: + + blurb add -s Library + # or + blurb add -s library + +The known sections names are defined as follows and +spaces in names can be substituted for underscores: + +{sections} """ editor = find_editor() @@ -891,7 +935,7 @@ def add(*, issue=None): os.close(handle) atexit.register(lambda : os.unlink(tmp_path)) - text = _blurb_template_text(issue=issue) + text = _blurb_template_text(issue=issue, section=section) with open(tmp_path, "w", encoding="utf-8") as file: file.write(text) @@ -940,7 +984,7 @@ def add(*, issue=None): git_add_files.append(path) flush_git_add_files() print("Ready for commit.") - +add.__doc__ = add.__doc__.format(sections='\n'.join(f'* {s}' for s in sections)) @subcommand diff --git a/tests/test_blurb_add.py b/tests/test_blurb_add.py index 814d7b2..d6d8287 100644 --- a/tests/test_blurb_add.py +++ b/tests/test_blurb_add.py @@ -7,7 +7,7 @@ def test_valid_no_issue_number(): assert blurb._extract_issue_number(None) is None - res = blurb._blurb_template_text(issue=None) + res = blurb._blurb_template_text(issue=None, section=None) lines = frozenset(res.splitlines()) assert '.. gh-issue:' not in lines assert '.. gh-issue: ' in lines @@ -37,7 +37,7 @@ def test_valid_issue_number_12345(issue): actual = blurb._extract_issue_number(issue) assert actual == 12345 - res = blurb._blurb_template_text(issue=issue) + res = blurb._blurb_template_text(issue=issue, section=None) lines = frozenset(res.splitlines()) assert '.. gh-issue:' not in lines assert '.. gh-issue: ' not in lines @@ -70,7 +70,7 @@ def test_valid_issue_number_12345(issue): 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) + blurb._blurb_template_text(issue=issue, section=None) @pytest.mark.parametrize('invalid', ( @@ -84,4 +84,80 @@ def test_malformed_gh_issue_line(invalid, monkeypatch): with monkeypatch.context() as cm: cm.setattr(blurb, 'template', template) with pytest.raises(SystemExit, match=error_message): - blurb._blurb_template_text(issue='1234') + blurb._blurb_template_text(issue='1234', section=None) + + +def _check_section_name(section_name, expected): + actual = blurb._extract_section_name(section_name) + assert actual == expected + + res = blurb._blurb_template_text(issue=None, section=section_name) + res = res.splitlines() + for section_name in blurb.sections: + if section_name == expected: + assert f'.. section: {section_name}' in res + else: + assert f'#.. section: {section_name}' in res + assert f'.. section: {section_name}' not in res + + +@pytest.mark.parametrize( + ('section_name', 'expected'), + [(name, name) for name in blurb.sections], +) +def test_exact_names(section_name, expected): + _check_section_name(section_name, expected) + + +@pytest.mark.parametrize( + ('section_name', 'expected'), + [(name.lower(), name) for name in blurb.sections], +) +def test_exact_names_lowercase(section_name, expected): + _check_section_name(section_name, expected) + + +@pytest.mark.parametrize('section', ( + '', + ' ', + '\t', + '\n', + '\r\n', + ' ', +)) +def test_empty_section_name(section): + error_message = re.escape('Empty section name!') + with pytest.raises(SystemExit, match=error_message): + blurb._extract_section_name(section) + + with pytest.raises(SystemExit, match=error_message): + blurb._blurb_template_text(issue=None, section=section) + + +@pytest.mark.parametrize('section', [ + # Wrong capitalisation + 'C api', + 'c API', + 'LibrarY', + # Invalid + '_', + '-', + '/', + 'invalid', + 'Not a section', + # Non-special names + 'c?api', + 'cXapi', + 'C+API', + # Super-strings + 'Library and more', + 'library3', + 'librari', +]) +def test_invalid_section_name(section): + error_message = rf"(?m)Invalid section name: '{re.escape(section)}'\n\n.+" + with pytest.raises(SystemExit, match=error_message): + blurb._extract_section_name(section) + + with pytest.raises(SystemExit, match=error_message): + blurb._blurb_template_text(issue=None, section=section)