新しいパッケージ管理ツールuvを使ってPythonの依存関係をgit clone前後で保持する

これまでPythonの仮想環境をAnaconda3で管理していたのですが、 **なにやらパッケージ管理ツールの「uv」が非常に軽快で、管理手順もわかりやすいとの話を聞きましたので導入してみました。** なお昨今のプロジェクトの多くはgitで管理されていることから、リポジトリにプロジェクトをpushしたものを別のメンバーがgit cloneした後にどれだけ簡単にパッケージを復元できるかも試してみました（**結論：`uv sync`のみで完全に復元できました**）。


## Anacondaによるパッケージ管理を無効にする

まず、これまで利用してきたAnacondaによるパッケージ管理を無効にします。uvと同居することも可能ですが、ここでは説明を簡単にするため除外しました。まず、`(base)`となっている状態から`deactivate`を実行し、ターミナルを開きなおしてもAnacondaが自動起動しないよう`--set auto_activate_base false`を指定します。設定後、念のため再起動し`(base)`がプロンプトに出てこないことを確認します。

```bash
# パッケージ管理ツールcondaの環境から抜ける
(base) $ conda deactivate
# パッケージ管理ツールcondaが自動起動しないように設定
$ conda config --set auto_activate_base false
# 再起動
$ sudo reboot
```

## uvをインストールする

それでは早速 **uv** をインストールしてみましょう。とはいうものの、インストール自体はひとつのコマンドのみで完了です。インストール後`.bashrc`を再度読み込み、uvへのパスを通します。

```bash
# uvをインストールする
# -->> https://docs.astral.sh/uv/getting-started/installation/
$ curl -LsSf https://astral.sh/uv/install.sh | sh

# .bashrcを読み込み直しパスを通す
$ source ~/.bashrc

####
## アンインストールする場合は
## $ rm -rf ~/.local
## ~/.bashrcの下記の行を削除
## . "$HOME/.local/bin/env"
```

https://docs.astral.sh/uv/getting-started/installation/

## プロジェクトをgitで管理する

本記事は、gitによりバージョン管理されたプロジェクトの中でuvを使うことを想定したものとなっています。gitの使い方の整理を兼ねて、GitHubでリポジトリを管理する手順をまとめておきます。

#### ローカルのワークスペースをgitにより管理する

まず作業ディレクトリを作成し、`git init`でバージョン管理を開始します。

```bash
# プロジェクトのディレクトリを作成する
$ cd ~
$ mkdir opencv-web-sandbox
$ cd opencv-web-sandbox
$ pwd
/home/shino/opencv-web-sandbox

# gitによるバージョン管理を有効にする
$ git init
$ ls -a
.  ..  .git
```

バージョン管理を開始した後、GitHubのデフォルトのブランチである`main`ブランチに`git branch`で切り替えます。そして`README.md`を作成し、コミットします。初回のコミット時はユーザー名とメールアドレスの登録を促されますので、これらを設定した後に再度コミットします。


```bash
# ブランチを切り替える
$ git branch -m main

# READMEを追加してコミット
$ touch README.md
$ git add -A
$ git commit -m "Add README.md"
# *** Please tell me who you are.
# Run
#   git config --global user.email "you@example.com"
#   git config --global user.name "Your Name"
# -->> 投稿者のメールアドレスと名前を登録しましょう

$ git config --global user.email "shino@xtrize.com"
$ git config --global user.name "Shinobu HUYUGIRI"

# 再度コミット
$ git commit -m "Add README.md"
# [master (root-commit) a2ef8de] Add README.md
#  1 file changed, 0 insertions(+), 0 deletions(-)
#  create mode 100644 README.md
```

### GitHub上にリモートリポジトリを作成する

GitHubにアカウントを作成し、はじめてのリモートリポジトリを作成します。`Repositories`を開き`New`を選択し、リポジトリの作成画面へ移行します。

![Screenshot from 2024-12-01 19-17-55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/1d8b9a15-999f-c8a1-2b63-c500683d81dc.png)

