From a3419770e14b15607a62e9a53fbd4fcf39a7a43d Mon Sep 17 00:00:00 2001 From: Prayasvadlakonda <84115263+Prayasvadlakonda@users.noreply.github.com> Date: Wed, 6 May 2026 22:28:52 -0400 Subject: [PATCH 1/3] 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 --- commitizen/config/base_config.py | 9 +++++ commitizen/config/json_config.py | 2 +- commitizen/config/toml_config.py | 2 +- commitizen/config/yaml_config.py | 2 +- commitizen/defaults.py | 2 ++ tests/test_conf.py | 62 ++++++++++++++++++++++++++++++++ 6 files changed, 76 insertions(+), 3 deletions(-) diff --git a/commitizen/config/base_config.py b/commitizen/config/base_config.py index f100cf995..9a0702170 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"): + 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..cd1962677 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -497,3 +497,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) From f90769385b0e384ae40c178bd8664c2be586abc6 Mon Sep 17 00:00:00 2001 From: Prayasvadlakonda <84115263+Prayasvadlakonda@users.noreply.github.com> Date: Wed, 6 May 2026 22:33:05 -0400 Subject: [PATCH 2/3] test(config): add strict_config to existing settings fixtures --- tests/test_conf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_conf.py b/tests/test_conf.py index cd1962677..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, } From 0ce9a8628d19fbaea17f5abbd79d531f342a9c34 Mon Sep 17 00:00:00 2001 From: Prayasvadlakonda <84115263+Prayasvadlakonda@users.noreply.github.com> Date: Wed, 6 May 2026 22:37:13 -0400 Subject: [PATCH 3/3] fix(config): check incoming data for strict_config before validating keys --- commitizen/config/base_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commitizen/config/base_config.py b/commitizen/config/base_config.py index 9a0702170..89f3c7923 100644 --- a/commitizen/config/base_config.py +++ b/commitizen/config/base_config.py @@ -50,7 +50,7 @@ def set_key(self, key: str, value: object) -> Self: raise NotImplementedError() def update(self, data: Settings) -> None: - if self._settings.get("strict_config"): + if self._settings.get("strict_config") or data.get("strict_config"): unknown = sorted(set(data) - _VALID_CONFIG_KEYS) if unknown: raise InvalidConfigurationError(