mcpField
T
toml-python

toml-python

github.com/jamie-bitflight開発35 回インストール
リポジトリ

toml-python について

Python 開発者向けに、TOML 設定ファイルの操作手順をまとめたスキルです。tomlkit でコメントや書式を保ちながら読み書き・検証・更新する方法と、読み取り専用の tomllib の使い分けを説明します。

主な機能

  1. 01tomlkit による TOML の読み書き
  2. 02コメントと書式の保持
  3. 03TOML ドキュメントの作成と編集
  4. 04解析・検証エラーへの対応
  5. 05tomlkit と tomllib の比較

活用シーン

  1. 01コメントを保って pyproject.toml を更新
  2. 02Python の設定ファイル管理機能の構築
  3. 03TOML の検証と構文エラーの調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/jamie-bitflight/claude_skills --skill toml-python

インストール後、Claude Code で/toml-pythonを入力して読み込まれたか確認します。

詳細

TOML Python 統合

読み取り・変更・書き込みのサイクル中にコメントと書式を保持する tomlkit ライブラリを使って、TOML 設定ファイルを操作します。

このスキルを使用する場面

次の場合にこのスキルを使用します。

  • TOML 設定ファイル(config.toml、pyproject.toml)の読み取りまたは書き込み
  • コメントと書式を保ちながら既存の TOML ファイルを変更する
  • TOML を Python のデータ構造に解析する
  • プログラムで TOML ドキュメントを作成する
  • TOML の構文エラーと検証を処理する
  • Python アプリケーションの設定ファイル管理を実装する
  • 設定ファイルの場所に関する XDG Base Directory 仕様を扱う

主な機能

ライブラリの選択:tomlkit と tomllib

次の場合は tomlkit を使用します。

  • 既存の設定ファイルを変更する(コメントと書式を保持します)
  • 設定を書き込むアプリケーションを構築する
  • 読み取りと書き込みの両方に単一のライブラリが必要
  • Python 3.8 以降との互換性が必要

次の場合は tomllib(標準ライブラリ)を使用します。

  • Python 3.11 以降のみを対象とする
  • 読み取り専用のアクセスで十分(書き込み機能は不要)
  • 依存関係を最小限に抑えたい

設定ファイルの管理には、tomlkit を推奨します。

インストール
# Using uv (recommended)
uv add tomlkit

# Using pip
pip install tomlkit

要件: Python >=3.8、tomlkit >=0.12.0

tomlkit API リファレンス

TOML の読み取り
import tomlkit

# From string
doc = tomlkit.parse(toml_string)
doc = tomlkit.loads(toml_string)  # Alias for parse()

# From file object
with open("config.toml", "r") as f:
    doc = tomlkit.load(f)

# Using TOMLFile class (convenient)
from tomlkit import TOMLFile

toml_file = TOMLFile("config.toml")
doc = toml_file.read()

戻り値: TOMLDocument オブジェクト(辞書のように扱え、書式を保持します)

TOML の書き込み
import tomlkit

# To string
toml_string = tomlkit.dumps(data)

# To file object
with open("config.toml", "w") as f:
    tomlkit.dump(data, f)

# Using TOMLFile class
from tomlkit import TOMLFile

toml_file = TOMLFile("config.toml")
toml_file.write(doc)
TOML ドキュメントの作成
from tomlkit import document, table, comment, nl, array, inline_table

# Create document
doc = document()
doc.add(comment("Configuration file"))
doc.add(nl())
doc.add("title", "My Config")

# Create table
db_config = table()
db_config["host"] = "localhost"
db_config["port"] = 5432
doc["database"] = db_config

# Create inline table
point = inline_table()
point.update({"x": 1, "y": 2})
doc["point"] = point

# Create array
numbers = array()
numbers.extend([1, 2, 3])
doc["numbers"] = numbers
ドキュメントの操作
# Dict-like access
doc["section"]["key"] = "value"
value = doc["section"]["key"]

# Get with default
value = doc.get("key", "default")

# Check existence
if "key" in doc:
    pass

# Iterate
for key, value in doc.items():
    print(key, value)

# Remove key
doc.pop("key")
doc.remove("key")

# Convert to pure Python dict
pure_dict = doc.unwrap()

# Get as TOML string
toml_str = doc.as_string()
値の作成ヘルパー
from tomlkit import (
    item,  # Auto-detect type
    string,  # String with options
    integer,  # Integer
    float_,  # Float
    boolean,  # Boolean
    datetime,  # Datetime
    date,  # Date
    time,  # Time
)

# Auto-detect type
doc["key"] = item(42)
doc["key"] = item([1, 2, 3])
doc["key"] = item({"nested": "table"})

# Explicit string types
doc["basic"] = string("text")
doc["literal"] = string("text", literal=True)  # Single quotes
doc["multiline"] = string("line1\nline2", multiline=True)

