Python で標準インストールでなく egg を作って手で置く的なこと

なんでこれを今まで考えてこなかったのかちょっと不思議。

wheel の方がベターなのでわ、てのは今は無視。古いテクニックの方が適切な場合もある。想定しているのは「標準 Python 本体しかインストールされていないレンタルサーバ」で自作 cgi を python で作る際の依存モジュールをどうするか問題、とか、あるいは kivyLauncher の「パッケージマネージャがないので」問題、への一つの回答として。

なんで egg にしたいか、というのはだからつまり、「アップロード」の手間の話。ロリポップ! レンタルサーバ相手に(Python で)FTPS笑えないモジュールインポートのEOFError (Python)なんかで苦労して見せた通り、「大量のファイル」のコピーや移動はとかくトラブルメーカなのだ。これはリモートに限らない。いつでも「一個のデカいファイル」より「大量の小さなファイル」の方が取り扱いが大変だ、一定サイズ(2GB、4GB などのある種の制約)を超えてない限りは。無論、複雑なパッケージツリーをミラーする、という行為そのものも普通は「ダルい」。どんなにクレバーな手(例えば rsync)を使おうが。

egg の作り方は、「setup.pysetuptools で書いているプロジェクト」であればこれだけ:

1 [me@host: ~]$ cd somelib
2 [me@host: somelib]$ python setup.py bdist_egg

これにより dist フォルダに egg が作られる。無論 setuptools は標準添付ライブラリではないので、インストールしていないならインストールする必要がある。

setup.pysetuptools で書いていないプロジェクト」は少々改変する必要がある。実例はまぁなんでもいいのだが、小さそうなものとして dictlib を探してみた。こうする:

 1 #from distutils.core import setup  # NOT distutils
 2 from setuptools import setup  # USE setuptools
 3 setup(
 4   name = 'dictlib',
 5   packages = ['dictlib'],
 6   version = '1.0.3',
 7   description = 'Yet another Dictionary Library including good deep merge and dictionary as objects',
 8   author = 'Brandon Gillespie',
 9   author_email = 'bjg-pypi@cold.org',
10   url = 'https://github.com/srevenant/dictlib', 
11   download_url = 'https://github.com/srevenant/dictlib/tarball/1.0',
12   keywords = ['dict', 'union', 'object'],
13   classifiers = [],
14 )

これで python setup.py bdist_egg を実行出来る。

そもそも setup.py がないもの、これはおそらく「あなたの自作モノ」だろう。この場合はミニマルなこんな:

1 from setuptools import setup, find_packages
2  
3 setup(
4     name = "yourpylib",
5     version = "0.1",
6     packages = find_packages()
7     )

でええよ、と Python 101: easy_install or how to create eggs で紹介されてた。自分では試してない。

あとはこの egg をどうやって使えるのか、ということだが、egg (つまり zip ファイル) に固めてないものと少しだけ扱いが違う。以下で伝わる?

 1 [me@host: ~]$ ls *.egg
 2 dictlib-1.0.3-py2.7.egg
 3 [me@host: ~]$ /usr/local/bin/python2.7 -m pydoc dictlib | head -4
 4 no Python documentation found for 'dictlib'
 5 
 6 [me@host: ~]$ PYTHONPATH=dictlib-1.0.3-py2.7.egg /usr/local/bin/python2.7 -m pydoc dictlib | head -4
 7 Help on package dictlib:
 8 
 9 NAME
10     dictlib - Dictionary Utility Library

つまり egg (zip) で固めちゃったものはフラットに置いてある *.py*.pyc と違う。java の classpath の振る舞いを知ってる人なら jar の類推からの理解そのままでいい。egg (zip) は「ディレクトリ」と同じ扱いがなされる。(なおこのシカケそのものは setuptools 非依存なので、配備先に setuptools がいるかどうかは気にしなくていい。)

というわけで「egg を置くだけ」というわけにもいかなくて、このパッケージ検索パス問題も一緒に考えなければならない、ということなのだけれど、比較的手軽なのは pth ファイルを作って配置して、sitedir をゴニョゴニョすることかな、と思う。pth ファイルは例えばこんな風に作っちゃえばいい:

1 [me@host: ~]$ ls *.egg
2 dictlib-1.0.3-py2.7.egg
3 [me@host: ~]$ echo ./dictlib-1.0.3-py2.7.egg > dictlib-1.0.3.pth

pth ファイルは「置くだけ」では(標準の site-packages などに置かない限りは)ダメで、「pth ファイルたち置き場」をインタプリタに教えてあげる必要がある:

依存したい egg と同じ場所から python 起動したとして
1 >>> import dictlib
2 Traceback (most recent call last):
3   File "<stdin>", line 1, in <module>
4 ImportError: No module named dictlib
5 >>> import site
6 >>> site.addsitedir(".")
7 >>> import dictlib

結果としてはいくつか悩ましいことはある:

  1. pth ファイルを egg 一個につき一つ作るか、一つに代表させてまとめちゃうか問題
  2. そもそも egg をどこに置こうか問題
  3. site.addsitedir(".") を毎度やるのもなぁ問題

1. については一長一短だと思う。特にリモートに配置する場合は、一個一個作る方が「ダウンロードしてきたものに追記してアップロード」としなくていいので簡単な気がする一方で、当然ファイルが多くなってくると管理しにくくもなるわけで。

2.、3. については、「user site」を受け容れるつもりならそこに置けばいい。(python -m site --user-site で場所はわかる。)ただ、特に「レンタルサーバ」での扱いの場合、さらに特に「cgi としてや cron で動くスクリプト」の場合その「「user site」を受け容れる」こと自体が苦痛になりうるので、「カレントに置いて site.addsitedir(".")」が、残念ながら一番簡単なんじゃないかなと思う。


08:30追記:
すまん、ちょっとだけ言葉足らずだった。これ、「インストール場所を変える」で済む場合はそうしてね。そうしたくない理由はこれは…、これこそが「パッケージマネージャ」の宿命。つまり「依存物を勝手にインストールしようと、ネットワークアクセスする」ことから逃げたいから、でしょ? 業務のためのサーバがインターネットから隔離されてるなんてことは滅茶苦茶多いわけで。てことね。(まぁ kivyLauncher も例にしたんで、わかる人はわかるとは思うけれど。)