From d5d11a2e71ddc55b7789655dd954f04670ccc325 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 01/30] Improve `blurb add` command. --- README.md | 41 +++++++++- src/blurb/blurb.py | 170 +++++++++++++++++++++++++++++++++++----- tests/test_blurb_add.py | 148 ++++++++++++++++++++++++++++++++++ 3 files changed, 338 insertions(+), 21 deletions(-) create mode 100644 tests/test_blurb_add.py diff --git a/README.md b/README.md index d1dcc8d..30013bd 100644 --- a/README.md +++ b/README.md @@ -109,12 +109,43 @@ The template for the `blurb add` message looks like this: Here's how you interact with the file: * Add the GitHub issue number for this commit to the - end of the `.. gh-issue:` line. + 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 the line reading `#.. section: Library`. To uncomment, just delete - the `#` at the front of the line. + the `#` at the front of the line. Alternatively, the section can + be specified via the ``-s/--section`` option: + + ```shell + blurb add -s "Library" + # or equivalently + blurb add -s 3 + ``` + + The section can be referred to from its name (case insensitive) or its ID + defined according to the following table: + + | ID | Section | + |----|-------------------| + | 1 | Security | + | 2 | Core and Builtins | + | 3 | Library | + | 4 | Documentation | + | 5 | Tests | + | 6 | Build | + | 7 | Windows | + | 8 | macOS | + | 9 | IDLE | + | 10 | Tools/Demos | + | 11 | C API | * Finally, go to the end of the file, and enter your `NEWS` entry. This should be a single paragraph of English text using @@ -239,6 +270,12 @@ part of the cherry-picking process. ## Changelog +### 1.2.0 + +- Add the `-i/--issue` and `-s/--section` options to the `add` command. + This lets you pre-fill-in the `gh-issue` and `section` fields + in the template. + ### 1.1.0 - Support GitHub Issues in addition to b.p.o (bugs.python.org). diff --git a/src/blurb/blurb.py b/src/blurb/blurb.py index cb1c4c8..47dfa01 100755 --- a/src/blurb/blurb.py +++ b/src/blurb/blurb.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """Command-line tool to manage CPython Misc/NEWS.d entries.""" -__version__ = "1.1.0" +__version__ = "1.1.1" ## ## blurb version 1.0 @@ -800,7 +800,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) @@ -869,10 +876,112 @@ def find_editor(): error('Could not find an editor! Set the EDITOR environment variable.') +def _extract_issue_number(issue): + if issue is None: + return None + + issue = raw_issue = str(issue).strip() + if issue.startswith('gh-'): + issue = issue[3:] + if issue.isdigit(): + return issue + + match = re.match(r'^(?:https?://)?github\.com/python/cpython/issues/(\d+)$', issue) + if match is None: + sys.exit(f"Invalid GitHub Issue: {raw_issue}") + return match.group(1) + + +def _extract_section_name(section): + if section is None: + return None + + section = raw_section = section.strip() + if section.strip('+-').isdigit(): + section_index = int(section) - 1 + if not (0 <= section_index < len(sections)): + sys.exit(f"Invalid section ID: {int(section)}\n\n" + f"Choose from the following table:\n\n" + f'{sections_table}') + return sections[section_index] + + if not section: + sys.exit(f"Empty section name!") + + section_words = section.lower().replace('_', ' ').split(' ') + section_pattern = '[_ ]'.join(map(re.escape, section_words)) + section_re = re.compile(section_pattern, re.I) + + matches = [] + for section_name in sections: + if section_re.match(section_name): + matches.append(section_name) + + if not matches: + sys.exit(f"Invalid section name: {raw_section}\n\n" + f"Choose from the following table:\n\n" + f'{sections_table}') + + if len(matches) > 1: + sys.exit(f"More than one match for: {raw_section}\n\n" + f"Choose from the following table:\n\n" + f'{sections_table}') + + return matches[0] + + +def _update_blurb_template(issue, section): + issue_number = _extract_issue_number(issue) + section_name = _extract_section_name(section) + + # Ensure that a whitespace is given after 'gh-issue:' + # to help filling up the template, unless an issue + # number was manually specified via the CLI. + text = template + + issue_line = ".. gh-issue:" + pattern = "\n" + issue_line + "\n" + if issue_number is None: + if pattern not in text: + sys.exit("Can't find gh-issue line to ensure there's a space on the end!") + replacement = "\n" + issue_line + " \n" + else: + if pattern not in text: + sys.exit("Can't find gh-issue line to fill!") + replacement = "\n" + issue_line + " " + str(issue_number) + "\n" + + text = text.replace(pattern, replacement) + + # Uncomment the section if needed. + if section_name is not None: + pattern = f'#.. section: {section_name}' + text = text.replace(pattern, pattern.lstrip('#')) + + return text + + @subcommand -def add(): +def add(*, issue=None, section=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 109198 + # or equivalently + blurb add -i https://github.com/python/cpython/issues/109198 + +The blurb's section can be specified via -s/--section +with its ID or name (case insenstitive), e.g.: + + blurb add -s %(section_example_name)r + # or equivalently + blurb add -s %(section_example_id)d + +The known sections IDs and names are defined as follows, +and spaces in names can be substituted for underscores: + +%(sections)s """ editor = find_editor() @@ -883,20 +992,8 @@ def add(): def init_tmp_with_template(): with open(tmp_path, "wt", encoding="utf-8") as file: - # hack: - # my editor likes to strip trailing whitespace from lines. - # normally this is a good idea. but in the case of the template - # it's unhelpful. - # so, manually ensure there's a space at the end of the gh-issue line. - text = template - - 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) - file.write(text) + updated = _update_blurb_template(issue, section) + file.write(updated) init_tmp_with_template() @@ -947,6 +1044,23 @@ def init_tmp_with_template(): print("Ready for commit.") +assert sections, 'sections is empty' +_sec_id_w = 2 + len(str(len(sections))) +_sec_name_w = 2 + max(map(len, sections)) +_sec_rowrule = '+'.join(['', '-' * _sec_id_w, '-' * _sec_name_w, '']) +_format_row = ('| {:%d} | {:%d} |' % (_sec_id_w - 2, _sec_name_w - 2)).format +sections_table = '\n'.join(itertools.chain( + [_sec_rowrule, _format_row('ID', 'Section'),_sec_rowrule.replace('-', '=')], + itertools.starmap(_format_row, enumerate(sections, 1)), + [_sec_rowrule] +)) +del _format_row, _sec_rowrule, _sec_name_w, _sec_id_w +add.__doc__ %= dict( + section_example_id=3, + section_example_name=sections[2], + sections=sections_table, +) + @subcommand def release(version): @@ -1221,25 +1335,39 @@ 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" + assert p.default is None or isinstance(p.default, (bool, str)), \ + "blurb command-line processing only handles boolean options" 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 + if a.startswith('-'): if a == "--": done_with_options = True @@ -1249,8 +1377,12 @@ def handle_option(s, dict): for s in a[1:]: handle_option(s, short_options) continue + filtered_args.append(a) + if consume_after: + sys.exit(f"Error: blurb: {subcommand} {consume_after} " + f"most be followed by an option argument") sys.exit(fn(*filtered_args, **kwargs)) except TypeError as e: diff --git a/tests/test_blurb_add.py b/tests/test_blurb_add.py new file mode 100644 index 0000000..0bfe1b8 --- /dev/null +++ b/tests/test_blurb_add.py @@ -0,0 +1,148 @@ +from itertools import chain, product +import re + +import pytest + +from blurb import blurb + + +ALLOWED_ISSUE_URL_PREFIX = [ + 'github.com/python/cpython/issues/', + 'http://github.com/python/cpython/issues/', + 'https://github.com/python/cpython/issues/' +] + +ALLOWED_SECTION_IDS = list(map(str, range(1 + len(blurb.sections), 1))) + + +def test_valid_no_issue_number(): + assert blurb._extract_issue_number(None) is None + res = blurb._update_blurb_template(issue=None, section=None) + lines = res.splitlines() + assert f'.. gh-issue:' not in lines + assert f'.. gh-issue: ' in lines + for line in lines: + assert not line.startswith('.. section: ') + + +@pytest.mark.parametrize(('issue', 'expect'), [ + (f'{w1}{prefix}12345{w2}', '12345') + for (w1, w2) in product(['', ' '], repeat=2) + for prefix in ('', 'gh-', *ALLOWED_ISSUE_URL_PREFIX) +]) +def test_valid_issue_number(issue, expect): + actual = blurb._extract_issue_number(issue) + assert actual == expect + + res = blurb._update_blurb_template(issue=issue, section=None) + + lines = res.splitlines() + assert f'.. gh-issue:' not in lines + for line in lines: + assert not line.startswith('.. section: ') + + assert f'.. gh-issue: {expect}' in lines + assert f'.. gh-issue: ' not in lines + + +@pytest.mark.parametrize('issue', [ + 'abc', + 'gh-abc', + 'gh-', + 'bpo-', + *[ + ''.join(_) for _ in + product(ALLOWED_ISSUE_URL_PREFIX, ('abc', '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._extract_issue_number(issue) + + with pytest.raises(SystemExit, match=error_message): + blurb._update_blurb_template(issue=issue, section=None) + + +@pytest.mark.parametrize('section', ALLOWED_SECTION_IDS) +def test_valid_section_id(section): + actual = blurb._extract_section_name(section) + assert actual == section + + res = blurb._update_blurb_template(issue=None, section=section) + res = res.splitlines() + for index, section_id in enumerate(ALLOWED_SECTION_IDS): + if section_id == section: + assert f'.. section: {blurb.sections[index]}' in res + else: + assert f'#.. section: {blurb.sections[index]}' in res + assert f'.. section: {blurb.sections[index]}' not in res + + +@pytest.mark.parametrize(('section', 'expect'), chain( + zip(blurb.sections, blurb.sections), + ((s.lower(), s) for s in blurb.sections), + ((s.upper(), s) for s in blurb.sections), + ((s.replace('_', ' '), s) for s in blurb.sections), + ((s.replace('_', ' ').lower(), s) for s in blurb.sections), + ((s.replace('_', ' ').upper(), s) for s in blurb.sections), +)) +def test_valid_section_name(section, expect): + actual = blurb._extract_section_name(section) + assert actual == expect + + res = blurb._update_blurb_template(issue=None, section=section) + res = res.splitlines() + for section_name in blurb.sections: + if section_name == expect: + 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', ['-1', '0', '1337']) +def test_invalid_section_id(section): + error_message = re.escape(f'Invalid section ID: {int(section)}') + error_message = re.compile(rf'{error_message}\n\n.+', re.MULTILINE) + with pytest.raises(SystemExit, match=error_message): + blurb._extract_section_name(section) + + with pytest.raises(SystemExit, match=error_message): + blurb._update_blurb_template(issue=None, section=section) + + +@pytest.mark.parametrize('section', ['libraryy', 'Not a section']) +def test_invalid_section_name(section): + error_message = re.escape(f'Invalid section name: {section}') + error_message = re.compile(rf'{error_message}\n\n.+', re.MULTILINE) + with pytest.raises(SystemExit, match=error_message): + blurb._extract_section_name(section) + + with pytest.raises(SystemExit, match=error_message): + blurb._update_blurb_template(issue=None, section=section) + + +@pytest.mark.parametrize('section', ['', ' ', ' ']) +def test_empty_section_name(section): + error_message = re.escape('Empty section name!') + with pytest.raises(SystemExit, match=error_message): + blurb._extract_section_name('') + + with pytest.raises(SystemExit, match=error_message): + blurb._update_blurb_template(issue=None, section='') + + +@pytest.mark.parametrize('invalid', [ + 'gh-issue: ', + 'gh-issue: 1', + 'gh-issue', +]) +def test_illformed_gh_issue_line(invalid, monkeypatch): + template = blurb.template.replace('.. gh-issue:', invalid) + error_message = re.escape("Can't find gh-issue line to fill!") + with monkeypatch.context() as cm: + cm.setattr(blurb, 'template', template) + with pytest.raises(SystemExit, match=error_message): + blurb._update_blurb_template(issue='1234', section=None) + From eaddee38a188cc0243bd355c3c28274cf2e92ab5 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 15:48:34 +0200 Subject: [PATCH 02/30] update version --- src/blurb/blurb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blurb/blurb.py b/src/blurb/blurb.py index 47dfa01..0331e2b 100755 --- a/src/blurb/blurb.py +++ b/src/blurb/blurb.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """Command-line tool to manage CPython Misc/NEWS.d entries.""" -__version__ = "1.1.1" +__version__ = "1.2.0" ## ## blurb version 1.0 From 403d3870b3289e7c01725750f7273ed5d02e1102 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 25 Jun 2024 13:52:36 +0000 Subject: [PATCH 03/30] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_blurb_add.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_blurb_add.py b/tests/test_blurb_add.py index 0bfe1b8..586099d 100644 --- a/tests/test_blurb_add.py +++ b/tests/test_blurb_add.py @@ -145,4 +145,3 @@ def test_illformed_gh_issue_line(invalid, monkeypatch): cm.setattr(blurb, 'template', template) with pytest.raises(SystemExit, match=error_message): blurb._update_blurb_template(issue='1234', section=None) - From ad230fe6162a3470a3f7e124ac1aa055b430bb2b 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 16:07:35 +0200 Subject: [PATCH 04/30] fix CI/CD --- .github/workflows/test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 388811d..5c2d790 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,9 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + # Temporarily remove 3.13 pending: + # https://github.com/pytest-dev/pyfakefs/issues/1017 + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 From 98565488bf7ef6dcc0dc3e911438c2627e2842c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Wed, 26 Jun 2024 08:58:55 +0200 Subject: [PATCH 05/30] fixup! typos --- src/blurb/blurb.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/blurb/blurb.py b/src/blurb/blurb.py index 0331e2b..24a3352 100755 --- a/src/blurb/blurb.py +++ b/src/blurb/blurb.py @@ -886,9 +886,9 @@ def _extract_issue_number(issue): if issue.isdigit(): return issue - match = re.match(r'^(?:https?://)?github\.com/python/cpython/issues/(\d+)$', issue) + match = re.match(r'^(?:https://)?github\.com/python/cpython/issues/(\d+)$', issue) if match is None: - sys.exit(f"Invalid GitHub Issue: {raw_issue}") + sys.exit(f"Invalid GitHub issue: {raw_issue}") return match.group(1) @@ -1382,7 +1382,7 @@ def handle_option(s, dict): if consume_after: sys.exit(f"Error: blurb: {subcommand} {consume_after} " - f"most be followed by an option argument") + f"must be followed by an option argument") sys.exit(fn(*filtered_args, **kwargs)) except TypeError as e: From a2a1fce88cae0df526444b96cf93fc39b895f90d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Wed, 26 Jun 2024 09:34:44 +0200 Subject: [PATCH 06/30] add test for known section names --- tests/test_blurb.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/test_blurb.py b/tests/test_blurb.py index 1c18a9f..7cbdef6 100644 --- a/tests/test_blurb.py +++ b/tests/test_blurb.py @@ -1,9 +1,16 @@ import pytest -from pyfakefs.fake_filesystem import FakeFilesystem from blurb import blurb +def test_section_names(): + assert tuple(blurb.sections) == ( + 'Security', 'Core and Builtins', 'Library', 'Documentation', + 'Tests', 'Build', 'Windows', 'macOS', 'IDLE', 'Tools/Demos', + 'C API', + ) + + UNCHANGED_SECTIONS = ( "Library", ) From 7d6783035c652834e4f65753b2118886c3d9fbe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Wed, 26 Jun 2024 09:37:46 +0200 Subject: [PATCH 07/30] expand tests --- tests/test_blurb_add.py | 153 +++++++++++++++++++++++++++++----------- 1 file changed, 111 insertions(+), 42 deletions(-) diff --git a/tests/test_blurb_add.py b/tests/test_blurb_add.py index 586099d..02f45de 100644 --- a/tests/test_blurb_add.py +++ b/tests/test_blurb_add.py @@ -1,4 +1,3 @@ -from itertools import chain, product import re import pytest @@ -6,12 +5,6 @@ from blurb import blurb -ALLOWED_ISSUE_URL_PREFIX = [ - 'github.com/python/cpython/issues/', - 'http://github.com/python/cpython/issues/', - 'https://github.com/python/cpython/issues/' -] - ALLOWED_SECTION_IDS = list(map(str, range(1 + len(blurb.sections), 1))) @@ -26,9 +19,26 @@ def test_valid_no_issue_number(): @pytest.mark.parametrize(('issue', 'expect'), [ - (f'{w1}{prefix}12345{w2}', '12345') - for (w1, w2) in product(['', ' '], repeat=2) - for prefix in ('', 'gh-', *ALLOWED_ISSUE_URL_PREFIX) + # issue given by their number + ('12345', '12345'), + ('12345 ', '12345'), + (' 12345', '12345'), + (' 12345 ', '12345'), + # issue given by their number and a 'gh-' prefix + ('gh-12345', '12345'), + ('gh-12345 ', '12345'), + (' gh-12345', '12345'), + (' gh-12345 ', '12345'), + # issue given by their URL (no protocol) + ('github.com/python/cpython/issues/12345', '12345'), + ('github.com/python/cpython/issues/12345 ', '12345'), + (' github.com/python/cpython/issues/12345', '12345'), + (' github.com/python/cpython/issues/12345 ', '12345'), + # issue given by their URL (with protocol) + ('https://github.com/python/cpython/issues/12345', '12345'), + ('https://github.com/python/cpython/issues/12345 ', '12345'), + (' https://github.com/python/cpython/issues/12345', '12345'), + (' https://github.com/python/cpython/issues/12345 ', '12345'), ]) def test_valid_issue_number(issue, expect): actual = blurb._extract_issue_number(issue) @@ -46,17 +56,27 @@ def test_valid_issue_number(issue, expect): @pytest.mark.parametrize('issue', [ + '', 'abc', 'gh-abc', 'gh-', 'bpo-', - *[ - ''.join(_) for _ in - product(ALLOWED_ISSUE_URL_PREFIX, ('abc', '1234?param=1')) - ] + '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}') + error_message = re.escape(f'Invalid GitHub issue: {issue}') with pytest.raises(SystemExit, match=error_message): blurb._extract_issue_number(issue) @@ -79,28 +99,6 @@ def test_valid_section_id(section): assert f'.. section: {blurb.sections[index]}' not in res -@pytest.mark.parametrize(('section', 'expect'), chain( - zip(blurb.sections, blurb.sections), - ((s.lower(), s) for s in blurb.sections), - ((s.upper(), s) for s in blurb.sections), - ((s.replace('_', ' '), s) for s in blurb.sections), - ((s.replace('_', ' ').lower(), s) for s in blurb.sections), - ((s.replace('_', ' ').upper(), s) for s in blurb.sections), -)) -def test_valid_section_name(section, expect): - actual = blurb._extract_section_name(section) - assert actual == expect - - res = blurb._update_blurb_template(issue=None, section=section) - res = res.splitlines() - for section_name in blurb.sections: - if section_name == expect: - 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', ['-1', '0', '1337']) def test_invalid_section_id(section): error_message = re.escape(f'Invalid section ID: {int(section)}') @@ -112,6 +110,70 @@ def test_invalid_section_id(section): blurb._update_blurb_template(issue=None, section=section) +class TestValidSectionNames: + @staticmethod + def check(section, expect): + actual = blurb._extract_section_name(section) + assert actual == expect + + res = blurb._update_blurb_template(issue=None, section=section) + res = res.splitlines() + for section_name in blurb.sections: + if section_name == expect: + 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', 'expect'), + tuple(zip(blurb.sections, blurb.sections)) + ) + def test_exact_names(self, section, expect): + self.check(section, expect) + + @pytest.mark.parametrize( + ('section', 'expect'), [ + ('Lib', 'Library'), + ('lib', 'Library'), + ('lib ', 'Library'), + ('doc', 'Documentation'), + ('Core and', 'Core and Builtins'), + ('core and', 'Core and Builtins'), + ('Core_and', 'Core and Builtins'), + ('core_and', 'Core and Builtins'), + ('core-and', 'Core and Builtins'), + ('Tools', 'Tools/Demos'), + ] + ) + def test_partial_names(self, section, expect): + self.check(section, expect) + + @pytest.mark.parametrize( + ('section', 'expect'), + [(name.lower(), name) for name in blurb.sections], + ) + def test_exact_names_lowercase(self, section, expect): + self.check(section, expect) + + @pytest.mark.parametrize( + ('section', 'expect'), + [(name.upper(), name) for name in blurb.sections], + ) + def test_exact_names_uppercase(self, section, expect): + self.check(section, expect) + + +@pytest.mark.parametrize('section', ['', ' ', ' ']) +def test_empty_section_name(section): + error_message = re.escape('Empty section name!') + with pytest.raises(SystemExit, match=error_message): + blurb._extract_section_name('') + + with pytest.raises(SystemExit, match=error_message): + blurb._update_blurb_template(issue=None, section='') + + @pytest.mark.parametrize('section', ['libraryy', 'Not a section']) def test_invalid_section_name(section): error_message = re.escape(f'Invalid section name: {section}') @@ -123,14 +185,21 @@ def test_invalid_section_name(section): blurb._update_blurb_template(issue=None, section=section) -@pytest.mark.parametrize('section', ['', ' ', ' ']) -def test_empty_section_name(section): - error_message = re.escape('Empty section name!') +@pytest.mark.parametrize(('section', 'matches'), [ + # 'matches' must be a sorted sequence of matching section names + ('C', ['C API', 'Core and Builtins']), + ('T', ['Tests', 'Tools/Demos']), +]) +def test_ambiguous_section_name(section, matches): + matching_list = ', '.join(map(repr, matches)) + error_message = re.escape(f'More than one match for: {section}\n' + f"Matches: {matching_list}") + error_message = re.compile(rf'{error_message}\n\n.+', re.MULTILINE) with pytest.raises(SystemExit, match=error_message): - blurb._extract_section_name('') + blurb._extract_section_name(section) with pytest.raises(SystemExit, match=error_message): - blurb._update_blurb_template(issue=None, section='') + blurb._update_blurb_template(issue=None, section=section) @pytest.mark.parametrize('invalid', [ From 635c8ac0a20935fb586c358943c0a30c6d423f00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Wed, 26 Jun 2024 09:38:02 +0200 Subject: [PATCH 08/30] improve section name detection --- src/blurb/blurb.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/blurb/blurb.py b/src/blurb/blurb.py index 24a3352..034709a 100755 --- a/src/blurb/blurb.py +++ b/src/blurb/blurb.py @@ -908,7 +908,8 @@ def _extract_section_name(section): if not section: sys.exit(f"Empty section name!") - section_words = section.lower().replace('_', ' ').split(' ') + sanitized = re.sub(r'[_-]', ' ', section) + section_words = re.split(r'\s+', sanitized) section_pattern = '[_ ]'.join(map(re.escape, section_words)) section_re = re.compile(section_pattern, re.I) @@ -923,7 +924,9 @@ def _extract_section_name(section): f'{sections_table}') if len(matches) > 1: - sys.exit(f"More than one match for: {raw_section}\n\n" + multiple_matches = ', '.join(map(repr, sorted(matches))) + sys.exit(f"More than one match for: {raw_section}\n" + f"Matches: {multiple_matches}\n\n" f"Choose from the following table:\n\n" f'{sections_table}') From 992b8ec4287fb72e8ff3ad8dd9539bfda3f619c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Wed, 26 Jun 2024 09:41:20 +0200 Subject: [PATCH 09/30] improve tests --- tests/test_blurb_add.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_blurb_add.py b/tests/test_blurb_add.py index 02f45de..aecc94b 100644 --- a/tests/test_blurb_add.py +++ b/tests/test_blurb_add.py @@ -135,15 +135,17 @@ def test_exact_names(self, section, expect): @pytest.mark.parametrize( ('section', 'expect'), [ ('Lib', 'Library'), - ('lib', 'Library'), - ('lib ', 'Library'), + ('Tools', 'Tools/Demos'), ('doc', 'Documentation'), + ('Core-and-Builtins', 'Core and Builtins'), + ('Core_and_Builtins', 'Core and Builtins'), + ('Core_and-Builtins', 'Core and Builtins'), ('Core and', 'Core and Builtins'), - ('core and', 'Core and Builtins'), ('Core_and', 'Core and Builtins'), ('core_and', 'Core and Builtins'), ('core-and', 'Core and Builtins'), - ('Tools', 'Tools/Demos'), + ('Core and Builtins', 'Core and Builtins'), + ('cOre _ and - bUILtins', 'Core and Builtins'), ] ) def test_partial_names(self, section, expect): From 842bc2d77468cb6d005ccb7c83bb622821f25e69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Wed, 26 Jun 2024 14:12:06 +0200 Subject: [PATCH 10/30] address review! --- tests/test_blurb_add.py | 86 +++++++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 37 deletions(-) diff --git a/tests/test_blurb_add.py b/tests/test_blurb_add.py index aecc94b..1e70a12 100644 --- a/tests/test_blurb_add.py +++ b/tests/test_blurb_add.py @@ -5,55 +5,52 @@ from blurb import blurb -ALLOWED_SECTION_IDS = list(map(str, range(1 + len(blurb.sections), 1))) - - def test_valid_no_issue_number(): assert blurb._extract_issue_number(None) is None res = blurb._update_blurb_template(issue=None, section=None) lines = res.splitlines() - assert f'.. gh-issue:' not in lines - assert f'.. gh-issue: ' in lines + assert '.. gh-issue:' not in lines + assert '.. gh-issue: ' in lines for line in lines: assert not line.startswith('.. section: ') -@pytest.mark.parametrize(('issue', 'expect'), [ +@pytest.mark.parametrize('issue', [ # issue given by their number - ('12345', '12345'), - ('12345 ', '12345'), - (' 12345', '12345'), - (' 12345 ', '12345'), + '12345', + '12345 ', + ' 12345', + ' 12345 ', # issue given by their number and a 'gh-' prefix - ('gh-12345', '12345'), - ('gh-12345 ', '12345'), - (' gh-12345', '12345'), - (' gh-12345 ', '12345'), + 'gh-12345', + 'gh-12345 ', + ' gh-12345', + ' gh-12345 ', # issue given by their URL (no protocol) - ('github.com/python/cpython/issues/12345', '12345'), - ('github.com/python/cpython/issues/12345 ', '12345'), - (' github.com/python/cpython/issues/12345', '12345'), - (' github.com/python/cpython/issues/12345 ', '12345'), + 'github.com/python/cpython/issues/12345', + 'github.com/python/cpython/issues/12345 ', + ' github.com/python/cpython/issues/12345', + ' github.com/python/cpython/issues/12345 ', # issue given by their URL (with protocol) - ('https://github.com/python/cpython/issues/12345', '12345'), - ('https://github.com/python/cpython/issues/12345 ', '12345'), - (' https://github.com/python/cpython/issues/12345', '12345'), - (' https://github.com/python/cpython/issues/12345 ', '12345'), + 'https://github.com/python/cpython/issues/12345', + 'https://github.com/python/cpython/issues/12345 ', + ' https://github.com/python/cpython/issues/12345', + ' https://github.com/python/cpython/issues/12345 ', ]) -def test_valid_issue_number(issue, expect): +def test_valid_issue_number_12345(issue): actual = blurb._extract_issue_number(issue) - assert actual == expect + assert actual == '12345' res = blurb._update_blurb_template(issue=issue, section=None) lines = res.splitlines() - assert f'.. gh-issue:' not in lines + assert '.. gh-issue:' not in lines + assert '.. gh-issue: ' not in lines + assert '.. gh-issue: 12345' in lines + for line in lines: assert not line.startswith('.. section: ') - assert f'.. gh-issue: {expect}' in lines - assert f'.. gh-issue: ' not in lines - @pytest.mark.parametrize('issue', [ '', @@ -84,15 +81,30 @@ def test_invalid_issue_number(issue): blurb._update_blurb_template(issue=issue, section=None) -@pytest.mark.parametrize('section', ALLOWED_SECTION_IDS) -def test_valid_section_id(section): - actual = blurb._extract_section_name(section) - assert actual == section - - res = blurb._update_blurb_template(issue=None, section=section) +@pytest.mark.parametrize(('section_index', 'section_id', 'section_name'), ( + (0, '1', 'Security'), + (1, '2', 'Core and Builtins'), + (2, '3', 'Library'), + (3, '4', 'Documentation'), + (4, '5', 'Tests'), + (5, '6', 'Build'), + (6, '7', 'Windows'), + (7, '8', 'macOS'), + (8, '9', 'IDLE'), + (9, '10', 'Tools/Demos'), + (10, '11', 'C API'), +)) +def test_valid_section_id(section_index, section_id, section_name): + actual = blurb._extract_section_name(section_id) + assert actual == section_name + assert actual == blurb.sections[section_index] + + res = blurb._update_blurb_template(issue=None, section=section_id) res = res.splitlines() - for index, section_id in enumerate(ALLOWED_SECTION_IDS): - if section_id == section: + + + for index, _ in enumerate(blurb.sections): + if index == section_index: assert f'.. section: {blurb.sections[index]}' in res else: assert f'#.. section: {blurb.sections[index]}' in res @@ -195,7 +207,7 @@ def test_invalid_section_name(section): def test_ambiguous_section_name(section, matches): matching_list = ', '.join(map(repr, matches)) error_message = re.escape(f'More than one match for: {section}\n' - f"Matches: {matching_list}") + f'Matches: {matching_list}') error_message = re.compile(rf'{error_message}\n\n.+', re.MULTILINE) with pytest.raises(SystemExit, match=error_message): blurb._extract_section_name(section) From 99261a5e6e6bff1bd8985c1f15829e42cfefb4e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Wed, 26 Jun 2024 14:15:47 +0200 Subject: [PATCH 11/30] remove extraneous line --- tests/test_blurb_add.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_blurb_add.py b/tests/test_blurb_add.py index 1e70a12..1984dbf 100644 --- a/tests/test_blurb_add.py +++ b/tests/test_blurb_add.py @@ -102,7 +102,6 @@ def test_valid_section_id(section_index, section_id, section_name): res = blurb._update_blurb_template(issue=None, section=section_id) res = res.splitlines() - for index, _ in enumerate(blurb.sections): if index == section_index: assert f'.. section: {blurb.sections[index]}' in res From 18a5563f48515193059cc8681fe294016f6b82f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 13 Jul 2024 09:35:56 +0200 Subject: [PATCH 12/30] address Larry's comments - remove section IDs matching - do not render the table in case of a multi-match - simplify `add` docstring construction - update tests - update README.md --- README.md | 41 ++++++++++++------------- src/blurb/blurb.py | 66 ++++++++++++++++++----------------------- tests/test_blurb_add.py | 48 ++++-------------------------- 3 files changed, 55 insertions(+), 100 deletions(-) diff --git a/README.md b/README.md index 7aef689..f67f7d8 100644 --- a/README.md +++ b/README.md @@ -113,9 +113,9 @@ Here's how you interact with the file: be specified via the ``-i/--issue`` option: ```shell - blurb add -i 109198 + $ blurb add -i 109198 # or equivalently - blurb add -i https://github.com/python/cpython/issues/109198 + $ blurb add -i https://github.com/python/cpython/issues/109198 ``` * Uncomment the line with the relevant `Misc/NEWS` section for this entry. @@ -125,27 +125,28 @@ Here's how you interact with the file: be specified via the ``-s/--section`` option: ```shell - blurb add -s "Library" + $ blurb add -s 'Library' # or equivalently - blurb add -s 3 + $ blurb add -s lib ``` - The section can be referred to from its name (case insensitive) or its ID - defined according to the following table: - - | ID | Section | - |----|-------------------| - | 1 | Security | - | 2 | Core and Builtins | - | 3 | Library | - | 4 | Documentation | - | 5 | Tests | - | 6 | Build | - | 7 | Windows | - | 8 | macOS | - | 9 | IDLE | - | 10 | Tools/Demos | - | 11 | C API | + The known section names are given in the following table. The match + is performed casse insensitively and partial matching is supported + as long as the match is unique: + + | Section Name | + |-------------------| + | Security | + | Core and Builtins | + | Library | + | Documentation | + | Tests | + | Build | + | Windows | + | macOS | + | IDLE | + | Tools/Demos | + | C API | * 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 8bda19a..ea77c56 100755 --- a/src/blurb/blurb.py +++ b/src/blurb/blurb.py @@ -60,13 +60,19 @@ import time import unittest -from . import __version__ +try: + from . import __version__ +except ImportError: + __version__ = 12343 # # This template is the canonical list of acceptable section names! # It's parsed internally into the "sections" set. # +# Do not forget to update the example for the 'add' command if +# the section names change as well as the corresponding tests. +# template = """ @@ -897,16 +903,8 @@ def _extract_section_name(section): return None section = raw_section = section.strip() - if section.strip('+-').isdigit(): - section_index = int(section) - 1 - if not (0 <= section_index < len(sections)): - sys.exit(f"Invalid section ID: {int(section)}\n\n" - f"Choose from the following table:\n\n" - f'{sections_table}') - return sections[section_index] - if not section: - sys.exit(f"Empty section name!") + sys.exit("Empty section name!") sanitized = re.sub(r'[_-]', ' ', section) section_words = re.split(r'\s+', sanitized) @@ -919,16 +917,14 @@ def _extract_section_name(section): matches.append(section_name) if not matches: - sys.exit(f"Invalid section name: {raw_section}\n\n" + sys.exit(f"Invalid section name: {raw_section!r}\n\n" f"Choose from the following table:\n\n" f'{sections_table}') if len(matches) > 1: multiple_matches = ', '.join(map(repr, sorted(matches))) - sys.exit(f"More than one match for: {raw_section}\n" - f"Matches: {multiple_matches}\n\n" - f"Choose from the following table:\n\n" - f'{sections_table}') + sys.exit(f"More than one match for: {raw_section!r}\n" + f"Matches: {multiple_matches}") return matches[0] @@ -971,20 +967,22 @@ def add(*, issue=None, section=None): Use -i/--issue to specify a GitHub issue number or link, e.g.: blurb add -i 109198 + # or equivalently blurb add -i https://github.com/python/cpython/issues/109198 The blurb's section can be specified via -s/--section -with its ID or name (case insenstitive), e.g.: +with its name (case insenstitive), e.g.: - blurb add -s %(section_example_name)r - # or equivalently - blurb add -s %(section_example_id)d + blurb add -s 'Core and Builtins' + + # or using a partial matching + blurb add -s core -The known sections IDs and names are defined as follows, -and spaces in names can be substituted for underscores: +The known sections names are defined as follows and +spaces in names can be substituted for underscores: -%(sections)s +{sections} """ editor = find_editor() @@ -1048,21 +1046,15 @@ def init_tmp_with_template(): assert sections, 'sections is empty' -_sec_id_w = 2 + len(str(len(sections))) -_sec_name_w = 2 + max(map(len, sections)) -_sec_rowrule = '+'.join(['', '-' * _sec_id_w, '-' * _sec_name_w, '']) -_format_row = ('| {:%d} | {:%d} |' % (_sec_id_w - 2, _sec_name_w - 2)).format -sections_table = '\n'.join(itertools.chain( - [_sec_rowrule, _format_row('ID', 'Section'),_sec_rowrule.replace('-', '=')], - itertools.starmap(_format_row, enumerate(sections, 1)), - [_sec_rowrule] -)) -del _format_row, _sec_rowrule, _sec_name_w, _sec_id_w -add.__doc__ %= dict( - section_example_id=3, - section_example_name=sections[2], - sections=sections_table, -) +_sec_name_width = 2 + max(map(len, sections)) +_sec_rowrule = '+'.join(['', '-' * _sec_name_width, '']) +_format_row = (f'| {{:{_sec_name_width - 2:d}}} |').format +del _sec_name_width +sections_table = '\n'.join(map(_format_row, sections)) +del _format_row +sections_table = '\n'.join((_sec_rowrule, sections_table, _sec_rowrule)) +del _sec_rowrule +add.__doc__ = add.__doc__.format(sections=sections_table) @subcommand diff --git a/tests/test_blurb_add.py b/tests/test_blurb_add.py index 1984dbf..8b20e82 100644 --- a/tests/test_blurb_add.py +++ b/tests/test_blurb_add.py @@ -81,46 +81,6 @@ def test_invalid_issue_number(issue): blurb._update_blurb_template(issue=issue, section=None) -@pytest.mark.parametrize(('section_index', 'section_id', 'section_name'), ( - (0, '1', 'Security'), - (1, '2', 'Core and Builtins'), - (2, '3', 'Library'), - (3, '4', 'Documentation'), - (4, '5', 'Tests'), - (5, '6', 'Build'), - (6, '7', 'Windows'), - (7, '8', 'macOS'), - (8, '9', 'IDLE'), - (9, '10', 'Tools/Demos'), - (10, '11', 'C API'), -)) -def test_valid_section_id(section_index, section_id, section_name): - actual = blurb._extract_section_name(section_id) - assert actual == section_name - assert actual == blurb.sections[section_index] - - res = blurb._update_blurb_template(issue=None, section=section_id) - res = res.splitlines() - - for index, _ in enumerate(blurb.sections): - if index == section_index: - assert f'.. section: {blurb.sections[index]}' in res - else: - assert f'#.. section: {blurb.sections[index]}' in res - assert f'.. section: {blurb.sections[index]}' not in res - - -@pytest.mark.parametrize('section', ['-1', '0', '1337']) -def test_invalid_section_id(section): - error_message = re.escape(f'Invalid section ID: {int(section)}') - error_message = re.compile(rf'{error_message}\n\n.+', re.MULTILINE) - with pytest.raises(SystemExit, match=error_message): - blurb._extract_section_name(section) - - with pytest.raises(SystemExit, match=error_message): - blurb._update_blurb_template(issue=None, section=section) - - class TestValidSectionNames: @staticmethod def check(section, expect): @@ -189,7 +149,7 @@ def test_empty_section_name(section): @pytest.mark.parametrize('section', ['libraryy', 'Not a section']) def test_invalid_section_name(section): - error_message = re.escape(f'Invalid section name: {section}') + error_message = re.escape(f'Invalid section name: {section!r}') error_message = re.compile(rf'{error_message}\n\n.+', re.MULTILINE) with pytest.raises(SystemExit, match=error_message): blurb._extract_section_name(section) @@ -200,14 +160,16 @@ def test_invalid_section_name(section): @pytest.mark.parametrize(('section', 'matches'), [ # 'matches' must be a sorted sequence of matching section names + ('c', ['C API', 'Core and Builtins']), ('C', ['C API', 'Core and Builtins']), + ('t', ['Tests', 'Tools/Demos']), ('T', ['Tests', 'Tools/Demos']), ]) def test_ambiguous_section_name(section, matches): matching_list = ', '.join(map(repr, matches)) - error_message = re.escape(f'More than one match for: {section}\n' + error_message = re.escape(f'More than one match for: {section!r}\n' f'Matches: {matching_list}') - error_message = re.compile(rf'{error_message}\n\n.+', re.MULTILINE) + error_message = re.compile(rf'{error_message}', re.MULTILINE) with pytest.raises(SystemExit, match=error_message): blurb._extract_section_name(section) From 350daeb976ba89baeb04fff80c65f0603a16654c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 13 Jul 2024 09:37:21 +0200 Subject: [PATCH 13/30] remove local fix --- src/blurb/blurb.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/blurb/blurb.py b/src/blurb/blurb.py index ea77c56..932ca71 100755 --- a/src/blurb/blurb.py +++ b/src/blurb/blurb.py @@ -60,10 +60,7 @@ import time import unittest -try: - from . import __version__ -except ImportError: - __version__ = 12343 +from . import __version__ # From 0e25cc02ff3ad8545270279e9c1492dfc4f8b1da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 13 Jul 2024 09:38:00 +0200 Subject: [PATCH 14/30] remove un-necessary blank line --- src/blurb/blurb.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/blurb/blurb.py b/src/blurb/blurb.py index 932ca71..aaf1609 100755 --- a/src/blurb/blurb.py +++ b/src/blurb/blurb.py @@ -62,7 +62,6 @@ from . import __version__ - # # This template is the canonical list of acceptable section names! # It's parsed internally into the "sections" set. From 2b976e2c6c41b9b69335d4c665746c361f72dc41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 13 Jul 2024 09:38:35 +0200 Subject: [PATCH 15/30] ... --- src/blurb/blurb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blurb/blurb.py b/src/blurb/blurb.py index aaf1609..81aa5b7 100755 --- a/src/blurb/blurb.py +++ b/src/blurb/blurb.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 """Command-line tool to manage CPython Misc/NEWS.d entries.""" - ## ## Part of the blurb package. ## Copyright 2015-2018 by Larry Hastings @@ -62,6 +61,7 @@ from . import __version__ + # # This template is the canonical list of acceptable section names! # It's parsed internally into the "sections" set. From d578f92480882e3d9334a5ebb73e3806e932110e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 13 Jul 2024 09:40:36 +0200 Subject: [PATCH 16/30] use the same example in the README and the docstring of `add` --- src/blurb/blurb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/blurb/blurb.py b/src/blurb/blurb.py index 81aa5b7..67bf84a 100755 --- a/src/blurb/blurb.py +++ b/src/blurb/blurb.py @@ -970,10 +970,10 @@ def add(*, issue=None, section=None): The blurb's section can be specified via -s/--section with its name (case insenstitive), e.g.: - blurb add -s 'Core and Builtins' + blurb add -s 'Library' # or using a partial matching - blurb add -s core + blurb add -s lib The known sections names are defined as follows and spaces in names can be substituted for underscores: From f11b9f11d8a11bbe6fe8fbd7e4ad4f8d5f20ed05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 13 Jul 2024 11:32:53 +0200 Subject: [PATCH 17/30] Update README.md --- README.md | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/README.md b/README.md index f67f7d8..3faf1bd 100644 --- a/README.md +++ b/README.md @@ -132,21 +132,7 @@ Here's how you interact with the file: The known section names are given in the following table. The match is performed casse insensitively and partial matching is supported - as long as the match is unique: - - | Section Name | - |-------------------| - | Security | - | Core and Builtins | - | Library | - | Documentation | - | Tests | - | Build | - | Windows | - | macOS | - | IDLE | - | Tools/Demos | - | C API | + as long as the match is unique. * Finally, go to the end of the file, and enter your `NEWS` entry. This should be a single paragraph of English text using From 6737ea40f968826d1fad92a824b38cff909860b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 13 Jul 2024 11:35:28 +0200 Subject: [PATCH 18/30] Update README.md Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3faf1bd..54a6897 100644 --- a/README.md +++ b/README.md @@ -243,7 +243,7 @@ part of the cherry-picking process. - Move code from `python/core-workflow` to own `python/blurb` repo. - Deploy to PyPI via Trusted Publishers. - Add the `-i/--issue` and `-s/--section` options to the `add` command. - This lets you pre-fill-in the `gh-issue` and `section` fields + This lets you pre-fill the `gh-issue` and `section` fields. in the template. ### 1.1.0 From 384079db511c0177c351d2a190f1fea5f75819c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 13 Jul 2024 11:35:57 +0200 Subject: [PATCH 19/30] Update src/blurb/blurb.py Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/blurb/blurb.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/blurb/blurb.py b/src/blurb/blurb.py index 67bf84a..8d2ce91 100755 --- a/src/blurb/blurb.py +++ b/src/blurb/blurb.py @@ -1043,13 +1043,13 @@ def init_tmp_with_template(): assert sections, 'sections is empty' _sec_name_width = 2 + max(map(len, sections)) -_sec_rowrule = '+'.join(['', '-' * _sec_name_width, '']) +_sec_row_rule = '+'.join(['', '-' * _sec_name_width, '']) _format_row = (f'| {{:{_sec_name_width - 2:d}}} |').format del _sec_name_width sections_table = '\n'.join(map(_format_row, sections)) del _format_row -sections_table = '\n'.join((_sec_rowrule, sections_table, _sec_rowrule)) -del _sec_rowrule +sections_table = '\n'.join((_sec_row_rule, sections_table, _sec_rowrule)) +del _sec_row_rule add.__doc__ = add.__doc__.format(sections=sections_table) From 9242ddcacbe18318347753b13deb272a04a74a70 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 13 Jul 2024 03:43:32 -0600 Subject: [PATCH 20/30] Update src/blurb/blurb.py --- src/blurb/blurb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blurb/blurb.py b/src/blurb/blurb.py index 8d2ce91..5d5d293 100755 --- a/src/blurb/blurb.py +++ b/src/blurb/blurb.py @@ -1048,7 +1048,7 @@ def init_tmp_with_template(): del _sec_name_width sections_table = '\n'.join(map(_format_row, sections)) del _format_row -sections_table = '\n'.join((_sec_row_rule, sections_table, _sec_rowrule)) +sections_table = '\n'.join((_sec_row_rule, sections_table, _sec_row_rule)) del _sec_row_rule add.__doc__ = add.__doc__.format(sections=sections_table) From 8619bc2dca5a018d9514123e54055823f9025662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 13 Jul 2024 11:50:42 +0200 Subject: [PATCH 21/30] Update README.md Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 54a6897..0fd9982 100644 --- a/README.md +++ b/README.md @@ -130,9 +130,8 @@ Here's how you interact with the file: $ blurb add -s lib ``` - The known section names are given in the following table. The match - is performed casse insensitively and partial matching is supported - as long as the match is unique. + The match is performed case insensitively and partial matching is + supported as long as the match is unique. * Finally, go to the end of the file, and enter your `NEWS` entry. This should be a single paragraph of English text using From 9de55afa26fa1c15c4ffb98de68a323051aea7e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 13 Jul 2024 14:10:23 +0200 Subject: [PATCH 22/30] improve matching algorithm --- src/blurb/blurb.py | 67 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 6 deletions(-) diff --git a/src/blurb/blurb.py b/src/blurb/blurb.py index 5d5d293..a85952b 100755 --- a/src/blurb/blurb.py +++ b/src/blurb/blurb.py @@ -42,6 +42,7 @@ import atexit import base64 import builtins +from collections import defaultdict import glob import hashlib import io @@ -894,6 +895,40 @@ def _extract_issue_number(issue): return match.group(1) +# Mapping from section names to additional allowed patterns +# which ignore whitespaces for composed section names. +# +# For instance, 'Core and Builtins' is represented by the +# pattern 'Core?and?Builtins' where are the +# allowed user separators '_', '-', ' ' and '/'. +_section_special_patterns = {__: set() for __ in sections} + +# Mapping from section names to sanitized names (no separators, lowercase). +# +# For instance, 'Core and Builtins' is mapped to 'coreandbuiltins', and +# passing a prefix of that would match to 'Core and Builtins'. Note that +# this is only used as a last resort. +_section_names_lower_nosep = {} + +for _section in sections: + _sanitized = re.sub(r'[_ /]', ' ', _section) + _section_words = re.split(r'\s+', _sanitized) + _section_names_lower_nosep[_section] = ''.join(_section_words).lower() + del _sanitized + _section_pattern = r'[_\- /]?'.join(map(re.escape, _section_words)) + # add '$' to avoid matching after the pattern + _section_pattern = f'{_section_pattern}$' + del _section_words + _section_pattern = re.compile(_section_pattern, re.I) + _section_special_patterns[_section].add(_section_pattern) + del _section_pattern, _section + +# the following statements will raise KeyError if the names are invalid +_section_special_patterns['C API'].add(re.compile(r'^((?<=c)[_\- /])?api?$', re.I)) +_section_special_patterns['Core and Builtins'].add(re.compile('^builtins?$', re.I)) +_section_special_patterns['Tools/Demos'].add(re.compile('^dem(?:o|os)?$', re.I)) + + def _extract_section_name(section): if section is None: return None @@ -902,16 +937,36 @@ def _extract_section_name(section): if not section: sys.exit("Empty section name!") - sanitized = re.sub(r'[_-]', ' ', section) - section_words = re.split(r'\s+', sanitized) - section_pattern = '[_ ]'.join(map(re.escape, section_words)) - section_re = re.compile(section_pattern, re.I) - matches = [] + + # '_', '-', ' ' and '/' are the allowed (user) separators + sanitized = re.sub(r'[_\- /]', ' ', section) + section_words = re.split(r'\s+', sanitized) + # '_', ' ' and '/' are the separators used by known sections + section_pattern = r'[_ /]'.join(map(re.escape, section_words)) + section_pattern = re.compile(section_pattern, re.I) for section_name in sections: - if section_re.match(section_name): + # try to use the input as the pattern to match against known names + if section_pattern.match(section_name): matches.append(section_name) + if not matches: + for section_name, special_patterns in _section_special_patterns.items(): + for special_pattern in special_patterns: + if special_pattern.match(sanitized): + matches.append(section_name) + break + + if not matches: + # try to use the input as the prefix of a known section name + normalized_prefix = ''.join(section_words).lower() + for section_name, normalized in _section_names_lower_nosep.items(): + if ( + len(normalized_prefix) <= len(normalized) and + normalized.startswith(normalized_prefix) + ): + matches.append(section_name) + if not matches: sys.exit(f"Invalid section name: {raw_section!r}\n\n" f"Choose from the following table:\n\n" From a7cd2630b94190420df1e229732be5481fd262dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 13 Jul 2024 14:10:43 +0200 Subject: [PATCH 23/30] increase test coverage --- tests/test_blurb_add.py | 81 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 3 deletions(-) diff --git a/tests/test_blurb_add.py b/tests/test_blurb_add.py index 8b20e82..ec6e871 100644 --- a/tests/test_blurb_add.py +++ b/tests/test_blurb_add.py @@ -105,9 +105,50 @@ def test_exact_names(self, section, expect): @pytest.mark.parametrize( ('section', 'expect'), [ + ('Sec', 'Security'), + ('sec', 'Security'), + ('security', 'Security'), + ('Core And', 'Core and Builtins'), + ('Core And Built', 'Core and Builtins'), + ('Core And Builtins', 'Core and Builtins'), ('Lib', 'Library'), - ('Tools', 'Tools/Demos'), ('doc', 'Documentation'), + ('document', 'Documentation'), + ('Tes', 'Tests'), + ('tes', 'Tests'), + ('Test', 'Tests'), + ('Tests', 'Tests'), + ('Buil', 'Build'), + ('buil', 'Build'), + ('build', 'Build'), + ('Tool', 'Tools/Demos'), + ('Tools', 'Tools/Demos'), + ('Tools/', 'Tools/Demos'), + ('core', 'Core and Builtins'), + ] + ) + def test_partial_words(self, section, expect): + # test that partial matching from the beginning is supported + self.check(section, expect) + + @pytest.mark.parametrize( + ('section', 'expect'), [ + ('builtin', 'Core and Builtins'), + ('builtins', 'Core and Builtins'), + ('api', 'C API'), + ('c-api', 'C API'), + ('c/api', 'C API'), + ('c api', 'C API'), + ('dem', 'Tools/Demos'), + ('demo', 'Tools/Demos'), + ('demos', 'Tools/Demos'), + ] + ) + def test_partial_special_names(self, section, expect): + self.check(section, expect) + + @pytest.mark.parametrize( + ('section', 'expect'), [ ('Core-and-Builtins', 'Core and Builtins'), ('Core_and_Builtins', 'Core and Builtins'), ('Core_and-Builtins', 'Core and Builtins'), @@ -117,11 +158,33 @@ def test_exact_names(self, section, expect): ('core-and', 'Core and Builtins'), ('Core and Builtins', 'Core and Builtins'), ('cOre _ and - bUILtins', 'Core and Builtins'), + ('Tools/demo', 'Tools/Demos'), + ('Tools-demo', 'Tools/Demos'), + ('Tools demo', 'Tools/Demos'), ] ) - def test_partial_names(self, section, expect): + def test_partial_separators(self, section, expect): self.check(section, expect) + @pytest.mark.parametrize( + ('prefix', 'expect'), [ + ('corean', 'Core and Builtins'), + ('coreand', 'Core and Builtins'), + ('coreandbuilt', 'Core and Builtins'), + ('coreand Builtins', 'Core and Builtins'), + ('coreand Builtins', 'Core and Builtins'), + ('coreAnd Builtins', 'Core and Builtins'), + ('CoreAnd Builtins', 'Core and Builtins'), + ('Coreand', 'Core and Builtins'), + ('Coreand Builtins', 'Core and Builtins'), + ('Coreand builtin', 'Core and Builtins'), + ('Coreand buil', 'Core and Builtins'), + ] + ) + def test_partial_prefix_words(self, prefix, expect): + # spaces are not needed if we cannot find a correct match + self.check(prefix, expect) + @pytest.mark.parametrize( ('section', 'expect'), [(name.lower(), name) for name in blurb.sections], @@ -147,7 +210,19 @@ def test_empty_section_name(section): blurb._update_blurb_template(issue=None, section='') -@pytest.mark.parametrize('section', ['libraryy', 'Not a section']) +@pytest.mark.parametrize('section', [ + # 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 = re.escape(f'Invalid section name: {section!r}') error_message = re.compile(rf'{error_message}\n\n.+', re.MULTILINE) From ba33c3834776e44f2dae783baad37167391b43bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 13 Jul 2024 14:13:51 +0200 Subject: [PATCH 24/30] update comments --- tests/test_blurb_add.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_blurb_add.py b/tests/test_blurb_add.py index ec6e871..fff2d50 100644 --- a/tests/test_blurb_add.py +++ b/tests/test_blurb_add.py @@ -128,7 +128,6 @@ def test_exact_names(self, section, expect): ] ) def test_partial_words(self, section, expect): - # test that partial matching from the beginning is supported self.check(section, expect) @pytest.mark.parametrize( @@ -164,6 +163,7 @@ def test_partial_special_names(self, section, expect): ] ) def test_partial_separators(self, section, expect): + # normalize the separtors '_', '-', ' ' and '/' self.check(section, expect) @pytest.mark.parametrize( @@ -182,7 +182,7 @@ def test_partial_separators(self, section, expect): ] ) def test_partial_prefix_words(self, prefix, expect): - # spaces are not needed if we cannot find a correct match + # try to find a match using prefixes (without separators and lowercase) self.check(prefix, expect) @pytest.mark.parametrize( From cb04947257db1a79316ce673cbddec9f06ba824a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 13 Jul 2024 14:21:42 +0200 Subject: [PATCH 25/30] simplify supported separators --- src/blurb/blurb.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/blurb/blurb.py b/src/blurb/blurb.py index a85952b..e86e6e2 100755 --- a/src/blurb/blurb.py +++ b/src/blurb/blurb.py @@ -42,7 +42,6 @@ import atexit import base64 import builtins -from collections import defaultdict import glob import hashlib import io @@ -911,10 +910,12 @@ def _extract_issue_number(issue): _section_names_lower_nosep = {} for _section in sections: - _sanitized = re.sub(r'[_ /]', ' ', _section) + # ' ' and '/' are the separators used by known sections + _sanitized = re.sub(r'[ /]', ' ', _section) _section_words = re.split(r'\s+', _sanitized) _section_names_lower_nosep[_section] = ''.join(_section_words).lower() del _sanitized + # '_', '-', ' ' and '/' are the allowed (user) separators _section_pattern = r'[_\- /]?'.join(map(re.escape, _section_words)) # add '$' to avoid matching after the pattern _section_pattern = f'{_section_pattern}$' @@ -942,8 +943,8 @@ def _extract_section_name(section): # '_', '-', ' ' and '/' are the allowed (user) separators sanitized = re.sub(r'[_\- /]', ' ', section) section_words = re.split(r'\s+', sanitized) - # '_', ' ' and '/' are the separators used by known sections - section_pattern = r'[_ /]'.join(map(re.escape, section_words)) + # ' ' and '/' are the separators used by known sections + section_pattern = r'[ /]'.join(map(re.escape, section_words)) section_pattern = re.compile(section_pattern, re.I) for section_name in sections: # try to use the input as the pattern to match against known names From 33ae76eba4baebecae16652c995c9a3d41dfb869 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 13 Jul 2024 14:22:02 +0200 Subject: [PATCH 26/30] fix regex --- src/blurb/blurb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blurb/blurb.py b/src/blurb/blurb.py index e86e6e2..754a8c0 100755 --- a/src/blurb/blurb.py +++ b/src/blurb/blurb.py @@ -925,7 +925,7 @@ def _extract_issue_number(issue): del _section_pattern, _section # the following statements will raise KeyError if the names are invalid -_section_special_patterns['C API'].add(re.compile(r'^((?<=c)[_\- /])?api?$', re.I)) +_section_special_patterns['C API'].add(re.compile(r'^((?<=c)[_\- /])?api$', re.I)) _section_special_patterns['Core and Builtins'].add(re.compile('^builtins?$', re.I)) _section_special_patterns['Tools/Demos'].add(re.compile('^dem(?:o|os)?$', re.I)) From 48fc24b197777d118a78ca0e95a21fccb2b353cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 13 Jul 2024 14:38:32 +0200 Subject: [PATCH 27/30] improve error messages --- src/blurb/blurb.py | 36 +++++++++++++++++++++++++----------- tests/test_blurb_add.py | 7 +++++-- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/blurb/blurb.py b/src/blurb/blurb.py index 754a8c0..7f2d0b4 100755 --- a/src/blurb/blurb.py +++ b/src/blurb/blurb.py @@ -930,21 +930,17 @@ def _extract_issue_number(issue): _section_special_patterns['Tools/Demos'].add(re.compile('^dem(?:o|os)?$', re.I)) -def _extract_section_name(section): - if section is None: - return None - - section = raw_section = section.strip() - if not section: - sys.exit("Empty section name!") +def _find_smart_matches(section): + # '_', '-' and ' ' are the allowed (user) whitespace separators + sanitized = re.sub(r'[_\- ]', ' ', section).strip() + if not sanitized: + return [] matches = [] - - # '_', '-', ' ' and '/' are the allowed (user) separators - sanitized = re.sub(r'[_\- /]', ' ', section) section_words = re.split(r'\s+', sanitized) # ' ' and '/' are the separators used by known sections section_pattern = r'[ /]'.join(map(re.escape, section_words)) + section_pattern = re.compile(section_pattern, re.I) for section_name in sections: # try to use the input as the pattern to match against known names @@ -959,7 +955,7 @@ def _extract_section_name(section): break if not matches: - # try to use the input as the prefix of a known section name + # try to use the input as the prefix of a flattened section name normalized_prefix = ''.join(section_words).lower() for section_name, normalized in _section_names_lower_nosep.items(): if ( @@ -968,6 +964,24 @@ def _extract_section_name(section): ): matches.append(section_name) + return matches + +def _extract_section_name(section): + if section is None: + return None + + section = raw_section = section.strip() + if not section: + sys.exit("Empty section name!") + + matches = [] + # try a simple exact match + for section_name in sections: + if section_name.lower().startswith(section.lower()): + matches.append(section_name) + # try a more complex algorithm if we are unlucky + matches = matches or _find_smart_matches(section) + if not matches: sys.exit(f"Invalid section name: {raw_section!r}\n\n" f"Choose from the following table:\n\n" diff --git a/tests/test_blurb_add.py b/tests/test_blurb_add.py index fff2d50..061dd48 100644 --- a/tests/test_blurb_add.py +++ b/tests/test_blurb_add.py @@ -204,14 +204,17 @@ def test_exact_names_uppercase(self, section, expect): def test_empty_section_name(section): error_message = re.escape('Empty section name!') with pytest.raises(SystemExit, match=error_message): - blurb._extract_section_name('') + blurb._extract_section_name(section) with pytest.raises(SystemExit, match=error_message): - blurb._update_blurb_template(issue=None, section='') + blurb._update_blurb_template(issue=None, section=section) @pytest.mark.parametrize('section', [ # invalid + '_', + '-', + '/', 'invalid', 'Not a section', # non-special names From 15271e158cb175e571963383f135e8713aaf5feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 13 Jul 2024 14:40:58 +0200 Subject: [PATCH 28/30] cleanup --- src/blurb/blurb.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/blurb/blurb.py b/src/blurb/blurb.py index 7f2d0b4..c893dc2 100755 --- a/src/blurb/blurb.py +++ b/src/blurb/blurb.py @@ -940,8 +940,8 @@ def _find_smart_matches(section): section_words = re.split(r'\s+', sanitized) # ' ' and '/' are the separators used by known sections section_pattern = r'[ /]'.join(map(re.escape, section_words)) - section_pattern = re.compile(section_pattern, re.I) + for section_name in sections: # try to use the input as the pattern to match against known names if section_pattern.match(section_name): @@ -958,14 +958,12 @@ def _find_smart_matches(section): # try to use the input as the prefix of a flattened section name normalized_prefix = ''.join(section_words).lower() for section_name, normalized in _section_names_lower_nosep.items(): - if ( - len(normalized_prefix) <= len(normalized) and - normalized.startswith(normalized_prefix) - ): + if normalized.startswith(normalized_prefix): matches.append(section_name) return matches + def _extract_section_name(section): if section is None: return None From 026052c93240c5a07b766a3a690ca543ae3d0f01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Wed, 14 Aug 2024 09:52:10 +0200 Subject: [PATCH 29/30] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 068d1f0..9045c2c 100644 --- a/README.md +++ b/README.md @@ -228,7 +228,6 @@ the right thing. If `NEWS` entries were already written to the final version directory, you'd have to move those around as part of the cherry-picking process. - ## Copyright **blurb** is Copyright 2015-2018 by Larry Hastings. From 5fe40bd6ed7f95c221cbc5e873a44a1bbcfab84b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Wed, 14 Aug 2024 09:53:31 +0200 Subject: [PATCH 30/30] Update CHANGELOG.md --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4783afd..0a7e7bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 1.2.2 + +- Add the `-i/--issue` and `-s/--section` options to the `add` command. + This lets you pre-fill the `gh-issue` and `section` fields in the template. + ## 1.2.1 - Fix `python3 -m blurb`.