PYTHONPATHを2/3で使い分けたい

Posted by rhoboro on 2017-07-16

最近は開発手順やデプロイ手順の自動化、ドキュメント化にも積極的に取り組んでいます。
Dockerとmakeをよく利用しているのですが、社内メイン言語はPython。
そこでmakeの代わりにFabricやAnsibleを使うのもいいかなと思ったところAnsibleの実行でこけました。 原因がPYTHONPATHだったので、2系と3系でPYTHONPATHを使い分けたいなと思った話です。

Ansible実行時のエラー内容と原因

まずはインストール。 Macの場合はpipでインストールできます。(Latest Releases on Mac OSX)

[alpaca]~/github/fab % python3 -m venv venv
[alpaca]~/github/fab % . venv/bin/activate
(venv) [alpaca]~/github/fab % pip install ansible

すんなり入りました。 ここでドキュメントにあるコマンドを実行したところエラーに。

(venv) [alpaca]~/github/fab % ansible localhost -m ping
ERROR! Unexpected Exception: No module named 'error'
the full traceback was:

Traceback (most recent call last):
  File "/Users/rhoboro/github/fab/venv/bin/ansible", line 88, in <module>
    mycli = getattr(__import__("ansible.cli.%s" % sub, fromlist=[myclass]), myclass)
  File "/Users/rhoboro/github/fab/venv/lib/python3.6/site-packages/ansible/cli/__init__.py", line 28, in <module>
    import yaml
  File "/Users/rhoboro/google-cloud-sdk/platform/google_appengine/lib/yaml/lib/yaml/__init__.py", line 2, in <module>
    from error import *
ModuleNotFoundError: No module named 'error'

エラーメッセージをよく見ると、ここでは全く関係なさそうなgoogle_appengineという文字列。 ということで$PYTHONPATHを確認。

(venv) [alpaca]~/github/fab % echo $PYTHONPATH
/Users/rhoboro/google-cloud-sdk/platform/google_appengine/lib/yaml/lib:/Users/rhoboro/google-cloud-sdk/platform/google_appengine
(venv) [alpaca]~/github/fab % cat ~/.zshrc | grep "PYTHONPATH"
export PYTHONPATH=$PYTHONPATH:${CLOUDSDK_ROOT}/platform/google_appengine/lib/yaml/lib:$GAE_PYTHON_ROOT

はい。やっぱりPYTHONPATHが原因っぽい。
ということで$PYTHONPATHを消してあげると無事動きました。

(venv) [alpaca]~/github/fab % env PYTHONPATH="" ansible localhost -m ping
 [WARNING]: Host file not found: /etc/ansible/hosts

 [WARNING]: provided hosts list is empty, only localhost is available

localhost | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

めでたしめでたし。

PYTHONPATHを2/3で分けたい

これ、根本の原因はおそらくPYTHONPATHで読み込んでるgoogle-cloud-sdkのpythonが2系、ansibleようにvenvで作ったpythonの環境は3系というところなんですよね。
PYTHONPATHをPython2とPython3でいい感じに分ける方法はないんでしょうか。
ただの環境変数だしやるとしたらこんな流れかな。

  1. PYTHONPATH2, PYTHONPATH3をそれぞれ定義しておく
  2. シェルコマンド実行をフック
  3. そのときのPythonのバージョンを判定
  4. PYTHONPATHを置き換える

ちょっと試しにそれっぽいのを書いてみた(.zshrc)。

export PYTHONPATH2="/python2"
export PYTHONPATH3="/python3"

function preexec_func() {
    a=`readlink \`which python\``
    if [ $(echo $a | grep -e 'python3') ] ; then
        export PYTHONPATH=$PYTHONPATH3 > /dev/null
    else
        export PYTHONPATH=$PYTHONPATH2 > /dev/null
    fi
}
autoload -Uz add-zsh-hook
add-zsh-hook preexec preexec_func

環境作って動作確認。

# python2の環境で確認
[alpaca]~/github/pythonpath % virtualenv venv2
[alpaca]~/github/pythonpath % . venv2/bin/activate
(venv2) [alpaca]~/github/pythonpath % python -c "import sys; print sys.path"
['', '/python2', '/Users/rhoboro/github/pythonpath/venv2/lib/python27.zip', '/Users/rhoboro/github/pythonpath/venv2/lib/python2.7',...]
(venv2) [alpaca]~/github/pythonpath % deactivate 

# python3の環境で確認
[alpaca]~/github/pythonpath % python3 -m venv venv3
[alpaca]~/github/pythonpath % . venv3/bin/activate
(venv3) [alpaca]~/github/pythonpath % python -c "import sys; print(sys.path)"
['', '/python3', '/usr/local/Cellar/python3/3.6.1/Frameworks/Python.framework/Versions/3.6/lib/python36.zip',...]
(venv3) [alpaca]~/github/pythonpath % deactivate

パッとみた感じは動いてそうです。
ただ、exportを使うとコマンド実行後に元に戻しておかないと気持ち悪いですね。
exportの代わりにenvで楽できないかなと思ったけどコマンド実行時には消えてるから使えませんでした。

判定適当でこんな動きしてるので実用的ではないかなー。

[alpaca]~/github/pythonpath % python3
Python 3.6.1 (default, May 13 2017, 14:08:52) 
[GCC 4.2.1 Compatible Apple LLVM 8.1.0 (clang-802.0.42)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.path
['', '/python2', '/usr/local/Cellar/python3/3.6.1/Frameworks/Python.framework/Versions/3.6/lib/python36.zip', '/usr/local/Cellar/python3/3.6.1/Frameworks/Python.framework/Versions/3.6/lib/python3.6', '/usr/local/Cellar/python3/3.6.1/Frameworks/Python.framework/Versions/3.6/lib/python3.6/lib-dynload', '/usr/local/lib/python3.6/site-packages']

ここまでやりましたが、そもそも論としてグローバルなPYTHONPATHを編集しないで必要なところでのみdirenvで設定するほうがいい気がしますね。
もし何かいい方法知ってる方はTwitterとかで教えてくれると嬉しいです。