mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-91089 uv workspace: new member added to pyproject.toml is never discovered as a module
Merge-request: IJ-MR-215001 Merged-by: Ilya Kazakevich <ilya.kazakevich@jetbrains.com> (cherry picked from commit 93493f7236d9d5e4d1e07bbed615381082f2b63d) GitOrigin-RevId: 7ea2ba8ad041a539fae1cb01b516422f74cc55c4
This commit is contained in:
committed by
intellij-monorepo-bot
parent
237631cb56
commit
76a4a4c1f6
+9
-4
@@ -7,6 +7,7 @@ import com.intellij.python.pyproject.model.spi.ProjectDependencies
|
||||
import com.intellij.python.pyproject.model.spi.ProjectName
|
||||
import com.intellij.python.pyproject.model.spi.PyProjectTomlProject
|
||||
import com.intellij.python.pyproject.model.spi.TomlDependencySpecification
|
||||
import com.intellij.python.pyproject.safeGet
|
||||
import com.intellij.python.pyproject.safeGetArr
|
||||
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
|
||||
import com.jetbrains.python.Result
|
||||
@@ -161,7 +162,10 @@ private fun getToolSpecificDependencies(
|
||||
): Sequence<Directory> {
|
||||
return tomlDependencySpecifications.asSequence().flatMap { specification ->
|
||||
when (specification) {
|
||||
is TomlDependencySpecification.PathDependency -> tomlTable.getTable(specification.tomlKey)?.let {
|
||||
// PY-91089: use safeGet instead of TomlTable.getTable, which throws TomlInvalidTypeException
|
||||
// (not returns null) when the key holds a non-table value such as an array (the `[[tool.uv.sources]]`
|
||||
// double-bracket typo). An unhandled throw here aborts the whole model sync and hides every member.
|
||||
is TomlDependencySpecification.PathDependency -> tomlTable.safeGet<TomlTable>(specification.tomlKey, unquotedDottedKey = true).successOrNull?.let {
|
||||
getToolSpecificDependenciesFromTomlTable(root, it)
|
||||
} ?: emptySet()
|
||||
is TomlDependencySpecification.Pep621Dependency -> {
|
||||
@@ -169,9 +173,9 @@ private fun getToolSpecificDependencies(
|
||||
deps.asSequence().mapNotNull(::parsePep621Dependency).toSet()
|
||||
}
|
||||
is TomlDependencySpecification.GroupPathDependency -> {
|
||||
val groups = tomlTable.getTable(specification.tomlKeyToGroup) ?: return@flatMap emptySet()
|
||||
val groups = tomlTable.safeGet<TomlTable>(specification.tomlKeyToGroup, unquotedDottedKey = true).successOrNull ?: return@flatMap emptySet()
|
||||
groups.keySet().flatMap { group ->
|
||||
groups.getTable("${group}.${specification.tomlKeyFromGroupToPath}")?.let {
|
||||
groups.safeGet<TomlTable>("${group}.${specification.tomlKeyFromGroupToPath}", unquotedDottedKey = true).successOrNull?.let {
|
||||
getToolSpecificDependenciesFromTomlTable(root, it)
|
||||
} ?: emptySet()
|
||||
}
|
||||
@@ -183,7 +187,8 @@ private fun getToolSpecificDependencies(
|
||||
@RequiresBackgroundThread
|
||||
private fun getToolSpecificDependenciesFromTomlTable(root: Path, tomlTable: TomlTable): Set<Directory> {
|
||||
return tomlTable.keySet().asSequence().mapNotNull {
|
||||
tomlTable.getString("${it}.path")?.let { depPathString -> parseDepFromPathString(root, depPathString) }
|
||||
// PY-91089: safeGet instead of getString, which throws when `<dep>.path` holds a non-string value.
|
||||
tomlTable.safeGet<String>("${it}.path", unquotedDottedKey = true).successOrNull?.let { depPathString -> parseDepFromPathString(root, depPathString) }
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -32,6 +32,7 @@ import com.intellij.platform.workspace.storage.url.VirtualFileUrlManager
|
||||
import com.intellij.project.stateStore
|
||||
import com.intellij.python.community.common.tools.ToolId
|
||||
import com.intellij.python.pyproject.PyProjectToml
|
||||
import com.intellij.python.pyproject.safeGet
|
||||
import com.intellij.python.pyproject.model.internal.PY_PROJECT_SYSTEM_ID
|
||||
import com.intellij.python.pyproject.model.internal.PyProjectTomlBundle
|
||||
import com.intellij.python.pyproject.model.internal.pyProjectToml.FSWalkInfoWithToml
|
||||
@@ -479,7 +480,8 @@ private suspend fun parseRawEntries(fsInfo: FSWalkInfoWithToml, pyProjectManager
|
||||
}
|
||||
}
|
||||
if (participatedManagers.isEmpty()) {
|
||||
toml.toml.getString("build-system.build-backend")?.let { buildBackend ->
|
||||
// PY-91089: safeGet instead of getString, which throws when `build-system.build-backend` is not a string.
|
||||
toml.toml.safeGet<String>("build-system.build-backend", unquotedDottedKey = true).successOrNull?.let { buildBackend ->
|
||||
pyProjectManagers.firstOrNull { it.id.id in buildBackend }?.let { buildTool ->
|
||||
participatedManagers.add(buildTool.id)
|
||||
}
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
// 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-91089: the root `pyproject.toml` declares `tool.uv.sources` as an array of tables
|
||||
* (`[[tool.uv.sources]]`) instead of a table (`[tool.uv.sources]`) — a common double-bracket
|
||||
* typo. `uv lock`/`uv sync` still succeed because uv ignores non-member files, so the project
|
||||
* looks fine outside the IDE. The malformed value must NOT abort the model sync: every
|
||||
* workspace member — including the freshly added `orgwiki-test` — must still be discovered as
|
||||
* a module.
|
||||
*/
|
||||
@PyDefaultTestApplication
|
||||
@TestClassInfo(contentRootPath = "python-pyproject/test")
|
||||
@TestDataPath($$"$CONTENT_ROOT/../testData/monorepo/PY-91089-uv-workspace-array-sources")
|
||||
internal class UvWorkspaceArraySourcesTest {
|
||||
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("orgwiki", contentRoot = ".", sourceRoots = listOf(".")),
|
||||
ExpectedModule("orgwiki-core", contentRoot = "orgwiki-core", sourceRoots = listOf("orgwiki-core" / "src")),
|
||||
ExpectedModule("orgwiki-test", contentRoot = "orgwiki-test", deps = listOf("orgwiki-core"), sourceRoots = listOf("orgwiki-test" / "src")),
|
||||
)
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
// 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-91089: the root `pyproject.toml` declares `tool.uv.workspace` as an array of tables
|
||||
* (`[[tool.uv.workspace]]`) instead of a table. The malformed value must NOT abort the model sync:
|
||||
* both members are still discovered as modules (only their workspace grouping — and the deps derived
|
||||
* from it — is lost, since the workspace declaration is unusable).
|
||||
*/
|
||||
@PyDefaultTestApplication
|
||||
@TestClassInfo(contentRootPath = "python-pyproject/test")
|
||||
@TestDataPath($$"$CONTENT_ROOT/../testData/monorepo/PY-91089-uv-workspace-array-workspace")
|
||||
internal class UvWorkspaceArrayWorkspaceTest {
|
||||
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("orgwiki", contentRoot = ".", sourceRoots = listOf(".")),
|
||||
ExpectedModule("orgwiki-core", contentRoot = "orgwiki-core", sourceRoots = listOf("orgwiki-core" / "src")),
|
||||
ExpectedModule("orgwiki-test", contentRoot = "orgwiki-test", sourceRoots = listOf("orgwiki-test" / "src")),
|
||||
)
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
# orgwiki
|
||||
|
||||
PY-91089 replica: a uv workspace whose root `pyproject.toml` declares `tool.uv.sources`
|
||||
as an array of tables (`[[tool.uv.sources]]`) instead of a table. The workspace members
|
||||
`orgwiki-core` and `orgwiki-test` must still be discovered as modules.
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
[project]
|
||||
name = "orgwiki-core"
|
||||
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/orgwiki_core"]
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
def ping() -> str:
|
||||
return "orgwiki-core"
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
[project]
|
||||
name = "orgwiki-test"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"orgwiki-core",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/orgwiki_test"]
|
||||
|
||||
[tool.uv.sources]
|
||||
orgwiki-core = { workspace = true }
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
from orgwiki_core import ping
|
||||
|
||||
__all__ = ["ping"]
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
[project]
|
||||
name = "orgwiki"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"orgwiki-core",
|
||||
"orgwiki-test",
|
||||
]
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = ["orgwiki-core", "orgwiki-test"]
|
||||
|
||||
# PY-91089: `tool.uv.sources` is written as an array of tables (`[[...]]`) instead of a
|
||||
# table (`[...]`). This is a common double-bracket typo; uv ignores non-member files so
|
||||
# `uv lock`/`uv sync` still succeed, but the IDE must not let it abort member discovery.
|
||||
[[tool.uv.sources]]
|
||||
orgwiki-core = { workspace = true }
|
||||
orgwiki-test = { workspace = true }
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
# orgwiki
|
||||
|
||||
PY-91089 replica: a uv workspace whose root `pyproject.toml` declares `tool.uv.workspace`
|
||||
as an array of tables (`[[tool.uv.workspace]]`) instead of a table. The members
|
||||
`orgwiki-core` and `orgwiki-test` must still be discovered as modules.
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
[project]
|
||||
name = "orgwiki-core"
|
||||
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/orgwiki_core"]
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
def ping() -> str:
|
||||
return "orgwiki-core"
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
[project]
|
||||
name = "orgwiki-test"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"orgwiki-core",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/orgwiki_test"]
|
||||
|
||||
[tool.uv.sources]
|
||||
orgwiki-core = { workspace = true }
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
from orgwiki_core import ping
|
||||
|
||||
__all__ = ["ping"]
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
[project]
|
||||
name = "orgwiki"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"orgwiki-core",
|
||||
"orgwiki-test",
|
||||
]
|
||||
|
||||
# PY-91089: `tool.uv.workspace` is written as an array of tables (`[[...]]`) instead of a table (`[...]`).
|
||||
# The malformed workspace declaration must not abort discovery — the members must still become modules.
|
||||
[[tool.uv.workspace]]
|
||||
members = ["orgwiki-core", "orgwiki-test"]
|
||||
|
||||
[tool.uv.sources]
|
||||
orgwiki-core = { workspace = true }
|
||||
orgwiki-test = { workspace = true }
|
||||
@@ -5,6 +5,7 @@ import com.intellij.openapi.diagnostic.fileLogger
|
||||
import com.intellij.openapi.util.getPathMatcher
|
||||
import com.intellij.python.community.common.tools.ToolId
|
||||
import com.intellij.python.pyproject.PyProjectToml
|
||||
import com.intellij.python.pyproject.safeGet
|
||||
import com.intellij.python.pyproject.model.spi.ProjectDependencies
|
||||
import com.intellij.python.pyproject.model.spi.ProjectName
|
||||
import com.intellij.python.pyproject.model.spi.ProjectStructureInfo
|
||||
@@ -72,9 +73,12 @@ internal class UvPyProjectManager : PyProjectManager {
|
||||
}
|
||||
|
||||
// Each member might have tool.uv.sources table.
|
||||
// PY-91089: use safeGet, not TomlTable.getTable, which throws TomlInvalidTypeException when the key
|
||||
// holds a non-table value (e.g. the `[[tool.uv.sources]]` array-of-tables typo). Such a member is
|
||||
// simply treated as having no sources instead of aborting the whole workspace model sync.
|
||||
val memberToUvSourceTable = entries
|
||||
.mapNotNull { (projectName, toml) ->
|
||||
toml.pyProjectToml.toml.getTable("tool.uv.sources")?.let { projectName to it }
|
||||
toml.pyProjectToml.toml.safeGet<TomlTable>("tool.uv.sources", unquotedDottedKey = true).successOrNull?.let { projectName to it }
|
||||
}
|
||||
.toMap()
|
||||
|
||||
@@ -168,9 +172,11 @@ private data class SourceTableWithOwner(val table: TomlTable, val ownerRoot: Pat
|
||||
|
||||
@RequiresBackgroundThread
|
||||
private fun getWorkspaceMembers(toml: TomlTable): WorkspaceInfo? {
|
||||
val workspace = toml.getTable("tool.uv.workspace") ?: return null
|
||||
val members = workspace.getArrayOrEmpty("members").asMatchers
|
||||
val exclude = workspace.getArrayOrEmpty("exclude").asMatchers
|
||||
// PY-91089: safeGet instead of getTable/getArrayOrEmpty, which throw TomlInvalidTypeException when
|
||||
// the key holds an unexpected type (e.g. the `[[tool.uv.workspace]]` array typo, or `members = "x"`).
|
||||
val workspace = toml.safeGet<TomlTable>("tool.uv.workspace", unquotedDottedKey = true).successOrNull ?: return null
|
||||
val members = workspace.safeGet<TomlArray>("members").successOrNull?.asMatchers ?: emptyList()
|
||||
val exclude = workspace.safeGet<TomlArray>("exclude").successOrNull?.asMatchers ?: emptyList()
|
||||
if (members.isEmpty()) return null
|
||||
return WorkspaceInfo(members = members, exclude = exclude)
|
||||
}
|
||||
@@ -202,12 +208,13 @@ private fun getUvDependencies(
|
||||
val depName = depByNormalizedName[normalizedKey] ?: continue
|
||||
val table = depTable as? TomlTable ?: continue
|
||||
|
||||
if (table.getBoolean("workspace") == true) {
|
||||
// PY-91089: safeGet instead of getBoolean/getString, which throw when the value has an unexpected type.
|
||||
if (table.safeGet<Boolean>("workspace").successOrNull == true) {
|
||||
workspaceDeps.add(ProjectName(depName))
|
||||
depByNormalizedName.remove(normalizedKey)
|
||||
}
|
||||
else {
|
||||
val path = table.getString("path") ?: continue
|
||||
val path = table.safeGet<String>("path").successOrNull ?: continue
|
||||
try {
|
||||
pathDeps.add(ownerRoot.resolve(path).normalize())
|
||||
depByNormalizedName.remove(normalizedKey)
|
||||
|
||||
Reference in New Issue
Block a user