diff --git a/python/python-hatch/src/com/intellij/python/hatch/impl/HatchTool.kt b/python/python-hatch/src/com/intellij/python/hatch/impl/HatchTool.kt index 100cce412a4a..9a98aa1a5f34 100644 --- a/python/python-hatch/src/com/intellij/python/hatch/impl/HatchTool.kt +++ b/python/python-hatch/src/com/intellij/python/hatch/impl/HatchTool.kt @@ -4,6 +4,7 @@ package com.intellij.python.hatch.impl import com.intellij.openapi.util.NlsSafe import com.intellij.python.common.tools.ToolId import com.intellij.python.hatch.icons.PythonHatchIcons +import com.intellij.python.pyproject.model.internal.pyProjectToml.TomlDependencySpecification import com.intellij.python.pyproject.model.spi.ProjectName import com.intellij.python.pyproject.model.spi.ProjectStructureInfo import com.intellij.python.pyproject.model.spi.PyProjectTomlProject @@ -26,5 +27,10 @@ internal class HatchTool : Tool { override suspend fun getProjectName(projectToml: TomlTable): @NlsSafe String? = null - override suspend fun getProjectStructure(entries: Map, rootIndex: Map): ProjectStructureInfo? = null + override suspend fun getProjectStructure( + entries: Map, + rootIndex: Map, + ): ProjectStructureInfo? = null + + override fun getTomlDependencySpecifications(): List = emptyList() } diff --git a/python/python-poetry/backend/src/com/intellij/python/community/impl/poetry/backend/PoetryTool.kt b/python/python-poetry/backend/src/com/intellij/python/community/impl/poetry/backend/PoetryTool.kt index 90b8b1500ace..99005868827f 100644 --- a/python/python-poetry/backend/src/com/intellij/python/community/impl/poetry/backend/PoetryTool.kt +++ b/python/python-poetry/backend/src/com/intellij/python/community/impl/poetry/backend/PoetryTool.kt @@ -6,6 +6,7 @@ import com.intellij.openapi.util.NlsSafe import com.intellij.python.common.tools.ToolId import com.intellij.python.community.impl.poetry.common.POETRY_TOOL_ID import com.intellij.python.community.impl.poetry.common.POETRY_UI_INFO +import com.intellij.python.pyproject.model.internal.pyProjectToml.TomlDependencySpecification import com.intellij.python.pyproject.model.spi.ProjectName import com.intellij.python.pyproject.model.spi.ProjectStructureInfo import com.intellij.python.pyproject.model.spi.PyProjectTomlProject @@ -24,6 +25,14 @@ internal class PoetryTool : Tool { override suspend fun getProjectName(projectToml: TomlTable): @NlsSafe String? = projectToml.getString("tool.poetry.name") - override suspend fun getProjectStructure(entries: Map, rootIndex: Map): ProjectStructureInfo? = null + override suspend fun getProjectStructure( + entries: Map, + rootIndex: Map, + ): ProjectStructureInfo? = null + override fun getTomlDependencySpecifications(): List = listOf( + TomlDependencySpecification.PathDependency("tool.poetry.dependencies"), + TomlDependencySpecification.PathDependency("tool.poetry.dev-dependencies"), + TomlDependencySpecification.GroupPathDependency("tool.poetry.group", "dependencies"), + ) } diff --git a/python/python-pyproject/src/com/intellij/python/pyproject/PyProjectExt.kt b/python/python-pyproject/src/com/intellij/python/pyproject/PyProjectExt.kt index 08a8d7541cd0..23528acfc1ec 100644 --- a/python/python-pyproject/src/com/intellij/python/pyproject/PyProjectExt.kt +++ b/python/python-pyproject/src/com/intellij/python/pyproject/PyProjectExt.kt @@ -35,6 +35,9 @@ sealed class TomlTableSafeGetError { * On a missing value, returns an instance of [Result.Success] with a value of null. * On a present value with the valid type, returns an instance of [Result.Success] with that value. * + * @param key is the key in a table + * @param unquotedDottedKey specifies whether a key should be treated as a dotted key without quotes + * * Example: * * ```kotlin @@ -46,8 +49,10 @@ sealed class TomlTableSafeGetError { * ``` */ @Internal -inline fun TomlTable.safeGet(key: String): Result { - val value = get("\"$key\"") +inline fun TomlTable.safeGet( + key: String, unquotedDottedKey: Boolean = false, +): Result { + val value = if (unquotedDottedKey) get(key) else get("\"$key\"") if (value == null) { return success(null) @@ -101,6 +106,9 @@ internal inline fun TomlTable.safeGetRequired(key: String): Result TomlTable.safeGetRequired(key: String): Result TomlTable.safeGetArr(key: String): Result?, TomlTableSafeGetError.UnexpectedType> { - val array = safeGet(key).getOr { return it } +inline fun TomlTable.safeGetArr( + key: String, unquotedDottedKey: Boolean = false, +): Result?, TomlTableSafeGetError.UnexpectedType> { + val array = safeGet(key, unquotedDottedKey).getOr { return it } if (array == null) { return success(null) diff --git a/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/pyProjectToml/tomFileTools.kt b/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/pyProjectToml/tomFileTools.kt index 0fad574b8a3d..06686a497999 100644 --- a/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/pyProjectToml/tomFileTools.kt +++ b/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/pyProjectToml/tomFileTools.kt @@ -6,12 +6,14 @@ import com.intellij.python.pyproject.PyProjectToml 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.safeGetArr import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.jetbrains.python.Result import com.jetbrains.python.venvReader.Directory import com.jetbrains.python.venvReader.VirtualEnvReader import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.apache.tuweni.toml.TomlTable import java.io.IOException import java.net.URI import java.net.URISyntaxException @@ -81,23 +83,6 @@ suspend fun walkFileSystemNoTomlContent( } } - -suspend fun getPEP621Deps( - entries: Map, - rootIndex: Map, -): ProjectDependencies = withContext(Dispatchers.Default) { - val deps = entries.asSequence().associate { (name, entry) -> - val deps = getDependenciesFromToml(entry.pyProjectToml).mapNotNull { dir -> - rootIndex[dir] ?: run { - logger.warn("Can't find project for dir $dir") - null - } - }.toSet() - Pair(name, deps) - } - ProjectDependencies(deps) // No workspace info (yet) -} - private val logger = fileLogger() private suspend fun readFile(file: Path): PyProjectToml? { @@ -118,16 +103,93 @@ private suspend fun readFile(file: Path): PyProjectToml? { } } -@RequiresBackgroundThread -private fun getDependenciesFromToml(projectToml: PyProjectToml): Set { - val depsFromFile = projectToml.project?.dependencies?.project ?: emptyList() - val moduleDependencies = depsFromFile - .mapNotNull { depSpec -> - val match = PEP_621_PATH_DEPENDENCY.matchEntire(depSpec) ?: return@mapNotNull null - val (_, depUri) = match.destructured - return@mapNotNull parseDepUri(depUri) +suspend fun getDependenciesFromToml( + entries: Map, + rootIndex: Map, + tomlDependencySpecifications: List, +): ProjectDependencies = withContext(Dispatchers.Default) { + val deps = entries.asSequence().associate { (name, entry) -> + val depsPaths = collectAllDependencies(entry, tomlDependencySpecifications) + val deps = processDependenciesWithRootIndex(depsPaths, rootIndex) + Pair(name, deps) + } + ProjectDependencies(deps) +} + +private fun processDependenciesWithRootIndex(dependencies: Sequence, rootIndex: Map): Set = + dependencies.mapNotNull { dir -> + rootIndex[dir] ?: run { + logger.warn("Can't find project for dir $dir") + null } - return moduleDependencies.toSet() + }.toSet() + +@RequiresBackgroundThread +private fun collectAllDependencies( + entry: PyProjectTomlProject, tomlDependencySpecifications: List, +): Sequence = sequence { + yieldAll(getDependenciesFromProject(entry.pyProjectToml)) + yieldAll(getDependenciesFromPep735Groups(entry.pyProjectToml.toml)) + yieldAll(getToolSpecificDependencies(entry.root, entry.pyProjectToml.toml, tomlDependencySpecifications)) +} + +@RequiresBackgroundThread +private fun getToolSpecificDependencies( + root: Path, tomlTable: TomlTable, tomlDependencySpecifications: List, +): Sequence { + return tomlDependencySpecifications.asSequence().flatMap { specification -> + when (specification) { + is TomlDependencySpecification.PathDependency -> tomlTable.getTable(specification.tomlKey)?.let { + getToolSpecificDependenciesFromTomlTable(root, it) + } ?: emptySet() + is TomlDependencySpecification.Pep621Dependency -> { + val deps = tomlTable.safeGetArr(specification.tomlKey, unquotedDottedKey = true).successOrNull ?: emptyList() + deps.asSequence().mapNotNull(::parsePep621Dependency).toSet() + } + is TomlDependencySpecification.GroupPathDependency -> { + val groups = tomlTable.getTable(specification.tomlKeyToGroup) ?: return@flatMap emptySet() + groups.keySet().flatMap { group -> + groups.getTable("${group}.${specification.tomlKeyFromGroupToPath}")?.let { + getToolSpecificDependenciesFromTomlTable(root, it) + } ?: emptySet() + } + } + } + } +} + +@RequiresBackgroundThread +private fun getToolSpecificDependenciesFromTomlTable(root: Path, tomlTable: TomlTable): Set { + return tomlTable.keySet().asSequence().mapNotNull { + tomlTable.getString("${it}.path")?.let { depPathString -> parseDepFromPathString(root, depPathString) } + }.toSet() +} + +@RequiresBackgroundThread +private fun getDependenciesFromPep735Groups(tomlTable: TomlTable): Sequence { + val groups = tomlTable.getTable("dependency-groups") ?: return emptySequence() + return groups.keySet().asSequence().flatMap { group -> + val deps = groups.safeGetArr(group).successOrNull ?: emptyList() + deps.asSequence().mapNotNull(::parsePep621Dependency) + } +} + +@RequiresBackgroundThread +private fun getDependenciesFromProject(projectToml: PyProjectToml): Sequence { + val depsFromFile = projectToml.project?.dependencies?.project ?: emptyList() + return depsFromFile.asSequence().mapNotNull(::parsePep621Dependency) +} + +private fun parsePep621Dependency(depSpec: String): Path? { + val match = PEP_621_PATH_DEPENDENCY.matchEntire(depSpec) ?: return null + val (_, depUri) = match.destructured + return parseDepUri(depUri) +} + +sealed interface TomlDependencySpecification { + data class PathDependency(val tomlKey: String) : TomlDependencySpecification + data class Pep621Dependency(val tomlKey: String) : TomlDependencySpecification + data class GroupPathDependency(val tomlKeyToGroup: String, val tomlKeyFromGroupToPath: String) : TomlDependencySpecification } @@ -145,4 +207,13 @@ private fun parseDepUri(depUri: String): Path? = catch (e: URISyntaxException) { logger.info("Dep $depUri can't be parsed", e) null - } \ No newline at end of file + } + +private fun parseDepFromPathString(root: Path, depPathString: String): Path? = + try { + root.resolve(depPathString).normalize() + } + catch (e: InvalidPathException) { + logger.info("Dep $depPathString points to wrong path", e) + null + } diff --git a/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/workspaceBridge/workspaceTools.kt b/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/workspaceBridge/workspaceTools.kt index d29e907d8c6f..27a7d953a770 100644 --- a/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/workspaceBridge/workspaceTools.kt +++ b/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/workspaceBridge/workspaceTools.kt @@ -36,7 +36,7 @@ import com.intellij.python.pyproject.PyProjectToml 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 -import com.intellij.python.pyproject.model.internal.pyProjectToml.getPEP621Deps +import com.intellij.python.pyproject.model.internal.pyProjectToml.getDependenciesFromToml import com.intellij.python.pyproject.model.spi.ProjectName import com.intellij.python.pyproject.model.spi.PyProjectTomlProject import com.intellij.python.pyproject.model.spi.Tool @@ -188,8 +188,8 @@ private suspend fun generatePyProjectTomlEntries( val entriesByName = entries.associateBy { it.name } val namesByDir = entries.associate { Pair(it.root, it.name) } val allNames = entriesByName.keys - var dependencies = getPEP621Deps(entriesByName, namesByDir) - for (tool in Tool.EP.extensionList) { + var dependencies = getDependenciesFromToml(entriesByName, namesByDir, tools.flatMap { it.getTomlDependencySpecifications() }) + for (tool in tools) { // Tool provides deps and workspace members val toolSpecificInfo = tool.getProjectStructure(entriesByName, namesByDir) // Tool-agnostic pep621 deps diff --git a/python/python-pyproject/src/com/intellij/python/pyproject/model/spi/Tool.kt b/python/python-pyproject/src/com/intellij/python/pyproject/model/spi/Tool.kt index 87fdaea1e132..9b40e33b77dd 100644 --- a/python/python-pyproject/src/com/intellij/python/pyproject/model/spi/Tool.kt +++ b/python/python-pyproject/src/com/intellij/python/pyproject/model/spi/Tool.kt @@ -3,6 +3,7 @@ package com.intellij.python.pyproject.model.spi import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.util.NlsSafe import com.intellij.python.common.tools.ToolId +import com.intellij.python.pyproject.model.internal.pyProjectToml.TomlDependencySpecification import com.jetbrains.python.PyToolUIInfo import com.jetbrains.python.venvReader.Directory import org.apache.tuweni.toml.TomlTable @@ -25,10 +26,18 @@ interface Tool { * All project names must be taken from provided data (use [rootIndex] to get name by directory). * If tool doesn't provide any specific structure (i.e: no dependencies except those described in pyproject.toml spec, no workspaces) return `null` */ - suspend fun getProjectStructure(entries: Map, rootIndex: Map): ProjectStructureInfo? + suspend fun getProjectStructure( + entries: Map, + rootIndex: Map, + ): ProjectStructureInfo? /** * Tool that supports build systems might return additional src directories */ suspend fun getSrcRoots(toml: TomlTable, projectRoot: Directory): Set -} \ No newline at end of file + + /** + * Tool-specific toml sections where dependencies may be specified + */ + fun getTomlDependencySpecifications(): List +} diff --git a/python/python-pyproject/test/com/intellij/python/junit5Tests/unit/pyproject/GetDependenciesFromTomlTest.kt b/python/python-pyproject/test/com/intellij/python/junit5Tests/unit/pyproject/GetDependenciesFromTomlTest.kt new file mode 100644 index 000000000000..19339d240c59 --- /dev/null +++ b/python/python-pyproject/test/com/intellij/python/junit5Tests/unit/pyproject/GetDependenciesFromTomlTest.kt @@ -0,0 +1,227 @@ +package com.intellij.python.junit5Tests.unit.pyproject + +import com.intellij.python.pyproject.PyProjectToml +import com.intellij.python.pyproject.model.internal.pyProjectToml.TomlDependencySpecification +import com.intellij.python.pyproject.model.internal.pyProjectToml.getDependenciesFromToml +import com.intellij.python.pyproject.model.spi.ProjectName +import com.intellij.python.pyproject.model.spi.PyProjectTomlProject +import com.intellij.testFramework.common.timeoutRunBlocking +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path +import kotlin.io.path.createDirectories + +internal class GetDependenciesFromTomlTest { + + private data class TestProject( + override val pyProjectToml: PyProjectToml, + override val root: Path, + ) : PyProjectTomlProject + + @Test + fun `pep 621 project dependencies`(@TempDir tempDir: Path): Unit = timeoutRunBlocking { + val mainDir = tempDir.resolve("main").createDirectories() + val libDir = tempDir.resolve("lib").createDirectories() + + val libUri = libDir.toUri() + val toml = PyProjectToml.parse(""" + [project] + name = "main" + version = "1.0" + dependencies = ["lib @ $libUri"] + """.trimIndent()) + + val mainName = ProjectName("main") + val libName = ProjectName("lib") + + val entries = mapOf(mainName to TestProject(toml, mainDir)) + val rootIndex = mapOf(libDir to libName) + + val result = getDependenciesFromToml(entries, rootIndex, emptyList()) + assertThat(result.map[mainName]).containsExactly(libName) + } + + @Test + fun `pep 735 dependency groups`(@TempDir tempDir: Path): Unit = timeoutRunBlocking { + val mainDir = tempDir.resolve("main").createDirectories() + val libDir = tempDir.resolve("lib").createDirectories() + val testLibDir = tempDir.resolve("test-lib").createDirectories() + + val libUri = libDir.toUri() + val testLibUri = testLibDir.toUri() + val toml = PyProjectToml.parse(""" + [project] + name = "main" + version = "1.0" + + [dependency-groups] + dev = ["lib @ $libUri"] + test = ["test-lib @ $testLibUri"] + """.trimIndent()) + + val mainName = ProjectName("main") + val libName = ProjectName("lib") + val testLibName = ProjectName("test-lib") + + val entries = mapOf(mainName to TestProject(toml, mainDir)) + val rootIndex = mapOf(libDir to libName, testLibDir to testLibName) + + val result = getDependenciesFromToml(entries, rootIndex, emptyList()) + assertThat(result.map[mainName]).containsExactlyInAnyOrder(libName, testLibName) + } + + @Test + fun `tool PathDependency`(@TempDir tempDir: Path): Unit = timeoutRunBlocking { + val mainDir = tempDir.resolve("main").createDirectories() + val libDir = tempDir.resolve("lib").createDirectories() + + val toml = PyProjectToml.parse(""" + [project] + name = "main" + version = "1.0" + + [tool.poetry.dependencies] + lib = {path = "../lib"} + """.trimIndent()) + + val mainName = ProjectName("main") + val libName = ProjectName("lib") + + val entries = mapOf(mainName to TestProject(toml, mainDir)) + val rootIndex = mapOf(libDir to libName) + val specs = listOf(TomlDependencySpecification.PathDependency("tool.poetry.dependencies")) + + val result = getDependenciesFromToml(entries, rootIndex, specs) + assertThat(result.map[mainName]).containsExactly(libName) + } + + @Test + fun `tool Pep621Dependency`(@TempDir tempDir: Path): Unit = timeoutRunBlocking { + val mainDir = tempDir.resolve("main").createDirectories() + val libDir = tempDir.resolve("lib").createDirectories() + + val libUri = libDir.toUri() + val toml = PyProjectToml.parse(""" + [project] + name = "main" + version = "1.0" + + [tool.uv] + dev-dependencies = ["lib @ $libUri"] + """.trimIndent()) + + val mainName = ProjectName("main") + val libName = ProjectName("lib") + + val entries = mapOf(mainName to TestProject(toml, mainDir)) + val rootIndex = mapOf(libDir to libName) + val specs = listOf(TomlDependencySpecification.Pep621Dependency("tool.uv.dev-dependencies")) + + val result = getDependenciesFromToml(entries, rootIndex, specs) + assertThat(result.map[mainName]).containsExactly(libName) + } + + @Test + fun `tool GroupPathDependency`(@TempDir tempDir: Path): Unit = timeoutRunBlocking { + val mainDir = tempDir.resolve("main").createDirectories() + val libDir = tempDir.resolve("lib").createDirectories() + + val toml = PyProjectToml.parse(""" + [project] + name = "main" + version = "1.0" + + [tool.poetry.group.test.dependencies] + lib = {path = "../lib"} + """.trimIndent()) + + val mainName = ProjectName("main") + val libName = ProjectName("lib") + + val entries = mapOf(mainName to TestProject(toml, mainDir)) + val rootIndex = mapOf(libDir to libName) + val specs = listOf(TomlDependencySpecification.GroupPathDependency("tool.poetry.group", "dependencies")) + + val result = getDependenciesFromToml(entries, rootIndex, specs) + assertThat(result.map[mainName]).containsExactly(libName) + } + + @Test + fun `non-path dependencies are ignored`(@TempDir tempDir: Path): Unit = timeoutRunBlocking { + val mainDir = tempDir.resolve("main").createDirectories() + + val toml = PyProjectToml.parse(""" + [project] + name = "main" + version = "1.0" + dependencies = ["pytest>=7", "requests"] + + [dependency-groups] + dev = ["black>=23"] + """.trimIndent()) + + val mainName = ProjectName("main") + val entries = mapOf(mainName to TestProject(toml, mainDir)) + + val result = getDependenciesFromToml(entries, emptyMap(), emptyList()) + assertThat(result.map[mainName]).isEmpty() + } + + @Test + fun `empty and missing sections`(@TempDir tempDir: Path): Unit = timeoutRunBlocking { + val mainDir = tempDir.resolve("main").createDirectories() + + val toml = PyProjectToml.parse(""" + [project] + name = "main" + version = "1.0" + """.trimIndent()) + + val mainName = ProjectName("main") + val entries = mapOf(mainName to TestProject(toml, mainDir)) + val specs = listOf( + TomlDependencySpecification.PathDependency("tool.poetry.dependencies"), + TomlDependencySpecification.Pep621Dependency("tool.uv.dev-dependencies"), + TomlDependencySpecification.GroupPathDependency("tool.poetry.group", "dependencies"), + ) + + val result = getDependenciesFromToml(entries, emptyMap(), specs) + assertThat(result.map[mainName]).isEmpty() + } + + @Test + fun `mixed sources collected together`(@TempDir tempDir: Path): Unit = timeoutRunBlocking { + val mainDir = tempDir.resolve("main").createDirectories() + val pep621Lib = tempDir.resolve("pep621-lib").createDirectories() + val groupLib = tempDir.resolve("group-lib").createDirectories() + val toolLib = tempDir.resolve("tool-lib").createDirectories() + + val pep621Uri = pep621Lib.toUri() + val groupUri = groupLib.toUri() + val toml = PyProjectToml.parse(""" + [project] + name = "main" + version = "1.0" + dependencies = ["pep621-lib @ $pep621Uri"] + + [dependency-groups] + dev = ["group-lib @ $groupUri"] + + [tool.poetry.dependencies] + tool-lib = {path = "../tool-lib"} + """.trimIndent()) + + val mainName = ProjectName("main") + val pep621Name = ProjectName("pep621-lib") + val groupName = ProjectName("group-lib") + val toolName = ProjectName("tool-lib") + + val entries = mapOf(mainName to TestProject(toml, mainDir)) + val rootIndex = mapOf(pep621Lib to pep621Name, groupLib to groupName, toolLib to toolName) + val specs = listOf(TomlDependencySpecification.PathDependency("tool.poetry.dependencies")) + + val result = getDependenciesFromToml(entries, rootIndex, specs) + assertThat(result.map[mainName]).containsExactlyInAnyOrder(pep621Name, groupName, toolName) + } +} diff --git a/python/python-pyproject/test/com/intellij/python/junit5Tests/unit/pyproject/PyProjectTomlTest.kt b/python/python-pyproject/test/com/intellij/python/junit5Tests/unit/pyproject/PyProjectTomlTest.kt index dc90ae582ba1..99ba81dced63 100644 --- a/python/python-pyproject/test/com/intellij/python/junit5Tests/unit/pyproject/PyProjectTomlTest.kt +++ b/python/python-pyproject/test/com/intellij/python/junit5Tests/unit/pyproject/PyProjectTomlTest.kt @@ -30,7 +30,7 @@ class PyProjectTomlTest { val configContents = "[proj" // WHEN - val result = PyProjectToml.Companion.parse(configContents) + val result = PyProjectToml.parse(configContents) // THEN Assertions.assertThat(result.toml.errors()).isNotEmpty() @@ -51,7 +51,7 @@ class PyProjectTomlTest { bar="test bar" baz="test baz" """.trimIndent() - val pyproject = PyProjectToml.Companion.parse(configContents) + val pyproject = PyProjectToml.parse(configContents) // WHEN val testTool = pyproject.getTool(TestPyProject) @@ -75,7 +75,7 @@ class PyProjectTomlTest { """.trimIndent() // WHEN - val pyproject = PyProjectToml.Companion.parse(configContents) + val pyproject = PyProjectToml.parse(configContents) val testTool = pyproject.getTool(TestPyProject) // THEN @@ -101,7 +101,7 @@ class PyProjectTomlTest { """.trimIndent() // WHEN - val pyproject = PyProjectToml.Companion.parse(configContents) + val pyproject = PyProjectToml.parse(configContents) val testTool = pyproject.getTool(TestPyProject) // THEN @@ -120,7 +120,7 @@ class PyProjectTomlTest { name="Some project" version="1.2.3" """.trimIndent() - val pyproject = PyProjectToml.Companion.parse(configContents) + val pyproject = PyProjectToml.parse(configContents) // WHEN val testTool = pyproject.getTool(TestPyProject) @@ -133,7 +133,7 @@ class PyProjectTomlTest { @ParameterizedTest(name = "{0}") @MethodSource("parseTestCases") fun parseTests(name: String, pyprojectToml: String, expectedProjectTable: PyProjectTable?, expectedIssues: List) { - val result = PyProjectToml.Companion.parse(pyprojectToml) + val result = PyProjectToml.parse(pyprojectToml) val unwrapped = result assertEquals(expectedProjectTable, unwrapped.project) diff --git a/python/python-uv/backend/src/com/intellij/python/community/impl/uv/backend/UvTool.kt b/python/python-uv/backend/src/com/intellij/python/community/impl/uv/backend/UvTool.kt index bacc1d59194d..1151a8915859 100644 --- a/python/python-uv/backend/src/com/intellij/python/community/impl/uv/backend/UvTool.kt +++ b/python/python-uv/backend/src/com/intellij/python/community/impl/uv/backend/UvTool.kt @@ -7,6 +7,7 @@ import com.intellij.openapi.util.getPathMatcher import com.intellij.python.common.tools.ToolId import com.intellij.python.community.impl.uv.common.UV_TOOL_ID import com.intellij.python.community.impl.uv.common.UV_UI_INFO +import com.intellij.python.pyproject.model.internal.pyProjectToml.TomlDependencySpecification import com.intellij.python.pyproject.model.spi.ProjectDependencies import com.intellij.python.pyproject.model.spi.ProjectName import com.intellij.python.pyproject.model.spi.ProjectStructureInfo @@ -97,6 +98,11 @@ internal class UvTool : Tool { } + override fun getTomlDependencySpecifications(): List = listOf( + TomlDependencySpecification.PathDependency("tool.uv.sources"), + TomlDependencySpecification.Pep621Dependency("tool.uv.dev-dependencies"), + ) + @RequiresBackgroundThread private fun getWorkspaceMembers(toml: TomlTable): WorkspaceInfo? { val workspace = toml.getTable("tool.uv.workspace") ?: return null