From 689fa89daa9ab2237ff5bece8afe8e4a7e68819f Mon Sep 17 00:00:00 2001 From: Ivan Kulikov Date: Wed, 29 Jul 2026 22:58:09 +0000 Subject: [PATCH] PY-91195 uv workspace: per-dependency [tool.uv.sources] arrays (multiple sources by marker) are not resolved as module dependencies Merge-request: IJ-MR-215299 Merged-by: Ilya Kazakevich GitOrigin-RevId: 91cd1b8caeb075e0802acef3b48cc022062afd0e --- .../testplan/UvSourcesMarkerArrayTest.kt | 39 ++++++++++++++++++ .../README.md | 5 +++ .../pyproject.toml | 22 ++++++++++ .../sub-projects/sub-project-a/pyproject.toml | 14 +++++++ .../src/sub_project_a/__init__.py | 2 + .../sub-projects/sub-project-b/pyproject.toml | 14 +++++++ .../src/sub_project_b/__init__.py | 2 + .../python/uv/backend/UvPyProjectManager.kt | 40 +++++++++++++------ 8 files changed, 125 insertions(+), 13 deletions(-) create mode 100644 python/python-pyproject/test/com/intellij/python/junit5Tests/unit/alsoWin/pyproject/model/testplan/UvSourcesMarkerArrayTest.kt create mode 100644 python/python-pyproject/testData/monorepo/PY-91195-uv-sources-marker-array/README.md create mode 100644 python/python-pyproject/testData/monorepo/PY-91195-uv-sources-marker-array/pyproject.toml create mode 100644 python/python-pyproject/testData/monorepo/PY-91195-uv-sources-marker-array/sub-projects/sub-project-a/pyproject.toml create mode 100644 python/python-pyproject/testData/monorepo/PY-91195-uv-sources-marker-array/sub-projects/sub-project-a/src/sub_project_a/__init__.py create mode 100644 python/python-pyproject/testData/monorepo/PY-91195-uv-sources-marker-array/sub-projects/sub-project-b/pyproject.toml create mode 100644 python/python-pyproject/testData/monorepo/PY-91195-uv-sources-marker-array/sub-projects/sub-project-b/src/sub_project_b/__init__.py diff --git a/python/python-pyproject/test/com/intellij/python/junit5Tests/unit/alsoWin/pyproject/model/testplan/UvSourcesMarkerArrayTest.kt b/python/python-pyproject/test/com/intellij/python/junit5Tests/unit/alsoWin/pyproject/model/testplan/UvSourcesMarkerArrayTest.kt new file mode 100644 index 000000000000..9ae7f91a918c --- /dev/null +++ b/python/python-pyproject/test/com/intellij/python/junit5Tests/unit/alsoWin/pyproject/model/testplan/UvSourcesMarkerArrayTest.kt @@ -0,0 +1,39 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.python.junit5Tests.unit.alsoWin.pyproject.model.testplan + +import com.intellij.python.junit5Tests.framework.PyDefaultTestApplication +import com.intellij.python.junit5Tests.framework.metaInfo.TestClassInfo +import com.intellij.python.junit5Tests.unit.alsoWin.pyproject.div +import com.intellij.python.junit5Tests.unit.alsoWin.pyproject.model.ExpectedModule +import com.intellij.python.junit5Tests.unit.alsoWin.pyproject.model.pyProjectTomlSyncFixture +import com.intellij.testFramework.TestDataPath +import com.intellij.testFramework.common.timeoutRunBlocking +import com.intellij.testFramework.junit5.fixture.projectFixture +import com.intellij.testFramework.junit5.fixture.tempPathFixture +import org.junit.jupiter.api.Test + +/** + * PY-91195 follow-up: each `[tool.uv.sources]` entry is an array of tables (multiple sources selected + * by platform marker — a valid uv feature), and the workspace is declared with the inline-table form + * `[tool.uv] workspace = { members = [...] }`. All members must be discovered as modules. + */ +@PyDefaultTestApplication +@TestClassInfo(contentRootPath = "python-pyproject/test") +@TestDataPath($$"$CONTENT_ROOT/../testData/monorepo/PY-91195-uv-sources-marker-array") +internal class UvSourcesMarkerArrayTest { + companion object { + private val tempDirFixture = tempPathFixture() + private val projectFixture = projectFixture(pathFixture = tempDirFixture) + } + private val f by pyProjectTomlSyncFixture(projectFixture, tempDirFixture) + + @Test + fun sanity(): Unit = timeoutRunBlocking { + f.reloadProject() + f.assertProjectStructure( + ExpectedModule("pythonproject", contentRoot = ".", deps = listOf("sub-project-a", "sub-project-b"), sourceRoots = listOf(".")), + ExpectedModule("sub-project-a", contentRoot = "sub-projects" / "sub-project-a", sourceRoots = listOf("sub-projects" / "sub-project-a" / "src")), + ExpectedModule("sub-project-b", contentRoot = "sub-projects" / "sub-project-b", sourceRoots = listOf("sub-projects" / "sub-project-b" / "src")), + ) + } +} diff --git a/python/python-pyproject/testData/monorepo/PY-91195-uv-sources-marker-array/README.md b/python/python-pyproject/testData/monorepo/PY-91195-uv-sources-marker-array/README.md new file mode 100644 index 000000000000..1bf256c5dd26 --- /dev/null +++ b/python/python-pyproject/testData/monorepo/PY-91195-uv-sources-marker-array/README.md @@ -0,0 +1,5 @@ +# pythonproject + +PY-91195 follow-up: a uv workspace where each `[tool.uv.sources]` entry is an array of tables +(multiple sources selected by platform marker — a valid uv feature). The members must still be +discovered as modules. diff --git a/python/python-pyproject/testData/monorepo/PY-91195-uv-sources-marker-array/pyproject.toml b/python/python-pyproject/testData/monorepo/PY-91195-uv-sources-marker-array/pyproject.toml new file mode 100644 index 000000000000..7a63abcba86a --- /dev/null +++ b/python/python-pyproject/testData/monorepo/PY-91195-uv-sources-marker-array/pyproject.toml @@ -0,0 +1,22 @@ +[project] +name = "pythonproject" +version = "0.1.0" +description = "Root project" +requires-python = ">=3.12" +dependencies = [ + "sub-project-a", + "sub-project-b", +] + +[tool.uv] +workspace = { members = ["sub-projects/*"] } + +[tool.uv.sources] +sub-project-a = [ + { workspace = true, marker = "sys_platform == 'linux'" }, + { workspace = true, marker = "sys_platform == 'win32'" }, +] +sub-project-b = [ + { workspace = true, marker = "sys_platform == 'linux'" }, + { workspace = true, marker = "sys_platform == 'win32'" }, +] diff --git a/python/python-pyproject/testData/monorepo/PY-91195-uv-sources-marker-array/sub-projects/sub-project-a/pyproject.toml b/python/python-pyproject/testData/monorepo/PY-91195-uv-sources-marker-array/sub-projects/sub-project-a/pyproject.toml new file mode 100644 index 000000000000..079204ed9ff0 --- /dev/null +++ b/python/python-pyproject/testData/monorepo/PY-91195-uv-sources-marker-array/sub-projects/sub-project-a/pyproject.toml @@ -0,0 +1,14 @@ +[project] +name = "sub-project-a" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/sub_project_a"] diff --git a/python/python-pyproject/testData/monorepo/PY-91195-uv-sources-marker-array/sub-projects/sub-project-a/src/sub_project_a/__init__.py b/python/python-pyproject/testData/monorepo/PY-91195-uv-sources-marker-array/sub-projects/sub-project-a/src/sub_project_a/__init__.py new file mode 100644 index 000000000000..08da228f2820 --- /dev/null +++ b/python/python-pyproject/testData/monorepo/PY-91195-uv-sources-marker-array/sub-projects/sub-project-a/src/sub_project_a/__init__.py @@ -0,0 +1,2 @@ +def ping() -> str: + return "sub-project-a" diff --git a/python/python-pyproject/testData/monorepo/PY-91195-uv-sources-marker-array/sub-projects/sub-project-b/pyproject.toml b/python/python-pyproject/testData/monorepo/PY-91195-uv-sources-marker-array/sub-projects/sub-project-b/pyproject.toml new file mode 100644 index 000000000000..1e423f3f8334 --- /dev/null +++ b/python/python-pyproject/testData/monorepo/PY-91195-uv-sources-marker-array/sub-projects/sub-project-b/pyproject.toml @@ -0,0 +1,14 @@ +[project] +name = "sub-project-b" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/sub_project_b"] diff --git a/python/python-pyproject/testData/monorepo/PY-91195-uv-sources-marker-array/sub-projects/sub-project-b/src/sub_project_b/__init__.py b/python/python-pyproject/testData/monorepo/PY-91195-uv-sources-marker-array/sub-projects/sub-project-b/src/sub_project_b/__init__.py new file mode 100644 index 000000000000..58195fcc4730 --- /dev/null +++ b/python/python-pyproject/testData/monorepo/PY-91195-uv-sources-marker-array/sub-projects/sub-project-b/src/sub_project_b/__init__.py @@ -0,0 +1,2 @@ +def ping() -> str: + return "sub-project-b" diff --git a/python/python-uv/backend/src/com/intellij/python/uv/backend/UvPyProjectManager.kt b/python/python-uv/backend/src/com/intellij/python/uv/backend/UvPyProjectManager.kt index c5d59994a7b3..fb1a697f26e7 100644 --- a/python/python-uv/backend/src/com/intellij/python/uv/backend/UvPyProjectManager.kt +++ b/python/python-uv/backend/src/com/intellij/python/uv/backend/UvPyProjectManager.kt @@ -203,26 +203,40 @@ private fun getUvDependencies( val workspaceDeps = mutableListOf() val pathDeps = hashSetOf() for ((sourcesTable, ownerRoot) in sourcesTablesWithRoots) { - for ((sourceKey, depTable) in sourcesTable.toMap().entries) { + for ((sourceKey, sourceValue) in sourcesTable.toMap().entries) { val normalizedKey = PyPackageName.normalizeProjectName(sourceKey) val depName = depByNormalizedName[normalizedKey] ?: continue - val table = depTable as? TomlTable ?: continue - // PY-91089: safeGet instead of getBoolean/getString, which throw when the value has an unexpected type. - if (table.safeGet("workspace").successOrNull == true) { - workspaceDeps.add(ProjectName(depName)) - depByNormalizedName.remove(normalizedKey) + // A source entry is either a single table or an array of tables (multiple sources selected by + // platform marker — a valid uv feature); resolve every table it contains (PY-91195). + val depTables = when (sourceValue) { + is TomlTable -> listOf(sourceValue) + is TomlArray -> sourceValue.toList().filterIsInstance() + else -> continue } - else { - val path = table.safeGet("path").successOrNull ?: continue - try { - pathDeps.add(ownerRoot.resolve(path).normalize()) - depByNormalizedName.remove(normalizedKey) + + var resolved = false + for (table in depTables) { + // PY-91089: safeGet instead of getBoolean/getString, which throw when the value has an unexpected type. + if (table.safeGet("workspace").successOrNull == true) { + workspaceDeps.add(ProjectName(depName)) + resolved = true } - catch (e: InvalidPathException) { - logger.info("Can't resolve $path against $ownerRoot", e) + else { + val path = table.safeGet("path").successOrNull ?: continue + try { + pathDeps.add(ownerRoot.resolve(path).normalize()) + resolved = true + } + catch (e: InvalidPathException) { + logger.info("Can't resolve $path against $ownerRoot", e) + } } } + // Once resolved by a higher-priority table, don't let parent workspace tables override it. + if (resolved) { + depByNormalizedName.remove(normalizedKey) + } } } return DependencyInfo(workspaceDeps = workspaceDeps.toSet(), pathDeps = pathDeps)