rcmdnk's blog

20241128_miseactive_200_200

Pythonのプロジェクト管理ツールをuvにした際に、 uvにはpoetryのようにpoetry shellのような仮想環境を自動認識する機能がないため、ちょっと調べた結果、 miseというツールを試しています。

Pythonプロジェクト下での仮想環境下でのコマンド実行の必要性

Pythonのプロジェクトを作る際、自分のやり方として pre-commitを使って コードをチェックするようにしています。

pre-commit用のツールはpre-commit自体に管理してもらう事もできますが、 フォーマッターとかをコマンドラインから直接使ったりもしたいので それらのツールはプロジェクトのdevelopment用の依存関係に入れておいてインストールするようにしています。

そのようなツールも毎回書くのが大変なので 以下のようなパッケージを作ってまとめてインストールできるようにもしています。

で、これらのツールは仮想環境の中にインストールされるわけで、 直接実行するには仮想環境内に居る必要があります。

これはエディタなど開いた際なども仮想環境下でそのプロジェクトに必要なツールにパスが通っていることでそれを使えるので pre-commitに直接管理してもらうよりも便利です。

というわけで仮想環境下に簡単に入れるようにしたい、という話。

uvの仮想環境

uvは通常pyproject.tomlファイルのあるディレクトリに.venvディレクトリを作成し、その中に仮想環境を作成します。

仮想環境を有効にするには、.venv/bin/activateを実行します。 出るにはdeactivateを実行します。 ただしこの場合はプロジェクトのルートディレクトリに居るか、そこをきちんと指定する必要があります。

また、プロジェクトのディレクトリ以下にいる場合はuv run <command>で仮想環境下としてコマンドを実行できます。

poetryの場合はデフォルトではキャッシュディレクトリに仮想環境を作成しますが、 同様にpoetry run で仮想環境下としてコマンドを実行できます。

一方、activateを実行するにはキャッシュディレクトリ下の仮想環境ディレクトリを参照する必要がありますが、 poetryでは直接そこを参照する方法はなく (poetry env infoから取得することは出来ますが)、 通常はpoetry shellというコマンドで仮想環境を有効にします。

ただし、この場合は仮想環境が有効なシェルを新たに立ち上げている形です。なので環境を出るときにはシェルを出るとき同様exitなどで出ます。

このpoetry shellに相当する機能がuvにはありません。

これは今のところ意図的な仕様で、Issueにはなってますが、 導入は見送られそうな雰囲気です。

Add a command to activate the virtual environment, e.g., uv shell · Issue #1910 · astral-sh/uv

一応、uv run $SHELLとすれば現在のシェルと同じシェルを仮想環境下で立ち上げることになるのでpoetry shellと同じような事ができます。

poetry shellが実際どの程度のことをしているかはわからないですが、実用上は同じ状態だと思って良いはず。

上のIssueでは新たなプロセスを立ち上げることに懸念点があり、その際にどの程度今の環境を引き継ぐべきか、シェルやOSなど毎に何を考慮すればよいのか、など考えることが沢山あって難しいという感じです。

確かにそうだな、と思うのと、逆に実用上はuv run $SHELLで事足りるので、無理に入れる必要はなさそう。

必要ならこれのエイリアスとか作って簡単に実行できるようにしておけばよいかと。

mise

uv run $SHELL、もしくはactivateすることでも良いのですが、 せっかくなのでこれを機に仮想環境を自動認識するツールを使ってみることにしました。

このような仮想環境を自動認識するツールとしては direnv が有名で古くから良く使われているものだと思います。

direnvは以前ちょっと使ったことがあったのでまた使い始めようかと思って調べてたところ、 mise という同じ様なことが出来る新しいツールがあるとのことで試しています。

流行りのRust製で高速。

なんとなく、mice的な感じでマイスと読むのかな、と思っていたら、 フランス語で料理などの準備をする、的な意味で ミーズと読むようです。

いろいろな機能があって、pyenvのように必要なバージョンのPythonをインストールしたり、poetryのように仮想環境を管理したり、direnvのようにプロジェクトのディレクトリ下に入った際に自動で環境を切り替えたり、といったことが出来ます。

取り敢えず現状は環境の切り替えだけのために使っています。

miseのインストール

Homebrewがあれば

1
brew install mise

で。

直接インストールスクリプトでいれる場合は

1
curl https://mise.run | sh

など、いろいろな方法でのインストール方法が用意されています。

Installing Mise

インストールしたらシェルのセットアップファイルに必要な設定を追加します。

Bashなら

1
echo 'eval "$(mise activate bash)"' >> ~/.bashrc

中身を見てみるとわかりますが、PROMPT_COMMAND_mise_hook関数を追加しています。

この関数の中では、

1
eval "$(mise hook-env -s bash)"

のようなコマンドを実行しています。 これは基本的にexport PATH=...といったようにPATHを設定します。

