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にまとめて記述したくなりますが、まとめると怒られます。怒られるのですぐわかります。
- 同じパッケージ名を 2 回書くことになるので
ソース切り替えが上手くいく例
Python パッケージ管理ツール uv では「このパッケージは (このオプションのときには) このソースから取得してくださいね」と記述できます (ドキュメント)。なので、オプションによるソース切り替えができます。
例えば以下の pyproject.toml は、uv sync --extra dog とするか uv sync --extra cat とするかでパッケージ toml の取得先を PyPI にするかローカルパスにするか切り替えられます。なお、../dummy/ にダミーパッケージを用意してあります (記事の最下部参照)。
[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 なので、明示的にソースを記述しなくても同じ挙動になります。
[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 と明記していますが、「これはきっと cat と dog 両方を指定したとき怒るためのもので、そんなミスをしない自信があるなら明記しなくてもよいのでは」という気がします。そこで、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 以下の dog も cat も内容が全く同じなので、プログラマ的感覚では project.dependencies に記述をまとめればよいのでは、と思うかもしれません (以下)。こうしたって、tool.uv.sources にはちゃんと「toml は dog のとき PyPI から、cat のとき ../dummy/ から取得してね」と書いているのだから上手くいきそうな気がします。
[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
tomlonly applies to extradog, buttomlwas not found under theproject.optional-dependenciessection for that extra. When an extra is present on a source (e.g.,extra = "dog"), the relevant package must be included in theproject.optional-dependenciessection for that extra (e.g.,project.optional-dependencies = { "dog" = ["toml"] }).
参考文献
-
https://docs.astral.sh/uv/concepts/projects/dependencies/#dependency-sources
- パッケージのソースとしてどんなソースを指定できるかの節です。また、この次の節が「オプション依存関係」で、「特定のオプション時のみのソースを記述できる」旨もあります。
-
https://docs.astral.sh/uv/reference/settings/#conflicts
-
tool.uv.conflictsに関する節です。ここをさらっと読むと It's useful to declare conflicts と useful (便利) とあるだけなので、conflicts の declare が必須なのかどうかわかりづらいですが、必須のようです。
-
参考: ダミーパッケージ
以下を用意するだけです。
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'))