Λlisue's blog

つれづれなるままに更新されないブログ

え?君せっかく Python のバージョン管理に pyenv 使ってるのに Vim の補完はシステムライブラリ参照してるの?

f:id:lambdalisue:20140521064818p:plain

どうも、ご無沙汰してます有末です。 Pythonistaならpyenvだよねーってことで当初からバリバリ使わせていただいているのですが、最近djangoのプロジェクトを書く際に困ったのでまとめておきます。 具体的にはpyenvでPytho 3をインストールし、pyenv-virtualenvを用いて仮想環境を構築し、その仮想環境に django をインストールしただけでは jedi-vim の補完が効かないという問題です。 いくつかの要因が複合して複雑に成っていたので、ひとつずつメモしていきます。

忙しい人のための簡易書

普段からpyenvpyenv-virtualenvを使用していてjedi-vimdjangoの補完が効かなくて困っている。 とにかく補完を効かせたい。 戯言なんてどうでもいいという人は下記を~/.vimrcに記載してください。

" ~/.pyenv/shimsを$PATHに追加
" jedi-vim や vim-pyenc のロードよりも先に行う必要がある、はず。
let $PATH = "~/.pyenv/shims:".$PATH

" ... neobundle.vim 初期化等

" DJANGO_SETTINGS_MODULE を自動設定
NeoBundleLazy "lambdalisue/vim-django-support", {
      \ "autoload": {
      \   "filetypes": ["python", "python3", "djangohtml"]
      \ }}

" 補完用に jedi-vim を追加
NeoBundle "davidhalter/jedi-vim"

" pyenv 処理用に vim-pyenv を追加
" Note: depends が指定されているため jedi-vim より後にロードされる(ことを期待)
NeoBundleLazy "lambdalisue/vim-pyenv", {
      \ "depends": ['davidhalter/jedi-vim'],
      \ "autoload": {
      \   "filetypes": ["python", "python3", "djangohtml"]
      \ }}

" ... neobundle.vim 終了処理等

これにより現在使用している(pyenv方式)の virtualenv を自動的にVim上で適用します。 手動による virtualenv の適用・解除はそれぞれ :PyenvActivate:PyenvDeactivate で行えます。 また DJANGO_SETTINGS_MODULE が自動的に設定されるようになったので、jedi-vimによるdjangoの補完が効きます。

なおneobundle.vimを使っていることを前提で書いていますが、以下の二点に気をつけていれば他の方法でも構いません。

  1. jedi-vimvim-pyenvが読み込まれる前に ~/.pyenv/shims(もしくはそれに準ずるディレクトリ)が $PATH に加わっている
  2. vim-pyenvjedi-vimよりも後にロードされる

普通のvirtualenvを使用している場合はVimを最強のPython開発環境にする2に習ってvim-pyenvの代わりにvim-virtualenvを使用してください。

Python2もしくはPython3のみを使用する場合

pyenvpyenv-virtualenvは使っているがPython2かPython3しか使わないという方向けの方法です。 ただ、何度も同じ事を説明するのも面倒なので、Python2とPython3どちらも使用する人も同じ設定を施してください。

Vimでインストールしたライブラリなど、一部の補完が効かない場合というのは通常 sys.path の設定が適切でないために発生します。 Vimは内部で独自にPythonの実行環境を持っているため、Terminal上でいくら pyenv virtualenv Hello; pyenv local Hello などとしても無駄足となります。 そこでVimの内部Pythonsys.path を適切に書き換えてやるために手前味噌ですが vim-pyenv というプラグインを利用します。

vim-pyenvpyenv local Hello などのコマンドにより適用された変更を自動的に読み取り、Vim内部のPythonに適用します。 これにより Hello にインストールされたライブラリも検索パスに含まれるようになるため jedi-vim での補完が可能となります。

また django のケースですが、補完が効かない最も大きな理由は DJANGO_SETTINGS_MODULE が適切に設定されていないことです。 django の一部のモジュール(例 django.db)は読み込む際に DJANGO_SETTINGS_MODULE の値を使用して自身の設定コードを呼び出します。 したがって DJANGO_SETTINGS_MODULE が設定されていないか不適切な場合は jedi-vim のような「モジュールをロードして補完リストを組み上げるタイプ」のプラグインでは、適切に補完リストが組めず補完ができなくなります。 適切に DJANGO_SETTINGS_MODULE を設定するのは骨が折れるので、これも手前味噌ですが vim-django-support というプラグインを利用します。 ご想像通り、このプラグインは状況により適切に DJANGO_SETTINGS_MODULE を設定するためのものです。

