Pythonのコマンドライン引数を処理する標準モジュールargparseを拡張したモジュール: argparse-extd

# argparse.ArgumentParserの拡張

**Bug fix in 2025/2/22**

Pythonスクリプトの実行時に動作変えるパラメータを与える方法としては、コマンドライン引数を用いる方法と、設定ファイルを読み込む方法があるが、それぞれ一長一短ある。

| 方法 | コマンドライン引数 | 設定ファイル |
|:-:|---|---|
| 柔軟性 | ◎ 引数を変えるだけで実行条件変更が可能 | × 実行ごとに設定ファイルの作成が必要 |
| 使用容易性 | ◯ コマンドラインの指定はだいたい典型的 | △ ソフトウェアごとの設定ファイルの書式(フォーマット)に従う必要がある。 |
| 再現性 | × 以前の実行条件の記録が残らない | ◯　実行条件が残るので、再度同じ条件で実行することが容易。 |
| 作成容易性| ◯　標準ライブラリあり | △ 設定の読み/書きのコードの作成が必要  |

そこで、標準モジュールの`argparse.ArgumentParser`でコマンドライン引数の解析した結果をファイル書き込んだり、逆にファイルから読み込んだデータを`argparse.ArgumentParser`(argparse.Namespaceのオブジェクト)のデータに流し込めるように拡張したpythonモジュール`argparse-extd`を作成し、PyPIに登録した。

ここで設定ファイルの形式としては、JSON, YAML, INI, TOMLおよびそれらの圧縮(.json.bz2, .json.gz, .json.xz)に対応していて、ファイルの拡張子から適切に形式を選択して読み書きされる。人間による可読性や、利用者の好みで選択可能。

# ファイル置き場

