diff --git a/commitizen/config/base_config.py b/commitizen/config/base_config.py index f100cf995..89f3c7923 100644 --- a/commitizen/config/base_config.py +++ b/commitizen/config/base_config.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING from commitizen.defaults import DEFAULT_SETTINGS, Settings +from commitizen.exceptions import InvalidConfigurationError if TYPE_CHECKING: import sys @@ -14,6 +15,8 @@ else: from typing import Self +_VALID_CONFIG_KEYS: frozenset[str] = frozenset(Settings.__annotations__) + class BaseConfig: def __init__(self) -> None: @@ -47,6 +50,12 @@ def set_key(self, key: str, value: object) -> Self: raise NotImplementedError() def update(self, data: Settings) -> None: + if self._settings.get("strict_config") or data.get("strict_config"): + unknown = sorted(set(data) - _VALID_CONFIG_KEYS) + if unknown: + raise InvalidConfigurationError( + f"Unknown commitizen config keys: {', '.join(unknown)}" + ) self._settings.update(data) def _parse_setting(self, data: bytes | str) -> None: diff --git a/commitizen/config/json_config.py b/commitizen/config/json_config.py index 688a6b9fe..e6ab8363a 100644 --- a/commitizen/config/json_config.py +++ b/commitizen/config/json_config.py @@ -65,6 +65,6 @@ def _parse_setting(self, data: bytes | str) -> None: raise InvalidConfigurationError(f"Failed to parse {self.path}: {e}") try: - self.settings.update(doc["commitizen"]) + self.update(doc["commitizen"]) except KeyError: pass diff --git a/commitizen/config/toml_config.py b/commitizen/config/toml_config.py index 28c05aaa5..0cef3df60 100644 --- a/commitizen/config/toml_config.py +++ b/commitizen/config/toml_config.py @@ -65,6 +65,6 @@ 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 + self.update(doc["tool"]["commitizen"]) # type: ignore[index,arg-type] except exceptions.NonExistentKey: pass diff --git a/commitizen/config/yaml_config.py b/commitizen/config/yaml_config.py index 1e9610e17..9dd709b19 100644 --- a/commitizen/config/yaml_config.py +++ b/commitizen/config/yaml_config.py @@ -51,7 +51,7 @@ def _parse_setting(self, data: bytes | str) -> None: raise InvalidConfigurationError(f"Failed to parse {self.path}: {e}") try: - self.settings.update(doc["commitizen"]) + self.update(doc["commitizen"]) except (KeyError, TypeError): pass diff --git a/commitizen/defaults.py b/commitizen/defaults.py index 4865ccc18..0c7da5b5e 100644 --- a/commitizen/defaults.py +++ b/commitizen/defaults.py @@ -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, ...] = ( @@ -115,6 +116,7 @@ class Settings(TypedDict, total=False): "extras": {}, "breaking_change_exclamation_in_title": False, "message_length_limit": 0, # 0 for no limit + "strict_config": False, } MAJOR = "MAJOR" diff --git a/tests/test_conf.py b/tests/test_conf.py index c004e96e1..681b77089 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -112,6 +112,7 @@ "extras": {}, "breaking_change_exclamation_in_title": False, "message_length_limit": 0, + "strict_config": False, } _new_settings: dict[str, Any] = { @@ -152,6 +153,7 @@ "extras": {}, "breaking_change_exclamation_in_title": False, "message_length_limit": 0, + "strict_config": False, } @@ -497,3 +499,65 @@ 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 TestStrictConfig: + """Tests for the strict_config option that rejects unknown config keys.""" + + def test_toml_strict_config_raises_on_unknown_key(self, tmp_path): + data = """ +[tool.commitizen] +strict_config = true +tga_format = "v$version" +""" + path = tmp_path / "pyproject.toml" + with pytest.raises(InvalidConfigurationError, match="tga_format"): + TomlConfig(data=data, path=path) + + def test_json_strict_config_raises_on_unknown_key(self, tmp_path): + data = json.dumps( + {"commitizen": {"strict_config": True, "tga_format": "v$version"}} + ) + path = tmp_path / ".cz.json" + with pytest.raises(InvalidConfigurationError, match="tga_format"): + JsonConfig(data=data, path=path) + + def test_yaml_strict_config_raises_on_unknown_key(self, tmp_path): + data = "commitizen:\n strict_config: true\n tga_format: v$version\n" + path = tmp_path / ".cz.yaml" + with pytest.raises(InvalidConfigurationError, match="tga_format"): + YAMLConfig(data=data, path=path) + + def test_strict_config_off_by_default_ignores_unknown_key(self, tmp_path): + data = """ +[tool.commitizen] +tga_format = "v$version" +""" + path = tmp_path / "pyproject.toml" + cfg = TomlConfig(data=data, path=path) + assert cfg.settings.get("strict_config") is False + + def test_strict_config_allows_all_known_keys(self, tmp_path): + data = """ +[tool.commitizen] +strict_config = true +name = "cz_conventional_commits" +tag_format = "v$version" +""" + path = tmp_path / "pyproject.toml" + cfg = TomlConfig(data=data, path=path) + assert cfg.settings["strict_config"] is True + assert cfg.settings["tag_format"] == "v$version" + + def test_strict_config_error_lists_all_unknown_keys(self, tmp_path): + data = """ +[tool.commitizen] +strict_config = true +typo_one = "foo" +typo_two = "bar" +""" + path = tmp_path / "pyproject.toml" + with pytest.raises(InvalidConfigurationError) as excinfo: + TomlConfig(data=data, path=path) + assert "typo_one" in str(excinfo.value) + assert "typo_two" in str(excinfo.value)