Skip to content

Commit

Permalink
Merge pull request #18 from Colin-b/develop
Browse files Browse the repository at this point in the history
Release 0.5.0
  • Loading branch information
Colin-b committed Apr 19, 2021
2 parents 1e88a33 + edc6ab9 commit 09a0502
Show file tree
Hide file tree
Showing 14 changed files with 1,212 additions and 41 deletions.
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
repos:
- repo: https://github.com/psf/black
rev: master
rev: 20.8b1
hooks:
- id: black
- id: black
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ python:
- "3.6"
- "3.7"
- "3.8"
- "3.9"
install:
- pip install .[testing]
script:
Expand All @@ -12,4 +13,4 @@ deploy:
username: __token__
edge: true
distributions: "sdist bdist_wheel"
skip_existing: true
skip_existing: true
15 changes: 14 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
40 changes: 34 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
<h2 align="center">Convert changelog into dict</h2>
<h2 align="center">Manipulate keep a changelog files</h2>

<p align="center">
<a href="https://pypi.org/project/keepachangelog/"><img alt="pypi version" src="https://img.shields.io/pypi/v/keepachangelog"></a>
<a href="https://travis-ci.org/Colin-b/keepachangelog"><img alt="Build status" src="https://api.travis-ci.org/Colin-b/keepachangelog.svg?branch=master"></a>
<a href="https://travis-ci.org/Colin-b/keepachangelog"><img alt="Coverage" src="https://img.shields.io/badge/coverage-100%25-brightgreen"></a>
<a href="https://travis-ci.com/Colin-b/keepachangelog"><img alt="Build status" src="https://api.travis-ci.com/Colin-b/keepachangelog.svg?branch=master"></a>
<a href="https://travis-ci.com/Colin-b/keepachangelog"><img alt="Coverage" src="https://img.shields.io/badge/coverage-100%25-brightgreen"></a>
<a href="https://github.com/psf/black"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
<a href="https://travis-ci.org/Colin-b/keepachangelog"><img alt="Number of tests" src="https://img.shields.io/badge/tests-14 passed-blue"></a>
<a href="https://travis-ci.com/Colin-b/keepachangelog"><img alt="Number of tests" src="https://img.shields.io/badge/tests-28 passed-blue"></a>
<a href="https://pypi.org/project/keepachangelog/"><img alt="Number of downloads" src="https://img.shields.io/pypi/dm/keepachangelog"></a>
</p>

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
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion keepachangelog/__init__.py
Original file line number Diff line number Diff line change
@@ -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
127 changes: 101 additions & 26 deletions keepachangelog/_changelog.py
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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)
72 changes: 72 additions & 0 deletions keepachangelog/_versioning.py
Original file line number Diff line number Diff line change
@@ -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}"
2 changes: 1 addition & 1 deletion keepachangelog/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
maintainer="Colin Bounouar",
maintainer_email="[email protected]",
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/",
Expand All @@ -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"],
Expand Down
Loading

0 comments on commit 09a0502

Please sign in to comment.