jediでenvironmentを指定すると、1つの実行環境(venv)で複数の環境をサポートできる話

たしか以前jediのコードを読んだ時に特に環境(venv)毎にjediを入れ直さなくても済むはずという結論になった記憶。python-language-serverのコードを読んでいたタイミングでそういえば上手く考慮できていないのでは?と思うところがあったのでPRを出したりする前の確認のためにメモしておく。

jedi?

https://jedi.readthedocs.io/en/latest/

Jedi is a static analysis tool for Python that can be used in IDEs/editors. Its historic focus is autocompletion, but does static analysis for now as well. Jedi is fast and is very well tested. It understands Python on a deeper level than all other static analysis frameworks for Python.

例えば以下の様な操作を提供している

  • コードの補完
  • 定義位置を取得

上の引用先で重要なのは静的解析(static analysis)という点。コードを静的解析して諸々を決めるということならどの環境から実行しても問題ないはず(ここでの環境は処理系のバージョン違いを含んでいない。venvのこと)。

つまり実行する環境を意識せず、同じ環境で実行したjediによって、複数の環境のコードに対するコードの補完などの機能が使えるはず。

コードの補完

その前にjediの利用方法の確認。

例えば以下の様なコードはsys.exitの補完のコード。

import jedi

source = '''\
import sys
sys.exi'''

script = jedi.Script(source, 2, len('sys.exi'), 'example.py')
script.completions() # => [<Completion: exit>]

とりあえずScriptというオブジェクトの使いかたが分かれば良い。この例ではsys.exiという部分がsys.exitと補完されている。

environmentという引数

記憶が確かならenvironmentという引数が取れるようになっていたはず。ドキュメントでは説明がTODOになっているけれど。以下の様にして、対象の環境を選択して利用できるようになっている。

import jedi

source = "<source>"

venv_path = "<ここにvenvのpathを指定>"
env = jedi.create_environment(venv_path)

script = jedi.Script(source, 2, len('sys.exi'), 'example.py', environment=env)
script.completions() # => [<Completion: exit>]

この引数が期待通りに動くか確かめるのが今回の主題。

実験

実際に実験をしてみる。

  1. 環境A,環境Bをつくる
  2. 環境Aだけにjediをインストールする
  3. 環境Aだけにpackage x、環境Bだけにpackage yをインストールする

以上のような状況で環境A中のjediを使って上手くコードの補完が動けば良い。

まっさらな環境の作り方

まっさらな環境の作り方は以下で良い。

$ python -m venv <env name>

実際の環境作成

特に理由は無いけれど、どちらも時間に関するライブラリで揃えて、arrowpendulumをx,yとして使うことにする。

setup:
  rm -rf a b
  # 環境の作成
  python -m venv a
  python -m venv b
  # パッケージのインストール
  a/bin/pip install arrow jedi
  b/bin/pip install pendulum

実行後の環境を見てみる。

環境Aの方

$ a/bin/pip freeze
arrow==0.13.0
jedi==0.13.2
parso==0.3.1
python-dateutil==2.7.5
six==1.12.0

環境B

$ b/bin/pip freeze
pendulum==2.0.4
python-dateutil==2.7.5
pytzdata==2018.7
six==1.12.0

インストールされた状況の確認

期待通りにパッケージがインストールされているか確認する。以下のようなMakefileをつくる。

PYTHON ?= python

run:
   @echo "** which python >>" ${PYTHON} "**"
   @echo "** arrow installed" "**"
   @(${PYTHON} -c 'import arrow' &> /dev/null; if [ $$? -eq 0 ]; then echo ok; else echo ng; fi)
   @echo "** pendulum installed" "**"
   @(${PYTHON} -c 'import pendulum' &> /dev/null; if [ $$? -eq 0 ]; then echo ok; else echo ng; fi)

runA:
  $(MAKE) run PYTHON=a/bin/python
runB:
  $(MAKE) run PYTHON=a/bin/python

環境Aでの状況

$ make runA |& grep -v "^make" 
** which python >> a/bin/python **
** arrow installed **
ok
** pendulum installed **
ng

環境Bでの状況

$ make runB |& grep -v "^make" 
** which python >> b/bin/python **
** arrow installed **
ng
** pendulum installed **
ok

問題なさそう。

コードの補完

ようやく本題以下の様なコードを書いて試す。どちらもnowはあるはず。

import sys
import jedi
import textwrap


def run(module, venv_path=None):
    source = textwrap.dedent(f"""
    import {module}
    {module}.no
    """).strip()

    env = None
    if venv_path is not None:
        env = jedi.create_environment(venv_path)
    script = jedi.Script(source, 2, len(module + ".no") - 1, environment=env)
    print(script.completions())


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("python program.py <module name> [<venv-path>]", file=sys.stderr)
        sys.exit(1)
    run(*sys.argv[1:])

またmakefileを書いておく(再現性というやつ)。

PYTHON ?= python
VENVPATH ?=

comp:
   @echo "** which python >>" ${PYTHON} "**"
   @echo "** venv path is >>" ${VENVPATH} "**"
   @echo "** arrow.no -> arrow.now?" "**"
  ${PYTHON} 02usejedi.py arrow ${VENVPATH}
   @echo "** pendulum.no -> pendulum.now?" "**"
  ${PYTHON} 02usejedi.py pendulum ${VENVPATH}

compAforA:
  $(MAKE) comp PYTHON=a/bin/python VENVPATH=a
compAforB:
  $(MAKE) comp PYTHON=a/bin/python VENVPATH=b

どちらも入っている環境では、どちらも補完される。

$ make comp |& grep -v "^make"
** which python >> python **
** venv path is >> **
** arrow.no -> arrow.now? **
python 02usejedi.py arrow 
[<Completion: now>]
** pendulum.no -> pendulum.now? **
python 02usejedi.py pendulum 
[<Completion: naive>, <Completion: now>]

環境Aのpythonで、A用のenvironemtを指定して実行

Aでのみインストールされたarrowに対してだけが補完が有効。

$ make compAForA |& grep -v "^make"
** which python >> a/bin/python **
** venv path is >> a **
** arrow.no -> arrow.now? **
a/bin/python 02usejedi.py arrow a
[<Completion: now>]
** pendulum.no -> pendulum.now? **
a/bin/python 02usejedi.py pendulum a
[]

環境Aのpythonで、B用のenvironemtを指定して実行

Bでのみインストールされたpendulumに対してだけが補完が有効(jediがインストールされているのも環境Aだけ)。

make compAforB |& grep -v "^make"
** which python >> a/bin/python **
** venv path is >> b **
** arrow.no -> arrow.now? **
a/bin/python 02usejedi.py arrow b
[]
** pendulum.no -> pendulum.now? **
a/bin/python 02usejedi.py pendulum b
[<Completion: naive>, <Completion: now>]

まとめ

まとめると以下が分かれば良い。

  • jediの環境は1つで良い
  • environmentを指定してScriptを作成すれば別の環境を対象にできる
  • environment作成時のpathはpython -m venvで指定したディレクト

試したgistはこちら