見出し画像

pipでパッケージの更新をしたらアンインストールエラーが出た話

pip でパッケージを管理してると色々思うようにいかないことってありますよね。
今回は pip を使ったパッケージのアップデート中にアンインストールエラーが出たときのお話をします。アップデート後に pip check で不整合が見つかった時の対処法も同時に記します。

この note でできること

pip でインストールしたパッケージ全体のアップデートができます。全自動ではなく半自動です。最後の方に手順をまとめてあります。例外処理は一応していますが、完璧ではないのである程度の知識を身に着けた上で実施してください。

経緯

量子コンピューター上のプログラミングの学習のため、anaconda な環境で blueqat をインストールしていたのですが、しばらく使わないうちに古くなってしまったので更新しようとしました。このとき、blueqat だけ更新すれば良かったのですが、せっかくだからできるものはすべてアップデートしてしまおうと思い、以下のコマンドを実行しました。

$ pip list -o | tail -n +3 | awk '{ print $1 }' | xargs pip install -U

すると、依存関係で芋づる式に更新がかかり、ここでエラーが発生します。

ERROR: Cannot uninstall 'PyYAML'. It is a distutils installed project and thus we cannot accurately determine which files belong to it which would lead to only a partial uninstall.

ちなみにバックアップした別環境で blueqat のみの更新をしてみましたがエラーは発生しませんでした。余計なことをしてしまった… 今回は anaconda だったので新しい環境を作りなおしてもよいのですが、せっかく作った環境がもったいないですし、実環境でこのような事態に陥ったら目も当てられないので、対処法を確立しておきたいと思い、記録します。

対処法

このようなとき、以下のような手順を踏みます。
1. pip を古いバージョンに後退
2. エラーが出たパッケージをアンインストール
3. pip を最新版に更新
4. 対象のパッケージを再度インストール

コマンドは以下のようになります。

$ pip install pip==8.1.1
$ pip uninstall PyYAML
$ pip install --upgrade pip
$ pip install PyYAML
$ pip install pip==8.1.1
Collecting pip==8.1.1
Downloading pip-8.1.1-py2.py3-none-any.whl (1.2 MB)
|████████████████████████████████| 1.2 MB 2.0 MB/s
Installing collected packages: pip
Attempting uninstall: pip
Found existing installation: pip 20.2.3
Uninstalling pip-20.2.3:
Successfully uninstalled pip-20.2.3
Successfully installed pip-8.1.1
$ pip uninstall -y PyYAML
DEPRECATION: Uninstalling a distutils installed project (PyYAML) has been deprecated and will be removed in a future version. This is due to the fact that uninstalling a distutils project will only partially uninstall the project.
Uninstalling PyYAML-5.1.1:
Successfully uninstalled PyYAML-5.1.1
You are using pip version 8.1.1, however version 20.2.3 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
$ pip install --upgrade pip
Cache entry deserialization failed, entry ignored
Collecting pip
Cache entry deserialization failed, entry ignored
Downloading https://files.pythonhosted.org/packages/4e/5f/528232275f6509b1fff703c9280e58951a81abe24640905de621c9f81839/pip-20.2.3-py2.py3-none-any.whl (1.5MB)
100% |████████████████████████████████| 1.5MB 758kB/s
Installing collected packages: pip
Found existing installation: pip 8.1.1
Uninstalling pip-8.1.1:
Successfully uninstalled pip-8.1.1
Successfully installed pip-20.2.3
$ pip install PyYAML
Processing ./.cache/pip/wheels/a7/c1/ea/cf5bd31012e735dc1dfea3131a2d5eae7978b251083d6247bd/PyYAML-5.3.1-cp37-cp37m-linux_x86_64.whl
Installing collected packages: PyYAML
Successfully installed PyYAML-5.3.1

せっかくだからスクリプトにしましょう。私は名前を pip_fix_upgrade.bash としてますが、お好みの名前でどうぞ。

#!/bin/bash

PRG_NAME=$(basename $0)

if [ "$1" = "" ]; then
 echo "usage:"
 echo "<Specifying a package name>"
 echo "  $PRG_NAME [package_name]"
 echo ""
 echo "<Automatic update and fix error at once>"
 echo "  $PRG_NAME --auto"
 echo "  $PRG_NAME -A"
 exit 2
elif [ "$1" = "--auto" -o "$1" = "-A" ]; then
 PKG=`pip list -o | tail -n +3 | awk '{print $1}'`
 if [ -z "$PKG" ]; then
   echo "Nothing to be upgraded for pip."
   exit 1
 else
   PKG_NAME=`pip install -U $PKG 3>&2 2>&1 1>&3 | grep "^ERROR: " | cut -d "'" -f 2`
 fi
else
 PKG_NAME=$1
fi

if [ -z "$PKG_NAME" ]; then
 echo "No packages marked for update."
 ret=1
else
 pip install pip==8.1.1
 pip uninstall -y $PKG_NAME
 pip install --upgrade pip
 pip install $PKG_NAME
 ret=$?
fi

exit $ret

