【Python】argparseとPydanticとPyYAMLでYAML/JSON設定検証CLIを作ってみた
公開:
設定ファイルの書き間違いは、アプリを起動して初めて気づくことが多いです。そこでYAMLとJSONの設定を読み込み、必須項目と型をその場で点検するCLIをconfig_validator.pyとして作りました。引数の受け口はargparse、検証の本体はPydantic、YAML解析はPyYAMLという構成です。
initで安全なサンプルを作り、validateで必須項目欠落や型不一致をフィールドパス付きに示すところまで用意しました。実際に5回のコマンドで動かした結果まで、順を追って紹介します。
Pydanticの基本概念、要件定義、実装、動作確認までを順番に学べる構成です。動画は目次から確認したい場面へ移動でき、本文だけでも手順と考え方が完結します。
動画の内容をテキストで確認する
オープニング。Pydanticとargparse・PyYAMLを使ってYAML・JSON設定検証CLIを作るカリキュラムを始めます。概要紹介。
Pydanticとargparse・PyYAMLの役割と使い方を学ぶYAML・JSON設定検証CLIの要件を整理する 完成コードと実行結果を確認する 最後に実コマンドとファイル状態で完成挙動を確かめる 具体的にやること。
initサブコマンドでカレント配下の作業ディレクトリを作成する 正常系と異常系を含む4件のサンプル設定を書き出す 既存の同名ファイルをskip表示にして上書きしない 作成件数と定義済み件数と検証例のコマンドを案内するvalidateサブコマンドで1件以上のパスを受け取る 実装環境・必須アプリ。
OS:Windows 11 Pro Python:3.13.3シェル:PowerShell 5.1必須アプリ:コードエディター、ターミナル、エクスプローラー パッケージ:pip、"pydantic>=2"、PyYAML Pydanticとargparse・PyYAMLとは。
Pydantic:Pydanticは、クラスへ書いた型注釈をそのまま入力データの検査ルールとして使えるデータ検証ライブラリargparse:argparseはPythonへ最初から入っているコマンドライン引数の解析モジュールで、PyYAMLはYAMLを読み書きするライブラリ Pydanticで作る設定検証CLIの要点。
先頭に置く設定ファイル検証の見出し行 成功時に続く5行の設定要約 失敗時に出る検出エラー件数付きの見出し YAML・JSON設定検証CLIの要件定義。
init実行でapp.yamlとservice.jsonが作成される 作成したサンプル件数と定義済み件数が表示される 正常なYAML設定が[OK]と要約付きで並ぶ 必須項目が欠落したYAMLが[NG]と検出エラー件数で出るserver.portのようなフィールドパスがエラー行に出る 型が一致しないJSONで入力値付きのエラーが並ぶ INTRO: Monaco EditorでYAML・JSON設定検証CLIを実装。
コードを1行ずつ入力し、補完と自動インデントを使いながら実行結果を確認します。LINE 001: モジュール全体の概要説明。このスクリプトがargparse・Pydantic・PyYAMLを組み合わせた設定ファイル検証CLIであることを説明するモジュールdocstringの書き出しです。
ファイル全体の目的を最初に示す役割を持っています。LINE 003: initサブコマンドの説明。initサブコマンドを実行すると、新しい作業ディレクトリに安全なサンプルYAML・JSON設定ファイルを作成することを説明しています。
利用者が最初に読む使い方ガイドの一部です。LINE 004: validateサブコマンドの説明1。validateサブコマンドが拡張子に応じてYAMLとJSONそれぞれの必須項目と型を検証することを説明しています。
処理の分岐方針をあらかじめ示す文です。LINE 005: validateサブコマンドの説明2。必須項目の欠落や型の不一致をフィールドパス付きで表示し、最後に件数を含む要約を表示する流れを説明しています。
検証結果の見せ方を事前に案内しています。LINE 006: docstringの終端。モジュールdocstringを閉じる記号です。
ここまでの説明文がファイル冒頭の解説として確定します。LINE 008: 型ヒントの将来構文を有効化。未来のPython構文である型ヒントの評価方法を先取りして有効にする宣言です。
list[str]のような新しい書き方を古いPythonでも安全に使えるようにします。LINE 010: argparseモジュールの読み込み。コマンドライン引数を解析するための標準ライブラリargparseを読み込んでいます。
init・validateのサブコマンドを実現するために使います。LINE 011: jsonモジュールの読み込み。JSON形式の読み書きを行うための標準ライブラリjsonを読み込んでいます。
設定ファイルの出力や読み込みで利用します。LINE 012: Pathクラスの読み込み。ファイルパスをオブジェクトとして扱うためのPathクラスを読み込んでいます。
ディレクトリやファイルの存在確認・作成に使います。LINE 013: 型ヒント用の型を読み込み。任意の型を表すAnyと、限定された文字列だけを許すLiteralを読み込んでいます。
設定モデルの型定義に使用します。LINE 015: PyYAMLモジュールの読み込み。YAML形式を読み書きするための外部ライブラリyamlを読み込んでいます。
YAML設定ファイルの解析と出力に使います。LINE 016: Pydanticの主要部品を読み込み。データ検証モデルの基底クラスBaseModel、フィールド定義用のField、検証失敗時の例外ValidationErrorを読み込んでいます。
設定の型検証の中核となる部品です。LINE 021: サーバ設定モデルの定義開始。サーバに関する設定項目をまとめるServerConfigクラスを定義しています。
Pydanticのモデルとして必須項目と型を宣言する土台になります。LINE 022: ホスト名フィールドの定義。サーバのホスト名を表すhostフィールドを文字列型として必須にしています。
値が指定されない場合は検証エラーになります。LINE 023: ポート番号フィールドの定義。ポート番号を表すportフィールドを整数型とし、1以上65535以下の範囲に制限しています。
範囲外の値を指定すると検証エラーになります。LINE 024: ワーカー数フィールドの定義。ワーカー数を表すworkersフィールドを整数型とし、既定値1で1以上64以下に制限しています。
指定を省略した場合は自動的に1が使われます。LINE 027: データベース設定モデルの定義開始。データベースに関する設定項目をまとめるDatabaseConfigクラスを定義しています。
接続先の種類や名前などを検証するためのモデルです。LINE 028: データベース種別フィールドの定義。engineフィールドをpostgresql・mysql・sqliteのいずれかに限定するLiteral型で定義しています。
指定した値以外はすべて検証エラーになります。LINE 029: データベース名フィールドの定義。データベース名を表すnameフィールドを文字列型として必須にしています。
空欄や未指定は検証エラーの対象です。LINE 030: 接続プールサイズフィールドの定義。接続プールの数を表すpool_sizeフィールドを整数型とし、既定値5を設定しています。
省略時は5件として扱われます。LINE 031: タイムアウト秒数フィールドの定義。接続待ちのタイムアウト秒数を表すtimeout_secondsフィールドを浮動小数点型とし、既定値5.0を設定しています。
省略時は5.0秒として扱われます。LINE 034: ログ設定モデルの定義開始。ログ出力に関する設定項目をまとめるLoggingConfigクラスを定義しています。
ログレベルとファイル出力有無を検証するためのモデルです。LINE 035: ログレベルフィールドの定義。levelフィールドをDEBUG・INFO・WARNING・ERRORのいずれかに限定し、既定値をINFOにしています。
省略時は情報レベルのログとして扱われます。LINE 036: ファイル出力フラグの定義。ログをファイルへ出力するかどうかを表すto_fileフィールドを真偽値型とし、既定値をFalseにしています。
省略時はファイル出力しない設定になります。LINE 039: アプリ全体設定モデルの定義開始。アプリケーション全体の設定をまとめるAppConfigクラスを定義しています。
これまで定義した個別モデルをまとめて検証する最上位のモデルです。LINE 040: アプリ名フィールドの定義。アプリケーション名を表すapp_nameフィールドを文字列型として必須にしています。
表示や識別に使われる名前です。LINE 041: 環境フィールドの定義。environmentフィールドをdevelopment・staging・productionのいずれかに限定しています。
指定外の値は検証エラーになります。LINE 042: デバッグフラグの定義。デバッグモードの有効・無効を表すdebugフィールドを真偽値型とし、既定値をFalseにしています。
省略時は本番相当の動作として扱われます。LINE 043: サーバ設定の埋め込み。serverフィールドとしてServerConfigモデルを埋め込み、サーバ設定全体を必須項目にしています。
ネストした構造ごと検証できるようにしています。LINE 044: データベース設定の埋め込み。databaseフィールドとしてDatabaseConfigモデルを埋め込み、データベース設定全体を必須項目にしています。
ネストした構造ごと検証できるようにしています。LINE 045: ログ設定の埋め込みと既定値。loggingフィールドにLoggingConfigモデルを埋め込み、指定がない場合は既定のLoggingConfigインスタンスを自動生成するようにしています。
LINE 046: 許可ホスト一覧フィールドの定義。allowed_hostsフィールドを文字列のリスト型とし、指定がない場合は空リストを自動生成するようにしています。RUN 1/9: 設定モデルの必須項目を確認する。
AppConfigまでのクラス定義を書き終えた時点です。model_fieldsを覗くと、型注釈だけを書いた項目が必須として扱われていることを確認できます。CHECK 1/9: 途中実行に成功。
AppConfigまでのクラス定義を書き終えた時点です。model_fieldsを覗くと、型注釈だけを書いた項目が必須として扱われていることを確認できます。RETURN 01: エディターへ戻りました。
入力済みのコードを維持したまま、次の実装へ進みます。LINE 051: 正常なYAMLサンプルの定義開始。正常なYAML設定として使うサンプルデータVALID_YAML_SAMPLEの辞書リテラルを定義し始めています。
すべての必須項目を満たした値になります。LINE 052: アプリ名の値設定。サンプル設定のapp_nameに「在庫管理API」という文字列を設定しています。
正常なアプリ名の例として使われます。LINE 053: 環境の値設定。サンプル設定のenvironmentに「production」を設定しています。
本番環境向けの正常な設定例として使われます。LINE 054: デバッグフラグの値設定。サンプル設定のdebugにFalseを設定しています。
本番環境らしくデバッグを無効にした状態を表しています。LINE 055: サーバ設定の値設定。サンプル設定のserverにホスト・ポート・ワーカー数を含む辞書を設定しています。
ServerConfigの必須項目をすべて満たす正常な値です。LINE 056: データベース設定の定義開始。サンプル設定のdatabaseキーに対応する辞書を定義し始めています。
DatabaseConfigの各項目をこの後の行で設定していきます。LINE 057: データベース種別の値設定。データベース設定のengineに「postgresql」を設定しています。
DatabaseConfigで許可された種別のひとつです。LINE 058: データベース名の値設定。データベース設定のnameに「inventory」を設定しています。
在庫管理に対応するデータベース名の例です。LINE 059: プールサイズの値設定。データベース設定のpool_sizeに10を設定しています。
既定値の5より多めの接続プールを想定した値です。LINE 060: タイムアウト秒数の値設定。データベース設定のtimeout_secondsに3.5を設定しています。
既定値5.0より短いタイムアウトを指定した例です。LINE 061: データベース設定の辞書を閉じる。database辞書の定義を閉じています。
ここまでの内容がdatabaseフィールドの値としてまとめられます。LINE 062: ログ設定の値設定。サンプル設定のloggingにレベルINFO、ファイル出力を有効にした辞書を設定しています。
LoggingConfigの項目を上書きする例です。LINE 063: 許可ホスト一覧の値設定。サンプル設定のallowed_hostsに2件のホスト名を含むリストを設定しています。
アクセスを許可するホストの例として使われます。LINE 064: YAMLサンプル辞書を閉じる。VALID_YAML_SAMPLEの辞書リテラルを閉じています。
ここまでの値がYAMLサンプルファイルとして書き出される内容になります。LINE 066: 正常なJSONサンプルの定義開始。正常なJSON設定として使うサンプルデータVALID_JSON_SAMPLEの辞書リテラルを定義し始めています。
YAMLサンプルとは異なる値でもう一つの正常例を用意しています。LINE 067: アプリ名の値設定。サンプル設定のapp_nameに「配送バッチ」という文字列を設定しています。
バッチ処理向けの正常なアプリ名の例です。LINE 068: 環境の値設定。サンプル設定のenvironmentに「staging」を設定しています。
ステージング環境向けの正常な設定例として使われます。LINE 069: デバッグフラグの値設定。サンプル設定のdebugにTrueを設定しています。
ステージング環境らしくデバッグを有効にした状態を表しています。LINE 070: サーバ設定の値設定。サンプル設定のserverにローカルホスト・ポート9000・ワーカー数2を含む辞書を設定しています。
ServerConfigの必須項目を満たす別の正常な値です。LINE 071: データベース設定の定義開始。サンプル設定のdatabaseキーに対応する辞書を定義し始めています。
この後の行でsqlite向けの値を設定していきます。LINE 072: データベース種別の値設定。データベース設定のengineに「sqlite」を設定しています。
DatabaseConfigで許可された種別のひとつです。LINE 073: データベース名の値設定。データベース設定のnameに「delivery」を設定しています。
配送処理に対応するデータベース名の例です。LINE 074: プールサイズの値設定。データベース設定のpool_sizeに3を設定しています。
既定値の5より少ない接続プールを想定した値です。LINE 075: タイムアウト秒数の値設定。データベース設定のtimeout_secondsに1.5を設定しています。
既定値5.0より短いタイムアウトを指定した例です。LINE 076: データベース設定の辞書を閉じる。database辞書の定義を閉じています。
ここまでの内容がdatabaseフィールドの値としてまとめられます。LINE 077: ログ設定の値設定。サンプル設定のloggingにレベルDEBUG、ファイル出力を無効にした辞書を設定しています。
開発中を想定したログ設定の例です。LINE 078: 許可ホスト一覧の値設定。サンプル設定のallowed_hostsに1件のホスト名を含むリストを設定しています。
バッチ処理向けの許可ホストの例です。LINE 079: JSONサンプル辞書を閉じる。VALID_JSON_SAMPLEの辞書リテラルを閉じています。
ここまでの値がJSONサンプルファイルとして書き出される内容になります。LINE 081: 必須項目欠落サンプルの定義開始。必須項目が欠けた不正な設定例MISSING_FIELD_SAMPLEの辞書リテラルを定義し始めています。
検証エラーの表示を確認するためのサンプルです。LINE 082: アプリ名の値設定。サンプル設定のapp_nameに「必須項目欠落サンプル」と分かる文字列を設定しています。
どのサンプルかを見分けやすくする目的があります。LINE 083: デバッグフラグの値設定。サンプル設定のdebugにFalseを設定しています。
このサンプルではenvironmentフィールドがあえて設定されていません。LINE 084: サーバ設定の値設定(不完全)。サンプル設定のserverにhostだけを含む辞書を設定しています。
ServerConfigで必須のportが欠けているため検証エラーの対象になります。LINE 085: database欠落サンプルの値。database設定にengineだけを指定し、必須項目のnameをわざと省略しています。
検証時に必須項目欠落エラーを再現するためのサンプル値です。LINE 086: logging欠落サンプルの値。logging設定にlevelだけを指定し、to_fileは省略していますがデフォルト値があるためエラーにはなりません。
あくまでMISSING_FIELD_SAMPLE全体としてserverやdatabaseの必須項目を欠落させるためのデータです。LINE 087: 辞書リテラルの終端。MISSING_FIELD_SAMPLEという辞書定義を閉じる行です。
これでhost以外のserver項目やname以外のdatabase項目が欠けたサンプルデータが完成します。LINE 089: 型不一致サンプルの定義開始。TYPE_MISMATCH_SAMPLEという変数名で、型が一致しないデータを持つ辞書の定義を開始しています。
検証処理で型エラーを表示するためのサンプルです。LINE 090: アプリ名の設定。app_nameに文字列で「配送バッチ(型不一致サンプル)」を設定しています。
ここは型としては正しいので後続の型不一致検証には影響しません。LINE 091: 環境設定の値。environmentに許可された文字列の一つである「staging」を設定しています。
この項目自体は正しい型なのでエラー対象にはなりません。LINE 092: debugに文字列を誤設定。本来はbool型であるべきdebugに「ときどき」という文字列を入れています。
これによりPydanticの型検証で不一致エラーが発生する仕組みです。LINE 093: serverの型不一致データ。portとworkersに本来int型であるべきところへ文字列「ポート未定」「four」を設定しています。
数値に変換できない文字列のため型エラーとして検出されます。LINE 094: databaseブロックの開始。databaseキーに対応する辞書の定義を開始しています。
この中に型が一致しない項目や正しい項目が混在しています。LINE 095: データベースエンジンの指定。engineに許可された値の一つ「mysql」を設定しています。
この項目自体は正しい型なのでエラーにはなりません。LINE 096: データベース名の指定。nameに文字列「delivery」を設定しています。
型としては問題なく、正常な値として扱われます。LINE 097: pool_sizeに小数を設定。本来int型のpool_sizeに小数の2.5を設定しています。
Pydanticの検証で整数として扱えない値として型不一致になります。LINE 098: timeout_secondsの指定。float型のtimeout_secondsに1.5を設定しています。
型としては正しいためエラーにはならない項目です。LINE 099: databaseブロックの終端。database辞書の定義を閉じる行です。
ここまでの内容でpool_sizeの型不一致だけが検証対象として残ります。LINE 100: loggingの正常な値。loggingにlevel「INFO」とto_file「False」を設定しています。
どちらも正しい型なのでここではエラーになりません。LINE 101: allowed_hostsに文字列を誤設定。本来list型であるべきallowed_hostsに単一の文字列「batch.example.local」を設定しています。
リストではないため型不一致として検出される項目です。LINE 102: 辞書リテラルの終端。TYPE_MISMATCH_SAMPLEという辞書定義を閉じる行です。
これでdebug・server・database・allowed_hostsに型不一致を含むサンプルデータが完成します。LINE 104: サンプルファイル一覧の定義開始。SAMPLE_FILESという変数名で、ファイル名・データ・説明文をタプルにしたリストの定義を開始しています。
これがinitコマンドで作成するファイル群のもとになります。LINE 105: 正常なYAMLサンプルの登録。ファイル名app.yamlと先ほど定義したVALID_YAML_SAMPLE、説明文をタプルとして1件登録しています。
initコマンドで作成されるファイルの1つ目です。LINE 106: 正常なJSONサンプルの登録。ファイル名service.jsonとVALID_JSON_SAMPLE、説明文をタプルとして登録しています。
正常なJSON形式の設定ファイルを作成するための情報です。LINE 107: 必須項目欠落サンプルの登録。ファイル名missing_required.yamlとMISSING_FIELD_SAMPLE、説明文をタプルとして登録しています。
検証時にエラーを確認できるサンプルファイルです。LINE 108: 型不一致サンプルの登録。ファイル名type_mismatch.jsonとTYPE_MISMATCH_SAMPLE、説明文をタプルとして登録しています。
これも検証時にエラーを確認できるサンプルファイルです。LINE 109: リストリテラルの終端。SAMPLE_FILESというリスト定義を閉じる行です。
これで4種類のサンプルファイル情報がまとまって管理できるようになります。RUN 2/9: サンプル設定の定義一覧を確認する。4種類のサンプルと説明文をまとめたSAMPLE_FILESを定義したところです。
どのファイル名にどの内容が対応するかを一覧で表示します。CHECK 2/9: 途中実行に成功。4種類のサンプルと説明文をまとめたSAMPLE_FILESを定義したところです。
どのファイル名にどの内容が対応するかを一覧で表示します。RETURN 02: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。
LINE 114: render_sample関数の定義。ファイル名とデータを受け取り、拡張子に応じた文字列形式に変換して返す関数を定義しています。initコマンドでファイル内容を作る際に使われます。
LINE 115: YAML拡張子かどうかの判定。ファイル名が.yamlまたは.ymlで終わるかどうかを調べています。該当する場合はYAML形式として出力する分岐に入ります。
LINE 116: YAML形式への変換開始。yaml.safe_dumpを呼び出してデータをYAML文字列に変換し、その結果を返しています。安全な変換方式を使うことで意図しない型の混入を防いでいます。
LINE 117: YAML変換のオプション指定。日本語をそのまま出力するallow_unicode、キーの並び順を保つsort_keys=False、見やすい複数行形式にするdefault_flow_style=Falseを指定しています。読みやすいYAMLを生成するための設定です。
LINE 118: safe_dump呼び出しの終端。yaml.safe_dumpの呼び出しを閉じる行です。これによりYAML形式の設定内容が文字列として得られます。
LINE 119: JSON形式への変換と改行付与。YAML以外の場合はjson.dumpsでデータをJSON文字列に変換し、末尾に改行を付けて返しています。ensure_ascii=Falseにより日本語をそのまま出力し、indent=2で見やすく整形しています。
LINE 122: create_samples関数の定義。作業ディレクトリのパス文字列を受け取り、サンプルファイルを作成した結果のメッセージ一覧を返す関数を定義しています。initコマンドの中心となる処理です。
LINE 123: パスの正規化。受け取ったtarget_dirをホームディレクトリ展開・絶対パス化して変数baseに格納しています。相対パスや~表記でも安全に扱えるようにする準備です。
LINE 124: カレントディレクトリの取得。現在の作業ディレクトリを絶対パスとして取得し、変数cwdに格納しています。次の行でbaseがカレント配下にあるかを確認するために使います。
LINE 125: 作業ディレクトリの安全確認。指定先がカレントディレクトリそのもの、またはカレント配下でない場合を判定しています。これにより意図しない場所へのファイル作成を防いでいます。
LINE 126: 不正なディレクトリ指定時の中止メッセージ。前の行の条件に該当した場合、処理を中止する旨のメッセージをリストとして返しています。関数はここで終了し、以降の作成処理は行われません。
LINE 127: 同名ファイル存在の確認。指定先パスが既に存在していて、かつディレクトリではない場合を判定しています。ファイルと同名でディレクトリを作れない状況を検出しています。
LINE 128: 同名ファイル存在時の中止メッセージ。前の行の条件に該当した場合、同名のファイルが既にある旨のメッセージを返して処理を中止しています。誤って既存ファイルを壊さないための安全策です。
LINE 130: 作業ディレクトリの作成。baseで指定したディレクトリを作成しています。parents=Trueで親ディレクトリもまとめて作成し、exist_ok=Trueで既に存在していてもエラーにしません。
LINE 131: 出力メッセージリストの初期化。これから画面に表示する文字列を格納するlinesというリストを作成し、作業ディレクトリのパスを先頭メッセージとして入れています。LINE 132: 作成件数カウンタの初期化。
実際に新規作成したファイル数を数えるための変数createdを0で初期化しています。後のループ内でファイルを作成するたびに加算されます。LINE 133: サンプルファイル一覧のループ開始。
SAMPLE_FILESに登録された4件のタプルを1件ずつ取り出し、ファイル名・データ・説明文をname・data・noteに割り当てて繰り返し処理しています。LINE 134: 作成先パスの組み立て。作業ディレクトリbaseとファイル名nameを結合し、実際に書き込む先のパスを変数pathに格納しています。
LINE 135: 既存ファイルの確認。path.exists()で同名のファイルが既に存在するかどうかを判定しています。存在する場合は上書きを避けるための分岐に入ります。
LINE 136: スキップメッセージの追加。既に存在するファイルについて、上書きしない旨のメッセージをlinesに追加しています。ユーザーに何が起きたかを分かりやすく伝えるための表示です。
LINE 137: ループの次周回へ移動。continueによってこのファイルに対する以降の作成処理をスキップし、次のサンプルファイルの処理に進んでいます。LINE 138: サンプルファイルの書き込み。
render_sample関数でファイル名とデータから生成した文字列を、指定パスにUTF-8で書き込んでいます。ここで実際にファイルが作成されます。LINE 139: 作成件数の加算。
ファイルを1件作成できたので、createdの値を1増やしています。最終的な作成件数の集計に使われます。LINE 140: 作成完了メッセージの追加。
作成したファイル名・説明文・ファイルサイズを含むメッセージをlinesに追加しています。ファイルサイズはpath.stat().st_sizeで取得したバイト数です。LINE 142: 作成件数サマリの追加。
実際に作成したファイル数と、定義済みサンプルの総数をまとめたメッセージをlinesに追加しています。全体の実行結果を一目で確認できるようにしています。LINE 143: 検証コマンド例の追加。
作成したサンプルファイルをvalidateサブコマンドで確認するための実行例メッセージをlinesに追加しています。ユーザーが次に何をすればよいかを示す案内です。LINE 144: 結果メッセージリストの返却。
ここまで組み立てたlinesを呼び出し元に返しています。main関数側でこのリストが画面へ出力されます。RUN 3/9: サンプルファイルを実際に作成する。
create_samplesが完成し、カレント配下のディレクトリへサンプルを書き出せるようになりました。戻り値の表示行をそのまま出力し、作成件数まで確かめます。CHECK 3/9: 途中実行に成功。
create_samplesが完成し、カレント配下のディレクトリへサンプルを書き出せるようになりました。戻り値の表示行をそのまま出力し、作成件数まで確かめます。RETURN 03: エディターへ戻りました。
入力済みのコードを維持したまま、次の実装へ進みます。LINE 149: detect_format関数の定義。パスを受け取り、その拡張子に対応するフォーマット名の文字列を返す関数を定義しています。
validate処理で読み込み方法を決めるために使われます。LINE 150: 拡張子からフォーマットを判定。辞書に.yaml・.yml・.jsonそれぞれに対応するフォーマット名を持たせ、getメソッドで小文字化した拡張子に一致する値を取り出しています。
LINE 151: 未対応拡張子時のデフォルト値。一致する拡張子が辞書に見つからなかった場合のデフォルト値として「未対応」を指定しています。これにより想定外の拡張子でもエラーにならず処理を続けられます。
LINE 152: get呼び出しの終端。getメソッドの呼び出しを閉じる行です。この結果がdetect_format関数の戻り値として使われます。
LINE 155: load_config関数の定義。パスとフォーマット名を受け取り、ファイルの内容を読み込んでPython上のデータ構造として返す関数を定義しています。LINE 156: ファイル内容の読み込み。
指定されたパスのファイルをUTF-8エンコーディングでテキストとして読み込み、変数textに格納しています。LINE 157: YAML形式かどうかの判定。引数fmtが「YAML」であるかどうかを調べています。
該当する場合は次の行でYAMLとして解析する処理に進みます。LINE 158: YAML文字列の解析。yaml.safe_loadを使ってYAML形式のテキストを安全にPythonのデータ構造へ変換し、その結果を返しています。
安全な読み込み方式のため任意コードの実行を防いでいます。LINE 159: JSON形式かどうかの判定。引数fmtが「JSON」であるかどうかを調べています。
該当する場合は次の行でJSONとして解析する処理に進みます。LINE 160: JSON文字列の解析。json.loadsを使ってJSON形式のテキストをPythonのデータ構造へ変換し、その結果を返しています。
LINE 161: 未対応拡張子時のエラー送出。YAMLでもJSONでもない場合に、対応していない拡張子である旨のValueErrorを発生させています。拡張子がない場合の表示も考慮された分かりやすいメッセージです。
RUN 4/9: 拡張子から形式を判定する。detect_formatとload_configを書き終えた状態です。3種類のファイル名を渡し、YAMLとJSONと未対応の判定が意図どおりかを確認します。
CHECK 4/9: 途中実行に成功。detect_formatとload_configを書き終えた状態です。3種類のファイル名を渡し、YAMLとJSONと未対応の判定が意図どおりかを確認します。
RETURN 04: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。LINE 166: field_path関数の定義。
Pydanticのエラー情報に含まれる場所を表すタプルlocを受け取り、人が読みやすい文字列に変換する関数を定義しています。LINE 167: フィールドパス文字列の組み立て。locの各要素を文字列化してドットで連結し、フィールドパスとして返しています。
locが空の場合はルート自体のエラーとみなし「(ルート)」という文字列を返します。LINE 170: classify関数の定義。Pydanticのエラー種別を表す文字列error_typeを受け取り、人が読みやすい分類名の文字列を返す関数を定義しています。
LINE 171: エラー種別の分類。error_typeが"missing"かどうかを判定し、必須項目の欠落なのか、それとも型や値の不一致なのかを日本語のラベル文字列で返しています。この結果は後でエラーメッセージの見出しとして使われます。
LINE 174: エラー整形関数の定義。Pydanticが返す1件のエラー情報を受け取り、画面表示用の1行の文字列に整形する関数を定義しています。この関数はエラー件数分だけ繰り返し呼び出されます。
LINE 175: 入力値の文字列化。エラーに含まれる入力値をrepr()で文字列表現に変換しています。値がどんな型であっても、そのまま表示できる形に整えています。
LINE 176: 文字列長のチェック。文字列化した入力値の長さが60文字を超えていないかを確認しています。長すぎる値をそのまま表示すると画面が見づらくなるための準備です。
LINE 177: 長い入力値の省略。60文字を超える場合は先頭57文字だけを残し、末尾に"..."を付けて短く省略しています。これにより表示が長くなりすぎるのを防いでいます。
LINE 178: エラー種別ラベルの取得。エラー情報からtypeの値を取り出し、classify関数に渡して「必須項目の欠落」か「型・値の不一致」かのラベルを取得しています。LINE 179: 整形済みエラー行の組み立て。
ラベル、フィールドパス、エラーメッセージ、入力値をまとめて1行の文字列として返しています。この文字列がそのまま検証結果の一覧に表示されます。LINE 182: ファイル検証関数の定義。
1件の設定ファイルパスを受け取り、読み込みと検証を行った結果を辞書にまとめて返す関数を定義しています。LINE 183: パスオブジェクトの作成。文字列で渡されたファイルパスをPathオブジェクトに変換しています。
以降の処理でファイルの拡張子取得などが扱いやすくなります。LINE 184: ファイル形式の判定。拡張子からYAMLかJSONかを判定し、fmt変数に格納しています。
この結果は後続の読み込み処理と表示に使われます。LINE 185: 検証結果辞書の初期化開始。ファイルごとの検証結果をまとめて保持するための辞書resultを作り始めています。
以降の行でこの辞書の各項目を初期値で埋めていきます。LINE 186: パス情報の格納。検証対象のファイルパスをそのまま結果辞書に保存しています。
後で結果を表示する際にどのファイルの結果かを示すために使われます。LINE 187: ファイル形式の格納。先ほど判定したファイル形式(YAMLまたはJSON)を結果辞書に保存しています。
表示時にファイル種別を示すために使われます。LINE 188: 検証成功フラグの初期化。検証が成功したかどうかを示すokの初期値をFalseに設定しています。
後で検証に成功した場合のみTrueに書き換えられます。LINE 189: 検証済み設定の初期化。検証済みのAppConfigオブジェクトを入れる項目configをNoneで初期化しています。
検証が成功した場合にここへ結果が入ります。LINE 190: 読み込みエラーの初期化。ファイル読み込み時に発生したエラーメッセージを入れるread_errorをNoneで初期化しています。
エラーが起きた場合のみここに文字列が入ります。LINE 191: 検証エラー一覧の初期化。Pydanticの検証で見つかったエラーを格納するリストerrorsを空リストで初期化しています。
後でエラーがあればここに追加されます。LINE 192: 辞書リテラルの終了。ここまでの項目を持つ結果辞書resultの定義を閉じています。
この辞書が関数の最終的な戻り値のベースになります。LINE 193: 読み込み処理の開始。ファイルの読み込みでエラーが発生する可能性があるため、try文で処理を囲み始めています。
LINE 194: 設定ファイルの読み込み。load_config関数を呼び出し、指定されたパスと形式に従って設定ファイルの中身をraw変数へ読み込んでいます。LINE 195: 読み込みエラーの捕捉。
ファイルが存在しない場合やYAML・JSONの構文が壊れている場合など、想定される例外をまとめて捕捉しています。LINE 196: エラーメッセージの記録。捕捉した例外の種類とメッセージを文字列にまとめ、結果辞書のread_errorに保存しています。
LINE 197: 読み込み失敗時の早期返却。読み込みに失敗した時点でPydanticの検証に進む意味がないため、ここで結果辞書をそのまま返して処理を終えています。LINE 199: 検証処理の開始。
読み込んだ設定データをPydanticモデルで検証する処理をtry文で囲み始めています。LINE 200: モデルによる検証実行。AppConfig.model_validateにraw変数を渡し、必須項目や型が正しいかを検証しています。
成功すればAppConfigのインスタンスがconfigに格納されます。LINE 201: 検証成功フラグの更新。検証が例外なく完了したことを示すため、okをTrueに更新しています。
LINE 202: 検証エラーの捕捉。必須項目の欠落や型の不一致があった場合に発生するValidationErrorを捕捉しています。LINE 203: エラー一覧の取得。
捕捉した例外からerrors()メソッドでエラーの詳細一覧を取り出し、結果辞書のerrorsに保存しています。LINE 204: 検証結果の返却。読み込みと検証を終えた結果辞書resultを呼び出し元へ返しています。
この辞書は後でレポート作成に使われます。RUN 5/9: 1件の設定ファイルを検証する。validate_fileが完成したので、型が一致しないサンプルを書き出してから検証します。
形式と成否、検出したエラー件数が辞書へ入ることを確認できます。CHECK 5/9: 途中実行に成功。validate_fileが完成したので、型が一致しないサンプルを書き出してから検証します。
形式と成否、検出したエラー件数が辞書へ入ることを確認できます。RETURN 05: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。
LINE 209: 要約生成関数の定義。検証に成功したAppConfigを受け取り、内容を表示用の文字列リストにまとめる関数を定義しています。LINE 210: 許可ホストの文字列化。
許可ホストのリストがあればカンマ区切りの文字列に変換し、なければ「指定なし」という文字列を用意しています。LINE 211: 要約リストの開始。設定内容を説明する複数行の文字列をまとめたリストの定義を開始しています。
LINE 212: アプリ情報行の作成。アプリ名と環境の値を1行の文字列にまとめています。この行はデバッグ設定の説明行と結合されます。
LINE 213: デバッグ状態の付加。debugの値が真偽値かによって「有効」または「無効」という日本語に変換し、先ほどのアプリ情報行に続けています。LINE 214: サーバ情報行の作成。
サーバのホストとポート番号をまとめた文字列を作成しています。この行はworkers情報の行と結合されます。LINE 215: ワーカー数の付加。
サーバのworkers設定値を文字列に含め、サーバ情報行を完成させています。LINE 216: データベース情報行の作成。データベースのエンジン名と名前をまとめた文字列を作成しています。
この行はプール設定の行と結合されます。LINE 217: プール設定の付加。pool_sizeとtimeout_secondsの値を文字列に含め、データベース情報行を完成させています。
LINE 218: ログ設定行の作成。ログレベルの値をまとめた文字列を作成しています。この行はファイル出力設定の行と結合されます。
LINE 219: ファイル出力設定の付加。to_fileの値が真偽値かによって「有効」または「無効」という日本語に変換し、ログ設定行を完成させています。LINE 220: 許可ホスト行の作成。
許可ホストの件数と内容をまとめた文字列を作成しています。件数は許可ホストリストの長さから求めています。LINE 221: 要約リストの終了。
ここまで作成した各行をまとめたリストの定義を閉じ、関数の戻り値として確定させています。RUN 6/9: 検証済み設定の要約を確認する。summarize_configを追加した時点です。
正常なサンプルをモデルへ通し、アプリ名から許可ホスト件数までの5行がどのように並ぶかを見ます。CHECK 6/9: 途中実行に成功。summarize_configを追加した時点です。
正常なサンプルをモデルへ通し、アプリ名から許可ホスト件数までの5行がどのように並ぶかを見ます。RETURN 06: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。
LINE 226: 検証実行関数の定義。複数の設定ファイルパスを受け取り、それぞれ検証したうえでレポート用の文字列リストを組み立てる関数を定義しています。LINE 227: 全ファイルの検証実行。
渡された各パスに対してvalidate_file関数を呼び出し、それぞれの検証結果をresultsというリストにまとめています。LINE 228: レポート見出しの初期化。表示するレポートの先頭行として、検証セクションの見出し文字列を持つリストlinesを作成しています。
LINE 229: 結果ごとのループ開始。検証結果を1件ずつ取り出し、それぞれの状態に応じた表示内容を組み立てるループを開始しています。LINE 230: 読み込みエラーの判定。
読み込み時にエラーが発生していたかどうかをread_errorの有無で確認しています。LINE 231: 読込NG見出しの追加。読み込みに失敗したファイルについて、パスと形式を含む「読込NG」の見出し行をレポートに追加しています。
LINE 232: 読み込みエラー詳細の追加。読み込み時に記録されたエラーメッセージをレポートに追加し、失敗した理由を表示できるようにしています。LINE 233: 検証成功の判定。
読み込みエラーがなく検証にも成功していた場合の分岐に入っています。LINE 234: OK見出しの追加。検証に成功したファイルについて、パスと形式を含む「OK」の見出し行をレポートに追加しています。
LINE 235: 設定要約の追加。summarize_config関数で作成した設定内容の要約行をレポートに追加しています。LINE 236: 検証失敗時の分岐。
読み込みエラーも成功もしていない、つまり検証エラーがあった場合の処理に入っています。LINE 237: NG見出しの組み立て開始。検証に失敗したファイルの見出し行を作成する処理を開始しています。
LINE 238: NGラベルとパスの組み立て。ファイルパスと形式を含む「NG」の文字列部分を作成しています。LINE 239: 検出エラー件数の付加。
そのファイルで検出されたエラーの件数を先ほどの文字列に続けています。LINE 240: NG見出し行の追加。組み立てた文字列をレポートのlinesに追加し、NG見出し行を完成させています。
LINE 241: エラー一覧のループ開始。検証で見つかった個々のエラー情報を1件ずつ取り出すループを開始しています。LINE 242: 整形済みエラー行の追加。
format_error関数で各エラーを整形した文字列をレポートに追加しています。LINE 243: 区切り空行の追加。ファイルごとの表示の後に空行を追加し、次のファイルの結果と見やすく区切っています。
LINE 245: YAML件数の集計。検証結果の中で形式がYAMLだったものの件数を数えて変数yaml_countに格納しています。LINE 246: JSON件数の集計。
検証結果の中で形式がJSONだったものの件数を数えて変数json_countに格納しています。LINE 247: 検証成功件数の集計。検証結果のうちokがTrueだったものの件数を数えて変数ok_countに格納しています。
LINE 248: 読み込みエラー件数の集計。検証結果のうちread_errorが設定されていたものの件数を数えて変数read_error_countに格納しています。LINE 249: 検証失敗件数の算出。
結果件数からOK件数と読み込みエラー件数を引いて、検証に失敗した件数を求めています。この値は後でサマリ表示に使われます。LINE 250: 必須項目欠落件数の集計開始。
すべての検証結果に含まれるエラーのうち、種類がmissingのものだけを数え始めています。sum関数とジェネレータ式を使って件数を数えています。LINE 251: missingタイプのエラーを抽出。
各結果のエラー一覧を走査し、エラータイプがmissingと一致するものだけを1件としてカウント対象にしています。必須項目が欠けているエラーを数えるための条件です。LINE 252: missing_count集計の締めくくり。
sum関数の呼び出しを閉じており、条件に合ったエラーの合計数がmissing_countに代入されます。必須項目欠落の総件数がここで確定します。LINE 253: 型不一致件数の集計開始。
missing以外のエラー、つまり型や値の不一致によるエラーの件数を数え始めています。missing_countと似た形のsum式です。LINE 254: missing以外のエラーを抽出。
各結果のエラーのうち、タイプがmissingでないものだけを1件としてカウントしています。型不一致や値の不正といったエラーを集計する条件です。LINE 255: mismatch_count集計の締めくくり。
sum関数の呼び出しを閉じて、型・値の不一致によるエラーの合計数がmismatch_countに代入されます。これで4つの集計値がすべて揃います。LINE 257: サマリ見出しの追加。
lines配列にサマリセクションの見出し文字列を追加しています。これ以降、集計結果をまとめた行が続いていくことを示す区切りです。LINE 258: 対象ファイル数の表示開始。
検証対象となったファイルの総数と、YAML・JSONそれぞれの件数を表示する行の追加を開始しています。f文字列を使って結果を整形しています。LINE 259: 対象ファイル件数の文字列内容。
results全体の件数と、先に集計したyaml_count・json_countを埋め込んで、対象ファイルの内訳を1行の文字列として組み立てています。LINE 260: 対象ファイル行の追加終了。append呼び出しを閉じており、組み立てた対象ファイル件数の文字列がlinesリストに1行として追加されます。
LINE 261: 検証済み件数の表示。検証に成功した設定ファイルの件数(ok_count)を1行のメッセージとしてlinesに追加しています。要約表示に含める重要な項目のひとつです。
LINE 262: 検証失敗件数の表示開始。検証に失敗した件数を表示する行の追加を開始しています。この後の行でエラー内訳も合わせて表示します。
LINE 263: 検証失敗件数の文字列内容。先ほど計算したng_countを埋め込んで、検証失敗の総件数を示す文字列の前半部分を組み立てています。LINE 264: エラー種別内訳の文字列内容。
必須項目の欠落件数と型・値の不一致件数を括弧書きで併記し、失敗の内訳が分かるように文字列を続けています。隣接する文字列リテラルとして自動的に連結されます。LINE 265: 検証失敗行の追加終了。
append呼び出しを閉じて、失敗件数と内訳をまとめた文字列がlinesリストに1行として追加されます。LINE 266: 読み込みエラーの有無判定。read_error_countが0より大きいかどうかを調べており、読み込みエラーが1件でもあった場合だけ次の行で追加のメッセージを表示するようにしています。
LINE 267: 読み込みエラー件数の表示。読み込みエラーが発生していた場合に限り、その件数をまとめた行をlinesに追加しています。エラーがなければこの行は実行されません。
LINE 268: 検証結果行リストの返却。ここまで組み立ててきたlinesリストを関数の戻り値として返しています。この結果が呼び出し元でそのまま画面表示に使われます。
RUN 7/9: 複数ファイルの検証レポートを出す。run_validateまで実装できたので、正常なYAMLと必須項目が欠落したYAMLをまとめて渡します。明細と検証サマリが一度に組み上がることを確認します。
CHECK 7/9: 途中実行に成功。run_validateまで実装できたので、正常なYAMLと必須項目が欠落したYAMLをまとめて渡します。明細と検証サマリが一度に組み上がることを確認します。
RETURN 07: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。LINE 273: 引数パーサー構築関数の定義。
コマンドライン引数を解析するためのArgumentParserを組み立てて返す関数を定義しています。この関数がCLI全体の入り口となる設定を担います。LINE 274: ArgumentParserの生成開始。
argparseのArgumentParserインスタンスを作成する処理を開始しています。この後の行でプログラム名や説明文などのオプションを指定します。LINE 275: プログラム名の指定。
CLIのヘルプ表示などに使われるプログラム名としてconfig_validatorという文字列を設定しています。利用者がヘルプを見たときに表示される名前です。LINE 276: CLIの説明文の指定。
このツール全体の役割を説明する文言を設定しています。ヘルプメッセージの先頭に表示され、利用者がツールの目的を把握できるようにしています。LINE 277: ArgumentParser生成の締めくくり。
ArgumentParserのコンストラクタ呼び出しを閉じており、prog名と説明文を持つparserオブジェクトが生成されて変数に代入されます。LINE 278: サブコマンドの受け皿を追加。initとvalidateという2つのサブコマンドを受け付けるための仕組みをparserに追加しています。
dest引数で選択されたコマンド名がargs.commandに格納されます。LINE 280: initサブコマンドの追加開始。サブコマンドの中にinitという名前のコマンドを追加する処理を開始しています。
この後の行でヘルプメッセージを指定します。LINE 281: initコマンドのヘルプ文言。initサブコマンドの用途を説明するヘルプ文字列を指定しています。
サンプル設定ファイルを作成する機能であることを伝えています。LINE 282: initサブコマンド追加の締めくくり。add_parser呼び出しを閉じており、initサブコマンドがinit_parser変数に代入されます。
この後に--dirオプションを追加していきます。LINE 283: --dirオプションの追加開始。initサブコマンドに--dirというオプション引数を追加する処理を開始しています。
作業ディレクトリを指定するための引数です。LINE 284: 引数名の指定。コマンドラインで指定する際のオプション名として--dirを指定しています。
利用者はinitコマンドの実行時にこのオプションでディレクトリを指定します。LINE 285: 格納先属性名の指定。--dirで受け取った値をargs.target_dirという属性名で扱うように指定しています。
関数内で分かりやすい名前でアクセスできるようにするための設定です。LINE 286: 必須指定の設定。このオプションを必須項目として設定しており、--dirを指定せずにinitコマンドを実行するとエラーになるようにしています。
LINE 287: --dirオプションのヘルプ文言。--dirオプションの説明文を指定しており、サンプルを作成する作業ディレクトリであること、カレントディレクトリ配下に限られることを伝えています。LINE 288: --dir引数追加の締めくくり。
add_argument呼び出しを閉じており、--dirオプションの定義がinit_parserに追加されます。LINE 290: validateサブコマンドの追加開始。サブコマンドの中にvalidateという名前のコマンドを追加する処理を開始しています。
設定ファイルを検証する機能を担うサブコマンドです。LINE 291: validateコマンドのヘルプ文言。validateサブコマンドの用途を説明するヘルプ文字列を指定し、add_parser呼び出しを閉じています。
設定ファイルの必須項目と型を検証する機能であることが分かります。LINE 292: validateサブコマンド追加の締めくくり。add_parser呼び出しを閉じており、validateサブコマンドがvalidate_parser変数に代入されます。
この後にpaths引数を追加していきます。LINE 293: paths引数の追加開始。validateサブコマンドに検証対象のファイルパスを受け取るpathsという引数を追加する処理を開始しています。
LINE 294: 位置引数名の指定。検証対象ファイルを指定するための位置引数としてpathsという名前を指定しています。オプションではなく直接パスを列挙して渡す形式です。
LINE 295: 複数値指定の許可。nargsに"+"を指定することで、1つ以上のファイルパスをまとめて指定できるようにしています。複数の設定ファイルを一度に検証できるようになります。
LINE 296: ヘルプ表示名の指定。ヘルプメッセージ中でこの引数を表す表示名としてCONFIGという文字列を指定しています。利用者にとって分かりやすい名前でヘルプに表示されます。
LINE 297: paths引数のヘルプ文言。paths引数の説明文を指定しており、1件以上の設定ファイルを指定できること、対応する拡張子がyaml・yml・jsonであることを伝えています。LINE 298: paths引数追加の締めくくり。
add_argument呼び出しを閉じており、paths引数の定義がvalidate_parserに追加されます。LINE 299: 完成したパーサーの返却。ここまで組み立てたparserオブジェクトを関数の戻り値として返しています。
この戻り値がmain関数でのコマンドライン解析に使われます。RUN 8/9: サブコマンドの引数解析を確認する。build_parserが完成した状態です。
initとvalidateそれぞれの引数を解析し、commandやpathsへ値が入ることを確かめます。CHECK 8/9: 途中実行に成功。build_parserが完成した状態です。
initとvalidateそれぞれの引数を解析し、commandやpathsへ値が入ることを確かめます。RETURN 08: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。
LINE 304: メイン関数の定義。CLI全体の処理の入り口となるmain関数を定義しています。戻り値は終了コードを表す整数となります。
LINE 305: コマンドライン引数の解析。build_parser関数で作成したパーサーを使って実際のコマンドライン引数を解析し、結果をargsに格納しています。ここで利用者が入力したコマンドやオプションの値が確定します。
LINE 306: initコマンドの判定。解析されたコマンド名がinitかどうかを調べており、initが指定された場合には次の行でサンプル作成処理を呼び出すようにしています。LINE 307: サンプル作成処理の呼び出し。
create_samples関数にargs.target_dirを渡して呼び出し、サンプルファイルの作成結果として表示用の行リストをlinesに格納しています。LINE 308: それ以外の場合の分岐。initコマンドでなかった場合、つまりvalidateコマンドが指定された場合の処理へ分岐しています。
LINE 309: 検証処理の呼び出し。run_validate関数にargs.pathsを渡して呼び出し、設定ファイルの検証結果として表示用の行リストをlinesに格納しています。LINE 310: 出力行のループ開始。
linesに格納された各行を1つずつ取り出すためのforループを開始しています。この後の行で画面へ実際に出力します。LINE 311: 結果行の出力。
取り出した各行の文字列をprint関数で画面に表示しています。init処理や検証処理の結果がここで実際にユーザーへ表示されます。LINE 312: 正常終了コードの返却。
main関数の戻り値として0を返しており、プログラムが正常に終了したことを示しています。この値はプロセスの終了コードとして扱われます。RUN 9/9: CLIとしてinitを実行する。
mainまで書き終わり、コマンドラインからの実行と同じ流れを再現できます。引数を渡してinitを動かし、戻り値が0になることまで確認します。CHECK 9/9: 途中実行に成功。
mainまで書き終わり、コマンドラインからの実行と同じ流れを再現できます。引数を渡してinitを動かし、戻り値が0になることまで確認します。RETURN 09: エディターへ戻りました。
入力済みのコードを維持したまま、次の実装へ進みます。LINE 315: スクリプト実行時の判定。このファイルが直接実行されたときにだけ以降の処理を行うようにする、Pythonでよく使われる定型的な条件分岐です。
LINE 316: main関数の呼び出し。main関数を呼び出してCLI全体の処理を開始しています。このファイルをスクリプトとして実行したときに最初に動くエントリポイントです。
実行1/5: サンプル設定ファイルを作成する。initサブコマンドでconfigsディレクトリを作り、正常系と異常系のサンプルを4件書き出します。作成件数と検証例のコマンドまで表示されることを確認します。
サンプル設定ファイルを作成する。initサブコマンドでconfigsディレクトリを作り、正常系と異常系のサンプルを4件書き出します。作成件数と検証例のコマンドまで表示されることを確認します。
正常なYAMLとJSONを検証する。サンプルを用意したうえでvalidateへ2件のパスを渡し、OK表示と設定内容の要約が並ぶことを確認します。必須項目の欠落を検出する。
environmentやserver.portが足りないYAMLを検証し、欠落した場所と件数がフィールドパス付きで並ぶことを確認します。型と値の不一致を検出する。文字列のportや真偽値でないdebugを含むJSONを検証し、入力値付きのエラー行が並ぶことを確認します。
4件をまとめて検証サマリを見る。正常系と異常系のサンプルを一度に渡し、対象件数と成功件数、失敗の内訳が検証サマリへまとまることを確認します。学習内容のまとめ。
portをFieldのgeとleで1から65535に制限add_subparsersでinitとvalidateへ分岐 先頭に置く設定ファイル検証の見出し行 作業ディレクトリをカレント配下だけに限定する 小さく実行確認しながら完成状態まで段階的に組み立てる エンディング。Python研修はCodeCampでご確認ください。
Pydanticとargparse・PyYAMLとは
今回使用する主要なライブラリについて、役割と使い分けを順番に確認します。
必須項目と型を宣言するPydantic
Pydanticは、クラスへ書いた型注釈をそのまま入力データの検査ルールとして使えるデータ検証ライブラリです。今回のCLIではAppConfigやServerConfigへ設定ファイルの構造を宣言し、読み込んだ辞書をmodel_validateへ渡すだけで点検が終わります。
条件を満たさない値があるとValidationErrorが送出され、errors()から場所と理由と入力値がそろった辞書の一覧を取り出せます。手書きのif文を積み上げなくても、モデル定義そのものが設定の仕様書として残る点が大きな利点でしょう。
config_validator.pyのモデル定義で宣言している検査ルールの内訳です。
- portをFieldのgeとleで1から65535に制限
- workersを既定値1かつ上限64として宣言
- engineをLiteralでpostgresql・mysql・sqliteに限定
- levelの候補をDEBUG・INFO・WARNING・ERRORへ固定
- loggingをdefault_factoryで省略可能にする
- allowed_hostsを文字列のリストとして受け取る
引数解析と読み込みを担うargparseとPyYAML
argparseはPythonへ最初から入っているコマンドライン引数の解析モジュールで、PyYAMLはYAMLを読み書きするライブラリです。argparse側ではinitとvalidateという2つのサブコマンドを登録し、initには必須の--dir、validateには1件以上のパスを受け取る引数を割り当てました。
PyYAML側はsafe_loadで設定を辞書へ変換し、safe_dumpでサンプル設定を日本語のまま書き出します。JSONは標準ライブラリのjsonが担当し、拡張子を見て呼び分ける構成にしました。
argparseとPyYAMLとjsonの呼び出しでこのCLIが指定している設定です。
- add_subparsersでinitとvalidateへ分岐
- destへcommandを指定して選択結果を受け取る
- required=Trueでサブコマンドの指定を必須化
- --dirを必須オプションとして宣言
- nargs=+で検証対象を1件以上受け取る
- yaml.safe_loadで基本的な型だけを読み込む
Python・Pydanticで開発する場合の環境構築
この記事のセットアップ手順と掲載コードは、Windows 11 Pro、PowerShell 5.1、Python 3.13.3で動作確認しています。仮想環境を有効化せず、その中のPythonを直接指定するため、以下のコマンドはPowerShellとコマンドプロンプト(cmd)の両方で使えます。
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install --upgrade pip
.\.venv\Scripts\python.exe -m pip install "pydantic>=2" PyYAML
macOS・Linuxでは仮想環境内のPythonパスが異なります。今回の動作確認環境とは異なるため、以下は環境差分を補う参考手順です。
python3 -m venv .venv
./.venv/bin/python -m pip install --upgrade pip
./.venv/bin/python -m pip install "pydantic>=2" PyYAML
- Pydanticは2系のAPI(model_validate・ValidationError.errors()のtype/loc)を前提にしています。1系が入った環境ではエラー構造が違うため、pydantic>=2を指定して導入してください。
- YAMLの読み書きはyaml.safe_load / yaml.safe_dumpだけを使います。yaml.loadは任意のPythonオブジェクトを構築し得るため使いません。
- initはカレントディレクトリ配下にしかファイルを作らず、既存ファイルは上書きしません。何度実行しても既存分はスキップされます。
PythonでYAML・JSON設定検証CLIの要件定義
目的は、YAMLとJSONの設定ファイルを読み込み、必須項目の欠落と型の不一致をフィールドパス付きで確認できるCLIを、argparseとPydanticとPyYAMLで作ることです。
対象者として、Pythonの基本文法を一通り学び、設定ファイルの検証をPydanticのモデルで自動化するCLIの作り方を学びたい人を想定しています。
完成物は、argparseのinitとvalidateサブコマンドを備え、Pydanticモデルで必須項目と型を検証し、フィールドパス付きエラーと件数サマリを表示する設定検証CLIです。
実装へ入る前に、機能・品質・受け入れ条件を分けて確認します。
機能要件
- initサブコマンドでカレント配下の作業ディレクトリを作成する
- 正常系と異常系を含む4件のサンプル設定を書き出す
- 既存の同名ファイルをskip表示にして上書きしない
- 作成件数と定義済み件数と検証例のコマンドを案内する
- validateサブコマンドで1件以上のパスを受け取る
- 拡張子からYAMLとJSONの形式を判定する
- PydanticのAppConfigモデルで必須項目と型を検証する
- 必須項目の欠落と型・値の不一致をラベルで区別する
- フィールドパスとメッセージと入力値をエラー行に出す
- 検証に成功した設定を5行の要約で表示する
- 対象件数とOK件数と失敗件数を検証サマリに出す
- 読み込みに失敗したファイルを読込NGとして報告する
非機能要件
- 作業ディレクトリをカレント配下だけに限定する
- カレント自身や配下外の指定を中止メッセージで止める
- 同名ファイルが存在する場合はディレクトリ作成を中止する
- サンプル値に認証情報を含めない
- YAML読み込みにyaml.safe_loadを使う
- エラー表示の入力値を60文字で切り詰める
- ファイルの読み書きをUTF-8で統一する
- 未対応拡張子をValueErrorとして扱う
- OSErrorとValueErrorとyaml.YAMLErrorをまとめて捕捉する
- パス操作を標準ライブラリのpathlibで行う
実装方針
今回はPydanticとargparse・PyYAMLの基本動作を追いやすくするため、YAML・JSON設定検証CLI本体を1つのPythonファイルへまとめます。
入力、判定、結果表示の役割を分け、実行結果を確認しながら機能を積み上げます。
YAML・JSON設定検証CLIを安全に組み立てるための実装方針は次のとおりです。
- 作業ディレクトリをカレント配下だけに限定する
- カレント自身や配下外の指定を中止メッセージで止める
- 同名ファイルが存在する場合はディレクトリ作成を中止する
- サンプル値に認証情報を含めない
- YAML読み込みにyaml.safe_loadを使う
- エラー表示の入力値を60文字で切り詰める
- ファイルの読み書きをUTF-8で統一する
- 未対応拡張子をValueErrorとして扱う
- OSErrorとValueErrorとyaml.YAMLErrorをまとめて捕捉する
- パス操作を標準ライブラリのpathlibで行う
完成と判断する条件
- init実行でapp.yamlとservice.jsonが作成される
- 作成したサンプル件数と定義済み件数が表示される
- 正常なYAML設定が[OK]と要約付きで並ぶ
- 必須項目が欠落したYAMLが[NG]と検出エラー件数で出る
- server.portのようなフィールドパスがエラー行に出る
- 型が一致しないJSONで入力値付きのエラーが並ぶ
- 検証サマリにYAML件数とJSON件数が出る
- 検証サマリに欠落と型・値の不一致の件数が出る
- 存在しないパスが[読込NG]として報告される
Pydanticで設定検証CLIを作る際の重要ポイント
完成物の中心にあるのは、1件ずつの検証結果を辞書へまとめ、最後に全体を数えて報告する流れです。validate_fileはパスと形式、成否、設定オブジェクト、読込エラー、エラー一覧を持つ辞書を返し、run_validateがその一覧から表示行を組み立てます。
読み込みに失敗したファイルは読込NG、検証に成功したファイルはOKと要約、失敗したファイルはNGとエラー明細というように、状態ごとに見せ方を切り替えました。締めくくりの検証サマリでは対象件数とOK件数と失敗件数に加えて、欠落と不一致の内訳まで数えています。
run_validateが1回の実行で組み立てる表示要素です。
- 先頭に置く設定ファイル検証の見出し行
- 成功時に続く5行の設定要約
- 失敗時に出る検出エラー件数付きの見出し
- フィールドパスと入力値を並べたエラー行
- YAML件数とJSON件数を含む対象ファイル行
- 読み込みエラーが1件以上のときだけ出る行
1ファイル分の結果を辞書へまとめる理由
validate_fileは例外を上位へ投げず、成否も含めた情報を1つの辞書へ詰めて返します。読み込みで失敗したときはread_errorへ例外名とメッセージを入れてすぐ戻り、検証で失敗したときはerrorsへValidationErrorの明細を保存する作りです。
表示を担当するrun_validateは辞書のキーを見るだけで分岐できるため、複数ファイルをまとめて処理しても途中で止まりません。
validate_fileが返す辞書のキーと、そこへ入る値です。
- 表示に使う元のパス文字列のpath
- YAMLかJSONかを示すformat
- 検証成功を表す真偽値のok
- 成功時のAppConfigを保持するconfig
- 失敗理由を保存するread_errorとerrors
読み込み失敗と検証失敗を分けて扱う判定
ファイルが見つからない、拡張子が対応外といった問題は、設定内容の良し悪し以前の話です。そこでOSErrorとValueError、yaml.YAMLErrorをまとめて捕まえ、読込NGという別枠で報告しています。読み込めたうえで条件を満たさない場合はNGとなり、検出エラー件数とともに明細が並びました。
サマリでも読み込みエラーの件数は検証失敗と分けて数えるため、原因の切り分けがしやすくなっています。
run_validateが1件ごとに選ぶ3種類の表示パターンです。
- 読み込み失敗の読込NGと例外メッセージ
- 検証成功のOKと設定要約
- 検証失敗のNGとエラー明細
- 各ファイルの区切りに入る空行
検証済み設定を5行にまとめる要約表示
検証に成功したファイルでは、AppConfigのインスタンスから読みやすい5行の要約を作ります。summarize_configはアプリ名と環境、サーバのホストとポート、データベースの種類と接続設定、ログ設定、許可ホストの件数を順に並べる関数です。デバッグやファイル出力の真偽値は有効と無効へ言い換え、許可ホストが空のときは指定なしと表示する工夫も入れました。
summarize_configが1件の設定について並べる5行の内容です。
- アプリ名と環境とデバッグ状態
- ホストとポートとworkers数
- データベースの種類と接続の設定値
- ログの出力レベルとファイル出力の有無
- 許可ホストの件数と一覧
件数サマリで全体像を伝える集計処理
最後に表示する検証サマリは、結果の辞書一覧をsumで数えるだけで組み立てられます。formatキーからYAMLとJSONの内訳、okキーから成功件数、read_errorキーから読み込みエラー件数を求め、残りを検証失敗として計算する流れです。
さらに各エラーのtypeがmissingかどうかで欠落と不一致を数え分けるため、どの種類の修正が必要かが一目で伝わる報告になりました。
検証サマリで表示している集計値の内訳です。
- 対象ファイル件数とYAML・JSONの内訳
- 検証済み設定のOK件数
- 検証に失敗したファイルの件数
- 必須項目の欠落と型・値の不一致の件数
- 読み込みエラーが出たときの件数
PythonでAppConfigスキーマとサンプル設定の設計
検証の中心はAppConfigというPydanticモデルです。トップレベルのアプリ名や環境名に加えて、server・database・loggingをネストしたモデルとして抱えています。
必須かどうかは、型注釈に既定値を書くかどうかで決まる仕組みです。Literalで宣言した項目は値そのものを列挙で縛るので、環境名の綴り違いもモデルの段階で弾けます。
サンプルは正常系のYAMLとJSONに加えて、必須項目欠落と型不一致の2種類も同梱しました。壊れた例が最初からあると、検証が本当に働くかをその場で確かめられます。
設計面のポイントとしては、サンプル値の安全性とLiteralによる値の固定が挙げられます。
このセクションの用語
- BaseModel
- Pydanticのモデルを作るときに継承する基底クラスです。型注釈を書くだけで、検証つきのデータ構造になります。
- Literal型
- 決められた値のどれかしか許さない型の書き方です。候補以外の文字列が来ると検証エラーになります。
- default_factory
- 既定値を毎回関数で作らせる指定です。リストや辞書のように後から書き換わる値を既定値にするときに使います。
- ネスト
- モデルの中に別のモデルを入れる入れ子構造です。
serverやdatabaseのようなブロックを型として表現できます。
| 項目 | 型と制約 | 必須・既定値 |
|---|---|---|
| app_name | str | 必須 |
| environment | Literalでdevelopment/staging/productionのみ | 必須 |
| debug | bool | 既定値はFalse |
| server.host | str | 必須 |
| server.port | intで1以上65535以下 | 必須 |
| server.workers | intで1以上64以下 | 既定値は1 |
| database.engine | Literalでpostgresql/mysql/sqliteのみ | 必須 |
| database.name | str | 必須 |
| logging.level | LiteralでDEBUG/INFO/WARNING/ERRORのみ | 既定値はINFO |
| allowed_hosts | list[str] | 既定値は空のリスト |
サンプル値:認証情報を含めない
Literal:許可値を列挙で固定
壊れた例:欠落と型不一致を同梱
PythonでYAML・JSON設定検証CLIの完成コード
config_validator.pyは1ファイル完結で、モジュールdocstring、import、モデル定義、サンプル定義、コマンド処理の順に並んでいます。上から読めば処理の流れがそのまま追えます。
docstringにはinitとvalidateが担う機能を日本語で書き下しました。実装を追う前に、このツールが何を保証するのかを最初の数行で確認できます。
検証に失敗するとValidationErrorが送出されます。Pydanticはエラーごとに発生位置を保持しているので、server.portのようなフィールドパスを組み立てて表示できるわけです。
エラー位置が入れ子まで分かる根拠は、公式ドキュメントにも明記されています。
このセクションの用語
- docstring
- モジュールや関数の先頭に置く説明文です。何をするコードなのかを、実装を読む前に伝えられます。
- ValidationError
- Pydanticの検証に失敗したときに送出される例外です。失敗した項目ごとの位置と理由をまとめて持っています。
- フィールドパス
- エラーが起きた項目までの道順を
server.portのようにつないだ表記です。設定のどこを直すかが一目で分かります。 - 型ヒント
- 変数や属性の型を注釈として書く記法です。Pydanticはこの注釈を読み取って検証ルールを組み立てます。
"""argparse + Pydantic + PyYAML を組み合わせた設定ファイル検証CLI。
init サブコマンドでは、新しい作業ディレクトリへ安全なサンプルYAML・JSON設定ファイルの作成を行う。
validate サブコマンドでは、拡張子に応じてYAML設定の必須項目と型の検証と、JSON設定の必須項目と型の検証を行い、
必須項目欠落と型不一致をフィールドパス付きで表示したうえで、最後に検証済み設定の非ゼロ件数を含む要約表示を行う。
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Any, Literal
import yaml
from pydantic import BaseModel, Field, ValidationError
# 設定モデルの定義(必須項目と型の契約をPydanticで宣言)
class ServerConfig(BaseModel):
host: str
port: int = Field(ge=1, le=65535)
workers: int = Field(default=1, ge=1, le=64)
class DatabaseConfig(BaseModel):
engine: Literal["postgresql", "mysql", "sqlite"]
name: str
pool_size: int = 5
timeout_seconds: float = 5.0
class LoggingConfig(BaseModel):
level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO"
to_file: bool = False
class AppConfig(BaseModel):
app_name: str
environment: Literal["development", "staging", "production"]
debug: bool = False
server: ServerConfig
database: DatabaseConfig
logging: LoggingConfig = Field(default_factory=LoggingConfig)
allowed_hosts: list[str] = Field(default_factory=list)
# サンプル設定の内容(認証情報を含まない安全な値だけを置く)
VALID_YAML_SAMPLE: dict[str, Any] = {
"app_name": "在庫管理API",
"environment": "production",
"debug": False,
"server": {"host": "0.0.0.0", "port": 8080, "workers": 4},
"database": {
"engine": "postgresql",
"name": "inventory",
"pool_size": 10,
"timeout_seconds": 3.5,
},
"logging": {"level": "INFO", "to_file": True},
"allowed_hosts": ["api.example.local", "admin.example.local"],
}
VALID_JSON_SAMPLE: dict[str, Any] = {
"app_name": "配送バッチ",
"environment": "staging",
"debug": True,
"server": {"host": "127.0.0.1", "port": 9000, "workers": 2},
"database": {
"engine": "sqlite",
"name": "delivery",
"pool_size": 3,
"timeout_seconds": 1.5,
},
"logging": {"level": "DEBUG", "to_file": False},
"allowed_hosts": ["batch.example.local"],
}
MISSING_FIELD_SAMPLE: dict[str, Any] = {
"app_name": "在庫管理API(必須項目欠落サンプル)",
"debug": False,
"server": {"host": "127.0.0.1"},
"database": {"engine": "sqlite"},
"logging": {"level": "WARNING"},
}
TYPE_MISMATCH_SAMPLE: dict[str, Any] = {
"app_name": "配送バッチ(型不一致サンプル)",
"environment": "staging",
"debug": "ときどき",
"server": {"host": "127.0.0.1", "port": "ポート未定", "workers": "four"},
"database": {
"engine": "mysql",
"name": "delivery",
"pool_size": 2.5,
"timeout_seconds": 1.5,
},
"logging": {"level": "INFO", "to_file": False},
"allowed_hosts": "batch.example.local",
}
SAMPLE_FILES: list[tuple[str, dict[str, Any], str]] = [
("app.yaml", VALID_YAML_SAMPLE, "正常なYAML設定"),
("service.json", VALID_JSON_SAMPLE, "正常なJSON設定"),
("missing_required.yaml", MISSING_FIELD_SAMPLE, "必須項目が欠落したYAML設定"),
("type_mismatch.json", TYPE_MISMATCH_SAMPLE, "型が一致しないJSON設定"),
]
# サンプル設定ファイルの作成(カレント配下のみ・既存ファイルは上書きしない)
def render_sample(name: str, data: dict[str, Any]) -> str:
if name.endswith((".yaml", ".yml")):
return yaml.safe_dump(
data, allow_unicode=True, sort_keys=False, default_flow_style=False
)
return json.dumps(data, ensure_ascii=False, indent=2) + "\n"
def create_samples(target_dir: str) -> list[str]:
base = Path(target_dir).expanduser().resolve()
cwd = Path.cwd().resolve()
if base == cwd or not base.is_relative_to(cwd):
return [f"[中止] 作業ディレクトリはカレント配下を指定してください: {base}"]
if base.exists() and not base.is_dir():
return [f"[中止] 同名のファイルが既に存在します: {base}"]
base.mkdir(parents=True, exist_ok=True)
lines = [f"作業ディレクトリ: {base}"]
created = 0
for name, data, note in SAMPLE_FILES:
path = base / name
if path.exists():
lines.append(f" [skip ] {name} は既に存在するため上書きしません")
continue
path.write_text(render_sample(name, data), encoding="utf-8")
created += 1
lines.append(f" [作成 ] {name} — {note} ({path.stat().st_size} bytes)")
lines.append(f"作成したサンプル: {created}件 / 定義済みサンプル: {len(SAMPLE_FILES)}件")
lines.append(f"検証例: validate {target_dir}/app.yaml {target_dir}/service.json")
return lines
# 設定ファイルの読み込み(拡張子でYAML・JSONを切り替え)
def detect_format(path: Path) -> str:
return {".yaml": "YAML", ".yml": "YAML", ".json": "JSON"}.get(
path.suffix.lower(), "未対応"
)
def load_config(path: Path, fmt: str) -> Any:
text = path.read_text(encoding="utf-8")
if fmt == "YAML":
return yaml.safe_load(text)
if fmt == "JSON":
return json.loads(text)
raise ValueError(f"未対応の拡張子です: {path.suffix or '(拡張子なし)'}")
# Pydanticによる検証とフィールドパス付きエラーの整形
def field_path(loc: tuple[Any, ...]) -> str:
return ".".join(str(part) for part in loc) if loc else "(ルート)"
def classify(error_type: str) -> str:
return "必須項目の欠落" if error_type == "missing" else "型・値の不一致"
def format_error(error: dict[str, Any]) -> str:
shown = repr(error.get("input"))
if len(shown) > 60:
shown = shown[:57] + "..."
label = classify(str(error.get("type", "")))
return f" [{label}] {field_path(error['loc'])}: {error['msg']} (入力値: {shown})"
def validate_file(path_text: str) -> dict[str, Any]:
path = Path(path_text)
fmt = detect_format(path)
result: dict[str, Any] = {
"path": path_text,
"format": fmt,
"ok": False,
"config": None,
"read_error": None,
"errors": [],
}
try:
raw = load_config(path, fmt)
except (OSError, ValueError, yaml.YAMLError) as exc:
result["read_error"] = f"{type(exc).__name__}: {exc}"
return result
try:
result["config"] = AppConfig.model_validate(raw)
result["ok"] = True
except ValidationError as exc:
result["errors"] = exc.errors()
return result
# 検証済み設定の要約作成
def summarize_config(config: AppConfig) -> list[str]:
hosts = ", ".join(config.allowed_hosts) if config.allowed_hosts else "指定なし"
return [
f" アプリ名: {config.app_name} / 環境: {config.environment}"
f" / デバッグ: {'有効' if config.debug else '無効'}",
f" サーバ: {config.server.host}:{config.server.port}"
f" (workers={config.server.workers})",
f" DB: {config.database.engine} / {config.database.name}"
f" (pool={config.database.pool_size}, timeout={config.database.timeout_seconds}秒)",
f" ログ: level={config.logging.level}"
f" / ファイル出力={'有効' if config.logging.to_file else '無効'}",
f" 許可ホスト: {len(config.allowed_hosts)}件 ({hosts})",
]
# 検証レポートと件数サマリの組み立て
def run_validate(paths: list[str]) -> list[str]:
results = [validate_file(path_text) for path_text in paths]
lines = ["=== 設定ファイル検証 ==="]
for result in results:
if result["read_error"]:
lines.append(f"[読込NG] {result['path']} ({result['format']})")
lines.append(f" {result['read_error']}")
elif result["ok"]:
lines.append(f"[OK] {result['path']} ({result['format']})")
lines.extend(summarize_config(result["config"]))
else:
lines.append(
f"[NG] {result['path']} ({result['format']})"
f" 検出エラー {len(result['errors'])}件"
)
for error in result["errors"]:
lines.append(format_error(error))
lines.append("")
yaml_count = sum(1 for r in results if r["format"] == "YAML")
json_count = sum(1 for r in results if r["format"] == "JSON")
ok_count = sum(1 for r in results if r["ok"])
read_error_count = sum(1 for r in results if r["read_error"])
ng_count = len(results) - ok_count - read_error_count
missing_count = sum(
1 for r in results for e in r["errors"] if e.get("type") == "missing"
)
mismatch_count = sum(
1 for r in results for e in r["errors"] if e.get("type") != "missing"
)
lines.append("=== 検証サマリ ===")
lines.append(
f"対象ファイル: {len(results)}件 (YAML {yaml_count}件 / JSON {json_count}件)"
)
lines.append(f"検証済み設定(OK): {ok_count}件")
lines.append(
f"検証失敗: {ng_count}件"
f" (必須項目の欠落 {missing_count}件 / 型・値の不一致 {mismatch_count}件)"
)
if read_error_count:
lines.append(f"読み込みエラー: {read_error_count}件")
return lines
# CLIの引数定義(argparse)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="config_validator",
description="YAML・JSON設定ファイルの必須項目と型をPydanticモデルで検証するCLI",
)
sub = parser.add_subparsers(dest="command", required=True, metavar="{init,validate}")
init_parser = sub.add_parser(
"init", help="新しい作業ディレクトリへ安全なサンプルYAML・JSON設定ファイルを作成する"
)
init_parser.add_argument(
"--dir",
dest="target_dir",
required=True,
help="サンプルを作成する作業ディレクトリ(カレント配下)",
)
validate_parser = sub.add_parser(
"validate", help="YAML・JSON設定ファイルの必須項目と型を検証する"
)
validate_parser.add_argument(
"paths",
nargs="+",
metavar="CONFIG",
help="検証する設定ファイル(1件以上・.yaml/.yml/.json)",
)
return parser
# エントリポイント
def main() -> int:
args = build_parser().parse_args()
if args.command == "init":
lines = create_samples(args.target_dir)
else:
lines = run_validate(args.paths)
for line in lines:
print(line)
return 0
if __name__ == "__main__":
main()
コード全文は上の折り畳みに入れてあるので、全部を上から読む必要はありません。ここでは特に重要な部分だけを抜き出して、何をしているのか順番に見ていきます。
docstringで5つの機能を明記する
"""argparse + Pydantic + PyYAML を組み合わせた設定ファイル検証CLI。
init サブコマンドでは、新しい作業ディレクトリへ安全なサンプルYAML・JSON設定ファイルの作成を行う。ファイル冒頭のdocstringで、initとvalidateが受け持つ役割を日本語のまま宣言しています。実装を読み始める前に守備範囲が分かるので、引き継ぎの説明も短く済みます。
importで役割ごとの部品をそろえる
import argparse
import json
from pathlib import Path
from typing import Any, Literal
import yaml
from pydantic import BaseModel, Field, ValidationErrorargparseが引数の解釈、jsonがJSONの読み込み、yamlがPyYAMLによるYAML解析を担当します。PydanticからはBaseModelとField、検証失敗時の例外ValidationErrorを持ち込んでいます。
ServerConfigのFieldで値の範囲を縛る
class ServerConfig(BaseModel):
host: str
port: int = Field(ge=1, le=65535)
workers: int = Field(default=1, ge=1, le=64)hostは既定値がないので必須、portはField(ge=1, le=65535)で型に加えて範囲まで縛っています。workersはdefault=1があるため省略可能で、書かなければ1として扱われます。
DatabaseConfigのLiteralで候補を固定する
class DatabaseConfig(BaseModel):
engine: Literal["postgresql", "mysql", "sqlite"]
name: str
pool_size: int = 5
timeout_seconds: float = 5.0engineはLiteralで3つの値しか受け付けない項目です。pool_sizeとtimeout_secondsには既定値があるので、書かれていない設定でも数値が自動的に埋まります。
AppConfigでネスト構造をまとめる
class AppConfig(BaseModel):
app_name: str
environment: Literal["development", "staging", "production"]
debug: bool = False
server: ServerConfig
database: DatabaseConfig
logging: LoggingConfig = Field(default_factory=LoggingConfig)
allowed_hosts: list[str] = Field(default_factory=list)トップレベルのモデルがserver・database・loggingを型として抱え、設定ファイルの入れ子をそのまま表しています。loggingとallowed_hostsはdefault_factoryで既定値を作るため、省略しても検証は通ります。
MISSING_FIELD_SAMPLEで欠落を再現する
MISSING_FIELD_SAMPLE: dict[str, Any] = {
"app_name": "在庫管理API(必須項目欠落サンプル)",
"debug": False,
"server": {"host": "127.0.0.1"},
"database": {"engine": "sqlite"},
"logging": {"level": "WARNING"},
}environmentやserver.port、database.nameをわざと抜いたサンプルです。必須項目欠落の検出が働いているかを、このファイル1つで毎回確認できます。
TYPE_MISMATCH_SAMPLEで型崩れを再現する
TYPE_MISMATCH_SAMPLE: dict[str, Any] = {
"app_name": "配送バッチ(型不一致サンプル)",
"environment": "staging",
"debug": "ときどき",
"server": {"host": "127.0.0.1", "port": "ポート未定", "workers": "four"},debugに日本語の文字列、portにポート未定、workersにfourを入れて型を崩しています。型不一致の指摘がフィールドパス付きで並ぶかを見るための、意図的に壊した設定です。
参考:
©Pydantic公式ドキュメントError Handlingloc: The error's location as a list. The first item in the list will be the field where the error occurred, and if the field is a sub-model, subsequent items will be present to indicate the nested location of the error.
PythonでYAML・JSON設定検証CLIのエラー対処
つまずきどころは検証ロジックより手前、つまり環境と書式に集まりがちです。ここでは一般に起こりやすいものを、原因と対処に分けて並べました。
多いのは依存パッケージが別の環境に入っているケースです。python -m pip installのように実行に使うPython自身へ入れると、食い違いが起きにくくなります。
YAMLとJSONでは許される書式が違う点にも注意が必要でした。JSONは末尾カンマもコメントも認めないので、YAMLの感覚のまま書くと解析の段階で止まります。
このセクションの用語
- ModuleNotFoundError
- importしたパッケージが見つからないときに出るPythonのエラーです。多くはインストール先の環境違いが原因になります。
- パーサ
- 文字列を解析して構造化データへ変換する部品です。PyYAMLや
jsonが設定ファイルの中身を辞書に変えてくれます。 - 相対パス
- 今いるディレクトリを起点にしたファイルの指定方法です。実行場所が変わるとファイルを見失う原因になります。
| エラー例 | 原因 | 対処 |
|---|---|---|
| ModuleNotFoundError: No module named 'yaml' | PyYAMLが未インストール、または別の仮想環境で実行している | 実行に使うPythonへpython -m pip install pyyamlを行い、python -m pip listで入った先を確認する |
| ModuleNotFoundError: No module named 'pydantic' | Pydanticが入っていない、エディタと端末で環境が食い違っている |
python -m pip install pydanticのあとpython -c 'import pydantic'で読み込みを確認する |
| yaml.scanner.ScannerError: mapping values are not allowed here | YAMLでコロンの後の空白が抜けている、インデントが揃っていない |
key: valueの形に直し、インデントを半角スペース2つで統一する |
| json.decoder.JSONDecodeError: Expecting ',' delimiter | JSONに末尾カンマやコメントが混ざっている | 末尾カンマとコメントを削り、エディタの構文チェックで括弧の対応を見る |
| FileNotFoundError: config_samples/app.yaml | initを実行していない、または別のディレクトリから実行している | 先にpython config_validator.py init --dir config_samplesを実行し、渡すパスを合わせる |
YAML・JSON設定検証CLIで注意したい点
Pydanticは値をある程度うまく解釈しますが、何でも通すわけではありません。既定の緩やかなモードでは数字だけの文字列を整数として受け取る一方、ときどきのような文字列はboolになりません。
必須かどうかはモデル定義の既定値で決まります。port: int = Field(ge=1, le=65535)は既定値がないので必須、workers: int = Field(default=1, ge=1, le=64)は省略できる項目です。
エラーに付くフィールドパスは、直す場所を示す道しるべになります。server.portと出たらserverブロックのportだけを見ればよく、設定全体を読み返さずに済みます。
ここまでのポイントとしては、必須判定の決まり方と型の解釈、そしてフィールドパスの読み方が挙げられるでしょう。
必須判定:既定値なしなら必須
型解釈:boolへ日本語文字列は不可
フィールドパス:locを.で連結
終了コード:0でも出力を確認
PythonでYAML・JSON設定検証CLIの動作確認
実行したのは5回のコマンドで、いずれも終了コード0で完了しました。順にinit、正常なYAML、正常なJSON、壊れた2件、そして全4ファイルまとめての検証です。
initではconfig_samplesディレクトリが新しく作られ、正常系と異常系のサンプルが書き出されます。以降のvalidateは、このディレクトリ内のファイルを指定するだけで再現できました。
壊れたファイルを渡した回も、コマンド自体は終了コード0で終わっています。検証結果は出力された内容で判断する作りなので、終了コードだけを見て安心しないのが肝心です。
このセクションの用語
- 終了コード
- コマンドが終わるときに返す数値です。慣習として0が正常終了を表します。
- 標準出力
- コマンドが結果を書き出す既定の出力先です。画面に表示される文字列はここへ送られています。
実際に打ったコマンドを、実行した順に並べます。
- python config_validator.py init --dir config_samples
- python config_validator.py validate config_samples/app.yaml
- python config_validator.py validate config_samples/service.json
- python config_validator.py validate config_samples/missing_required.yaml config_samples/type_mismatch.json
- python config_validator.py validate config_samples/app.yaml config_samples/service.json config_samples/missing_required.yaml config_samples/type_mismatch.json





設定検証CLIをPython開発で活かす場面
設定ファイルの検証は地味ですが、効き目のある自動化です。人が目視で追っていた書き忘れや型違いを、コマンド1本でふるいにかけられます。
複数ファイルをまとめて渡せる点も実務向きでした。環境別の設定を並べて実行すれば、どのファイルのどの項目が問題かを一度の出力で見比べられます。
このセクションの用語
- CI
- コードを変更するたびに自動でテストや検査を回す仕組みです。設定ファイルの検証もその一部として組み込めます。
- プルリクエスト
- 変更をレビューしてもらうための依頼です。この単位で検証を走らせると、取り込む前に誤りへ気づけます。
| 使える場面 | 具体的な使い方 |
|---|---|
| 新メンバーのオンボーディング |
initで安全なサンプルを配り、雛形の値だけを自分の環境向けに書き換えてもらう |
| プルリクエストのレビュー前 | CIでvalidateを回し、必須項目欠落や型不一致を人がレビューする前に落とす |
| 本番デプロイ前の最終確認 |
environmentがproductionの設定を通し、ポート範囲やengine名の取り違えを事前に弾く |
| 環境別設定の一括点検 | dev・staging・prodのYAMLとJSONを引数に並べ、1回の実行で指摘と要約を見比べる |
| 障害対応時の切り分け | まずvalidateにかけ、設定起因かどうかをフィールドパスの指摘から判断する |
PythonでYAML・JSON設定検証CLI開発のまとめ
入口をargparseで作り、契約をPydanticで書き、YAML解析をPyYAMLに任せる構成にまとまりました。追加のフレームワークなしでも、設定ファイル検証CLIは実用に足る形になります。
実行は5回とも終了コード0で完走し、正常な設定の要約表示と、壊れた設定へのフィールドパス付きの指摘まで確認できました。
検証ルールをモデルとして1か所へ集めておくと、仕様が変わったときに直す場所も1か所で済みます。まずは手元の設定をAppConfigに写し取るところから始めるのがおすすめです。
この構成から先へ進むときの、次の一歩の候補を挙げます。
- 独自バリデータを足して、本番環境では
debugを許さないといった業務ルールまで検証する - CIで落としたい場合は、検証失敗時の終了コードの扱いを先に決めてから運用へ組み込む
- サンプルを自チームの設定内容に差し替え、
initをオンボーディング手順に載せる
参考にした一次情報
- ^ argparse — コマンドラインオプション、引数、サブコマンドのパーサー(Python標準ライブラリ). https://docs.python.org/ja/3/library/argparse.html, (参照26-09-10).
- ^ Pydantic — Models(モデル定義とmodel_validate). https://docs.pydantic.dev/latest/concepts/models/, (参照26-09-10).
- ^ Pydantic — Error Handling(ValidationErrorとerrors()のloc/type). https://docs.pydantic.dev/latest/errors/errors/, (参照26-09-10).
- ^ PyYAML Documentation(safe_load / safe_dump). https://pyyaml.org/wiki/PyYAMLDocumentation, (参照26-09-10).
※内容は執筆時点のものです。ライブラリやサイトの仕様は変わる可能性があるため、公式ドキュメントもあわせてご確認ください。







