diff --git a/python/pluginResources/intellij.python.community.impl.xml b/python/pluginResources/intellij.python.community.impl.xml
index 1e26de2775b2..a48958a40737 100644
--- a/python/pluginResources/intellij.python.community.impl.xml
+++ b/python/pluginResources/intellij.python.community.impl.xml
@@ -767,6 +767,10 @@
interface="com.jetbrains.python.packaging.management.PythonPackageManagerProvider"
dynamic="true"/>
+
+
@@ -884,6 +888,9 @@
+
+
+
diff --git a/python/src/com/jetbrains/python/packaging/PythonDependenciesExtractor.kt b/python/src/com/jetbrains/python/packaging/PythonDependenciesExtractor.kt
new file mode 100644
index 000000000000..47b1ff60f1ca
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/PythonDependenciesExtractor.kt
@@ -0,0 +1,29 @@
+// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.packaging
+
+import com.intellij.openapi.extensions.ExtensionPointName
+import com.intellij.openapi.module.Module
+import com.intellij.openapi.projectRoots.Sdk
+import com.jetbrains.python.packaging.common.PythonPackage
+import org.jetbrains.annotations.ApiStatus
+
+@ApiStatus.Internal
+interface PythonDependenciesExtractor {
+
+ suspend fun extract(module: Module): List
+
+ companion object {
+ fun forSdk(sdk: Sdk): PythonDependenciesExtractor? =
+ PythonDependenciesExtractorProvider.EP_NAME.extensionList.firstNotNullOf { it.createExtractor(sdk) }
+ }
+}
+
+@ApiStatus.Internal
+interface PythonDependenciesExtractorProvider {
+
+ fun createExtractor(sdk: Sdk): PythonDependenciesExtractor?
+
+ companion object {
+ val EP_NAME: ExtensionPointName = ExtensionPointName.create("Pythonid.PyProjectDependenciesExtractorProvider")
+ }
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/conda/CompositePythonPackageManager.kt b/python/src/com/jetbrains/python/packaging/conda/CompositePythonPackageManager.kt
index be8b3950a7a2..c7da373784d4 100644
--- a/python/src/com/jetbrains/python/packaging/conda/CompositePythonPackageManager.kt
+++ b/python/src/com/jetbrains/python/packaging/conda/CompositePythonPackageManager.kt
@@ -19,6 +19,7 @@ internal class CompositePythonPackageManager(
@Volatile
override var installedPackages: List = emptyList()
+ override var dependencies: List = emptyList()
override var repositoryManager: PythonRepositoryManager =
CompositePythonRepositoryManager(project, managers.map { it.repositoryManager })
@@ -91,6 +92,10 @@ internal class CompositePythonPackageManager(
}
}
+ override suspend fun reloadDependencies(): List = dependencies
+
+ override fun listDependencies(): List = dependencies
+
private suspend fun processPackageOperation(
errorMessageKey: String,
operation: suspend (PythonPackageManager) -> Result<*>,
diff --git a/python/src/com/jetbrains/python/packaging/conda/CondaPackageManager.kt b/python/src/com/jetbrains/python/packaging/conda/CondaPackageManager.kt
index 823d5bcb5c2f..6b8be72b4167 100644
--- a/python/src/com/jetbrains/python/packaging/conda/CondaPackageManager.kt
+++ b/python/src/com/jetbrains/python/packaging/conda/CondaPackageManager.kt
@@ -41,6 +41,7 @@ private fun PythonPackageInstallRequest.buildInstallationArguments(): Result = emptyList()
+ override var dependencies: List = emptyList()
override val repositoryManager: PythonRepositoryManager = CondaRepositoryManger(project, sdk)
override suspend fun loadOutdatedPackagesCommand(): Result> {
@@ -92,6 +93,10 @@ class CondaPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(pro
Result.failure(ex)
}
+ override suspend fun reloadDependencies(): List = dependencies
+
+ override fun listDependencies(): List = dependencies
+
private fun parseCondaPackageList(text: String): List {
return text.lineSequence()
.filterNot { it.startsWith("#") }
diff --git a/python/src/com/jetbrains/python/packaging/management/PythonPackageManager.kt b/python/src/com/jetbrains/python/packaging/management/PythonPackageManager.kt
index defd38ddd9c4..534be1b9c9be 100644
--- a/python/src/com/jetbrains/python/packaging/management/PythonPackageManager.kt
+++ b/python/src/com/jetbrains/python/packaging/management/PythonPackageManager.kt
@@ -43,6 +43,8 @@ fun PythonRepositoryPackageSpecification.toInstallRequest(): PythonPackageInstal
@ApiStatus.Experimental
abstract class PythonPackageManager(val project: Project, val sdk: Sdk) {
abstract var installedPackages: List
+ @get:ApiStatus.Internal @set:ApiStatus.Internal
+ protected abstract var dependencies: List
@ApiStatus.Internal
@Volatile
@@ -130,14 +132,16 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) {
abstract suspend fun installPackageCommand(installRequest: PythonPackageInstallRequest, options: List): Result
@ApiStatus.Internal
abstract suspend fun updatePackageCommand(specification: PythonRepositoryPackageSpecification): Result
-
@ApiStatus.Internal
abstract suspend fun uninstallPackageCommand(pkg: PythonPackage): Result
-
@ApiStatus.Internal
abstract suspend fun reloadPackagesCommand(): Result>
@ApiStatus.Internal
abstract suspend fun loadOutdatedPackagesCommand(): Result>
+ @ApiStatus.Internal
+ abstract suspend fun reloadDependencies(): List
+ @ApiStatus.Internal
+ abstract fun listDependencies(): List
internal suspend fun refreshPaths() {
edtWriteAction {
diff --git a/python/src/com/jetbrains/python/packaging/pip/PipPythonPackageManager.kt b/python/src/com/jetbrains/python/packaging/pip/PipPythonPackageManager.kt
index db9feae9917c..7c1a8320ac3e 100644
--- a/python/src/com/jetbrains/python/packaging/pip/PipPythonPackageManager.kt
+++ b/python/src/com/jetbrains/python/packaging/pip/PipPythonPackageManager.kt
@@ -43,6 +43,7 @@ private fun PythonPackageInstallRequest.buildPipInstallArguments(): List
open class PipPythonPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(project, sdk) {
@Volatile
override var installedPackages: List = emptyList()
+ override var dependencies: List = emptyList()
override val repositoryManager: PythonRepositoryManager = PipRepositoryManager(project)
override suspend fun installPackageCommand(installRequest: PythonPackageInstallRequest, options: List): Result {
@@ -109,6 +110,10 @@ open class PipPythonPackageManager(project: Project, sdk: Sdk) : PythonPackageMa
return Result.failure(ex)
}
}
+
+ override suspend fun reloadDependencies(): List = dependencies
+
+ override fun listDependencies(): List = dependencies
}
@ApiStatus.Internal
diff --git a/python/src/com/jetbrains/python/sdk/poetry/PoetryDependenciesExtractor.kt b/python/src/com/jetbrains/python/sdk/poetry/PoetryDependenciesExtractor.kt
new file mode 100644
index 000000000000..3b496849d3f7
--- /dev/null
+++ b/python/src/com/jetbrains/python/sdk/poetry/PoetryDependenciesExtractor.kt
@@ -0,0 +1,43 @@
+// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.sdk.poetry
+
+import com.intellij.openapi.module.Module
+import com.intellij.openapi.projectRoots.Sdk
+import com.jetbrains.python.packaging.PythonDependenciesExtractor
+import com.jetbrains.python.packaging.PythonDependenciesExtractorProvider
+import com.jetbrains.python.packaging.common.PythonPackage
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+/**
+ * Extractor for top-level dependencies from pyproject.toml for Poetry SDKs.
+ */
+internal class PoetryDependenciesExtractor(private val sdk: Sdk) : PythonDependenciesExtractor {
+ override suspend fun extract(module: Module): List {
+ val output = runPoetryWithSdk(sdk, "show", "--top-level")
+ val data = output.getOrNull()?.ifEmpty { return emptyList() } ?: return emptyList()
+ return parsePackageData(data.lines())
+ }
+
+ private suspend fun parsePackageData(lines: List): List = withContext(Dispatchers.Default) {
+ val packageList = mutableListOf()
+
+ for (line in lines) {
+ val parts = line.split(WHITESPACE_REGEX)
+ val packageName = parts[0]
+ val version = parts.getOrElse(1) { "" }
+ packageList.add(PythonPackage(packageName, version, false))
+ }
+
+ packageList
+ }
+}
+
+private class PoetryDependenciesExtractorProvider: PythonDependenciesExtractorProvider {
+ override fun createExtractor(sdk: Sdk): PythonDependenciesExtractor? {
+ if (!sdk.isPoetry) return null
+ return PoetryDependenciesExtractor(sdk)
+ }
+}
+
+private val WHITESPACE_REGEX = Regex("\\s+")
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageManager.kt b/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageManager.kt
index d6581d4ed2e8..aad5c54ca84b 100644
--- a/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageManager.kt
+++ b/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageManager.kt
@@ -2,7 +2,9 @@
package com.jetbrains.python.sdk.poetry
import com.intellij.openapi.project.Project
+import com.intellij.openapi.project.modules
import com.intellij.openapi.projectRoots.Sdk
+import com.jetbrains.python.packaging.PythonDependenciesExtractor
import com.jetbrains.python.packaging.common.PythonOutdatedPackage
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification
@@ -10,6 +12,7 @@ import com.jetbrains.python.packaging.management.PythonPackageInstallRequest
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.management.PythonRepositoryManager
import com.jetbrains.python.packaging.pip.PipRepositoryManager
+import com.jetbrains.python.sdk.pythonSdk
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.TestOnly
@@ -17,6 +20,7 @@ import org.jetbrains.annotations.TestOnly
class PoetryPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(project, sdk) {
@Volatile
override var installedPackages: List = emptyList()
+ override var dependencies: List = emptyList()
override val repositoryManager: PythonRepositoryManager = PipRepositoryManager(project)
@@ -64,6 +68,15 @@ class PoetryPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(pr
it.values.toList()
}
+ override suspend fun reloadDependencies(): List {
+ val dependenciesExtractor = PythonDependenciesExtractor.forSdk(sdk) ?: return emptyList()
+ val targetModule = project.modules.find { it.pythonSdk == sdk } ?: return emptyList()
+ dependencies = dependenciesExtractor.extract(targetModule)
+ return dependencies
+ }
+
+ override fun listDependencies(): List = dependencies
+
private fun PythonRepositoryPackageSpecification.getPackageWithVersionInPoetryFormat(): String {
return versionSpec?.let { "$name@${it.presentableText}" } ?: name
}
diff --git a/python/src/com/jetbrains/python/sdk/uv/Uv.kt b/python/src/com/jetbrains/python/sdk/uv/Uv.kt
index e75a84486c4a..c28930796d61 100644
--- a/python/src/com/jetbrains/python/sdk/uv/Uv.kt
+++ b/python/src/com/jetbrains/python/sdk/uv/Uv.kt
@@ -1,4 +1,4 @@
-// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.sdk.uv
import com.jetbrains.python.errorProcessing.PyExecResult
@@ -35,6 +35,7 @@ interface UvLowLevel {
suspend fun listPackages(): PyExecResult>
suspend fun listOutdatedPackages(): PyResult>
+ suspend fun listTopLevelPackages(): PyResult>
suspend fun listPackageRequirements(name: PythonPackage): PyResult>
suspend fun isProjectSynced(inexact: Boolean): PyExecResult
diff --git a/python/src/com/jetbrains/python/sdk/uv/UvDependenciesExtractor.kt b/python/src/com/jetbrains/python/sdk/uv/UvDependenciesExtractor.kt
new file mode 100644
index 000000000000..6cb01e5b3924
--- /dev/null
+++ b/python/src/com/jetbrains/python/sdk/uv/UvDependenciesExtractor.kt
@@ -0,0 +1,30 @@
+// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.sdk.uv
+
+import com.intellij.openapi.module.Module
+import com.intellij.openapi.projectRoots.Sdk
+import com.jetbrains.python.packaging.PythonDependenciesExtractor
+import com.jetbrains.python.packaging.PythonDependenciesExtractorProvider
+import com.jetbrains.python.packaging.common.PythonPackage
+import com.jetbrains.python.sdk.basePath
+import com.jetbrains.python.sdk.uv.impl.createUvCli
+import com.jetbrains.python.sdk.uv.impl.createUvLowLevel
+import java.nio.file.Path
+
+/**
+ * Extractor for top-level dependencies from pyproject.toml for UV SDKs.
+ */
+internal class UvDependenciesExtractor(private val uvWorkingDirectory: Path?) : PythonDependenciesExtractor {
+ override suspend fun extract(module: Module): List {
+ val uvWorkingDirectory = uvWorkingDirectory ?: Path.of(module.basePath!!)
+ val uv = createUvLowLevel(uvWorkingDirectory, createUvCli())
+ return uv.listTopLevelPackages().successOrNull ?: emptyList()
+ }
+}
+
+private class UvDependenciesExtractorProvider: PythonDependenciesExtractorProvider {
+ override fun createExtractor(sdk: Sdk): PythonDependenciesExtractor? {
+ val data = sdk.sdkAdditionalData as? UvSdkAdditionalData ?: return null
+ return UvDependenciesExtractor(data.uvWorkingDirectory)
+ }
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/sdk/uv/UvPackageManager.kt b/python/src/com/jetbrains/python/sdk/uv/UvPackageManager.kt
index 983b8f4627b6..5cf0fdc4e982 100644
--- a/python/src/com/jetbrains/python/sdk/uv/UvPackageManager.kt
+++ b/python/src/com/jetbrains/python/sdk/uv/UvPackageManager.kt
@@ -2,20 +2,24 @@
package com.jetbrains.python.sdk.uv
import com.intellij.openapi.project.Project
+import com.intellij.openapi.project.modules
import com.intellij.openapi.projectRoots.Sdk
import com.jetbrains.python.errorProcessing.PyExecResult
import com.jetbrains.python.errorProcessing.asKotlinResult
+import com.jetbrains.python.packaging.PythonDependenciesExtractor
import com.jetbrains.python.packaging.common.PythonOutdatedPackage
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification
import com.jetbrains.python.packaging.management.*
import com.jetbrains.python.packaging.pip.PipRepositoryManager
+import com.jetbrains.python.sdk.pythonSdk
import com.jetbrains.python.sdk.uv.impl.createUvCli
import com.jetbrains.python.sdk.uv.impl.createUvLowLevel
import java.nio.file.Path
internal class UvPackageManager(project: Project, sdk: Sdk, private val uv: UvLowLevel) : PythonPackageManager(project, sdk) {
override var installedPackages: List = emptyList()
+ override var dependencies: List = emptyList()
override val repositoryManager: PythonRepositoryManager = PipRepositoryManager(project)
@@ -72,6 +76,15 @@ internal class UvPackageManager(project: Project, sdk: Sdk, private val uv: UvLo
suspend fun lock(): PyExecResult {
return uv.lock()
}
+
+ override suspend fun reloadDependencies(): List {
+ val dependenciesExtractor = PythonDependenciesExtractor.forSdk(sdk) ?: return emptyList()
+ val targetModule = project.modules.find { it.pythonSdk == sdk } ?: return emptyList()
+ dependencies = dependenciesExtractor.extract(targetModule)
+ return dependencies
+ }
+
+ override fun listDependencies(): List = dependencies
}
class UvPackageManagerProvider : PythonPackageManagerProvider {
diff --git a/python/src/com/jetbrains/python/sdk/uv/impl/UvLowLevel.kt b/python/src/com/jetbrains/python/sdk/uv/impl/UvLowLevel.kt
index d94a2064c670..2258fe9fec52 100644
--- a/python/src/com/jetbrains/python/sdk/uv/impl/UvLowLevel.kt
+++ b/python/src/com/jetbrains/python/sdk/uv/impl/UvLowLevel.kt
@@ -5,12 +5,9 @@ import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.intellij.util.io.delete
-import com.jetbrains.python.errorProcessing.ExecError
-import com.jetbrains.python.errorProcessing.ExecErrorReason
-import com.jetbrains.python.errorProcessing.PyError
+import com.jetbrains.python.errorProcessing.*
import com.jetbrains.python.errorProcessing.PyExecResult
import com.jetbrains.python.errorProcessing.PyResult
-import com.jetbrains.python.errorProcessing.failure
import com.jetbrains.python.onFailure
import com.jetbrains.python.packaging.common.NormalizedPythonPackageName
import com.jetbrains.python.packaging.common.PythonOutdatedPackage
@@ -21,6 +18,8 @@ import com.jetbrains.python.sdk.uv.UvCli
import com.jetbrains.python.sdk.uv.UvLowLevel
import com.jetbrains.python.venvReader.VirtualEnvReader
import com.jetbrains.python.venvReader.tryResolvePath
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
import java.nio.file.Path
import kotlin.io.path.exists
import kotlin.io.path.pathString
@@ -128,6 +127,13 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev
}
}
+ override suspend fun listTopLevelPackages(): PyResult> {
+ val out = uvCli.runUv(cwd, "tree", "--depth=1")
+ .getOr { return it }
+
+ return PyExecResult.success(parsePackageList(out))
+ }
+
override suspend fun listPackageRequirements(name: PythonPackage): PyResult> {
val out = uvCli.runUv(cwd, "pip", "show", name.name)
.getOr { return it }
@@ -249,6 +255,19 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev
return uvCli.runUv(cwd, "lock")
}
+ suspend fun parsePackageList(input: String): List = withContext(Dispatchers.Default) {
+ val packageList = mutableListOf()
+
+ for (line in input.lines().drop(1)) {
+ val parts = line.trim().split(WHITESPACE_REGEX).drop(1)
+ val packageName = parts[0]
+ val version = parts.getOrElse(1) { "" }
+ packageList.add(PythonPackage(packageName, version, false))
+ }
+
+ packageList
+ }
+
private fun parsePackageRequirements(input: String): List {
val requiresLine = input.lines().find { it.startsWith(REQUIRES_LINE_PREFIX) } ?: return emptyList()
@@ -261,6 +280,7 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev
}
companion object {
+ private val WHITESPACE_REGEX = Regex("\\s+")
private const val REQUIRES_LINE_PREFIX = "Requires:"
}
}
diff --git a/python/testSrc/com/jetbrains/python/packaging/management/TestPackageManager.kt b/python/testSrc/com/jetbrains/python/packaging/management/TestPackageManager.kt
index 60dd36305daa..f57b0ca12054 100644
--- a/python/testSrc/com/jetbrains/python/packaging/management/TestPackageManager.kt
+++ b/python/testSrc/com/jetbrains/python/packaging/management/TestPackageManager.kt
@@ -13,7 +13,7 @@ import org.jetbrains.annotations.TestOnly
class TestPythonPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(project, sdk) {
override var installedPackages: List = DEFAULT_PACKAGES.toMutableList()
-
+ override var dependencies: List = emptyList()
private var packageNames: List = emptyList()
private var packageDetails: PythonPackageDetails? = null
@@ -56,6 +56,10 @@ class TestPythonPackageManager(project: Project, sdk: Sdk) : PythonPackageManage
return Result.success(installedPackages)
}
+ override suspend fun reloadDependencies(): List = dependencies
+
+ override fun listDependencies(): List = dependencies
+
private fun findPackageByName(name: String): PythonPackage? {
return installedPackages.find { it.name == name }
}