From bcba076665b17942af6b525a3da50f4f95765098 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Thu, 22 Feb 2018 16:27:07 -0800 Subject: [PATCH 01/15] Untested start to the changelog generator --- news/1 Enhancements/README.md | 1 + news/2 Fixes/README.md | 1 + news/3 Code Health/README.md | 1 + news/README.md | 33 +++++++++++++++ news/announce.py | 77 +++++++++++++++++++++++++++++++++++ 5 files changed, 113 insertions(+) create mode 100644 news/1 Enhancements/README.md create mode 100644 news/2 Fixes/README.md create mode 100644 news/3 Code Health/README.md create mode 100644 news/README.md create mode 100644 news/announce.py diff --git a/news/1 Enhancements/README.md b/news/1 Enhancements/README.md new file mode 100644 index 000000000000..ecc51777759d --- /dev/null +++ b/news/1 Enhancements/README.md @@ -0,0 +1 @@ +Changes that add new features. diff --git a/news/2 Fixes/README.md b/news/2 Fixes/README.md new file mode 100644 index 000000000000..cc5e1020961d --- /dev/null +++ b/news/2 Fixes/README.md @@ -0,0 +1 @@ +Changes that fix broken behaviour. diff --git a/news/3 Code Health/README.md b/news/3 Code Health/README.md new file mode 100644 index 000000000000..10619f41f3a4 --- /dev/null +++ b/news/3 Code Health/README.md @@ -0,0 +1 @@ +Changes that should not be user-facing. diff --git a/news/README.md b/news/README.md new file mode 100644 index 000000000000..728be11e8588 --- /dev/null +++ b/news/README.md @@ -0,0 +1,33 @@ +# News + +Our changelog is automatically generate from individual news entries. +This alleviates the burden of having to go back and try to figure out +what changed in a release. It also helps tie pull requests back to the +issue(s) it addresses. + +## Entries + +Each news entries is represented by a Markdown file that contains the +relevant details of what changed. The file name of the news entry is +the issue that corresponds to the change along with an option nonce in +case a single issue corresponds to multiple changes. The directory +the news entry is saved in specifies what section of the changelog the +change corresponds to. + +As an example, a change corresponding to a bug reported in issue #42 +would be saved in the `1 Fixes` directory and named `42.md` +(or `42-nonce_value.md` if there was a need for multiple entries +regarding issue #42) and could contain the following: + +```markdown +[Answer](https://en.wikipedia.org/wiki/42_(number)#The_Hitchhiker's_Guide_to_the_Galaxy) to the Ultimate Question of +Life, the Universe, and Everything! +``` + +This would then be made into an entry in the changelog that was in the +`Fixes` section, contained the details as found in the file, and tied +to issue #42. + +## Generating the changelog + +XXX diff --git a/news/announce.py b/news/announce.py new file mode 100644 index 000000000000..036c86ee22bc --- /dev/null +++ b/news/announce.py @@ -0,0 +1,77 @@ +"""Generate the changelog.""" +import operator +import pathlib +import re + + +FILENAME_RE = re.compile(r"(?P\d+)(?P-\S+)?\.md") +ISSUE_URL = "https://github.com/Microsoft/vscode-python/issues/{issue}" +ENTRY_TEMPLATE = "- {entry} ([#{issue}]({issue_url}))" +SECTION_DEPTH = "##" + + +def news_entries(directory): + """Iterate over the news entries in the directory.""" + for path in directory.iterdir(): + if path.name == 'README.md': + continue + match = FILENAME_RE.match(path.name) + if match is None: + raise ValueError(f'{path} has a bad file name') + issue = match.group('issue') + entry = path.read_text("utf-8") + # I want dataclasses! + yield issue, entry + + +def sections(directory): + """Iterate over the sections.""" + found = [] + for path in directory.iterdir(): + if not path.is_dir(): + continue + position, _, title = path.name.partition(' ') + found.append((int(position), title, path)) + ordered_found = sorted(found, key=operator.itemgetter(0)) + # I want dataclasses! + yield from (section[1:] for section in ordered_found) + + +def gather(directory): + """Gather all the entries together.""" + data = [] + for name, path in sections(directory): + # I want dataclasses! + data.append((name, news_entries(path))) + return data + + +def entry_markdown(entry): + """Generate the Markdown for the specified entry.""" + issue_url = ISSUE_URL.format(issue=entry[0]) + return ENTRY_TEMPLATE.format(entry=entry[1], issue=entry[0], + issue_url=issue_url) + + +def changelog_markdown(data): + """Generate the Markdown for the release.""" + changelog = [] + for section, entries in data: + changelog.append(f"{SECTION_DEPTH} {section}") + changelog.append("") + changelog.extend(map(entry_markdown, entries)) + changelog.append("") + return "\n".join(changelog) + + +def main(directory): + # XXX Dry-run + # XXX Final + # XXX Directory containing sections + # XXX Location of package.json + data = gather(directory) + print(changelog_markdown(data)) + + +if __name__ == '__main__': + main(pathlib.Path(__file__).parent) From fde56083215c2cf7409d81b4dae51d475ea1cf93 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Thu, 22 Feb 2018 16:29:01 -0800 Subject: [PATCH 02/15] Add some notes --- news/announce.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/news/announce.py b/news/announce.py index 036c86ee22bc..20feeaa67bb9 100644 --- a/news/announce.py +++ b/news/announce.py @@ -65,8 +65,8 @@ def changelog_markdown(data): def main(directory): - # XXX Dry-run - # XXX Final + # XXX Dry-run (validate, but don't print anything) + # XXX Final (`git rm` news entry files) # XXX Directory containing sections # XXX Location of package.json data = gather(directory) From 239a72ca63405986f3079137bb0e510623c443b6 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Fri, 23 Feb 2018 12:01:18 -0800 Subject: [PATCH 03/15] Ignore virtual environments under .venv --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 593f771cfab8..0335c480b8bf 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ npm-debug.log !yarn.lock coverage/ .vscode-test/** +.venv From 1c991458e06ab608f2a306ea6107746e80f04123 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Fri, 23 Feb 2018 12:01:55 -0800 Subject: [PATCH 04/15] Add a CLI --- news/announce.py | 45 +++++++++++++++++++++++++++++++------------ news/requirements.txt | 1 + 2 files changed, 34 insertions(+), 12 deletions(-) create mode 100644 news/requirements.txt diff --git a/news/announce.py b/news/announce.py index 20feeaa67bb9..d1d6dc66cc5d 100644 --- a/news/announce.py +++ b/news/announce.py @@ -1,8 +1,11 @@ """Generate the changelog.""" +import enum import operator import pathlib import re +import click + FILENAME_RE = re.compile(r"(?P\d+)(?P-\S+)?\.md") ISSUE_URL = "https://github.com/Microsoft/vscode-python/issues/{issue}" @@ -10,7 +13,7 @@ SECTION_DEPTH = "##" -def news_entries(directory): +def news_entries(directory, *, cleanup=False): """Iterate over the news entries in the directory.""" for path in directory.iterdir(): if path.name == 'README.md': @@ -18,10 +21,11 @@ def news_entries(directory): match = FILENAME_RE.match(path.name) if match is None: raise ValueError(f'{path} has a bad file name') - issue = match.group('issue') + issue = int(match.group('issue')) entry = path.read_text("utf-8") # I want dataclasses! yield issue, entry + # XXX if cleanup: `git rm` the file def sections(directory): @@ -37,12 +41,12 @@ def sections(directory): yield from (section[1:] for section in ordered_found) -def gather(directory): +def gather(directory, *, cleanup=False): """Gather all the entries together.""" data = [] for name, path in sections(directory): # I want dataclasses! - data.append((name, news_entries(path))) + data.append((name, news_entries(path, cleanup=cleanup))) return data @@ -64,14 +68,31 @@ def changelog_markdown(data): return "\n".join(changelog) -def main(directory): - # XXX Dry-run (validate, but don't print anything) - # XXX Final (`git rm` news entry files) - # XXX Directory containing sections - # XXX Location of package.json - data = gather(directory) - print(changelog_markdown(data)) +class RunType(enum.Enum): + + """Possible run-time options.""" + + dry_run = 0 + interim = 1 + final = 2 + + +@click.command() +@click.option('--dry-run', 'run_type', flag_value=RunType.dry_run, + help='validate input') +@click.option('--interim', 'run_type', flag_value=RunType.interim, default=True, + help='generate Markdown') +@click.option('--final', 'run_type', flag_value=RunType.final, + help='generate Markdown & `git rm` news files') +@click.argument('directory', default=pathlib.Path(__file__).parent, + type=click.Path(exists=True, file_okay=False)) +def main(run_type, directory): + cleanup = run_type == RunType.final + data = gather(directory, cleanup=cleanup) + markdown = changelog_markdown(data) + if run_type != RunType.dry_run: + print(markdown) if __name__ == '__main__': - main(pathlib.Path(__file__).parent) + main() diff --git a/news/requirements.txt b/news/requirements.txt new file mode 100644 index 000000000000..d28251baad28 --- /dev/null +++ b/news/requirements.txt @@ -0,0 +1 @@ +click~=6.7.0 From a17d4baeb2b7b87edfa74c33fb813591673e7c40 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Fri, 23 Feb 2018 13:23:41 -0800 Subject: [PATCH 05/15] Detect malformed directory names --- news/announce.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/news/announce.py b/news/announce.py index d1d6dc66cc5d..5774fe495488 100644 --- a/news/announce.py +++ b/news/announce.py @@ -34,6 +34,8 @@ def sections(directory): for path in directory.iterdir(): if not path.is_dir(): continue + if ' ' not in path.name: + raise ValueError('directory is missing position part') position, _, title = path.name.partition(' ') found.append((int(position), title, path)) ordered_found = sorted(found, key=operator.itemgetter(0)) From 50ce2e7e5d41a32abf2a79f62a420cbf9522890b Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Fri, 23 Feb 2018 13:24:13 -0800 Subject: [PATCH 06/15] Add tests --- news/requirements.txt | 1 + news/test_announce.py | 104 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 news/test_announce.py diff --git a/news/requirements.txt b/news/requirements.txt index d28251baad28..d47e9da45e5a 100644 --- a/news/requirements.txt +++ b/news/requirements.txt @@ -1 +1,2 @@ click~=6.7.0 +pytest~=3.4.1 diff --git a/news/test_announce.py b/news/test_announce.py new file mode 100644 index 000000000000..8dc101b7ee84 --- /dev/null +++ b/news/test_announce.py @@ -0,0 +1,104 @@ +import pathlib + +import pytest + +import announce as ann + + +@pytest.fixture +def directory(tmpdir): + return pathlib.Path(tmpdir) + + +def test_news_entry_formatting(directory): + issue = 42 + normal_entry = directory / f'{issue}.md' + nonce_entry = directory / f'{issue}-nonce.md' + body = 'Hello, world!' + normal_entry.write_text(body, encoding='utf-8') + nonce_entry.write_text(body, encoding='utf-8') + results = list(ann.news_entries(directory)) + assert len(results) == 2 + for found_issue, found_body in results: + assert found_issue == issue + assert found_body == body + + +def test_bad_news_entry_file_name(directory): + entry = directory / 'bunk.md' + entry.write_text('Hello, world!') + with pytest.raises(ValueError): + list(ann.news_entries(directory)) + +def test_news_entry_README_skipping(directory): + entry = directory / 'README.md' + entry.write_text('Hello, world!') + assert len(list(ann.news_entries(directory))) == 0 + + +def test_sections_sorting(directory): + dir2 = directory / '2 Hello' + dir1 = directory / '1 World' + dir2.mkdir() + dir1.mkdir() + results = list(ann.sections(directory)) + assert [found[0] for found in results] == ['World', 'Hello'] + + +def test_sections_naming(directory): + (directory / 'Hello').mkdir() + with pytest.raises(ValueError): + list(ann.sections(directory)) + + +def test_gather(directory): + fixes = directory / '2 Fixes' + fixes.mkdir() + fix1 = fixes / '1.md' + fix1.write_text('Fix 1', encoding='utf-8') + fix2 = fixes / '3.md' + fix2.write_text('Fix 2', encoding='utf-8') + enhancements = directory / '1 Enhancements' + enhancements.mkdir() + enhancement1 = enhancements / '2.md' + enhancement1.write_text('Enhancement 1', encoding='utf-8') + enhancement2 = enhancements / '4.md' + enhancement2.write_text('Enhancement 2', encoding='utf-8') + results = ann.gather(directory) + assert len(results) == 2 + section, entries = results[0] + assert section == 'Enhancements' + entries_dict = dict(entries) + assert len(entries_dict) == 2 + assert entries_dict[2] == 'Enhancement 1' + assert entries_dict[4] == 'Enhancement 2' + section, entries = results[1] + assert section == 'Fixes' + entries_dict = dict(entries) + assert len(entries_dict) == 2 + assert entries_dict[1] == 'Fix 1' + assert entries_dict[3] == 'Fix 2' + +def test_entry_markdown(): + markdown = ann.entry_markdown((42, 'Hello, world!')) + assert '42' in markdown + assert 'Hello, world!' in markdown + assert 'https://github.com/Microsoft/vscode-python/issues/42' in markdown + + +def test_changelog_markdown(): + data = [('Enhancements', [(2, 'Enhancement 1'), (4, 'Enhancement 2')]), + ('Fixes', [(1, 'Fix 1'), (3, 'Fix 2')])] + markdown = ann.changelog_markdown(data) + assert '## Enhancements' in markdown + assert '## Fixes' in markdown + assert '1' in markdown + assert 'Fix 1' in markdown + assert '2' in markdown + assert 'Enhancement 1' in markdown + assert 'https://github.com/Microsoft/vscode-python/issues/2' in markdown + assert '3' in markdown + assert 'Fix 2' in markdown + assert 'https://github.com/Microsoft/vscode-python/issues/3' in markdown + assert '4' in markdown + assert 'Enhancement 2' in markdown From 0cefbf37324d241d88901e808375635c0113ea8e Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Fri, 23 Feb 2018 13:24:39 -0800 Subject: [PATCH 07/15] Validate news entries as part of CI --- .travis.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index da3ee2fc8b6b..6053fb50f743 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,9 +4,9 @@ cache: pip matrix: include: - os: linux - python: 2.7 + python: "2.7" - os: linux - python: 3.6 + python: "3.6-dev" before_install: | if [ $TRAVIS_OS_NAME == "linux" ]; then export CXX="g++-4.9" CC="gcc-4.9" DISPLAY=:99.0; @@ -47,6 +47,9 @@ script: - if [ $TRAVIS_UPLOAD_COVERAGE == "true" ]; then bash <(curl -s https://codecov.io/bash); fi + if [ $TRAVIS_PYTHON_VERSION != "2.7"]; then + python3 news/announce.py --dry-run + fi after_success: - if [ $AZURE_STORAGE_ACCOUNT ]; then yarn run clean; From 3a89951ed201c4f6caf709ebe564484f835b9eb9 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Fri, 23 Feb 2018 14:16:14 -0800 Subject: [PATCH 08/15] git-rm files when doing a final run --- news/announce.py | 16 +++++++++++++--- news/test_announce.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/news/announce.py b/news/announce.py index 5774fe495488..0bca67b6e05c 100644 --- a/news/announce.py +++ b/news/announce.py @@ -1,8 +1,10 @@ """Generate the changelog.""" import enum import operator +import os import pathlib import re +import subprocess import click @@ -13,8 +15,15 @@ SECTION_DEPTH = "##" +def git_rm(path): + """Run git-rm on the path.""" + status = subprocess.run(['git', 'rm', os.fspath(path.resolve())], + shell=True) + status.check_returncode() + + def news_entries(directory, *, cleanup=False): - """Iterate over the news entries in the directory.""" + """Yield news entries in the directory.""" for path in directory.iterdir(): if path.name == 'README.md': continue @@ -23,13 +32,14 @@ def news_entries(directory, *, cleanup=False): raise ValueError(f'{path} has a bad file name') issue = int(match.group('issue')) entry = path.read_text("utf-8") + if cleanup: + git_rm(path) # I want dataclasses! yield issue, entry - # XXX if cleanup: `git rm` the file def sections(directory): - """Iterate over the sections.""" + """Yield the sections in their appropriate order.""" found = [] for path in directory.iterdir(): if not path.is_dir(): diff --git a/news/test_announce.py b/news/test_announce.py index 8dc101b7ee84..005c66b83c60 100644 --- a/news/test_announce.py +++ b/news/test_announce.py @@ -7,6 +7,7 @@ @pytest.fixture def directory(tmpdir): + """Fixture to create a temp directory wrapped in a pathlib.Path object.""" return pathlib.Path(tmpdir) @@ -36,6 +37,21 @@ def test_news_entry_README_skipping(directory): assert len(list(ann.news_entries(directory))) == 0 +def test_news_entry_cleanup(directory, monkeypatch): + rm_path = None + def fake_git_rm(path): + nonlocal rm_path + rm_path = path + monkeypatch.setattr(ann, 'git_rm', fake_git_rm) + issue = 42 + normal_entry = directory / f'{issue}.md' + body = 'Hello, world!' + normal_entry.write_text(body, encoding='utf-8') + results = list(ann.news_entries(directory, cleanup=True)) + assert len(results) == 1 + assert normal_entry == rm_path + + def test_sections_sorting(directory): dir2 = directory / '2 Hello' dir1 = directory / '1 World' @@ -79,6 +95,23 @@ def test_gather(directory): assert entries_dict[1] == 'Fix 1' assert entries_dict[3] == 'Fix 2' + +def test_gather_cleanup(directory, monkeypatch): + rm_path = None + def fake_git_rm(path): + nonlocal rm_path + rm_path = path + monkeypatch.setattr(ann, 'git_rm', fake_git_rm) + fixes = directory / '2 Fixes' + fixes.mkdir() + fix1 = fixes / '1.md' + fix1.write_text('Fix 1', encoding='utf-8') + results = ann.gather(directory, cleanup=True) + assert len(results) == 1 + section, entries = results.pop() + assert len(list(entries)) == 1 # Exhaust generator. + assert rm_path == fix1 + def test_entry_markdown(): markdown = ann.entry_markdown((42, 'Hello, world!')) assert '42' in markdown From 190e82eaf18cfa40b64296b7819e7db32004003f Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Fri, 23 Feb 2018 14:44:59 -0800 Subject: [PATCH 09/15] Flesh out README --- news/README.md | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/news/README.md b/news/README.md index 728be11e8588..8b8350f5ddec 100644 --- a/news/README.md +++ b/news/README.md @@ -20,8 +20,8 @@ would be saved in the `1 Fixes` directory and named `42.md` regarding issue #42) and could contain the following: ```markdown -[Answer](https://en.wikipedia.org/wiki/42_(number)#The_Hitchhiker's_Guide_to_the_Galaxy) to the Ultimate Question of -Life, the Universe, and Everything! +[Answer](https://en.wikipedia.org/wiki/42_(number)) +to the Ultimate Question of Life, the Universe, and Everything! ``` This would then be made into an entry in the changelog that was in the @@ -30,4 +30,27 @@ to issue #42. ## Generating the changelog -XXX +The `announce` script can do 3 possible things: + +1. Validate that the changelog _could_ be successfully generated +2. Generate the changelog entry +3. Generate the changelog entry **and** `git-rm` the news entries + +The first option is used in CI to make sure any added news entries +will not cause trouble at release time. The second option is for +filling in the changelog for interim releases, e.g. a beta release. +The third option is for final releases that get published to the +[VS Code marketplace](https://marketplace.visualstudio.com/VSCode). + +For options 2 & 3, the changelog is sent to stdout so it's temporarily +saved to a file: + +```sh +python3 news/announce.py > entry.txt +``` + +It can also be redirected to a file or into an editor buffer, e.g.: + +```sh +python3 news/announce.py --final | code-insiders +``` From 06e9bd5f5b97ec6c4858d8ff6b333c17c1e243d6 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Mon, 26 Feb 2018 10:59:28 -0800 Subject: [PATCH 10/15] Fix a typo --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6053fb50f743..ca5866aecc13 100644 --- a/.travis.yml +++ b/.travis.yml @@ -47,7 +47,7 @@ script: - if [ $TRAVIS_UPLOAD_COVERAGE == "true" ]; then bash <(curl -s https://codecov.io/bash); fi - if [ $TRAVIS_PYTHON_VERSION != "2.7"]; then + - if [ $TRAVIS_PYTHON_VERSION != "2.7" ]; then python3 news/announce.py --dry-run fi after_success: From 18b5f65d34fdf50d1fe12a215387ffff56beb43e Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Mon, 26 Feb 2018 16:09:27 -0800 Subject: [PATCH 11/15] Try to make bash happy yet again --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index ca5866aecc13..16f55bae8308 100644 --- a/.travis.yml +++ b/.travis.yml @@ -47,8 +47,8 @@ script: - if [ $TRAVIS_UPLOAD_COVERAGE == "true" ]; then bash <(curl -s https://codecov.io/bash); fi - - if [ $TRAVIS_PYTHON_VERSION != "2.7" ]; then - python3 news/announce.py --dry-run + - if [ "$TRAVIS_PYTHON_VERSION" != "2.7" ]; then + python3 news/announce.py --dry-run; fi after_success: - if [ $AZURE_STORAGE_ACCOUNT ]; then From 178b18334f1742f779cb6992a9b03f4882646819 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Mon, 26 Feb 2018 17:06:50 -0800 Subject: [PATCH 12/15] Install the dependencies --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 16f55bae8308..4b4a8bddda4c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -48,6 +48,7 @@ script: bash <(curl -s https://codecov.io/bash); fi - if [ "$TRAVIS_PYTHON_VERSION" != "2.7" ]; then + python3 -m pip install --user --upgrade -r news/requirements.txt; python3 news/announce.py --dry-run; fi after_success: From 8b60152a253d24bc7b596fb4980cfd65788037bb Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Tue, 27 Feb 2018 12:49:04 -0800 Subject: [PATCH 13/15] Travis is already working in a virtual env, so no --user necessary --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4b4a8bddda4c..c3a4faf04958 100644 --- a/.travis.yml +++ b/.travis.yml @@ -48,7 +48,7 @@ script: bash <(curl -s https://codecov.io/bash); fi - if [ "$TRAVIS_PYTHON_VERSION" != "2.7" ]; then - python3 -m pip install --user --upgrade -r news/requirements.txt; + python3 -m pip install --upgrade -r news/requirements.txt; python3 news/announce.py --dry-run; fi after_success: From 78d1ae09c9144a07676da142d0651165709e1837 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Tue, 27 Feb 2018 13:40:53 -0800 Subject: [PATCH 14/15] Address Eric's comments --- news/announce.py | 67 ++++++++++++++++++++------------ news/test_announce.py | 89 +++++++++++++++++++------------------------ 2 files changed, 82 insertions(+), 74 deletions(-) diff --git a/news/announce.py b/news/announce.py index 0bca67b6e05c..424eb037717f 100644 --- a/news/announce.py +++ b/news/announce.py @@ -5,6 +5,7 @@ import pathlib import re import subprocess +import types import click @@ -12,17 +13,17 @@ FILENAME_RE = re.compile(r"(?P\d+)(?P-\S+)?\.md") ISSUE_URL = "https://github.com/Microsoft/vscode-python/issues/{issue}" ENTRY_TEMPLATE = "- {entry} ([#{issue}]({issue_url}))" -SECTION_DEPTH = "##" +SECTION_DEPTH = "###" -def git_rm(path): - """Run git-rm on the path.""" - status = subprocess.run(['git', 'rm', os.fspath(path.resolve())], - shell=True) - status.check_returncode() +def NewsEntry(issue_number, description, path): + """Construct a data object for a news entry.""" + # TODO: replace with a dataclass in Python 3.7. + return types.SimpleNamespace(issue_number=issue_number, + description=description, path=path) -def news_entries(directory, *, cleanup=False): +def news_entries(directory): """Yield news entries in the directory.""" for path in directory.iterdir(): if path.name == 'README.md': @@ -32,10 +33,13 @@ def news_entries(directory, *, cleanup=False): raise ValueError(f'{path} has a bad file name') issue = int(match.group('issue')) entry = path.read_text("utf-8") - if cleanup: - git_rm(path) - # I want dataclasses! - yield issue, entry + yield NewsEntry(issue, entry, path) + + +def SectionTitle(index, title, path): + """Create a data object for a section of the changelog.""" + # TODO: replace with a dataclass in Python 3.7. + return types.SimpleNamespace(index=index, title=title, path=path) def sections(directory): @@ -44,28 +48,26 @@ def sections(directory): for path in directory.iterdir(): if not path.is_dir(): continue - if ' ' not in path.name: + position, sep, title = path.name.partition(' ') + if not sep: raise ValueError('directory is missing position part') - position, _, title = path.name.partition(' ') - found.append((int(position), title, path)) - ordered_found = sorted(found, key=operator.itemgetter(0)) - # I want dataclasses! - yield from (section[1:] for section in ordered_found) + found.append(SectionTitle(int(position), title, path)) + return sorted(found, key=operator.attrgetter('index')) -def gather(directory, *, cleanup=False): +def gather(directory): """Gather all the entries together.""" data = [] - for name, path in sections(directory): - # I want dataclasses! - data.append((name, news_entries(path, cleanup=cleanup))) + for section in sections(directory): + data.append((section, list(news_entries(section.path)))) return data def entry_markdown(entry): """Generate the Markdown for the specified entry.""" - issue_url = ISSUE_URL.format(issue=entry[0]) - return ENTRY_TEMPLATE.format(entry=entry[1], issue=entry[0], + issue_url = ISSUE_URL.format(issue=entry.issue_number) + return ENTRY_TEMPLATE.format(entry=entry.description, + issue=entry.issue_number, issue_url=issue_url) @@ -73,15 +75,28 @@ def changelog_markdown(data): """Generate the Markdown for the release.""" changelog = [] for section, entries in data: - changelog.append(f"{SECTION_DEPTH} {section}") + changelog.append(f"{SECTION_DEPTH} {section.title}") changelog.append("") changelog.extend(map(entry_markdown, entries)) changelog.append("") return "\n".join(changelog) -class RunType(enum.Enum): +def git_rm(path): + """Run git-rm on the path.""" + status = subprocess.run(['git', 'rm', os.fspath(path.resolve())], + shell=True) + status.check_returncode() + +def cleanup(data): + """Remove news entries from git and disk.""" + for section, entries in data: + for entry in entries: + git_rm(entry.path) + + +class RunType(enum.Enum): """Possible run-time options.""" dry_run = 0 @@ -104,6 +119,8 @@ def main(run_type, directory): markdown = changelog_markdown(data) if run_type != RunType.dry_run: print(markdown) + if run_type == RunType.final: + cleanup(data) if __name__ == '__main__': diff --git a/news/test_announce.py b/news/test_announce.py index 005c66b83c60..6c9aa256bdc1 100644 --- a/news/test_announce.py +++ b/news/test_announce.py @@ -20,9 +20,9 @@ def test_news_entry_formatting(directory): nonce_entry.write_text(body, encoding='utf-8') results = list(ann.news_entries(directory)) assert len(results) == 2 - for found_issue, found_body in results: - assert found_issue == issue - assert found_body == body + for result in results: + assert result.issue_number == issue + assert result.description == body def test_bad_news_entry_file_name(directory): @@ -37,28 +37,13 @@ def test_news_entry_README_skipping(directory): assert len(list(ann.news_entries(directory))) == 0 -def test_news_entry_cleanup(directory, monkeypatch): - rm_path = None - def fake_git_rm(path): - nonlocal rm_path - rm_path = path - monkeypatch.setattr(ann, 'git_rm', fake_git_rm) - issue = 42 - normal_entry = directory / f'{issue}.md' - body = 'Hello, world!' - normal_entry.write_text(body, encoding='utf-8') - results = list(ann.news_entries(directory, cleanup=True)) - assert len(results) == 1 - assert normal_entry == rm_path - - def test_sections_sorting(directory): dir2 = directory / '2 Hello' dir1 = directory / '1 World' dir2.mkdir() dir1.mkdir() results = list(ann.sections(directory)) - assert [found[0] for found in results] == ['World', 'Hello'] + assert [found.title for found in results] == ['World', 'Hello'] def test_sections_naming(directory): @@ -83,48 +68,36 @@ def test_gather(directory): results = ann.gather(directory) assert len(results) == 2 section, entries = results[0] - assert section == 'Enhancements' - entries_dict = dict(entries) - assert len(entries_dict) == 2 - assert entries_dict[2] == 'Enhancement 1' - assert entries_dict[4] == 'Enhancement 2' + assert section.title == 'Enhancements' + assert len(entries) == 2 + assert entries[0].description == 'Enhancement 1' + assert entries[1].description == 'Enhancement 2' section, entries = results[1] - assert section == 'Fixes' - entries_dict = dict(entries) - assert len(entries_dict) == 2 - assert entries_dict[1] == 'Fix 1' - assert entries_dict[3] == 'Fix 2' + assert len(entries) == 2 + assert section.title == 'Fixes' + assert entries[0].description == 'Fix 1' + assert entries[1].description == 'Fix 2' + -def test_gather_cleanup(directory, monkeypatch): - rm_path = None - def fake_git_rm(path): - nonlocal rm_path - rm_path = path - monkeypatch.setattr(ann, 'git_rm', fake_git_rm) - fixes = directory / '2 Fixes' - fixes.mkdir() - fix1 = fixes / '1.md' - fix1.write_text('Fix 1', encoding='utf-8') - results = ann.gather(directory, cleanup=True) - assert len(results) == 1 - section, entries = results.pop() - assert len(list(entries)) == 1 # Exhaust generator. - assert rm_path == fix1 def test_entry_markdown(): - markdown = ann.entry_markdown((42, 'Hello, world!')) + markdown = ann.entry_markdown(ann.NewsEntry(42, 'Hello, world!', None)) assert '42' in markdown assert 'Hello, world!' in markdown assert 'https://github.com/Microsoft/vscode-python/issues/42' in markdown def test_changelog_markdown(): - data = [('Enhancements', [(2, 'Enhancement 1'), (4, 'Enhancement 2')]), - ('Fixes', [(1, 'Fix 1'), (3, 'Fix 2')])] + data = [(ann.SectionTitle(1, 'Enhancements', None), + [ann.NewsEntry(2, 'Enhancement 1', None), + ann.NewsEntry(4, 'Enhancement 2', None)]), + (ann.SectionTitle(1, 'Fixes', None), + [ann.NewsEntry(1, 'Fix 1', None), + ann.NewsEntry(3, 'Fix 2', None)])] markdown = ann.changelog_markdown(data) - assert '## Enhancements' in markdown - assert '## Fixes' in markdown + assert '### Enhancements' in markdown + assert '### Fixes' in markdown assert '1' in markdown assert 'Fix 1' in markdown assert '2' in markdown @@ -135,3 +108,21 @@ def test_changelog_markdown(): assert 'https://github.com/Microsoft/vscode-python/issues/3' in markdown assert '4' in markdown assert 'Enhancement 2' in markdown + + +def test_cleanup(directory, monkeypatch): + rm_path = None + def fake_git_rm(path): + nonlocal rm_path + rm_path = path + monkeypatch.setattr(ann, 'git_rm', fake_git_rm) + fixes = directory / '2 Fixes' + fixes.mkdir() + fix1 = fixes / '1.md' + fix1.write_text('Fix 1', encoding='utf-8') + results = ann.gather(directory) + assert len(results) == 1 + ann.cleanup(results) + section, entries = results.pop() + assert len(entries) == 1 + assert rm_path == entries[0].path From a2e4e820ce0328656cba85d5e9e939642b07c241 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Tue, 27 Feb 2018 14:06:45 -0800 Subject: [PATCH 15/15] Fix the CLI --- news/announce.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/news/announce.py b/news/announce.py index 424eb037717f..b21b401c8f5c 100644 --- a/news/announce.py +++ b/news/announce.py @@ -114,8 +114,7 @@ class RunType(enum.Enum): @click.argument('directory', default=pathlib.Path(__file__).parent, type=click.Path(exists=True, file_okay=False)) def main(run_type, directory): - cleanup = run_type == RunType.final - data = gather(directory, cleanup=cleanup) + data = gather(directory) markdown = changelog_markdown(data) if run_type != RunType.dry_run: print(markdown)