Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit f15672b

Browse files
committedMar 10, 2023
feat(template): allow to override the template from cli, configuration and plugins
Fixes commitizen-tools#132 Fixes commitizen-tools#384 Fixes commitizen-tools#433 Closes commitizen-tools#376
1 parent 9b5f311 commit f15672b

File tree

15 files changed

+594
-9
lines changed

15 files changed

+594
-9
lines changed
 

‎commitizen/changelog.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,21 @@
3131
from datetime import date
3232
from typing import Callable, Dict, Iterable, List, Optional, Tuple
3333

34-
from jinja2 import Environment, PackageLoader
34+
from jinja2 import (
35+
BaseLoader,
36+
ChoiceLoader,
37+
Environment,
38+
FileSystemLoader,
39+
PackageLoader,
40+
)
3541

3642
from commitizen import defaults
3743
from commitizen.bump import normalize_tag
3844
from commitizen.exceptions import InvalidConfigurationError, NoCommitsFoundError
3945
from commitizen.git import GitCommit, GitTag
4046

47+
DEFAULT_TEMPLATE = "keep_a_changelog_template.j2"
48+
4149

4250
def get_commit_tag(commit: GitCommit, tags: List[GitTag]) -> Optional[GitTag]:
4351
return next((tag for tag in tags if tag.rev == commit.rev), None)
@@ -139,11 +147,18 @@ def order_changelog_tree(tree: Iterable, change_type_order: List[str]) -> Iterab
139147
return sorted_tree
140148

141149

142-
def render_changelog(tree: Iterable) -> str:
143-
loader = PackageLoader("commitizen", "templates")
150+
def render_changelog(
151+
tree: Iterable,
152+
loader: Optional[BaseLoader] = None,
153+
template: Optional[str] = None,
154+
**kwargs,
155+
) -> str:
156+
loader = ChoiceLoader(
157+
[FileSystemLoader("."), loader or PackageLoader("commitizen", "templates")]
158+
)
144159
env = Environment(loader=loader, trim_blocks=True)
145-
jinja_template = env.get_template("keep_a_changelog_template.j2")
146-
changelog: str = jinja_template.render(tree=tree)
160+
jinja_template = env.get_template(template or DEFAULT_TEMPLATE)
161+
changelog: str = jinja_template.render(tree=tree, **kwargs)
147162
return changelog
148163

149164

‎commitizen/cli.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import argparse
22
import logging
33
import sys
4+
from copy import deepcopy
45
from functools import partial
56
from types import TracebackType
67
from typing import List
@@ -17,6 +18,51 @@
1718
)
1819

1920
logger = logging.getLogger(__name__)
21+
22+
23+
class ParseKwargs(argparse.Action):
24+
"""
25+
Parse arguments in the for `key=value`.
26+
27+
Quoted strings are automatically unquoted.
28+
Can be submitted multiple times:
29+
30+
ex:
31+
-k key=value -k double-quotes="value" -k single-quotes='value'
32+
33+
will result in
34+
35+
namespace["opt"] == {
36+
"key": "value",
37+
"double-quotes": "value",
38+
"single-quotes": "value",
39+
}
40+
"""
41+
42+
def __call__(self, parser, namespace, kwarg, option_string=None):
43+
kwargs = getattr(namespace, self.dest, None) or {}
44+
key, value = kwarg.split("=", 1)
45+
kwargs[key] = value.strip("'\"")
46+
setattr(namespace, self.dest, kwargs)
47+
48+
49+
tpl_arguments = (
50+
{
51+
"name": ["--template", "-t"],
52+
"help": (
53+
"changelog template file name "
54+
"(relative to the current working directory)"
55+
),
56+
},
57+
{
58+
"name": ["--extra", "-e"],
59+
"action": ParseKwargs,
60+
"dest": "extras",
61+
"metavar": "EXTRA",
62+
"help": "a changelog extra variable (in the form 'key=value')",
63+
},
64+
)
65+
2066
data = {
2167
"prog": "cz",
2268
"description": (
@@ -190,6 +236,7 @@
190236
"default": None,
191237
"help": "keep major version at zero, even for breaking changes",
192238
},
239+
*deepcopy(tpl_arguments),
193240
{
194241
"name": ["--prerelease-offset"],
195242
"type": int,
@@ -252,6 +299,7 @@
252299
"If not set, it will generate changelog from the start"
253300
),
254301
},
302+
*deepcopy(tpl_arguments),
255303
],
256304
},
257305
{

‎commitizen/commands/bump.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def __init__(self, config: BaseConfig, arguments: dict):
4848
"annotated_tag",
4949
"major_version_zero",
5050
"prerelease_offset",
51+
"template",
5152
]
5253
if arguments[key] is not None
5354
},
@@ -62,6 +63,8 @@ def __init__(self, config: BaseConfig, arguments: dict):
6263
self.retry = arguments["retry"]
6364
self.pre_bump_hooks = self.config.settings["pre_bump_hooks"]
6465
self.post_bump_hooks = self.config.settings["post_bump_hooks"]
66+
self.template = arguments["template"] or self.config.settings.get("template")
67+
self.extras = arguments["extras"]
6568

