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
71 changes: 45 additions & 26 deletions commitizen/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@

import os
import shutil
from pathlib import Path
from typing import Any, NamedTuple

import questionary
import yaml

from commitizen import cmd, factory, out
from commitizen.__version__ import __version__
from commitizen.config import BaseConfig, JsonConfig, TomlConfig, YAMLConfig
from commitizen.config import (
BaseConfig,
)
from commitizen.config.factory import create_config
from commitizen.cz import registry
from commitizen.defaults import CONFIG_FILES, DEFAULT_SETTINGS
from commitizen.exceptions import InitFailedError, NoAnswersError
Expand Down Expand Up @@ -187,45 +191,33 @@ def __call__(self) -> None:
)
out.write("commitizen pre-commit hook is now installed in your '.git'\n")

# Initialize configuration
if "toml" in config_path:
self.config = TomlConfig(data="", path=config_path)
elif "json" in config_path:
self.config = JsonConfig(data="{}", path=config_path)
elif "yaml" in config_path:
self.config = YAMLConfig(data="", path=config_path)

# Create and initialize config
self.config.init_empty_config_content()

self.config.set_key("name", cz_name)
self.config.set_key("tag_format", tag_format)
self.config.set_key("version_scheme", version_scheme)
if version_provider == "commitizen":
self.config.set_key("version", version.public)
else:
self.config.set_key("version_provider", version_provider)
if update_changelog_on_bump:
self.config.set_key("update_changelog_on_bump", update_changelog_on_bump)
if major_version_zero:
self.config.set_key("major_version_zero", major_version_zero)
_write_config_to_file(
path=config_path,
cz_name=cz_name,
version_provider=version_provider,
version_scheme=version_scheme,
version=version,
tag_format=tag_format,
update_changelog_on_bump=update_changelog_on_bump,
major_version_zero=major_version_zero,
)

out.write("\nYou can bump the version running:\n")
out.info("\tcz bump\n")
out.success("Configuration complete 🚀")

def _ask_config_path(self) -> str:
def _ask_config_path(self) -> Path:
default_path = (
"pyproject.toml" if self.project_info.has_pyproject else ".cz.toml"
)

