From a5b92a19c46e1abd922dcd6299ae29f6d785f241 Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Mon, 5 Oct 2026 14:47:42 +0800 Subject: [PATCH] docs(customization): verify executable no-bump plugin example --- docs/customization/python_class.md | 26 ++-- docs/examples/cz_docs_only.py | 21 +++ mkdocs.yml | 1 + pyproject.toml | 2 +- .../test_documented_python_class_examples.py | 120 ++++++++++++++++++ 5 files changed, 158 insertions(+), 12 deletions(-) create mode 100644 docs/examples/cz_docs_only.py create mode 100644 tests/commands/test_documented_python_class_examples.py diff --git a/docs/customization/python_class.md b/docs/customization/python_class.md index 9d54d8072..b72b1b3fb 100644 --- a/docs/customization/python_class.md +++ b/docs/customization/python_class.md @@ -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. + + +```python title="cz_docs_only.py" +--8<-- "docs/examples/cz_docs_only.py" ``` + -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 diff --git a/docs/examples/cz_docs_only.py b/docs/examples/cz_docs_only.py new file mode 100644 index 000000000..fa612bf0b --- /dev/null +++ b/docs/examples/cz_docs_only.py @@ -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"} diff --git a/mkdocs.yml b/mkdocs.yml index b786cb5c1..b21112ef9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -101,6 +101,7 @@ markdown_extensions: - codehilite - extra - pymdownx.highlight + - pymdownx.snippets - pymdownx.superfences - toc: permalink: true diff --git a/pyproject.toml b/pyproject.toml index 6d411676c..97adc1493 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 diff --git a/tests/commands/test_documented_python_class_examples.py b/tests/commands/test_documented_python_class_examples.py new file mode 100644 index 000000000..aedca6c0a --- /dev/null +++ b/tests/commands/test_documented_python_class_examples.py @@ -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