mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-05 01:50:56 +07:00
Python: add UI for system pythons
See `spi.kt` Show provided pythons in UI And sort them. See sort logic and a test. GitOrigin-RevId: 7b98be47f41653d73400a64c65fbbfe3e1d78ee2
This commit is contained in:
committed by
intellij-monorepo-bot
parent
de41298dc0
commit
c6e616043d
@@ -26,15 +26,16 @@ import kotlin.io.path.relativeTo
|
||||
class PythonWithLanguageLevelImpl internal constructor(
|
||||
override val pythonBinary: PythonBinary,
|
||||
override val languageLevel: LanguageLevel,
|
||||
) : PythonWithLanguageLevel, Comparable<PythonWithLanguageLevelImpl> {
|
||||
) : PythonWithLanguageLevel, Comparable<PythonWithLanguageLevel> {
|
||||
companion object {
|
||||
|
||||
private val concurrentLimit = Semaphore(permits = 4)
|
||||
|
||||
/**
|
||||
* Like [createByPythonBinary] but runs in parallel up to [concurrentLimit]
|
||||
* @return python path -> python with language level sorted from highest to lowest.
|
||||
*/
|
||||
suspend fun createByPythonBinaries(pythonBinaries: Collection<PythonBinary>): Collection<Pair<PythonBinary, Result<PythonWithLanguageLevelImpl, @Nls String>>> =
|
||||
suspend fun createByPythonBinaries(pythonBinaries: Collection<PythonBinary>): Collection<Pair<PythonBinary, Result<PythonWithLanguageLevel, @Nls String>>> =
|
||||
coroutineScope {
|
||||
pythonBinaries.map {
|
||||
async {
|
||||
@@ -43,7 +44,7 @@ class PythonWithLanguageLevelImpl internal constructor(
|
||||
}
|
||||
}
|
||||
}.awaitAll()
|
||||
}
|
||||
}.sortedBy { it.first }
|
||||
|
||||
suspend fun createByPythonBinary(pythonBinary: PythonBinary): Result<PythonWithLanguageLevelImpl, @Nls String> {
|
||||
val languageLevel = pythonBinary.validatePythonAndGetVersion().getOr { return it }
|
||||
@@ -80,6 +81,6 @@ class PythonWithLanguageLevelImpl internal constructor(
|
||||
return "$pythonString ($languageLevel)"
|
||||
}
|
||||
|
||||
// TODO: DOC backward
|
||||
override fun compareTo(other: PythonWithLanguageLevelImpl): Int = other.languageLevel.compareTo(languageLevel)
|
||||
// Backward: first python is the highest
|
||||
override fun compareTo(other: PythonWithLanguageLevel): Int = languageLevel.compareTo(other.languageLevel) * -1
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import com.jetbrains.python.PythonBinary
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
import org.jetbrains.annotations.Nls
|
||||
|
||||
interface PythonWithLanguageLevel {
|
||||
interface PythonWithLanguageLevel : Comparable<PythonWithLanguageLevel> {
|
||||
val pythonBinary: PythonBinary
|
||||
val languageLevel: LanguageLevel
|
||||
|
||||
|
||||
@@ -29,5 +29,8 @@
|
||||
<orderEntry type="module" module-name="intellij.python.community.impl.installer" />
|
||||
<orderEntry type="module" module-name="intellij.python.community.impl.venv" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.python.community.testFramework.testEnv" scope="TEST" />
|
||||
<orderEntry type="library" scope="TEST" name="io.mockk" level="project" />
|
||||
<orderEntry type="library" scope="TEST" name="io.mockk.jvm" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.platform.testFramework" scope="TEST" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -5,12 +5,12 @@ import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.platform.eel.EelApi
|
||||
import com.intellij.platform.eel.provider.localEel
|
||||
import com.intellij.python.community.services.internal.impl.PythonWithLanguageLevelImpl
|
||||
import com.intellij.python.community.services.shared.PythonWithLanguageLevel
|
||||
import com.jetbrains.python.PythonBinary
|
||||
import com.jetbrains.python.Result
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.annotations.Nls
|
||||
import javax.swing.Icon
|
||||
|
||||
/**
|
||||
* Service to register and obtain [SystemPython]s
|
||||
@@ -18,7 +18,7 @@ import org.jetbrains.annotations.Nls
|
||||
@ApiStatus.NonExtendable
|
||||
sealed interface SystemPythonService {
|
||||
/**
|
||||
* @return system pythons installed on OS in order from highest (hence, the first one is usually the best one)
|
||||
* @return system pythons installed on OS sorted by type, then by lang.level: in order from highest (hence, the first one is usually the best one)
|
||||
*/
|
||||
suspend fun findSystemPythons(eelApi: EelApi = localEel): List<SystemPython>
|
||||
|
||||
@@ -42,11 +42,36 @@ fun SystemPythonService(): SystemPythonService = ApplicationManager.getApplicati
|
||||
/**
|
||||
* Python installed on OS.
|
||||
* [pythonBinary] is guaranteed to be usable and have [languageLevel] at the moment of instance creation.
|
||||
* Use [ui] to customize view.
|
||||
*
|
||||
* When sorted, sorted first by [ui], then by [languageLevel] (from the highest)
|
||||
*
|
||||
* Instances could be obtained with [SystemPythonService]
|
||||
*/
|
||||
@JvmInline
|
||||
value class SystemPython internal constructor(private val impl: PythonWithLanguageLevelImpl) : PythonWithLanguageLevel by impl
|
||||
class SystemPython internal constructor(private val impl: PythonWithLanguageLevel, val ui: UICustomization?) : PythonWithLanguageLevel by impl {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as SystemPython
|
||||
|
||||
if (impl != other.impl) return false
|
||||
if (ui != other.ui) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = impl.hashCode()
|
||||
result = 31 * result + (ui?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "SystemPython(impl=$impl, ui=$ui)"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool to install python on OS.
|
||||
@@ -60,4 +85,12 @@ sealed interface PythonInstallerService {
|
||||
*/
|
||||
@ApiStatus.Experimental
|
||||
suspend fun installLatestPython(): Result<Unit, String>
|
||||
}
|
||||
}
|
||||
|
||||
data class UICustomization(
|
||||
/**
|
||||
* i.e: "UV" for pythons found by UV
|
||||
*/
|
||||
val title: @Nls String,
|
||||
val icon: Icon? = null,
|
||||
)
|
||||
@@ -32,7 +32,7 @@ internal class SystemPythonServiceImpl : SystemPythonService, SimplePersistentSt
|
||||
override suspend fun registerSystemPython(pythonPath: PythonBinary): Result<SystemPython, @Nls String> {
|
||||
val impl = PythonWithLanguageLevelImpl.createByPythonBinary(pythonPath).getOr { return it }
|
||||
state.userProvidedPythons.add(pythonPath)
|
||||
return Result.success(SystemPython(impl))
|
||||
return Result.success(SystemPython(impl, null))
|
||||
}
|
||||
|
||||
override fun getInstaller(eelApi: EelApi): PythonInstallerService? =
|
||||
@@ -47,9 +47,18 @@ internal class SystemPythonServiceImpl : SystemPythonService, SimplePersistentSt
|
||||
}
|
||||
else emptyList()
|
||||
|
||||
val pythonsUi = mutableMapOf<PythonBinary, UICustomization>()
|
||||
|
||||
val pythonsFromExtensions = SystemPythonProvider.EP
|
||||
.extensionList
|
||||
.flatMap { it.findSystemPythons(eelApi).getOrNull() ?: emptyList() }.filter { it.getEelDescriptor().upgrade() == eelApi }
|
||||
.flatMap { provider ->
|
||||
val pythons = provider.findSystemPythons(eelApi).getOrNull() ?: emptyList()
|
||||
val ui = provider.uiCustomization
|
||||
if (ui != null) {
|
||||
pythons.forEach { pythonsUi[it] = ui }
|
||||
}
|
||||
pythons
|
||||
}.filter { it.getEelDescriptor().upgrade() == eelApi }
|
||||
|
||||
val badPythons = mutableSetOf<PythonBinary>()
|
||||
val pythons = corePythons + pythonsFromExtensions + state.userProvidedPythons.filter { it.getEelDescriptor() == eelApi.descriptor }
|
||||
@@ -57,7 +66,7 @@ internal class SystemPythonServiceImpl : SystemPythonService, SimplePersistentSt
|
||||
val result = PythonWithLanguageLevelImpl.createByPythonBinaries(pythons.toSet())
|
||||
.mapNotNull { (python, r) ->
|
||||
when (r) {
|
||||
is Result.Success -> SystemPython(r.result)
|
||||
is Result.Success -> SystemPython(r.result, pythonsUi[r.result.pythonBinary])
|
||||
is Result.Failure -> {
|
||||
fileLogger().info("Skipping $python : ${r.error}")
|
||||
badPythons.add(python)
|
||||
@@ -68,7 +77,7 @@ internal class SystemPythonServiceImpl : SystemPythonService, SimplePersistentSt
|
||||
}.toSet()
|
||||
// Remove stale pythons from cache
|
||||
state.userProvidedPythons.removeAll(badPythons)
|
||||
return@withContext result.sortedByDescending { it.languageLevel }
|
||||
return@withContext result.sorted()
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -14,5 +14,10 @@ interface SystemPythonProvider {
|
||||
val EP: ExtensionPointName<SystemPythonProvider> = ExtensionPointName<SystemPythonProvider>("Pythonid.systemPythonProvider")
|
||||
}
|
||||
|
||||
/**
|
||||
* You can optionally customize how your pythons are displayed
|
||||
*/
|
||||
val uiCustomization: UICustomization? get() = null
|
||||
|
||||
suspend fun findSystemPythons(eelApi: EelApi): Result<Set<PythonBinary>>
|
||||
}
|
||||
@@ -1,15 +1,28 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.python.junit5Tests.env.systemPython.impl
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.python.community.impl.venv.createVenv
|
||||
import com.intellij.python.community.services.systemPython.SystemPythonProvider
|
||||
import com.intellij.python.community.services.systemPython.SystemPythonService
|
||||
import com.intellij.python.community.services.systemPython.UICustomization
|
||||
import com.intellij.python.junit5Tests.framework.env.PyEnvTestCase
|
||||
import com.intellij.python.junit5Tests.framework.env.PythonBinaryPath
|
||||
import com.intellij.testFramework.common.timeoutRunBlocking
|
||||
import com.intellij.testFramework.junit5.TestDisposable
|
||||
import com.intellij.testFramework.registerExtension
|
||||
import com.jetbrains.python.PythonBinary
|
||||
import com.jetbrains.python.getOrThrow
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.Matchers
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
import java.nio.file.Path
|
||||
|
||||
@PyEnvTestCase
|
||||
class EnvProviderTest {
|
||||
@@ -23,8 +36,24 @@ class EnvProviderTest {
|
||||
if (systemPythons.size > 1) {
|
||||
val best = systemPythons.first()
|
||||
for (python in systemPythons.subList(1, systemPythonBinaries.size)) {
|
||||
Assertions.assertTrue(python.languageLevel <= best.languageLevel, "$best is the first, bust worse than $python")
|
||||
assertTrue(python.languageLevel <= best.languageLevel, "$best is the first, bust worse than $python")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testProviderWithUi(
|
||||
@TestDisposable disposable: Disposable,
|
||||
@PythonBinaryPath python: PythonBinary,
|
||||
@TempDir venvDir: Path,
|
||||
): Unit = timeoutRunBlocking {
|
||||
val venvPython = createVenv(python, venvDir).getOrThrow()
|
||||
val ui = UICustomization("myui")
|
||||
val provider = mockk<SystemPythonProvider>()
|
||||
coEvery { provider.findSystemPythons(any()) } returns Result.success(setOf(venvPython))
|
||||
coEvery { provider.uiCustomization } returns ui
|
||||
ApplicationManager.getApplication().registerExtension(SystemPythonProvider.EP, provider, disposable)
|
||||
val python = SystemPythonService().findSystemPythons().first { it.pythonBinary == venvPython }
|
||||
assertEquals(ui, python.ui, "Wrong UI")
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,9 @@ import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import com.intellij.python.community.services.internal.impl.PythonWithLanguageLevelImpl
|
||||
import com.intellij.python.community.services.shared.PythonWithLanguageLevel
|
||||
import com.intellij.python.community.services.systemPython.SystemPython
|
||||
import com.intellij.python.community.services.systemPython.SystemPythonService
|
||||
import com.intellij.python.community.services.systemPython.UICustomization
|
||||
import com.jetbrains.python.PyBundle.message
|
||||
import com.jetbrains.python.configuration.PyConfigurableInterpreterList
|
||||
import com.jetbrains.python.errorProcessing.ErrorSink
|
||||
@@ -157,13 +159,13 @@ abstract class PythonAddInterpreterModel(params: PyInterpreterModelParams, priva
|
||||
}
|
||||
|
||||
// System (base) pythons
|
||||
val system: List<PythonWithLanguageLevel> = systemPythonService.findSystemPythons()
|
||||
val system: List<SystemPython> = systemPythonService.findSystemPythons()
|
||||
|
||||
// Python + isBase. Both: system and venv.
|
||||
val detected = (venvs.map { Pair(it, false) } + system.map { Pair(it, true) })
|
||||
val detected = (venvs.map { Triple(it, false, null) } + system.map { Triple(it, true, it.ui) })
|
||||
.filterNot { (python, _) -> python.pythonBinary in existingSdkPaths }
|
||||
.map { (python, base) -> DetectedSelectableInterpreter(python.pythonBinary.pathString, python.languageLevel, base) }
|
||||
.sortedByDescending { it.languageLevel }
|
||||
.map { (python, base, ui) -> DetectedSelectableInterpreter(python.pythonBinary.pathString, python.languageLevel, base, ui) }
|
||||
.sorted()
|
||||
|
||||
withContext(uiContext) {
|
||||
installable = filteredInstallable
|
||||
@@ -285,6 +287,7 @@ sealed class PythonSelectableInterpreter {
|
||||
|
||||
abstract val homePath: String
|
||||
abstract val languageLevel: LanguageLevel
|
||||
open val uiCustomization: UICustomization? = null
|
||||
override fun toString(): String =
|
||||
"PythonSelectableInterpreter(homePath='$homePath')"
|
||||
}
|
||||
@@ -300,8 +303,16 @@ class ExistingSelectableInterpreter(val sdk: Sdk, override val languageLevel: La
|
||||
/**
|
||||
* [isBase] is a system interpreter, see [isBasePython]
|
||||
*/
|
||||
class DetectedSelectableInterpreter(override val homePath: String, override val languageLevel: LanguageLevel, private val isBase: Boolean) : PythonSelectableInterpreter() {
|
||||
class DetectedSelectableInterpreter(override val homePath: String, override val languageLevel: LanguageLevel, private val isBase: Boolean, override val uiCustomization: UICustomization? = null) : PythonSelectableInterpreter(), Comparable<DetectedSelectableInterpreter> {
|
||||
override suspend fun isBasePython(): Boolean = isBase
|
||||
|
||||
|
||||
override fun compareTo(other: DetectedSelectableInterpreter): Int {
|
||||
// First by type
|
||||
val byType = (uiCustomization?.title ?: "").compareTo(other.uiCustomization?.title ?: "")
|
||||
// Then from the highest python to the lowest
|
||||
return if (byType != 0) byType else (languageLevel.compareTo(other.languageLevel) * -1)
|
||||
}
|
||||
}
|
||||
|
||||
class ManuallyAddedSelectableInterpreter(override val homePath: String, override val languageLevel: LanguageLevel) : PythonSelectableInterpreter()
|
||||
|
||||
@@ -149,9 +149,10 @@ class PythonNewEnvironmentDialogNavigator {
|
||||
internal fun SimpleColoredComponent.customizeForPythonInterpreter(interpreter: PythonSelectableInterpreter) {
|
||||
when (interpreter) {
|
||||
is DetectedSelectableInterpreter, is ManuallyAddedSelectableInterpreter -> {
|
||||
icon = IconLoader.getTransparentIcon(PythonPsiApiIcons.Python)
|
||||
icon = IconLoader.getTransparentIcon(interpreter.uiCustomization?.icon ?: PythonPsiApiIcons.Python)
|
||||
append(replaceHomePathToTilde(interpreter.homePath))
|
||||
append(" " + message("sdk.rendering.detected.grey.text"), SimpleTextAttributes.GRAYED_SMALL_ATTRIBUTES)
|
||||
val message = interpreter.uiCustomization?.title ?: message("sdk.rendering.detected.grey.text")
|
||||
append(" $message", SimpleTextAttributes.GRAYED_SMALL_ATTRIBUTES)
|
||||
}
|
||||
is InstallableSelectableInterpreter -> {
|
||||
icon = AllIcons.Actions.Download
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
// 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.util.NlsSafe
|
||||
import com.intellij.platform.eel.EelApi
|
||||
import com.intellij.platform.eel.provider.localEel
|
||||
import com.intellij.python.community.services.systemPython.SystemPythonProvider
|
||||
import com.intellij.python.community.services.systemPython.UICustomization
|
||||
import com.jetbrains.python.PythonBinary
|
||||
import com.jetbrains.python.icons.PythonIcons
|
||||
import com.jetbrains.python.sdk.uv.impl.createUvLowLevel
|
||||
import com.jetbrains.python.sdk.uv.impl.hasUvExecutable
|
||||
import java.nio.file.Path
|
||||
@@ -19,4 +22,7 @@ internal class UvSystemPythonProvider : SystemPythonProvider {
|
||||
val uv = createUvLowLevel(Path.of("."))
|
||||
return uv.listUvPythons()
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral") // tool name is untranslatable
|
||||
override val uiCustomization: UICustomization = UICustomization("uv", PythonIcons.UV)
|
||||
}
|
||||
Reference in New Issue
Block a user