リポジトリの名前と説明文を入力し、公開リポジトリであれば「Public」を選択します。開発する言語に合わせて「.gitignore」ファイルを設定（今回はPythonを選択）し、ソースコードのライセンスを指定してください。ここでは「[2条項 BSD License](https://licenses.opensource.jp/BSD-2-Clause/BSD-2-Clause.html)」を指定しました。

![Screenshot from 2024-12-01 19-18-25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/c82cc799-0407-26dc-3cef-67ef79d62b68.png)

### GitHub上のファイルとローカルのコミットをマージする

続いて、ローカルとリモートのリポジトリを結合します。GitHub上に作成されたリポジトリを開き、右上の「Code」をクリックしリモートリポジトリのURLを控えておいてください。

![Screenshot from 2024-12-01 19-18-57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/72746b2c-f054-3ef2-b9e1-d52b81e03ec6.png)

GitHubのリモートリポジトリとローカルリポジトリは`git remote add`により紐づけします。ブランチはデフォルトの`origin`を選択しました。ここで、リモートに格納されているファイル`.gitignore`と`LICENSE`を取得しようと`git pull`すると、ブランチと結合するデフォルトの方法を聞かれます。今回は`git config pull.rebase false`を指定しました。そして、リモートリポジトリとローカルリポジトリを`git merge --allow-unrelated-histories origin/main`によりマージします。

``` bash
# GitHub上に作成したgitと結合する
$ git remote add origin https://github.com/xtrizeShino/opencv-web-sandbox.git

# リモートのファイルをpullしようとすると...
$ git pull origin main
# hint: You have divergent branches and need to specify how to reconcile them.
# hint: You can do so by running one of the following commands sometime before
# hint: your next pull:
# hint: 
# hint:   git config pull.rebase false  # merge
# hint:   git config pull.rebase true   # rebase
# hint:   git config pull.ff only       # fast-forward only
# hint: 
# hint: You can replace "git config" with "git config --global" to set a default
# hint: preference for all repositories. You can also pass --rebase, --no-rebase,
# hint: or --ff-only on the command line to override the configured default per
# hint: invocation.
# fatal: Need to specify how to reconcile divergent branches.

# リモートとローカルのコミットをマージする
$ git config pull.rebase false
$ git merge --allow-unrelated-histories origin/main
```

マージ作業は特に必要ないため、下記の画面が出たら【Ctrl+x】で抜けます。

![Screenshot from 2024-12-01 19-20-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/9b075042-0f0e-6d2d-9856-73777cc18416.png)

```bash
$ git merge --allow-unrelated-histories origin/main
# Merge made by the 'ort' strategy.
#  .gitignore | 162 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
#  LICENSE    |  24 +++++++++
#  2 files changed, 186 insertions(+)
#  create mode 100644 .gitignore
#  create mode 100644 LICENSE

# リモートで作成したLICENSEと.gitignoreが含まれていることを確認
$ ls -al
# drwxrwxr-x  3 shino shino 4096 12月  1 19:12 .
# drwxr-x--- 27 shino shino 4096 12月  1 19:10 ..
# drwxrwxr-x  8 shino shino 4096 12月  1 19:12 .git
# -rw-rw-r--  1 shino shino 3139 12月  1 19:12 .gitignore
# -rw-rw-r--  1 shino shino 1305 12月  1 19:12 LICENSE
# -rw-rw-r--  1 shino shino    0 12月  1 19:09 README.md
```

以上でマージ作業は完了です。

### GitHubに変更をpushする

最後に変更をpushしましょう。GitHubへのpushには、トークンと呼ばれるパスワードを生成する必要があります（ **GitHubのログインパスワードではありません** ）。GitHub右上のユーザーアイコンから「 **Settings** 」を選択し、左下の「 **Developer settings** 」から「 **Fine-grained tokens** 」を開き、「 **Generate new token** 」をクリックします。

![Screenshot from 2024-12-01 19-23-42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/b65caa10-8d47-2c91-d58f-c467a08498c3.png)

![Screenshot from 2024-12-01 19-23-54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/71a076a1-081c-c5e4-9881-d36325ca8508.png)

![Screenshot from 2024-12-01 19-24-10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/85f1018f-b665-fbc6-170d-bad1edea2005.png)

トークンの名前と説明を入力した後、「 **All repositories** 」を選択し「 **Reposiroty permissions** 」の「 **Contents -->> Read and write** 」を設定してください。本設定が抜けているとリモートリポジトリへの書き込み権限が不足となるため、pushに失敗します。最後に 「 **Generate token** 」をクリックするとトークンが生成されます。

![Screenshot from 2024-12-01 19-25-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/7c301399-4b2c-e348-084a-800ca549ec64.png)

生成できたトークンをコピーして`git push`のパスワードに指定します。

![Screenshot from 2024-12-01 19-25-37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/7f38a001-0486-fab8-9b11-041883230f04.png)

GitHubのユーザー名と、上記の手順により生成したトークンをパスワードに指定することで、下記のようにpushできます。

```bash
# ローカルの変更をpushする
$ git push origin main
# Username for 'https://github.com': xtrizeShino
# Password for 'https://xtrizeShino@github.com': 
# Enumerating objects: 6, done.
# Counting objects: 100% (6/6), done.
# Delta compression using up to 20 threads
# Compressing objects: 100% (3/3), done.
# Writing objects: 100% (5/5), 551 bytes | 551.00 KiB/s, done.
# Total 5 (delta 0), reused 0 (delta 0), pack-reused 0
# To https://github.com/xtrizeShino/opencv-web-sandbox.git
#    95c10ca..40e2799  main -> main

```

#### git pushでのパスワード入力を省略する

上記の手順の場合、pushする度に毎回パスワード(トークン)を入力しなければなりません。これは非効率的なため、sshの公開鍵を使ってパスワードを聞かれないようにします。まず、ssh接続するために`git remote set-url`でリモートリポジトリのURLを `https://github.com/...` から `git@github.com:...` に変更します。


```bash
# リモートの設定を確認
$ git remote -v
# origin	https://github.com/xtrizeShino/opencv-web-sandbox.git (fetch)
# origin	https://github.com/xtrizeShino/opencv-web-sandbox.git (push)

# SSH接続に切り替える
$ git remote set-url origin git@github.com:xtrizeShino/opencv-web-sandbox.git

# リモートの設定を確認
$ git remote -v
# origin	git@github.com:xtrizeShino/opencv-web-sandbox.git (fetch)
# origin	git@github.com:xtrizeShino/opencv-web-sandbox.git (push)
```

続いてsshの公開鍵を作成します。 `ssh-keygen` を実行し鍵のペアを作成します。passphraseも指定できますが今回はemptyにしました。鍵が作成されると `~/.ssh` に格納されますので、公開鍵 `*.pub` のファイルを`cat`して内容をコピーします。

```bash
# SSH鍵を作成する
$ ssh-keygen
# Generating public/private ed25519 key pair.
# Enter file in which to save the key (/home/shino/.ssh/id_ed25519): 
# Enter passphrase (empty for no passphrase): 
# Enter same passphrase again: 
# Your identification has been saved in /home/shino/.ssh/id_ed25519
# Your public key has been saved in /home/shino/.ssh/id_ed25519.pub
# The key fingerprint is:
# SHA256:xhmdjnTMna7aFlGzF2Ond6th24TZ4sXIFN5hEM+OE3Y shino@shino-MS-7D98
# The key's randomart image is:
# +--[ED25519 256]--+
# |             oo  |
# |         + ooo*o.|
# |        o *.+*+E.|
# |       o *..ooB.o|
# |        S ..++*oo|
# |       .  .. O.* |
# |          ..o O  |
# |         o.  + . |
# |        ...      |
# +----[SHA256]-----+

# 作成した鍵を参照する
$ ls ~/.ssh
# authorized_keys  id_ed25519  id_ed25519.pub
$ cat ~/.ssh/id_ed25519.pub 
# ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBh6LNzSwsxJSHS/ijKhTTdjJmSNgDvgaIWWhgB04i+/ shino@shino-MS-7D98

# この鍵(*.pub)をGitHubに登録する
```

GitHubに公開鍵を登録するには、GitHub右上のユーザーアイコンから「 **Settings** 」を選択し、左のメニューから「 **SSH and GPG keys** 」を選択「 **New SSH key** 」をクリックします。そして先の手順にてコピーした公開鍵の内容をペーストすれば、sshの設定は完了です。

![Screenshot from 2024-12-01 19-36-46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/8a2c15a8-9abe-3117-9ca1-857836bbe57e.png)

![Screenshot from 2024-12-01 19-38-53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/6dd526af-9b92-dab2-878a-0b58fe0db2f8.png)

以上の設定をすることにより、パスワード無しで`git push`できます。

```bash
# 登録後にpushしてみる
$ git push
# fatal: The current branch main has no upstream branch.
# To push the current branch and set the remote as upstream, use
#     git push --set-upstream origin main

$ git push --set-upstream origin main
# The authenticity of host 'github.com (20.27.177.113)' can't be established.
# ED25519 key fingerprint is SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU.
# This key is not known by any other names.
# Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
# Everything up-to-date
```

## uvでプロジェクトを作成する

それではいよいよuvによりパッケージを管理してみましょう。まずワークスペースとなるディレクトリへ移動した後に `uv init <プロジェクト名>` を実行しましょう。すると、指定したプロジェクト名のディレクトリが作成され、パッケージ管理に必要となる`README.md`、`pyproject.toml`が生成され、仮想環境を操作する`.venv`が作成されます。`. .venv/bin/activate`で仮想環境に入ることができます。

```bash
# 仮想環境＋プロジェクトを作成する
$ uv init opencv-web-sandbox
# Adding `opencv-web-sandbox` as member of workspace `/home/shino/opencv-web-sandbox`
# Initialized project `opencv-web-sandbox` at `/home/shino/opencv-web-sandbox/opencv-web-sandbox`

# 仮想環境に入る
$ . .venv/bin/activate

# 生成されたプロジェクトファイルを確認する
(opencv-web-sandbox) $ ls opencv-web-sandbox/
README.md  hello.py  pyproject.toml
```

`uv sync`を実行すると仮想環境の内容と`pyproject.toml`の内容が同期されますので、`uv sync`してから`git commit`して`git push`すれば仮想環境の情報をGitHub上に保持することができます。


```bash
# 最初の同期
(opencv-web-sandbox) $ cd opencv-web-sandbox/
(opencv-web-sandbox) $ uv sync
# Using CPython 3.12.3 interpreter at: /usr/bin/python3
# Creating virtual environment at: /home/shino/opencv-web-sandbox/.venv
# Resolved 1 package in 1ms
# Audited in 0.00ms
### -->> びっくりするほど早い

# このあたりでgit addしておく
(opencv-web-sandbox) $ cd ..
(opencv-web-sandbox) $ pwd 
/home/shino/opencv-web-sandbox
(opencv-web-sandbox) $ git commit -m "uv init opencv-web-sandbox"
# [main b9dbb9e] uv init opencv-web-sandbox
#  6 files changed, 40 insertions(+)
#  create mode 100644 opencv-web-sandbox/.python-version
#  create mode 100644 opencv-web-sandbox/README.md
#  create mode 100644 opencv-web-sandbox/hello.py
#  create mode 100644 opencv-web-sandbox/pyproject.toml
#  create mode 100644 pyproject.toml
#  create mode 100644 uv.lock

# リポジトリへpushする
$ git push
```

#### 仮想環境の設定ファイルを確認する

仮想環境の設定ファイルである`pyproject.toml`を開くと、必要とされるPythonのバージョン等をみることができます。

```bash
# プロジェクトを触っていきましょう
(opencv-web-sandbox) $ cd opencv-web-sandbox
(opencv-web-sandbox) $ pwd
# /home/shino/opencv-web-sandbox/opencv-web-sandbox

# プロジェクトの設定は以下のようになっている
(opencv-web-sandbox) $ cat pyproject.toml 
[project]
name = "opencv-web-sandbox"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
```

### パッケージを追加する

パッケージの追加は **`uv add`** を利用します。`pip install`と同じ感覚で利用できますが、`uv`を介することにより本プロジェクトにそのパッケージが必要となることが明記されます。以下は`numpy`パッケージを追加した例です。`uv add numpy`後に`uv sync`することにより`pyproject.toml`が更新されます。

```bash
# numpyをいれてみる
$ uv add numpy

# syncしてプロジェクトファイルを確認する
$ uv sync
$ cat pyproject.toml 
[project]
name = "opencv-web-sandbox"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "numpy>=2.1.3",
]
## -->> プロジェクトにnumpyが必要な旨が追記されている
```

### Pythonのバージョンを切り替える

`pyproject.toml`に定義されている `requires-python` の設定を変えることで、異なるPythonのバージョンを導入することができます。以下の例では `requires-python` を3.12以降から3.10以降に変更した後に、Python3.10を導入しています。Pythonのバージョンは **`uv python pin`** により変更可能で、変更した後は必ず `source .venv/bin/activate` で仮想環境をリロードしてください。

```bash
# Pythonのバージョンを切り替える
$ python --version
Python 3.12.3

# プロジェクトファイルの依存部を書き直す
$ vi pyproject.toml 
# requires-python = ">=3.12" を
# requires-python = ">=3.10" に変更

# Python3.10をインストールする
$ uv venv -p 3.10
# Using CPython 3.10.15
# Creating virtual environment at: .venv
# Activate with: source .venv/bin/activate

# Python3.10に切り替える
$ uv python pin 3.10
# Updated `.python-version` from `3.12` -> `3.10`

# .venvを読み込み直せばPython3.10に切り替わっている
$ source .venv/bin/activate
$ python --version
Python 3.10.15

# ここまでの変更をgitへpush
$ git add -A
$ git commit -m "change python 3.12->3.10"
$ git push
```

### uv管理下にPyTorchをインストールする

続いて、よく利用されるPyTorchとOpenCVを導入してみましょう。まず、PyTorchのWebサイトから実行すべきコマンドを確認します。`pip3 install torch torchvision torchaudio`とのことなので **`uv add torch torchvision torchaudio`** によりインストールします。OpenCVも同様に **`uv add opencv-python`** でインストールします。インストールしたパッケージの依存関係は **`uv tree`** により確認することができます。

![Screenshot from 2024-12-01 20-38-23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/6bf59f25-7a53-ae29-3b59-d78efc623a8b.png)

```bash
# PyTorchの公式では以下とのことなので
# -->> pip3 install torch torchvision torchaudio
# uv pipで以下のようにインストールする
$ uv add torch torchvision torchaudio

# OpenCVもインストールする
$ uv add opencv-python

# プロジェクトファイルと同期する
$ uv sync
# warning: `VIRTUAL_ENV=.venv` does not match the project environment path `/# home/shino/opencv-web-sandbox/.venv` and will be ignored
# Resolved 29 packages in 1ms
# Audited 28 packages in 0.02ms

# インストールしたパッケージの依存関係を見る
$ uv tree

# ここまでの変更をコミットする
$ git add -A
$ git commit -m "add PyTorch and OpenCV"
$ git push
```

![Screenshot from 2024-12-01 20-45-06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/9dea731e-98b1-0a46-5479-5f9e794136c6.png)

#### PyTorchを実行してみる

PyTorchがちゃんと動くことを確認しておきましょう。

```bash
# python
>>> import torch
>>> torch.cuda.is_available()
True
```

## 作成済みのプロジェクトを利用する

多人数開発をイメージして、以上の変更をすべてコミットしてリモートリポジトリへ`git push`した後に、別のPCで`git clone`してパッケージを復元できるか試してみました。 **結論だけ言いますと `pyproject.toml` のあるディレクトリで `uv sync` するだけで仮想環境を復元することができました。個別にパッケージをインストールしなおすような非効率的な作業は不要です。** 

```bash
# 作業ディレクトリを作成する
$ cd ~
$ mkdir tmp
$ cd tmp
$ git clone https://github.com/xtrizeShino/opencv-web-sandbox.git
$ cd opencv-web-sandbox/

# 作業ディレクトリの内容は以下の通り
$ ls
# LICENSE  README.md  opencv-web-sandbox  uv.lock
$ cd opencv-web-sandbox/
$ pwd
# /home/shino/tmp/opencv-web-sandbox/opencv-web-sandbox
$ ls
# README.md  hello.py  pyproject.toml
$ uv sync
# Using CPython 3.10.15
# Creating virtual environment at: .venv
# Resolved 29 packages in 1.03s
# Installed 27 packages in 82ms
# ...

# 仮想環境にログインする
$ . .venv/bin/activate

# PyTorchが使えることを確認
(opencv-web-sandbox) $ python
>>> import torch
>>> torch.cuda.is_available()
True

$ deactivate
```

便利ですね！是非開発にお役立てください！