Skip to content
Open
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: 29 additions & 2 deletions commitizen/config/base_config.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any

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

if TYPE_CHECKING:
import sys
from collections.abc import Mapping

# Self is Python 3.11+ but backported in typing-extensions
if sys.version_info < (3, 11):
Expand Down Expand Up @@ -52,6 +55,30 @@ def update(self, data: Settings) -> None:
def _parse_setting(self, data: bytes | str) -> None:
raise NotImplementedError()

def _validate_known_keys(self, raw_settings: Mapping[str, Any]) -> None:
"""Detect unknown keys in the commitizen section of the config file.

- When ``strict_config`` is ``True`` in ``raw_settings``, raise
:class:`InvalidConfigurationError`.
- Otherwise emit a warning so users notice typos without breaking back
compatibility.
"""
unknown_keys = sorted(k for k in raw_settings if k not in KNOWN_SETTINGS_KEYS)
if not unknown_keys:
return

location = f" in '{self._path}'" if self._path is not None else ""
keys_str = ", ".join(repr(k) for k in unknown_keys)
plural = "keys" if len(unknown_keys) > 1 else "key"
message = (
f"Unknown commitizen configuration {plural}{location}: {keys_str}. "
f"If this is intentional, move the value(s) under the 'extras' setting."
)

if bool(raw_settings.get("strict_config", False)):
raise InvalidConfigurationError(message)
out.warn(message)

