Skip to content

Commit a341977

Browse files
feat(config): add strict_config option to reject unknown config keys
Add a `strict_config` boolean option (default: false). When enabled, commitizen raises InvalidConfigurationError if any key in the config file is not a recognised Settings field. This catches typos early instead of silently ignoring them. - Add `strict_config` to the Settings TypedDict and DEFAULT_SETTINGS - Add `_VALID_CONFIG_KEYS` in base_config derived from Settings.__annotations__ - Validate unknown keys in BaseConfig.update() when strict_config=True - Route all _parse_setting calls through self.update() so validation runs - Add tests covering TOML, JSON, YAML parsers and edge cases Closes #300
1 parent 0acf5d6 commit a341977

6 files changed

Lines changed: 76 additions & 3 deletions

File tree

commitizen/config/base_config.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import TYPE_CHECKING
55

66
from commitizen.defaults import DEFAULT_SETTINGS, Settings
7+
from commitizen.exceptions import InvalidConfigurationError
78

89
if TYPE_CHECKING:
910
import sys
@@ -14,6 +15,8 @@
1415
else:
1516
from typing import Self
1617

18+
_VALID_CONFIG_KEYS: frozenset[str] = frozenset(Settings.__annotations__)
19+
1720

1821
class BaseConfig:
1922
def __init__(self) -> None:
@@ -47,6 +50,12 @@ def set_key(self, key: str, value: object) -> Self:
4750
raise NotImplementedError()
4851

4952
def update(self, data: Settings) -> None:
53+
if self._settings.get("strict_config"):
54+
unknown = sorted(set(data) - _VALID_CONFIG_KEYS)
55+
if unknown:
56+
raise InvalidConfigurationError(
57+
f"Unknown commitizen config keys: {', '.join(unknown)}"
58+
)
5059
self._settings.update(data)
5160

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

commitizen/config/json_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,6 @@ def _parse_setting(self, data: bytes | str) -> None:
6565
raise InvalidConfigurationError(f"Failed to parse {self.path}: {e}")
6666

6767
try:
68-
self.settings.update(doc["commitizen"])
68+
self.update(doc["commitizen"])
6969
except KeyError:
7070
pass

commitizen/config/toml_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,6 @@ def _parse_setting(self, data: bytes | str) -> None:
6565
raise InvalidConfigurationError(f"Failed to parse {self.path}: {e}")
6666

6767
try:
68-
self.settings.update(doc["tool"]["commitizen"]) # type: ignore[index,typeddict-item] # TODO: fix this
68+
self.update(doc["tool"]["commitizen"]) # type: ignore[index,arg-type]
6969
except exceptions.NonExistentKey:
7070
pass

commitizen/config/yaml_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def _parse_setting(self, data: bytes | str) -> None:
5151
raise InvalidConfigurationError(f"Failed to parse {self.path}: {e}")
5252

5353
try:
54-
self.settings.update(doc["commitizen"])
54+
self.update(doc["commitizen"])
5555
except (KeyError, TypeError):
5656
pass
5757

commitizen/defaults.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class Settings(TypedDict, total=False):
6565
version_type: str | None
6666
version: str | None
6767
breaking_change_exclamation_in_title: bool
68+
strict_config: bool
6869

6970

7071
CONFIG_FILES: tuple[str, ...] = (
@@ -115,6 +116,7 @@ class Settings(TypedDict, total=False):
115116
"extras": {},
116117
"breaking_change_exclamation_in_title": False,
117118
"message_length_limit": 0, # 0 for no limit
119+
"strict_config": False,
118120
}
119121

120122
MAJOR = "MAJOR"

tests/test_conf.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,3 +497,65 @@ def test_init_with_invalid_content(self, tmp_path, config_file):
497497
with pytest.raises(InvalidConfigurationError) as excinfo:
498498
YAMLConfig(data=existing_content, path=path)
499499
assert config_file in str(excinfo.value)
500+
501+
502+
class TestStrictConfig:
503+
"""Tests for the strict_config option that rejects unknown config keys."""
504+
505+
def test_toml_strict_config_raises_on_unknown_key(self, tmp_path):
506+
data = """
507+
[tool.commitizen]
508+
strict_config = true
509+
tga_format = "v$version"
510+
"""
511+
path = tmp_path / "pyproject.toml"
512+
with pytest.raises(InvalidConfigurationError, match="tga_format"):
513+
TomlConfig(data=data, path=path)
514+
515+
def test_json_strict_config_raises_on_unknown_key(self, tmp_path):
516+
data = json.dumps(
517+
{"commitizen": {"strict_config": True, "tga_format": "v$version"}}
518+
)
519+
path = tmp_path / ".cz.json"
520+
with pytest.raises(InvalidConfigurationError, match="tga_format"):
521+
JsonConfig(data=data, path=path)
522+
523+
def test_yaml_strict_config_raises_on_unknown_key(self, tmp_path):
524+
data = "commitizen:\n strict_config: true\n tga_format: v$version\n"
525+
path = tmp_path / ".cz.yaml"
526+
with pytest.raises(InvalidConfigurationError, match="tga_format"):
527+
YAMLConfig(data=data, path=path)
528+
529+
def test_strict_config_off_by_default_ignores_unknown_key(self, tmp_path):
530+
data = """
531+
[tool.commitizen]
532+
tga_format = "v$version"
533+
"""
534+
path = tmp_path / "pyproject.toml"
535+
cfg = TomlConfig(data=data, path=path)
536+
assert cfg.settings.get("strict_config") is False
537+
538+
def test_strict_config_allows_all_known_keys(self, tmp_path):
539+
data = """
540+
[tool.commitizen]
541+
strict_config = true
542+
name = "cz_conventional_commits"
543+
tag_format = "v$version"
544+
"""
545+
path = tmp_path / "pyproject.toml"
546+
cfg = TomlConfig(data=data, path=path)
547+
assert cfg.settings["strict_config"] is True
548+
assert cfg.settings["tag_format"] == "v$version"
549+
550+
def test_strict_config_error_lists_all_unknown_keys(self, tmp_path):
551+
data = """
552+
[tool.commitizen]
553+
strict_config = true
554+
typo_one = "foo"
555+
typo_two = "bar"
556+
"""
557+
path = tmp_path / "pyproject.toml"
558+
with pytest.raises(InvalidConfigurationError) as excinfo:
559+
TomlConfig(data=data, path=path)
560+
assert "typo_one" in str(excinfo.value)
561+
assert "typo_two" in str(excinfo.value)

0 commit comments

Comments
 (0)