読者です 読者をやめる 読者になる 読者になる

CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: 自作パッケージにデータファイルを含める

Python

Python で自作パッケージを作る際に、ソースコード以外のファイルを含めたくなる場合がある。 例えば Web アプリケーションを作るときの HTML テンプレートなんかはよくあるパターン。 今回は自作パッケージにソースコード以外のファイル (データファイル) を含めるやり方について書いてみる。

使用する環境は次の通り。

$ python --version
Python 3.5.1
$ pip list
pip (7.1.2)
setuptools (18.2)
wheel (0.24.0)

自作パッケージを用意する

データファイル云々を説明する前に、まずはそれを入れるためのパッケージから作っていこう。 今回作成するパッケージは 'mypackage' という名前にする。

最初にプロジェクトのディレクトリを用意する。 作成するパッケージ名とプロジェクトのディレクトリ名は揃える場合が多い。

$ mkdir mypackage
$ cd mypackage

プロジェクトのディレクトリに移動したら、さらに同じ名前でディレクトリを作る。 これが Python パッケージになるディレクトリだ。

$ mkdir mypackage

作成したディレクトリの配下に init.py という名前のファイルを作る。 これで mypackage ディレクトリが Python のパッケージとして認識できるようになる。 ファイルには動作確認用に greet() という名前の関数をひとつだけ用意しておこう。

$ cat << 'EOF' > mypackage/__init__.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-


def greet():
    print('Hello, World!')
EOF

次に mypackage のセットアップスクリプト (setup.py) を用意する。 このファイルを用意することで自作の Python パッケージを pip コマンドなどから扱えるようになる。

$ cat << 'EOF' > setup.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from setuptools import setup
from setuptools import find_packages


def main():
    description = 'mypackage'

    setup(
        name='mypackage',
        version='0.0.1',
        author='example',
        author_email='example@example.jp',
        url='www.example.jp',
        description=description,
        long_description=description,
        zip_safe=False,
        include_package_data=True,
        packages=find_packages(),
        install_requires=[],
        tests_require=[],
        setup_requires=[],
    )


if __name__ == '__main__':
    main()
EOF

これで mypackage パッケージが完成した。 ここまででディレクトリ構成は次のようになっている。

$ tree
.
├── mypackage
│   └── __init__.py
└── setup.py

1 directory, 2 files

自作パッケージをインストールする

それでは先ほど作ったセットアップスクリプトを使ってパッケージをインストールしよう。 パッケージのインストールにはスクリプトのサブコマンドとして install を指定する。

$ python setup.py install
...(省略)...
Processing dependencies for mypackage==0.0.1
Finished processing dependencies for mypackage==0.0.1

pip コマンドでインストール済みのパッケージを見ると mypackage がインストールされたことがわかる。

$ pip list | grep -i mypackage
mypackage (0.0.1)

インストールされたパッケージが使えることを確認しておこう。 Python からパッケージをインポートして動作確認用の関数を呼び出してみる。

$ python -c "import mypackage; mypackage.greet()"
Hello, World!

ばっちりだ。

ソースコード配布物を作る

ここからは説明の都合上、ソースコード配布物を使うことにする。 パッケージのソースコード配布物は sdist サブコマンドで作成できる。

$ python setup.py sdist
...(省略)...
Writing mypackage-0.0.1/setup.cfg
Creating tar archive
removing 'mypackage-0.0.1' (and everything under it)

コマンドを実行すると dist ディレクトリにソースコード配布物の tar ball ができている。

$ ls dist | grep tar.gz$
mypackage-0.0.1.tar.gz

できたソースコード配布物を適当な場所に展開しよう。

$ tar xvf dist/mypackage-0.0.1.tar.gz -C /tmp

中身はこんなかんじになっている。 あまり詳しくは説明しないものの、元のセットアップスクリプトとパッケージ以外にもパッケージ情報を格納したファイルなどが入っている。

$ tree /tmp/mypackage-0.0.1
/tmp/mypackage-0.0.1
├── PKG-INFO
├── mypackage
│   └── __init__.py
├── mypackage.egg-info
│   ├── PKG-INFO
│   ├── SOURCES.txt
│   ├── dependency_links.txt
│   ├── not-zip-safe
│   └── top_level.txt
├── setup.cfg
└── setup.py

2 directories, 9 files

ソースコード配布物にデータファイルを含める

さて、ここから今回の本題に入っていく。 まずはパッケージというかソースコード配布物に含めたいデータファイルを用意しよう。 例として greeting.txt という名前のテキストファイルを作ることにする。

$ cat << 'EOF' > greeting.txt
Hello, World!
EOF

さて、この状態で改めてソースコード配布物をビルドして展開してみよう。