def init_empty_config_content(self) -> None:
"""Create a config file with the empty config content.

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

try:
self.settings.update(doc["commitizen"])
section = doc["commitizen"]
except KeyError:
pass
return

self.settings.update(section)
self._validate_known_keys(section)
7 changes: 5 additions & 2 deletions commitizen/config/toml_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ 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
section = doc["tool"]["commitizen"] # type: ignore[index]
except exceptions.NonExistentKey:
pass
return

self.settings.update(section) # type: ignore[typeddict-item] # TODO: fix this
self._validate_known_keys(section) # type: ignore[arg-type]
7 changes: 5 additions & 2 deletions commitizen/config/yaml_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,12 @@ def _parse_setting(self, data: bytes | str) -> None:
raise InvalidConfigurationError(f"Failed to parse {self.path}: {e}")

try:
self.settings.update(doc["commitizen"])
section = doc["commitizen"]
except (KeyError, TypeError):
pass
return

self.settings.update(section)
self._validate_known_keys(section)

def set_key(self, key: str, value: object) -> Self:
with self.path.open("rb") as yaml_file:
Expand Down
5 changes: 5 additions & 0 deletions commitizen/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class Settings(TypedDict, total=False):
version_type: str | None
version: str | None
breaking_change_exclamation_in_title: bool
strict_config: bool


CONFIG_FILES: tuple[str, ...] = (
Expand Down Expand Up @@ -115,8 +116,12 @@ class Settings(TypedDict, total=False):
"extras": {},
"breaking_change_exclamation_in_title": False,
"message_length_limit": 0, # 0 for no limit
"strict_config": False,
}


KNOWN_SETTINGS_KEYS: frozenset[str] = frozenset(Settings.__annotations__)

MAJOR = "MAJOR"
MINOR = "MINOR"
PATCH = "PATCH"
Expand Down
25 changes: 25 additions & 0 deletions docs/config/option.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,31 @@ Custom rules for committing and bumping.

See [customization](../customization/config_file.md) for more details.

## `strict_config`

When enabled, Commitizen raises an error if the configuration file contains
keys that are not recognized as valid commitizen settings (for example because
of a typo such as `update_changelog_on_bumb` instead of
`update_changelog_on_bump`).

When disabled (the default), unknown keys only produce a warning so they can be
spotted without breaking existing setups.

- Type: `bool`
- Default: `False`

**Example**

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

If you intentionally need to keep additional plugin-specific data inside the
commitizen section, put it under the `extras` setting so it is not flagged as
unknown.

## `use_shortcuts`

Show keyboard shortcuts when selecting from a list. When enabled, each choice shows a shortcut key; press that key or use the arrow keys to select.
Expand Down
124 changes: 124 additions & 0 deletions tests/test_conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
"extras": {},
"breaking_change_exclamation_in_title": False,
"message_length_limit": 0,
"strict_config": False,
}

_new_settings: dict[str, Any] = {
Expand Down Expand Up @@ -152,6 +153,7 @@
"extras": {},
"breaking_change_exclamation_in_title": False,
"message_length_limit": 0,
"strict_config": False,
}


Expand Down Expand Up @@ -497,3 +499,125 @@ def test_init_with_invalid_content(self, tmp_path, config_file):
with pytest.raises(InvalidConfigurationError) as excinfo:
YAMLConfig(data=existing_content, path=path)
assert config_file in str(excinfo.value)


class TestUnknownConfigKeys:
"""Validate handling of unknown keys in the commitizen section."""

@pytest.mark.parametrize(
("config_file", "content_template"),
[
(
"pyproject.toml",
'[tool.commitizen]\nname = "cz_conventional_commits"\n{extra}\n',
),
(
".cz.toml",
'[tool.commitizen]\nname = "cz_conventional_commits"\n{extra}\n',
),
(
".cz.json",
'{{"commitizen": {{"name": "cz_conventional_commits"{extra}}}}}',
),
(
".cz.yaml",
"commitizen:\n name: cz_conventional_commits\n{extra}\n",
),
],
)
def test_warns_on_unknown_keys_by_default(
self, tmp_path, monkeypatch, capsys, config_file, content_template
):
monkeypatch.chdir(tmp_path)
if config_file == ".cz.json":
extra = ', "update_changelog_on_bumb": true, "another_typo": 1'
elif config_file == ".cz.yaml":
extra = " update_changelog_on_bumb: true\n another_typo: 1"
else:
extra = "update_changelog_on_bumb = true\nanother_typo = 1"
(tmp_path / config_file).write_text(content_template.format(extra=extra))

cfg = config.read_cfg()
captured = capsys.readouterr()

assert "Unknown commitizen configuration keys" in captured.err
assert "'another_typo'" in captured.err
assert "'update_changelog_on_bumb'" in captured.err
# The unknown keys are still loaded into settings (back-compat) but flagged.
assert cfg.settings["name"] == "cz_conventional_commits"

@pytest.mark.parametrize(
("config_file", "content"),
[
(
"pyproject.toml",
"[tool.commitizen]\n"
'name = "cz_conventional_commits"\n'
"strict_config = true\n"
"update_changelog_on_bumb = true\n",
),
(
".cz.json",
json.dumps(
{
"commitizen": {
"name": "cz_conventional_commits",
"strict_config": True,
"update_changelog_on_bumb": True,
}
}
),
),
(
".cz.yaml",
"commitizen:\n"
" name: cz_conventional_commits\n"
" strict_config: true\n"
" update_changelog_on_bumb: true\n",
),
],
)
def test_raises_on_unknown_keys_when_strict(
self, tmp_path, monkeypatch, config_file, content
):
monkeypatch.chdir(tmp_path)
(tmp_path / config_file).write_text(content)

with pytest.raises(InvalidConfigurationError) as excinfo:
config.read_cfg()
assert "update_changelog_on_bumb" in str(excinfo.value)

@pytest.mark.parametrize(
("config_file", "content_template"),
[
(
"pyproject.toml",
'[tool.commitizen]\nname = "cz_conventional_commits"\n{extra}',
),
(
".cz.json",
'{{"commitizen": {{"name": "cz_conventional_commits"{extra}}}}}',
),
(
".cz.yaml",
"commitizen:\n name: cz_conventional_commits\n{extra}",
),
],
)
def test_no_warning_for_known_keys(
self, tmp_path, monkeypatch, capsys, config_file, content_template
):
monkeypatch.chdir(tmp_path)
if config_file == ".cz.json":
extra = ', "update_changelog_on_bump": true'
elif config_file == ".cz.yaml":
extra = " update_changelog_on_bump: true"
else:
extra = "update_changelog_on_bump = true"

(tmp_path / config_file).write_text(content_template.format(extra=extra))

config.read_cfg()
captured = capsys.readouterr()

assert "Unknown commitizen configuration" not in captured.err
Loading