diff --git a/python/pluginResources/intellij.python.community.impl.xml b/python/pluginResources/intellij.python.community.impl.xml
index fbcde3d9dffb..1e26de2775b2 100644
--- a/python/pluginResources/intellij.python.community.impl.xml
+++ b/python/pluginResources/intellij.python.community.impl.xml
@@ -778,6 +778,10 @@
+
+
@@ -880,6 +884,9 @@
+
+
+
diff --git a/python/src/com/jetbrains/python/packaging/common/packages.kt b/python/src/com/jetbrains/python/packaging/common/packages.kt
index 5e1c6d5b8f86..4c079e2d794c 100644
--- a/python/src/com/jetbrains/python/packaging/common/packages.kt
+++ b/python/src/com/jetbrains/python/packaging/common/packages.kt
@@ -6,14 +6,24 @@ import com.jetbrains.python.packaging.pyRequirementVersionSpec
import com.jetbrains.python.packaging.repository.PyPackageRepository
import com.jetbrains.python.packaging.requirement.PyRequirementRelation
import com.jetbrains.python.packaging.requirement.PyRequirementVersionSpec
+import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nls
+@ApiStatus.Internal
+@JvmInline
+value class NormalizedPythonPackageName private constructor(val name: String) {
+ companion object {
+ fun from(name: String): NormalizedPythonPackageName =
+ NormalizedPythonPackageName(normalizePackageName(name))
+ }
+}
+
open class PythonPackage(name: String, val version: String, val isEditableMode: Boolean) {
companion object {
private const val HASH_MULTIPLIER = 31
}
- val name: String = normalizePackageName(name)
+ val name: String = NormalizedPythonPackageName.from(name).name
val presentableName: String = name
override fun toString(): String {
diff --git a/python/src/com/jetbrains/python/packaging/packageRequires/PythonPackageRequiresExtractor.kt b/python/src/com/jetbrains/python/packaging/packageRequires/PythonPackageRequiresExtractor.kt
new file mode 100644
index 000000000000..2fe94348c225
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/packageRequires/PythonPackageRequiresExtractor.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.packaging.packageRequires
+
+import com.intellij.openapi.extensions.ExtensionPointName
+import com.intellij.openapi.module.Module
+import com.intellij.openapi.projectRoots.Sdk
+import com.jetbrains.python.packaging.common.NormalizedPythonPackageName
+import com.jetbrains.python.packaging.common.PythonPackage
+import org.jetbrains.annotations.ApiStatus
+
+@ApiStatus.Internal
+interface PythonPackageRequiresExtractor {
+
+ suspend fun extract(pkg: PythonPackage, module: Module): List
+
+ companion object {
+ fun forSdk(sdk: Sdk): PythonPackageRequiresExtractor? =
+ PythonPackageRequiresExtractorProvider.EP_NAME.extensionList.firstNotNullOf { it.createExtractor(sdk) }
+ }
+}
+
+@ApiStatus.Internal
+interface PythonPackageRequiresExtractorProvider {
+
+ fun createExtractor(sdk: Sdk): PythonPackageRequiresExtractor?
+
+ companion object {
+ val EP_NAME: ExtensionPointName = ExtensionPointName.create("Pythonid.PythonPackageRequiresExtractorProvider")
+ }
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/poetry/packaging/PoetryPythonPackageRequiresExtractor.kt b/python/src/com/jetbrains/python/poetry/packaging/PoetryPythonPackageRequiresExtractor.kt
new file mode 100644
index 000000000000..6dd54adbd0c4
--- /dev/null
+++ b/python/src/com/jetbrains/python/poetry/packaging/PoetryPythonPackageRequiresExtractor.kt
@@ -0,0 +1,60 @@
+// 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.poetry.packaging
+
+import com.intellij.openapi.diagnostic.thisLogger
+import com.intellij.openapi.module.Module
+import com.intellij.openapi.projectRoots.Sdk
+import com.jetbrains.python.packaging.common.NormalizedPythonPackageName
+import com.jetbrains.python.packaging.common.PythonPackage
+import com.jetbrains.python.packaging.packageRequires.PythonPackageRequiresExtractor
+import com.jetbrains.python.packaging.packageRequires.PythonPackageRequiresExtractorProvider
+import com.jetbrains.python.sdk.poetry.isPoetry
+import com.jetbrains.python.sdk.poetry.runPoetryWithSdk
+
+internal class PoetryPackageRequiresExtractor(private val sdk: Sdk) : PythonPackageRequiresExtractor {
+
+ override suspend fun extract(pkg: PythonPackage, module: Module): List {
+ val output = runPoetryWithSdk(sdk, "show", pkg.name)
+ val data = output.getOrElse {
+ thisLogger().info("extracting requires for package ${pkg.name}: error. Output: \n${it.stackTraceToString()}")
+ return emptyList()
+ }
+ return parsePackageData(data.lines())
+ }
+
+ private fun parsePackageData(lines: List): List {
+ val packages = mutableListOf()
+
+ fun parseDependencyLine(line: String): NormalizedPythonPackageName? {
+ val depLine = line.removePrefix(DEPENDENCY_PREFIX).trim()
+ val name = depLine.split(" ", limit = 2).let { parts ->
+ parts.getOrNull(0)?.trim()
+ }
+ return name?.let { NormalizedPythonPackageName.Companion.from(name) }
+ }
+
+ var inDependenciesSection = false
+ for (line in lines) {
+ val trimmedLine = line.trim()
+ when {
+ trimmedLine == DEPENDENCIES_MARKER -> inDependenciesSection = true
+ inDependenciesSection && line.startsWith(DEPENDENCY_PREFIX) -> parseDependencyLine(line)?.let { packages.add(it) }
+ inDependenciesSection && trimmedLine.isEmpty() -> break
+ }
+ }
+
+ return packages
+ }
+
+ private companion object {
+ const val DEPENDENCIES_MARKER = "dependencies"
+ const val DEPENDENCY_PREFIX = " - "
+ }
+}
+
+private class PoetryRequiresExtractorProvider: PythonPackageRequiresExtractorProvider {
+ override fun createExtractor(sdk: Sdk): PythonPackageRequiresExtractor? {
+ if (!sdk.isPoetry) return null
+ return PoetryPackageRequiresExtractor(sdk)
+ }
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/sdk/uv/Uv.kt b/python/src/com/jetbrains/python/sdk/uv/Uv.kt
index 840e2d53a709..e75a84486c4a 100644
--- a/python/src/com/jetbrains/python/sdk/uv/Uv.kt
+++ b/python/src/com/jetbrains/python/sdk/uv/Uv.kt
@@ -1,8 +1,9 @@
-// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+// Copyright 2000-2024 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
import com.jetbrains.python.errorProcessing.PyResult
+import com.jetbrains.python.packaging.common.NormalizedPythonPackageName
import com.jetbrains.python.packaging.common.PythonOutdatedPackage
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.management.PythonPackageInstallRequest
@@ -34,6 +35,7 @@ interface UvLowLevel {
suspend fun listPackages(): PyExecResult>
suspend fun listOutdatedPackages(): PyResult>
+ suspend fun listPackageRequirements(name: PythonPackage): PyResult>
suspend fun isProjectSynced(inexact: Boolean): PyExecResult
suspend fun isScriptSynced(inexact: Boolean, scriptPath: Path): PyExecResult
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 4544028ecc70..d94a2064c670 100644
--- a/python/src/com/jetbrains/python/sdk/uv/impl/UvLowLevel.kt
+++ b/python/src/com/jetbrains/python/sdk/uv/impl/UvLowLevel.kt
@@ -12,6 +12,7 @@ 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
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.management.PythonPackageInstallRequest
@@ -127,6 +128,13 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev
}
}
+ override suspend fun listPackageRequirements(name: PythonPackage): PyResult> {
+ val out = uvCli.runUv(cwd, "pip", "show", name.name)
+ .getOr { return it }
+
+ return PyExecResult.success(parsePackageRequirements(out))
+ }
+
override suspend fun installPackage(name: PythonPackageInstallRequest, options: List): PyExecResult {
uvCli.runUv(cwd, "pip", "install", name.formatPackageName(), *options.toTypedArray())
.onFailure { return PyResult.failure(it) }
@@ -235,11 +243,26 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev
override suspend fun sync(): PyExecResult {
return uvCli.runUv(cwd, "sync")
- }
+ }
override suspend fun lock(): PyExecResult {
return uvCli.runUv(cwd, "lock")
}
+
+ private fun parsePackageRequirements(input: String): List {
+ val requiresLine = input.lines().find { it.startsWith(REQUIRES_LINE_PREFIX) } ?: return emptyList()
+
+ return requiresLine
+ .removePrefix(REQUIRES_LINE_PREFIX)
+ .split(",")
+ .map { it.trim() }
+ .filter { it.isNotEmpty() }
+ .map { NormalizedPythonPackageName.from(it) }
+ }
+
+ companion object {
+ private const val REQUIRES_LINE_PREFIX = "Requires:"
+ }
}
fun createUvLowLevel(cwd: Path, uvCli: UvCli = createUvCli()): UvLowLevel {
diff --git a/python/src/com/jetbrains/python/uv/packaging/UvPythonPackageRequiresExtractor.kt b/python/src/com/jetbrains/python/uv/packaging/UvPythonPackageRequiresExtractor.kt
new file mode 100644
index 000000000000..335c7cae5fd1
--- /dev/null
+++ b/python/src/com/jetbrains/python/uv/packaging/UvPythonPackageRequiresExtractor.kt
@@ -0,0 +1,33 @@
+// 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.uv.packaging
+
+import com.intellij.openapi.diagnostic.thisLogger
+import com.intellij.openapi.module.Module
+import com.intellij.openapi.projectRoots.Sdk
+import com.jetbrains.python.packaging.common.NormalizedPythonPackageName
+import com.jetbrains.python.packaging.common.PythonPackage
+import com.jetbrains.python.packaging.packageRequires.PythonPackageRequiresExtractor
+import com.jetbrains.python.packaging.packageRequires.PythonPackageRequiresExtractorProvider
+import com.jetbrains.python.sdk.basePath
+import com.jetbrains.python.sdk.uv.UvSdkAdditionalData
+import com.jetbrains.python.sdk.uv.impl.createUvCli
+import com.jetbrains.python.sdk.uv.impl.createUvLowLevel
+import java.nio.file.Path
+
+internal class UvPackageRequiresExtractor(private val uvWorkingDirectory: Path?) : PythonPackageRequiresExtractor {
+ override suspend fun extract(pkg: PythonPackage, module: Module): List {
+ val uvWorkingDirectory = uvWorkingDirectory ?: Path.of(module.basePath!!)
+ val uv = createUvLowLevel(uvWorkingDirectory, createUvCli())
+ return uv.listPackageRequirements(pkg).getOr {
+ thisLogger().info("extracting requires for package ${pkg.name}: error. Output: \n${it.error}")
+ return emptyList()
+ }
+ }
+}
+
+private class UvPackageRequiresExtractorProvider: PythonPackageRequiresExtractorProvider {
+ override fun createExtractor(sdk: Sdk): PythonPackageRequiresExtractor? {
+ val data = sdk.sdkAdditionalData as? UvSdkAdditionalData ?: return null
+ return UvPackageRequiresExtractor(data.uvWorkingDirectory)
+ }
+}
\ No newline at end of file