support for listing uv provided pythons PY-77537

GitOrigin-RevId: 7003a66ba729d7565c1cf876198d85d45d025659
This commit is contained in:
Aleksandr Sorotskii
2025-02-03 17:10:53 +00:00
committed by intellij-monorepo-bot
parent ade80998a2
commit e0e9e611a6
10 changed files with 105 additions and 39 deletions
@@ -166,6 +166,8 @@
<orderEntry type="library" name="ap-validation" level="project" />
<orderEntry type="module" module-name="intellij.python.community.impl.poetry" />
<orderEntry type="module" module-name="intellij.python.community.impl.installer" />
<orderEntry type="module" module-name="intellij.platform.eel" />
<orderEntry type="module" module-name="intellij.platform.eel.provider" />
<orderEntry type="module" module-name="intellij.python.community.impl.venv" />
<orderEntry type="module" module-name="intellij.python.community.execService" />
</component>
@@ -14,5 +14,5 @@ interface SystemPythonProvider {
val EP: ExtensionPointName<SystemPythonProvider> = ExtensionPointName<SystemPythonProvider>("Pythonid.systemPythonProvider")
}
suspend fun findSystemPythons(eelApi: EelApi): Set<PythonBinary>
suspend fun findSystemPythons(eelApi: EelApi): Result<Set<PythonBinary>>
}
@@ -872,6 +872,8 @@ The Python plug-in provides smart editing for Python scripts. The feature set of
<pythonPackageManagerProvider implementation="com.jetbrains.python.packaging.pip.PipPackageManagerProvider" order="last"/>
<pythonPackageManagerProvider implementation="com.jetbrains.python.packaging.conda.CondaPackageManagerProvider"/>
<systemPythonProvider implementation="com.jetbrains.python.sdk.uv.UvSystemPythonProvider"/>
<inspectionExtension implementation="com.jetbrains.python.sdk.configuration.PyInterpreterInspectionSuppressor"/>
<inspectionExtension implementation="com.jetbrains.python.inspections.PyUnresolvedReferenceDefaultInspectionExtension" order="last"/>
@@ -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<PythonBinary>()
val pythons = corePythons + pythonsFromExtensions + state.userProvidedPythons.filter { it.getEelDescriptor() == eelApi.descriptor }
@@ -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<PythonBinary> {
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<Set<PythonBinary>> {
var pythons = emptySet<PythonBinary>()
if (eelApi == localEel) {
pythons = TypeVanillaPython3
.getTestEnvironments()
.map { (python, closeable) ->
Disposer.register(ApplicationManager.getApplication()) {
closeable.close()
}
python
}.toSet()
}
return Result.success(pythons)
}
}
+4 -2
View File
@@ -13,8 +13,7 @@ interface UvCli {
interface UvLowLevel {
suspend fun initializeEnvironment(init: Boolean, python: Path?): Result<Path>
suspend fun listPackages(): Result<List<PythonPackage>>
suspend fun listOutdatedPackages(): Result<List<PythonOutdatedPackage>>
suspend fun discoverUvInstalledPythons(): Result<Set<Path>>
/**
* 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<String>): Result<Unit>
suspend fun uninstallPackage(name: PythonPackage): Result<Unit>
suspend fun listPackages(): Result<List<PythonPackage>>
suspend fun listOutdatedPackages(): Result<List<PythonOutdatedPackage>>
}
@@ -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
@@ -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<Set<PythonBinary>> {
if (eelApi != localEel || !hasUvExecutable()) {
// TODO: support for remote execution
return Result.success(emptySet())
}
val uv = createUvLowLevel(Path.of("."))
return uv.discoverUvInstalledPythons()
}
}
@@ -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)
}
@@ -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<Path> {
val addPythonArg: (MutableList<String>) -> 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<List<PythonPackage>> {
val out = uvCli.runUv(cwd, "pip", "list", "--format", "json").getOrElse {
return Result.failure(it)
override suspend fun discoverUvInstalledPythons(): Result<Set<Path>> {
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<List<PythonPackage>> {
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<List<PythonOutdatedPackage>> {
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<String>): Result<Unit> {
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<String>): Result<Unit> {
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<Path> {
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 {