$ python setup.py sdist
$ tar xvf dist/mypackage-0.0.1.tar.gz -C /tmp

先ほど作ったデータファイルが展開されたディレクトリに含まれるか調べてみよう。

$ find /tmp/mypackage-0.0.1 -name "greeting.txt"

残念ながら見つからない。 何もしない状態ではデータファイルはソースコード配布物に含まれないようだ。

では、どうすればデータファイルがソースコード配布物に含まれるようになるのだろうか。 これには MANIFEST.in という名前をもった設定ファイルが必要になる。 先ほど作った greeting.txt を含めるように include で指定しよう。

$ cat << 'EOF' > MANIFEST.in
include greeting.txt
EOF

改めてソースコード配布物をビルドして展開し直す。

$ python setup.py sdist
$ tar xvf dist/mypackage-0.0.1.tar.gz -C /tmp

すると今度は展開されたディレクトリの中に greeting.txt が見つかった!

$ find /tmp/mypackage-0.0.1 -name "greeting.txt"
/tmp/mypackage-0.0.1/greeting.txt

パッケージ内にデータファイルを含める

先ほどはソースコード配布物の中には含めたものの Python パッケージの中に入れていなかった。 次はデータファイルをパッケージ内に作るパターンを確認しておこう。

パッケージ内に data という名前でディレクトリを作って、そこに先ほどの greeting.txt を移動させよう。

$ mkdir -p mypackage/data
$ mv greeting.txt mypackage/data

次は MANIFEST.in の指定もちょっと工夫してみる。 具体的には recursive-include を使うことで特定のディレクトリ以下の内容を再帰的に検索して含めるようにする。 例えば次の設定では mypackage/data に含まれるすべての内容が含まれるようになる。

$ cat << 'EOF' > MANIFEST.in
recursive-include mypackage/data *
EOF

ディレクトリ構成としては次のようになる。 pycache というディレクトリは先ほどソースコード配布物をビルドした際にパッケージの内容がコンパイルされたためにできた。

$ tree mypackage
mypackage
├── __init__.py
├── __pycache__
│   └── __init__.cpython-35.pyc
└── data
    └── greeting.txt

2 directories, 3 files

この状態で改めてソースコード配布物をビルドして展開し直そう。 先ほど展開した内容を途中で一旦削除している点に注意する。

$ python setup.py sdist
$ rm -rf /tmp/mypackage-0.0.1
$ tar xvf dist/mypackage-0.0.1.tar.gz -C /tmp

ソースコード配布物を展開した内容を調べるとパッケージ内に greeting.txt が含まれていることがわかる。

$ find /tmp/mypackage-0.0.1 -name "greeting.txt"
/tmp/mypackage-0.0.1/mypackage/data/greeting.txt

データファイルがインストールされることを確認する

データファイルがソースコード配布物に含まれることは確認できたので、次はそれが実際にインストールされることを確認しよう。 まずは最初にインストールした内容 (データファイルが含まれていなかった) をアンインストールした上で、もう一度パッケージをインストールし直す。

$ pip uninstall -y mypackage
$ python setup.py install

この状態でサードパーティ製のパッケージがインストールされるディレクトリに移動する。 具体的には Python の site-packages というディレクトリになるんだけど、その場所を取得してそこに移動するには次のようにする。

$ cd $(python -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")

インストールされた内容を確認すると、ちゃんと data ディレクトリと greeting.txt ファイルが格納されていることがわかる。

$ tree mypackage-0.0.1-py3.5.egg
mypackage-0.0.1-py3.5.egg
├── EGG-INFO
│   ├── PKG-INFO
│   ├── SOURCES.txt
│   ├── dependency_links.txt
│   ├── not-zip-safe
│   └── top_level.txt
└── mypackage
    ├── __init__.py
    ├── __pycache__
    │   └── __init__.cpython-35.pyc
    └── data
        └── greeting.txt

4 directories, 8 files

インストールされたデータファイルを扱う

さて、データファイルがインストールされたことは確認できたので、次は実際にそれを使ってみよう。

インストール済みのデータファイルにアクセスするには、標準ライブラリであれば pkgutil というモジュールを使う。 pkgutil.get_data() 関数にパッケージ名とファイル名を指定するとその内容がバイナリで取得できることがわかる。

$ python
>>> import pkgutil
>>> pkgutil.get_data('mypackage', 'data/greeting.txt')
b'Hello, World!\n'

あとは取得したデータを煮るなり焼くなり好きにすれば良い。

ちなみに、これは setuptools の pkg_reources モジュール経由でも取得できる。

>>> import pkg_resources
>>> pkg_resources.resource_string('mypackage', 'data/greeting.txt')
b'Hello, World!\n'

めでたしめでたし。