Parse Keep a Changelog formatted CHANGELOG.md files into Python objects
Project description
patchnotes
Parse Keep a Changelog formatted CHANGELOG.md files into structured Python objects. Render to HTML, RSS, or plain text. Fetch directly from GitHub.
Zero dependencies. Pure Python. Typed.
import patchnotes
cl = patchnotes.parse_file("CHANGELOG.md")
cl.latest() # Release(v2.1.0, 2024-11-15, 6 entries)
cl.unreleased() # Release(vUnreleased, unreleased, 2 entries)
# What broke between 1.4.0 and 2.1.0?
for r in cl.diff("1.4.0", "2.1.0"):
for entry in r.breaking_changes:
print(f"v{r.version}: {entry.text}")
Install
pip install patchnotes
Requires Python 3.10+.
Usage
Parse
import patchnotes
# From a file
cl = patchnotes.parse_file("CHANGELOG.md")
# From a string
cl = patchnotes.parse(raw_text)
# From any URL
cl = patchnotes.Changelog.from_url(
"https://raw.githubusercontent.com/user/repo/main/CHANGELOG.md"
)
# From a GitHub repo — just owner + repo name, no URL needed
cl = patchnotes.Changelog.from_github("Londopy", "patchnotes")
# Different branch or filename
cl = patchnotes.Changelog.from_github(
"psf", "requests",
branch="main",
filename="HISTORY.md" # also works with CHANGES.md, NEWS.md, etc.
)
from_github automatically falls back to the master branch if main returns a 404.
Access releases
cl.latest() # highest versioned release
cl.unreleased() # [Unreleased] block, or None
cl.get_version("2.0.0") # specific version, or None
cl.releases # all Release objects, in file order
Query entries
r = cl.get_version("2.0.0")
r.entries # all Entry objects
r.by_type # dict: {"Breaking": [...], "Added": [...], ...}
r.breaking_changes # shortcut: Breaking + Removed entries
r.yanked # bool
r.release_date # datetime.date or None
Diff and history
# All releases strictly between 1.4.0 (exclusive) and 2.1.0 (inclusive)
releases = cl.diff("1.4.0", "2.1.0")
# All releases newer than a version (includes Unreleased)
releases = cl.since_version("1.4.0")
# Every breaking change across the entire changelog
for version, entry in cl.all_breaking_changes():
print(f"v{version}: {entry.text}")
Serialize to JSON
cl.to_dict() # plain Python dict, JSON-safe
cl.to_json() # JSON string (indent=2 by default)
cl.to_json(indent=4)
Rendering
HTML
# Full standalone HTML page
html = patchnotes.to_html(cl)
with open("changelog.html", "w") as f:
f.write(html)
# Bare <div> fragment for embedding in your own page
fragment = patchnotes.to_html(cl, full_page=False)
RSS
rss = patchnotes.to_rss(cl, project_url="https://github.com/you/project")
with open("changelog.rss", "w") as f:
f.write(rss)
Each versioned release becomes an <item>. Unreleased entries are skipped.
Plain text
# Full summary
print(patchnotes.to_text(cl))
# Only the 3 most recent releases
print(patchnotes.to_text(cl, max_releases=3))
CLI
# Summary of all releases
patchnotes CHANGELOG.md
# Latest release
patchnotes CHANGELOG.md latest
# Unreleased changes
patchnotes CHANGELOG.md unreleased
# Specific version
patchnotes CHANGELOG.md show 2.0.0
# Diff between versions
patchnotes CHANGELOG.md diff 1.4.0 2.1.0
# All breaking changes
patchnotes CHANGELOG.md breaking
# Dump as JSON
patchnotes CHANGELOG.md json
Data model
Changelog
├── title: str
├── description: str
├── releases: list[Release]
│ ├── version: str
│ ├── release_date: date | None
│ ├── is_unreleased: bool
│ ├── yanked: bool
│ ├── entries: list[Entry]
│ │ ├── text: str
│ │ └── change_type: ChangeType
│ ├── by_type → dict[str, list[Entry]]
│ └── breaking_changes → list[Entry]
├── latest() → Release | None
├── unreleased() → Release | None
├── get_version(v) → Release | None
├── since_version(v) → list[Release]
├── diff(from, to) → list[Release]
├── all_breaking_changes() → list[tuple[str, Entry]]
├── to_dict() → dict
├── to_json() → str
├── from_url(url) → Changelog
└── from_github(owner, repo, branch, filename) → Changelog
ChangeType values: Added, Changed, Deprecated, Removed, Fixed, Security, Breaking
Changelog format
patchnotes parses the Keep a Changelog spec:
# Project Name
## [Unreleased]
### Added
- New feature
## [1.2.0] - 2024-11-15
### Breaking
- Renamed `foo()` to `bar()`
### Fixed
- Some bug
## [1.1.0] - 2024-09-01 [YANKED]
### Security
- Patched CVE-2024-1234
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file patchnotes-1.1.0.tar.gz.
File metadata
- Download URL: patchnotes-1.1.0.tar.gz
- Upload date:
- Size: 14.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e6e2e7c90d66b57f251131687426056949bc4165d66743ac949435c9995a3298
|
|
| MD5 |
610573b9361bb3764c142e87064da63b
|
|
| BLAKE2b-256 |
2e9dc920469a8960587c20da7f7bfd5650ffd21cab9f1fc5e6855fa17e6995fd
|
File details
Details for the file patchnotes-1.1.0-py3-none-any.whl.
File metadata
- Download URL: patchnotes-1.1.0-py3-none-any.whl
- Upload date:
- Size: 13.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
60120bcdfb20aa0423b7027bac917e9bb015cfcc228d24d5e99706494eaed9f5
|
|
| MD5 |
379fae88f62dd470c3c06a68f514a184
|
|
| BLAKE2b-256 |
4941badea1d9c3f7236cdab86faa0ceee5a170e64d7768e4082ad6b8998083e1
|