qtatsuの週報

初心者ですわぁ

Pythonで個人用CLIツールを作成する

前書き

Pythonを使って、個人用CLIツールを作成するときの手順と参考になるリソースのまとめです。

最終的に、こんなイメージで使えるようにします。(例: noteというコマンドを作成)

# 仮想環境に入る
% source env/bin/activate

# 自作ツールのpip install
(env) % pip install --editable .  

# 自作ツールを使う
(env) % note "abc def efg"

GitHub

サンプルコードをgithubにアップロードしました。

GitHub - Kyutatsu-sandbox/python-cli-for-blog: sample code for blog post.

参考リンク

setup.pyを書いてパッケージ化する方法

How To Package Your Python Code — Python Packaging Tutorial

pythonのsetup.pyについてまとめる - Qiita

PythonでサクッとCLIツールを作る - Qiita

コマンドの引数を扱う標準ライブラリArgparse公式チュートリアル

Argparse チュートリアル — Python 3.9.1 ドキュメント

ターミナルで文字に色をつけるためのPythonライブラリ

termcolor · PyPI

環境

バージョン
MacOS Catalina 10.15.6
Python3 3.9.0

※ 古い方のMacを使ったのでCatalinaに戻ってます。誤植ではありません。

setup.pyを作成する

まずは仮想環境に入ります。

% python3.9 -m venv env
% source env/bin/activate

メインとなるpythonファイル(cli.py)とセットアップ用のファイルsetup.pyを作成します。

(env) % touch {cli,setup}.py

# このような構成になっていればOK
(env) % tree -L 1
.
├── cli.py
├── env
└── setup.py

まずcli.pyには、処理の入り口となる関数を定義します。コマンドを叩いたときに呼ばれる関数です。

from termcolor import colored, cprint


def main():
    print('STDOUT')
    return 'RETURN'

def color():
    cprint('STDOUT', 'green')
    text = colored('RETURN', 'red')
    return text

二つ関数を定義しました。color関数では、参考リンクに記述したライブラリtermcolorを利用しています。 ANSIカラー文字列を付加するwrapperです。ターミナル上に出力される文字列に色をつけることができます。


次にsetup.pyを書きます。

from setuptools import setup


setup(
        name='mycli-package',
        version='1.0.0',
        install_requires=['termcolor>=1.1.0'],
        entry_points={
            "console_scripts": ['mycli = cli:main', 'mycli_color = cli:color']
        }
)

とりあえず自分で作成したツールを使う上では上記の設定だけ分かっていれば事足りると思います。 詳しい説明は、参考リンクとして挙げた記事などを参照してください。

少し補足します。

install_requires

依存パッケージです。ここに書いておいたパッケージが、自作ツールをpipで入れるときに一緒にインストールされます。

package==1.0.0 (バージョン1.0.0を必ずいれる) のようなバージョン指定もできますし、単にpackageと言うふうに名称を書くだけでもOKです。 上記の例では1.1.0以上、としています。

entry_points

実際に、「コマンド(上例だとmycli, mycli_color)」と「実行される関数(上例だとmain, color)」をマッピングします。 console_scriptsというキーは決まっていて、CLIツールとして利用するコマンドをこのエントリーポイントへ書くことになっています(console_scripts Entry Point)。

pipでインストールする

ではコマンドを使ってみます。

まずは別の仮想環境を作成し、そちらに入ります。 (依存インストールなどがうまくいくことを確認するために行う手順です。ツール作成に必須ではありません)

% python3.9 -m venv another_env
% source another_env/bin/activate

# 出力なし。
(another_env) % pip freeze

先ほど作成したsetup.pyが存在するディレクトリへのパスを指定し、以下のようにしてインストールを行います。 (--editableについては後述)

# setup.pyがあるディレクトリ
(another_env) % pip install --editable  /Users/yourname/XXXX/python-cli-tool-template

pip freezeで確認します。 依存ライブラリとして設定したtermcolorも一緒にインストールされていることがわかります。

(another_env) % pip freeze
# Editable Git install with no remote (mycli-package==1.0.0)
-e /Users/yourname/XXXX/python-cli-tool-template
termcolor==1.1.0

ではコマンドを使ってみましょう!

f:id:Qtatsu:20210103155504g:plain
commnad-test

mycliコマンドで黒色の文字、mycli_colorで色付き文字がプリントされていることがわかります。

retrunした値は標準エラーに出力される

結果を見ると気が付くと思いますが、printで出力した文字だけでなく、returnした値も表示されています。

returnした値は標準エラー扱いです。

標準出力(1)を/dev/nullに捨てる、もしくは標準エラー(2)を捨てると以下のようになります。

(another_env) kyutatsu@Kmbp Temp % mycli 1> /dev/null
RETURN
(another_env) kyutatsu@Kmbp Temp % mycli 2> /dev/null
STDOUT

