Pythonで作ったコマンドをGitHub経由でpipインストール可能にする
こんにちは、ターミナル住人の平野です。
先日Pythonで作ったコマンドを公開する際、 Pythonなんだし、やっぱりpipでインストールできるようにしたいなと思い、 そのやり方を調べたので記録しておきます。
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_points
のconsole_scripts
について
これを書くことによって、いわゆるコマンドとして使えるようになります。 逆にこれがないと、別のPythonスクリプトから呼び出すライブラリという位置付けになるようです。
'console_scripts': ['sample_command = sample_command.sample_command:main']
sample_command
だらけでわけがわからなくなりそうですが、
- 左辺
- コマンド名を入力します。
- 右辺
- コマンドの起点となるメソッドを指定します。
- 最初の
sample_command
はディレクトリ名としてのsample_command
- 2つ目の
sample_command
はsample_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_command
の
main
メソッドをインポートして呼んでいるだけです。
sample_command
パッケージの実体はsys.path
で見つけることができる別の場所に格納され、
このようなファイルだけがPATHが通った場所に出力されるという形で実現されるようです。
また、1行目のシバンではインストールに使用したpipに対応したPythonが明示的に指定されています。 pipでインストールされたコマンドについては、どのPythonで動くのかが明示的に決まっているということなんですね。 (もちろん明示的にPythonを指定してコマンドの起点ファイルを実行した場合は除く)
ディレクトリ名について
__init__.py
とsample_command.py
を格納したディレクトリですが、
最初はsrc
などという名前で試していました。
ですが、それだと上記のfrom import
文のところが
from src.sample_command import main
となってしまい、起点がsrc
というめちゃくちゃ一般的な名前になってしまいます
(リポジトリの名前は登場しないので、これだけ見てもどのアプリか全然わからない)。
これだと同じ発想の人とディレクトリが一緒になってしまい、明らかに何かの不具合に繋がりそうです。
なので、固有のディレクトリ名をつける必要がありそうです。
実際にはツール名と同名にするのが一番安直で良いのかなと思います。
まとめ
pipでGitHubからコマンドをインストールできるような配布方法のやり方を調べてみました。
この方法だと配布が非常にやり易いですね。 ただ単にコマンドにするだけなら
- 実行権限付ける
- シバンを書く
- PATHの通った場所におく
を満たせばどんな形でも良い訳ですが、 やはり広く知れ渡った形式がある場合は、それに乗っかるのが一番いいです!
Pythonのパッケージ配布方法には結構ややこしい紆余曲折の歴史があるようなので、 時間を見つけてもう少し突っ込んで理解したいなぁと思っています。