Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-6: add -i/--issue and -s/--section flags to blurb add #16

Open
wants to merge 34 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d5d11a2
Improve `blurb add` command.
picnixz Jun 25, 2024
eaddee3
update version
picnixz Jun 25, 2024
403d387
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 25, 2024
ad230fe
fix CI/CD
picnixz Jun 25, 2024
edd8eea
Merge branch 'add-gh-flags' of github.com:picnixz/blurb into add-gh-f…
picnixz Jun 25, 2024
9856548
fixup! typos
picnixz Jun 26, 2024
a2a1fce
add test for known section names
picnixz Jun 26, 2024
7d67830
expand tests
picnixz Jun 26, 2024
635c8ac
improve section name detection
picnixz Jun 26, 2024
992b8ec
improve tests
picnixz Jun 26, 2024
842bc2d
address review!
picnixz Jun 26, 2024
99261a5
remove extraneous line
picnixz Jun 26, 2024
114013c
Merge branch 'main' into add-gh-flags
picnixz Jul 12, 2024
18a5563
address Larry's comments
picnixz Jul 13, 2024
350daeb
remove local fix
picnixz Jul 13, 2024
0e25cc0
remove un-necessary blank line
picnixz Jul 13, 2024
2b976e2
...
picnixz Jul 13, 2024
d578f92
use the same example in the README and the docstring of `add`
picnixz Jul 13, 2024
f11b9f1
Update README.md
picnixz Jul 13, 2024
6737ea4
Update README.md
picnixz Jul 13, 2024
384079d
Update src/blurb/blurb.py
picnixz Jul 13, 2024
9242ddc
Update src/blurb/blurb.py
hugovk Jul 13, 2024
8619bc2
Update README.md
picnixz Jul 13, 2024
9de55af
improve matching algorithm
picnixz Jul 13, 2024
a7cd263
increase test coverage
picnixz Jul 13, 2024
ba33c38
update comments
picnixz Jul 13, 2024
cb04947
simplify supported separators
picnixz Jul 13, 2024
33ae76e
fix regex
picnixz Jul 13, 2024
48fc24b
improve error messages
picnixz Jul 13, 2024
15271e1
cleanup
picnixz Jul 13, 2024
a3899ec
Merge branch 'main' into add-gh-flags
picnixz Aug 14, 2024
026052c
Update README.md
picnixz Aug 14, 2024
5fe40bd
Update CHANGELOG.md
picnixz Aug 14, 2024
1f789b0
Merge branch 'main' into add-gh-flags
picnixz Nov 1, 2024
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 1.3.1

- 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.
Added by @picnixz in https://github.com/python/blurb/pull/16.

## 1.3.0

* Add support for Python 3.13 by @hugovk in https://github.com/python/blurb/pull/26
Expand Down
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,29 @@ 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 lib
```

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
Expand Down
228 changes: 210 additions & 18 deletions src/blurb/blurb.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@
# 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 = """

Expand Down Expand Up @@ -806,7 +809,14 @@
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}]")

Check warning on line 813 in src/blurb/blurb.py

View check run for this annotation

Codecov / codecov/patch

src/blurb/blurb.py#L812-L813

Added lines #L812 - L813 were not covered by tests
else:
if p.default is None:
metavar = f'{name.upper()}'

Check warning on line 816 in src/blurb/blurb.py

View check run for this annotation

Codecov / codecov/patch

src/blurb/blurb.py#L815-L816

Added lines #L815 - L816 were not covered by tests
else:
metavar = f'{name.upper()}[={p.default}]'
options.append(f" [-{short_option}|--{name} {metavar}]")

Check warning on line 819 in src/blurb/blurb.py

View check run for this annotation

Codecov / codecov/patch

src/blurb/blurb.py#L818-L819

Added lines #L818 - L819 were not covered by tests
elif p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
positionals.append(" ")
has_default = (p.default != inspect._empty)
Expand Down Expand Up @@ -887,10 +897,175 @@
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)


# 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<SEP>?and<SEP>?Builtins' where <SEP> 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:
# ' ' 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}$'
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 _find_smart_matches(section):
# '_', '-' and ' ' are the allowed (user) whitespace separators
sanitized = re.sub(r'[_\- ]', ' ', section).strip()
if not sanitized:
return []

matches = []
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):
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 flattened section name
normalized_prefix = ''.join(section_words).lower()
for section_name, normalized in _section_names_lower_nosep.items():
if normalized.startswith(normalized_prefix):
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"
f'{sections_table}')

if len(matches) > 1:
multiple_matches = ', '.join(map(repr, sorted(matches)))
sys.exit(f"More than one match for: {raw_section!r}\n"
f"Matches: {multiple_matches}")

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!")

Check warning on line 1028 in src/blurb/blurb.py

View check run for this annotation

Codecov / codecov/patch

src/blurb/blurb.py#L1028

Added line #L1028 was not covered by tests
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 name (case insenstitive), e.g.:

blurb add -s 'Library'

# or using a partial matching
blurb add -s lib

The known sections names are defined as follows and
spaces in names can be substituted for underscores:

{sections}
"""

editor = find_editor()
Expand All @@ -901,20 +1076,8 @@

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)

Check warning on line 1080 in src/blurb/blurb.py

View check run for this annotation

Codecov / codecov/patch

src/blurb/blurb.py#L1079-L1080

Added lines #L1079 - L1080 were not covered by tests

init_tmp_with_template()

Expand Down Expand Up @@ -965,6 +1128,17 @@
print("Ready for commit.")


assert sections, 'sections is empty'
_sec_name_width = 2 + max(map(len, sections))
_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_row_rule, sections_table, _sec_row_rule))
del _sec_row_rule
add.__doc__ = add.__doc__.format(sections=sections_table)


@subcommand
def release(version):
Expand Down Expand Up @@ -1239,25 +1413,39 @@
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
Expand All @@ -1267,8 +1455,12 @@
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"must be followed by an option argument")

sys.exit(fn(*filtered_args, **kwargs))
except TypeError as e:
Expand Down
9 changes: 8 additions & 1 deletion tests/test_blurb.py
Original file line number Diff line number Diff line change
@@ -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",
)
Expand Down
Loading