最後に pyenv を使用する際の注意です。 pyenv を使用した場合Pythonの実行パスは ~/.pyenv/shims/python となり、これは pyenv init により PATH が適切に設定されるために実現します。 しかし Vim は独自に PATH を持っているため、このままでは pyenvPythonを参照することができません。 したがって下記のように PATH を書き換えてやる必要があります。

let PATH = expand("~/.pyenv/shims") . ":" . $PATH

ただし、このままだと ~/.vimrc のリロードなどにより PATH が凄いことになってしまうので下記のような関数を作って登録するようにしましょう。

" PATHの自動更新関数
" | 指定された path が $PATH に存在せず、ディレクトリとして存在している場合
" | のみ $PATH に加える
function! IncludePath(path)
  " define delimiter depends on platform
  if has('win16') || has('win32') || has('win64')
    let delimiter = ";"
  else
    let delimiter = ":"
  endif
  let pathlist = split($PATH, delimiter)
  if isdirectory(a:path) && index(pathlist, a:path) == -1
    let $PATH=a:path.delimiter.$PATH
  endif
endfunction

" ~/.pyenv/shims を $PATH に追加する
" これを行わないとpythonが正しく検索されない
IncludePath(expand("~/.pyenv/shims"))

この設定はPythonを扱うプラグインが読み込まれる前に行う必要があります。 したがって ~/.vimrc の先頭あたりに書いておくとよいでしょう。

上記を踏まえた ~/.vimrc は下記のようになります。なお neobundle.vim を使用することを前提としていますが他のパッケージマネージャを用いても構いません。

" ... 省略

" PATHの自動更新関数
" | 指定された path が $PATH に存在せず、ディレクトリとして存在している場合
" | のみ $PATH に加える
function! IncludePath(path)
  " define delimiter depends on platform
  if has('win16') || has('win32') || has('win64')
    let delimiter = ";"
  else
    let delimiter = ":"
  endif
  let pathlist = split($PATH, delimiter)
  if isdirectory(a:path) && index(pathlist, a:path) == -1
    let $PATH=a:path.delimiter.$PATH
  endif
endfunction

" ~/.pyenv/shims を $PATH に追加する
" これを行わないとpythonが正しく検索されない
IncludePath(expand("~/.pyenv/shims"))

" ... neobundle.vim 初期化等

" DJANGO_SETTINGS_MODULE を自動設定
NeoBundleLazy "lambdalisue/vim-django-support", {
      \ "autoload": {
      \   "filetypes": ["python", "python3", "djangohtml"]
      \ }}

" 補完用に jedi-vim を追加
NeoBundleLazy "davidhalter/jedi-vim", {
      \ "autoload": {
      \   "filetypes": ["python", "python3", "djangohtml"]
      \ }}

" pyenv 処理用に vim-pyenv を追加
" Note: depends が指定されているため jedi-vim より後にロードされる
NeoBundleLazy "lambdalisue/vim-pyenv", {
      \ "depends": ['davidhalter/jedi-vim'],
      \ "autoload": {
      \   "filetypes": ["python", "python3", "djangohtml"]
      \ }}

" ... neobundle.vim 終了処理等

f:id:lambdalisue:20140521064941p:plain

vim-pyenv はデフォルトで現在選択されている virtualenv を適用します。 virtualenv の切り替えは :PyenvActivate:PyenvDeactivate で行えます。 このあたりの詳しい説明は :help vim-pyenv をご覧ください。

Python2とPython3どちらも使用する場合

何度も同じ事を説明するのも面倒なので、前章を読んでいないかたは、まず前章と同じ設定を施してください。

pyenv を使う一番のメリットはPythonのバージョンを簡単に切り替えられることだと思います。 このメリットを最大限にVimで活かすためには、Vim+python/+python3コンパイルされている必要があります。 この確認は vim --version を使って以下のように行えます。

$ vim --version
VIM - Vi IMproved 7.4 (2013 Aug 10, compiled May 20 2014 18:20:07)
適用済パッチ: 1-295
Compiled by alisue@alisue-labdesk
通常 版 with GTK2-GNOME GUI.  機能の一覧 有効(+)/無効(-)
...略...
+cryptv          +linebreak       +python/dyn      +viminfo
+cscope          +lispindent      +python3/dyn     +vreplace
...略...

実行結果の中に +python/dyn+python3/dyn という文字がありますが、これが +python/+python3 の状態です。 どちらかの頭に - がついている場合は、残念ながらPython2かPython3のどちらかが正しく機能しません。 また確認が面倒くさいので行なっていませんが /dyn という文字がついていない場合はどちらかが使えないかもしれません(要出典)。 どちらも使えるかどうか確認するには、Vimを起動して下記二つのコマンドで正しい値が帰ってくることを確認してください。