このスクリプトは --auto または -A オプションを指定することで一括アップデートを開始し、失敗したときにそのパッケージ名を自動で取得して先述の方法で更新をして終了します。無事更新ができたら0、更新するパッケージがもうなければ1を返します。
パッケージ名は標準エラー出力に表示されるため、2>&1にしたいところですが、それをすると標準出力まで見られなくなるので、3>&2 2>&1 1>&3 として標準出力と標準エラー出力を入れ替えます
また、パッケージ名を引数で指定できるので、他の pip の作業でこのエラーが出たときなどにも使えます。このときは再インストール作業しかしないため処理が速いです。
"No packages marked for update." と表示されるまでヒストリーで繰り返し実行するかループで回すなどして下さい。

整合性チェック

ここで終わりにはせず、整合性のチェックをしておきます。
パッケージの依存関係では新しすぎても不都合があるからです。

$ pip check
$ pip check
spyder 4.1.5 has requirement jedi==0.17.1, but you have jedi 0.17.2.
spyder 4.1.5 has requirement parso==0.7.0, but you have parso 0.8.0.
spyder 4.1.5 has requirement pyqt5<5.13; python_version >= "3", but you have pyqt5 5.15.1.
spyder 4.1.5 has requirement pyqtwebengine<5.13; python_version >= "3", but you have pyqtwebengine 5.15.1.
jedi 0.17.2 has requirement parso<0.8.0,>=0.7.0, but you have parso 0.8.0.
dwave-ocean-sdk 3.0.1 has requirement dimod==0.9.7, but you have dimod 0.9.8.
astroid 2.4.2 has requirement lazy-object-proxy==1.4.*, but you have lazy-object-proxy 1.5.1.

この例では spyder が依存する jedi 等、いくつかのパッケージのバージョンが新しすぎることが分かります。上の出力結果のうち1行目のパッケージについて整合性を図ります。

$ pip install `pip check | grep " has requirement " | head -n 1 | awk '{print $5}' | sed -e 's%>=%==%;s%\;%%' -e 's%,$%%'`
$ pip install `pip check | grep " has requirement " | head -n 1 | awk '{print $5}' | sed -e 's%>=%==%;s%\;%%' -e 's%,$%%'`

Collecting jedi==0.17.1
Using cached jedi-0.17.1-py2.py3-none-any.whl (1.4 MB)
Collecting parso<0.8.0,>=0.7.0
Downloading parso-0.7.1-py2.py3-none-any.whl (109 kB)
|████████████████████████████████| 109 kB 3.8 MB/s
Installing collected packages: parso, jedi
Attempting uninstall: parso
Found existing installation: parso 0.8.0
Uninstalling parso-0.8.0:
Successfully uninstalled parso-0.8.0
Attempting uninstall: jedi
Found existing installation: jedi 0.17.2
Uninstalling jedi-0.17.2:
Successfully uninstalled jedi-0.17.2
ERROR: After October 2020 you may experience errors when installing or updating packages. This is because pip will change the way that it resolves dependency conflicts.

We recommend you use --use-feature=2020-resolver to test your packages with the new resolver before it becomes the default.

spyder 4.1.5 requires parso==0.7.0, but you'll have parso 0.7.1 which is incompatible.
spyder 4.1.5 requires pyqt5<5.13; python_version >= "3", but you'll have pyqt5 5.15.1 which is incompatible.
spyder 4.1.5 requires pyqtwebengine<5.13; python_version >= "3", but you'll have pyqtwebengine 5.15.1 which is incompatible.
Successfully installed jedi-0.17.1 parso-0.7.1

何かエラーが出てますけど目的としていることとは関係ないので気にしないことにします。これをヒストリーで何度も繰り返せばいずれ整合性のある状態になりますが、繰り返しが面倒なのでまたまたスクリプトにします。私は名前を pip_chk+.bash としてますが、お好みで。

#!/bin/bash

# pip check
# pip install `pip check | grep " has requirement " | head -n 1 | awk '{print $5}' | sed -e 's%>=%==%;s%\;%%' -e 's%,$%%'`

while :
do
  PKG_NAME=`pip check | grep " has requirement " | head -n 1 | awk '{print $5}' | sed -e 's%>=%==%;s%\;%%' -e 's%,$%%'`

  if [ -z "$PKG_NAME" ]; then
    echo "No broken requirements found."
    break
  else
    pip install $PKG_NAME
  fi
done

exit 0

スクリプトを実行した後(エラー出るけど気にしない)、"No broken requirements found." と出力されたら終了です。心配性な方は再度 pip check で確認してください。

$ pip check
No broken requirements found.

手順まとめ

# パッケージ全体をアップデートします。
$ pip list -o | tail -n +3 | awk '{ print $1 }' | xargs pip install -U

# ここでアンインストールエラーが出たら、以下のスクリプトを実行します。
$ pip_fix_upgrade.bash -A

# ここでまたアンインストールエラーが出たら同じスクリプトを実行します。
$ pip_fix_upgrade.bash -A

# ↑アンインストールエラーが出なくなるまで繰り返し実行します。

# 整合性をチェックします。
$ pip check

# ここで"No broken requirements found."と出力されればこれ以上の処理は不要です。

# 整合性を図ります。
$ pip_chk+.bash

# 以上で終了です。

最後に

実を言うと筆者はこれらの作業をこまめに実施しており、最近はなかなかこのような事態に遭遇しなかったのですが、ファイルの整理をしていた時、 anaconda に blueqat をセットアップした VM の古いバックアップを発見したので、起動してこの仕組みを作った時のことを思い出して書いてみました。みなさんもまずは VM やコンテナで試してみてから導入してみてください。

この記事が気に入ったらサポートをしてみませんか?