6669
def is_initial_tag(self, current_tag_version: str, is_yes: bool = False) -> bool:
6770
"""Check if reading the whole git tree up to HEAD is needed."""
@@ -255,6 +258,8 @@ def __call__(self): # noqa: C901
255258
"unreleased_version": new_tag_version,
256259
"incremental": True,
257260
"dry_run": dry_run,
261+
"template": self.template,
262+
"extras": self.extras,
258263
},
259264
)
260265
changelog_cmd()

‎commitizen/commands/changelog.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ def __init__(self, config: BaseConfig, args):
4949
self.tag_format = args.get("tag_format") or self.config.settings.get(
5050
"tag_format"
5151
)
52+
self.template = args.get("template") or self.config.settings.get("template")
53+
self.extras = args.get("extras") or {}
5254

5355
def _find_incremental_rev(self, latest_version: str, tags: List[GitTag]) -> str:
5456
"""Try to find the 'start_rev'.
@@ -161,7 +163,13 @@ def __call__(self):
161163
)
162164
if self.change_type_order:
163165
tree = changelog.order_changelog_tree(tree, self.change_type_order)
164-
changelog_out = changelog.render_changelog(tree)
166+
167+
extras = self.cz.template_extras.copy()
168+
extras.update(self.config.settings["extras"])
169+
extras.update(self.extras)
170+
changelog_out = changelog.render_changelog(
171+
tree, loader=self.cz.template_loader, template=self.template, **extras
172+
)
165173
changelog_out = changelog_out.lstrip("\n")
166174

167175
if self.dry_run:

‎commitizen/cz/base.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
from __future__ import annotations
2+
13
from abc import ABCMeta, abstractmethod
2-
from typing import Callable, Dict, List, Optional, Tuple
4+
from typing import Any, Callable, Dict, List, Optional, Tuple
35

6+
from jinja2 import BaseLoader
47
from prompt_toolkit.styles import Style, merge_styles
58

69
from commitizen import git
10+
from commitizen.changelog import DEFAULT_TEMPLATE
711
from commitizen.config.base_config import BaseConfig
812
from commitizen.defaults import Questions
913

@@ -40,6 +44,10 @@ class BaseCommitizen(metaclass=ABCMeta):
4044
# Executed only at the end of the changelog generation
4145
changelog_hook: Optional[Callable[[str, Optional[str]], str]] = None
4246

47+
template: str = DEFAULT_TEMPLATE
48+
template_loader: Optional[BaseLoader] = None
49+
template_extras: dict[str, Any] = {}
50+
4351
def __init__(self, config: BaseConfig):
4452
self.config = config
4553
if not self.config.settings.get("style"):

‎commitizen/defaults.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import pathlib
24
from collections import OrderedDict
35
from typing import Any, Dict, Iterable, List, MutableMapping, Optional, Tuple, Union
@@ -44,6 +46,8 @@ class Settings(TypedDict, total=False):
4446
pre_bump_hooks: Optional[List[str]]
4547
post_bump_hooks: Optional[List[str]]
4648
prerelease_offset: int
49+
template: Optional[str]
50+
extras: dict[str, Any]
4751

4852

4953
name: str = "cz_conventional_commits"
@@ -73,6 +77,8 @@ class Settings(TypedDict, total=False):
7377
"pre_bump_hooks": [],
7478
"post_bump_hooks": [],
7579
"prerelease_offset": 0,
80+
"template": None,
81+
"extras": {},
7682
}
7783

7884
MAJOR = "MAJOR"

‎docs/bump.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ usage: cz bump [-h] [--dry-run] [--files-only] [--local-version] [--changelog]
6060
[--devrelease DEVRELEASE] [--increment {MAJOR,MINOR,PATCH}]
6161
[--check-consistency] [--annotated-tag] [--gpg-sign]
6262
[--changelog-to-stdout] [--retry] [--major-version-zero]
63+
[--template TEMPLATE] [--extra EXTRA]
6364
[MANUAL_VERSION]
6465

6566
positional arguments:
@@ -96,6 +97,10 @@ options:
9697
--retry retry commit if it fails the 1st time
9798
--major-version-zero keep major version at zero, even for breaking changes
9899
--prerelease-offset start pre-releases with this offset
100+
--template TEMPLATE, -t TEMPLATE
101+
changelog template file name (relative to the current working directory)
102+
--extra EXTRA, -e EXTRA
103+
a changelog extra variable (in the form 'key=value')
99104
```
100105
101106
### `--files-only`
@@ -214,6 +219,21 @@ We recommend setting `major_version_zero = true` in your configuration file whil
214219
is in its initial development. Remove that configuration using a breaking-change commit to bump
215220
your project’s major version to `v1.0.0` once your project has reached maturity.
216221
222+
### `--template`
223+
224+
Provides your own changelog jinja template.
225+
See [the template customization section](customization.md#customizing-the-changelog-template)
226+
227+
### `--extra`
228+
229+
Provides your own changelog extra variables by using the `extras` settings or the `--extra/-e` parameter.
230+
231+
```bash
232+
cz bump --changelog --extra key=value -e short="quoted value"
233+
```
234+
235+
See [the template customization section](customization.md#customizing-the-changelog-template).
236+
217237
## Avoid raising errors
218238
219239
Some situations from commitizen rise an exit code different than 0.

‎docs/changelog.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ This command will generate a changelog following the committing rules establishe
77
```bash
88
$ cz changelog --help
99
usage: cz changelog [-h] [--dry-run] [--file-name FILE_NAME] [--unreleased-version UNRELEASED_VERSION] [--incremental] [--start-rev START_REV]
10+
[--template TEMPLATE] [--extra EXTRA]
1011
[rev_range]
1112

1213
positional arguments:
@@ -22,6 +23,10 @@ optional arguments:
2223
--incremental generates changelog from last created version, useful if the changelog has been manually modified
2324
--start-rev START_REV
2425
start rev of the changelog.If not set, it will generate changelog from the start
26+
--template TEMPLATE, -t TEMPLATE
27+
changelog template file name (relative to the current working directory)
28+
--extra EXTRA, -e EXTRA
29+
a changelog extra variable (in the form 'key=value')
2530
```
2631
2732
### Examples
@@ -161,6 +166,21 @@ cz changelog --start-rev="v0.2.0"
161166
changelog_start_rev = "v0.2.0"
162167
```
163168
169+
### `template`
170+
171+
Provides your own changelog jinja template by using the `template` settings or the `--template` parameter.
172+
See [the template customization section](customization.md#customizing-the-changelog-template)
173+
174+
### `extras`
175+
176+
Provides your own changelog extra variables by using the `extras` settings or the `--extra/-e` parameter.
177+
178+
```bash
179+
cz changelog --extra key=value -e short="quoted value"
180+
```
181+
182+
See [the template customization section](customization.md#customizing-the-changelog-template)
183+
164184
## Hooks
165185
166186
Supported hook methods:

‎docs/config.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
| `use_shortcuts` | `bool` | `false` | If enabled, commitizen will show keyboard shortcuts when selecting from a list. Define a `key` for each of your choices to set the key. [See more][shortcuts] |
2323
| `major_version_zero` | `bool` | `false` | When true, breaking changes on a `0.x` will remain as a `0.x` version. On `false`, a breaking change will bump a `0.x` version to `1.0`. [major-version-zero] |
2424
| `prerelease_offset` | `int` | `0` | In special cases it may be necessary that a prerelease cannot start with a 0, e.g. in an embedded project the individual characters are encoded in bytes. This can be done by specifying an offset from which to start counting. [prerelease-offset] |
25+
| <a name="cfg-template"></a>`template` | `str` | `None` | Provide custom changelog jinja template path relative to the current working directory. [See more (template customization)][template-customization] |
26+
| <a name="cfg-extras"></a>`extras` | `dict` | `{}` | Provide extra variables to the changelog template. [See more (template customization)][template-customization] |
2527

2628
## pyproject.toml or .cz.toml
2729

@@ -181,4 +183,5 @@ setup(
181183
[additional-features]: https://github.com/tmbo/questionary#additional-features
182184
[customization]: customization.md
183185
[shortcuts]: customization.md#shortcut-keys
186+
[template-customization]: customization.md#customizing-the-changelog-template
184187
[annotated-tags-vs-lightweight]: https://stackoverflow.com/a/11514139/2047185

‎docs/customization.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,3 +435,84 @@ setup(
435435
```
436436

437437
Then your plugin will be available under the name `plugin`.
438+
439+
## Customizing the changelog template
440+
441+
Commitizen gives you the possibility to provide your own changelog template, by:
442+
443+
- providing one with your customization class
444+
- providing one from the current working directory and setting it:
445+
- as [configuration][template-config]
446+
- as `--template` parameter to both `bump` and `changelog` commands
447+
- either by providing a template with the same name as the default template
448+
449+
By default, the template used is the `keep_a_changelog_template.j2` file from the commitizen repository.
450+
451+
### Providing a template with your customization class
452+
453+
There is 3 parameters available to hcange the template rendering from your custom `BaseCommitizen`.
454+
455+
| Parameter | Type | Default | Description |
456+
| ----------------- | ------ | ------- | ----------------------------------------------------------------------------------------------------- |
457+
| `template` | `str` | `None` | Provide your own template name (default to `keep_a_changelog_template.j2`) |
458+
| `template_loader` | `str` | `None` | Override the default template loader (so you can provide template from you customization class) |
459+
| `template_extras` | `dict` | `None` | Provide some extra template parameters |
460+
461+
Let's see an example.
462+
463+
```python
464+
from commitizen.cz.base import BaseCommitizen
465+
from jinja2 import PackageLoader
466+
467+
468+
class MyPlugin(BaseCommitizen):
469+
template = "CHANGELOG.md.jinja"
470+
template_loader = PackageLoader("my_plugin", "templates")
471+
template_extras = {"key": "value"}
472+
```
473+
474+
This snippet will:
475+
476+
- use `CHANGELOG.md.jinja` as template name
477+
- search for it in the `templates` directory for `my_plugin` package
478+
- add the `key=value` variable in the template
479+
480+
### Providing a template from the current working directory
481+
482+
Users can provides their own template from their current working directory (your project root) by:
483+
484+
- providing a template with the same name (`keep_a_changelog_template.j2` unless overriden by your custom class)
485+
- setting your template path as `template` configuration
486+
- giving your template path as `--template` parameter to `bump` and `changelog` commands
487+
488+
!!! Note
489+
The path is relative to the current working directory, aka. your project root most of the time,
490+
491+
### Template variables
492+
493+
The default template use a single `tree` variable which is a list of entries (a release) with the following format:
494+
495+
| Name | Type | Description |
496+
| ---- | ---- | ----------- |
497+
| version | `str` | The release version |
498+
| date | `datetime` | The release date |
499+
| changes | `list[tuple[str, list[Change]]]` | The release sorted changes list in the form `(type,changes)` |
500+
501+
Each `Change` has the following fields:
502+
503+
| Name | Type | Description |
504+
| ---- | ---- | ----------- |
505+
| scope | `Optional[str]` | An optionnal scope |
506+
| message | `str` | The commit message body |
507+
508+
!!! Note
509+
The field values depend on the customization class and/or the settings you provide
510+
511+
When using another template (either provided by a plugin or by yourself), you can also pass extra template variables
512+
by:
513+
514+
- defining them in your configuration with the [`extras` settings][extras-config]
515+
- providing them on the commandline with the `--extra/-e` parameter to `bump` and `changelog` commands
516+
517+
[template-config]: config.md#cfg-template
518+
[extras-config]: config.md#cfg-extras

‎tests/commands/test_bump_command.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import inspect
22
import sys
3+
from pathlib import Path
4+
from textwrap import dedent
35
from typing import Tuple
46
from unittest.mock import MagicMock, call
57

@@ -8,6 +10,8 @@
810

911
import commitizen.commands.bump as bump
1012
from commitizen import cli, cmd, git, hooks
13+
from commitizen.changelog import DEFAULT_TEMPLATE
14+
from commitizen.cz.base import BaseCommitizen
1115
from commitizen.exceptions import (
1216
BumpTagFailedError,
1317
CommitizenException,
@@ -853,3 +857,129 @@ def test_bump_use_version_provider(mocker: MockFixture):
853857
get_provider.assert_called_once()
854858
mock.get_version.assert_called_once()
855859
mock.set_version.assert_called_once_with("0.0.1")
860+
861+
862+
@pytest.mark.parametrize(
863+
"arg,cfg,expected",
864+
(
865+
pytest.param("", "", "default", id="default"),
866+
pytest.param("", "changelog.cfg", "from config", id="from-config"),
867+
pytest.param(
868+
"--template=changelog.cmd", "changelog.cfg", "from cmd", id="from-command"
869+
),
870+
),
871+
)
872+
def test_bump_template_option_precedance(
873+
mocker: MockFixture,
874+
tmp_commitizen_project: Path,
875+
mock_plugin: BaseCommitizen,
876+
arg: str,
877+
cfg: str,
878+
expected: str,
879+
):
880+
project_root = Path(tmp_commitizen_project)
881+
cfg_template = project_root / "changelog.cfg"
882+
cmd_template = project_root / "changelog.cmd"
883+
default_template = project_root / mock_plugin.template
884+
changelog = project_root / "CHANGELOG.md"
885+
886+
cfg_template.write_text("from config")
887+
cmd_template.write_text("from cmd")
888+
default_template.write_text("default")
889+
890+
create_file_and_commit("feat: new file")
891+
892+
if cfg:
893+
pyproject = project_root / "pyproject.toml"
894+
pyproject.write_text(
895+
dedent(
896+
f"""\
897+
[tool.commitizen]
898+
version = "0.1.0"
899+
template = "{cfg}"
900+
"""
901+
)
902+
)
903+
904+
testargs = ["cz", "bump", "--yes", "--changelog"]
905+
if arg:
906+
testargs.append(arg)
907+
mocker.patch.object(sys, "argv", testargs + ["0.1.1"])
908+
cli.main()
909+
910+
out = changelog.read_text()
911+
assert out == expected
912+
913+
914+
def test_bump_template_extras_precedance(
915+
mocker: MockFixture,
916+
tmp_commitizen_project: Path,
917+
mock_plugin: BaseCommitizen,
918+
):
919+
project_root = Path(tmp_commitizen_project)
920+
changelog_tpl = project_root / DEFAULT_TEMPLATE
921+
changelog_tpl.write_text("{{first}} - {{second}} - {{third}}")
922+
923+
mock_plugin.template_extras = dict(
924+
first="from-plugin", second="from-plugin", third="from-plugin"
925+
)
926+
927+
pyproject = project_root / "pyproject.toml"
928+
pyproject.write_text(
929+
dedent(
930+
"""\
931+
[tool.commitizen]
932+
version = "0.1.0"
933+
934+
[tool.commitizen.extras]
935+
first = "from-config"
936+
second = "from-config"
937+
"""
938+
)
939+
)
940+
941+
create_file_and_commit("feat: new file")
942+
943+
testargs = [
944+
"cz",
945+
"bump",
946+
"--yes",
947+
"--changelog",
948+
"--extra",
949+
"first=from-command",
950+
"0.1.1",
951+
]
952+
mocker.patch.object(sys, "argv", testargs)
953+
cli.main()
954+
955+
changelog = project_root / "CHANGELOG.md"
956+
assert changelog.read_text() == "from-command - from-config - from-plugin"
957+
958+
959+
def test_bump_template_extra_quotes(
960+
mocker: MockFixture,
961+
tmp_commitizen_project: Path,
962+
):
963+
project_root = Path(tmp_commitizen_project)
964+
changelog_tpl = project_root / DEFAULT_TEMPLATE
965+
changelog_tpl.write_text("{{first}} - {{second}} - {{third}}")
966+
967+
create_file_and_commit("feat: new file")
968+
969+
testargs = [
970+
"cz",
971+
"bump",
972+
"--changelog",
973+
"--yes",
974+
"-e",
975+
"first=no-quote",
976+
"-e",
977+
"second='single quotes'",
978+
"-e",
979+
'third="double quotes"',
980+
]
981+
mocker.patch.object(sys, "argv", testargs)
982+
cli.main()
983+
984+
changelog = project_root / "CHANGELOG.md"
985+
assert changelog.read_text() == "no-quote - single quotes - double quotes"

‎tests/commands/test_changelog_command.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import sys
22
from datetime import datetime
3+
from pathlib import Path
4+
from textwrap import dedent
35

46
import pytest
57
from pytest_mock import MockFixture
68

79
from commitizen import cli, git
10+
from commitizen.changelog import DEFAULT_TEMPLATE
811
from commitizen.commands.changelog import Changelog
12+
from commitizen.cz.base import BaseCommitizen
913
from commitizen.exceptions import (
1014
DryRunExit,
1115
NoCommitsFoundError,
@@ -969,3 +973,119 @@ def test_empty_commit_list(mocker):
969973
mocker.patch.object(sys, "argv", testargs)
970974
with pytest.raises(NoCommitsFoundError):
971975
cli.main()
976+
977+
978+
@pytest.mark.parametrize(
979+
"arg,cfg,expected",
980+
(
981+
pytest.param("", "", "default", id="default"),
982+
pytest.param("", "changelog.cfg", "from config", id="from-config"),
983+
pytest.param(
984+
"--template=changelog.cmd", "changelog.cfg", "from cmd", id="from-command"
985+
),
986+
),
987+
)
988+
def test_changelog_template_option_precedance(
989+
mocker: MockFixture,
990+
tmp_commitizen_project: Path,
991+
mock_plugin: BaseCommitizen,
992+
arg: str,
993+
cfg: str,
994+
expected: str,
995+
):
996+
project_root = Path(tmp_commitizen_project)
997+
cfg_template = project_root / "changelog.cfg"
998+
cmd_template = project_root / "changelog.cmd"
999+
default_template = project_root / mock_plugin.template
1000+
changelog = project_root / "CHANGELOG.md"
1001+
1002+
cfg_template.write_text("from config")
1003+
cmd_template.write_text("from cmd")
1004+
default_template.write_text("default")
1005+
1006+
create_file_and_commit("feat: new file")
1007+
1008+
if cfg:
1009+
pyproject = project_root / "pyproject.toml"
1010+
pyproject.write_text(
1011+
dedent(
1012+
f"""\
1013+
[tool.commitizen]
1014+
version = "0.1.0"
1015+
template = "{cfg}"
1016+
"""
1017+
)
1018+
)
1019+
1020+
testargs = ["cz", "changelog"]
1021+
if arg:
1022+
testargs.append(arg)
1023+
mocker.patch.object(sys, "argv", testargs)
1024+
cli.main()
1025+
1026+
out = changelog.read_text()
1027+
assert out == expected
1028+
1029+
1030+
def test_changelog_template_extras_precedance(
1031+
mocker: MockFixture,
1032+
tmp_commitizen_project: Path,
1033+
mock_plugin: BaseCommitizen,
1034+
):
1035+
project_root = Path(tmp_commitizen_project)
1036+
changelog_tpl = project_root / mock_plugin.template
1037+
changelog_tpl.write_text("{{first}} - {{second}} - {{third}}")
1038+
1039+
mock_plugin.template_extras = dict(
1040+
first="from-plugin", second="from-plugin", third="from-plugin"
1041+
)
1042+
1043+
pyproject = project_root / "pyproject.toml"
1044+
pyproject.write_text(
1045+
dedent(
1046+
"""\
1047+
[tool.commitizen]
1048+
version = "0.1.0"
1049+
1050+
[tool.commitizen.extras]
1051+
first = "from-config"
1052+
second = "from-config"
1053+
"""
1054+
)
1055+
)
1056+
1057+
create_file_and_commit("feat: new file")
1058+
1059+
testargs = ["cz", "changelog", "--extra", "first=from-command"]
1060+
mocker.patch.object(sys, "argv", testargs)
1061+
cli.main()
1062+
1063+
changelog = project_root / "CHANGELOG.md"
1064+
assert changelog.read_text() == "from-command - from-config - from-plugin"
1065+
1066+
1067+
def test_changelog_template_extra_quotes(
1068+
mocker: MockFixture,
1069+
tmp_commitizen_project: Path,
1070+
):
1071+
project_root = Path(tmp_commitizen_project)
1072+
changelog_tpl = project_root / DEFAULT_TEMPLATE
1073+
changelog_tpl.write_text("{{first}} - {{second}} - {{third}}")
1074+
1075+
create_file_and_commit("feat: new file")
1076+
1077+
testargs = [
1078+
"cz",
1079+
"changelog",
1080+
"-e",
1081+
"first=no-quote",
1082+
"-e",
1083+
"second='single quotes'",
1084+
"-e",
1085+
'third="double quotes"',
1086+
]
1087+
mocker.patch.object(sys, "argv", testargs)
1088+
cli.main()
1089+
1090+
changelog = project_root / "CHANGELOG.md"
1091+
assert changelog.read_text() == "no-quote - single quotes - double quotes"

‎tests/conftest.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22
import re
33
import tempfile
44
from pathlib import Path
5+
from typing import Iterator
56

67
import pytest
8+
from pytest_mock import MockerFixture
79

810
from commitizen import cmd, defaults
911
from commitizen.config import BaseConfig
12+
from commitizen.cz.base import BaseCommitizen
1013

1114
SIGNER = "GitHub Action"
1215
SIGNER_MAIL = "action@github.com"
@@ -26,6 +29,14 @@ def git_sandbox(monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
2629
cmd.run(f"git config --global user.email {SIGNER_MAIL}")
2730

2831

32+
@pytest.fixture
33+
def chdir(tmp_path: Path) -> Iterator[Path]:
34+
cwd = os.getcwd()
35+
os.chdir(tmp_path)
36+
yield tmp_path
37+
os.chdir(cwd)
38+
39+
2940
@pytest.fixture(scope="function")
3041
def tmp_git_project(tmpdir):
3142
with tmpdir.as_cwd():
@@ -85,3 +96,18 @@ def config():
8596
@pytest.fixture()
8697
def config_path() -> str:
8798
return os.path.join(os.getcwd(), "pyproject.toml")
99+
100+
101+
class MockPlugin(BaseCommitizen):
102+
def questions(self) -> defaults.Questions:
103+
return []
104+
105+
def message(self, answers: dict) -> str:
106+
return ""
107+
108+
109+
@pytest.fixture
110+
def mock_plugin(mocker: MockerFixture, config: BaseConfig) -> BaseCommitizen:
111+
mock = MockPlugin(config)
112+
mocker.patch("commitizen.factory.commiter_factory", return_value=mock)
113+
return mock

‎tests/test_changelog.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
from pathlib import Path
2+
13
import pytest
4+
from jinja2 import FileSystemLoader
25

36
from commitizen import changelog, defaults, git
47
from commitizen.cz.conventional_commits.conventional_commits import (
@@ -853,6 +856,91 @@ def test_render_changelog(gitcommits, tags, changelog_content):
853856
assert result == changelog_content
854857

855858

859+
def test_render_changelog_from_default_plugin_values(
860+
gitcommits, tags, changelog_content
861+
):
862+
parser = ConventionalCommitsCz.commit_parser
863+
changelog_pattern = ConventionalCommitsCz.bump_pattern
864+
loader = ConventionalCommitsCz.template_loader
865+
template = ConventionalCommitsCz.template
866+
tree = changelog.generate_tree_from_commits(
867+
gitcommits, tags, parser, changelog_pattern
868+
)
869+
result = changelog.render_changelog(tree, loader=loader, template=template)
870+
assert result == changelog_content
871+
872+
873+
def test_render_changelog_override_loader(gitcommits, tags, tmp_path: Path):
874+
loader = FileSystemLoader(tmp_path)
875+
tpl = "loader overridden"
876+
(tmp_path / changelog.DEFAULT_TEMPLATE).write_text(tpl)
877+
parser = ConventionalCommitsCz.commit_parser
878+
changelog_pattern = ConventionalCommitsCz.bump_pattern
879+
tree = changelog.generate_tree_from_commits(
880+
gitcommits, tags, parser, changelog_pattern
881+
)
882+
result = changelog.render_changelog(tree, loader=loader)
883+
assert result == tpl
884+
885+
886+
def test_render_changelog_override_template_from_cwd(gitcommits, tags, chdir: Path):
887+
tpl = "overridden from cwd"
888+
(chdir / changelog.DEFAULT_TEMPLATE).write_text(tpl)
889+
parser = ConventionalCommitsCz.commit_parser
890+
changelog_pattern = ConventionalCommitsCz.bump_pattern
891+
tree = changelog.generate_tree_from_commits(
892+
gitcommits, tags, parser, changelog_pattern
893+
)
894+
result = changelog.render_changelog(tree)
895+
assert result == tpl
896+
897+
898+
def test_render_changelog_override_template_from_cwd_with_custom_name(
899+
gitcommits, tags, chdir: Path
900+
):
901+
tpl = "template overridden from cwd"
902+
tpl_name = "tpl.j2"
903+
(chdir / tpl_name).write_text(tpl)
904+
parser = ConventionalCommitsCz.commit_parser
905+
changelog_pattern = ConventionalCommitsCz.bump_pattern
906+
tree = changelog.generate_tree_from_commits(
907+
gitcommits, tags, parser, changelog_pattern
908+
)
909+
result = changelog.render_changelog(tree, template=tpl_name)
910+
assert result == tpl
911+
912+
913+
def test_render_changelog_override_loader_and_template(
914+
gitcommits, tags, tmp_path: Path
915+
):
916+
loader = FileSystemLoader(tmp_path)
917+
tpl = "loader and template overridden"
918+
tpl_name = "tpl.j2"
919+
(tmp_path / tpl_name).write_text(tpl)
920+
parser = ConventionalCommitsCz.commit_parser
921+
changelog_pattern = ConventionalCommitsCz.bump_pattern
922+
tree = changelog.generate_tree_from_commits(
923+
gitcommits, tags, parser, changelog_pattern
924+
)
925+
result = changelog.render_changelog(tree, loader=loader, template=tpl_name)
926+
assert result == tpl
927+
928+
929+
def test_render_changelog_support_arbitrary_kwargs(gitcommits, tags, tmp_path: Path):
930+
loader = FileSystemLoader(tmp_path)
931+
tpl_name = "tpl.j2"
932+
(tmp_path / tpl_name).write_text("{{ key }}")
933+
parser = ConventionalCommitsCz.commit_parser
934+
changelog_pattern = ConventionalCommitsCz.bump_pattern
935+
tree = changelog.generate_tree_from_commits(
936+
gitcommits, tags, parser, changelog_pattern
937+
)
938+
result = changelog.render_changelog(
939+
tree, loader=loader, template=tpl_name, key="value"
940+
)
941+
assert result == "value"
942+
943+
856944
def test_render_changelog_unreleased(gitcommits):
857945
some_commits = gitcommits[:7]
858946
parser = ConventionalCommitsCz.commit_parser

‎tests/test_conf.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
from __future__ import annotations
2+
13
import json
24
import os
35
from pathlib import Path
6+
from typing import Any
47

58
import pytest
69
import yaml
@@ -41,7 +44,7 @@
4144
}
4245

4346

44-
_settings = {
47+
_settings: dict[str, Any] = {
4548
"name": "cz_jira",
4649
"version": "1.0.0",
4750
"version_provider": "commitizen",
@@ -59,9 +62,11 @@
5962
"pre_bump_hooks": ["scripts/generate_documentation.sh"],
6063
"post_bump_hooks": ["scripts/slack_notification.sh"],
6164
"prerelease_offset": 0,
65+
"template": None,
66+
"extras": {},
6267
}
6368

64-
_new_settings = {
69+
_new_settings: dict[str, Any] = {
6570
"name": "cz_jira",
6671
"version": "2.0.0",
6772
"version_provider": "commitizen",
@@ -79,6 +84,8 @@
7984
"pre_bump_hooks": ["scripts/generate_documentation.sh"],
8085
"post_bump_hooks": ["scripts/slack_notification.sh"],
8186
"prerelease_offset": 0,
87+
"template": None,
88+
"extras": {},
8289
}
8390

8491
_read_settings = {

0 commit comments

Comments
 (0)
Please sign in to comment.