Skip to content
Closed
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
31 changes: 31 additions & 0 deletions commitizen/config/base_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
from typing import TYPE_CHECKING

from commitizen.defaults import DEFAULT_SETTINGS, Settings
from commitizen.exceptions import InvalidConfigurationError

if TYPE_CHECKING:
import sys
from collections.abc import Iterable

# Self is Python 3.11+ but backported in typing-extensions
if sys.version_info < (3, 11):
Expand All @@ -15,6 +17,15 @@
from typing import Self


# Top-level keys that may appear in the commitizen section of a
# configuration file. Derived from the Settings TypedDict plus
# ``annotated_tag_message``, which is read from settings but predates the
# TypedDict (see commitizen/commands/bump.py).
KNOWN_SETTINGS: frozenset[str] = frozenset(
set(Settings.__required_keys__) | set(Settings.__optional_keys__)
) | frozenset({"annotated_tag_message"})


class BaseConfig:
def __init__(self) -> None:
self._settings: Settings = DEFAULT_SETTINGS.copy()
Expand Down Expand Up @@ -49,6 +60,26 @@ def set_key(self, key: str, value: object) -> Self:
def update(self, data: Settings) -> None:
self._settings.update(data)

def _check_unknown_keys(self, keys: Iterable[str]) -> None:
"""Raise when the configuration contains keys that are not known settings.

Only enforced when the ``strict_config`` setting is enabled. Unknown
top-level keys usually indicate a typo in the configuration file
(e.g. ``bump_mesage``); silently ignoring them makes such mistakes
hard to notice. Keys nested under ``customize`` and ``extras`` are
plugin-owned and deliberately not checked.
"""
if not self._settings.get("strict_config"):
return

unknown_keys = sorted(key for key in keys if key not in KNOWN_SETTINGS)
if unknown_keys:
raise InvalidConfigurationError(
f"Unknown configuration key(s) in {self.path}: "
f"{', '.join(unknown_keys)}. "
"Check for typos in your configuration file."
)

def _parse_setting(self, data: bytes | str) -> None:
raise NotImplementedError()

Expand Down
4 changes: 3 additions & 1 deletion commitizen/config/json_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ def _parse_setting(self, data: bytes | str) -> None:
raise InvalidConfigurationError(f"Failed to parse {self.path}: {e}")

try:
self.settings.update(doc["commitizen"])
commitizen_section = doc["commitizen"]
self.settings.update(commitizen_section)
self._check_unknown_keys(commitizen_section.keys())
except KeyError:
pass
6 changes: 5 additions & 1 deletion commitizen/config/toml_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@

if TYPE_CHECKING:
import sys
from collections.abc import Mapping
from pathlib import Path
from typing import Any

# Self is Python 3.11+ but backported in typing-extensions
if sys.version_info < (3, 11):
Expand Down Expand Up @@ -65,6 +67,8 @@ def _parse_setting(self, data: bytes | str) -> None:
raise InvalidConfigurationError(f"Failed to parse {self.path}: {e}")

try:
self.settings.update(doc["tool"]["commitizen"]) # type: ignore[index,typeddict-item] # TODO: fix this
commitizen_section: Mapping[str, Any] = doc["tool"]["commitizen"] # type: ignore[index, assignment]
self.settings.update(commitizen_section) # type: ignore[typeddict-item] # TODO: fix this
self._check_unknown_keys(commitizen_section.keys())
except exceptions.NonExistentKey:
pass
4 changes: 3 additions & 1 deletion commitizen/config/yaml_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ def _parse_setting(self, data: bytes | str) -> None:
raise InvalidConfigurationError(f"Failed to parse {self.path}: {e}")

try:
self.settings.update(doc["commitizen"])
commitizen_section = doc["commitizen"]
self.settings.update(commitizen_section)
self._check_unknown_keys(commitizen_section.keys())
except (KeyError, TypeError):
pass

