Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Use jinja template to render changelog lines, support custom user templates. #43

Merged
merged 6 commits into from
Aug 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading