Skip to content

Commit

Permalink
feat: Use jinja template to render changelog lines, support custom us…
Browse files Browse the repository at this point in the history
…er templates. (#43)

closes #38
  • Loading branch information
EdgyEdgemond authored Aug 18, 2024
1 parent c24f246 commit 7aa9705
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 75 deletions.
2 changes: 1 addition & 1 deletion changelog_gen/cli/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ def _gen( # noqa: PLR0913, C901, PLR0915
if date_fmt:
version_string += f" {datetime.now(timezone.utc).strftime(date_fmt)}"

w = writer.new_writer(context, extension, dry_run=dry_run)
w = writer.new_writer(context, extension, dry_run=dry_run, change_template=cfg.change_template)

w.add_version(version_string)
w.consume(cfg.type_headers, changes)
Expand Down
1 change: 1 addition & 0 deletions changelog_gen/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ class Config:
version_string: str = "v{new_version}"
footer_parsers: list[str] = dataclasses.field(default_factory=lambda: FOOTER_PARSERS[::])
link_parsers: list[dict[str, str]] = dataclasses.field(default_factory=list)
change_template: str | None = None

# Hooks
post_process: PostProcessConfig | None = None
Expand Down
10 changes: 1 addition & 9 deletions changelog_gen/extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,6 @@ def issue_ref(self: t.Self) -> str:
return footer.value
return ""

@property
def authors(self: t.Self) -> str:
"""Extract authors from footers."""
for footer in self.footers:
if footer.footer == "Authors":
return footer.value
return ""


class ChangeExtractor:
"""Parse commit logs and generate change list."""
Expand Down Expand Up @@ -101,7 +93,7 @@ def extract(self: t.Self) -> list[Change]: # noqa: C901, PLR0912, PLR0915
footers = {}

commit_type = m[1].lower()
scope = (m[2] or "").replace("(", "(`").replace(")", "`)")
scope = (m[2] or "").replace("(", "").replace(")", "")
breaking = m[3] is not None
description = m[4].strip()
prm = re.search(r"\(#\d+\)$", description)
Expand Down
66 changes: 42 additions & 24 deletions changelog_gen/writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from pathlib import Path
from tempfile import NamedTemporaryFile

from jinja2 import BaseLoader, Environment

from changelog_gen.util import timer

if t.TYPE_CHECKING:
Expand All @@ -34,6 +36,7 @@ def __init__(
self: t.Self,
changelog: Path,
context: Context,
change_template: str | None = None,
*,
dry_run: bool = False,
) -> None:
Expand All @@ -45,6 +48,7 @@ def __init__(
self.existing = lines[self.file_header_line_count + 1 :]
self.content = []
self.dry_run = dry_run
self._change_template = change_template

@timer
def add_version(self: t.Self, version: str) -> None:
Expand Down Expand Up @@ -74,27 +78,15 @@ def add_section(self: t.Self, header: str, changes: list[Change]) -> None:
"""Add a section to changelog file."""
self._add_section_header(header)
for change in sorted(changes):
description = f"{change.scope} {change.description}" if change.scope else change.description
description = f"{self.bold_string('Breaking:')} {description}" if change.breaking else description
description = f"{description} {change.authors}" if change.authors else description

self._add_section_line(
description,
change,
)
self._add_section_line(change)
self._post_section()

@timer
def bold_string(self: t.Self, string: str) -> str:
"""Render a string as bold."""
return f"**{string.strip()}**"

@timer
def _add_section_header(self: t.Self, header: str) -> None:
raise NotImplementedError

@timer
def _add_section_line(self: t.Self, description: str, change: Change) -> None:
def _add_section_line(self: t.Self, change: Change) -> None:
raise NotImplementedError

@timer
Expand Down Expand Up @@ -132,6 +124,20 @@ class MdWriter(BaseWriter):
file_header = "# Changelog\n"
extension = Extension.MD

@timer
def __init__(self: t.Self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self._change_template = (
self._change_template
or """
-{% if change.scope %} (`{{change.scope}}`){% endif %}
{% if change.breaking %} **Breaking**{% endif %}
{{ change.description }}
{% for footer in change.footers %}{% if footer.footer == "Authors"%} {{footer.value}}{% endif %}{% endfor %}
{% for link in change.links %} [[{{ link.text }}]({{ link.link }})]{% endfor %}
"""
)

@timer
def _add_version(self: t.Self, version: str) -> None:
self.content.extend([f"## {version}", ""])
Expand All @@ -141,11 +147,10 @@ def _add_section_header(self: t.Self, header: str) -> None:
self.content.extend([f"### {header}", ""])

@timer
def _add_section_line(self: t.Self, description: str, change: Change) -> None:
line = f"- {description}"
def _add_section_line(self: t.Self, change: Change) -> None:
rtemplate = Environment(loader=BaseLoader()).from_string(self._change_template.replace("\n", "")) # noqa: S701

for link in change.links:
line = f"{line} [[{link.text}]({link.link})]"
line = rtemplate.render(change=change)

self.content.append(line)

Expand All @@ -164,6 +169,16 @@ class RstWriter(BaseWriter):
@timer
def __init__(self: t.Self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self._change_template = (
self._change_template
or """
*{% if change.scope %} (`{{change.scope}}`){% endif %}
{% if change.breaking %} **Breaking**{% endif %}
{{ change.description }}
{% for footer in change.footers %}{% if footer.footer == "Authors"%} {{footer.value}}{% endif %}{% endfor %}
{% for link in change.links %} [`{{ link.text }}`_]{% endfor %}
"""
)
self._links = {}

@timer
Expand All @@ -185,15 +200,17 @@ def _add_section_header(self: t.Self, header: str) -> None:
self.content.extend([header, "-" * len(header), ""])

@timer
def _add_section_line(self: t.Self, description: str, change: Change) -> None:
line = f"* {description}"
def _add_section_line(self: t.Self, change: Change) -> None:
rtemplate = Environment(loader=BaseLoader()).from_string(self._change_template.replace("\n", "")) # noqa: S701

line = rtemplate.render(change=change)

self.content.extend([line, ""])

for link in change.links:
line = f"{line} [`{link.text}`_]"
self._links[link.text] = link.link

self.content.extend([line, ""])

@timer
def write(self: t.Self) -> str:
"""Write contents to destination."""
Expand All @@ -206,16 +223,17 @@ def write(self: t.Self) -> str:
def new_writer(
context: Context,
extension: Extension,
change_template: str | None = None,
*,
dry_run: bool = False,
) -> BaseWriter:
"""Generate a new writer based on the required extension."""
changelog = Path(f"CHANGELOG.{extension.value}")

if extension == Extension.MD:
return MdWriter(changelog, context, dry_run=dry_run)
return MdWriter(changelog, context, dry_run=dry_run, change_template=change_template)
if extension == Extension.RST:
return RstWriter(changelog, context, dry_run=dry_run)
return RstWriter(changelog, context, dry_run=dry_run, change_template=change_template)

msg = f'Changelog extension "{extension.value}" not supported.'
raise ValueError(msg)
24 changes: 24 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,30 @@ key = "value"
a_list = ["key", "key2"]
```

### `change_template`
_**[optional]**_<br />
**default**: None

Customise how changelog entries are formatted, uses
[Jinja2](https://jinja.palletsprojects.com/en/3.1.x/) formatting.

The template will provided with the change object and can render all
extracted information as desired. For readability the template can be split
over multiple lines, it will be flattened before it is rendered to generate a
single line entry.

Example:

```toml
[tool.changelog_gen]
change_template = """
-{% if change.scope %} (`{{change.scope}}`){% endif %}
{% if change.breaking %} **Breaking**{% endif %}
{{ change.description }}
{% for footer in change.footers %}{% if footer.footer == "Authors"%} {{footer.value}}{% endif %}{% endfor %}
{% for link in change.links %} [[{{ link.text }}]({{ link.link }})]{% endfor %}
"""
```

## Versioning

Expand Down
88 changes: 87 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ rtoml = ">=0.9"
gitpython = "^3.1.43"
pygments = "^2.18.0"
typing_extensions = { version = "^4.7.0", python = "<3.10" }
jinja2 = "^3.1.4"

[tool.poetry.extras]
post-process = ["httpx"]
Expand Down
Loading

0 comments on commit 7aa9705

Please sign in to comment.