Expand Down
2 changes: 2 additions & 0 deletions commitizen/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class Settings(TypedDict, total=False):
pre_bump_hooks: list[str] | None
prerelease_offset: int
retry_after_failure: bool
strict_config: bool
style: list[tuple[str, str]]
tag_format: str
template: str | None
Expand Down Expand Up @@ -90,6 +91,7 @@ class Settings(TypedDict, total=False):
"ignored_tag_formats": [],
"bump_message": None, # bumped v$current_version to $new_version
"retry_after_failure": False,
"strict_config": False,
"allow_abort": False,
"allowed_prefixes": [
"Merge",
Expand Down
1 change: 1 addition & 0 deletions docs/config/configuration_file.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ Key configuration categories include:
- **Changelog**: `changelog_file`, `changelog_format`, `changelog_incremental`, `update_changelog_on_bump`
- **Bumping**: `bump_message`, `major_version_zero`, `prerelease_offset`, `pre_bump_hooks`, `post_bump_hooks`
- **Commit Validation**: `allowed_prefixes`, `message_length_limit`, `allow_abort`, `retry_after_failure`
- **Configuration Validation**: `strict_config` - reject unknown keys in the configuration file
- **Customization**: `customize`, `style`, `use_shortcuts`, `template`, `extras`

## Customization
Expand Down
19 changes: 19 additions & 0 deletions docs/config/option.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,25 @@ Style for the prompts.

It will merge this value with default style. See [Styling your prompts with your favorite colors](https://github.com/tmbo/questionary#additional-features) for more details.

## `strict_config`

Reject unknown keys in the `[tool.commitizen]` (or `commitizen`) section of the configuration file.

- Type: `bool`
- Default: `false`

When enabled, any unknown top-level key makes Commitizen fail with an `InvalidConfigurationError` listing the offending keys. This is useful to catch typos such as `bump_mesage` instead of silently ignoring them.

Keys nested under `customize` and `extras` are plugin-owned and are not checked.

**Example**

```toml title="pyproject.toml"
[tool.commitizen]
name = "cz_conventional_commits"
strict_config = true
```

## `customize`

Custom rules for committing and bumping.
Expand Down
60 changes: 60 additions & 0 deletions tests/test_conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
"prerelease_offset": 0,
"encoding": "utf-8",
"always_signoff": False,
"strict_config": False,
"template": None,
"extras": {},
"breaking_change_exclamation_in_title": False,
Expand Down Expand Up @@ -150,6 +151,7 @@
"prerelease_offset": 0,
"encoding": "utf-8",
"always_signoff": False,
"strict_config": False,
"template": None,
"extras": {},
"breaking_change_exclamation_in_title": False,
Expand Down Expand Up @@ -497,3 +499,61 @@ def test_init_with_invalid_content(self, tmp_path, config_file):

with pytest.raises(InvalidConfigurationError, match=re.escape(config_file)):
YAMLConfig(data=existing_content, path=path)


class TestStrictConfig:
@pytest.mark.parametrize(
("config_content", "config_path"),
[
pytest.param(
'[tool.commitizen]\nname = "cz_conventional_commits"\n'
'strict_config = true\nbump_mesage = "typo"\n',
"pyproject.toml",
id="toml",
),
pytest.param(
'{"commitizen": {"name": "cz_conventional_commits", '
'"strict_config": true, "bump_mesage": "typo"}}',
".cz.json",
id="json",
),
pytest.param(
"commitizen:\n name: cz_conventional_commits\n"
" strict_config: true\n bump_mesage: typo\n",
".cz.yaml",
id="yaml",
),
],
)
def test_strict_config_rejects_unknown_keys(
self, tmp_path, config_content, config_path
):
path = tmp_path / config_path
path.write_text(config_content, encoding="utf-8")

with pytest.raises(InvalidConfigurationError, match="bump_mesage"):
config.create_config(data=config_content, path=path)

def test_unknown_keys_allowed_when_strict_config_disabled(self, tmp_path):
path = tmp_path / "pyproject.toml"
path.write_text(
'[tool.commitizen]\nname = "cz_conventional_commits"\nunknown_key = 1\n',
encoding="utf-8",
)

conf = config.create_config(data=path.read_text(), path=path)

assert conf.settings["name"] == "cz_conventional_commits"

def test_known_keys_accepted_when_strict_config_enabled(self, tmp_path):
path = tmp_path / "pyproject.toml"
path.write_text(
'[tool.commitizen]\nname = "cz_conventional_commits"\n'
'strict_config = true\nannotated_tag_message = "bump: $current_version"\n',
encoding="utf-8",
)

conf = config.create_config(data=path.read_text(), path=path)

assert conf.settings["strict_config"] is True
assert conf.settings["annotated_tag_message"] == "bump: $current_version"
Loading