diff --git a/python/intellij.python.community.impl.iml b/python/intellij.python.community.impl.iml
index f21de45119cc..448a4a2411d2 100644
--- a/python/intellij.python.community.impl.iml
+++ b/python/intellij.python.community.impl.iml
@@ -166,6 +166,8 @@
+
+
diff --git a/python/openapi/src/com/jetbrains/python/systemPythonSpi/SystemPythonProvider.kt b/python/openapi/src/com/jetbrains/python/systemPythonSpi/SystemPythonProvider.kt
index 4a67680f661f..2736365472ff 100644
--- a/python/openapi/src/com/jetbrains/python/systemPythonSpi/SystemPythonProvider.kt
+++ b/python/openapi/src/com/jetbrains/python/systemPythonSpi/SystemPythonProvider.kt
@@ -14,5 +14,5 @@ interface SystemPythonProvider {
val EP: ExtensionPointName = ExtensionPointName("Pythonid.systemPythonProvider")
}
- suspend fun findSystemPythons(eelApi: EelApi): Set
+ suspend fun findSystemPythons(eelApi: EelApi): Result>
}
\ No newline at end of file
diff --git a/python/pluginCore/resources/META-INF/plugin.xml b/python/pluginCore/resources/META-INF/plugin.xml
index 2e7f8d90a465..fc04f692236c 100644
--- a/python/pluginCore/resources/META-INF/plugin.xml
+++ b/python/pluginCore/resources/META-INF/plugin.xml
@@ -872,6 +872,8 @@ The Python plug-in provides smart editing for Python scripts. The feature set of
+
+
diff --git a/python/services/system-python/src/com/intellij/python/community/services/systemPython/SystemPythonService.kt b/python/services/system-python/src/com/intellij/python/community/services/systemPython/SystemPythonService.kt
index b02e4372381a..3d957097c7f3 100644
--- a/python/services/system-python/src/com/intellij/python/community/services/systemPython/SystemPythonService.kt
+++ b/python/services/system-python/src/com/intellij/python/community/services/systemPython/SystemPythonService.kt
@@ -76,7 +76,7 @@ private class SystemPythonServiceImpl : SystemPythonService, SimplePersistentSta
val pythonsFromExtensions = SystemPythonProvider.EP
.extensionList
- .flatMap { it.findSystemPythons(eelApi) }.filter { it.getEelDescriptor().upgrade() == eelApi }
+ .flatMap { it.findSystemPythons(eelApi).getOrNull() ?: emptyList() }.filter { it.getEelDescriptor().upgrade() == eelApi }
val badPythons = mutableSetOf()
val pythons = corePythons + pythonsFromExtensions + state.userProvidedPythons.filter { it.getEelDescriptor() == eelApi.descriptor }
diff --git a/python/services/system-python/tests/com/intellij/python/junit5Tests/env/systemPython/impl/EnvTestPythonProvider.kt b/python/services/system-python/tests/com/intellij/python/junit5Tests/env/systemPython/impl/EnvTestPythonProvider.kt
index f43c11e06472..1252e48c6e36 100644
--- a/python/services/system-python/tests/com/intellij/python/junit5Tests/env/systemPython/impl/EnvTestPythonProvider.kt
+++ b/python/services/system-python/tests/com/intellij/python/junit5Tests/env/systemPython/impl/EnvTestPythonProvider.kt
@@ -15,16 +15,20 @@ import kotlinx.coroutines.flow.toSet
* Register tests pythons as system pythons
*/
internal class EnvTestPythonProvider : SystemPythonProvider {
- override suspend fun findSystemPythons(eelApi: EelApi): Set {
- if (eelApi != localEel) return emptySet()
- return TypeVanillaPython3
- .getTestEnvironments()
- .map { (python, closeable) ->
- Disposer.register(ApplicationManager.getApplication()) {
- closeable.close()
- }
- python
- }
- .toSet()
+ override suspend fun findSystemPythons(eelApi: EelApi): Result> {
+ var pythons = emptySet()
+ if (eelApi == localEel) {
+ pythons = TypeVanillaPython3
+ .getTestEnvironments()
+ .map { (python, closeable) ->
+ Disposer.register(ApplicationManager.getApplication()) {
+ closeable.close()
+ }
+
+ python
+ }.toSet()
+ }
+
+ return Result.success(pythons)
}
}
\ 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 8daec67c4e62..1e95bddc6b6d 100644
--- a/python/src/com/jetbrains/python/sdk/uv/Uv.kt
+++ b/python/src/com/jetbrains/python/sdk/uv/Uv.kt
@@ -13,8 +13,7 @@ interface UvCli {
interface UvLowLevel {
suspend fun initializeEnvironment(init: Boolean, python: Path?): Result
- suspend fun listPackages(): Result>
- suspend fun listOutdatedPackages(): Result>
+ suspend fun discoverUvInstalledPythons(): Result>
/**
* Manage project dependencies by adding/removing them to the project along side installation
@@ -27,4 +26,7 @@ interface UvLowLevel {
*/
suspend fun installPackage(name: PythonPackageSpecification, options: List): Result
suspend fun uninstallPackage(name: PythonPackage): Result
+
+ suspend fun listPackages(): Result>
+ suspend fun listOutdatedPackages(): Result>
}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/sdk/uv/UvExt.kt b/python/src/com/jetbrains/python/sdk/uv/UvExt.kt
index 38e69b2f1438..7b0740893409 100644
--- a/python/src/com/jetbrains/python/sdk/uv/UvExt.kt
+++ b/python/src/com/jetbrains/python/sdk/uv/UvExt.kt
@@ -13,11 +13,11 @@ import com.jetbrains.python.icons.PythonIcons
import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.uv.impl.createUvCli
import com.jetbrains.python.sdk.uv.impl.createUvLowLevel
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
import java.nio.file.Path
import javax.swing.Icon
import kotlin.io.path.pathString
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
internal val Sdk.isUv: Boolean
get() = sdkAdditionalData is UvSdkAdditionalData
diff --git a/python/src/com/jetbrains/python/sdk/uv/UvSystemPythonProvider.kt b/python/src/com/jetbrains/python/sdk/uv/UvSystemPythonProvider.kt
new file mode 100644
index 000000000000..d02508b0e1d7
--- /dev/null
+++ b/python/src/com/jetbrains/python/sdk/uv/UvSystemPythonProvider.kt
@@ -0,0 +1,22 @@
+// 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.platform.eel.EelApi
+import com.intellij.platform.eel.provider.localEel
+import com.jetbrains.python.PythonBinary
+import com.jetbrains.python.sdk.uv.impl.createUvLowLevel
+import com.jetbrains.python.sdk.uv.impl.hasUvExecutable
+import com.jetbrains.python.systemPythonSpi.SystemPythonProvider
+import java.nio.file.Path
+
+class UvSystemPythonProvider : SystemPythonProvider {
+ override suspend fun findSystemPythons(eelApi: EelApi): Result> {
+ if (eelApi != localEel || !hasUvExecutable()) {
+ // TODO: support for remote execution
+ return Result.success(emptySet())
+ }
+
+ val uv = createUvLowLevel(Path.of("."))
+ return uv.discoverUvInstalledPythons()
+ }
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/sdk/uv/impl/UvCli.kt b/python/src/com/jetbrains/python/sdk/uv/impl/UvCli.kt
index e0b4c25b451d..9fbf23a17d68 100644
--- a/python/src/com/jetbrains/python/sdk/uv/impl/UvCli.kt
+++ b/python/src/com/jetbrains/python/sdk/uv/impl/UvCli.kt
@@ -93,6 +93,10 @@ fun setUvExecutable(path: Path) {
PropertiesComponent.getInstance().uvPath = path
}
-fun createUvCli(dispatcher: CoroutineDispatcher = Dispatchers.IO, uv: Path? = null): UvCli {
+fun hasUvExecutable(): Boolean {
+ return getUvExecutable() != null
+}
+
+fun createUvCli(uv: Path? = null, dispatcher: CoroutineDispatcher = Dispatchers.IO): UvCli {
return UvCliImpl(dispatcher, uv)
}
\ No newline at end of file
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 47f5b047d6c0..8cecf8cd8a53 100644
--- a/python/src/com/jetbrains/python/sdk/uv/impl/UvLowLevel.kt
+++ b/python/src/com/jetbrains/python/sdk/uv/impl/UvLowLevel.kt
@@ -8,14 +8,15 @@ import com.intellij.util.io.delete
import com.jetbrains.python.packaging.common.PythonOutdatedPackage
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.common.PythonPackageSpecification
-import com.jetbrains.python.venvReader.VirtualEnvReader
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 java.nio.file.Path
import kotlin.io.path.exists
import kotlin.io.path.pathString
-internal class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLevel {
+private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLevel {
override suspend fun initializeEnvironment(init: Boolean, python: Path?): Result {
val addPythonArg: (MutableList) -> Unit = { args ->
python?.let {
@@ -32,16 +33,14 @@ internal class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLe
initArgs.add("--vcs")
initArgs.add("none")
- uvCli.runUv(cwd, *initArgs.toTypedArray()).getOrElse {
- return Result.failure(it)
- }
+ uvCli.runUv(cwd, *initArgs.toTypedArray())
+ .onFailure { return Result.failure(it) }
}
- val venvArgs = mutableListOf("venv");
+ val venvArgs = mutableListOf("venv")
addPythonArg(venvArgs)
- uvCli.runUv(cwd, *venvArgs.toTypedArray()).getOrElse {
- return Result.failure(it)
- }
+ uvCli.runUv(cwd, *venvArgs.toTypedArray())
+ .onFailure { return Result.failure(it) }
// TODO: ask for an uv option not to create
val hello = cwd.resolve("hello.py").takeIf { it.exists() }
@@ -55,11 +54,27 @@ internal class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLe
return Result.success(path)
}
- override suspend fun listPackages(): Result> {
- val out = uvCli.runUv(cwd, "pip", "list", "--format", "json").getOrElse {
- return Result.failure(it)
+ override suspend fun discoverUvInstalledPythons(): Result> {
+ var out = uvCli.runUv(cwd, "python", "dir")
+ .getOrElse { return Result.failure(it) }
+
+ val uvDir = tryResolvePath(out)
+ if (uvDir == null) {
+ return Result.failure(RuntimeException("failed to detect uv python directory"))
}
+ // TODO: ask for json output format
+ out = uvCli.runUv(cwd, "python", "list", "--only-installed")
+ .getOrElse { return Result.failure(it) }
+
+ val pythons = parseUvPythonList(uvDir, out)
+ return Result.success(pythons)
+ }
+
+ override suspend fun listPackages(): Result> {
+ val out = uvCli.runUv(cwd, "pip", "list", "--format", "json")
+ .getOrElse { return Result.failure(it) }
+
data class PackageInfo(val name: String, val version: String)
val mapper = jacksonObjectMapper()
@@ -73,9 +88,8 @@ internal class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLe
override suspend fun listOutdatedPackages(): Result> {
- val out = uvCli.runUv(cwd, "pip", "list", "--outdated", "--format", "json").getOrElse {
- return Result.failure(it)
- }
+ val out = uvCli.runUv(cwd, "pip", "list", "--outdated", "--format", "json")
+ .getOrElse { return Result.failure(it) }
data class OutdatedPackageInfo(val name: String, val version: String, val latest_version: String)
@@ -94,9 +108,8 @@ internal class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLe
}
override suspend fun installPackage(name: PythonPackageSpecification, options: List): Result {
- uvCli.runUv(cwd, "pip", "install", formatPackageName(name), *options.toTypedArray()).getOrElse {
- return Result.failure(it)
- }
+ uvCli.runUv(cwd, "pip", "install", formatPackageName(name), *options.toTypedArray())
+ .onFailure { return Result.failure(it) }
return Result.success(Unit)
}
@@ -110,9 +123,8 @@ internal class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLe
}
override suspend fun addDependency(name: PythonPackageSpecification, options: List): Result {
- uvCli.runUv(cwd, "add", formatPackageName(name), *options.toTypedArray()).getOrElse {
- return Result.failure(it)
- }
+ uvCli.runUv(cwd, "add", formatPackageName(name), *options.toTypedArray())
+ .onFailure { return Result.failure(it) }
return Result.success(Unit)
}
@@ -127,6 +139,24 @@ internal class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLe
fun formatPackageName(name: PythonPackageSpecification): String {
return if (name.versionSpecs.isNullOrBlank()) name.name else "${name.name}${name.versionSpecs}"
}
+
+ fun parseUvPythonList(uvDir: Path, out: String): Set {
+ val lines = out.lines()
+ val pythons = lines.mapNotNull { line ->
+ var arrow = line.indexOf("->").takeIf { it >= 0 } ?: line.length
+ val pythonAndPath = line.substring(0, arrow).trim().split(" ")
+ if (pythonAndPath.size != 2) {
+ return@mapNotNull null
+ }
+
+ val python = tryResolvePath(pythonAndPath[1])
+ ?.takeIf { it.exists() && it.startsWith(uvDir) }
+
+ python
+ }.toSet()
+
+ return pythons
+ }
}
fun createUvLowLevel(cwd: Path, uvCli: UvCli = createUvCli()): UvLowLevel {