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>]
この引数が期待通りに動くか確かめるのが今回の主題。
実験
実際に実験をしてみる。
- 環境A,環境Bをつくる
- 環境Aだけにjediをインストールする
- 環境Aだけにpackage x、環境Bだけにpackage yをインストールする
以上のような状況で環境A中のjediを使って上手くコードの補完が動けば良い。
まっさらな環境の作り方
まっさらな環境の作り方は以下で良い。
$ python -m venv <env name>
実際の環境作成
特に理由は無いけれど、どちらも時間に関するライブラリで揃えて、arrow
とpendulum
を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はこちら
Makefile上の個々の操作の実行時間をファイルに出力したかったところからの諸々
Makefile上の操作に対して、実行時間を計測したい。ただしコンソール上に出力するのではなくファイルに出力したい(というところから始まった)。
ファイルを分けたいので呼び出し側でリダイレクトでは無理
例えば以下の様なタスクがある。それぞれの操作の実行時間を個別にファイルに出力したい。
default: # 本来sleep 1の部分は何らかのコマンドの記述になっている time sleep 1 # aaaに出力したい time sleep 1 # bbbに出力したい
タスク内の操作で出力するファイルを分けたいので以下のように呼び出す側でリダイレクトでは無理。これでは全ての出力結果が1つのファイルに出力されてしまう。
# ちなみに make > xxx 2>&1 と同じ意味 $ make &> xxx
ただmakeのタスク内にリダイレクトを書けば良いわけでもない
試しにそれっぽい感じのものを書いてみる。気持ちとしてはaaa,bbbにそれぞれの出力結果が含まれて欲しい
default: clean time sleep 1 &> aaa time sleep 1 &> bbb clean: rm -f aaa bbb
実行結果
$ make rm -f aaa bbb time sleep 1 &> aaa real 0m1.003s user 0m0.003s sys 0m0.000s time sleep 1 &> bbb real 0m1.002s user 0m0.001s sys 0m0.000s
上手く動いてくれない。これは通常の環境ではtime
はコマンドではなくshellの組み込みの操作として実装されているため。
$ which: no time in ( ....) # /usr/bin/time にgnuのtimeコマンドが入っている環境もあるかもしれない
内部でshellが動いてくれれば良さそう
内部でshellが動いてくれれば良さそう。()
で囲む。ただしこれはbashでだけの話かもしれない(他のshellでは試していない)
default: clean (time sleep 1) &> aaa (time sleep 1) &> bbb clean: rm -f aaa bbb
実行結果
$ make rm -f aaa bbb (time sleep 1) &> aaa (time sleep 1) &> bbb $ cat aaa real 0m1.003s user 0m0.002s sys 0m0.000s
上手くいっていそう。
本来の目的は達成されたけれど、もう少しバリエーションを試してみる。
追記したい場合
&>
を &>>
にすれば良い。
default: (time sleep 1) &>> aaa (time sleep 1) &>> bbb clean: rm -f aaa bbb
コンソールにも出力したい場合
teeを使うように書き換えれば、ファイル出力とコンソール出力を兼ねられる。ただし&>
の代わりに|&
を使う。具体的には以下のように。
default: clean (time sleep 1) |& tee aaa (time sleep 1) |& tee bbb clean: rm -f aaa bbb
とはいえ、全部書き換えて回るのはめんどくさい。間に結合する演算子的なものを定義しておくとオプションで切り替えられるようにはなる。
TRACE ?= ifeq ($(TRACE),) outOP := &> else outOP := |& tee endif default: clean (time sleep 1) ${outOP} aaa (time sleep 1) ${outOP} bbb clean: rm -f aaa bbb
ただしちょっとMakefile上でのプログラミングに足を踏み入れてしまっている感じがある。
一応期待通りに動きはする。
$ make -n rm -f aaa bbb (time sleep 1) &> aaa (time sleep 1) &> bbb $ make -n TRACE=1 rm -f aaa bbb (time sleep 1) |& tee aaa (time sleep 1) |& tee bbb
並行して
makeに任せればさっきの2つを並行して呼び出すことも出来はする。
TRACE ?= ifeq ($(TRACE),) outOP := &> else outOP := |& tee endif default: clean $(MAKE) aaa bbb aaa: (time sleep 1) ${outOP} aaa bbb: (time sleep 1) ${outOP} bbb clean: rm -f aaa bbb .PHONY: aaa bbb
(この例だと気にする必要がないことではあるけれど)cleanが終了しきった後にそれぞれのタスクが実行されて欲しいのでdefaultタスクの依存にaaa,bbbを書いていない。
$ time make >/dev/null real 0m2.044s user 0m0.033s sys 0m0.010s $ time make -j >/dev/null real 0m1.031s user 0m0.031s sys 0m0.011s
2つのタスクが同時に動いているので時間は半分になっている。とはいえこのあたりになってくるともう色々頑張り過ぎなような気がする。