- [PyPI](https://pypi.org/project/argparse-extd/)
- [Github](https://github.com/nanigashi-uji/argparse-extd)
- [GitLab](https://gitlab.com/nanigashi_uji/argparse-extd)

# インストール方法

```bash:インストール方法
% pip install argparse-extd
```

# 利用例

```python:example/ex_argparseextd.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import pydoc
import sys
import pkgstruct
import argparse_extd

def main():
    # Step 1. デフォルトで読み込む設定ファイルのパスの決定
    #   1.a スクリプト名から(structモジュールをつかって)ソフトウェアパッケージの場所と
    #        デフォルトの設定ファイル名を推測
    this_script_name = sys.argv[0].removesuffix('.py')
    pkg_info=pkgstruct.PkgStruct(script_path=this_script_name)
    config_name_default = pkg_info.script_basename+'.config.json'
    
    #   1.b コマンドライン引数'--prefix'で、ソフトウェアパッケージの場所を変えたり、
    #       コマンドライン引数'--default-config'で、デフォルトのファイル名を変えられるようにする
    #       ここで、2段階でコマンドライン引数を解析するので、(add_help=False) を指定する。
    argprsr = argparse_extd.ArgumentParserExtd(add_help=False)
    argprsr.add_argument('-p', '--prefix', type=str, help='Directory Prefix')
    argprsr.add_argument('-c', '--default-config', type=str, default=config_name_default, help='Default config filename')
    #   下記で一旦コマンドライン引数を解析して、"--prefix"と"--default-config"のみチェック
    opts,remains=argprsr.parse_known_args()
    #   1.c コマンドラインオプションで指定された内容で設定ファイルのパスを再推定
    pkg_info=pkgstruct.PkgStruct(prefix=opts.prefix, script_path=this_script_name)
    pkg_default_config=pkg_info.concat_path('pkg_statedatadir', 'config', opts.default_config)
    
    # Step. 2 デフォルトの設定ファイルの読み込み
    argprsr.load_config(pkg_default_config)

    # Step 3. 1.bのところで"auto_help"を止めているので、下記でその機能を復活させる
     argprsr.add_argument_help()
    # Step 4. 下記で"--config"オプションを設定して、このオプションがあったら、
    #          指定されたファイルを読み込んでStep.2で指定された内容を上書きする。
    argprsr.add_argument_config()
    # Step 5. 下記で"--save-config"オプションを設定されて、指定されたファイルパスか
    #         もしくはデフォルトの設定ファイルのパスに、設定を保存する。
    argprsr.add_argument_save_config(default_path=pkg_default_config)
    # Step 6. 下記ではよく使う典型的なオプション設定'--verbose'を設定する。
    argprsr.add_argument_verbose()
    # Step 7. 下記ではよく使う典型的なオプション設定'--quiet'を設定する。
    argprsr.add_argument_quiet(dest='verbose')

    # Step 8. 下記では、通常通りソフトウェアの機能に合わせてオプション引数を設定する 
    argprsr.add_argument('-H', '--class-help', action='store_true',
                         help='Show help for ArgumentParserExtd classes')
    argprsr.add_argument('-s', '--skimmed-output',
                         action='store_true', help='Active status')
    argprsr.add_argument('-o', '--output', type=str, help='output filename') 
    argprsr.add_argument('-f', '--dump-format', type=str,
                         choices=argparse_extd.ArgumentParserExtd.CONFIG_FORMAT,
                         default='json', help='Output format')

    argprsr.add_argument('-x', '--first-property', type=str, help='CL option 1')
    argprsr.add_argument('-y', '--second-property', type=str, help='CL option 2')
    argprsr.add_argument('-z', '--third-property', action='store_true', help='CL option 3')

    argprsr.add_argument('argv', nargs='*', help='non-optional CL arguments')

    # Step 9. 下記では、コマンドライン引数のうち、設定ファイルに保存する必要がないものを指定する。
    argprsr.append_write_config_exclude(('--prefix', '--default-config', 'verbose',
                                         '--skimmed-output', '--output', '--save-config', 'argv'))
    
    # Step 10. 下記では通常通りのコマンドライン引数の解析を行う
    #           このとき、クラスのコンストラクタで'add_help=False'を指定した場合には、
    #           ここで'action_help=True'とすると機能が復活する。
    args = argprsr.parse_args(action_help=True)

    # Step 11. コマンドライン引数の解析結果にアクセスするには２つの方法がある.
    #   A. 通常どおり、parse_args()の出力オブジェクト(NamespaceExtクラス)のプロパティとして取得
    #      ex.
    #      args    = argprsr.parse_args()
    #      flg_zzz = args.zzz
    #
    #   B. ArgumentParserExtdクラスの実体に保持されている解析結果:
    #      'args'という名のメンバー変数(NamespaceExtクラス)のプロパティとして取得
    #      ex.
    #      flg_zzz = argprsr.args.zzz
    #
    if argprsr.args.class_help:
        pydoc.help = pydoc.Helper(output=sys.stdout)
        help(argparse_extd.ArgumentParserExtd)
        help(argparse_extd.ArgumentParserExtd.NamespaceExt)
        help(argparse_extd.ArgumentParserExtd.ConfigActionExt)
        sys.exit()

    #
    # Step 12. 下記で設定ファイルのパスとして指定されているパスに設定ファイルを出力する
    #
    argprsr.save_config_action()

    if argprsr.args.verbose:
        print('Prefix              : ', pkg_info.prefix)
        print('Default config      : ', argprsr.args.default_config)

    print('Default config path : ', pkg_default_config)

    print('Final Namespace: ', argprsr.args)
    print('Serialized %-4s:\n----\n%s\n----\n' % (argprsr.args.dump_format.upper(), 
                                                  (argprsr.skimmed_args_to_string(output_format=argprsr.args.dump_format)
                                                   if argprsr.args.skimmed_output
                                                   else argprsr.args_to_string(output_format=argprsr.args.dump_format))))
    #      
    # Step 13. 下記は、指定されたパスに追加で出力する例
    argprsr.write_config(argprsr.args.output)

if __name__ == '__main__':
    main()
```

- 下記は上のサンプルの実行例

```bash:実行例1_ヘルプ出力
% python3 ex_argparseextd.py --help 

usage: ex_argparseextd.py [-p PREFIX] [-c DEFAULT_CONFIG] [-h] [-C CONFIG]
                          [-S [SAVE_CONFIG]] [-v] [-q] [-H] [-s]
                          [-o OUTPUT] [-f {ini,yaml,json,toml}]
                          [-x FIRST_PROPERTY] [-y SECOND_PROPERTY] [-z]
                          [argv ...]

positional arguments:
  argv                  non-optional CL arguments

options:
  -p, --prefix PREFIX   Directory Prefix
  -c, --default-config DEFAULT_CONFIG
                        Default config filename
  -h, --help            show this help message and exit
  -C, --config CONFIG   path of the configuration file to be loaded
  -S, --save-config [SAVE_CONFIG]
                        path of the configuration file to be saved
  -v, --verbose         show verbose messages
  -q, --quiet           supress verbose messages
  -H, --class-help      Show help for ArgumentParserExt classes
  -s, --skimmed-output  Active status
  -o, --output OUTPUT   output filename
  -f, --dump-format {ini,yaml,json,toml}
                        Output format
  -x, --first-property FIRST_PROPERTY
                        CL option 1
  -y, --second-property SECOND_PROPERTY
                        CL option 2
  -z, --third-property  CL option 3
``` 

2段階でコマンドライン解析していますが、ヘルプは全部でます。

```bash:実行例2_オプション指定とファイルへの出力
% python3 ex_argparseextd.py -q -x 'prop1' -o sample.json.bz2 

Default config path :  ***/var/lib/py_encstorage/config/ex_argparseextd.config.json
Final Namespace:  NamespaceExt(prefix=None, default_config='ex_argparseextd.config.json', help=False, config=None, save_config=None, verbose=False, class_help=False, skimmed_output=False, output='sample.json.bz2', dump_format='json', first_property='prop1', second_property=None, third_property=False, argv=[])
Serialized JSON:
----
{
    "prefix": null,
    "default_config": "ex_argparseextd.config.json",
    "help": false,
    "config": null,
    "save_config": null,
    "verbose": false,
    "class_help": false,
    "skimmed_output": false,
    "output": "sample.json.bz2",
    "dump_format": "json",
    "first_property": "prop1",
    "second_property": null,
    "third_property": false,
    "argv": []
}
----
```
下記で確認できるように、内容を選択して設定ファイルに保存されます。
```bash:実行例2_のファイルの内容確認
% bunzip2 -c sample.json.bz2
{
    "class_help": false,
    "dump_format": "json",
    "first_property": "prop1",
    "second_property": null,
    "third_property": false
}
```

下記は保存された設定ファイルの読み込み実行例。下記でコマンドラインオプションでは"-x"は指定されていないですが、設定ファイルから`"first_property": "prop1",`と設定されていることが確認できます。

```bash:実行例3_設定ファイルの読み込み
% python3 ex_argparseextd.py --config sample.json.bz2

Default config path :  ***/var/lib/py_encstorage/config/ex_argparseextd.config.json
Final Namespace:  NamespaceExt(prefix=None, default_config='ex_argparseextd.config.json', help=False, config=None, save_config=None, verbose=False, class_help=False, skimmed_output=False, output=None, dump_format='json', first_property='prop1', second_property=None, third_property=False, argv=[])
Serialized JSON:
----
{
    "prefix": null,
    "default_config": "ex_argparseextd.config.json",
    "help": false,
    "config": null,
    "save_config": null,
    "verbose": false,
    "class_help": false,
    "skimmed_output": false,
    "output": null,
    "dump_format": "json",
    "first_property": "prop1",
    "second_property": null,
    "third_property": false,
    "argv": []
}
----
```
## まとめ

- Pythonモジュール`argparse-extd`を利用することで、コマンドライン引数での柔軟な実行オプション指定と、設定ファイルからの読み込みが可能になる。また、ファイルへの設定の保存が可能になる。
- 設定ファイルの形式としては、JSON, YAML, INI, TOMLおよびそれらの圧縮ファイル(.bz2, .gz, .xz)に対応