Skip to content
Draft
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
26 changes: 15 additions & 11 deletions docs/customization/python_class.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,21 +101,25 @@ You need to define 2 parameters inside your custom `BaseCommitizen`.
| `bump_pattern` | `str` | `None` | Regex to extract information from commit (subject and body) |
| `bump_map` | `dict` | `None` | Dictionary mapping the extracted information to a `SemVer` increment type (`MAJOR`, `MINOR`, `PATCH`). Use `None` when a matched rule should not bump the version. |

Let's see an example.

```python title="cz_strange.py"
from commitizen.cz.base import BaseCommitizen


class StrangeCommitizen(BaseCommitizen):
bump_pattern = r"^(break|new|fix|hotfix)"
bump_map = {"break": "MAJOR", "new": "MINOR", "fix": "PATCH", "hotfix": "PATCH"}
If you only need to customize bump behavior, subclassing an existing rule set
keeps the example executable while still overriding the bump rules. The module
below is the same file exercised by Commitizen's test suite and type-checked by
mypy.

<!-- blacken-docs:off -->
```python title="cz_docs_only.py"
--8<-- "docs/examples/cz_docs_only.py"
```
<!-- blacken-docs:on -->

That's it, your Commitizen now supports custom rules, and you can run.
Package and install `cz_docs_only.py` just like the earlier `cz_jira.py`
example, and expose it through the same `commitizen.plugin` entry point group,
for example with `cz_docs_only = cz_docs_only:DocsOnlyPatchCommitizen`.
After installing that package, your Commitizen now supports custom rules, and
you can run:

```bash
cz -n cz_strange bump
cz -n cz_docs_only bump
```

### Filter commits before bump and changelog generation
Expand Down
21 changes: 21 additions & 0 deletions docs/examples/cz_docs_only.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Executable custom bump-rule example published in the documentation."""

from __future__ import annotations

from commitizen.cz.conventional_commits import ConventionalCommitsCz


class DocsOnlyPatchCommitizen(ConventionalCommitsCz):
"""Skip version bumps for docs commits while patch-bumping fixes.

Example:
Configure the plugin as ``cz_docs_only`` to ignore ``docs:`` commits for
version bumps while still treating ``fix:`` commits as patch releases.

Attributes:
bump_pattern: Extracts the commit types that participate in bump logic.
bump_map: Maps ``docs`` to no increment and ``fix`` to a patch bump.
"""

bump_pattern = r"^(docs|fix)(?:\([^()\r\n]*\))?:"
bump_map = {"docs": None, "fix": "PATCH"}
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ markdown_extensions:
- codehilite
- extra
- pymdownx.highlight
- pymdownx.snippets
- pymdownx.superfences
- toc:
permalink: true
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ known-first-party = ["commitizen", "tests"]
convention = "google"

[tool.mypy]
files = ["commitizen", "tests", "scripts"]
files = ["commitizen", "tests", "scripts", "docs/examples"]
disallow_untyped_decorators = true
disallow_subclassing_any = true
warn_return_any = true
Expand Down
120 changes: 120 additions & 0 deletions tests/commands/test_documented_python_class_examples.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""Tests for executable custom-plugin examples published in the docs."""

from __future__ import annotations

import importlib.util
from pathlib import Path
from typing import TYPE_CHECKING, cast

import pytest

from commitizen import git
from commitizen.cz import registry
from commitizen.exceptions import NoneIncrementExit

if TYPE_CHECKING:
from pytest_mock import MockFixture

from commitizen.cz.base import BaseCommitizen
from tests.utils import UtilFixture


DOCUMENTED_PLUGIN_PATH = (
Path(__file__).resolve().parents[2] / "docs" / "examples" / "cz_docs_only.py"
)


def _load_documented_plugin() -> type[BaseCommitizen]:
"""Load the custom plugin example from the file embedded in the docs."""
spec = importlib.util.spec_from_file_location(
"tests_documented_cz_docs_only", DOCUMENTED_PLUGIN_PATH
)
assert spec is not None
assert spec.loader is not None

module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)

return cast("type[BaseCommitizen]", getattr(module, "DocsOnlyPatchCommitizen"))


@pytest.fixture
def documented_plugin_name(mocker: MockFixture) -> str:
"""Expose the documented example through Commitizen's plugin registry."""
plugin_name = "cz_docs_only"
plugin_class = _load_documented_plugin()
mocker.patch.dict("commitizen.cz.registry", {**registry, plugin_name: plugin_class})
return plugin_name


@pytest.mark.parametrize("major_version_zero", [False, True])
@pytest.mark.parametrize(
"commit_message",
["fix: ship executable docs example", "fix(api): ship executable docs example"],
)
@pytest.mark.usefixtures("tmp_commitizen_project")
def test_documented_python_plugin_fix_commit_bumps_patch(
util: UtilFixture,
documented_plugin_name: str,
major_version_zero: bool,
commit_message: str,
) -> None:
"""The documented example bumps a fix commit as a patch release."""
util.create_file_and_commit(commit_message)

args = ["--name", documented_plugin_name, "bump", "--yes"]
if major_version_zero:
args.append("--major-version-zero")
util.run_cli(*args)

assert git.tag_exist("0.1.1") is True


@pytest.mark.parametrize("major_version_zero", [False, True])
@pytest.mark.parametrize(
"commit_message",
["docs: expand plugin guide", "docs(api): expand plugin guide"],
)
@pytest.mark.usefixtures("tmp_commitizen_project")
def test_documented_python_plugin_docs_commit_does_not_bump(
util: UtilFixture,
documented_plugin_name: str,
major_version_zero: bool,
commit_message: str,
) -> None:
"""The documented example treats docs commits as a no-bump match."""
first_bump_args = ["--name", documented_plugin_name, "bump", "--yes"]
if major_version_zero:
first_bump_args.append("--major-version-zero")

util.create_file_and_commit("fix: seed release")
util.run_cli(*first_bump_args)
util.create_file_and_commit(commit_message)

with pytest.raises(NoneIncrementExit):
util.run_cli(*first_bump_args)

assert git.tag_exist("0.1.2") is False


@pytest.mark.parametrize(
"commit_message",
[
"fix!: drop old API",
"fixup! rebase cleanup",
"fixture: rename helper",
],
)
@pytest.mark.usefixtures("tmp_commitizen_project")
def test_documented_python_plugin_ignores_nonmatching_fix_prefixes(
util: UtilFixture, documented_plugin_name: str, commit_message: str
) -> None:
"""Only plain fix commits with an optional scope participate in bumping."""
util.create_file_and_commit("fix: seed release")
util.run_cli("--name", documented_plugin_name, "bump", "--yes")
util.create_file_and_commit(commit_message)

with pytest.raises(NoneIncrementExit):
util.run_cli("--name", documented_plugin_name, "bump", "--yes")

assert git.tag_exist("0.1.2") is False
Loading