これによって、cdとかでディレクトリを変更した際、そこがプロジェクトのディレクトリ下であるかどうかを判定し、 ディレクトリ下ならそのプロジェクトの仮想環境のPATHを設定し、 逆にそこから出た際には元のPATHに戻す、といったことを行っています。

uvのプロジェクトだけなら<project>/.venv/binをPATHに追加したり外したりするだけなので その分だけ自作しても良いかな、とも思いましたが、 uvのプロジェクトのディレクトリ下かどうか、と判定するのは割と面倒で やっぱりmiseを使おう、と 1

Pythonプロジェクト下でのmiseの使い方

プロジェクトのルートディレクトリに.mise.tomlファイルを作成します。

.mise.toml
1
2
[env]
_.python.venv = ".venv"

これでそのプロジェクト下に入った際、

1
2
3
4
mise ERROR error parsing config file: <path/to/project>/.mise.toml
mise ERROR Config file <path/to/project>/.mise.toml is not trusted.
Trust it with `mise trust`.
mise ERROR Run with --verbose or MISE_VERBOSE=1 for more information

と言われるのでmise trustを実行します。

そうするとそれ以降、そのプロジェクト下に入った際に自動的に仮想環境が有効になります。 また、そのプロジェクトから出た際には元の環境に戻ります。

プロンプトへの表示

. .venv/bin/activateを実行した際にはシェルのプロンプトに仮想環境の名前が表示されるようになります。

poetry shellの場合も同じようにプロンプトに仮想環境の名前が表示されます。

miseを使った場合にはプロンプトは変更されません。

direnvの場合も直接プロンプトは変更されませんが、 プロジェクト下に入った際、仮想環境が有効化されると VIRTUAL_ENV以外にVIRTUAL_ENV_PROMPTという環境変数が設定されるので、それを使ってプロンプトを変更することが出来ます。

PS1 · direnv/direnv Wiki

miseの場合はVIRTUAL_ENVは設定されるのですが、VIRTUAL_ENV_PROMPTは設定されません。

なのでちょっと手をいれる必要があります。

Python environments activating, but zsh prompt not updated · Issue #2027 · jdx/mise

一旦Pythonのvenv限定ですが、 以下の様な関数でプロンプトに仮想環境の名前を表示することが出来ます。

1
2
3
4
5
6
7
8
9
10
11
12
_venv_prompt () {
  if [ -z "$VIRTUAL_ENV_PROMPT" ] && [ -n "$VIRTUAL_ENV" ];then
    local prompt
    prompt=$(grep "^prompt *=" "$VIRTUAL_ENV/pyvenv.cfg" 2>/dev/null|cut -d "=" -f 2|sed 's/^ *//')
    if [ -z "$prompt" ];then
      prompt="$(basename "$VIRTUAL_ENV")"
    fi
    printf '(%s) ' "$prompt"
  fi
}

PS1='$(_venv_prompt)'$PS1

uvで作る場合、通常.venv/pyvenv.cfgprompt = ...という行が追加されそこにプロジェクト名があるのでそれを使っています。

また、ちょっと手動で仮想環境を作ってactivateしたとか、直接activateした場合、プロンプトが設定されるので重複してしまいますが、 その場合にはVIRTUAL_ENV_PROMPTが設定されているので、その場合は上の関よる追加は無いようにしています。

これで、my_projectというプロジェクトのディレクトリ下に入ると

1
(my_project) $

のようにプロンプトが変更されます。

GNU screenのCaptionに仮想環境名を表示

とプロンプトに表示も出来ますが、 個人的にプロンプト部分に色々表示されて変わるのは好きではないです。

また、ターミナル作業は基本的に常にGNU screenを使っているので、 そのCaptionに仮想環境名を表示するようにしてみます。

まず、.screenrcではcaption%hを含めてウィンドウのhardstatusを表示するようにしておきます。

このhardstatusは

1
$ printf "\e]0;%s\a" "hardstatus"

のようにすることでコマンドで変更することが出来ます。

%tでタイトルを表示できますが、こちらは

1
$ printf "\ek%s\e\134" "hardstatus"

で変更できます。

どちらでも同じようなものなのですが、%hの方は大概のターミナルアプリ自体のタイトルに表示されるものでもあるのでそちらへも表示できるよう%hを使うことにします。

逆に%hは全体のhardstatusの設定の方にいれておきます。 こちらには全ウィンドウの情報が表示されるようにするのでなるべく情報は少ないほうが良いので。

これを使って、.bashrcとかに以下のような設定を追加します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
_venv_prompt () {
  if [ -z "$VIRTUAL_ENV_PROMPT" ] && [ -n "$VIRTUAL_ENV" ];then
    local prompt
    prompt=$(grep "^prompt *=" "$VIRTUAL_ENV/pyvenv.cfg" 2>/dev/null|cut -d "=" -f 2|sed 's/^ *//')
    if [ -z "$prompt" ];then
      prompt="$(basename "$VIRTUAL_ENV")"
    fi
    printf '(%s) ' "$prompt"
  fi
}

