From e0e9e611a67105039a8dd6818fb7d6134e2c02a4 Mon Sep 17 00:00:00 2001 From: Aleksandr Sorotskii Date: Thu, 30 Jan 2025 18:18:04 +0100 Subject: [PATCH] support for listing uv provided pythons PY-77537 GitOrigin-RevId: 7003a66ba729d7565c1cf876198d85d45d025659 --- python/intellij.python.community.impl.iml | 2 + .../systemPythonSpi/SystemPythonProvider.kt | 2 +- .../pluginCore/resources/META-INF/plugin.xml | 2 + .../systemPython/SystemPythonService.kt | 2 +- .../impl/EnvTestPythonProvider.kt | 26 ++++--- python/src/com/jetbrains/python/sdk/uv/Uv.kt | 6 +- .../src/com/jetbrains/python/sdk/uv/UvExt.kt | 4 +- .../python/sdk/uv/UvSystemPythonProvider.kt | 22 ++++++ .../com/jetbrains/python/sdk/uv/impl/UvCli.kt | 6 +- .../python/sdk/uv/impl/UvLowLevel.kt | 72 +++++++++++++------ 10 files changed, 105 insertions(+), 39 deletions(-) create mode 100644 python/src/com/jetbrains/python/sdk/uv/UvSystemPythonProvider.kt 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 {