python のパッケージ化について

今回 Azure Automation アカウント で python プログラムを実行するため、久しぶりに python に触れた。 その際、パッケージ化した時の挙動が良く分からず苦戦したのでメモしておく。

一から Automation 上でコードを書くのではなく、既にある モジュールを Automation で動作させる。 このプログラム構成を以下とする。 ポイントは、config ディレクトリの位置で、src ディレクトリ(モジュール)配下ではなく、 別のディレクトリに存在するパターンだ。

もともとのモジュール構成

C:.
│ setup.py
│
├─config
│    hoge.yml
└─src
    └─ saitama
          hoge.py
          __init__.py

既存のモジュールをパッケージ化する

setup.py の内容は以下である

from setuptools import setup

setup(
    name='saitama',
    version='1.0.0',
    include_package_data=True,
    packages=["saitama"],
    package_dir={'saitama': 'src/saitama'},
    # package_data={'saitama': ['config/hoge.yml']},
    # data_files=[('config', ['config/hoge.yml'])],
)

配布パッケージを作成する(whl 形式)

python setup.py bdist_wheel

配布パッケージを作成する(tar.gz や zip 形式)

python setup.py sdist --formats=gztar

なお、Automation で python パッケージをインポートできるのは、"whl" か "tar.gz" のみである

上記のまま実行すると、パッケージファイルには、config ディレクトリが含まれないが、 コメントアウトしている package_data や data_files を設定すると含めることができる。

package_data と data_files

これが分かりにくかった。 package_data をつけると、パッケージ内に存在するファイルを配布パッケージの中に含ませることができる。 注意点として、そもそもパッケージ内にファイルが存在する前提で、 src/saitama/config のように配置された状態でパッケージしなければならない。 外部のパスを指定しようとしても受け付けないので、モジュール外にある config ディレクトリは指定できない。

なお、モジュール内に存在する場合は指定できるが以下のように

{'saitama': ['config/hoge.yml']} 

としても

# 空文字
{'': ['config/hoge.yml']}
# パッケージに存在しないようなキー
{'chiba': ['config/hoge.yml']}

でもよい。 オブジェクトのキーに関係なく、そのパッケージ内に指定したパスでファイルがあればコピーされる。 動作としては、どのパッケージ内のファイルを含むかわかりやすくキー付けしているレベル。

次に、data_files であるが、これはパッケージ内でなく、パッケージ外にあるファイルも指定できる。 なので、最初の構成にあるような位置にある config ディレクトリを配布パッケージに含めることができる。 そして、package_data と同様、第一引数は重要ではない。重要ではないというか少し後で説明する。 data_files は配列にタプルで指定するが、タプルの第 2 引数であるパスはプロジェクトルートから実際に存在するパスでなければならない。

# 存在しない
['config/aho.yml']
# ルートからの相対パスではない
['hoge.yml']

これが正しくないとファイルが配布パッケージに含まれない。 また、package_data と同様に(正確には少し異なる)、以下でも動作する

data_files=[('', ['config/hoge.yml'])],

公式ドキュメントのここで説明されている。

2. setup スクリプトを書く — Python 3.11.3 ドキュメント

シーケンス中のそれぞれの (directory, files) ペアは、インストール先ディレクトリとそこにインストールするファイルのリストを指定します。

files リスト中の各ファイル名はパッケージのソースディストリビューションの最上位にある setup.py スクリプトからの相対パスとして解釈されます。データファイルがインストールされるディレクトリは指定できますが、データファイルの名前を変更することはできないことに注意してください。

directory は相対パスである必要があり、インストールプレフィックス (システムインストールの場合は Python の sys.prefix; ユーザーインストールの場合は site.USER_BASE) からの相対パスと解釈されます。 Distutils は directory に絶対パスを指定することができますが、この方法は wheel パッケージング形式と互換性がないため推奨されません。 files のディレクトリ情報はインストール先の決定には使われません; ファイル名だけが使われます。

data_files オプションは、ターゲットディレクトリを指定せずに、単にファイルの列を指定できます。しかし、このやり方は推奨されておらず、指定すると install コマンドが警告を出力します。ターゲットディレクトリにデータファイルを直接インストールしたいなら、ディレクトリ名として空文字列を指定してください。

配布パッケージ化した時は含まれていたのにインストールすると含まれない

配布パッケージを作るだけなら、data_files の設定が正しければファイルは追加される。 しかし、以下のようにシステムにインストールしようとしたときにファイルが追加されない!

py -m pip install dist\saitama-1.0.0-py3-none-any.whl

data_files のポイント 2 つめは、第一引数は sys.prefix からの相対パスであること。 その場所にインストールされる。配布パッケージ内ではない! 言ってしまえば、別モジュールの扱いになるのだ。

例. モジュールのインストール先

C:\Users\YAMADA\AppData\Local\Programs\Python\Python311\Lib\site-packages

例. data_files で指定したファイルのインストール先

# sys.prefix 配下
C:\Users\YAMADA\AppData\Local\Programs\Python\Python311

そして、Azure Automation で python モジュールをインポートした時も同様と考えられ、一番最初の構成だと ファイルが参照できない。

python モジュールのインポートに成功すると以下のように参照できる

# インポートできる
import saitama

# ファイルを参照する
d = os.path.dirname(sys.modules["saitama"].__file__)
data = open(os.path.join(d, "config\hoge.yml"), 'rb').read()

最終的に採用した構成

config ディレクトリは、src 配下に移動した。

C:.
│ setup.py
└─src
    └─ saitama
        │  hoge.py
        │  __init__.py
        │
        └─config
              hoge.yml

setup.py

from setuptools import setup

setup(
    name='saitama',
    version='1.0.0',
    include_package_data=True,
    packages=["saitama"],
    package_dir={"saitama": "src/saitama"},
    package_data={'': ['config/hoge.yml']},
    # data_files=[('config', ['config/hoge.yml'])],
)

配布パッケージにする

python setup.py sdist --formats=gztar

Azure Automation の 共有リソース -> Python パッケージ から追加する

追加できたら、Runbook でテストコードを実行する

#!/usr/bin/env python3
import os
import pkgutil
import sys

import saitama

d = os.path.dirname(sys.modules["saitama"].__file__)
data = open(os.path.join(d, "config\hoge.yml"), 'rb').read()
print(data)

# こちらでも参照はできる
data = pkgutil.get_data('saitama', 'config/hoge.yml')
print(data)

sys.exit(0)

テスト