Pythonで作ったコマンドをGitHub経由でpipインストール可能にする

Pythonで作ったコマンドをpipでインストールできる形にしてGitHubで公開する方法を紹介しています。
2020.02.03

こんにちは、ターミナル住人の平野です。

先日Pythonで作ったコマンドを公開する際、 Pythonなんだし、やっぱりpipでインストールできるようにしたいなと思い、 そのやり方を調べたので記録しておきます。

CSV変形のお供に。テキストの一部分にだけコマンド適用するツールを作ってみた。

GitHubでの公開

pipを使うと通常はPyPIからパッケージを探そうとしますが、 探しに行く先としてGitHubを指定することもできるようです。

いくらPyPIが誰でもパッケージを登録できるリポジトリとは言え、 いきなりここに登録するのは心理的なハードル高すぎなので、 まずはGitHubでの公開だけに留めておきます。

スタート地点

スタート地点としては、sample_command.pyというファイルが一つあるだけの状態です。 このファイルは、コマンドsample_commandと実行する代わりに

python sample_command.py

とすれば全く同様に動作するようなものです。 今回は例として、「Hello!!!」と挨拶してくれる素敵なコマンドを作りました。

def main():
    print("Hello!!!")

if __name__ == '__main__':
    main()

まずはこのファイルが適当なディレクトリに1個だけ存在しているとします。

$ tree
.
└── sample_command.py

このコマンドをpipでインストールして使えるようにしてみます。

コマンド公開の手順

GitHubにリポジトリを作成する

この部分のやり方については情報はいくらでも出てくると思うので省略させて頂きます。 今回は以下のリポジトリでサンプルを作成しています。

https://github.com/cm-hirano-shigetoshi/python_sample_command

ディレクトリ構成

単独のファイルしかなかった状態から、いくつかのファイルを追加して、以下のようなファイル構成にします。

$ tree
.
├── README.md
├── sample_command
│   ├── __init__.py
│   └── sample_command.py
└── setup.py

実際の所は、上で作ったカラのリポジトリをgit cloneしてきて、 そのディレクトリの中に上記の構成を作成する感じになると思います。

以下、追加したファイルの説明です。

setup.py

pipを通じてインストールを可能にするキモとなるファイルのようです。 公式マニュアルに書いてあるものをコピーしてきて適宜変更します。

今回の場合こんなファイルになります。

import setuptools

with open("README.md", "r") as fh:
    long_description = fh.read()

setuptools.setup(
    name="sample_command",
    version="0.1.0",
    author="cm-hirano-shigetoshi",
    author_email="hirano.shigetoshi@classmethod.jp",
    description="You can receive the message 'Hello!!!'",
    long_description=long_description,
    long_description_content_type="text/markdown",
    url="https://github.com/cm-hirano-shigetoshi/python_sample_command",
    packages=setuptools.find_packages(),
    classifiers=[
        "Programming Language :: Python :: 3",
        "License :: OSI Approved :: MIT License",
        "Operating System :: OS Independent",
    ],
    entry_points = {
        'console_scripts': ['sample_command = sample_command.sample_command:main']
    },
    python_requires='>=3.7',
)

entry_pointsconsole_scriptsについて

これを書くことによって、いわゆるコマンドとして使えるようになります。 逆にこれがないと、別のPythonスクリプトから呼び出すライブラリという位置付けになるようです。

'console_scripts': ['sample_command = sample_command.sample_command:main']

sample_commandだらけでわけがわからなくなりそうですが、

  • 左辺
    • コマンド名を入力します。
  • 右辺
    • コマンドの起点となるメソッドを指定します。
      • 最初のsample_commandはディレクトリ名としてのsample_command
      • 2つ目のsample_commandsample_command.pyファイルの指定

README.md

このファイルは本質的には省略可能ですが、 setup.pyから呼ばれていますし、 パブリックに公開するのであれば何かしらの記述をした方が良いと思います。 動作確認であればカラでも大丈夫です。

init.py

このファイルは必須ですが、中身はカラでも大丈夫なようです。

この__init__.pyについてはsetup.py同様、パッケージ配布のキモになるファイルらしいのですが、 まだほとんど理解できていないので今回は「カラで動いたよ」という情報に留めたいと思います。

配布&インストール方法

下記のような構成(再掲)ができたら、これをcommitしてGitHubにpushします。

$ tree
.
├── README.md
├── sample_command
│   ├── __init__.py
│   └── sample_command.py
└── setup.py

これで公開は完了です。

インストールは以下のコマンドです。

pip install git+https://github.com/cm-hirano-shigetoshi/python_sample_command

これで、sample_commandコマンドが使えるようになるはずです。

$ sample_command
Hello!!!

コマンドの中身はどんなにつまらないものでも、 きちんとpipの仕組みに乗ってインストールされるので、 コマンドのアップデートや削除などもpipで一元管理することができます。

インストールされた状態

おまけ的に、pipでインストールされたファイルがどう展開されるのか、少しだけ調べてみました。

使用したpip

以下の状況はインストールに使用するpipによって場所が異なるかと思います。 今回の例では以下のようなpipを使っています。

$ pip -V
pip 19.3.1 from /usr/local/lib/python3.7/site-packages/pip (python 3.7)

これはmacOSでbrew install pythonでインストールされたPythonに付属されていたものです。

実行の起点となるファイル

pipでインストールしたsample_commandですが、whichしてみると

$ which sample_command
/usr/local/bin/sample_command

で、ファイルの中身を見てみると

#!/usr/local/opt/python/bin/python3.7
# -*- coding: utf-8 -*-
import re
import sys
from sample_command.sample_command import main
if __name__ == '__main__':
    sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
    sys.exit(main())

pipでコマンドをインストールするとこんなファイルが自動的に用意されるんですね。

やっていることとしては、インストールした本体があるsample_commandmainメソッドをインポートして呼んでいるだけです。 sample_commandパッケージの実体はsys.pathで見つけることができる別の場所に格納され、 このようなファイルだけがPATHが通った場所に出力されるという形で実現されるようです。

また、1行目のシバンではインストールに使用したpipに対応したPythonが明示的に指定されています。 pipでインストールされたコマンドについては、どのPythonで動くのかが明示的に決まっているということなんですね。 (もちろん明示的にPythonを指定してコマンドの起点ファイルを実行した場合は除く)

ディレクトリ名について

__init__.pysample_command.pyを格納したディレクトリですが、 最初はsrcなどという名前で試していました。 ですが、それだと上記のfrom import文のところが

from src.sample_command import main

となってしまい、起点がsrcというめちゃくちゃ一般的な名前になってしまいます (リポジトリの名前は登場しないので、これだけ見てもどのアプリか全然わからない)。 これだと同じ発想の人とディレクトリが一緒になってしまい、明らかに何かの不具合に繋がりそうです。 なので、固有のディレクトリ名をつける必要がありそうです。

実際にはツール名と同名にするのが一番安直で良いのかなと思います。

まとめ

pipでGitHubからコマンドをインストールできるような配布方法のやり方を調べてみました。

この方法だと配布が非常にやり易いですね。 ただ単にコマンドにするだけなら

  • 実行権限付ける
  • シバンを書く
  • PATHの通った場所におく

を満たせばどんな形でも良い訳ですが、 やはり広く知れ渡った形式がある場合は、それに乗っかるのが一番いいです!

Pythonのパッケージ配布方法には結構ややこしい紆余曲折の歴史があるようなので、 時間を見つけてもう少し突っ込んで理解したいなぁと思っています。

参考情報