diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9c42b63..ee3d6a7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ repos: - repo: https://github.com/psf/black - rev: master + rev: 20.8b1 hooks: - - id: black \ No newline at end of file + - id: black diff --git a/.travis.yml b/.travis.yml index a4fe5a1..64f9fd5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ python: - "3.6" - "3.7" - "3.8" + - "3.9" install: - pip install .[testing] script: @@ -12,4 +13,4 @@ deploy: username: __token__ edge: true distributions: "sdist bdist_wheel" - skip_existing: true \ No newline at end of file + skip_existing: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 2feb7af..56bfe58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.5.0] - 2021-04-19 +### 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 - `keepachangelog.flask_restx.add_changelog_endpoint` function to add a changelog endpoint to a [Flask-RestX](https://flask-restx.readthedocs.io/en/latest/) application. @@ -30,7 +42,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Initial release. -[Unreleased]: https://github.com/Colin-b/keepachangelog/compare/v0.4.0...HEAD +[Unreleased]: https://github.com/Colin-b/keepachangelog/compare/v0.5.0...HEAD +[0.5.0]: https://github.com/Colin-b/keepachangelog/compare/v0.4.0...v0.5.0 [0.4.0]: https://github.com/Colin-b/keepachangelog/compare/v0.3.1...v0.4.0 [0.3.1]: https://github.com/Colin-b/keepachangelog/compare/v0.3.0...v0.3.1 [0.3.0]: https://github.com/Colin-b/keepachangelog/compare/v0.2.0...v0.3.0 diff --git a/README.md b/README.md index 571ea24..000265f 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,23 @@ -

Convert changelog into dict

+

Manipulate keep a changelog files

pypi version -Build status -Coverage +Build status +Coverage Code style: black -Number of tests +Number of tests Number of downloads