普通に使っているとどちらも表示されてしまいますが、以上のような挙動になっているので気をつけます。(詳しい理由と仕組みを知っている方がいれば教えていただけると幸いです)

--editableオプションをつけるとコードへの変更が即反映される

先ほどツールを入れるときに指定した--editableオプションですが、これは「develop mode」でのインストールになります。 ではdevelop modeとはなんぞやと言うことになりますが、順番に辿っていこうと思います。

実際の挙動

まずは挙動をみてみます。 先ほどインストールした自作ツールに変更を加えます。

def main():
    print('@@@@@@@@@@@@@STDOUT@@@@@@@@@@') 
    return 'RETURN'

保存してファイルを閉じ、mycliコマンドを打ってみます。

(another_env) % mycli
@@@@@@@@@@@@@STDOUT@@@@@@@@@@
RETURN

変更が反映されています。

では、--editableをつけなかった場合はどうなるのでしょうか。

まずcli.pyを元に戻します。

def main():
    print('STDOUT')
    return 'RETURN'

別の環境でインストールを行います。

# 別の環境
% python -m venv not_editable
% source not_editable/bin/activate

# 何もないことを確認
(not_editable) % pip freeze

MuduleNotFoundErrorを回避する

注意 Editableモードならこのままインストールすれば良いのですが、通常インストールではpackageやmoduleの場所を指定する必要があります。 今回は、setup.pyと同じディレクトリにモジュール(cli.py)を置いているので、以下のように追記します。このように書かないと、moduleを見つけることができずにエラーとなります。

  • setup.py
setup(
# .......(省略)..........
        py_modules = ['cli'],
# .......(省略)..........

ではインストールしていきます。

% pip install /User/yourname/XXXX/python-cli-tool-template

ここでfreezeしてみると、先ほどとは出力が違っていることがわかります! 自作ツールについていたはずのコメントなどや、editableモードであることを示す記述がなくなり、通常のライブラリと同じようにprintされています。

 % pip freeze
mycli-package==1.0.0
termcolor==1.1.0

今ここで、cli.pyを編集してみます。

def main():
    print('--------------STDOUT^---------------')
    return 'RETURN'

次にmycliコマンドを実行します。 先ほどと異なり、編集した内容が反映されません

反映されるには、パッケージを通常と同じように更新する必要があります

まずsetup.pyのバージョンもあげておきましょう。

  • setup.py
from setuptools import setup


setup(
        name='python-cli-tool-template',
        version='1.1.0',   # バージョンあっぷ!
# ....(省略).........
)

インストールします。

(not_editable)% pip install --upgrade  ~/MyProjects/GitHubForBlog/python-cli-tool-template

# 確認すると、バージョンが上がっているはず。
(not_editable) % pip freeze
python-cli-tool-template==1.1.0

挙動も更新されました!

(not_editable)  % mycli
--------------STDOUT^---------------
RETURN

editableモードの説明をドキュメントからたどる

まずはpipのヘルプをみてみます。

% pip install --help | grep -A 2 editable

  -e, --editable <path/url>   Install a project in editable mode (i.e.
                              setuptools "develop mode") from a local project
                              path or a VCS url.

(余談: grep-A 2は該当する行+後ろ2行も表示、と言う意味です)

「editable mode」は setuptoolsの「develop mode」を意味していることがわかります。

そこでsetuptools のドキュメントから該当箇所を探します。

Building and Distributing Packages with Setuptools — setuptools 51.1.1 documentation

Deploy your project in “development mode”, such that it’s available on sys.path, yet can still be edited directly from its source checkout.

ソースを直接変更できると言うことが書かれています。

さらに詳しい箇所がありました。 “Development Mode” — setuptools 51.1.1 documentation

It works very similarly to setup.py install, except that it doesn’t actually install anything. Instead, it creates a special .egg-link file in the deployment directory, that links to your project’s source code.

つまりeditableモードでinstallをした場合にはinstall先の仮想環境には何もインストールされず、直接ソースコードとコマンドがマップされるみたいですね。

Argparseでコマンドに引数を渡す

公式チュートリアルがあるので紹介程度に留めますが、作成したコマンドの引数を処理するには標準ライブラリArgparseが利用できます。

Argparse チュートリアル — Python 3.9.1 ドキュメント

  • 自動でヘルプ(-h)作成
  • 同時に使えない引数の設定や、可変長引数などの設定

など、sys.argsを直接パースしていては苦しい処理が楽にかけます。 上記チュートリアルを完了したら、以下のadd_argument関数のドキュメント部分を読めば大抵のことができると思います。

argparse --- コマンドラインオプション、引数、サブコマンドのパーサー — Python 3.9.1 ドキュメント

また、CLIツールを作成するフレームワークも存在します。本格的なCLIツールを作成したければそちらを使った方がいいかもしれません。 (自分は使ったことがないので、リンクの紹介のみです)

The Python Fire Guide - Python Fire

Welcome to Click — Click Documentation (7.x)