エラー処理

例外の種類
from tomlkit.exceptions import (
    TOMLKitError,  # Base exception
    ParseError,  # Syntax errors (has .line and .col)
    NonExistentKey,  # Missing key access
    KeyAlreadyPresent,  # Duplicate key
    ConvertError,  # Type conversion failure
)

# Handle parse errors
try:
    doc = tomlkit.parse(toml_string)
except ParseError as e:
    print(f"Parse error at line {e.line}, column {e.col}: {e}")

# Handle missing keys
try:
    value = doc["nonexistent"]
except (KeyError, NonExistentKey):
    value = "default"

# Handle file not found
try:
    with open("config.toml", "r") as f:
        doc = tomlkit.load(f)
except FileNotFoundError:
    # Create default config
    doc = create_default_config()

よくあるパターン

パターン 1:設定を読み込むか作成する
import tomlkit
from pathlib import Path


def load_or_create_config(path: Path) -> tomlkit.TOMLDocument:
    """Load existing config or create default if missing."""
    if path.exists():
        with open(path, "r") as f:
            return tomlkit.load(f)

    # Create default
    doc = tomlkit.document()
    doc.add(tomlkit.comment("Default configuration"))
    doc.add(tomlkit.nl())

    doc["app"] = tomlkit.table()
    doc["app"]["name"] = "myapp"
    doc["app"]["version"] = "1.0.0"

    path.parent.mkdir(parents=True, exist_ok=True)
    with open(path, "w") as f:
        tomlkit.dump(doc, f)

    return doc
パターン 2:単一の値を更新する(コメントを保持)
import tomlkit


def update_config_value(path: str, section: str, key: str, value):
    """Update single value while preserving all comments."""
    with open(path, "r") as f:
        doc = tomlkit.load(f)

    if section not in doc:
        doc[section] = tomlkit.table()

    doc[section][key] = value

    with open(path, "w") as f:
        tomlkit.dump(doc, f)


# Usage
update_config_value("config.toml", "database", "port", 5433)
パターン 3:アトミックな更新
import tomlkit
from pathlib import Path
import tempfile
import shutil


def atomic_config_update(path: Path, updates: dict):
    """Update config atomically to prevent corruption."""
    with open(path, "r") as f:
        doc = tomlkit.load(f)

    # Apply updates
    for section, values in updates.items():
        if section not in doc:
            doc[section] = tomlkit.table()
        for key, value in values.items():
            doc[section][key] = value

    # Write to temp file, then atomic move
    temp_fd, temp_path = tempfile.mkstemp(suffix=".toml")
    try:
        with open(temp_fd, "w") as f:
            tomlkit.dump(doc, f)
        shutil.move(temp_path, path)
    except Exception:
        Path(temp_path).unlink(missing_ok=True)
        raise
パターン 4:設定の検証
import tomlkit
from tomlkit.exceptions import ParseError


def validate_config(path: str) -> tuple[bool, str]:
    """Validate config structure. Returns (is_valid, error_message)."""
    try:
        with open(path, "r") as f:
            doc = tomlkit.load(f)
    except FileNotFoundError:
        return False, "Config file not found"
    except ParseError as e:
        return False, f"Invalid TOML at line {e.line}, col {e.col}"

    required_sections = ["app", "database"]
    missing = [s for s in required_sections if s not in doc]

    if missing:
        return False, f"Missing sections: {', '.join(missing)}"

    if "name" not in doc.get("app", {}):
        return False, "Missing required key: app.name"

    return True, ""

XDG Base Directory との統合

XDG 仕様に準拠した設定ファイルの場所を使用するには、xdg-base-directory スキルを有効にします。

Skill(skill: "xdg-base-directory:xdg-base-directory")

標準的な設定パスのパターン:

from pathlib import Path


def get_config_path(app_name: str) -> Path:
    """Get XDG-compliant config path."""
    config_dir = Path.home() / ".config" / app_name
    return config_dir / "config.toml"


# Usage
config_path = get_config_path("myapp")
# Returns: ~/.config/myapp/config.toml

TOML 構文クイックリファレンス

基本型
# Strings
string = "Hello, World!"
multiline = """
Multiple
lines
"""
literal = 'C:\path\no\escaping'

# Numbers
integer = 42
float = 3.14
scientific = 1e10

# Boolean
flag = true

# Date/Time
datetime = 2024-01-15T10:30:00Z
date = 2024-01-15
time = 10:30:00
テーブルと配列
# Standard table
[database]
host = "localhost"
port = 5432

# Nested table
[database.pool]
max_connections = 100

# Inline table
point = { x = 1, y = 2 }

# Array
numbers = [1, 2, 3]

# Array of tables
[[products]]
name = "Widget"
price = 9.99