-Convert changelog markdown file following [keep a changelog](https://keepachangelog.com/en/1.0.0/) format into python dict. +* [Convert to dict](#convert-changelog-to-dict) +* [Release a new version](#release) +* [Add changelog retrieval REST API endpoint](#endpoint) + * [Starlette](#starlette) + * [Flask-RestX](#flask-restx) + +## Convert changelog to dict + +Convert changelog markdown file following [keep a changelog](https://keepachangelog.com/en/1.1.0/) format into python dict. ```python import keepachangelog @@ -55,7 +63,7 @@ For a markdown file with the following content: # 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/), +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] @@ -115,6 +123,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `show_unreleased` parameter can be specified in order to include `Unreleased` section information. Note that `release_date` will be set to None in such as case. +## Release + +You can create a new release by using `keepachangelog.release` function. + +```python +import keepachangelog + +new_version = keepachangelog.release("path/to/CHANGELOG.md") +``` + +This will: +* Guess the new version number and return it: + * `Removed` or `Changed` sections will be considered as breaking changes, thus incrementing the major version. + * If the only section is `Fixed`, only patch will be incremented. + * Otherwise, minor will be incremented. +* Update changelog. + * Unreleased section content will be moved into a new section. + * `[Unreleased]` link will be updated. + * New link will be created corresponding to the new section (based on the format of the Unreleased link). + ## Endpoint ### Starlette diff --git a/keepachangelog/__init__.py b/keepachangelog/__init__.py index 1ee1b48..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 +from keepachangelog._changelog import to_dict, to_raw_dict, release diff --git a/keepachangelog/_changelog.py b/keepachangelog/_changelog.py index 4629bd2..aa699f7 100644 --- a/keepachangelog/_changelog.py +++ b/keepachangelog/_changelog.py @@ -1,41 +1,50 @@ +import datetime import re -from typing import Dict, List +from typing import Dict, List, Optional -# Release pattern should match lines like: "## [0.0.1] - 2020-12-31" or ## [Unreleased] -release_pattern = re.compile(r"^## \[(.*)\](?: - (.*))?$") +from keepachangelog._versioning import guess_unreleased_version -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 is_release(line: str) -> bool: + return line.startswith("## ") -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" @@ -53,16 +62,82 @@ 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) + release_version(changelog_path, current_version, new_version) + return new_version + + +def release_version( + changelog_path: str, current_version: Optional[str], new_version: str +): + unreleased_link_pattern = re.compile(r"^\[Unreleased\]: (.*)$", re.DOTALL) + lines = [] + with open(changelog_path) as change_log: + for line in change_log.readlines(): + # Move Unreleased section to new version + if re.fullmatch(r"^## \[Unreleased\].*$", line, re.DOTALL): + lines.append(line) + lines.append("\n") + lines.append( + f"## [{new_version}] - {datetime.date.today().isoformat()}\n" + ) + # Add new version link and update Unreleased link + elif unreleased_link_pattern.fullmatch(line): + unreleased_compare_pattern = re.fullmatch( + r"^.*/(.*)\.\.\.(\w*).*$", line, re.DOTALL + ) + # Unreleased link compare previous version to HEAD (unreleased tag) + if unreleased_compare_pattern: + new_unreleased_link = line.replace(current_version, new_version) + lines.append(new_unreleased_link) + current_tag = unreleased_compare_pattern.group(1) + unreleased_tag = unreleased_compare_pattern.group(2) + new_tag = current_tag.replace(current_version, new_version) + lines.append( + line.replace(new_version, current_version) + .replace(unreleased_tag, new_tag) + .replace("Unreleased", new_version) + ) + # Consider that there is no way to know how to create a link to compare versions + else: + lines.append(line) + lines.append(line.replace("Unreleased", new_version)) + else: + lines.append(line) + + with open(changelog_path, "wt") as change_log: + change_log.writelines(lines) diff --git a/keepachangelog/_versioning.py b/keepachangelog/_versioning.py new file mode 100644 index 0000000..be70fea --- /dev/null +++ b/keepachangelog/_versioning.py @@ -0,0 +1,72 @@ +import re +from typing import Tuple, Optional + + +def contains_breaking_changes(unreleased: dict) -> bool: + return "removed" in unreleased or "changed" in unreleased + + +def only_contains_bug_fixes(unreleased: dict) -> bool: + # unreleased contains at least 2 entries: version and release_date + return "fixed" in unreleased and len(unreleased) == 3 + + +def bump_major(version: str) -> str: + major, *_ = to_semantic(version) + return from_semantic(major + 1, 0, 0) + + +def bump_minor(version: str) -> str: + major, minor, _ = to_semantic(version) + return from_semantic(major, minor + 1, 0) + + +def bump_patch(version: str) -> str: + major, minor, patch = to_semantic(version) + return from_semantic(major, minor, patch + 1) + + +def bump(unreleased: dict, version: str) -> str: + if contains_breaking_changes(unreleased): + return bump_major(version) + if only_contains_bug_fixes(unreleased): + return bump_patch(version) + return bump_minor(version) + + +def actual_version(changelog: dict) -> Optional[str]: + versions = sorted(changelog.keys()) + current_version = versions.pop() if versions else None + 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", {}) + if not unreleased or len(unreleased) < 3: + raise Exception( + "Release content must be provided within changelog Unreleased section." + ) + + version = actual_version(changelog) + return version, bump(unreleased, version) + + +# Semantic versioning pattern should match version like 1.2.3" +version_pattern = re.compile(r"^(\d+)\.(\d+)\.(\d+)$") + + +def to_semantic(version: Optional[str]) -> Tuple[int, int, int]: + if not version: + return 0, 0, 0 + + match = version_pattern.fullmatch(version) + if match: + return int(match.group(1)), int(match.group(2)), int(match.group(3)) + + raise Exception(f"{version} is not following semantic versioning.") + + +def from_semantic(major: int, minor: int, patch: int) -> str: + return f"{major}.{minor}.{patch}" diff --git a/keepachangelog/version.py b/keepachangelog/version.py index 151e68f..c4b9ff7 100644 --- a/keepachangelog/version.py +++ b/keepachangelog/version.py @@ -3,4 +3,4 @@ # Major should be incremented in case there is a breaking change. (eg: 2.5.8 -> 3.0.0) # Minor should be incremented in case there is an enhancement. (eg: 2.5.8 -> 2.6.0) # Patch should be incremented in case there is a bug fix. (eg: 2.5.8 -> 2.5.9) -__version__ = "0.4.0" +__version__ = "0.5.0" diff --git a/setup.py b/setup.py index 8931555..13df3fb 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ maintainer="Colin Bounouar", maintainer_email="colin.bounouar.dev@gmail.com", url="https://colin-b.github.io/keepachangelog/", - description="Convert changelog into dict.", + description="Manipulate keep a changelog files.", long_description=long_description, long_description_content_type="text/markdown", download_url="https://pypi.org/project/keepachangelog/", @@ -30,6 +30,7 @@ "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Build Tools", ], keywords=["changelog", "CHANGELOG.md", "markdown"], 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_release.py b/tests/test_changelog_release.py new file mode 100644 index 0000000..95e63eb --- /dev/null +++ b/tests/test_changelog_release.py @@ -0,0 +1,695 @@ +import datetime +import os +import os.path + +import pytest + +import keepachangelog +import keepachangelog._changelog + +_date_time_for_tests = datetime.datetime(2021, 3, 19, 15, 5, 5, 663979) + + +class DateTimeModuleMock: + class DateTimeMock(datetime.datetime): + @classmethod + def now(cls, tz=None): + return _date_time_for_tests.replace(tzinfo=tz) + + class DateMock(datetime.date): + @classmethod + def today(cls): + return _date_time_for_tests.date() + + timedelta = datetime.timedelta + timezone = datetime.timezone + datetime = DateTimeMock + date = DateMock + + +@pytest.fixture +def mock_date(monkeypatch): + monkeypatch.setattr(keepachangelog._changelog, "datetime", DateTimeModuleMock) + + +@pytest.fixture +def major_changelog(tmpdir): + changelog_file_path = os.path.join(tmpdir, "MAJOR_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). + +## [Unreleased] +### 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] - 2018-05-31 +### Changed +- Enhancement 1 (1.1.0) +- sub *enhancement 1* +- sub enhancement 2 +- Enhancement 2 (1.1.0) + +## [1.0.1] - 2018-05-31 +### Fixed +- Bug fix 1 (1.0.1) +- sub bug 1 +- sub bug 2 +- Bug fix 2 (1.0.1) + +## [1.0.0] - 2017-04-10 +### Deprecated +- Known issue 1 (1.0.0) +- Known issue 2 (1.0.0) + +[Unreleased]: https://github.test_url/test_project/compare/v1.1.0...HEAD +[1.1.0]: https://github.test_url/test_project/compare/v1.0.1...v1.1.0 +[1.0.1]: https://github.test_url/test_project/compare/v1.0.0...v1.0.1 +[1.0.0]: https://github.test_url/test_project/releases/tag/v1.0.0 +""" + ) + return changelog_file_path + + +@pytest.fixture +def minor_changelog(tmpdir): + changelog_file_path = os.path.join(tmpdir, "MINOR_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). + +## [Unreleased] +### 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 + +## [1.1.0] - 2018-05-31 +### Changed +- Enhancement 1 (1.1.0) +- sub *enhancement 1* +- sub enhancement 2 +- Enhancement 2 (1.1.0) + +## [1.0.1] - 2018-05-31 +### Fixed +- Bug fix 1 (1.0.1) +- sub bug 1 +- sub bug 2 +- Bug fix 2 (1.0.1) + +## [1.0.0] - 2017-04-10 +### Deprecated +- Known issue 1 (1.0.0) +- Known issue 2 (1.0.0) + +[Unreleased]: https://github.test_url/test_project/compare/v1.1.0...HEAD +[1.1.0]: https://github.test_url/test_project/compare/v1.0.1...v1.1.0 +[1.0.1]: https://github.test_url/test_project/compare/v1.0.0...v1.0.1 +[1.0.0]: https://github.test_url/test_project/releases/tag/v1.0.0 +""" + ) + return changelog_file_path + + +@pytest.fixture +def patch_changelog(tmpdir): + changelog_file_path = os.path.join(tmpdir, "PATCH_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). + +## [Unreleased] +### Fixed +- Bug fix 1 + - sub bug 1 + * sub bug 2 +- Bug fix 2 + +## [1.1.0] - 2018-05-31 +### Changed +- Enhancement 1 (1.1.0) +- sub *enhancement 1* +- sub enhancement 2 +- Enhancement 2 (1.1.0) + +## [1.0.1] - 2018-05-31 +### Fixed +- Bug fix 1 (1.0.1) +- sub bug 1 +- sub bug 2 +- Bug fix 2 (1.0.1) + +## [1.0.0] - 2017-04-10 +### Deprecated +- Known issue 1 (1.0.0) +- Known issue 2 (1.0.0) + +[Unreleased]: https://github.test_url/test_project/compare/v1.1.0...HEAD +[1.1.0]: https://github.test_url/test_project/compare/v1.0.1...v1.1.0 +[1.0.1]: https://github.test_url/test_project/compare/v1.0.0...v1.0.1 +[1.0.0]: https://github.test_url/test_project/releases/tag/v1.0.0 +""" + ) + return changelog_file_path + + +@pytest.fixture +def first_major_changelog(tmpdir): + changelog_file_path = os.path.join(tmpdir, "FIRST_MAJOR_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). + +## [Unreleased] +### 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 + +[Unreleased]: https://github.test_url/test_project +""" + ) + return changelog_file_path + + +@pytest.fixture +def first_minor_changelog(tmpdir): + changelog_file_path = os.path.join(tmpdir, "FIRST_MINOR_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). + +## [Unreleased] +### 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 + +[Unreleased]: https://github.test_url/test_project +""" + ) + return changelog_file_path + + +@pytest.fixture +def first_patch_changelog(tmpdir): + changelog_file_path = os.path.join(tmpdir, "FIRST_PATCH_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). + +## [Unreleased] +### Fixed +- Bug fix 1 + - sub bug 1 + * sub bug 2 +- Bug fix 2 + +[Unreleased]: https://github.test_url/test_project +""" + ) + return changelog_file_path + + +@pytest.fixture +def empty_unreleased_changelog(tmpdir): + changelog_file_path = os.path.join(tmpdir, "EMPTY_UNRELEASED_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). + +## [Unreleased] + +[Unreleased]: https://github.test_url/test_project +""" + ) + return changelog_file_path + + +@pytest.fixture +def empty_changelog(tmpdir): + changelog_file_path = os.path.join(tmpdir, "EMPTY_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). +""" + ) + return changelog_file_path + + +@pytest.fixture +def non_semantic_changelog(tmpdir): + changelog_file_path = os.path.join(tmpdir, "NON_SEMANTIC_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). + +## [Unreleased] +### Changed +- Enhancement 1 (1.1.0) + +## [20180531] - 2018-05-31 +### Changed +- Enhancement 1 (1.1.0) +- sub *enhancement 1* +- sub enhancement 2 +- Enhancement 2 (1.1.0) + +[Unreleased]: https://github.test_url/test_project/compare/v20180531...HEAD +[20180531]: https://github.test_url/test_project/releases/tag/v20180531 +""" + ) + return changelog_file_path + + +def test_major_release(major_changelog, mock_date): + assert keepachangelog.release(major_changelog) == "2.0.0" + with open(major_changelog) as file: + assert ( + file.read() + == """# 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). + +## [Unreleased] + +## [2.0.0] - 2021-03-19 +### 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] - 2018-05-31 +### Changed +- Enhancement 1 (1.1.0) +- sub *enhancement 1* +- sub enhancement 2 +- Enhancement 2 (1.1.0) + +## [1.0.1] - 2018-05-31 +### Fixed +- Bug fix 1 (1.0.1) +- sub bug 1 +- sub bug 2 +- Bug fix 2 (1.0.1) + +## [1.0.0] - 2017-04-10 +### Deprecated +- Known issue 1 (1.0.0) +- Known issue 2 (1.0.0) + +[Unreleased]: https://github.test_url/test_project/compare/v2.0.0...HEAD +[2.0.0]: https://github.test_url/test_project/compare/v1.1.0...v2.0.0 +[1.1.0]: https://github.test_url/test_project/compare/v1.0.1...v1.1.0 +[1.0.1]: https://github.test_url/test_project/compare/v1.0.0...v1.0.1 +[1.0.0]: https://github.test_url/test_project/releases/tag/v1.0.0 +""" + ) + + +def test_minor_release(minor_changelog, mock_date): + assert keepachangelog.release(minor_changelog) == "1.2.0" + with open(minor_changelog) as file: + assert ( + file.read() + == """# 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). + +## [Unreleased] + +## [1.2.0] - 2021-03-19 +### 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 + +## [1.1.0] - 2018-05-31 +### Changed +- Enhancement 1 (1.1.0) +- sub *enhancement 1* +- sub enhancement 2 +- Enhancement 2 (1.1.0) + +## [1.0.1] - 2018-05-31 +### Fixed +- Bug fix 1 (1.0.1) +- sub bug 1 +- sub bug 2 +- Bug fix 2 (1.0.1) + +## [1.0.0] - 2017-04-10 +### Deprecated +- Known issue 1 (1.0.0) +- Known issue 2 (1.0.0) + +[Unreleased]: https://github.test_url/test_project/compare/v1.2.0...HEAD +[1.2.0]: https://github.test_url/test_project/compare/v1.1.0...v1.2.0 +[1.1.0]: https://github.test_url/test_project/compare/v1.0.1...v1.1.0 +[1.0.1]: https://github.test_url/test_project/compare/v1.0.0...v1.0.1 +[1.0.0]: https://github.test_url/test_project/releases/tag/v1.0.0 +""" + ) + + +def test_patch_release(patch_changelog, mock_date): + assert keepachangelog.release(patch_changelog) == "1.1.1" + with open(patch_changelog) as file: + assert ( + file.read() + == """# 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). + +## [Unreleased] + +## [1.1.1] - 2021-03-19 +### Fixed +- Bug fix 1 + - sub bug 1 + * sub bug 2 +- Bug fix 2 + +## [1.1.0] - 2018-05-31 +### Changed +- Enhancement 1 (1.1.0) +- sub *enhancement 1* +- sub enhancement 2 +- Enhancement 2 (1.1.0) + +## [1.0.1] - 2018-05-31 +### Fixed +- Bug fix 1 (1.0.1) +- sub bug 1 +- sub bug 2 +- Bug fix 2 (1.0.1) + +## [1.0.0] - 2017-04-10 +### Deprecated +- Known issue 1 (1.0.0) +- Known issue 2 (1.0.0) + +[Unreleased]: https://github.test_url/test_project/compare/v1.1.1...HEAD +[1.1.1]: https://github.test_url/test_project/compare/v1.1.0...v1.1.1 +[1.1.0]: https://github.test_url/test_project/compare/v1.0.1...v1.1.0 +[1.0.1]: https://github.test_url/test_project/compare/v1.0.0...v1.0.1 +[1.0.0]: https://github.test_url/test_project/releases/tag/v1.0.0 +""" + ) + + +def test_first_major_release(first_major_changelog, mock_date): + assert keepachangelog.release(first_major_changelog) == "1.0.0" + with open(first_major_changelog) as file: + assert ( + file.read() + == """# 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). + +## [Unreleased] + +## [1.0.0] - 2021-03-19 +### 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 + +[Unreleased]: https://github.test_url/test_project +[1.0.0]: https://github.test_url/test_project +""" + ) + + +def test_first_minor_release(first_minor_changelog, mock_date): + assert keepachangelog.release(first_minor_changelog) == "0.1.0" + with open(first_minor_changelog) as file: + assert ( + file.read() + == """# 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). + +## [Unreleased] + +## [0.1.0] - 2021-03-19 +### 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 + +[Unreleased]: https://github.test_url/test_project +[0.1.0]: https://github.test_url/test_project +""" + ) + + +def test_first_patch_release(first_patch_changelog, mock_date): + assert keepachangelog.release(first_patch_changelog) == "0.0.1" + with open(first_patch_changelog) as file: + assert ( + file.read() + == """# 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). + +## [Unreleased] + +## [0.0.1] - 2021-03-19 +### Fixed +- Bug fix 1 + - sub bug 1 + * sub bug 2 +- Bug fix 2 + +[Unreleased]: https://github.test_url/test_project +[0.0.1]: https://github.test_url/test_project +""" + ) + + +def test_empty_release(empty_changelog): + with pytest.raises(Exception) as exception_info: + keepachangelog.release(empty_changelog) + assert ( + str(exception_info.value) + == "Release content must be provided within changelog Unreleased section." + ) + + +def test_empty_unreleased_release(empty_unreleased_changelog): + with pytest.raises(Exception) as exception_info: + keepachangelog.release(empty_unreleased_changelog) + assert ( + str(exception_info.value) + == "Release content must be provided within changelog Unreleased section." + ) + + +def test_non_semantic_release(non_semantic_changelog): + with pytest.raises(Exception) as exception_info: + keepachangelog.release(non_semantic_changelog) + assert str(exception_info.value) == "20180531 is not following semantic versioning." 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", + }, + }