Python: Use SystemPythonService in v2 add interprets widget, also fix PY-78732.

We now use `SystemPythonService` to look for system pythons and validate new pythons against it.

GitOrigin-RevId: c06201cd03f7e1cb060a44c4578dd71f8bb05df6
This commit is contained in:
Ilya.Kazakevich
2025-02-04 04:31:32 +00:00
committed by intellij-monorepo-bot
parent e7f1c532bd
commit 3e14c16503
6 changed files with 83 additions and 29 deletions
@@ -171,5 +171,6 @@
<orderEntry type="module" module-name="intellij.python.community.impl.venv" />
<orderEntry type="module" module-name="intellij.python.community.execService" />
<orderEntry type="module" module-name="intellij.python.community.services.systemPython" />
<orderEntry type="module" module-name="intellij.python.community.services.internal.impl" />
</component>
</module>
@@ -22,7 +22,7 @@
<orderEntry type="module" module-name="intellij.platform.projectModel" />
<orderEntry type="module" module-name="intellij.python.community.testFramework" scope="TEST" />
<orderEntry type="library" scope="TEST" name="hamcrest" level="project" />
<orderEntry type="module" module-name="intellij.python.community.services.shared" />
<orderEntry type="module" module-name="intellij.python.community.services.shared" exported="" />
<orderEntry type="module" module-name="intellij.python.community.services.internal.impl" />
<orderEntry type="module" module-name="intellij.platform.util" />
<orderEntry type="module" module-name="intellij.python.community.junit5Tests.framework" scope="TEST" />
@@ -7,6 +7,8 @@ import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.vfs.toNioPathOrNull
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.asPythonResult
import com.jetbrains.python.sdk.ModuleOrProject
import com.jetbrains.python.sdk.poetry.detectPoetryEnvs
import com.jetbrains.python.sdk.poetry.isPoetry
@@ -14,8 +16,6 @@ import com.jetbrains.python.sdk.poetry.pyProjectToml
import com.jetbrains.python.sdk.poetry.setupPoetrySdkUnderProgress
import com.jetbrains.python.statistics.InterpreterType
import com.jetbrains.python.statistics.version
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.asPythonResult
import java.nio.file.Path
import kotlin.io.path.pathString
@@ -37,8 +37,8 @@ internal class PoetryExistingEnvironmentSelector(model: PythonMutableTargetAddIn
}
override suspend fun detectEnvironments(modulePath: Path) {
val existingEnvs = detectPoetryEnvs(null, null, modulePath.pathString).mapNotNull {
env -> env.homePath?.let { path -> DetectedSelectableInterpreter(path, env.version) }
val existingEnvs = detectPoetryEnvs(null, null, modulePath.pathString).mapNotNull { env ->
env.homePath?.let { path -> DetectedSelectableInterpreter(path, env.version, false) }
}
existingEnvironments.value = existingEnvs
@@ -6,6 +6,9 @@ import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.vfs.toNioPathOrNull
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.asPythonResult
import com.jetbrains.python.errorProcessing.failure
import com.jetbrains.python.sdk.ModuleOrProject
import com.jetbrains.python.sdk.associatedModulePath
import com.jetbrains.python.sdk.isAssociatedWithModule
@@ -14,9 +17,6 @@ import com.jetbrains.python.sdk.uv.pyProjectToml
import com.jetbrains.python.sdk.uv.setupUvSdkUnderProgress
import com.jetbrains.python.statistics.InterpreterType
import com.jetbrains.python.statistics.version
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.asPythonResult
import com.jetbrains.python.errorProcessing.failure
import java.nio.file.Path
import kotlin.io.path.pathString
@@ -45,7 +45,7 @@ internal class UvExistingEnvironmentSelector(model: PythonMutableTargetAddInterp
val existingEnvs = ProjectJdkTable.getInstance().allJdks.filter {
it.isUv && (it.associatedModulePath == modulePath.pathString || it.associatedModulePath == null)
}.mapNotNull { env ->
env.homePath?.let { path -> DetectedSelectableInterpreter(path, env.version) }
env.homePath?.let { path -> DetectedSelectableInterpreter(path, env.version, false) }
}
existingEnvironments.value = existingEnvs
@@ -3,40 +3,47 @@ package com.jetbrains.python.sdk.add.v2
import com.intellij.execution.target.TargetEnvironmentConfiguration
import com.intellij.openapi.application.EDT
import com.intellij.openapi.diagnostic.fileLogger
import com.intellij.openapi.diagnostic.getOrLogException
import com.intellij.openapi.fileChooser.FileChooser
import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.observable.properties.PropertyGraph
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.NlsSafe
import com.jetbrains.python.failure
import com.intellij.python.community.services.internal.impl.PythonWithLanguageLevelImpl
import com.intellij.python.community.services.shared.PythonWithLanguageLevel
import com.intellij.python.community.services.systemPython.SystemPythonService
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.configuration.PyConfigurableInterpreterList
import com.jetbrains.python.errorProcessing.ErrorSink
import com.jetbrains.python.errorProcessing.emit
import com.jetbrains.python.failure
import com.jetbrains.python.newProject.steps.ProjectSpecificSettingsStep
import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathFlows
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.conda.suggestCondaPath
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
import com.jetbrains.python.sdk.flavors.VirtualEnvSdkFlavor
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
import com.jetbrains.python.sdk.pipenv.getPipEnvExecutable
import com.jetbrains.python.sdk.poetry.getPoetryExecutable
import com.jetbrains.python.sdk.uv.impl.getUvExecutable
import com.jetbrains.python.errorProcessing.ErrorSink
import com.jetbrains.python.errorProcessing.emit
import com.jetbrains.python.venvReader.tryResolvePath
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.plus
import kotlinx.coroutines.withContext
import java.nio.file.InvalidPathException
import java.nio.file.Path
import kotlin.io.path.Path
import kotlin.io.path.pathString
@OptIn(ExperimentalCoroutinesApi::class)
abstract class PythonAddInterpreterModel(params: PyInterpreterModelParams) {
abstract class PythonAddInterpreterModel(params: PyInterpreterModelParams, private val systemPythonService: SystemPythonService = SystemPythonService()) {
val propertyGraph = PropertyGraph()
val navigator = PythonNewEnvironmentDialogNavigator()
@@ -118,7 +125,6 @@ abstract class PythonAddInterpreterModel(params: PyInterpreterModelParams) {
private suspend fun initInterpreterList() {
withContext(Dispatchers.IO) {
// TODO: PythonInterpreterService: load valid system pythons
val existingSdks = PyConfigurableInterpreterList.getInstance(null).getModel().sdks.toList()
@@ -135,16 +141,32 @@ abstract class PythonAddInterpreterModel(params: PyInterpreterModelParams) {
val existingSdkPaths = existingSdks.mapNotNull { it.homePath }.mapNotNull { tryResolvePath(it) }.toSet()
val detected = PythonSdkFlavor.getApplicableFlavors(true).flatMap { sdkFlavor ->
// TODO: PythonInterpreterService: detect valid system pythons
sdkFlavor.suggestLocalHomePaths(null, null)
.filterNot { it in existingSdkPaths }
.map { DetectedSelectableInterpreter(it.pathString, sdkFlavor.getLanguageLevel(it.pathString)) }
}
// Venvs are not detected manually, but must migrate to VenvService or so
val venvs: List<PythonWithLanguageLevel> = VirtualEnvSdkFlavor.getInstance()
.suggestLocalHomePaths(null, null)
.mapNotNull { venv ->
return@mapNotNull when (val r = PythonWithLanguageLevelImpl.createByPythonBinary(venv)) {
is com.jetbrains.python.Result.Failure -> {
fileLogger().warn("Skipping $venv : ${r.error}")
null
}
is com.jetbrains.python.Result.Success -> r.result
}
}
// System (base) pythons
val system: List<PythonWithLanguageLevel> = systemPythonService.findSystemPythons()
// Python + isBase. Both: system and venv.
val detected = (venvs.map { Pair(it, false) } + system.map { Pair(it, true) })
.filterNot { (python, _) -> python.pythonBinary in existingSdkPaths }
.map { (python, base) -> DetectedSelectableInterpreter(python.pythonBinary.pathString, python.languageLevel, base) }
.sortedByDescending { it.languageLevel }
withContext(uiContext) {
installable = filteredInstallable
knownInterpreters.value = allValidSdks // todo check target?
knownInterpreters.value = allValidSdks
_detectedInterpreters.value = detected
}
@@ -162,6 +184,28 @@ abstract class PythonAddInterpreterModel(params: PyInterpreterModelParams) {
manuallyAddedInterpreters.value += ExistingSelectableInterpreter(sdk, PySdkUtil.getLanguageLevelForSdk(sdk), sdk.isSystemWide)
}
/**
* Given [pathToPython] returns either cleaned path (if valid) or null and reports error to [errorSink]
*/
suspend fun getSystemPythonFromSelection(pathToPython: String, errorSink: ErrorSink): String? {
val result = try {
when (val r = systemPythonService.registerSystemPython(Path(pathToPython))) {
is com.jetbrains.python.Result.Failure -> com.jetbrains.python.errorProcessing.failure(r.error)
is com.jetbrains.python.Result.Success -> com.jetbrains.python.Result.success(r.result)
}
}
catch (e: InvalidPathException) {
com.jetbrains.python.errorProcessing.failure(e.localizedMessage)
}
return when (result) {
is com.jetbrains.python.Result.Success -> result.result.pythonBinary.pathString
is com.jetbrains.python.Result.Failure -> {
errorSink.emit(result.error)
null
}
}
}
}
abstract class PythonMutableTargetAddInterpreterModel(params: PyInterpreterModelParams)
@@ -176,7 +220,6 @@ abstract class PythonMutableTargetAddInterpreterModel(params: PyInterpreterModel
}
suspend fun detectPoetryExecutable() {
// FIXME: support targets
getPoetryExecutable().getOrNull()?.let {
withContext(Dispatchers.EDT) {
state.poetryExecutable.set(it.pathString)
@@ -185,7 +228,6 @@ abstract class PythonMutableTargetAddInterpreterModel(params: PyInterpreterModel
}
suspend fun detectPipEnvExecutable() {
// FIXME: support targets
getPipEnvExecutable().getOrNull()?.let {
withContext(Dispatchers.EDT) {
state.pipenvExecutable.set(it.pathString)
@@ -194,7 +236,6 @@ abstract class PythonMutableTargetAddInterpreterModel(params: PyInterpreterModel
}
suspend fun detectUvExecutable() {
// FIXME: support targets
getUvExecutable()?.pathString?.let {
withContext(Dispatchers.EDT) {
state.uvExecutable.set(it)
@@ -255,7 +296,12 @@ class ExistingSelectableInterpreter(val sdk: Sdk, override val languageLevel: La
override val homePath = sdk.homePath!! // todo is it safe
}
class DetectedSelectableInterpreter(override val homePath: String, override val languageLevel: LanguageLevel) : PythonSelectableInterpreter()
/**
* [isBase] is a system interpreter, see [isBasePython]
*/
class DetectedSelectableInterpreter(override val homePath: String, override val languageLevel: LanguageLevel, private val isBase: Boolean) : PythonSelectableInterpreter() {
override suspend fun isBasePython(): Boolean = isBase
}
class ManuallyAddedSelectableInterpreter(override val homePath: String, override val languageLevel: LanguageLevel) : PythonSelectableInterpreter()
@@ -18,6 +18,8 @@ import com.intellij.openapi.ui.validation.WHEN_PROPERTY_CHANGED
import com.intellij.openapi.ui.validation.and
import com.intellij.openapi.util.IconLoader
import com.intellij.openapi.util.NlsSafe
import com.intellij.platform.ide.progress.ModalTaskOwner
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
import com.intellij.python.community.impl.installer.CondaInstallManager
import com.intellij.ui.AnimatedIcon
import com.intellij.ui.ColoredListCellRenderer
@@ -41,6 +43,7 @@ import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.VIRTUA
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
import com.jetbrains.python.util.ShowingMessageErrorSync
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.flow.SharedFlow
@@ -239,7 +242,7 @@ internal fun Row.pythonInterpreterComboBox(
onPathSelected: (String) -> Unit, busyState: StateFlow<Boolean>? = null,
): Cell<PythonInterpreterComboBox> {
val comboBox = PythonInterpreterComboBox(selectedSdkProperty, model, onPathSelected)
val comboBox = PythonInterpreterComboBox(selectedSdkProperty, model, onPathSelected, ShowingMessageErrorSync)
val cell = cell(comboBox)
.bindItem(selectedSdkProperty)
.applyToComponent {
@@ -270,6 +273,7 @@ internal class PythonInterpreterComboBox(
private val backingProperty: ObservableMutableProperty<PythonSelectableInterpreter?>,
val controller: PythonAddInterpreterModel,
val onPathSelected: (String) -> Unit,
private val errorSink: ErrorSink,
) : ComboBox<PythonSelectableInterpreter?>() {
private lateinit var itemsFlow: StateFlow<List<PythonSelectableInterpreter>>
@@ -281,11 +285,14 @@ internal class PythonInterpreterComboBox(
init {
renderer = PythonSdkComboBoxListCellRenderer()
val newOnPathSelected: (String) -> Unit = {
interpreterToSelect.set(it)
onPathSelected(it)
runWithModalProgressBlocking(ModalTaskOwner.guess(), message("python.sdk.validating.environment")) {
controller.getSystemPythonFromSelection(it, errorSink)?.let { python ->
interpreterToSelect.set(python)
onPathSelected(python)
}
}
}
editor = PythonSdkComboBoxWithBrowseButtonEditor(this, controller, newOnPathSelected)
}
fun setItems(flow: StateFlow<List<PythonSelectableInterpreter>>) {