diff --git a/docs/dependency-specification.md b/docs/dependency-specification.md
index 63299e91c8e..500999394e5 100644
--- a/docs/dependency-specification.md
+++ b/docs/dependency-specification.md
@@ -572,6 +572,109 @@ pathlib2 = { version = "^2.2", markers = "python_version <= '3.4' or sys_platfor
{{< /tab >}}
{{< /tabs >}}
+### `extra` environment marker
+
+Poetry populates the `extra` marker with each of the selected extras of the root package.
+For example, consider the following dependency:
+```toml
+[project.optional-dependencies]
+paths = [
+ "pathlib2 (>=2.2,<3.0) ; sys_platform == 'win32'"
+]
+```
+
+`pathlib2` will be installed when you install your package with `--extras paths` on a `win32` machine.
+
+#### Exclusive extras
+
+{{% warning %}}
+The first example will only work completely if you configure Poetry to not re-resolve for installation:
+
+```bash
+poetry config installer.re-resolve false
+```
+
+This is a new feature of Poetry 2.0 that may become the default in a future version of Poetry.
+
+{{% /warning %}}
+
+Keep in mind that all combinations of possible extras available in your project need to be compatible with each other.
+This means that in order to use differing or incompatible versions across different combinations, you need to make your
+extra markers *exclusive*. For example, the following installs PyTorch from one source repository with CPU versions
+when the `cuda` extra is *not* specified, while the other installs from another repository with a separate version set
+for GPUs when the `cuda` extra *is* specified:
+
+```toml
+[project]
+name = "torch-example"
+requires-python = ">=3.10"
+dependencies = [
+ "torch (==2.3.1+cpu) ; extra != 'cuda'",
+]
+
+[project.optional-dependencies]
+cuda = [
+ "torch (==2.3.1+cu118)",
+]
+
+[tool.poetry]
+package-mode = false
+
+[tool.poetry.dependencies]
+torch = [
+ { markers = "extra != 'cuda'", source = "pytorch-cpu"},
+ { markers = "extra == 'cuda'", source = "pytorch-cuda"},
+ ]
+
+[[tool.poetry.source]]
+name = "pytorch-cpu"
+url = "https://download.pytorch.org/whl/cpu"
+priority = "explicit"
+
+[[tool.poetry.source]]
+name = "pytorch-cuda"
+url = "https://download.pytorch.org/whl/cu118"
+priority = "explicit"
+```
+
+For the CPU case, we have to specify `"extra != 'cuda'"` because the version specified is not compatible with the
+GPU (`cuda`) version.
+
+This same logic applies when you want either-or extras:
+
+```toml
+[project]
+name = "torch-example"
+requires-python = ">=3.10"
+
+[project.optional-dependencies]
+cpu = [
+ "torch (==2.3.1+cpu)",
+]
+cuda = [
+ "torch (==2.3.1+cu118)",
+]
+
+[tool.poetry]
+package-mode = false
+
+[tool.poetry.dependencies]
+torch = [
+ { markers = "extra == 'cpu' and extra != 'cuda'", source = "pytorch-cpu"},
+ { markers = "extra == 'cuda' and extra != 'cpu'", source = "pytorch-cuda"},
+ ]
+
+[[tool.poetry.source]]
+name = "pytorch-cpu"
+url = "https://download.pytorch.org/whl/cpu"
+priority = "explicit"
+
+[[tool.poetry.source]]
+name = "pytorch-cuda"
+url = "https://download.pytorch.org/whl/cu118"
+priority = "explicit"
+```
+
## Multiple constraints dependencies
Sometimes, one of your dependency may have different version ranges depending
diff --git a/poetry.lock b/poetry.lock
index 35b8db01c61..5a6b6a44b07 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -970,7 +970,7 @@ develop = false
type = "git"
url = "https://github.com/python-poetry/poetry-core.git"
reference = "main"
-resolved_reference = "616d7bfaf018d50bd09bc24c630b697b6368d5a3"
+resolved_reference = "b95ec5321f1842286b042ac206c9f5395850c684"
[[package]]
name = "pre-commit"
@@ -1221,108 +1221,103 @@ files = [
[[package]]
name = "rapidfuzz"
-version = "3.9.4"
+version = "3.10.1"
description = "rapid fuzzy string matching"
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
files = [
- {file = "rapidfuzz-3.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c9b9793c19bdf38656c8eaefbcf4549d798572dadd70581379e666035c9df781"},
- {file = "rapidfuzz-3.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:015b5080b999404fe06ec2cb4f40b0be62f0710c926ab41e82dfbc28e80675b4"},
- {file = "rapidfuzz-3.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc5ceca9c1e1663f3e6c23fb89a311f69b7615a40ddd7645e3435bf3082688a"},
- {file = "rapidfuzz-3.9.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1424e238bc3f20e1759db1e0afb48a988a9ece183724bef91ea2a291c0b92a95"},
- {file = "rapidfuzz-3.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed01378f605aa1f449bee82cd9c83772883120d6483e90aa6c5a4ce95dc5c3aa"},
- {file = "rapidfuzz-3.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb26d412271e5a76cdee1c2d6bf9881310665d3fe43b882d0ed24edfcb891a84"},
- {file = "rapidfuzz-3.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f37e9e1f17be193c41a31c864ad4cd3ebd2b40780db11cd5c04abf2bcf4201b"},
- {file = "rapidfuzz-3.9.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d070ec5cf96b927c4dc5133c598c7ff6db3b833b363b2919b13417f1002560bc"},
- {file = "rapidfuzz-3.9.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:10e61bb7bc807968cef09a0e32ce253711a2d450a4dce7841d21d45330ffdb24"},
- {file = "rapidfuzz-3.9.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:31a2fc60bb2c7face4140010a7aeeafed18b4f9cdfa495cc644a68a8c60d1ff7"},
- {file = "rapidfuzz-3.9.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fbebf1791a71a2e89f5c12b78abddc018354d5859e305ec3372fdae14f80a826"},
- {file = "rapidfuzz-3.9.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:aee9fc9e3bb488d040afc590c0a7904597bf4ccd50d1491c3f4a5e7e67e6cd2c"},
- {file = "rapidfuzz-3.9.4-cp310-cp310-win32.whl", hash = "sha256:005a02688a51c7d2451a2d41c79d737aa326ff54167211b78a383fc2aace2c2c"},
- {file = "rapidfuzz-3.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:3a2e75e41ee3274754d3b2163cc6c82cd95b892a85ab031f57112e09da36455f"},
- {file = "rapidfuzz-3.9.4-cp310-cp310-win_arm64.whl", hash = "sha256:2c99d355f37f2b289e978e761f2f8efeedc2b14f4751d9ff7ee344a9a5ca98d9"},
- {file = "rapidfuzz-3.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:07141aa6099e39d48637ce72a25b893fc1e433c50b3e837c75d8edf99e0c63e1"},
- {file = "rapidfuzz-3.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db1664eaff5d7d0f2542dd9c25d272478deaf2c8412e4ad93770e2e2d828e175"},
- {file = "rapidfuzz-3.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc01a223f6605737bec3202e94dcb1a449b6c76d46082cfc4aa980f2a60fd40e"},
- {file = "rapidfuzz-3.9.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1869c42e73e2a8910b479be204fa736418741b63ea2325f9cc583c30f2ded41a"},
- {file = "rapidfuzz-3.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:62ea7007941fb2795fff305ac858f3521ec694c829d5126e8f52a3e92ae75526"},
- {file = "rapidfuzz-3.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:698e992436bf7f0afc750690c301215a36ff952a6dcd62882ec13b9a1ebf7a39"},
- {file = "rapidfuzz-3.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b76f611935f15a209d3730c360c56b6df8911a9e81e6a38022efbfb96e433bab"},
- {file = "rapidfuzz-3.9.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129627d730db2e11f76169344a032f4e3883d34f20829419916df31d6d1338b1"},
- {file = "rapidfuzz-3.9.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:90a82143c14e9a14b723a118c9ef8d1bbc0c5a16b1ac622a1e6c916caff44dd8"},
- {file = "rapidfuzz-3.9.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ded58612fe3b0e0d06e935eaeaf5a9fd27da8ba9ed3e2596307f40351923bf72"},
- {file = "rapidfuzz-3.9.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f16f5d1c4f02fab18366f2d703391fcdbd87c944ea10736ca1dc3d70d8bd2d8b"},
- {file = "rapidfuzz-3.9.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:26aa7eece23e0df55fb75fbc2a8fb678322e07c77d1fd0e9540496e6e2b5f03e"},
- {file = "rapidfuzz-3.9.4-cp311-cp311-win32.whl", hash = "sha256:f187a9c3b940ce1ee324710626daf72c05599946bd6748abe9e289f1daa9a077"},
- {file = "rapidfuzz-3.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d8e9130fe5d7c9182990b366ad78fd632f744097e753e08ace573877d67c32f8"},
- {file = "rapidfuzz-3.9.4-cp311-cp311-win_arm64.whl", hash = "sha256:40419e98b10cd6a00ce26e4837a67362f658fc3cd7a71bd8bd25c99f7ee8fea5"},
- {file = "rapidfuzz-3.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b5d5072b548db1b313a07d62d88fe0b037bd2783c16607c647e01b070f6cf9e5"},
- {file = "rapidfuzz-3.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cf5bcf22e1f0fd273354462631d443ef78d677f7d2fc292de2aec72ae1473e66"},
- {file = "rapidfuzz-3.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c8fc973adde8ed52810f590410e03fb6f0b541bbaeb04c38d77e63442b2df4c"},
- {file = "rapidfuzz-3.9.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2464bb120f135293e9a712e342c43695d3d83168907df05f8c4ead1612310c7"},
- {file = "rapidfuzz-3.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d9d58689aca22057cf1a5851677b8a3ccc9b535ca008c7ed06dc6e1899f7844"},
- {file = "rapidfuzz-3.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:167e745f98baa0f3034c13583e6302fb69249a01239f1483d68c27abb841e0a1"},
- {file = "rapidfuzz-3.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db0bf0663b4b6da1507869722420ea9356b6195aa907228d6201303e69837af9"},
- {file = "rapidfuzz-3.9.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cd6ac61b74fdb9e23f04d5f068e6cf554f47e77228ca28aa2347a6ca8903972f"},
- {file = "rapidfuzz-3.9.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:60ff67c690acecf381759c16cb06c878328fe2361ddf77b25d0e434ea48a29da"},
- {file = "rapidfuzz-3.9.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:cb934363380c60f3a57d14af94325125cd8cded9822611a9f78220444034e36e"},
- {file = "rapidfuzz-3.9.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fe833493fb5cc5682c823ea3e2f7066b07612ee8f61ecdf03e1268f262106cdd"},
- {file = "rapidfuzz-3.9.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2797fb847d89e04040d281cb1902cbeffbc4b5131a5c53fc0db490fd76b2a547"},
- {file = "rapidfuzz-3.9.4-cp312-cp312-win32.whl", hash = "sha256:52e3d89377744dae68ed7c84ad0ddd3f5e891c82d48d26423b9e066fc835cc7c"},
- {file = "rapidfuzz-3.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:c76da20481c906e08400ee9be230f9e611d5931a33707d9df40337c2655c84b5"},
- {file = "rapidfuzz-3.9.4-cp312-cp312-win_arm64.whl", hash = "sha256:f2d2846f3980445864c7e8b8818a29707fcaff2f0261159ef6b7bd27ba139296"},
- {file = "rapidfuzz-3.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:355fc4a268ffa07bab88d9adee173783ec8d20136059e028d2a9135c623c44e6"},
- {file = "rapidfuzz-3.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4d81a78f90269190b568a8353d4ea86015289c36d7e525cd4d43176c88eff429"},
- {file = "rapidfuzz-3.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e618625ffc4660b26dc8e56225f8b966d5842fa190e70c60db6cd393e25b86e"},
- {file = "rapidfuzz-3.9.4-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b712336ad6f2bacdbc9f1452556e8942269ef71f60a9e6883ef1726b52d9228a"},
- {file = "rapidfuzz-3.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc1ee19fdad05770c897e793836c002344524301501d71ef2e832847425707"},
- {file = "rapidfuzz-3.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1950f8597890c0c707cb7e0416c62a1cf03dcdb0384bc0b2dbda7e05efe738ec"},
- {file = "rapidfuzz-3.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a6c35f272ec9c430568dc8c1c30cb873f6bc96be2c79795e0bce6db4e0e101d"},
- {file = "rapidfuzz-3.9.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:1df0f9e9239132a231c86ae4f545ec2b55409fa44470692fcfb36b1bd00157ad"},
- {file = "rapidfuzz-3.9.4-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:d2c51955329bfccf99ae26f63d5928bf5be9fcfcd9f458f6847fd4b7e2b8986c"},
- {file = "rapidfuzz-3.9.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:3c522f462d9fc504f2ea8d82e44aa580e60566acc754422c829ad75c752fbf8d"},
- {file = "rapidfuzz-3.9.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:d8a52fc50ded60d81117d7647f262c529659fb21d23e14ebfd0b35efa4f1b83d"},
- {file = "rapidfuzz-3.9.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:04dbdfb0f0bfd3f99cf1e9e24fadc6ded2736d7933f32f1151b0f2abb38f9a25"},
- {file = "rapidfuzz-3.9.4-cp38-cp38-win32.whl", hash = "sha256:4968c8bd1df84b42f382549e6226710ad3476f976389839168db3e68fd373298"},
- {file = "rapidfuzz-3.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:3fe4545f89f8d6c27b6bbbabfe40839624873c08bd6700f63ac36970a179f8f5"},
- {file = "rapidfuzz-3.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9f256c8fb8f3125574c8c0c919ab0a1f75d7cba4d053dda2e762dcc36357969d"},
- {file = "rapidfuzz-3.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f5fdc09cf6e9d8eac3ce48a4615b3a3ee332ea84ac9657dbbefef913b13e632f"},
- {file = "rapidfuzz-3.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d395d46b80063d3b5d13c0af43d2c2cedf3ab48c6a0c2aeec715aa5455b0c632"},
- {file = "rapidfuzz-3.9.4-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7fa714fb96ce9e70c37e64c83b62fe8307030081a0bfae74a76fac7ba0f91715"},
- {file = "rapidfuzz-3.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc1a0f29f9119be7a8d3c720f1d2068317ae532e39e4f7f948607c3a6de8396"},
- {file = "rapidfuzz-3.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6022674aa1747d6300f699cd7c54d7dae89bfe1f84556de699c4ac5df0838082"},
- {file = "rapidfuzz-3.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcb72e5f9762fd469701a7e12e94b924af9004954f8c739f925cb19c00862e38"},
- {file = "rapidfuzz-3.9.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ad04ae301129f0eb5b350a333accd375ce155a0c1cec85ab0ec01f770214e2e4"},
- {file = "rapidfuzz-3.9.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f46a22506f17c0433e349f2d1dc11907c393d9b3601b91d4e334fa9a439a6a4d"},
- {file = "rapidfuzz-3.9.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:01b42a8728c36011718da409aa86b84984396bf0ca3bfb6e62624f2014f6022c"},
- {file = "rapidfuzz-3.9.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:e590d5d5443cf56f83a51d3c4867bd1f6be8ef8cfcc44279522bcef3845b2a51"},
- {file = "rapidfuzz-3.9.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4c72078b5fdce34ba5753f9299ae304e282420e6455e043ad08e4488ca13a2b0"},
- {file = "rapidfuzz-3.9.4-cp39-cp39-win32.whl", hash = "sha256:f75639277304e9b75e6a7b3c07042d2264e16740a11e449645689ed28e9c2124"},
- {file = "rapidfuzz-3.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:e81e27e8c32a1e1278a4bb1ce31401bfaa8c2cc697a053b985a6f8d013df83ec"},
- {file = "rapidfuzz-3.9.4-cp39-cp39-win_arm64.whl", hash = "sha256:15bc397ee9a3ed1210b629b9f5f1da809244adc51ce620c504138c6e7095b7bd"},
- {file = "rapidfuzz-3.9.4-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:20488ade4e1ddba3cfad04f400da7a9c1b91eff5b7bd3d1c50b385d78b587f4f"},
- {file = "rapidfuzz-3.9.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:e61b03509b1a6eb31bc5582694f6df837d340535da7eba7bedb8ae42a2fcd0b9"},
- {file = "rapidfuzz-3.9.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:098d231d4e51644d421a641f4a5f2f151f856f53c252b03516e01389b2bfef99"},
- {file = "rapidfuzz-3.9.4-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:17ab8b7d10fde8dd763ad428aa961c0f30a1b44426e675186af8903b5d134fb0"},
- {file = "rapidfuzz-3.9.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e272df61bee0a056a3daf99f9b1bd82cf73ace7d668894788139c868fdf37d6f"},
- {file = "rapidfuzz-3.9.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d6481e099ff8c4edda85b8b9b5174c200540fd23c8f38120016c765a86fa01f5"},
- {file = "rapidfuzz-3.9.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ad61676e9bdae677d577fe80ec1c2cea1d150c86be647e652551dcfe505b1113"},
- {file = "rapidfuzz-3.9.4-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:af65020c0dd48d0d8ae405e7e69b9d8ae306eb9b6249ca8bf511a13f465fad85"},
- {file = "rapidfuzz-3.9.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d38b4e026fcd580e0bda6c0ae941e0e9a52c6bc66cdce0b8b0da61e1959f5f8"},
- {file = "rapidfuzz-3.9.4-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f74ed072c2b9dc6743fb19994319d443a4330b0e64aeba0aa9105406c7c5b9c2"},
- {file = "rapidfuzz-3.9.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aee5f6b8321f90615c184bd8a4c676e9becda69b8e4e451a90923db719d6857c"},
- {file = "rapidfuzz-3.9.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3a555e3c841d6efa350f862204bb0a3fea0c006b8acc9b152b374fa36518a1c6"},
- {file = "rapidfuzz-3.9.4-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0772150d37bf018110351c01d032bf9ab25127b966a29830faa8ad69b7e2f651"},
- {file = "rapidfuzz-3.9.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:addcdd3c3deef1bd54075bd7aba0a6ea9f1d01764a08620074b7a7b1e5447cb9"},
- {file = "rapidfuzz-3.9.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fe86b82b776554add8f900b6af202b74eb5efe8f25acdb8680a5c977608727f"},
- {file = "rapidfuzz-3.9.4-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0fc91ac59f4414d8542454dfd6287a154b8e6f1256718c898f695bdbb993467"},
- {file = "rapidfuzz-3.9.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a944e546a296a5fdcaabb537b01459f1b14d66f74e584cb2a91448bffadc3c1"},
- {file = "rapidfuzz-3.9.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4fb96ba96d58c668a17a06b5b5e8340fedc26188e87b0d229d38104556f30cd8"},
- {file = "rapidfuzz-3.9.4.tar.gz", hash = "sha256:366bf8947b84e37f2f4cf31aaf5f37c39f620d8c0eddb8b633e6ba0129ca4a0a"},
+ {file = "rapidfuzz-3.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f17d9f21bf2f2f785d74f7b0d407805468b4c173fa3e52c86ec94436b338e74a"},
+ {file = "rapidfuzz-3.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b31f358a70efc143909fb3d75ac6cd3c139cd41339aa8f2a3a0ead8315731f2b"},
+ {file = "rapidfuzz-3.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f4f43f2204b56a61448ec2dd061e26fd344c404da99fb19f3458200c5874ba2"},
+ {file = "rapidfuzz-3.10.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9d81bf186a453a2757472133b24915768abc7c3964194406ed93e170e16c21cb"},
+ {file = "rapidfuzz-3.10.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3611c8f45379a12063d70075c75134f2a8bd2e4e9b8a7995112ddae95ca1c982"},
+ {file = "rapidfuzz-3.10.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3c3b537b97ac30da4b73930fa8a4fe2f79c6d1c10ad535c5c09726612cd6bed9"},
+ {file = "rapidfuzz-3.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:231ef1ec9cf7b59809ce3301006500b9d564ddb324635f4ea8f16b3e2a1780da"},
+ {file = "rapidfuzz-3.10.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed4f3adc1294834955b7e74edd3c6bd1aad5831c007f2d91ea839e76461a5879"},
+ {file = "rapidfuzz-3.10.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:7b6015da2e707bf632a71772a2dbf0703cff6525732c005ad24987fe86e8ec32"},
+ {file = "rapidfuzz-3.10.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1b35a118d61d6f008e8e3fb3a77674d10806a8972c7b8be433d6598df4d60b01"},
+ {file = "rapidfuzz-3.10.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bc308d79a7e877226f36bdf4e149e3ed398d8277c140be5c1fd892ec41739e6d"},
+ {file = "rapidfuzz-3.10.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f017dbfecc172e2d0c37cf9e3d519179d71a7f16094b57430dffc496a098aa17"},
+ {file = "rapidfuzz-3.10.1-cp310-cp310-win32.whl", hash = "sha256:36c0e1483e21f918d0f2f26799fe5ac91c7b0c34220b73007301c4f831a9c4c7"},
+ {file = "rapidfuzz-3.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:10746c1d4c8cd8881c28a87fd7ba0c9c102346dfe7ff1b0d021cdf093e9adbff"},
+ {file = "rapidfuzz-3.10.1-cp310-cp310-win_arm64.whl", hash = "sha256:dfa64b89dcb906835e275187569e51aa9d546a444489e97aaf2cc84011565fbe"},
+ {file = "rapidfuzz-3.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:92958ae075c87fef393f835ed02d4fe8d5ee2059a0934c6c447ea3417dfbf0e8"},
+ {file = "rapidfuzz-3.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ba7521e072c53e33c384e78615d0718e645cab3c366ecd3cc8cb732befd94967"},
+ {file = "rapidfuzz-3.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00d02cbd75d283c287471b5b3738b3e05c9096150f93f2d2dfa10b3d700f2db9"},
+ {file = "rapidfuzz-3.10.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:efa1582a397da038e2f2576c9cd49b842f56fde37d84a6b0200ffebc08d82350"},
+ {file = "rapidfuzz-3.10.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f12912acee1f506f974f58de9fdc2e62eea5667377a7e9156de53241c05fdba8"},
+ {file = "rapidfuzz-3.10.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666d5d8b17becc3f53447bcb2b6b33ce6c2df78792495d1fa82b2924cd48701a"},
+ {file = "rapidfuzz-3.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26f71582c0d62445067ee338ddad99b655a8f4e4ed517a90dcbfbb7d19310474"},
+ {file = "rapidfuzz-3.10.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8a2ef08b27167bcff230ffbfeedd4c4fa6353563d6aaa015d725dd3632fc3de7"},
+ {file = "rapidfuzz-3.10.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:365e4fc1a2b95082c890f5e98489b894e6bf8c338c6ac89bb6523c2ca6e9f086"},
+ {file = "rapidfuzz-3.10.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1996feb7a61609fa842e6b5e0c549983222ffdedaf29644cc67e479902846dfe"},
+ {file = "rapidfuzz-3.10.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:cf654702f144beaa093103841a2ea6910d617d0bb3fccb1d1fd63c54dde2cd49"},
+ {file = "rapidfuzz-3.10.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec108bf25de674781d0a9a935030ba090c78d49def3d60f8724f3fc1e8e75024"},
+ {file = "rapidfuzz-3.10.1-cp311-cp311-win32.whl", hash = "sha256:031f8b367e5d92f7a1e27f7322012f3c321c3110137b43cc3bf678505583ef48"},
+ {file = "rapidfuzz-3.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:f98f36c6a1bb9a6c8bbec99ad87c8c0e364f34761739b5ea9adf7b48129ae8cf"},
+ {file = "rapidfuzz-3.10.1-cp311-cp311-win_arm64.whl", hash = "sha256:f1da2028cb4e41be55ee797a82d6c1cf589442504244249dfeb32efc608edee7"},
+ {file = "rapidfuzz-3.10.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1340b56340896bede246f612b6ecf685f661a56aabef3d2512481bfe23ac5835"},
+ {file = "rapidfuzz-3.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2316515169b7b5a453f0ce3adbc46c42aa332cae9f2edb668e24d1fc92b2f2bb"},
+ {file = "rapidfuzz-3.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e06fe6a12241ec1b72c0566c6b28cda714d61965d86569595ad24793d1ab259"},
+ {file = "rapidfuzz-3.10.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d99c1cd9443b19164ec185a7d752f4b4db19c066c136f028991a480720472e23"},
+ {file = "rapidfuzz-3.10.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1d9aa156ed52d3446388ba4c2f335e312191d1ca9d1f5762ee983cf23e4ecf6"},
+ {file = "rapidfuzz-3.10.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54bcf4efaaee8e015822be0c2c28214815f4f6b4f70d8362cfecbd58a71188ac"},
+ {file = "rapidfuzz-3.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0c955e32afdbfdf6e9ee663d24afb25210152d98c26d22d399712d29a9b976b"},
+ {file = "rapidfuzz-3.10.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:191633722203f5b7717efcb73a14f76f3b124877d0608c070b827c5226d0b972"},
+ {file = "rapidfuzz-3.10.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:195baad28057ec9609e40385991004e470af9ef87401e24ebe72c064431524ab"},
+ {file = "rapidfuzz-3.10.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0fff4a6b87c07366662b62ae994ffbeadc472e72f725923f94b72a3db49f4671"},
+ {file = "rapidfuzz-3.10.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4ffed25f9fdc0b287f30a98467493d1e1ce5b583f6317f70ec0263b3c97dbba6"},
+ {file = "rapidfuzz-3.10.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d02cf8e5af89a9ac8f53c438ddff6d773f62c25c6619b29db96f4aae248177c0"},
+ {file = "rapidfuzz-3.10.1-cp312-cp312-win32.whl", hash = "sha256:f3bb81d4fe6a5d20650f8c0afcc8f6e1941f6fecdb434f11b874c42467baded0"},
+ {file = "rapidfuzz-3.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:aaf83e9170cb1338922ae42d320699dccbbdca8ffed07faeb0b9257822c26e24"},
+ {file = "rapidfuzz-3.10.1-cp312-cp312-win_arm64.whl", hash = "sha256:c5da802a0d085ad81b0f62828fb55557996c497b2d0b551bbdfeafd6d447892f"},
+ {file = "rapidfuzz-3.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fc22d69a1c9cccd560a5c434c0371b2df0f47c309c635a01a913e03bbf183710"},
+ {file = "rapidfuzz-3.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38b0dac2c8e057562b8f0d8ae5b663d2d6a28c5ab624de5b73cef9abb6129a24"},
+ {file = "rapidfuzz-3.10.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fde3bbb14e92ce8fcb5c2edfff72e474d0080cadda1c97785bf4822f037a309"},
+ {file = "rapidfuzz-3.10.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9141fb0592e55f98fe9ac0f3ce883199b9c13e262e0bf40c5b18cdf926109d16"},
+ {file = "rapidfuzz-3.10.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:237bec5dd1bfc9b40bbd786cd27949ef0c0eb5fab5eb491904c6b5df59d39d3c"},
+ {file = "rapidfuzz-3.10.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18123168cba156ab5794ea6de66db50f21bb3c66ae748d03316e71b27d907b95"},
+ {file = "rapidfuzz-3.10.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b75fe506c8e02769cc47f5ab21ce3e09b6211d3edaa8f8f27331cb6988779be"},
+ {file = "rapidfuzz-3.10.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9da82aa4b46973aaf9e03bb4c3d6977004648c8638febfc0f9d237e865761270"},
+ {file = "rapidfuzz-3.10.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c34c022d5ad564f1a5a57a4a89793bd70d7bad428150fb8ff2760b223407cdcf"},
+ {file = "rapidfuzz-3.10.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1e96c84d6c2a0ca94e15acb5399118fff669f4306beb98a6d8ec6f5dccab4412"},
+ {file = "rapidfuzz-3.10.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e8e154b84a311263e1aca86818c962e1fa9eefdd643d1d5d197fcd2738f88cb9"},
+ {file = "rapidfuzz-3.10.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:335fee93188f8cd585552bb8057228ce0111bd227fa81bfd40b7df6b75def8ab"},
+ {file = "rapidfuzz-3.10.1-cp313-cp313-win32.whl", hash = "sha256:6729b856166a9e95c278410f73683957ea6100c8a9d0a8dbe434c49663689255"},
+ {file = "rapidfuzz-3.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:0e06d99ad1ad97cb2ef7f51ec6b1fedd74a3a700e4949353871cf331d07b382a"},
+ {file = "rapidfuzz-3.10.1-cp313-cp313-win_arm64.whl", hash = "sha256:8d1b7082104d596a3eb012e0549b2634ed15015b569f48879701e9d8db959dbb"},
+ {file = "rapidfuzz-3.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:779027d3307e1a2b1dc0c03c34df87a470a368a1a0840a9d2908baf2d4067956"},
+ {file = "rapidfuzz-3.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:440b5608ab12650d0390128d6858bc839ae77ffe5edf0b33a1551f2fa9860651"},
+ {file = "rapidfuzz-3.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cac41a411e07a6f3dc80dfbd33f6be70ea0abd72e99c59310819d09f07d945"},
+ {file = "rapidfuzz-3.10.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:958473c9f0bca250590200fd520b75be0dbdbc4a7327dc87a55b6d7dc8d68552"},
+ {file = "rapidfuzz-3.10.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ef60dfa73749ef91cb6073be1a3e135f4846ec809cc115f3cbfc6fe283a5584"},
+ {file = "rapidfuzz-3.10.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7fbac18f2c19fc983838a60611e67e3262e36859994c26f2ee85bb268de2355"},
+ {file = "rapidfuzz-3.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a0d519ff39db887cd73f4e297922786d548f5c05d6b51f4e6754f452a7f4296"},
+ {file = "rapidfuzz-3.10.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bebb7bc6aeb91cc57e4881b222484c26759ca865794187217c9dcea6c33adae6"},
+ {file = "rapidfuzz-3.10.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fe07f8b9c3bb5c5ad1d2c66884253e03800f4189a60eb6acd6119ebaf3eb9894"},
+ {file = "rapidfuzz-3.10.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:bfa48a4a2d45a41457f0840c48e579db157a927f4e97acf6e20df8fc521c79de"},
+ {file = "rapidfuzz-3.10.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2cf44d01bfe8ee605b7eaeecbc2b9ca64fc55765f17b304b40ed8995f69d7716"},
+ {file = "rapidfuzz-3.10.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e6bbca9246d9eedaa1c84e04a7f555493ba324d52ae4d9f3d9ddd1b740dcd87"},
+ {file = "rapidfuzz-3.10.1-cp39-cp39-win32.whl", hash = "sha256:567f88180f2c1423b4fe3f3ad6e6310fc97b85bdba574801548597287fc07028"},
+ {file = "rapidfuzz-3.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:6b2cd7c29d6ecdf0b780deb587198f13213ac01c430ada6913452fd0c40190fc"},
+ {file = "rapidfuzz-3.10.1-cp39-cp39-win_arm64.whl", hash = "sha256:9f912d459e46607ce276128f52bea21ebc3e9a5ccf4cccfef30dd5bddcf47be8"},
+ {file = "rapidfuzz-3.10.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ac4452f182243cfab30ba4668ef2de101effaedc30f9faabb06a095a8c90fd16"},
+ {file = "rapidfuzz-3.10.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:565c2bd4f7d23c32834652b27b51dd711814ab614b4e12add8476be4e20d1cf5"},
+ {file = "rapidfuzz-3.10.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:187d9747149321607be4ccd6f9f366730078bed806178ec3eeb31d05545e9e8f"},
+ {file = "rapidfuzz-3.10.1-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:616290fb9a8fa87e48cb0326d26f98d4e29f17c3b762c2d586f2b35c1fd2034b"},
+ {file = "rapidfuzz-3.10.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:073a5b107e17ebd264198b78614c0206fa438cce749692af5bc5f8f484883f50"},
+ {file = "rapidfuzz-3.10.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:39c4983e2e2ccb9732f3ac7d81617088822f4a12291d416b09b8a1eadebb3e29"},
+ {file = "rapidfuzz-3.10.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ac7adee6bcf0c6fee495d877edad1540a7e0f5fc208da03ccb64734b43522d7a"},
+ {file = "rapidfuzz-3.10.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:425f4ac80b22153d391ee3f94bc854668a0c6c129f05cf2eaf5ee74474ddb69e"},
+ {file = "rapidfuzz-3.10.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65a2fa13e8a219f9b5dcb9e74abe3ced5838a7327e629f426d333dfc8c5a6e66"},
+ {file = "rapidfuzz-3.10.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:75561f3df9a906aaa23787e9992b228b1ab69007932dc42070f747103e177ba8"},
+ {file = "rapidfuzz-3.10.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:edd062490537e97ca125bc6c7f2b7331c2b73d21dc304615afe61ad1691e15d5"},
+ {file = "rapidfuzz-3.10.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfcc8feccf63245a22dfdd16e222f1a39771a44b870beb748117a0e09cbb4a62"},
+ {file = "rapidfuzz-3.10.1.tar.gz", hash = "sha256:5a15546d847a915b3f42dc79ef9b0c78b998b4e2c53b252e7166284066585979"},
]
[package.extras]
-full = ["numpy"]
+all = ["numpy"]
[[package]]
name = "requests"
diff --git a/src/poetry/installation/installer.py b/src/poetry/installation/installer.py
index eff9aeffdd5..8a243e15b45 100644
--- a/src/poetry/installation/installer.py
+++ b/src/poetry/installation/installer.py
@@ -305,6 +305,7 @@ def _do_install(self) -> int:
self._installed_repository.packages,
locked_repository.packages,
NullIO(),
+ active_root_extras=self._extras,
)
# Everything is resolved at this point, so we no longer need
# to load deferred dependencies (i.e. VCS, URL and path dependencies)
diff --git a/src/poetry/puzzle/provider.py b/src/poetry/puzzle/provider.py
index 9bae818bc56..b3b29e5f6c6 100644
--- a/src/poetry/puzzle/provider.py
+++ b/src/poetry/puzzle/provider.py
@@ -8,6 +8,7 @@
from collections import defaultdict
from contextlib import contextmanager
from typing import TYPE_CHECKING
+from typing import Any
from typing import ClassVar
from typing import cast
@@ -17,6 +18,7 @@
from poetry.core.constraints.version import VersionRange
from poetry.core.packages.utils.utils import get_python_constraint_from_marker
from poetry.core.version.markers import AnyMarker
+from poetry.core.version.markers import parse_marker
from poetry.core.version.markers import union as marker_union
from poetry.mixology.incompatibility import Incompatibility
@@ -115,6 +117,7 @@ def __init__(
io: IO,
*,
locked: list[Package] | None = None,
+ active_root_extras: Collection[NormalizedName] | None = None,
) -> None:
self._package = package
self._pool = pool
@@ -130,6 +133,9 @@ def __init__(
self._direct_origin_packages: dict[str, Package] = {}
self._locked: dict[NormalizedName, list[DependencyPackage]] = defaultdict(list)
self._use_latest: Collection[NormalizedName] = []
+ self._active_root_extras = (
+ frozenset(active_root_extras) if active_root_extras is not None else None
+ )
self._explicit_sources: dict[str, str] = {}
for package in locked or []:
@@ -416,21 +422,12 @@ def incompatibilities_for(
)
]
- _dependencies = [
- dep
- for dep in dependencies
- if dep.name not in self.UNSAFE_PACKAGES
- and self._python_constraint.allows_any(dep.python_constraint)
- and (not self._env or dep.marker.validate(self._env.marker_env))
- ]
- dependencies = self._get_dependencies_with_overrides(_dependencies, package)
-
return [
Incompatibility(
[Term(package.to_dependency(), True), Term(dep, False)],
DependencyCauseError(),
)
- for dep in dependencies
+ for dep in self._get_dependencies_with_overrides(dependencies, package)
]
def complete_package(
@@ -480,7 +477,7 @@ def complete_package(
package = dependency_package.package
dependency = dependency_package.dependency
new_dependency = package.without_features().to_dependency()
- new_dependency.marker = AnyMarker()
+ new_dependency.marker = dependency.marker
# When adding dependency foo[extra] -> foo, preserve foo's source, if it's
# specified. This prevents us from trying to get foo from PyPI
@@ -497,8 +494,14 @@ def complete_package(
if dep.name in self.UNSAFE_PACKAGES:
continue
- if self._env and not dep.marker.validate(self._env.marker_env):
- continue
+ if self._env:
+ marker_values = (
+ self._marker_values(self._active_root_extras)
+ if package.is_root()
+ else self._env.marker_env
+ )
+ if not dep.marker.validate(marker_values):
+ continue
if not package.is_root() and (
(dep.is_optional() and dep.name not in optional_dependencies)
@@ -509,6 +512,24 @@ def complete_package(
):
continue
+ # For normal dependency resolution, we have to make sure that root extras
+ # are represented in the markers. This is required to identify mutually
+ # exclusive markers in cases like 'extra == "foo"' and 'extra != "foo"'.
+ # However, for installation with re-resolving (installer.re-resolve=true,
+ # which results in self._env being not None), this spoils the result
+ # because we have to keep extras so that they are uninstalled
+ # when calculating the operations of the transaction.
+ if self._env is None and package.is_root() and dep.in_extras:
+ # The clone is required for installation with re-resolving
+ # without an existing lock file because the root package is used
+ # once for solving and a second time for re-resolving for installation.
+ dep = dep.clone()
+ dep.marker = dep.marker.intersect(
+ parse_marker(
+ " or ".join(f'extra == "{extra}"' for extra in dep.in_extras)
+ )
+ )
+
_dependencies.append(dep)
if self._load_deferred:
@@ -545,7 +566,7 @@ def complete_package(
# • pypiwin32 (219); sys_platform == "win32" and python_version < "3.6"
duplicates: dict[str, list[Dependency]] = defaultdict(list)
for dep in dependencies:
- duplicates[dep.complete_name].append(dep)
+ duplicates[dep.name].append(dep)
dependencies = []
for dep_name, deps in duplicates.items():
@@ -556,9 +577,39 @@ def complete_package(
self.debug(f"Duplicate dependencies for {dep_name}")
# For dependency resolution, markers of duplicate dependencies must be
- # mutually exclusive.
- active_extras = None if package.is_root() else dependency.extras
- deps = self._resolve_overlapping_markers(package, deps, active_extras)
+ # mutually exclusive. However, we have to take care about duplicates
+ # with differing extras.
+ duplicates_by_extras: dict[str, list[Dependency]] = defaultdict(list)
+ for dep in deps:
+ duplicates_by_extras[dep.complete_name].append(dep)
+
+ if len(duplicates_by_extras) == 1:
+ active_extras = (
+ self._active_root_extras if package.is_root() else dependency.extras
+ )
+ deps = self._resolve_overlapping_markers(package, deps, active_extras)
+ else:
+ # There are duplicates with different extras.
+ for complete_dep_name, deps_by_extra in duplicates_by_extras.items():
+ if len(deps_by_extra) > 1:
+ duplicates_by_extras[complete_dep_name] = (
+ self._resolve_overlapping_markers(package, deps, None)
+ )
+ if all(len(d) == 1 for d in duplicates_by_extras.values()) and all(
+ d1[0].marker.intersect(d2[0].marker).is_empty()
+ for d1, d2 in itertools.combinations(
+ duplicates_by_extras.values(), 2
+ )
+ ):
+ # Since all markers are mutually exclusive,
+ # we can trigger overrides.
+ deps = list(itertools.chain(*duplicates_by_extras.values()))
+ else:
+ # Too complicated to handle with overrides,
+ # fallback to basic handling without overrides.
+ for d in duplicates_by_extras.values():
+ dependencies.extend(d)
+ continue
if len(deps) == 1:
self.debug(f"Merging requirements for {dep_name}")
@@ -909,3 +960,19 @@ def _resolve_overlapping_markers(
# dependencies by constraint again. After overlapping markers were
# resolved, there might be new dependencies with the same constraint.
return self._merge_dependencies_by_constraint(new_dependencies)
+
+ def _marker_values(
+ self, extras: Collection[NormalizedName] | None = None
+ ) -> dict[str, Any]:
+ """
+ Marker values, from `self._env` if present plus the supplied extras
+
+ :param extras: the values to add to the 'extra' marker value
+ """
+ result = self._env.marker_env.copy() if self._env is not None else {}
+ if extras is not None:
+ assert (
+ "extra" not in result
+ ), "'extra' marker key is already present in environment"
+ result["extra"] = set(extras)
+ return result
diff --git a/src/poetry/puzzle/solver.py b/src/poetry/puzzle/solver.py
index e32d000df98..234518fc52e 100644
--- a/src/poetry/puzzle/solver.py
+++ b/src/poetry/puzzle/solver.py
@@ -49,6 +49,7 @@ def __init__(
installed: list[Package],
locked: list[Package],
io: IO,
+ active_root_extras: Collection[NormalizedName] | None = None,
) -> None:
self._package = package
self._pool = pool
@@ -56,7 +57,13 @@ def __init__(
self._locked_packages = locked
self._io = io
- self._provider = Provider(self._package, self._pool, self._io, locked=locked)
+ self._provider = Provider(
+ self._package,
+ self._pool,
+ self._io,
+ locked=locked,
+ active_root_extras=active_root_extras,
+ )
self._overrides: list[dict[Package, dict[str, Dependency]]] = []
@property
diff --git a/src/poetry/puzzle/transaction.py b/src/poetry/puzzle/transaction.py
index 05a9432818f..338e43d8b5c 100644
--- a/src/poetry/puzzle/transaction.py
+++ b/src/poetry/puzzle/transaction.py
@@ -78,7 +78,7 @@ def calculate_operations(
else:
priorities = defaultdict(int)
relevant_result_packages: set[NormalizedName] = set()
- uninstalls: set[NormalizedName] = set()
+ pending_extra_uninstalls: list[Package] = [] # list for deterministic order
for result_package in self._result_packages:
is_unsolicited_extra = False
if self._marker_env:
@@ -95,11 +95,12 @@ def calculate_operations(
else:
continue
else:
- relevant_result_packages.add(result_package.name)
is_unsolicited_extra = extras is not None and (
result_package.optional
and result_package.name not in extra_packages
)
+ if not is_unsolicited_extra:
+ relevant_result_packages.add(result_package.name)
installed = False
for installed_package in self._installed_packages:
@@ -108,9 +109,7 @@ def calculate_operations(
# Extras that were not requested are always uninstalled.
if is_unsolicited_extra:
- uninstalls.add(installed_package.name)
- if installed_package.name not in system_site_packages:
- operations.append(Uninstall(installed_package))
+ pending_extra_uninstalls.append(installed_package)
# We have to perform an update if the version or another
# attribute of the package has changed (source type, url, ref, ...).
@@ -153,6 +152,13 @@ def calculate_operations(
op.skip("Not required")
operations.append(op)
+ uninstalls: set[NormalizedName] = set()
+ for package in pending_extra_uninstalls:
+ if package.name not in (relevant_result_packages | uninstalls):
+ uninstalls.add(package.name)
+ if package.name not in system_site_packages:
+ operations.append(Uninstall(package))
+
if with_uninstalls:
for current_package in self._current_packages:
found = current_package.name in (relevant_result_packages | uninstalls)
diff --git a/tests/installation/fixtures/with-conflicting-dependency-extras-root.test b/tests/installation/fixtures/with-conflicting-dependency-extras-root.test
new file mode 100644
index 00000000000..c99c37a5694
--- /dev/null
+++ b/tests/installation/fixtures/with-conflicting-dependency-extras-root.test
@@ -0,0 +1,28 @@
+[[package]]
+name = "conflicting-dep"
+version = "1.1.0"
+description = ""
+optional = true
+python-versions = "*"
+files = [ ]
+groups = [ "main" ]
+markers = "extra == \"extra-one\" and extra != \"extra-two\""
+
+[[package]]
+name = "conflicting-dep"
+version = "1.2.0"
+description = ""
+optional = true
+python-versions = "*"
+files = [ ]
+groups = [ "main" ]
+markers = "extra != \"extra-one\" and extra == \"extra-two\""
+
+[extras]
+extra-one = [ "conflicting-dep", "conflicting-dep" ]
+extra-two = [ "conflicting-dep", "conflicting-dep" ]
+
+[metadata]
+lock-version = "2.1"
+python-versions = "*"
+content-hash = "123456789"
diff --git a/tests/installation/fixtures/with-conflicting-dependency-extras-transitive.test b/tests/installation/fixtures/with-conflicting-dependency-extras-transitive.test
new file mode 100644
index 00000000000..5e6f11ab7f9
--- /dev/null
+++ b/tests/installation/fixtures/with-conflicting-dependency-extras-transitive.test
@@ -0,0 +1,52 @@
+[[package]]
+name = "conflicting-dep"
+version = "1.1.0"
+description = ""
+optional = true
+python-versions = "*"
+files = [ ]
+groups = [ "main" ]
+markers = "extra == \"root-extra-one\" and extra != \"root-extra-two\""
+
+[[package]]
+name = "conflicting-dep"
+version = "1.2.0"
+description = ""
+optional = true
+python-versions = "*"
+files = [ ]
+groups = [ "main" ]
+markers = "extra != \"root-extra-one\" and extra == \"root-extra-two\""
+
+[[package]]
+name = "intermediate-dep"
+version = "1.0.0"
+description = ""
+optional = true
+python-versions = "*"
+files = [ ]
+groups = [ "main" ]
+markers = "extra == \"root-extra-one\" and extra != \"root-extra-two\" or extra == \"root-extra-two\" and extra != \"root-extra-one\""
+
+[[package.dependencies.conflicting-dep]]
+version = "1.1.0"
+optional = true
+markers = 'extra == "extra-one" and extra != "extra-two"'
+
+[[package.dependencies.conflicting-dep]]
+version = "1.2.0"
+optional = true
+markers = 'extra != "extra-one" and extra == "extra-two"'
+
+ [package.extras]
+ extra-one = [ "conflicting-dep (==1.1.0)", "conflicting-dep (==1.2.0)" ]
+ extra-two = [ "conflicting-dep (==1.1.0)", "conflicting-dep (==1.2.0)" ]
+
+[extras]
+root-extra-one = [ "intermediate-dep", "intermediate-dep" ]
+root-extra-two = [ "intermediate-dep", "intermediate-dep" ]
+
+[metadata]
+lock-version = "2.1"
+python-versions = "*"
+content-hash = "123456789"
diff --git a/tests/installation/fixtures/with-dependencies-differing-extras.test b/tests/installation/fixtures/with-dependencies-differing-extras.test
new file mode 100644
index 00000000000..7dcaacf26ae
--- /dev/null
+++ b/tests/installation/fixtures/with-dependencies-differing-extras.test
@@ -0,0 +1,52 @@
+[[package]]
+name = "demo"
+version = "1.0.0"
+description = ""
+optional = true
+python-versions = "*"
+files = [ ]
+groups = [ "main" ]
+markers = "extra == \"extra-one\" and extra != \"extra-two\" or extra == \"extra-two\" and extra != \"extra-one\""
+
+[package.dependencies.transitive-dep-one]
+version = "1.1.0"
+optional = true
+markers = 'extra == "demo-extra-one" and extra != "demo-extra-two"'
+
+[package.dependencies.transitive-dep-two]
+version = "1.2.0"
+optional = true
+markers = 'extra != "demo-extra-one" and extra == "demo-extra-two"'
+
+ [package.extras]
+ demo-extra-one = [ "transitive-dep-one", "transitive-dep-two" ]
+ demo-extra-two = [ "transitive-dep-one", "transitive-dep-two" ]
+
+[[package]]
+name = "transitive-dep-one"
+version = "1.1.0"
+description = ""
+optional = true
+python-versions = "*"
+files = [ ]
+groups = [ "main" ]
+markers = "extra == \"extra-one\" and extra != \"extra-two\""
+
+[[package]]
+name = "transitive-dep-two"
+version = "1.2.0"
+description = ""
+optional = true
+python-versions = "*"
+files = [ ]
+groups = [ "main" ]
+markers = "extra != \"extra-one\" and extra == \"extra-two\""
+
+[extras]
+extra-one = [ "demo", "demo" ]
+extra-two = [ "demo", "demo" ]
+
+[metadata]
+lock-version = "2.1"
+python-versions = "*"
+content-hash = "123456789"
diff --git a/tests/installation/fixtures/with-exclusive-extras.test b/tests/installation/fixtures/with-exclusive-extras.test
new file mode 100644
index 00000000000..c4764e4a8af
--- /dev/null
+++ b/tests/installation/fixtures/with-exclusive-extras.test
@@ -0,0 +1,38 @@
+[[package]]
+name = "torch"
+version = "1.11.0+cpu"
+description = ""
+optional = true
+python-versions = "*"
+files = []
+groups = [ "main" ]
+markers = "extra == \"cpu\" and extra != \"cuda\""
+
+[package.source]
+reference = "pytorch-cpu"
+type = "legacy"
+url = "https://download.pytorch.org/whl/cpu"
+
+[[package]]
+name = "torch"
+version = "1.11.0+cuda"
+description = ""
+optional = true
+python-versions = "*"
+files = []
+groups = [ "main" ]
+markers = "extra != \"cpu\" and extra == \"cuda\""
+
+[package.source]
+reference = "pytorch-cuda"
+type = "legacy"
+url = "https://download.pytorch.org/whl/cuda"
+
+[extras]
+cpu = ["torch", "torch"]
+cuda = ["torch", "torch"]
+
+[metadata]
+python-versions = "*"
+lock-version = "2.1"
+content-hash = "123456789"
diff --git a/tests/installation/test_installer.py b/tests/installation/test_installer.py
index f4b81757c82..a8e3b726c36 100644
--- a/tests/installation/test_installer.py
+++ b/tests/installation/test_installer.py
@@ -1077,6 +1077,358 @@ def test_run_with_dependencies_nested_extras(
assert locker.written_data == expected
+@pytest.mark.parametrize("root", [True, False])
+@pytest.mark.parametrize("locked", [False, True])
+@pytest.mark.parametrize("extra", [None, "extra-one", "extra-two"])
+def test_run_with_conflicting_dependency_extras(
+ installer: Installer,
+ pool: RepositoryPool,
+ locker: Locker,
+ installed: CustomInstalledRepository,
+ repo: Repository,
+ config: Config,
+ package: ProjectPackage,
+ extra: str | None,
+ locked: bool,
+ root: bool,
+) -> None:
+ """
+ - https://github.com/python-poetry/poetry/issues/6419
+
+ Tests resolution of extras with conflicting dependencies. Tests in both as direct dependencies of
+ root package and as transitive dependencies.
+ """
+ # A package with two optional dependencies, one for each extra
+ # If root, this is the root package, otherwise an intermediate package
+ main_package = package if root else get_package("intermediate-dep", "1.0.0")
+
+ # Two conflicting versions of a dependency, one in each extra
+ conflicting_dep_one_pkg = get_package("conflicting-dep", "1.1.0")
+ conflicting_dep_two_pkg = get_package("conflicting-dep", "1.2.0")
+
+ conflicting_dep_one = Factory.create_dependency(
+ "conflicting-dep",
+ {
+ "version": "1.1.0",
+ "markers": "extra == 'extra-one' and extra != 'extra-two'",
+ "optional": True,
+ },
+ )
+ conflicting_dep_two = Factory.create_dependency(
+ "conflicting-dep",
+ {
+ "version": "1.2.0",
+ "markers": "extra != 'extra-one' and extra == 'extra-two'",
+ "optional": True,
+ },
+ )
+
+ # Include both just for extra validation that our marker validation works as expected
+ main_package.extras = {
+ canonicalize_name("extra-one"): [conflicting_dep_one, conflicting_dep_two],
+ canonicalize_name("extra-two"): [conflicting_dep_one, conflicting_dep_two],
+ }
+ main_package.add_dependency(conflicting_dep_one)
+ main_package.add_dependency(conflicting_dep_two)
+
+ repo.add_package(conflicting_dep_one_pkg)
+ repo.add_package(conflicting_dep_two_pkg)
+ if not root:
+ repo.add_package(main_package)
+
+ # If we have an intermediate package, add extras to our root package
+ if not root:
+ extra_one_dep = Factory.create_dependency(
+ "intermediate-dep",
+ {
+ "version": "1.0.0",
+ "markers": "extra == 'root-extra-one' and extra != 'root-extra-two'",
+ "extras": ["extra-one"],
+ "optional": True,
+ },
+ )
+ extra_two_dep = Factory.create_dependency(
+ "intermediate-dep",
+ {
+ "version": "1.0.0",
+ "markers": "extra != 'root-extra-one' and extra == 'root-extra-two'",
+ "extras": ["extra-two"],
+ "optional": True,
+ },
+ )
+ package.add_dependency(extra_one_dep)
+ package.add_dependency(extra_two_dep)
+ # Include both just for extra validation that our marker validation works as expected
+ package.extras = {
+ canonicalize_name("root-extra-one"): [extra_one_dep, extra_two_dep],
+ canonicalize_name("root-extra-two"): [extra_one_dep, extra_two_dep],
+ }
+
+ fixture_name = "with-conflicting-dependency-extras-" + (
+ "root" if root else "transitive"
+ )
+ locker.locked(locked)
+ if locked:
+ locker.mock_lock_data(dict(fixture(fixture_name)))
+
+ if extra is not None:
+ extras = [f"root-{extra}"] if not root else [extra]
+ installer.extras(extras)
+ result = installer.run()
+ assert result == 0
+
+ if not locked:
+ expected = fixture(fixture_name)
+ assert locker.written_data == expected
+
+ # Results of installation are consistent with the 'extra' input
+ assert isinstance(installer.executor, Executor)
+
+ expected_installations = []
+ if extra == "extra-one":
+ expected_installations.append(conflicting_dep_one_pkg)
+ elif extra == "extra-two":
+ expected_installations.append(conflicting_dep_two_pkg)
+ if not root and extra is not None:
+ expected_installations.append(get_package("intermediate-dep", "1.0.0"))
+
+ assert len(installer.executor.installations) == len(expected_installations)
+ assert set(installer.executor.installations) == set(expected_installations)
+
+
+@pytest.mark.parametrize("locked", [True, False])
+@pytest.mark.parametrize("extra", [None, "cpu", "cuda"])
+def test_run_with_exclusive_extras_different_sources(
+ installer: Installer,
+ locker: Locker,
+ installed: CustomInstalledRepository,
+ config: Config,
+ package: ProjectPackage,
+ extra: str | None,
+ locked: bool,
+) -> None:
+ """
+ - https://github.com/python-poetry/poetry/issues/6409
+ - https://github.com/python-poetry/poetry/issues/6419
+ - https://github.com/python-poetry/poetry/issues/7748
+ - https://github.com/python-poetry/poetry/issues/9537
+ """
+ # Setup repo for each of our sources
+ cpu_repo = Repository("pytorch-cpu")
+ cuda_repo = Repository("pytorch-cuda")
+ pool = RepositoryPool()
+ pool.add_repository(cpu_repo)
+ pool.add_repository(cuda_repo)
+ config.config["repositories"] = {
+ "pytorch-cpu": {"url": "https://download.pytorch.org/whl/cpu"},
+ "pytorch-cuda": {"url": "https://download.pytorch.org/whl/cuda"},
+ }
+
+ # Configure packages that read from each of the different sources
+ torch_cpu_pkg = get_package("torch", "1.11.0+cpu")
+ torch_cpu_pkg._source_reference = "pytorch-cpu"
+ torch_cpu_pkg._source_type = "legacy"
+ torch_cpu_pkg._source_url = "https://download.pytorch.org/whl/cpu"
+ torch_cuda_pkg = get_package("torch", "1.11.0+cuda")
+ torch_cuda_pkg._source_reference = "pytorch-cuda"
+ torch_cuda_pkg._source_type = "legacy"
+ torch_cuda_pkg._source_url = "https://download.pytorch.org/whl/cuda"
+ cpu_repo.add_package(torch_cpu_pkg)
+ cuda_repo.add_package(torch_cuda_pkg)
+
+ # Depend on each package based on exclusive extras
+ torch_cpu_dep = Factory.create_dependency(
+ "torch",
+ {
+ "version": "1.11.0+cpu",
+ "markers": "extra == 'cpu' and extra != 'cuda'",
+ "source": "pytorch-cpu",
+ },
+ )
+ torch_cuda_dep = Factory.create_dependency(
+ "torch",
+ {
+ "version": "1.11.0+cuda",
+ "markers": "extra != 'cpu' and extra == 'cuda'",
+ "source": "pytorch-cuda",
+ },
+ )
+ package.add_dependency(torch_cpu_dep)
+ package.add_dependency(torch_cuda_dep)
+ # We don't want to cheat by only including the correct dependency in the 'extra' mapping
+ package.extras = {
+ canonicalize_name("cpu"): [torch_cpu_dep, torch_cuda_dep],
+ canonicalize_name("cuda"): [torch_cpu_dep, torch_cuda_dep],
+ }
+
+ # Set locker state
+ locker.locked(locked)
+ if locked:
+ locker.mock_lock_data(dict(fixture("with-exclusive-extras")))
+
+ # Perform install
+ installer = Installer(
+ NullIO(),
+ MockEnv(),
+ package,
+ locker,
+ pool,
+ config,
+ installed=installed,
+ executor=Executor(
+ MockEnv(),
+ pool,
+ config,
+ NullIO(),
+ ),
+ )
+ if extra is not None:
+ installer.extras([extra])
+ result = installer.run()
+ assert result == 0
+
+ # Results of locking are expected and installation are consistent with the 'extra' input
+ if not locked:
+ expected = fixture("with-exclusive-extras")
+ assert locker.written_data == expected
+ assert isinstance(installer.executor, Executor)
+ if extra is None:
+ assert len(installer.executor.installations) == 0
+ else:
+ assert len(installer.executor.installations) == 1
+ version = f"1.11.0+{extra}"
+ source_url = f"https://download.pytorch.org/whl/{extra}"
+ source_reference = f"pytorch-{extra}"
+ assert installer.executor.installations[0] == Package(
+ "torch",
+ version,
+ source_type="legacy",
+ source_url=source_url,
+ source_reference=source_reference,
+ )
+
+
+@pytest.mark.parametrize("locked", [True, False])
+@pytest.mark.parametrize("extra", [None, "extra-one", "extra-two"])
+def test_run_with_different_dependency_extras(
+ installer: Installer,
+ pool: RepositoryPool,
+ locker: Locker,
+ installed: CustomInstalledRepository,
+ repo: Repository,
+ config: Config,
+ package: ProjectPackage,
+ extra: str | None,
+ locked: bool,
+) -> None:
+ """
+ - https://github.com/python-poetry/poetry/issues/834
+ - https://github.com/python-poetry/poetry/issues/7748
+
+ This tests different sets of extras in a dependency of the root project. These different dependency extras are
+ themselves conditioned on extras in the root project.
+ """
+ # Three packages in addition to root: demo (direct dependency) and two transitive dep packages
+ demo_pkg = get_package("demo", "1.0.0")
+ transitive_one_pkg = get_package("transitive-dep-one", "1.1.0")
+ transitive_two_pkg = get_package("transitive-dep-two", "1.2.0")
+
+ # Switch each transitive dependency based on extra markers in the 'demo' package
+ transitive_dep_one = Factory.create_dependency(
+ "transitive-dep-one",
+ {
+ "version": "1.1.0",
+ "markers": "extra == 'demo-extra-one' and extra != 'demo-extra-two'",
+ "optional": True,
+ },
+ )
+ transitive_dep_two = Factory.create_dependency(
+ "transitive-dep-two",
+ {
+ "version": "1.2.0",
+ "markers": "extra != 'demo-extra-one' and extra == 'demo-extra-two'",
+ "optional": True,
+ },
+ )
+ # Include both packages in both demo extras, to validate that they're filtered out based on extra markers alone
+ demo_pkg.extras = {
+ canonicalize_name("demo-extra-one"): [
+ get_dependency("transitive-dep-one"),
+ get_dependency("transitive-dep-two"),
+ ],
+ canonicalize_name("demo-extra-two"): [
+ get_dependency("transitive-dep-one"),
+ get_dependency("transitive-dep-two"),
+ ],
+ }
+ demo_pkg.add_dependency(transitive_dep_one)
+ demo_pkg.add_dependency(transitive_dep_two)
+
+ # Now define the demo dependency, similarly switched on extra markers in the root package
+ extra_one_dep = Factory.create_dependency(
+ "demo",
+ {
+ "version": "1.0.0",
+ "markers": "extra == 'extra-one' and extra != 'extra-two'",
+ "extras": ["demo-extra-one"],
+ },
+ )
+ extra_two_dep = Factory.create_dependency(
+ "demo",
+ {
+ "version": "1.0.0",
+ "markers": "extra != 'extra-one' and extra == 'extra-two'",
+ "extras": ["demo-extra-two"],
+ },
+ )
+ package.add_dependency(extra_one_dep)
+ package.add_dependency(extra_two_dep)
+ # Again we don't want to cheat by only including the correct dependency in the 'extra' mapping
+ package.extras = {
+ canonicalize_name("extra-one"): [extra_one_dep, extra_two_dep],
+ canonicalize_name("extra-two"): [extra_one_dep, extra_two_dep],
+ }
+
+ repo.add_package(demo_pkg)
+ repo.add_package(transitive_one_pkg)
+ repo.add_package(transitive_two_pkg)
+
+ locker.locked(locked)
+ if locked:
+ locker.mock_lock_data(dict(fixture("with-dependencies-differing-extras")))
+
+ installer = Installer(
+ NullIO(),
+ MockEnv(),
+ package,
+ locker,
+ pool,
+ config,
+ installed=installed,
+ executor=Executor(
+ MockEnv(),
+ pool,
+ config,
+ NullIO(),
+ ),
+ )
+ if extra is not None:
+ installer.extras([extra])
+ result = installer.run()
+ assert result == 0
+
+ if not locked:
+ expected = fixture("with-dependencies-differing-extras")
+ assert locker.written_data == expected
+
+ # Results of installation are consistent with the 'extra' input
+ assert isinstance(installer.executor, Executor)
+ if extra is None:
+ assert len(installer.executor.installations) == 0
+ else:
+ assert len(installer.executor.installations) == 2
+
+
@pytest.mark.parametrize("is_locked", [False, True])
@pytest.mark.parametrize("is_installed", [False, True])
@pytest.mark.parametrize("with_extras", [False, True])
diff --git a/tests/puzzle/test_solver.py b/tests/puzzle/test_solver.py
index a51a520dc33..a566e5c588e 100644
--- a/tests/puzzle/test_solver.py
+++ b/tests/puzzle/test_solver.py
@@ -4755,6 +4755,136 @@ def test_solver_resolves_duplicate_dependency_in_extra(
)
+def test_solver_resolves_conflicting_dependency_in_root_extra(
+ package: ProjectPackage,
+ pool: RepositoryPool,
+ repo: Repository,
+ io: NullIO,
+) -> None:
+ package_a1 = get_package("A", "1.0")
+ package_a2 = get_package("A", "2.0")
+
+ dep = get_dependency("A", {"version": "1.0", "markers": "extra != 'foo'"})
+ package.add_dependency(dep)
+
+ dep_extra = get_dependency("A", "2.0", optional=True)
+ dep_extra._in_extras = [canonicalize_name("foo")]
+ package.extras = {canonicalize_name("foo"): [dep_extra]}
+ package.add_dependency(dep_extra)
+
+ repo.add_package(package_a1)
+ repo.add_package(package_a2)
+
+ solver = Solver(package, pool, [], [], io)
+ transaction = solver.solve()
+
+ check_solver_result(
+ transaction,
+ (
+ [
+ {"job": "install", "package": package_a1},
+ {"job": "install", "package": package_a2},
+ ]
+ ),
+ )
+ solved_packages = transaction.get_solved_packages()
+ assert solved_packages[package_a1].markers["main"] == parse_marker("extra != 'foo'")
+ assert solved_packages[package_a2].markers["main"] == parse_marker("extra == 'foo'")
+
+
+def test_solver_resolves_conflicting_dependency_in_root_extras(
+ package: ProjectPackage,
+ pool: RepositoryPool,
+ repo: Repository,
+ io: NullIO,
+) -> None:
+ package_a1 = get_package("A", "1.0")
+ package_a2 = get_package("A", "2.0")
+
+ dep_extra1 = get_dependency( # extra == 'foo' is implicit via _in_extras!
+ "A", {"version": "1.0", "markers": "extra != 'bar'"}, optional=True
+ )
+ dep_extra1._in_extras = [canonicalize_name("foo")]
+ package.add_dependency(dep_extra1)
+
+ dep_extra2 = get_dependency( # extra == 'bar' is implicit via _in_extras!
+ "A", {"version": "2.0", "markers": "extra != 'foo'"}, optional=True
+ )
+ dep_extra2._in_extras = [canonicalize_name("bar")]
+ package.extras = {
+ canonicalize_name("foo"): [dep_extra1],
+ canonicalize_name("bar"): [dep_extra2],
+ }
+ package.add_dependency(dep_extra1)
+ package.add_dependency(dep_extra2)
+
+ repo.add_package(package_a1)
+ repo.add_package(package_a2)
+
+ solver = Solver(package, pool, [], [], io)
+ transaction = solver.solve()
+
+ check_solver_result(
+ transaction,
+ (
+ [
+ {"job": "install", "package": package_a1},
+ {"job": "install", "package": package_a2},
+ ]
+ ),
+ )
+ solved_packages = transaction.get_solved_packages()
+ assert solved_packages[package_a1].markers["main"] == parse_marker(
+ "extra != 'bar' and extra == 'foo'"
+ )
+ assert solved_packages[package_a2].markers["main"] == parse_marker(
+ "extra != 'foo' and extra == 'bar'"
+ )
+
+
+@pytest.mark.parametrize("with_extra", [False, True])
+def test_solver_resolves_duplicate_dependency_in_root_extra_for_installation(
+ package: ProjectPackage,
+ pool: RepositoryPool,
+ repo: Repository,
+ io: NullIO,
+ with_extra: bool,
+) -> None:
+ """
+ Without extras, a newer version of A can be chosen than with root extras.
+ """
+ extra = [canonicalize_name("foo")] if with_extra else []
+
+ package_a1 = get_package("A", "1.0")
+ package_a2 = get_package("A", "2.0")
+
+ dep = get_dependency("A", ">=1.0")
+ package.add_dependency(dep)
+
+ dep_extra = get_dependency("A", "^1.0", optional=True)
+ dep_extra.marker = parse_marker("extra == 'foo'")
+ package.extras = {canonicalize_name("foo"): [dep_extra]}
+ package.add_dependency(dep_extra)
+
+ repo.add_package(package_a1)
+ repo.add_package(package_a2)
+
+ solver = Solver(
+ package, pool, [], [package_a1, package_a2], io, active_root_extras=extra
+ )
+ with solver.use_environment(MockEnv()):
+ transaction = solver.solve()
+
+ check_solver_result(
+ transaction,
+ (
+ [
+ {"job": "install", "package": package_a1 if with_extra else package_a2},
+ ]
+ ),
+ )
+
+
def test_solver_resolves_duplicate_dependencies_with_restricted_extras(
package: ProjectPackage,
pool: RepositoryPool,
diff --git a/tests/puzzle/test_transaction.py b/tests/puzzle/test_transaction.py
index a68c981d159..2dcea971e79 100644
--- a/tests/puzzle/test_transaction.py
+++ b/tests/puzzle/test_transaction.py
@@ -429,3 +429,55 @@ def test_calculate_operations_extras(
),
ops,
)
+
+
+@pytest.mark.parametrize("extra", ["", "foo", "bar"])
+def test_calculate_operations_extras_no_redundant_uninstall(extra: str) -> None:
+ extra1 = canonicalize_name("foo")
+ extra2 = canonicalize_name("bar")
+ package = ProjectPackage("root", "1.0")
+ dep_a1 = Dependency("a", "1", optional=True)
+ dep_a1._in_extras = [canonicalize_name("foo")]
+ dep_a1.marker = parse_marker("extra != 'bar'")
+ dep_a2 = Dependency("a", "2", optional=True)
+ dep_a2._in_extras = [canonicalize_name("bar")]
+ dep_a2.marker = parse_marker("extra != 'foo'")
+ package.add_dependency(dep_a1)
+ package.add_dependency(dep_a2)
+ package.extras = {extra1: [dep_a1], extra2: [dep_a2]}
+ opt_a1 = Package("a", "1")
+ opt_a1.optional = True
+ opt_a2 = Package("a", "2")
+ opt_a2.optional = True
+
+ transaction = Transaction(
+ [Package("a", "1")],
+ {
+ opt_a1: TransitivePackageInfo(
+ 0, {"main"}, {"main": parse_marker("extra == 'foo' and extra != 'bar'")}
+ ),
+ opt_a2: TransitivePackageInfo(
+ 0, {"main"}, {"main": parse_marker("extra == 'bar' and extra != 'foo'")}
+ ),
+ },
+ [Package("a", "1")],
+ package,
+ {"python_version": "3.9"},
+ {"main"},
+ )
+
+ if not extra:
+ ops = [{"job": "remove", "package": Package("a", "1")}]
+ elif extra == "foo":
+ ops = [{"job": "install", "package": Package("a", "1"), "skipped": True}]
+ elif extra == "bar":
+ ops = [{"job": "update", "from": Package("a", "1"), "to": Package("a", "2")}]
+ else:
+ raise NotImplementedError
+
+ check_operations(
+ transaction.calculate_operations(
+ extras=set() if not extra else {canonicalize_name(extra)},
+ ),
+ ops,
+ )