あとで後悔しないPythonのディレクトリ構成をつくってみる

Pythonを触った当初は、とりあえず hoge.py にたくさんメソッドを書き、そのうちファイルが複数になり、ディレクトリが増えていきました。
しかしこのようにすると自作プログラムの中の依存関係の解決に失敗したり、どんどん訳の分からない構成になってしまい、あとで正すことも難しく感じました。

そのためよっぽど緊急なプログラミングでないかぎりは、最初に**「ライブラリ化しやすいようにディレクトリ、ファイルを準備する」**ことが重要かと思っています。
そのテンプレートを、本記事でつくってみます。

## まずはsetup.pyから書いてみる

まずプロジェクトのフォルダとして python_package ディレクトリをつくって、setup.py を新規作成します。
プロジェクト直下は当然ながら下のようになります。

```
.
└── setup.py
```

**setup.py は、自作のプログラムをライブラリとして提供するときに絶対に必要なものです。**

とりあえず現状で入れられるものを埋めていきましょう。
自分が作ろうとしているプロジェクトの名前やソースの配置先を決めてしまいます。

```setup.py
from setuptools import setup

setup(
    name="koboripackage",
    version='1.0',
    description='Pythonのディレクトリ構成のテスト用',
    author='Kobori Akira',
    author_email='private.beats@gmail.com',
    url='https://github.com/koboriakira/python_package',
)
```

つくるプロダクトが明確であれば、この段階で README.md を作成しておいてもいいと思います。

## とりあえずライブラリにしてみる

setup.py を書いたら、つぎにソースのディレクトリを準備します。

setupの `name` に書いたのと同名のディレクトリを作成し、ディレクトリ名と同じファイル名のPythonファイルも作ります。これがライブラリとして利用されるモジュールだとしましょう。

```
.
├── koboripackage
│   └── koboripackage.py
└── setup.py
```

```koboripackage.py
def hello(name):
    print('Hello, %s!' % name)
```

作成が完了したら setup.py のあるディレクトリで `python setup.py sdist` を実行します。

```bash
$ python setup.py sdist
running sdist
running egg_info
.
.
.
creating dist
Creating tar archive
removing 'koboripackage-1.0' (and everything under it)
```

すると色々とファイルが増えました。

```
.
├── dist
│   └── koboripackage-1.0.tar.gz
├── koboripackage
│   └── koboripackage.py
├── koboripackage.egg-info
│   ├── PKG-INFO
│   ├── SOURCES.txt
│   ├── dependency_links.txt
│   └── top_level.txt
└── setup.py
```

このうち、distに入っているtarが `pip install` などで皆に使ってもらうライブラリです。
まだライブラリを公開していないため、さしあたりこのtarを直接指定して自分のローカルに入れてみましょう。

```
pip install dist/koboripackage-1.0.tar.gz
```

実際にライブラリとして利用できるか試してみましょう。CLIでPythonを実行します。

```
$ python
>>> from koboripackage import koboripackage
>>> koboripackage.hello('World')
Hello, World!
>>> 
```

このように自分のつくったモジュール・関数をインポートして使うことができました。
なお `pip uninstall koboripackage` でライブラリをアンインストールできます。

## CLIでも使えるようにする

Pythonで作ったライブラリは、ターミナルからコマンドラインで実行することもできます。

そのための設定を追加しましょう。まずソースディレクトリ直下に cli.py を作成します。

```cli.py
def execute():
    print('コマンドが実行されました！')
```

そしてsetup.pyを編集します。

1. `version` を1.1にします
2. importに`find_packages` を追加して、 `packages=find_packages()`を追加します
3. `entry_points` を追加します。形式は `コマンド名 = モジュール:メソッド` で書きます

```setup.py
from setuptools import setup, find_packages

setup(
    name="koboripackage",
    version='1.1',
    description='Pythonのディレクトリ構成のテスト用',
    author='Kobori Akira',
    author_email='private.beats@gmail.com',
    url='https://github.com/koboriakira/python_package',
    packages=find_packages(),
    entry_points="""
      [console_scripts]
      koboripackage = koboripackage.cli:execute
    """,
)
```

完成したら再度 `python setup.py sdist` を実行します。
するとdistに新しくv1.1のtarが作成され、以下のような構成になります。

```
.
├── dist
│   ├── koboripackage-1.0.tar.gz
│   └── koboripackage-1.1.tar.gz
├── find_package.txt
├── koboripackage
│   ├── cli.py
│   └── koboripackage.py
├── koboripackage.egg-info
└── setup.py
```

さきほど同様、 `pip install dist/koboripackage-1.1.tar.gz` でライブラリをインストールしてみます。すると、

```
$ koboripackage
コマンドが実行されました！
```

上記のようにコマンド名だけで cli.pyのexecuteメソッドを実行できたことがわかります。


## 既存ライブラリの利用

cli.pyに既存ライブラリをimportしてみます。

```cli.py
import requests


def execute():
    print('コマンドが実行されました！')
    response = requests.get('https://www.google.com/')
    print(response.status_code)

```

requestsを（ローカルで）利用するには `pip install requests` でライブラリをインポートする必要があります。
同様にライブラリとして提供するにも、「何をインポートしないといけないか」を明記する必要があります。

このために準備するのが requirements.txt です。プロジェクト直下に配置しましょう。

```requirements.txt
requests==2.23.0
```

ちなみに必要なライブラリ、バージョンは `pip freeze` コマンドで確認できます。
ただし現在インストールしているすべてのライブラリが出力されるので、そのままコピペしてしまうと不要なライブラリも入れてしまうかもしれません。

つぎに `python setup.py sdist` でtarを作成するときに requirements.txt が正しく利用されるように、2つ設定を行います。