[[products]]
name = "Gadget"
price = 19.99

型の対応関係

TOML 型Python 型
文字列str
整数int
浮動小数点数float
ブール値bool
オフセット付き日時datetime.datetime
ローカル日時datetime.datetime
ローカル日付datetime.date
ローカル時刻datetime.time
配列list
テーブルdict

tomlkit の主な機能

コメントの保持
import tomlkit

original = """
# Configuration file
[database]
# Database host
host = "localhost"
# Database port
port = 5432
"""

doc = tomlkit.parse(original)
doc["database"]["port"] = 5433

result = tomlkit.dumps(doc)
# Comments are preserved in result

理由: 設定ファイルにユーザーが追加したコメントは、アプリケーションの更新後も保持される必要があります。

書式の保持

tomlkit は次の要素を維持します。

  • 元のインデント
  • 空白のパターン
  • キーの順序
  • コメントの配置
  • 引用符のスタイル設定

理由: 設定を変更した際のバージョン管理上の差分を最小限に抑えます。

テーブル作成ヘルパー
from tomlkit import document, table

doc = document()

# Regular table
config = table()
config["key"] = "value"
doc["config"] = config

# Super table (parent of nested tables)
parent = table(is_super_table=True)
child = table()
child["x"] = 1
parent.append("child", child)
doc.append("parent", parent)

print(doc.as_string())
# [parent.child]
# x = 1

よくある落とし穴

問題:コメントが失われる
# ❌ Wrong: Using unwrap() loses formatting
doc = tomlkit.load(f)
pure_dict = doc.unwrap()
# Modifications to pure_dict lose all comments

# ✓ Correct: Modify doc directly
doc = tomlkit.load(f)
doc["section"]["key"] = "value"
# Comments preserved
問題:型が一致しない
# ❌ Wrong: Assuming types
value = doc["port"]  # Might be string or int

# ✓ Correct: Validate types
port = doc["port"]
if not isinstance(port, int):
    raise ValueError(f"Expected int for port, got {type(port)}")
問題:キーが存在しない
# ❌ Wrong: Direct access without checking
value = doc["section"]["key"]  # KeyError if missing

# ✓ Correct: Use .get() with defaults
value = doc.get("section", {}).get("key", "default")

設定ファイルの例

# ~/.config/myapp/config.toml
# Application configuration

[app]
# Application name
name = "myapp"
# Application version
version = "1.0.0"
# Debug mode
debug = false

[database]
# Database connection settings
host = "localhost"
port = 5432
name = "myapp_db"
pool_size = 10

[logging]
# Logging configuration
level = "INFO"
file = "/var/log/myapp/app.log"
max_size_mb = 100

[features]
# Feature flags
enable_api = true
enable_web = true
enable_workers = false

データクラスとの統合パターン

from dataclasses import dataclass
import tomlkit
from pathlib import Path


@dataclass
class AppConfig:
    name: str
    version: str
    debug: bool = False


@dataclass
class DatabaseConfig:
    host: str
    port: int
    name: str
    pool_size: int = 10


@dataclass
class Config:
    app: AppConfig
    database: DatabaseConfig


def load_config(path: Path) -> Config:
    """Load TOML config into dataclasses."""
    with open(path, "r") as f:
        data = tomlkit.load(f)

    return Config(app=AppConfig(**data.get("app", {})), database=DatabaseConfig(**data.get("database", {})))


def save_config(config: Config, path: Path):
    """Save dataclasses to TOML, preserving existing comments."""
    if path.exists():
        with open(path, "r") as f:
            doc = tomlkit.load(f)
    else:
        doc = tomlkit.document()

    # Update from dataclasses
    if "app" not in doc:
        doc["app"] = tomlkit.table()
    doc["app"]["name"] = config.app.name
    doc["app"]["version"] = config.app.version
    doc["app"]["debug"] = config.app.debug

    if "database" not in doc:
        doc["database"] = tomlkit.table()
    doc["database"]["host"] = config.database.host
    doc["database"]["port"] = config.database.port
    doc["database"]["name"] = config.database.name
    doc["database"]["pool_size"] = config.database.pool_size

    path.parent.mkdir(parents=True, exist_ok=True)
    with open(path, "w") as f:
        tomlkit.dump(doc, f)

参考資料

公式ドキュメント
関連スキル
  • xdg-base-directory - XDG 準拠の設定ファイルの場所に関するスキル
  • standards-for-python-development - 共通の Python ルールに関するスキル
  • uv - 依存関係管理に関するスキル
ツール
  • tomlkit - コメントを保持する TOML ライブラリ(読み取り/書き込み)
  • tomllib - 標準ライブラリの TOML パーサー(読み取り専用、Python 3.11 以降)
  • tomli_w - 標準ライブラリ互換の TOML ライター

コメント