diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f7393f..99bab4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - `keepachangelog.release` function to guess new version number based on `Unreleased` section, update changelog and return new version number. +- `keepachangelog.to_raw_dict` function returning a raw markdown description of the release under `raw` dict. + +### Fixed +- Handle any category name. +- Add more flexibility for release format. + +### Changed +- `Unreleased` is now reported as lower cased `unreleased`. ## [0.4.0] - 2020-09-21 ### Added diff --git a/README.md b/README.md index b0432c3..000265f 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Build status Coverage Code style: black -Number of tests +Number of tests Number of downloads

diff --git a/keepachangelog/__init__.py b/keepachangelog/__init__.py index ac00948..31e7d59 100644 --- a/keepachangelog/__init__.py +++ b/keepachangelog/__init__.py @@ -1,2 +1,2 @@ from keepachangelog.version import __version__ -from keepachangelog._changelog import to_dict, release +from keepachangelog._changelog import to_dict, to_raw_dict, release diff --git a/keepachangelog/_changelog.py b/keepachangelog/_changelog.py index 69b7e3d..aa699f7 100644 --- a/keepachangelog/_changelog.py +++ b/keepachangelog/_changelog.py @@ -4,41 +4,47 @@ from keepachangelog._versioning import guess_unreleased_version -# Release pattern should match lines like: "## [0.0.1] - 2020-12-31" or ## [Unreleased] -release_pattern = re.compile(r"^## \[(.*)\](?: - (.*))?$") +def is_release(line: str) -> bool: + return line.startswith("## ") -def is_release(line: str, show_unreleased: bool) -> bool: - match = release_pattern.fullmatch(line) - if match and (not show_unreleased and match.group(1) == "Unreleased"): - return False - return match is not None - -def add_release(changes: Dict[str, dict], line: str) -> dict: - release_info = release_pattern.fullmatch(line) +def add_release(changes: Dict[str, dict], line: str, show_unreleased: bool) -> dict: + release_line = line[3:].lower().strip(" ") + # A release is separated by a space between version and release date + # Release pattern should match lines like: "[0.0.1] - 2020-12-31" or [Unreleased] + version, release_date = ( + release_line.split(" ", maxsplit=1) + if " " in release_line + else (release_line, None) + ) + if not show_unreleased and not release_date: + return {} + version = unlink(version) return changes.setdefault( - release_info.group(1), - {"version": release_info.group(1), "release_date": release_info.group(2)}, + version, + {"version": version, "release_date": extract_date(release_date)}, ) -categories = { - "### Added": "added", - "### Changed": "changed", - "### Deprecated": "deprecated", - "### Removed": "removed", - "### Fixed": "fixed", - "### Security": "security", -} +def unlink(value: str) -> str: + return value.lstrip("[").rstrip("]") + + +def extract_date(date: str) -> str: + if not date: + return date + + return date.lstrip(" -(").rstrip(" )") def is_category(line: str) -> bool: - return line in categories + return line.startswith("### ") def add_category(release: dict, line: str) -> List[str]: - return release.setdefault(categories[line], []) + category = line[4:].lower().strip(" ") + return release.setdefault(category, []) # Link pattern should match lines like: "[1.2.3]: https://github.com/user/project/releases/tag/v0.0.1" @@ -56,21 +62,38 @@ def add_information(category: List[str], line: str): def to_dict(changelog_path: str, *, show_unreleased: bool = False) -> Dict[str, dict]: changes = {} with open(changelog_path) as change_log: - release = {} + current_release = {} category = [] for line in change_log: line = line.strip(" \n") - if is_release(line, show_unreleased): - release = add_release(changes, line) + if is_release(line): + current_release = add_release(changes, line, show_unreleased) elif is_category(line): - category = add_category(release, line) + category = add_category(current_release, line) elif is_information(line): add_information(category, line) return changes +def to_raw_dict(changelog_path: str) -> Dict[str, dict]: + changes = {} + with open(changelog_path) as change_log: + current_release = {} + for line in change_log: + clean_line = line.strip(" \n") + + if is_release(clean_line): + current_release = add_release( + changes, clean_line, show_unreleased=False + ) + elif is_category(clean_line) or is_information(clean_line): + current_release["raw"] = current_release.get("raw", "") + line + + return changes + + def release(changelog_path: str) -> str: changelog = to_dict(changelog_path, show_unreleased=True) current_version, new_version = guess_unreleased_version(changelog) diff --git a/keepachangelog/_versioning.py b/keepachangelog/_versioning.py index 7645a14..be70fea 100644 --- a/keepachangelog/_versioning.py +++ b/keepachangelog/_versioning.py @@ -37,13 +37,13 @@ def bump(unreleased: dict, version: str) -> str: def actual_version(changelog: dict) -> Optional[str]: versions = sorted(changelog.keys()) current_version = versions.pop() if versions else None - while "Unreleased" == current_version: + while "unreleased" == current_version: current_version = versions.pop() if versions else None return current_version def guess_unreleased_version(changelog: dict) -> Tuple[Optional[str], str]: - unreleased = changelog.get("Unreleased", {}) + unreleased = changelog.get("unreleased", {}) if not unreleased or len(unreleased) < 3: raise Exception( "Release content must be provided within changelog Unreleased section." diff --git a/tests/test_changelog.py b/tests/test_changelog.py index 798fbaa..eaa991e 100644 --- a/tests/test_changelog.py +++ b/tests/test_changelog.py @@ -119,3 +119,63 @@ def test_changelog_with_versions_and_all_categories(changelog): "version": "1.0.0", }, } + + +def test_raw_changelog_with_versions_and_all_categories(changelog): + assert keepachangelog.to_raw_dict(changelog) == { + "1.2.0": { + "raw": """### Changed +- Release note 1. +- Release note 2. +### Added +- Enhancement 1 +- sub enhancement 1 +- sub enhancement 2 +- Enhancement 2 +### Fixed +- Bug fix 1 +- sub bug 1 +- sub bug 2 +- Bug fix 2 +### Security +- Known issue 1 +- Known issue 2 +### Deprecated +- Deprecated feature 1 +- Future removal 2 +### Removed +- Deprecated feature 2 +- Future removal 1 +""", + "release_date": "2018-06-01", + "version": "1.2.0", + }, + "1.1.0": { + "raw": """### Changed +- Enhancement 1 (1.1.0) +- sub enhancement 1 +- sub enhancement 2 +- Enhancement 2 (1.1.0) +""", + "release_date": "2018-05-31", + "version": "1.1.0", + }, + "1.0.1": { + "raw": """### Fixed +- Bug fix 1 (1.0.1) +- sub bug 1 +- sub bug 2 +- Bug fix 2 (1.0.1) +""", + "release_date": "2018-05-31", + "version": "1.0.1", + }, + "1.0.0": { + "raw": """### Deprecated +- Known issue 1 (1.0.0) +- Known issue 2 (1.0.0) +""", + "release_date": "2017-04-10", + "version": "1.0.0", + }, + } diff --git a/tests/test_changelog_no_version.py b/tests/test_changelog_no_version.py index 9c41a47..b88cbc2 100644 --- a/tests/test_changelog_no_version.py +++ b/tests/test_changelog_no_version.py @@ -17,3 +17,7 @@ def changelog(tmpdir): def test_changelog_without_versions(changelog): assert keepachangelog.to_dict(changelog) == {} + + +def test_raw_changelog_without_versions(changelog): + assert keepachangelog.to_raw_dict(changelog) == {} diff --git a/tests/test_changelog_unreleased.py b/tests/test_changelog_unreleased.py index 76950a9..1495ebe 100644 --- a/tests/test_changelog_unreleased.py +++ b/tests/test_changelog_unreleased.py @@ -76,8 +76,8 @@ def changelog(tmpdir): def test_changelog_with_versions_and_all_categories(changelog): assert keepachangelog.to_dict(changelog, show_unreleased=True) == { - "Unreleased": { - "version": "Unreleased", + "unreleased": { + "version": "unreleased", "release_date": None, "changed": ["Release note 1.", "Release note 2."], "added": [ diff --git a/tests/test_non_standard_changelog.py b/tests/test_non_standard_changelog.py new file mode 100644 index 0000000..d5c9b90 --- /dev/null +++ b/tests/test_non_standard_changelog.py @@ -0,0 +1,222 @@ +import os +import os.path + +import pytest + +import keepachangelog + + +@pytest.fixture +def changelog(tmpdir): + changelog_file_path = os.path.join(tmpdir, "CHANGELOG.md") + with open(changelog_file_path, "wt") as file: + file.write( + """# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Master + +## 1.2.0 (August 28, 2019) +### Changed +- Release note 1. +- Release note 2. + +### Added +- Enhancement 1 +- sub enhancement 1 +- sub enhancement 2 +- Enhancement 2 + +### Fixed +- Bug fix 1 +- sub bug 1 +- sub bug 2 +- Bug fix 2 + +### Security +- Known issue 1 +- Known issue 2 + +### Deprecated +- Deprecated feature 1 +- Future removal 2 + +### Removed +- Deprecated feature 2 +- Future removal 1 + +## [1.1.0] (May 03, 2018) +### Changed +- Enhancement 1 (1.1.0) +- sub enhancement 1 +- sub enhancement 2 +- Enhancement 2 (1.1.0) + +## [1.0.1] - May 01, 2018 +### Fixed +- Bug fix 1 (1.0.1) +- sub bug 1 +- sub bug 2 +- Bug fix 2 (1.0.1) + +## 1.0.0 (2017-01-01) +### Deprecated +- Known issue 1 (1.0.0) +- Known issue 2 (1.0.0) +""" + ) + return changelog_file_path + + +def test_changelog_with_versions_and_all_categories(changelog): + assert keepachangelog.to_dict(changelog) == { + "1.2.0": { + "added": [ + "Enhancement 1", + "sub enhancement 1", + "sub enhancement 2", + "Enhancement 2", + ], + "changed": ["Release note 1.", "Release note 2."], + "deprecated": ["Deprecated feature 1", "Future removal 2"], + "fixed": ["Bug fix 1", "sub bug 1", "sub bug 2", "Bug fix 2"], + "release_date": "august 28, 2019", + "removed": ["Deprecated feature 2", "Future removal 1"], + "security": ["Known issue 1", "Known issue 2"], + "version": "1.2.0", + }, + "1.1.0": { + "changed": [ + "Enhancement 1 (1.1.0)", + "sub enhancement 1", + "sub enhancement 2", + "Enhancement 2 (1.1.0)", + ], + "release_date": "may 03, 2018", + "version": "1.1.0", + }, + "1.0.1": { + "fixed": [ + "Bug fix 1 (1.0.1)", + "sub bug 1", + "sub bug 2", + "Bug fix 2 (1.0.1)", + ], + "release_date": "may 01, 2018", + "version": "1.0.1", + }, + "1.0.0": { + "deprecated": ["Known issue 1 (1.0.0)", "Known issue 2 (1.0.0)"], + "release_date": "2017-01-01", + "version": "1.0.0", + }, + } + + +def test_changelog_with_unreleased_versions_and_all_categories(changelog): + assert keepachangelog.to_dict(changelog, show_unreleased=True) == { + "master": {"release_date": None, "version": "master"}, + "1.2.0": { + "added": [ + "Enhancement 1", + "sub enhancement 1", + "sub enhancement 2", + "Enhancement 2", + ], + "changed": ["Release note 1.", "Release note 2."], + "deprecated": ["Deprecated feature 1", "Future removal 2"], + "fixed": ["Bug fix 1", "sub bug 1", "sub bug 2", "Bug fix 2"], + "release_date": "august 28, 2019", + "removed": ["Deprecated feature 2", "Future removal 1"], + "security": ["Known issue 1", "Known issue 2"], + "version": "1.2.0", + }, + "1.1.0": { + "changed": [ + "Enhancement 1 (1.1.0)", + "sub enhancement 1", + "sub enhancement 2", + "Enhancement 2 (1.1.0)", + ], + "release_date": "may 03, 2018", + "version": "1.1.0", + }, + "1.0.1": { + "fixed": [ + "Bug fix 1 (1.0.1)", + "sub bug 1", + "sub bug 2", + "Bug fix 2 (1.0.1)", + ], + "release_date": "may 01, 2018", + "version": "1.0.1", + }, + "1.0.0": { + "deprecated": ["Known issue 1 (1.0.0)", "Known issue 2 (1.0.0)"], + "release_date": "2017-01-01", + "version": "1.0.0", + }, + } + + +def test_raw_changelog_with_versions_and_all_categories(changelog): + assert keepachangelog.to_raw_dict(changelog) == { + "1.2.0": { + "raw": """### Changed +- Release note 1. +- Release note 2. +### Added +- Enhancement 1 +- sub enhancement 1 +- sub enhancement 2 +- Enhancement 2 +### Fixed +- Bug fix 1 +- sub bug 1 +- sub bug 2 +- Bug fix 2 +### Security +- Known issue 1 +- Known issue 2 +### Deprecated +- Deprecated feature 1 +- Future removal 2 +### Removed +- Deprecated feature 2 +- Future removal 1 +""", + "release_date": "august 28, 2019", + "version": "1.2.0", + }, + "1.1.0": { + "raw": """### Changed +- Enhancement 1 (1.1.0) +- sub enhancement 1 +- sub enhancement 2 +- Enhancement 2 (1.1.0) +""", + "release_date": "may 03, 2018", + "version": "1.1.0", + }, + "1.0.1": { + "raw": """### Fixed +- Bug fix 1 (1.0.1) +- sub bug 1 +- sub bug 2 +- Bug fix 2 (1.0.1) +""", + "release_date": "may 01, 2018", + "version": "1.0.1", + }, + "1.0.0": { + "raw": """### Deprecated +- Known issue 1 (1.0.0) +- Known issue 2 (1.0.0) +""", + "release_date": "2017-01-01", + "version": "1.0.0", + }, + }