1. MANIFEST.in を新規作成します
2. setup.py に `install_requires` を追加します（ついでにバージョンを1.2に）

```
.
├── MANIFEST.in
├── dist
├── koboripackage
│   ├── cli.py
│   └── koboripackage.py
├── koboripackage.egg-info
├── requirements.txt
└── setup.py
```

```MANIFEST.in
include requirements.txt
```

```setup.py
from setuptools import setup, find_packages

setup(
    name="koboripackage",
    version='1.2',
    description='Pythonのディレクトリ構成のテスト用',
    author='Kobori Akira',
    author_email='private.beats@gmail.com',
    url='https://github.com/koboriakira/python_package',
    packages=find_packages(),
    entry_points="""
      [console_scripts]
      koboripackage = koboripackage.cli:execute
    """,
    install_requires=open('requirements.txt').read().splitlines(),
)
```

**MANIFEST.inは、「パッケージ化されるときに通常含まれないものを追加する（その逆も可）」**ことができます。
`include requirements.txt` と記述したので、パッケージ化するときに requirements.txtを追加することになります。

そしてsetupに追加した `install_requires` で、必要なライブラリを指定します。
直接ライブラリ名を配列で指定してもいいのですが、上記のようにrequirements.txtの中身を読み込ませるようにするのが一般的なようです。

※直接指定する場合

```
install_requires=['requests']
```

ここまで準備が終わったら `python setup.py sdist` でファイルで、v1.2のtarを作成します。
`pip install dist/koboripackage-1.2.tar.gz` で新バージョンのライブラリをインストールしたあとにコマンドを実行してみると、

```
$ koboripackage
コマンドが実行されました！
200
```

上記のようにライブラリを利用できていることがわかります。

## PyPiにアップロード

ここまで準備しておければ、PyPi（Python Package Index）に登録することができます。
具体的な手順は https://blog.amedama.jp/entry/2017/12/31/175036 を参考にしてください。
うまくアップロードできれば、 `pip install koboripackage` などでインストールすることが可能となります。

## testsを準備

テストができるようにも準備しておきましょう。

プロジェクト直下にtestsフォルダを作成します。
ソースのあるディレクトリと同じ構成にして、テスト用のPythonファイルを作成します（プレフィクスにtestをつける）。

```
.
├── MANIFEST.in
├── dist
├── koboripackage
│   ├── cli.py
│   └── koboripackage.py
├── koboripackage.egg-info
├── requirements.txt
├── setup.py
└── tests
    └── test_koboripackage.py
```

koboripackage.py、test_koboripackage.py それぞれの次のようにソースを書いて、`multiply()` の挙動を確かめるようにしてみます。

```koboripackage.py
def hello(name):
    print('Hello, %s!' % name)


def multiply(a, b):
    return a * b
```

```test_koboripackage.py
from koboripackage import koboripackage


def test_multiply():
    assert koboripackage.multiply(2, 3) == 6
```

`pip install pytest` でpytestをインストールしておいてから、`pytest` を実行します。

```
$ pytest
================================================================================================== test session starts ==================================================================================================
platform darwin -- Python 3.8.2, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: ...............
collected 1 item                                                                                                                                                                                                        

tests/test_koboripackage.py .                                                                                                                                                                                     [100%]

=================================================================================================== 1 passed in 0.02s ===================================================================================================
```

これで一般的なPythonプロジェクトを作ることができました。

## Docker、gitを準備

最後にせっかくなのでDockerイメージの準備もしてみます。
必要なファイルを追加したうえで、あらかじめライブラリをインストールしたイメージをつくります。

```Dockerfile
FROM python:3.7.5-slim

WORKDIR /work

ADD koboripackage koboripackage
ADD tests tests
ADD requirements.txt requirements.txt
ADD setup.py setup.py
ADD MANIFEST.in MANIFEST.in

RUN pip install --upgrade pip \
  && pip install -r requirements.txt \
  && pip install pytest

CMD bash
```

`docker build -t python_package .` でイメージを作成して、 `docker run -it --rm python_package` でコンテナを作成します。
Dockerコンテナの中でも `pytest` を実行することができるようになりました。
そのまま開発する場合は、次のようにしてソースを共有することも可能です。

```
docker run -it --rm -v $(pwd)/koboripackage:/work/koboripackage -v $(pwd)/tests:/work/tests  python_package
```

最後にこれをgithubにpushします。
この工程の間に作られたファイルの中には、わざわざgithubに上げる必要のないものもあるので、.gitignoreを準備しておきます。

```.gitignore
**/__pycache__
dist/
koboripackage.egg-info/
```

## さいごに

こうして作成したパッケージが次の通りです。いちおうPyPiにもアップロードしておきました。
https://github.com/koboriakira/python_package

なお本来はsetup.pyにライセンスを記述することあるのですが、立ち入る自信がなかったので今回は避けました。
現状は「とりあえずMITにしておけばいいかな」程度の把握です。

このようにライブラリ化できる状態を最初から準備することで、適切なサイズのコンポーネントを意識的につくるのが容易になればと思います。

## 参考

* https://python-guideja.readthedocs.io/ja/latest/writing/structure.html
* https://rinatz.github.io/python-book/ch04-06-project-structures/
* https://www.rhoboro.com/2018/01/25/project-directories.html
* https://github.com/rhoboro/webapp_skeleton/blob/master/Makefile
* https://techblog.asahi-net.co.jp/entry/2018/06/15/162951#%E3%83%86%E3%82%B9%E3%83%88%E5%8F%AF%E8%83%BD%E3%81%AA%E6%9C%80%E5%B0%8F%E6%A7%8B%E6%88%90%E3%82%92%E4%BD%9C%E3%82%8B