設定を読み込む用クラス(ConfigReader)

設定を読み込む用クラス(ConfigReader#

  • haniwersの設定ファイルを読み込むための統一的なインターフェースを提供する

  • 設定ファイルは、特定のディレクトリから自動検出、もしくはCLIオプションで明示的に指定する

設定ファイルの優先度#

  1. CLIオプション

    • --config PATH

  2. カレントディレクトリ

    • ./hnw.toml

    • ./config.toml

    • ./config/*toml

  3. XDG準拠(platformdirsパッケージを利用)

    • ./.config/haniwers/hnw.toml

    • ./.config/haniwers/config.toml

    • ./.config/haniwers/*.toml

この中で最初に見つかったパスを採用する

クラス設計#

# config_loader.py
from pathlib import Path
from typing import Optional, Union
from haniwers.config_model import HaniwersConfig, load_config
from platformdirs import user_config_dir

class ConfigLoader:
    def __init__(self, config_path: Optional[Path] = None):
        self._config_path = config_path or self._get_default_config_path()
        self._config = self._load_file(self.config_path)

    @property
    def config(self) -> HaniwersConfig:
        """Get configuration

        @propetryに設定することで、configに直接書き込むことを防ぐ効果がある
        """
        return self._config

    def _load_file(self, path:Path) -> HaniwersConfig:
        if not path.exists():
            raise FileNotFoundError(f"Config file not found: {path}")
        return load_config(path)

    @classmethod
    def get_default_config_path(cls) -> Path:
        """Find the default config file in the standard locations.

        Returns:
            Path: Path to the config file.

        Raises:
            FileNotFoundError: If no config file could be found.
        """
        config_dir = Path(user_config_dir("haniwers"))
        # -> Linux: ~/.config/haniwers/
        # ->
        candidates = [
            Path("./haniwers.toml"),
            Path("./config.toml"),
            *sorted(Path("./config/").glob("*.toml")),
            config_dir / "hnw.toml",
            config_dir / "config.toml",
            *sorted(config_dir.glob("*.toml")),
        ]

        # Add platform-specific paths
        if sys.platform == "win32":
            candidates.append(
                Path(os.environ.get("LOCALAPPDATA", "")) / "haniwers" / "config.toml"
            )

        for path in candidates:
            if path.exists():
                return path

        # Build a more helpful error message
        searched_paths = "\n - ".join([str(p) for p in candidates])
        raise FileNotFoundError(
            f"No config file found in expected locations:\n - {searched_paths}\n"
            "Run 'haniwers init' to create one in the current directory."
        )

def get_default_config_path() -> Path:
    return ConfigLoader.get_default_config_path()

if __name__ == "__main__":
    """Self Test.

    uv run src/haniwers/config_loader.py
    """

    cfg = ConfigLoader()
    cfg.config    # -> HaniwersConfig

  • 設定ファイルのパスを指定して、設定を読み込めるようにする

    • 設定ファイルのパスしていがない場合は、デフォルトのパスを検索する

  • デフォルトパスの検索機能は、外部からも呼び出すことができるようにした

    • ConfigLoader.get_default_config_pathをクラスメソッドにした

    • config_loader.get_default_config_pathで外部から利用できるようにした

    • platformdirsパッケージを利用してクロスプラットフォーム対応する

  • 読み込んだ設定情報は、内部変数_configとして保持する

    • @propetry configで呼び出せるようにする。再代入による書き込みを禁止する

今後の設計方針#

  1. CLI統合との連携

    • --config によるパス指定に対応し、CLI全体に設定を適用

    • typer.Option() などと併用して動的な設定変更も視野に

  2. ConfigPatcher の導入(次フェーズ)

    • CLIオプションでの閾値 (threshold) などの上書き処理を ConfigLoader とは分離

    • ConfigPatcher.apply(cfg, overrides) のような形式で整合的な変更を可能に