
こんにちは。 DSOC R&D グループの高橋寛治です。
前回の「CircleCI と pytest に入門」でさらっと触れた内容である、Python パッケージを pip 1 でインストール可能にする方法について、もう少し説明します。
pip でインストール可能な状態にすることにより、一行のコマンドでインストールできたり、名前空間を意識した良いコードになったりと、たくさんの利点があります。 何よりインストールが可能であるため、他の人に使ってもらえます!
ただ、初めてパッケージを書くときは、独自記法を覚える必要がありやや大変でした。 今回の記事では、パッケージ化からテストを行うまでをなるべく最低限の記法について紹介したいと思います。
インストール可能なパッケージを作ったことがない人や、作ったことはあるがよくわからない人の助けになれば幸いです。
本記事のゴール
- パッケージやモジュールといった、パッケージ作成に必要な Python の名前空間を知る
- 次のコマンドでインストール可能なパッケージを作れる
pip install git+https://github.com/your-github-id/your-repository
- パッケージに対してテスト(pytest)を行う
- PyPI への登録に挑戦したくなる
pip コマンドでできること
pip コマンドでは Python のパッケージ管理を行います。
pip install パッケージ名 で Python パッケージをインストールします。
pip list でインストール済みの Python パッケージの一覧を出力します。
様々なオプションがありますので、 pip help で確認してみてください。
pip は setup.py の記述に従ってパッケージのインストールを行います。
この setup.py を書きパッケージ化する上で重要な点について紹介します。
まずは、インストールするパッケージのバージョンを指定する記法についてです。
pip install パッケージ名==バージョン で指定します。
等号2個( == )は特定のバージョンを指定し、大なり小なりも利用可能です。
次に、インストールするパッケージ一覧をファイルから読み込むことが可能だ、ということです。
慣習的に requirements.txt と呼ばれるファイルを作成し、所定の記法でパッケージを記述します。
requirements.txt
パッケージA パッケージB==1.0.0
記述された requirements.txt ファイルは pip install -r requirments.txt と r オプションの引数で指定します。
Pythonの名前空間を知る
名前空間とは、オブジェクトへの名前の対応づけのことを言います。
変数名やクラス名などは名前です。
よく、 object.variable のようにアクセスすると思います。
これは object という名前空間の属性 variable を表します。
たとえば count という変数が様々なオブジェクトに属していた場合、オブジェクト内では count で一意に定まりますが、オブジェクト外から参照する場合は オブジェクト名.count で特定できます。
人の名前も似ており、名だけだと特定しづらいですが姓名とすると特定しやすくなります。
名前を一意に決めるためのきまりと捉えるとわかりやすいかと思います。
Pythonにおいては、モジュールやパッケージも名前空間の一つだと考えるといいでしょう。
モジュールとは、 Python で書かれたメソッドの定義や文が記述されたファイル(*.py)のことです。
たとえば、モジュール内のメソッドは、 module.method という名前空間で示されます。
パッケージとはモジュールの名前空間を構造化する(モジュールをまとめて取り扱う)ものです。
たとえば、package.module は package パッケージの module モジュールということを表します。
パッケージ内でも同様に、モジュールごとに名前空間があります。
名前空間があるからこそ同名の名前でも、別モジュールのものだということを識別できます。
名前空間のスコープについては複雑であるため、ここでは割愛いたします。 詳しくは、Pythonの公式マニュアルをご覧ください。
パッケージを作る
実際に setup.py を書いてみます。
setup.py(for humans)が非常に参考になります。
PyPIには登録しないパッケージを書きます。
ローカルのPython環境へのインストールができる状態ということです。
テストには pytest を用いる前提で、test_requires と setup_requires を記入します。
setup.py
from glob import glob from os.path import basename from os.path import splitext from setuptools import setup from setuptools import find_packages def _requires_from_file(filename): return open(filename).read().splitlines() setup( name="パッケージ名", version="0.1.0", license="ライセンス", description="パッケージの説明", author="作成者", url="GitHubなどURL", packages=find_packages("src"), package_dir={"": "src"}, py_modules=[splitext(basename(path))[0] for path in glob('src/*.py')], include_package_data=True, zip_safe=False, install_requires=_requires_from_file('requirements.txt'), setup_requires=["pytest-runner"], tests_require=["pytest", "pytest-cov"] )
依存モジュールは _requires_from_file メソッドにより、外部ファイルから取得します。
別途必要なモジュールの一覧を記述した requirements.txt を用意しましょう。
packages や package_dir などでは、 src ディレクトリ以下のファイルがパッケージングの対象であることを示しています。
MANIFEST.in
include requirements.txt
MANIFEST.in ではパッケージ化する際に requirements.txt をパッケージに含めること宣言しています。
setuptools によるパッケージ化では、 通常は Python コードのみが対象となっています。
なおディレクトリ構成は次の通りです。
src 以下にパッケージディレクトリを作成しているのは、pytest のベストプラクティスに従っているためです。
.
├── requirements.txt
├── setup.py
├── setup.cfg
├── src
│ └── packagename
│ ├── __init__.py
│ └── module.py
└── tests
├── __init__.py
└── test_module.py
setup.py のあるディレクトリで、 pip install . とするとパッケージがインストールされます。
インストール後に pip list でインストールされたことを確認しましょう。
setup(name="パッケージ名") で指定したパッケージ名の行(パッケージ名 0.1.0)が増えているはずです。
ローカルでインストールできるようになったら、ぜひGitHubレポジトリにプッシュしてみてください。
pip install git+https://github.com/your-github-id/your-repository でインストールできるようになります。
もし、プライベートレポジトリを対象にインストールを行う場合は、 pip install git+ssh://git@github.com/your-github-id/your-repository としてください。
また、SSHの設定ファイルに対象のプライベートレポジトリへの接続設定を記述する必要があります。
この場合は、ホストgithub.com にアクセスする際に git ユーザで、指定した鍵でアクセスするという設定が必要です(GitHubを利用されている場合は何かしら設定されているはずです。)
テストを行う
ここまでで、 pip でインストール可能なパッケージを作成しました。
テストを行うために、開発者モードでパッケージをインストール pip install -e . しましょう。
pip list で確認すると、 先ほどとは異なりバージョンの右側に実体へのパス パッケージ名 0.1.0 /path/to/src が増えているはずです。
Python パッケージをインストールしたのですが、実体は開発中のディレクトリを参照しているということです。(通常のインストールでは site-packages以下にインストールされる。)
packagename.module という名前空間でアクセスできるようになります。
これにより、 pytest が本番相当の構成で動作することになります。
pytest を実行すると、 tests ディレクトリ以下のテストが見事に実行され、次のような出力を得られるはずです。
======================================== test session starts ======================================== platform linux -- Python 3.6.8, pytest-4.4.1, py-1.8.0, pluggy-0.9.0 rootdir: /path/to/package plugins: requests-mock-1.6.0, mock-1.10.0, cov-2.6.1 collected 1 items tests/test_module.py ... [100%] ===================================== 1 passed in 2.03 seconds ======================================
次にインストール前にテストを行えるように設定します。
例えば git clone をした後に、 python setup.py test というコマンドでテストをできるようにします。
setup.cfg ファイルを作りましょう。
これは setup.py 実行時のパラメータなどを記述する設定ファイルです。
setup.cfg
[aliases] test=pytest
test を pytest としています。
これにより python setup.py test が python setup.py pytest と等価になります。
実は設定済みなのですが、 setup.py の中に次の記述がありました。
setup_requires=["pytest-runner"], tests_require=["pytest", "pytest-cov"]
setup_requires は setup.py で pytest を実行可能にするためのものです。
tests_require はテスト実行時に必要なモジュールのリストです。(require が単数形であることに注意)
python setup.py test 実行時に、テストに必要なモジュールがプロジェクトルートに保存されます。
すなわち、 pip install や python setup.py install でのインストール時にテストに必要なモジュールはパッケージとしてインストールされない(デフォルトだと site-packages 以下にインストールされない)ということです。
PyPIもいいぞ
pip install パッケージ名 でインストールできるようにするために公開レポジトリである PyPI に公開する場合は、詳細は割愛しますが次の手順を踏みます。
- PyPIのアカウントを作成
python setup.py sdist bdist_wheelにより配布可能なパッケージ化を行うtwineを利用して、PyPI にアップロード
ソースコード以外のファイルを含める場合の MANIFEST.in の動作や、配布物の圧縮などの細かい話は、公式の sdist に関するマニュアルがおすすめです。
私もPythonパッケージを一つアップロードしています。
pip install パッケージ名 で導入できると、かっこよくてモチベーションがあがるのでおすすめです。
pipでインストールできると使われる
pip コマンド一つで導入ができると、ビルドツールに組み込みやすいですし、何よりお試しで使いやすくなります。
今回の記事の説明もわかりづらいですが、一度作ってしまえばあとはバージョンや説明を変えるだけとなります。 パッケージ化は利点が大きいと思いますので、ぜひ挑戦してみてください。
執筆者プロフィール
高橋寛治 Sansan株式会社 DSOC (Data Strategy & Operation Center) R&Dグループ研究員
阿南工業高等専門学校卒業後に、長岡技術科学大学に編入学。同大学大学院電気電子情報工学専攻修了。在学中は、自然言語処理の研究に取り組み、解析ツールの開発や機械翻訳に関連する研究を行う。大学院を卒業後、2017年にSansan株式会社に入社。現在はキーワード抽出など自然言語処理を生かした研究に取り組んでいる。
-
pip とは、PyPI(The Python Package Index)に登録されている Python パッケージのインストールや、インストールされているパッケージの管理などを行うツールです。Python 3.4 以降では標準でインストールされています。↩