-
-
Notifications
You must be signed in to change notification settings - Fork 267
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(formats): expose some new customizable changelog formats on the …
…`commitizen.changelog_format` endpoint (Textile, AsciiDoc and RestructuredText)
- Loading branch information
1 parent
4aa2bf7
commit e61aceb
Showing
41 changed files
with
1,592 additions
and
688 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
from __future__ import annotations | ||
|
||
from typing import ClassVar, Protocol | ||
|
||
import importlib_metadata as metadata | ||
|
||
from commitizen.changelog import Metadata | ||
from commitizen.exceptions import ChangelogFormatUnknown | ||
from commitizen.config.base_config import BaseConfig | ||
|
||
|
||
CHANGELOG_FORMAT_ENTRYPOINT = "commitizen.changelog_format" | ||
TEMPLATE_EXTENSION = "j2" | ||
|
||
|
||
class ChangelogFormat(Protocol): | ||
extension: ClassVar[str] | ||
"""Standard known extension associated with this format""" | ||
|
||
alternative_extensions: ClassVar[set[str]] | ||
"""Known alternatives extensions for this format""" | ||
|
||
config: BaseConfig | ||
|
||
def __init__(self, config: BaseConfig): | ||
self.config = config | ||
|
||
@property | ||
def ext(self) -> str: | ||
"""Dotted version of extensions, as in `pathlib` and `os` modules""" | ||
return f".{self.extension}" | ||
|
||
@property | ||
def template(self) -> str: | ||
"""Expected template name for this format""" | ||
return f"CHANGELOG.{self.extension}.{TEMPLATE_EXTENSION}" | ||
|
||
@property | ||
def default_changelog_file(self) -> str: | ||
return f"CHANGELOG.{self.extension}" | ||
|
||
def get_metadata(self, filepath: str) -> Metadata: | ||
""" | ||
Extract the changelog metadata. | ||
""" | ||
raise NotImplementedError | ||
|
||
|
||
KNOWN_CHANGELOG_FORMATS: dict[str, type[ChangelogFormat]] = { | ||
ep.name: ep.load() | ||
for ep in metadata.entry_points(group=CHANGELOG_FORMAT_ENTRYPOINT) | ||
} | ||
|
||
|
||
def get_changelog_format( | ||
config: BaseConfig, filename: str | None = None | ||
) -> ChangelogFormat: | ||
""" | ||
Get a format from its name | ||
:raises FormatUnknown: if a non-empty name is provided but cannot be found in the known formats | ||
""" | ||
name: str | None = config.settings.get("changelog_format") | ||
format: type[ChangelogFormat] | None = guess_changelog_format(filename) | ||
|
||
if name and name in KNOWN_CHANGELOG_FORMATS: | ||
format = KNOWN_CHANGELOG_FORMATS[name] | ||
|
||
if not format: | ||
raise ChangelogFormatUnknown(f"Unknown changelog format '{name}'") | ||
|
||
return format(config) | ||
|
||
|
||
def guess_changelog_format(filename: str | None) -> type[ChangelogFormat] | None: | ||
""" | ||
Try guessing the file format from the filename. | ||
Algorithm is basic, extension-based, and won't work | ||
for extension-less file names like `CHANGELOG` or `NEWS`. | ||
""" | ||
if not filename or not isinstance(filename, str): | ||
return None | ||
for format in KNOWN_CHANGELOG_FORMATS.values(): | ||
if filename.endswith(f".{format.extension}"): | ||
return format | ||
for alt_extension in format.alternative_extensions: | ||
if filename.endswith(f".{alt_extension}"): | ||
return format | ||
return None |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
from __future__ import annotations | ||
|
||
import re | ||
|
||
from .base import BaseFormat | ||
|
||
|
||
class AsciiDoc(BaseFormat): | ||
extension = "adoc" | ||
|
||
RE_TITLE = re.compile(r"^(?P<level>=+) (?P<title>.*)$") | ||
|
||
def parse_version_from_title(self, line: str) -> str | None: | ||
m = self.RE_TITLE.match(line) | ||
if not m: | ||
return None | ||
# Capture last match as AsciiDoc use postfixed URL labels | ||
matches = list(re.finditer(self.version_parser, m.group("title"))) | ||
if not matches: | ||
return None | ||
return matches[-1].group("version") | ||
|
||
def parse_title_level(self, line: str) -> int | None: | ||
m = self.RE_TITLE.match(line) | ||
if not m: | ||
return None | ||
return len(m.group("level")) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
from __future__ import annotations | ||
|
||
import os | ||
from abc import ABCMeta | ||
from re import Pattern | ||
from typing import IO, Any, ClassVar | ||
|
||
from commitizen.changelog import Metadata | ||
from commitizen.config.base_config import BaseConfig | ||
from commitizen.version_schemes import get_version_scheme | ||
|
||
from . import ChangelogFormat | ||
|
||
|
||
class BaseFormat(ChangelogFormat, metaclass=ABCMeta): | ||
""" | ||
Base class to extend to implement a changelog file format. | ||
""" | ||
|
||
extension: ClassVar[str] = "" | ||
alternative_extensions: ClassVar[set[str]] = set() | ||
|
||
def __init__(self, config: BaseConfig): | ||
# Constructor needs to be redefined because `Protocol` prevent instantiation by default | ||
# See: https://bugs.python.org/issue44807 | ||
self.config = config | ||
|
||
@property | ||
def version_parser(self) -> Pattern: | ||
return get_version_scheme(self.config).parser | ||
|
||
def get_metadata(self, filepath: str) -> Metadata: | ||
if not os.path.isfile(filepath): | ||
return Metadata() | ||
|
||
with open(filepath) as changelog_file: | ||
return self.get_metadata_from_file(changelog_file) | ||
|
||
def get_metadata_from_file(self, file: IO[Any]) -> Metadata: | ||
meta = Metadata() | ||
unreleased_level: int | None = None | ||
for index, line in enumerate(file): | ||
line = line.strip().lower() | ||
|
||
unreleased: int | None = None | ||
if "unreleased" in line: | ||
unreleased = self.parse_title_level(line) | ||
# Try to find beginning and end lines of the unreleased block | ||
if unreleased: | ||
meta.unreleased_start = index | ||
unreleased_level = unreleased | ||
continue | ||
elif unreleased_level and self.parse_title_level(line) == unreleased_level: | ||
meta.unreleased_end = index | ||
|
||
# Try to find the latest release done | ||
version = self.parse_version_from_title(line) | ||
if version: | ||
meta.latest_version = version | ||
meta.latest_version_position = index | ||
break # there's no need for more info | ||
if meta.unreleased_start is not None and meta.unreleased_end is None: | ||
meta.unreleased_end = index | ||
|
||
return meta | ||
|
||
def parse_version_from_title(self, line: str) -> str | None: | ||
""" | ||
Extract the version from a title line if any | ||
""" | ||
raise NotImplementedError( | ||
"Default `get_metadata_from_file` requires `parse_version_from_changelog` to be implemented" | ||
) | ||
|
||
def parse_title_level(self, line: str) -> int | None: | ||
""" | ||
Get the title level/type of a line if any | ||
""" | ||
raise NotImplementedError( | ||
"Default `get_metadata_from_file` requires `parse_title_type_of_line` to be implemented" | ||
) |
Oops, something went wrong.