Python | dict を toml 形式の文字列に変換する

Python 3.11 にて標準ライブラリに tomllib が追加され、toml ファイルを dict として読み込むことが可能になりました。
しかし、dict を toml として出力する機能がないため、これを実現したい場合は他の手段を検討する必要があります。

toml の構造を厳密に扱って出力したい場合は、外部ライブラリの tomlkit を利用するのが良いと思います。
ただし、dict をとりあえず toml 出力したい、という目的にはあまり向いていません。

この記事では、dict をそれっぽくお手軽に toml 出力するための実装例を紹介します。

参考:

目次

dict を toml に変換するコードサンプル

ちょっと長いですが、dict を toml 文字列に変換するためのサンプルです。
dict を引数にして TomlStr.dumpsを呼び出すと、toml の文字列が返却されます。
dict の階層が深くても、toml_components の再帰でうまく処理させるようにしています。

Python
from datetime import date, datetime, time


class TomlStr:
    @classmethod
    def dumps(cls, _dict):
        return "\n".join(cls.toml_components(_dict, "", is_list=False))

    @classmethod
    def toml_components(cls, _dict, title, is_list):
        single_line_items = []
        multi_line_items = []
        for k, v in _dict.items():
            if v is None:
                raise RuntimeError(f"None value is not allowed. key: {repr(k)}")

            if cls.is_single_line_value(v):
                single_line_items.append((k, v))
            elif cls.is_multi_line_value(v):
                multi_line_items.append((k, v))
            else:
                raise RuntimeError(f"unexpected value. key: {k}, value: {repr(v)}")

        components = []
        if single_line_items:
            lines = []
            if title:
                if is_list:
                    lines.append(f"[[{title}]]")
                else:
                    lines.append(f"[{title}]")
            for k, v in single_line_items:
                lines.append(f"{k} = {cls.value_str(v)}")
            lines.append("")
            components.append("\n".join(lines))

        for k, v in multi_line_items:
            if title:
                full_key = title + "." + k
            else:
                full_key = k
            components.extend(cls.container_components(full_key, v))

        return components

    @classmethod
    def is_single_line_value(cls, v):
        if isinstance(v, (str, int, bool, float, date, time, datetime)):
            # bool は int を継承しているため、列挙せずとも問題ない
            # datetime も date を継承していることから、列挙せずとも問題ない
            # ここでは、分かりやすさのため冗長に列挙している
            return True
        if isinstance(v, list):
            return all([cls.is_single_line_value(elem) for elem in v])
        return False

    @staticmethod
    def is_multi_line_value(v):
        if isinstance(v, dict):
            return True
        if isinstance(v, list):
            return all([isinstance(elem, dict) for elem in v])
        return False

    @classmethod
    def value_str(cls, v):
        if isinstance(v, str):
            return f'"{v}"'
        if isinstance(v, bool):
            return str(v).lower()
        if isinstance(v, (int, float)):
            return v
        if isinstance(v, (date, time, datetime)):
            return v.isoformat()
        if isinstance(v, list):
            return "[" + ", ".join([cls.value_str(elem) for elem in v]) + "]"
        raise RuntimeError("unexpected error")

    @classmethod
    def container_components(cls, full_key, value):
        if isinstance(value, dict):
            return cls.toml_components(value, full_key, is_list=False)
        if isinstance(value, list):
            components = []
            for _dict in value:
                components.extend(cls.toml_components(_dict, full_key, is_list=True))
            return components
        raise RuntimeError("unexpected error")

実行例

読み込み用の toml サンプル

動作確認のため、それっぽい toml ファイルを用意します。

TOML
# toml の読み込みサンプル

item_str = "hello"
item_int = 123
item_float = 1.234
item_bool = true
item_date = 2024-03-31
item_time = 18:06:30.123
item_datetime = 2024-03-31T18:06:30.123
item_datetime_jst = 2024-03-31T18:06:30.123+09:00
item_list = ["1", "2", "3", "4"]
item_complex_list = [["1", "2"], ["3", "4"], "a", [[["abc"]]]]
item_table = { "a" = 1, "b" = 2, "c" = 3 }

[[child_list]]
item_str = "child0"
item_int = 0

[child_list.sub]
item_str = "child_sub"
item_int = 3

[child_list.sub.sub.sub]
item_str = "child_sub_sub_sub"

[[child_list]]
item_str = "child1"
item_int = 1

実行スクリプト

実行用の python スクリプトも準備します。
前述の toml を読み込んで dict に変換し、これを再度 str に変換します。

Python
import tomllib

with open("sample.toml", "rb") as f:
    data = tomllib.load(f)

toml_str = TomlStr.dumps(data)
print(toml_str)

なお、tomllib を使用するため、このスクリプトを実行するためには Python 3.11 以降である必要があります。

実行結果

スクリプトを実行すると、次のように toml 文字列が出力されます。

標準出力
item_str = "hello"
item_int = 123
item_float = 1.234
item_bool = true
item_date = 2024-03-31
item_time = 18:06:30.123000
item_datetime = 2024-03-31T18:06:30.123000
item_datetime_jst = 2024-03-31T18:06:30.123000+09:00
item_list = ["1", "2", "3", "4"]
item_complex_list = [["1", "2"], ["3", "4"], "a", [[["abc"]]]]

[item_table]
a = 1
b = 2
c = 3

[[child_list]]
item_str = "child0"
item_int = 0

[child_list.sub]
item_str = "child_sub"
item_int = 3

[child_list.sub.sub.sub]
item_str = "child_sub_sub_sub"

[[child_list]]
item_str = "child1"
item_int = 1

当然ですが、dict を経由しているため、元ファイルのフォーマットは保全されません。
例えば、1行目のコメントは消えています。
また、インナーテーブルであった item_table 項目は、通常のテーブル形式に展開されています。

構造は気にしないから、それっぽく toml 出力したい!という場面ではある程度有用かと思います。

  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

目次