pythonで設定ファイル読み込み処理をconfigparserからtomlibに変えてみた
概要
Pythonアプリケーションでのconfigparserで生じた不具合に対処するため、代わりにtomlを利用する実装に変更しました。
この記事では、tomlibを使用してファイルから設定値を取得する実装例を記載します。
読者層
Pythonでアプリケーション開発を行っており、設定ファイルの読み込みを実装する方が対象。ログレベルをハードコーディングせず、tomlファイルから値を取得する実装を検討している方が対象です。
発生した現象
ログ出力処理の実装と同時に、ログレベルも手軽に切り替えられるよう、設定ファイルを読み取る処理を実装した時に、こんな問題が発生しました。
python main.py
アプリケーションの起動に失敗しました: Bad value substitution: option 'format.console' in section 'logging' contains an interpolation key 'asctime' which is not a valid option name. Raw value: "'%(asctime)s - %(name)s - %(levelname)s - %(message)s'"
どうやら、format.fileの値を取得する際に補間(interpolation)構文として処理されエラーになっているらしい。
このときの該当箇所の実装は以下の通り。
[logging]
format.console = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
import configparser
import os
class ConfigManager:
def __init__(self, config_path='config.ini'):
self.config = configparser.ConfigParser()
# self.config_path = config_path
# self.load_config()
if os.path.exists(config_path):
self.config.read(config_path, encoding='utf-8')
else:
self._set_defaults()
def _set_defaults(self):
"""デフォルト設定の定義"""
self.config['logging'] = {
'file.directory': 'logs',
'file.name': 'project_name.log'
}
def load_config(self):
"""設定ファイルの読み込み"""
self.config.read(self.config_path)
def get_console_log_format(self) -> str:
"""コンソール出力のログフォーマットを取得"""
return self.config.get('logging', 'format.console', fallback='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
原因
ということで調べてみたところ、
ConfigParserはデフォルトでキーの値に含まれる%()構文を文字列補間(interpolation)として解釈する。このため、%(asctime)sのような文字列が補間の対象として誤認識され、エラーが発生したと考えています。
「%(asctime)s - %(name)s - %(levelname)s - %(message)s」をシングルクオートで囲むべき、とか%をエスケープすべき、とった記述を見つけ、試してみたがエラーは解決されませんでした。
結論:tomlib使い方だけ知りたい場合はここだけ読めばOK
あまり時間をかける実装でもないので、良い機会だから比較的新しいtomlを使ってみようと考えた。
そこでPython3.11から利用可能な標準ライブラリ「tomlib」を使用して、以下の通り実装を変更したところ、正常に設定値が取得されるようになった。
[logging.format]
# ログフォーマット指定
console = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
import tomllib
import os
class ConfigManager:
def __init__(self, config_path='config.toml'):
self.config = {}
if os.path.exists(config_path):
with open(config_path, 'rb') as f:
self.config = tomllib.load(f)
else:
self._set_defaults()
def _set_defaults(self):
"""デフォルト設定の定義"""
self.config = {
'logging': {
'format': {
'consol': '%(asctime)s - %(name)s -s %(levelname)s -s %(message)s',
'file': '%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s'
}
}
}
def get_console_log_format(self) -> str:
"""コンソール出力のログフォーマットを取得"""
return self.config.get('logging', {}).get('format', {}).get('console', '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
def get_file_log_format(self) -> str:
"""ファイル出力のログフォーマットを取得"""
return self.config.get('logging', {}).get('format', {}).get('file', '%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s')
設定値をゲッター経由で取得する処理は、以下のように実装しています。
import logging
import os
from datetime import datetime
def setup_logger(module_name=None, log_dir="logs"):
"""AP用のロガーセットアップ
Args:
module_name (str, optional): モジュール名。指定がない場合はプロジェクト名を仕様
log_dir (str): ログファイル保存先ディレクトリ
Returns:
logging.Logger: 設定済みのロガーインスタンス
"""
# ConfigManagerから設定を取得
config = ConfigManager()
console_log_level = getattr(logging, config.get_console_log_level().upper())
file_log_level = getattr(logging, config.get_file_log_level().upper())
元々ゲッター経由で値を取得する実装にしていたため、コンフィグマネージャから値を受け取ったあとの処理については特に修正も不要だったため、非常に楽に移行ができました。
余談:
python3.11よりも前のバージョンを使用している場合、tomliというライブラリが代用できます。
Discussion