name: str = questionary.select(
filename: str = questionary.select(
"Please choose a supported config file: ",
choices=CONFIG_FILES,
default=default_path,
style=self.cz.style,
).unsafe_ask()
return name
return Path(filename)

def _ask_name(self) -> str:
name: str = questionary.select(
Expand Down Expand Up @@ -369,3 +361,30 @@ def _get_config_data(self) -> dict[str, Any]:
else:
repos.append(CZ_HOOK_CONFIG)
return config_data


def _write_config_to_file(
*,
path: Path,
cz_name: str,
version_provider: str,
version_scheme: str,
version: Version,
tag_format: str,
update_changelog_on_bump: bool,
major_version_zero: bool,
) -> None:
out_config = create_config(path=path)
out_config.init_empty_config_content()

out_config.set_key("name", cz_name)
out_config.set_key("tag_format", tag_format)
out_config.set_key("version_scheme", version_scheme)
if version_provider == "commitizen":
out_config.set_key("version", version.public)
else:
out_config.set_key("version_provider", version_provider)
if update_changelog_on_bump:
out_config.set_key("update_changelog_on_bump", update_changelog_on_bump)
if major_version_zero:
out_config.set_key("major_version_zero", major_version_zero)
58 changes: 23 additions & 35 deletions commitizen/config/__init__.py
Original file line number Diff line number Diff line change
@@ -1,58 +1,46 @@
from __future__ import annotations

from collections.abc import Generator
from pathlib import Path

from commitizen import defaults, git
from commitizen.config.factory import create_config
from commitizen.exceptions import ConfigFileIsEmpty, ConfigFileNotFound

from .base_config import BaseConfig
from .json_config import JsonConfig
from .toml_config import TomlConfig
from .yaml_config import YAMLConfig


def read_cfg(filepath: str | None = None) -> BaseConfig:
conf = BaseConfig()

def _resolve_config_paths(filepath: str | None = None) -> Generator[Path, None, None]:
if filepath is not None:
if not Path(filepath).exists():
out_path = Path(filepath)
if not out_path.exists():
raise ConfigFileNotFound()

cfg_paths = (path for path in (Path(filepath),))
else:
git_project_root = git.find_git_project_root()
cfg_search_paths = [Path(".")]
if git_project_root:
cfg_search_paths.append(git_project_root)
yield out_path
return

cfg_paths = (
path / Path(filename)
for path in cfg_search_paths
for filename in defaults.CONFIG_FILES
)
git_project_root = git.find_git_project_root()
cfg_search_paths = [Path(".")]
if git_project_root:
cfg_search_paths.append(git_project_root)

for filename in cfg_paths:
if not filename.exists():
continue
for path in cfg_search_paths:
for filename in defaults.CONFIG_FILES:
out_path = path / Path(filename)
if out_path.exists():
yield out_path

_conf: TomlConfig | JsonConfig | YAMLConfig

def read_cfg(filepath: str | None = None) -> BaseConfig:
for filename in _resolve_config_paths(filepath):
with open(filename, "rb") as f:
data: bytes = f.read()

if "toml" in filename.suffix:
_conf = TomlConfig(data=data, path=filename)
elif "json" in filename.suffix:
_conf = JsonConfig(data=data, path=filename)
elif "yaml" in filename.suffix:
_conf = YAMLConfig(data=data, path=filename)
conf = create_config(data=data, path=filename)
if not conf.is_empty_config:
return conf

if filepath is not None and _conf.is_empty_config:
if filepath is not None:
raise ConfigFileIsEmpty()
elif _conf.is_empty_config:
continue
else:
conf = _conf
break

return conf
return BaseConfig()
8 changes: 6 additions & 2 deletions commitizen/config/base_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@

class BaseConfig:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be an abstract class?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, a Protocol, but it doesn't hurt like this

Copy link
Contributor Author

@bearomorphism bearomorphism Sep 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I found it odd that we can create BaseConfig objects and it's sometimes confusing. I attempted to refactor it on another branch (not published yet) but many tests would be affected.

def __init__(self) -> None:
self.is_empty_config = False
self._settings: Settings = DEFAULT_SETTINGS.copy()
self.encoding = self.settings["encoding"]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why this member variable exists. It will be stale if self._settings is updated somewhere.

self._path: Path | None = None

@property
Expand All @@ -30,7 +30,7 @@ def path(self) -> Path:
return self._path # type: ignore[return-value]

@path.setter
def path(self, path: str | Path) -> None:
def path(self, path: Path) -> None:
self._path = Path(path)

def set_key(self, key: str, value: Any) -> Self:
Expand All @@ -48,4 +48,8 @@ def _parse_setting(self, data: bytes | str) -> None:
raise NotImplementedError()

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

The implementation is different for each config file type.
"""
raise NotImplementedError()
22 changes: 22 additions & 0 deletions commitizen/config/factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from __future__ import annotations

from pathlib import Path

from commitizen.config.base_config import BaseConfig
from commitizen.config.json_config import JsonConfig
from commitizen.config.toml_config import TomlConfig
from commitizen.config.yaml_config import YAMLConfig


def create_config(*, data: bytes | str | None = None, path: Path) -> BaseConfig:
if "toml" in path.suffix:
return TomlConfig(data=data or "", path=path)
if "json" in path.suffix:
return JsonConfig(data=data or "{}", path=path)
if "yaml" in path.suffix:
return YAMLConfig(data=data or "", path=path)

# Should be unreachable. See the constant CONFIG_FILES.
raise ValueError(
f"Unsupported config file: {path.name} due to unknown file extension"
)
9 changes: 5 additions & 4 deletions commitizen/config/json_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@


class JsonConfig(BaseConfig):
def __init__(self, *, data: bytes | str, path: Path | str) -> None:
def __init__(self, *, data: bytes | str, path: Path) -> None:
super().__init__()
self.is_empty_config = False
self.path = path
self._parse_setting(data)

def init_empty_config_content(self) -> None:
with smart_open(self.path, "a", encoding=self.encoding) as json_file:
with smart_open(
self.path, "a", encoding=self._settings["encoding"]
) as json_file:
json.dump({"commitizen": {}}, json_file)

def set_key(self, key: str, value: Any) -> Self:
Expand All @@ -40,7 +41,7 @@ def set_key(self, key: str, value: Any) -> Self:
parser = json.load(f)

parser["commitizen"][key] = value
with smart_open(self.path, "w", encoding=self.encoding) as f:
with smart_open(self.path, "w", encoding=self._settings["encoding"]) as f:
json.dump(parser, f, indent=2)
return self

Expand Down
9 changes: 5 additions & 4 deletions commitizen/config/toml_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@


class TomlConfig(BaseConfig):
def __init__(self, *, data: bytes | str, path: Path | str) -> None:
def __init__(self, *, data: bytes | str, path: Path) -> None:
super().__init__()
self.is_empty_config = False
self.path = path
self._parse_setting(data)

Expand All @@ -38,7 +37,9 @@ def init_empty_config_content(self) -> None:
if parser.get("tool") is None:
parser["tool"] = table()
parser["tool"]["commitizen"] = table() # type: ignore[index]
output_toml_file.write(parser.as_string().encode(self.encoding))
output_toml_file.write(
parser.as_string().encode(self._settings["encoding"])
)

def set_key(self, key: str, value: Any) -> Self:
"""Set or update a key in the conf.
Expand All @@ -51,7 +52,7 @@ def set_key(self, key: str, value: Any) -> Self:

parser["tool"]["commitizen"][key] = value # type: ignore[index]
with open(self.path, "wb") as f:
f.write(parser.as_string().encode(self.encoding))
f.write(parser.as_string().encode(self._settings["encoding"]))
return self

def _parse_setting(self, data: bytes | str) -> None:
Expand Down
11 changes: 7 additions & 4 deletions commitizen/config/yaml_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@


class YAMLConfig(BaseConfig):
def __init__(self, *, data: bytes | str, path: Path | str) -> None:
def __init__(self, *, data: bytes | str, path: Path) -> None:
super().__init__()
self.is_empty_config = False
self.path = path
self._parse_setting(data)

def init_empty_config_content(self) -> None:
with smart_open(self.path, "a", encoding=self.encoding) as json_file:
with smart_open(
self.path, "a", encoding=self._settings["encoding"]
) as json_file:
yaml.dump({"commitizen": {}}, json_file, explicit_start=True)

def _parse_setting(self, data: bytes | str) -> None:
Expand Down Expand Up @@ -61,7 +62,9 @@ def set_key(self, key: str, value: Any) -> Self:
parser = yaml.load(yaml_file, Loader=yaml.FullLoader)

parser["commitizen"][key] = value
with smart_open(self.path, "w", encoding=self.encoding) as yaml_file:
with smart_open(
self.path, "w", encoding=self._settings["encoding"]
) as yaml_file:
yaml.dump(parser, yaml_file, explicit_start=True)

return self
3 changes: 2 additions & 1 deletion tests/commands/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import pytest

from commitizen import defaults
from commitizen.config import BaseConfig, JsonConfig
from commitizen.config import BaseConfig
from commitizen.config.json_config import JsonConfig


@pytest.fixture()
Expand Down
3 changes: 2 additions & 1 deletion tests/commands/test_init_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import json
import os
import sys
from pathlib import Path
from typing import Any

import pytest
Expand Down Expand Up @@ -87,7 +88,7 @@ def test_init_without_setup_pre_commit_hook(

def test_init_when_config_already_exists(config: BaseConfig, capsys):
# Set config path
path = os.sep.join(["tests", "pyproject.toml"])
path = Path(os.sep.join(["tests", "pyproject.toml"]))
config.path = path

commands.Init(config)()
Expand Down
Loading
Loading