pydanticからArgumentParserを作るライブラリを作った

# 作ったもの

こんな感じにpydanticからArgumentParserを自動で生成するライブラリを作りました。
https://github.com/elda27/pydantic_argify

```python
from argparse import ArgumentParser
from pydantic import BaseModel, Field
from pydantic_argify import build_parser

class Config(BaseModel):
    string: str = Field(description="string parameter")
    integer: int = Field(description="integer parameter")

parser = ArgumentParser()
build_parser(parser)
parser.print_help()
# config = Config(**vars(parser.parse_args()))
```

```
usage: basic.py [-h] --string STRING --integer INTEGER

optional arguments:
  -h, --help            show this help message and exit

Config:
  --string STRING, -s STRING
                        a required string
  --integer INTEGER, -i INTEGER
                        a required integer
```

# pydanticとは
pydanticは、Pythonのライブラリで、データ検証と設定の管理に使用されます。
Pythonの標準ライブラリであるdataclassesの高機能版と言ってもいいかもしれません。

特徴は、データを型アノテーションに基づいてデータの検証を行い、適切な型への変換や不正なデータをフィルタリングすることができることです。
特に型の自動変換は非常に便利で、文字列→数値、文字列→floatはもちろん、メールアドレスやUUIDの書式検証・変換も行ってくれるので非常に便利で、私は愛用しています。


# `pydantic-argparse-builder`の他のライブラリとの違いについて
似たようなことができるライブラリとして`pydantic-argparse`や`pydantic-cli`があります。

いずれも、同様にpydanticを用いて構築したクラスを基に、引数パーサーを作ることができます。
非常に便利で、pydanticを用いて構築したクラスを引数にした関数を1つ作れば、たくさんの引数をVSCodeの補完ありで実装できるため開発者体験がとても向上します。
私も自作ライブラリを作る前はよくお世話になっていました。

一方で、ArgumentParserを内部で隠蔽した設計のため、SubParserや引数のグループ化, 排他的引数など込み入った使い方をすることは容易ではありません。
特にSubParserをネストした使い方（例えば、`aws s3 cp <引数>`）はいずれのライブラリも非対応のように思います。

# 使い方
冒頭でも説明しましたとおりですが、少し実用的な使い方について例を上げて使用方法を示します。
ライブラリのインストールは以下のとおりです。
```
pip install pydantic_argify
```

今回の例は先述の`pydantic-argparse`や`pydantic-cli`でも実現可能ですが、これがネストしたサブコマンドになると同じ様にすることは不可能です。

```python example.py
from argparse import ArgumentParser

from pydantic import BaseModel, Field

from pydantic_argify import build_parser


class SubConfigA(BaseModel):
    string: str = Field(description="string parameter")
    integer: int = Field(description="integer parameter")


class SubConfigB(BaseModel):
    double: float = Field(description="a required string")
    integer: int = Field(0, description="a required integer")


def main():
    parser = ArgumentParser()
    subparsers = parser.add_subparsers()
    alpha_parser = subparsers.add_parser("alpha")
    build_parser(alpha_parser, SubConfigA)
    alpha_parser.set_defaults(_command="A")
    build_parser(subparsers.add_parser("beta"), SubConfigB)

    beta_parser = subparsers.add_parser("beta")
    build_parser(beta_parser, SubConfigB)
    beta_parser.set_defaults(_command="B")
    parser.set_defaults(_command="help")

    args = parser.parse_args()

    if args._command == "A":
        command_a(config=SubConfigA(**vars(args)))
    elif args._command == "B":
        command_b(config=SubConfigA(**vars(args)))
    else:
        parser.print_help()


def command_a(config: SubConfigA):
    print(config)


def command_b(config: SubConfigB):
    print(config)


if __name__ == "__main__":
    main()

```

```bash
$ python -m example alpha -s main -i 10
string='main' integer=10
```

## より単純な記法
```python example2.py
from pydantic import BaseModel, Field

from pydantic_argify import sub_command, main


class SubConfigA(BaseModel):
    string: str = Field(description="string parameter")
    integer: int = Field(description="integer parameter")


class SubConfigB(BaseModel):
    double: float = Field(description="a required string")
    integer: int = Field(0, description="a required integer")

@sub_command("alpha")
def command_a(config: SubConfigA):
    print(config)

@sub_command("beta")
def command_b(config: SubConfigB):
    print(config)


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

```bash
$ python -m example2 alpha -s main -i 10
string='main' integer=10
```

# まとめ
今回は自作ライブラリについてご紹介しました。
一応、一通りのユースケースについては実装しているつもり担っていますが、まだまだ網羅できていない機能はたくさんあると思うので、もし使えないケース等あればissueを立てるなりでご教示いただきたいです。