1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

uv でパッケージのソースをオプションで切り替えるときの注意点

1
Last updated at Posted at 2025-12-24

uv でパッケージのソースをオプションで切り替えたいとき、ドキュメント通りにすれば可能です。が、ドキュメントがリファレンス指向でないので「この記述は必須なのだろうか」となることもあると思います。それでやってみて、やはり必須だったことを書きました。

uv==0.9.18 で動作確認しています。

まとめ

uv であるパッケージのソース (どこから取得するか) をオプションによって切り替えたいときがあると思います。例えば、以下のようなユースケースがあると思います。

  • uv sync --extra cpu なら CPU 用インデックス URL から、uv sync --extra gpu なら GPU 用インデックス URL からパッケージを取得したい。
  • uv sync --extra repo なら GitHub リポジトリから、uv sync --extra local なら手元のコードからパッケージを取得したい。

これらは実現可能ですが、以下を守る必要があります。

  • オプションによってあるパッケージのソースを切り替える関係上、必ずオプション同士が競合 (=同時にインストールし得ない) しますが、このようなオプションの組は明示的に tool.uv.conflicts に書く必要があります。
    • ドキュメントの書きぶりが「tool.uv.conflicts に宣言すると便利」といった感じだし、同じパッケージのソースを切り替えているので競合するのは自動的に識別できそうに感じますが、ちゃんと書かないと意図しない側のソースからのインストールが起こり得ます。
  • ソースを切り替えたいパッケージは明示的に project.optional-dependencies に記述する必要があります。
    • 同じパッケージ名を 2 回書くことになるので project.dependencies にまとめて記述したくなりますが、まとめると怒られます。怒られるのですぐわかります。

ソース切り替えが上手くいく例

Python パッケージ管理ツール uv では「このパッケージは (このオプションのときには) このソースから取得してくださいね」と記述できます (ドキュメント)。なので、オプションによるソース切り替えができます。

例えば以下の pyproject.toml は、uv sync --extra dog とするか uv sync --extra cat とするかでパッケージ toml の取得先を PyPI にするかローカルパスにするか切り替えられます。なお、../dummy/ にダミーパッケージを用意してあります (記事の最下部参照)。

pyproject.toml ( OK )
[project]
name = "test"
version = "0.0.1"
requires-python = "==3.12.*"
dependencies = []

[project.optional-dependencies]  # オプション指定時に入れるパッケージ
dog = [ "toml", ]
cat = [ "toml", ]

[tool.uv]
conflicts = [ [ { extra = "dog" }, { extra = "cat" } ] ]  # オプション間が競合するので必要
sources = { toml = [  # toml のソース定義 (このオプションならこのソース)
    { extra = "dog", index = "pypi" },
    { extra = "cat", path = "../dummy/" },
] }
index = [ { name = "pypi", url = "https://pypi.org/simple/" } ]  # インデックス定義

ちなみに上記の場合、dog オプション時のソースがデフォルトソースの PyPI なので、明示的にソースを記述しなくても同じ挙動になります。

pyproject.toml ( OK : PyPI 省略 )
[project]
name = "test"
version = "0.0.1"
requires-python = "==3.12.*"
dependencies = []

[project.optional-dependencies]  # オプション指定時に入れるパッケージ
dog = [ "toml", ]
cat = [ "toml", ]

[tool.uv]
conflicts = [ [ { extra = "dog" }, { extra = "cat" } ] ]  # オプション間が競合するので必要
sources = { toml = [  # ソース定義 (このオプションならこのソース)
    { extra = "cat", path = "../dummy/" },
] }

ソース切り替えが上手くいかない例 ( conflicts を明記しない )

上手くいく例をみると、conflicts と明記していますが、「これはきっと catdog 両方を指定したとき怒るためのもので、そんなミスをしない自信があるなら明記しなくてもよいのでは」という気がします。そこで、conflicts を削除してみます (以下)。

pyproject.toml ( NG : conflicts を明記しない )
[project]
name = "test"
version = "0.0.1"
requires-python = "==3.12.*"
dependencies = []

[project.optional-dependencies]  # オプション指定時に入れるパッケージ
dog = [ "toml", ]
cat = [ "toml", ]

[tool.uv]
sources = { toml = [  # toml のソース定義 (このオプションならこのソース)
    { extra = "dog", index = "pypi" },
    { extra = "cat", path = "../dummy/" },
] }
index = [ { name = "pypi", url = "https://pypi.org/simple/" } ]  # インデックス定義

こうすると、uv sync --extra dog としたときであっても ../dummy/ から toml をインストールすることがわかります (少なくとも手元では)。これは意図しない挙動です。

uv がこのような挙動をする原因をきちんと理解していませんが、uv は conflicts と教えない限り、あらゆる条件下でインストール可能なパッケージ及びそのバージョン一覧を導こうとするため (universal resolution)、何らかの理由でローカルパスが優先されてしまうものと考えられます (ちなみにオプションの記述順序を変えてもローカルパスからインストールされたので、「後に記述した cat が上書きする」という原理ではなさそうです)。

何にせよ、conflicts は単に「両方指定したとき怒るためのもの」ではなく、「明記しない限りすべてのオプションが成り立つよう解決するもの」で、明記する必要があります。

上手くいかない例 ( optional-dependencies を明記しない )

上手くいく例をみると、project.optional-dependencies 以下の dogcat も内容が全く同じなので、プログラマ的感覚では project.dependencies に記述をまとめればよいのでは、と思うかもしれません (以下)。こうしたって、tool.uv.sources にはちゃんと「toml は dog のとき PyPI から、cat のとき ../dummy/ から取得してね」と書いているのだから上手くいきそうな気がします。

pyproject.toml ( NG : optional-dependencies を明記しない )
[project]
name = "test"
version = "0.0.1"
requires-python = "==3.12.*"
dependencies = [ "toml", ]

[project.optional-dependencies]  # オプション指定時に入れるパッケージ
dog = []
cat = []

[tool.uv]
conflicts = [ [ { extra = "dog" }, { extra = "cat" } ] ]  # オプション間が競合するので必要
sources = { toml = [  # toml のソース定義 (このオプションならこのソース)
    { extra = "dog", index = "pypi" },
    { extra = "cat", path = "../dummy/" },
] }
index = [ { name = "pypi", url = "https://pypi.org/simple/" } ]  # インデックス定義

しかし、これで uv sync --extra dog すると以下のように具体的に怒られインストールできません。なので明記する必要があります。

Source entry for toml only applies to extra dog, but toml was not found under the project.optional-dependencies section for that extra. When an extra is present on a source (e.g., extra = "dog"), the relevant package must be included in the project.optional-dependencies section for that extra (e.g., project.optional-dependencies = { "dog" = ["toml"] }).

参考文献

参考: ダミーパッケージ

以下を用意するだけです。

dummy/
├─ toml/__init__.py  # 空ファイル
└─ pyproject.toml

pyproject.toml には以下を記述するだけです。

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "toml"
version = "0.0.1"
requires-python = "==3.12.*"
dependencies = []

こちらのダミーパッケージがインストールされた場合、バージョンが 0.0.1 になるので PyPI からインストールされる 0.10.2 (2025/12/24 現在) でないことがわかります (標準出力にも ../dummy/ からインストールされていることが表示されると思います)。

from importlib.metadata import version
print(version('toml'))
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?