python-boto3/scripts/new-change

216 lines
6.9 KiB
Plaintext
Raw Normal View History

2016-05-22 04:03:29 +02:00
#!/usr/bin/env python
"""Generate a new changelog entry.
Usage
=====
To generate a new changelog entry::
scripts/new-change
This will open up a file in your editor (via the ``EDITOR`` env var).
You'll see this template::
# Type should be one of: feature, bugfix
type:
# Category is the high level feature area.
# This can be a service identifier (e.g ``s3``),
# or something like: Paginator.
category:
# A brief description of the change. You can
# use github style references to issues such as
# "fixes #489", "boto/boto3#100", etc. These
# will get automatically replaced with the correct
# link.
description:
Fill in the appropriate values, save and exit the editor.
Make sure to commit these changes as part of your pull request.
2021-09-22 18:34:33 +02:00
If, when your editor is open, you decide don't want to add a changelog
2016-05-22 04:03:29 +02:00
entry, save an empty file and no entry will be generated.
You can then use the ``scripts/gen-changelog`` to generate the
CHANGELOG.rst file.
"""
import os
import re
import sys
import json
import string
import random
import tempfile
import subprocess
import argparse
2016-11-09 01:23:44 +01:00
VALID_CHARS = set(string.ascii_letters + string.digits)
2016-05-22 04:03:29 +02:00
CHANGES_DIR = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
'.changes'
)
TEMPLATE = """\
2018-07-11 07:39:36 +02:00
# Type should be one of: feature, bugfix, enhancement, api-change
# feature: A larger feature or change in behavior, usually resulting in a
# minor version bump.
# bugfix: Fixing a bug in an existing code path.
# enhancment: Small change to an underlying implementation detail.
# api-change: Changes to a modeled API.
2016-05-22 04:03:29 +02:00
type: {change_type}
# Category is the high level feature area.
# This can be a service identifier (e.g ``s3``),
# or something like: Paginator.
category: {category}
# A brief description of the change. You can
# use github style references to issues such as
# "fixes #489", "boto/boto3#100", etc. These
# will get automatically replaced with the correct
# link.
description: {description}
"""
def new_changelog_entry(args):
# Changelog values come from one of two places.
# Either all values are provided on the command line,
# or we open a text editor and let the user provide
# enter their values.
if all_values_provided(args):
parsed_values = {
'type': args.change_type,
'category': args.category,
'description': args.description,
}
else:
parsed_values = get_values_from_editor(args)
if has_empty_values(parsed_values):
sys.stderr.write(
"Empty changelog values received, skipping entry creation.\n")
return 1
replace_issue_references(parsed_values, args.repo)
write_new_change(parsed_values)
return 0
def has_empty_values(parsed_values):
return not (parsed_values.get('type') and
parsed_values.get('category') and
parsed_values.get('description'))
def all_values_provided(args):
return args.change_type and args.category and args.description
def get_values_from_editor(args):
with tempfile.NamedTemporaryFile('w') as f:
contents = TEMPLATE.format(
change_type=args.change_type,
category=args.category,
description=args.description,
)
f.write(contents)
f.flush()
env = os.environ
editor = env.get('VISUAL', env.get('EDITOR', 'vim'))
p = subprocess.Popen('%s %s' % (editor, f.name), shell=True)
p.communicate()
with open(f.name) as f:
filled_in_contents = f.read()
parsed_values = parse_filled_in_contents(filled_in_contents)
return parsed_values
def replace_issue_references(parsed, repo_name):
description = parsed['description']
def linkify(match):
number = match.group()[1:]
return (
'`%s <https://github.com/%s/issues/%s>`__' % (
match.group(), repo_name, number))
2021-11-03 18:27:47 +01:00
new_description = re.sub(r'#\d+', linkify, description)
2016-05-22 04:03:29 +02:00
parsed['description'] = new_description
def write_new_change(parsed_values):
if not os.path.isdir(CHANGES_DIR):
os.makedirs(CHANGES_DIR)
# Assume that new changes go into the next release.
dirname = os.path.join(CHANGES_DIR, 'next-release')
if not os.path.isdir(dirname):
os.makedirs(dirname)
# Need to generate a unique filename for this change.
# We'll try a couple things until we get a unique match.
category = parsed_values['category']
short_summary = ''.join(filter(lambda x: x in VALID_CHARS, category))
filename = '{type_name}-{summary}'.format(
type_name=parsed_values['type'],
summary=short_summary)
2016-11-09 01:23:44 +01:00
possible_filename = os.path.join(
dirname, '%s-%s.json' % (filename, str(random.randint(1, 100000))))
2016-05-22 04:03:29 +02:00
while os.path.isfile(possible_filename):
possible_filename = os.path.join(
dirname, '%s-%s.json' % (filename, str(random.randint(1, 100000))))
with open(possible_filename, 'w') as f:
2016-11-09 01:23:44 +01:00
f.write(json.dumps(parsed_values, indent=2) + "\n")
2016-05-22 04:03:29 +02:00
def parse_filled_in_contents(contents):
"""Parse filled in file contents and returns parsed dict.
Return value will be::
{
"type": "bugfix",
"category": "category",
"description": "This is a description"
}
"""
if not contents.strip():
return {}
parsed = {}
lines = iter(contents.splitlines())
for line in lines:
line = line.strip()
if line.startswith('#'):
continue
if 'type' not in parsed and line.startswith('type:'):
parsed['type'] = line.split(':')[1].strip()
elif 'category' not in parsed and line.startswith('category:'):
parsed['category'] = line.split(':')[1].strip()
elif 'description' not in parsed and line.startswith('description:'):
# Assume that everything until the end of the file is part
# of the description, so we can break once we pull in the
# remaining lines.
first_line = line.split(':')[1].strip()
full_description = '\n'.join([first_line] + list(lines))
parsed['description'] = full_description.strip()
break
return parsed
def main():
parser = argparse.ArgumentParser()
parser.add_argument('-t', '--type', dest='change_type',
2018-07-11 07:39:36 +02:00
default='', choices=('bugfix', 'feature',
'enhancement', 'api-change'))
2016-05-22 04:03:29 +02:00
parser.add_argument('-c', '--category', dest='category',
default='')
parser.add_argument('-d', '--description', dest='description',
default='')
parser.add_argument('-r', '--repo', default='boto/boto3',
help='Optional repo name, e.g: boto/boto3')
args = parser.parse_args()
sys.exit(new_changelog_entry(args))
if __name__ == '__main__':
main()