:python print(sys.version)
:python3 print(sys.version)

Macユーザーの方はMac-Vim-Kaoriyaのバイナリが+python/+python3なようなのでそれを利用すれば良いようです(未確認)。 Linuxユーザーの方で +python/+python3 では無かった方は下記方法にしたがって pyenv でインストールした Python2, Python3 を参照するVimコンパイルしなおしてください。

Vimを+python/+python3でコンパイル

通常VimはPythoと静的リンクしますが、Python 2,3 を共存させたい場合は動的リンクにする必要があります。 詳しいことは割愛しますが、動的リンクを可能にするために--enable-sharedオプションを指定してPythonコンパイルるする必要があります。 pyenv でこのオプションを指定して Python2, 3 をインストールします。 下記に従ってください。 なお、正しく --enable-shared でインストールされたかどうかは libpython2.7.so.1.0libpython3.4m.so.1.0$HOME/.pyenv/versions/XXXX/lib/ の中に生成されているかどうかで確かめることができます。

$ CONFIGURE_OPTS="--enable-shared" pyenv install 2.7.6
$ CONFIGURE_OPTS="--enable-shared" pyenv install 3.4.0

次にこの二つの Python をリンクさせたVimコンパイルします。 まずは正しく Python 2, 3 を探せるように下記のように同時に二つのバージョンを指定します

$ pyenv local --unset
$ pyenv shell --unset
$ pyenv global 2.7.6 3.4.0
$ pyenv versions
   system
 * 2.7.6 (set by /home/XXXX/.pyenv/version)
 * 3.4.0 (set by /home/XXXX/.pyenv/version)
   XXXXX
$ python --version
Python 2.7.6
$ python3 --version
Python 3.4.0

ここまでで利用する Python のインストールが終わったのでコンパイルする Vimソースコードを Marcurial でレポジトリから落としてきます。 下記に従ってください

$ hg clone https://code.google.com/p/vim/
$ cd vim

ここで適切なオプションを付加して ./configure {options} すれば Vimコンパイルが行われますが、動的リンクであるため出来上がったバイナリファイルが pyenv でインストールした Python を見つけることができません。 LD_LIBRARY_PATH などでリンク先を指定しても良いのですがバイナリ固有のライブラリ検索パスとして -rpath オプションがあるので、これを用いて下記のようにライブラリ検索パスを指定します。 なお下記したオプションはすべて ./configure --help に解説が書いてあるので必要に応じて追加・削除してください。

$ LDFLAGS="-Wl,-rpath=${HOME}/.pyenv/versions/2.7.6/lib:${HOME}/.pyenv/versions/3.4.0/lib" ./configure \
    --enable-fail-if-missing \
    --enable-luainterp \
    --enable-perlinterp \
    --enable-pythoninterp=dynamic \
    --enable-python3interp=dynamic \
    --enable-tclinterp \
    --enable-rubyinterp=yes \
    --enable-multibyte \
    --enable-fontset \
    --enable-gui=gnome2 \
    --with-features=huge \
    --with-luajit
$ make

これで src/vim にバイナリファイルが作成されるので srv/vim --version を実行して +python/+python3 なバイナリが出来上がったかを確認してください。 また -rpath が適切に設定されたかどうかは readelf -d srv/vim で表示される Library rpath: にて確認することができます。

最後に念の為Vimを起動し、下記コマンドで Python 2, 3 双方が実行できることを確認して sudo make install してください。

:python print(sys.version)
:python3 print(sys.version)

参考

+python/+python3 でなければ行けない理由

とりあえず何故この様な面倒なことをする必要が合ったかについて簡単に説明します。 ご存知の通り Python には 2x 系列と 3x 系列が存在します。 両者は構文レベルでの互換性を捨てているため、Python2のコードをPython3で読む、またその逆も出来ません。 さて、ここで pyenv を使用すると Python2 と Python3 を簡単に切り替えることができます。 さらに言えば pyenv-virtualenv を使用するとベースが Python2 や Python3 の様々な仮想環境を構築することができます。 そのため、補完を効かせたいがために無理やり sys.pathPYTHONPATH を書き換えると Python3 で Python2 のライブラリを読み込むミスが発生し、嵐の如きエラーに見舞われることになります。 したがって、補完プラグインは読み込むライブラリがどちらに向けたライブラリなのかを把握し、適切に使用するPythonを選択する必要があります。 もうお分かりかとは思いますが、この切り替えを行うためには Vim+python/+python3コンパイルされている必要があったので、面倒ですが上記のような手順をふみました。

