今まで、Python でアプリを作成してきて設定ファイルは settings.py
ファイルを使用してきました。
これだと利用する方にソースファイルを編集していただくことになります。
設定ファイルの読み書きをアプリの中で行い、設定画面を提供し、利用する方に分かりやすくしたいと考えました。
そのために設定ファイルとして TOML
ファイルを使うことにしました。
TOML
ファイルがあれば設定画面も作成するようなユーティリティを作成したので紹介します。
目次
- ◆なぜTOMLファイル
- ◆TOMLでやりたいこと
- ◆TOMLファイルの読み書き
- ◆TOMLファイルから設定画面を作成
- ◆設定画面の作成
- ◆設定画面作成メソッド(TomlFileUtilクラス)
- ◆settings.pyを使ったアプリでの互換性
- ■必要なパッケージ
- ◆ソースの取得
- ◆免責事項
- ◆さいごに
- ◆参考
◆なぜTOMLファイル
まずはウィキペディアの解説から
TOMLは、設定ファイルのフォーマットの1種である。「ミニマル」であることを目指した明確な構文を採用することで、読みやすいフォーマットとなるように作られた。TOMLの設定項目は、ディクショナリ構造に明確にマッピングされるように設計されている。 TOML - Wikipedia
また、設定ファイルとして候補に挙がる JSON, YAML, INI, TOML の比較記事 1 2 を参考に自分なりの判断は、
- INI 風で書きやすく見やすい
- コメントが書ける ※今回、コメントは使いませんでした
- 型がある
- Python ライブラリがある
ということで、可能性を感じて設定ファイルに TOML を使ってみることにしました。
解説にある通り、ディクショナリ構造(辞書型)にマッピングして使えます。
Python の辞書と相性が良いです。
◇Python用TOMLライブラリ
今回、使用したのは次の2つのパッケージです。
tomli
:読み込み用tomli-w
:書き込み用
※これらを pip すると toml も入ります
また、tomlkit
を使うとスタイルとコメントを保持することができます。
◇書き方(手書き)
TOML の設定ファイルの書き方を簡単にまとめます。
この記事では TOML ファイルを手書きで書くのが主目的ではないので簡単な説明にします。
詳しい解説はこちらの外部サイト や公式サイト を参照してください。
- コメント:#以降
- キーと値:
キー=値
- キー:
A-Z
,a-z
,0-9
,_
,-
ベア・キー、クォート付きキー、ドット付きキーがあります - 値
- データ型:文字列("")、整数、小数、論理値(true,false)、
日付(yyyy-mm-ddThh:mm:ss
など)、
配列(Pythonのリストと同じ記述) - テーブル:キーと値の集合。INIファイルのグループ相当
例[table]
key=value
[table2]
key=value2
- インラインテーブル:1行で記述するテーブル。
例name = { first = "Python", last = "太郎" }
- データ型:文字列("")、整数、小数、論理値(true,false)、
▽キーの設定サンプル
# この行は全てコメントです。 key = "value" # 行末までコメントです。 bare_key = "value" bare-key = "value" "character encoding" = "value" # クォート付キー physical.color = "orange" # ドット付キー physical.shape = "round" 1234 = "value" int1 = +99 int2 = 42 int3 = 0 int4 = -17 bool1 = true bool2 = false odt1 = 1979-05-27T07:32:00Z odt2 = 1979-05-27T00:32:00-07:00 # 配列 integers = [ 1, 2, 3 ] colors = [ "red", "yellow", "green" ] # テーブル [table-1] key1 = "some string" key2 = 123 [table-2] key1 = "another string" key2 = 456 # インラインテーブル name = { first = "Tom", last = "Preston-Werner" } point = { x = 1, y = 2 }
◆TOMLでやりたいこと
今回、TOML で実装したいのは次の2点です。
Python アプリの設定ファイルとして TOML ファイルを読み書きする
TOMLファイルの中に設定値の定義を入れて設定画面を簡単に作る
TOML は、設定ファイル用の仕様で Python のパッケージも提供されているので、設定ファイルとしての読み書きは難しくありません。
アプリを作成した場合、その設定値を編集する UI をアプリごとに作るのは面倒だなと思います。
そこで、設定ファイルに設定値の定義を持たせてフレキシブルな画面作成ができるように考えてみました。
これらを目的にしたので、目で見て分かりやすい書式の TOML ファイルを手書きで作成するところは、解説していません。
TOML ファイルは、Python のデータとしては辞書型のデータなので、Python のプログラムで辞書を定義して、それをファイルに出力して TOML ファイルを作成しています。
プログラムで出力した TOML ファイルなので読み込みも問題なくできます。
手書きで TOML ファイルを作成した場合、プログラムで読んだらうまく読めないということが起きるかもしれません。プログラムで作成すれば、その問題は起きずに済みます。
◆TOMLファイルの読み書き
tomli
、tomli-w
パッケージを使えば TOML ファイルの読み込み書き込みは簡単です。
- 読み込み:
load()
メソッド - 書き込み:
dump()
メソッド
どちらも通常のファイル操作と併せて使用します。
【コード】
▽読み込み
import tomli, tomli_w with open(path, "rb") as f: toml_dict = tomli.load(f)
▽書き込み
import tomli, tomli_w with open(path, 'wb') as configfile: tomli_w.dump(toml_dict, configfile)
◆TOMLファイルから設定画面を作成
TOML ファイルは設定ファイルとして可読性に優れているとされています。
しかし、プログラミングとかに縁がない方には設定画面が必要です。
アプリごとに設定画面を用意するのが UI としてはユーザーフレンドリーなのでしょうが、そこは少し手抜きさせていただいて、複数のアプリで使える設定画面を検討しました。
具体的には、実際に使う設定値の他に画面作成用の設定値を用意し、それを素に画面を作成します。
そのための TOML ファイルの使い方の仕様を考えました。
◇要件
- 定義用のテーブルを持つ
- デフォルトロード用のテーブルを持つ
- ユーザー用のテーブルを持つ
- ユーザー用は複数のテーブルも可
- 各テーブルにはINIファイルのセクションにあたるテーブル(以下セクション)を持つ
- セクションは定義用など各テーブルで同じ構成とする
- セクション内のキーはテーブル内でユニークとする
- 定義用テーブル内のキーの値は配列とし、キーの説明、型とする
◇テーブル構成
- 定義用テーブル
- セクション1テーブル
- キー1=[キーの説明、型]
- キー2=[キーの説明、型]
- セクション2テーブル
- キー3=[キーの説明、型]
- キー4=[キーの説明、型]
- セクション1テーブル
- デフォルト用テーブル
- セクション1テーブル
- キー1=値1
- キー2=値2
- セクション2テーブル
- キー3=値3
- キー4=値4
- セクション1テーブル
- ユーザー用テーブル
- セクション1テーブル
- キー1=値1
- キー2=値2
- セクション2テーブル
- キー3=値3
- キー4=値4
- セクション1テーブル
◇要件にあったTOMLファイル
TOML ファイルを読んでその TOML ファイルの書式が正しいかどうか悩むのが嫌だったので、TOML ファイルはプログラムで出力することにしました。
Python の場合、辞書型のデータを作成して dump() メソッドで出力すれば、TOML ファイルを作成できます。
▽サンプル TOML ファイル(Python のプログラムで出力したもの)
[DEFINITION."セクション1"] key1 = ["key1の説明","bool",] key2 = ["key2の説明","str",] key3 = ["key3の説明","int",] [DEFINITION."セクション2"] key4 = ["key4の説明","bool",] key5 = ["key5の説明","str",] key6 = ["key6の説明","str",] [DEFINITION."セクション3"] key7 = ["key7の説明","bool",] key8 = ["key8の説明","str",] [DEFAULT."セクション1"] key1 = true key2 = "文字列2" key3 = 800 [DEFAULT."セクション2"] key4 = false key5 = "文字列5" key6 = "文字列6" [DEFAULT."セクション3"] key7 = true key8 = "文字列8" [USER."セクション1"] key1 = true key2 = "文字列2" key3 = 800 [USER."セクション2"] key4 = false key5 = "文字列5" key6 = "文字列6" [USER."セクション3"] key7 = true key8 = "文字列8"
▽プログラムで出力したそのもの
[DEFINITION."セクション1"] key1 = [ "key1の説明", "bool", ] key2 = [ "key2の説明", "str", ] key3 = [ "key3の説明", "int", ] [DEFINITION."セクション2"] key4 = [ "key4の説明", "bool", ] key5 = [ "key5の説明", "str", ] key6 = [ "key6の説明", "str", ] [DEFINITION."セクション3"] key7 = [ "key7の説明", "bool", ] key8 = [ "key8の説明", "str", ] [DEFAULT."セクション1"] key1 = true key2 = "文字列2" key3 = 800 [DEFAULT."セクション2"] key4 = false key5 = "文字列5" key6 = "文字列6" [DEFAULT."セクション3"] key7 = true key8 = "文字列8" [USER."セクション1"] key1 = true key2 = "文字列2" key3 = 800 [USER."セクション2"] key4 = false key5 = "文字列5" key6 = "文字列6" [USER."セクション3"] key7 = true key8 = "文字列8"
◇TOMLファイルの作成
要件にあった TOML ファイルを Python アプリで出力します。
Python で TOML ファイルを作成するのは、単に辞書を定義して出力するだけです。
辞書は、テーブルごとにネストして作成します。
ネストとは、辞書の値に辞書を設定することです。
◎TOMLファイル作成コード
def create_toml_sample(self, path:str): """ tomlファイルを作成する 出力内容を辞書でロジックに定義 tomlファイルを手作業で作ってうまく読めないと悩むより辞書から作った方が間違いないから tomlをjsonにすれば転用できるかも """ toml_data = {} toml_data['DEFINITION'] = { "セクション1":{ "key1":["key1の説明", "bool"] , "key2":["key2の説明", "str"] , "key3":["key3の説明", "int"] }, "セクション2":{ "key4":["key4の説明", "bool"] , "key5":["key5の説明", "str"] , "key6":["key6の説明", "str"] }, "セクション3":{ "key7":["key7の説明", "bool"] , "key8":["key8の説明", "str"] } } toml_data['DEFAULT'] = { "セクション1":{ "key1":True , "key2":"文字列2" , "key3":800 }, "セクション2":{ "key4":False , "key5":"文字列5" , "key6":"文字列6" }, "セクション3":{ "key7":True , "key8":"文字列8" } } toml_data['USER'] = { "セクション1":{ "key1":True , "key2":"文字列2" , "key3":800 }, "セクション2":{ "key4":False , "key5":"文字列5" , "key6":"文字列6" }, "セクション3":{ "key7":True , "key8":"文字列8" } } self.tomlu.dump_toml(toml_data, path) def dump_toml(self, toml_dict:dict, path:str): """ tomlで読み込んだ辞書をtomlファイルに出力する Args: dict: tomlで読み込んだ辞書 str: 保存先ファイル名 """ try: with open(path, 'wb') as configfile: tomli_w.dump(toml_dict, configfile) except exception as e: print(e)
◆設定画面の作成
設定画面作成の考え方はシンプルです。
前述の キー = [ キーの説明、型 ] に合わせてウィジェットを積み重ねていきます。
設定画面に作成したウィジェットの値を利用するにはウィジェット変数を用意する必要があります。
ウィジェット変数は型によって異なるクラスを使用するので、型ごとに処理を分けます。
もちろん、型ごとに作成するウィジェットの種類も変更します。
今回対応した型とその処理は次の通りです。
型 | ウィジェット | ウィジェット変数 |
---|---|---|
str | Label+Entry | StringVar |
int | Label+Entry | IntVar |
bool | CheckButton | BooleanVar |
▷ウィジェットの配置
ウィジェットの配置方法を2つ用意しました。
- pack() による配置
- 単純に上から列挙します
- 説明文は左寄せします
- Entry は設定項目によって幅を変えていないので中央に配置します
- Checkbutton はすべての Checkbutton に表示する文字の最大幅を幅として設定して左寄せします
- grid() による配置
- Checkbutton と説明文はカラム0に Entry はカラム1に配置します
- 説明文は Entry に寄せるため右寄せします
- Checkbutton はすべての Checkbutton に表示する文字の最大幅を幅として設定して左寄せします
- Entry の位置を合わせるため、カラム 0 の余白を伸ばすのにカラム 0 の weight を 1 にします
▽ pack による配置と grid による配置の例
◇処理
- TOML ファイルを読み込んでできた辞書から
DEFINITION
キーの値を取得 - 説明文の最大文字数を求める
DEFINITION
キーの値はセクション名をキーにした辞書なので キーごとにLabelFrame ウィジェットを作成する- セクション名をキーにした値は、設定項目のキーと説明文と型なので設定項目ごとに以下の処理を行う
- grid の場合、カラム0の weight を 1 にする
- 必要なら「設定保存」ボタンを作成する
- 必要なら「デフォルトに戻す」ボタンを作成する
◎設定画面作成コード
def create_frame_from_toml_dict(self, parent:Any , has_save_button:bool=False, has_default_button:bool=False, is_grid:bool=False) -> dict: """ tomlデータからDEFINITIONセクション(キー:[説明, 型]の辞書)を抜き出し、フレームを作成 tomlデータは事前に読んでおく 保存ボタンを表示する場合、ボタンに割り付けるメソッドを後から指定する 例 obj.btn_save.config(command=lambda path=my_path: toml.save_toml(path, "USER")) Args: Any: 主にFrame 親コンテナ bool: 保存用ボタンの有無 bool: labelとentryを横並びにする Returns: dict: ウィジェット変数の辞書 """ toml_dict = self.toml_doc["DEFINITION"] self._var_dict = {} # 説明の最大文字数を求める max_length = max([len(v2[0]) for v1 in toml_dict.values() for v2 in v1.values()]) for group_key, group_item in toml_dict.items(): label_frame = tk.LabelFrame(parent, text=group_key, padx=5, pady=5) label_frame.pack(fill="x", padx=5,pady=5) for i, (k, item) in enumerate(group_item.items()): if item[1] == "str": self._var_dict[k] = tk.StringVar() entry_ = tk.Entry(label_frame, textvariable=self._var_dict[k]) if item[1] == "int": self._var_dict[k] = tk.IntVar() entry_ = tk.Entry(label_frame, textvariable=self._var_dict[k] , validate="key", vcmd=(parent.register(self.entry_validate), "%d", "%S")) if item[1] == "bool": self._var_dict[k] = tk.BooleanVar() checkbutton_ = tk.Checkbutton(label_frame, variable=self._var_dict[k] , text=item[0], anchor="w", width=max_length*2) if is_grid: checkbutton_.grid(row=i, column=0, columnspan=2 , sticky="w") else: checkbutton_.pack() continue label_ = tk.Label(label_frame, text=item[0]) # pack,grid if is_grid: label_.grid(row=i, column=0, sticky="e") entry_.grid(row=i, column=1) else: label_.pack(anchor="w") entry_.pack() if is_grid: label_frame.columnconfigure(0, weight=1) # grid column 0 の余白を引き延ばす if has_save_button: self.btn_save = tk.Button(parent, text="設定保存") self.btn_save.pack(side="bottom", fill="x") if has_default_button: self.btn_default = tk.Button(parent, text="デフォルトに戻す" , command=self.load_default) self.btn_default.pack(side="bottom", fill="x") return self._var_dict
◆設定画面作成メソッド(TomlFileUtilクラス)
前節で説明したものを TomlFileUtil クラスの create_frame_from_toml_dict() メソッドとして作成しました。
使い方を説明します。
◇設定画面作成メソッドの使い方
create_frame_from_toml_dict() メソッド
▶インポート
from toml_file_util import TomlFileUtil
※もちろんインポートでなく、コピーしても使えます
▶メソッド
【構文】
create_frame_from_toml_dict( self, parent:Any, has_save_button:bool=False, has_default_button:bool=False, is_grid:bool=False)
引数
parent
:親ウィジェット(通常 Frame を想定)has_save_button
:保存用ボタンの有無has_default_button
:labelとentryを横並びにするかどうか
戻り値
使い方
- TomlFileUtil クラスのインスタンスを作成
toml = TomlFileUtil()
- TOML ファイルを読み込みます
result = toml.read_toml(ファイルパス)
- メソッドの呼び出し
create_frame_from_toml_dict( 親フレーム )
- 保存ボタンを表示する場合、ボタンに割り付けるメソッドを後から指定します
例obj.btn_save.config( command = lambda path = my_path: toml.save_toml( path, "USER"))
- TomlFileUtil クラスのインスタンスを作成
◇TomlFileUtilクラス
TomlFileUtil クラスについて補足します。
▶属性
path
:TOML ファイルのパスtoml_doc
:TOML ファイルを読んで作成した TOML 辞書_var_dict
:Tkinter variable変数(ウィジェット変数)を値にした辞書
create_frame_from_toml_dict() メソッドで返す
▶メソッド
dump_toml(self, toml_dict:dict, path:str)
:TOML 辞書を TOMLファイルに出力するsave_toml(self, save_path:str="", section:str="", event=None)
:ウィジェット変数の値を section で指定したセクションにコピーして save_path で指定したファイルに保存load_default(self, event=None)
:ウィジェット変数の辞書に TOML 辞書の DEFAULT セクションの値を設定read_toml(self, path:str)
:TOML ファイルを読み込み TOML 辞書を返すcreate_frame_from_toml_dict
:こちらを参照set_toml2var_dict(self, section:str)
:TOML 辞書から section で指定したセクションをウィジェット変数の辞書に設定するset_var_dict2toml_dict(self, section:str)
:ウィジェット変数の辞書の設定値を TOML 辞書の section で指定したセクションに設定する
◆settings.pyを使ったアプリでの互換性
以前に作成したアプリでは、設定ファイルとして settings.py を使いました。
同じアプリをライブラリとして使い、TOML ファイルから設定値を辞書として作成して動かすようにしました。
そのため元々のアプリは、settings.py に定義した変数と値を取得して辞書として作れれば同じように動きます。
その方法を捜し、次のような方法を見つけられたので紹介します。
◇設定ファイルを辞書に
globals() 関数を用いて実装します。
▽例
def get_book_variable_module_name(module_name): module = globals().get(module_name, None) book = {} if module: book = {key: value for key, value in module.__dict__.items() if not (key.startswith('__') or key.startswith('_'))} return book
※globals()はグローバル名前空間にあるシンボル一覧を返します。
シンボル一覧には __dict__
など Python が自動的に割り当てる変数も含まれるため if 節で除きます。
■必要なパッケージ
□必要な Python パッケージ
- tomli
【インストール】pip install tomli
【インポート】 import tomli - tomli-w
【インストール】pip install tomli-w
【インポート】 import tomli-w
◆ソースの取得
全体のソースはこちらから取得できます。
- ソース:toml_file_util.py
- ソース:toml_file_create.py
- 取得先:GitHub juu7g/Python-TOML-util
◆免責事項
ご利用に際しては、『免責事項』をご確認ください。
お気づきの点がございましたら『お問い合わせ』からお問い合わせください。
ただし、回答をお約束するものではありません。
◆さいごに
アプリの設定ファイルに何を使うのか試行錯誤しています。
はじめは手軽さから settings.py を使いました。
しかし、ユーザーフレンドリーではないので、やはり画面の必要性を感じました。
画面には凝りたい気持ちと手間をかけたくない気持ちが交錯します。
間を取って作ってみたのですが、中途半端なものになっていないか心配です。
ご意見があればぜひお聞かせください。
今回、ユーティリティを作ってみて、Python の場合、内部的に辞書でデータを扱うことが多いように思います。
今回作成したユーティリティも TOML ファイルの読み込みのところを JSON の読み込みに変えただけで JSON でも使えるように思います。
◆参考
- ウィキペディア:TOML - Wikipedia
- 日本語解説:Pythonを使ったTOMLの使い方とTOMLの書き方を詳しく解説
- 公式サイト:TOML: Tom's Obvious Minimal Language
- 仕様:TOML: 日本語 v1.0.0-rc.2
- PyPI:tomli · PyPI
- PyPI:tomli-w · PyPI
- 設定ファイル読み込み:How to get a list of variables in specific Python module? - Stack Overflow