GAE/pyの環境で開発していても、他のプロジェクトのdatastoreにアクセスするなど、
GoogleのRPC系のAPIを使用するために、
google-cloud-pythonのライブラリを使いたいケースがあるかと思います。
その際にパス関連で注意すべき点があったのでメモします。
google-cloud-python
このライブラリはGCEやローカルからGCPのAPIを叩くためのものですが、
Cを使っていないため?、GAE/pyでも動きます。
以前はgcloud-pythonという名前でしたが、2016/9ごろにgcloud->google.cloudにnamespaceがリネームされました。
gcloud-python
(このリネームのせいでこの記事を書いています)
使い方
基本的な使い方はreadthedocsを見ていただければわかるかと思いますが、
google-cloudまたは使用するモジュールをpypiから探してpipでインストールします。
$ pip install google-cloud-datastore
あとはgoogle.cloud以下にモジュールが展開されるので、そのまま使用できます。
from google.cloud import datastore
client = datastore.Client()
product_key = client.key('Product', 123)
print(client.get(product_key))
appengineのモジュールとの競合
ここまで読んでいただければお気づきかと思いますが、
GAE/pyではappengineモジュールをgoogle.appengine以下からimportして使います。
そのため、google-cloudをインストールすると、
ローカルのvirtualenv環境でgoogle以下のモジュールが、
GAE SDKからgoogle-cloudの方に上書きされてしまいます。
この時、virtualenv上でpythonを実行すると、
REPLにせよunittest等のファイル実行にせよ、
>>> import google.appengine
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: No module named appengine
のようなエラーが発生します。
回避方法
結論
virtualenvのlib/python2.7/site-packages/_virtualenv_path_extensions.pth
に
google_appengineのライブラリのパスを追加するついでにmodule追加ロジックを追記しました。
もちろん、独自の.pthファイルに追記しても同じでしょう。
# ... 以下はpython sdkの場所が/usr/local/google_appengineのケース
/usr/local/google_appengine
import sys, types, os;has_mfs = sys.version_info > (3, 5);p = os.path.join('/usr/local/google_appengine', *('google',));importlib = has_mfs and __import__('importlib.util');has_mfs and __import__('importlib.machinery');m = has_mfs and sys.modules.setdefault('google', importlib.util.module_from_spec(importlib.machinery.PathFinder.find_spec('google', [os.path.dirname(p)])));m = m or not has_mfs and sys.modules.setdefault('google', types.ModuleType('google'));mp = (m or []) and m.__dict__.setdefault('__path__',[]);(p not in mp) and mp.append(p)
# ...
内容
ワンラインで読みづらいので改行を入れて中身を見ます。
googleの他のライブラリの記載を持ってきたので3.5対応の記載もありますが、
GAE/pyは2.7系なので削れると思います。
import sys, types, os
has_mfs = sys.version_info > (3, 5)
# google_appengineの中のgoogleモジュールのパスを作成
p = os.path.join('/usr/local/google_appengine', *('google',))
# Moduleオブジェクトを取り出す
# 3.5のケース
importlib = has_mfs and __import__('importlib.util')
has_mfs and __import__('importlib.machinery')
m = has_mfs and sys.modules.setdefault('google', importlib.util.module_from_spec(importlib.machinery.PathFinder.find_spec('google', [os.path.dirname(p)])))
# 2.7のケース
m = m or not has_mfs and sys.modules.setdefault('google', types.ModuleType('google'))
mp = (m or []) and m.__dict__.setdefault('__path__',[])
# pathをモジュールに追加
(p not in mp) and mp.append(p)
調査の方法
調査というか参照ですが。。
pythonでなくても同じnamespaceに複数のモジュールを追加していくのは
あまり歓迎されないんじゃないかと思います。
なので、結構解決が難しそうだなぁ、と思っていました。
しかし、よくgoogle-cloudライブラリを見てみると、
複数のpypi用のパッケージがインストールされています。
そしてそれらはgoogleやgoogle.cloudモジュール以下に追加されています。
同じ方法をとればパスの修正ができそうだと思い、.pthファイルを探して見たら答えがありました。
.pthファイルの流儀についてはあまり理解していませんが、
ちなみに
本番では
GAE/pyに上記対策を加えることはできなさそうですが、
デプロイしたところ、きちんとgoogleモジュールにどちらのmoduleも追加されていました。
さすがですね。
(google-cloudライブラリのようにappengine sdkもpypiで配布してほしいですね)
nose
また、python -m unittest discover
などではエラーが発生していましたが、
nosetests
を実行すると、特に問題なくテストが完了してしまいます。
これはnoseライブラリがunittestをwrapしてテスト実行前に
PYTHONPATHを修正しているからのようでした。
CircleCIではtest runnerがデフォルトではnoseになっていて、
テストが無事通過したためパスの競合が解決されていなかったことに気づくのが遅れてしまいました。
ローカルでもnosetestsを行なっているぶんには問題はないかと思いますが、
REPL等でのデバッグ時には困るので、解消した方がいいかと思います。
参考
PYTHONPATHや.pthファイルについての理解が浅かったので色々と調べました。。
- PYTHONPATHとは
- Pythonでデフォルトパスを追加する方法
- Pythonライブラリパスをコントロールする
- 210:Pythonのライブラリローダを制御する
- Python の module search path ついて調べてみる