🐍

【Python】クラスで定義するコマンドラインパーサー

2022/07/17に公開約3,400字4件のコメント

はじめに

本記事では、筆者が作成した『クラスでコマンドラインパーサーを定義できるライブラリclassopt 』を紹介します。

https://github.com/moisutsu/classopt

このライブラリはRustのStructOptを参考に作成した、コマンドライン引数を解析するためのライブラリであり、Pythonの標準ライブラリであるargparse変数の型が推定できないという問題を解決しています。
変数の型を推定できるようにすることで、VSCodeなどのエディタでコーディングする際に、引数に対する補完(インテリセンス)を効かせることができます。

argparseを使った際のコーディング
argparse_demo

classoptを使った際のコーディング

classopt_demo

argparseを使った場合、解析するコマンドライン引数は動的に定義されるため、静的解析で引数名と型が解析できておらず、インテリセンスが効いていません。
一方で、classoptを使った場合、解析するコマンドライン引数はクラスで定義されており、静的解析で引数名と型がわかるため、インテリセンスを効かせることができます。

型が解析可能であるため、例えばstr型のコマンドライン引数の場合だと、strのメソッドであるsplitなど、それぞれの型で定義されたメソッドも補完で出すことが可能です。

このように補完が効く部分を増やすことで、長い変数名を簡単に入力できるだけでなく、タイポなどのミスも減らすことができ、快適なコーディングを行なうことができるようになります。

Install & Usage

Install

PyPIで公開しており、pipでインストールすることができます。

pip install classopt

Usage

classoptでは、デコレータを活用してコマンドラインパーサーの定義を行います。

まずはシンプルな例を用いて説明を行います。

demo.py
from classopt import classopt

@classopt
class Args:
    count: int
    text: str

args: Args = Args.from_args()

print(args.count)
print(args.text)

実行例

$ python demo.py 10 hello
10
hello

コマンドラインパーサーを作成するために、デコレータclassoptでデコレートしたクラスを定義します。
そして、このクラス内で<引数名>: <型>の形で解析するコマンドライン引数を定義します。
これは内部で用いているdataclasses.dataclassと同じ定義方法です。

また、classoptは内部でargparseを用いており、config関数を用いることで、ArgumentParser.add_argumentで指定することができる全てのオプションを指定することができます(オプション引数と位置引数の指定以外)。

オプション引数と位置引数の指定は、ArgumentParser.add_argumentでは、以下のように引数名の指定と同時に行います。

argparse
# 位置引数
parser.add_argument("foo")

# オプション引数
parser.add_argument("-b", "--bar")

一方、classoptの場合は引数名はクラス定義時に指定しているので、config関数でオプション引数か位置引数かを指定します。
具体的には、config関数にshort=Trueを渡すとショートオプション、long=Trueを渡すとロングオプションを引数名から自動で定義し、どちらも渡さなければ位置引数となります。

classopt
@classopt
class Args:
    # 位置引数
    foo: str

    # オプション引数
    bar: str = config(long=True, short=True)
    # bar: str = config(long="--foo", short="-f") のように別名のオプション引数も指定可能

他にも便利な機能として、@classopt(default_long=True, default_short=True)のようにデコレータにdefault_long=Truedefault_short=Trueを渡すことで、全てのコマンドライン引数をオプション引数化することができます。

また、型としてlist[型]を与えるとnargs="*"を自動で定義し、boolを与えるとaction="store_true"を自動で定義してくれます。

まとめると、classoptは以下のように使うことができます。

demo.py
from classopt import classopt, config

@classopt(default_long=True, default_short=True)
class Args:
    paths: list[str] = config(help="Input paths")
    flag: bool = config(help="Input flag")

args: Args = Args.from_args()

print(args.paths)
print(args.flag)

実行例

$ python demo.py --paths sample/path1 sample/path2 -f
['sample/path1', 'sample/path2']
True

$ python demo.py --help
usage: demo.py [-h] [--paths [PATHS ...]] [--flag]

optional arguments:
  -h, --help            show this help message and exit
  --paths [PATHS ...], -p [PATHS ...]
                        Input paths
  --flag, -f            Input flag

おわりに

本記事では、クラスで定義するコマンドラインパーサーclassoptを紹介しました。
Pythonでのコーディングを快適にできる便利なライブラリなので、是非使ってみてください。

GitHubで編集を提案

Discussion

既にご存知かもしれませんが、似たようなものでclicktyperなどがあります。
この辺との差別化がアピールできるとさらに良くなると思います!
例えば、clickやtyperは引数をクラスで指定しないけど、classoptならできるから○○という点で良い!みたいな。
応援してます!

コメントありがとうございます!これらのライブラリは知らなかったので、参考になります🙇‍♂️
ぱっと思いついた差別化だと、コマンドライン引数の定義が関数と結合しておらず、独立しているとかでしょうか(見やすい、別ファイルに切り出せる)。
また時間があるときに、記事やREADMEに追記しておこうと思います!

似たような時期に似たようなコンセプトのライブラリを作っていてビックリしましたw

僕は開発止めちゃったので、いちstructopt好きとして応援しています 👍

コメントありがとうございます!記事とリポジトリを拝見しましたが、かなり似ていて自分も驚きました😲
良いライブラリにできるように、これからも頑張ります!

ログインするとコメントできます