ちなみに、この Python2/3 の切り替えに対応している補完プラグインは、僕が知る限りでは jedi-vim のみです。 ただし、このjedi-vimはユーザーが事前に設定したPython2/3を使用して補完を試みるだけなので、仮想環境を切り替えるだけでは先の問題が解決できません。 したがって vim-pyenv では選択された仮想環境に応じて適切に自身と jedi-vim が使用するPythonのバージョンを切り替える設計になっています。

vim-pyenv を使用して動的に仮想環境と使用Pythonを切り替える

vim-pyenv+python/+python3Vim にインストールされると自動で Python のバージョン切り替え機能が有効化されます。 すべての処理がバックグラウンドで行われるので通常は意識する必要がありませんが、今回は確認も兼ねて一つづつステップを踏んでいきます。 まずは Python 2/3 それぞれをベースとした仮想環境 django2/3 を作成し、それぞれにdjangoをインストールします。

$ pyenv virtualenv 2.7.6 django2
$ pyenv virtualenv 3.4.0 django3
$ pyenv shell django2
$ pip install django
$ pyenv shell django3
$ pip install django
$ pyenv rehash

ここで Vim を起動すると vim-pyenv, jed-vim が使用する Python のバージョンは下記のように外部の Python のバージョンにあわせて初期化されます

Name 起動直後 jedi-vim 初期化 vim-pyenv 初期化 pyenv#activate()
Terminal 3.4.0 3.4.0 3.4.0 3.4.0
jedi-vim 2.7.6 2.7.6 3.4.0
vim-pyenv 2.7.6 3.4.0

したがって、仮に vim-pyenv が jedi-vim より先に読み込まれてしまうと、下記のようになってしまい正しく初期化が行えないことになります。

Name 起動直後 vim-pyenv 初期化 pyenv#activate() jedi-vim 初期化
Terminal 3.4.0 3.4.0 3.4.0 3.4.0
vim-pyenv 2.7.6 3.4.0 3.4.0
jedi-vim 3.4.0 2.7.6

実際に正しいバージョンの Python が使用されているかどうかは下記のコマンドで調べることができます。

:Python print("jedi-vim", sys.path)
:PyenvPython print("vim-pyenv", sys.path)

まぁ正しく設定されていたと仮定します。 次に仮想環境を django2(Python 2.7.4 ベース)に切り替えてみましょう。 下記コマンドを実行してください。

:PyenvActivate django2

これで自動的に jedi-vim, vim-pyenv が Python 2 を参照するようになります。 またライブラリの参照先も Python 3 向けのもの(~/.pyenv/versions/django3/lib/python3.4/site-packages) から Python 2 向けのもの(~/.pyenv/versions/django2/lib/python2.7/site-packages)に変更されます。

lightline に現在の仮想環境とPythonのバージョンを表示する

f:id:lambdalisue:20140521065307p:plain

vim-pyenvスクリーンショットのようなインディケータを提供しています。 文字列を返す関数なので lightline.vim だけでなくVim標準のstatuslineなどでも利用できます。 詳しく知りたい方は :help pyenv#statusline#component() としてください。 とりあえず僕の lightline.vim の設定を下記します。

let g:lightline = {
      \ 'colorscheme': 'hybrid',
      \ 'active': {
      \   'left': [ 
      \       [ 'mode', 'paste' ], 
      \       [ 'pyenv' ],  " <= ここ
      \       [ 'fugitive', 'filename' ] 
      \   ],
      \   'right': [
      \       [ 'syntastic', 'lineinfo' ],
      \       [ 'percent' ],
      \       [ 'fileformat', 'fileencoding', 'filetype' ]
      \   ]
      \ },
      \ 'component_expand': {
      \   'syntastic': 'SyntasticStatuslineFlag',
      \ },
      \ 'component_type': {
      \   'syntastic': 'error',
      \ },
      \ 'component_function': {
      \   'fugitive': 'MyFugitive',
      \   'filename': 'MyFilename',
      \   'fileformat': 'MyFileformat',
      \   'filetype': 'MyFiletype',
      \   'fileencoding': 'MyFileencoding',
      \   'mode': 'MyMode',
      \   'pyenv': 'pyenv#statusline#component', " <= ここ
      \ },
      \ 'separator': { 'left': '⮀', 'right': '⮂' },
      \ 'subseparator': { 'left': '⮁', 'right': '⮃' }
      \ }

やっぱり美しくないとね!