if [[ $TERM =~ screen ]];then
  _screen_prompt () {
    local dir=${PWD/#$HOME/\~}
    printf "\ek%s %s\e\134" "$(hostname -s)" "$dir"
    printf "\e]0;%s %s%s\a" "$(hostname -s)" "$(_venv_prompt)" "$dir"
  }

  PROMPT_COMMAND="${PROMPT_COMMAND:+${PROMPT_COMMAND};}_screen_prompt"
fi

.screenrcの関連部分は以下のようにしています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# Caption for each window
setenv a ""
setenv a "$a%?%F"     # if the window has the focus
setenv a "$a%{= 0;255}" #   set attribute = {dim, black on white}
setenv a "$a%:"       # else
setenv a "$a%{=d 0;240}" #   set attribute = {dim, black on gray}
setenv a "$a%?"       # end if
setenv a "$a%?%P"     # if copy/paste mode
setenv a "$a%{+ 0;012}" #   set attribute = {on bright blue}
setenv a "$a%?"       # end if
setenv a "$a%n"       # window number
setenv a "$a "        # space
setenv a "$a%L="      # Mark the position for the next truncation
setenv a "$a%h"       # window' hardstatus
setenv a "$a%L>"      # Truncate %L= ~ %L>, 'L' for > gives marks at the truncation point
setenv a "$a%="       # With the last %-0=, it makes right alignment for following lines
setenv a "$a%?%P"     # if copy/paste mode
setenv a "$a  *copy/paste*" # draw this sentence
setenv a "$a%?"       # end if
setenv a "$a%?%E"     # if the escape character has  been pressed
setenv a "$a  ***command**" # draw this sentence
setenv a "$a%?"       # end if
setenv a "$a%-0="     # pad the string to the display's width. "-0" means
                      # start from the rightest side
                      # (e.g. 10=: 10% from left, 010, 10 characters from left
                      # (     -10=: 10% from right)
                      # With %L>, %= above, screen tries to truncates the part 
                      # in the left of "%L>"
caption always "$a"
unsetenv a


# hardstatus
setenv a ""
setenv a "$a%{=d 0;240}" # set attribute = {black(k) on bright Black(K)}
setenv a "$a%-w"     # windows' list up to the current window (shown as "%n %t")
setenv a "$a%40>"    # Mark here as a point to move when truncation
                     # Try to move this point to the 40% point
setenv a "$a%{= 0;255}" # set attribute = {black(k) on bright Write(W)}
setenv a "$a%n"      # current window number
setenv a "$a "       # space
setenv a "$a%t"      # current window title
setenv a "$a%{-}"    # remove the set from the current attributes
setenv a "$a%+w"     # windows' list starting with the window after the current one (shown as "%n %t")
setenv a "$a%-0="    # pad the string to the display's width.
                     # About Truncation/Padding(=,<,>)
                     # http://aperiodic.net/screen/truncation_padding_escapes
hardstatus alwayslastline "$a"
unsetenv a

これで、環境外だと

20241128_misenonactive.jpg

環境内に入ると

20241128_miseactive.jpg

といった感じにウィンドウのCaption部分に仮想環境名が表示されるようになります。

Sponsored Links
  1. uv run envとするとプロジェクトのディレクトリ下ならVIRTUAL_ENVが設定されているのでそれを使って判定することがある程度は可能です。

    uv run echo $VIRTUAL_ENVだとuv run envVIRTUAL_ENVが設定されている場合も何も表示されません。 これだとuv runの前に今の$VIRTUAL_ENVが展開されてしまうからの模様。uv run eval ...のようにすると

    1
    2
    
    error: Failed to spawn: `eval`
      Caused by: No such file or directory (os error 2)
    

    とエラーになってしまいます。

    なのでuv run envで全環境変数を取得して、その中にVIRTUAL_ENVがあるかどうかを判定します。

    VIRTUAL_ENVがあり、かつPATH$VIRTUAL_ENV/binがない場合は追加する、逆にuv run envVIRTUAL_ENVがない場合は外す、など。

    . .venv/bin/activateしていた場合はディレクトリ関係なく常にVIRTUAL_ENVが設定される状態になります。 なのでこの場合は上の設定を入れてもプロジェクト外に出てもそのままactivateされたまま残ります。(普通にactivateしたときと同じ)

    uv run $SHELLしたときも常にVIRTUAL_ENVが設定されるので常時PATHが通った状態を保つことになります。

    そんな感じで一応できそうですが、ちょっとしたシェルスクリプトでも おそらくmiseの方が高速にやってくれるだろうと思います。

Sponsored Links

« Homebrew-fileへwhalerew, VSCodeの拡張機能管理を追加

}