Python NPW: Heavy refactoring:

1. lots of funs now `suspend`
2. errors reported to `ErrorSink`
3. validation improved (yet, still not perfect)
4. tests added

GitOrigin-RevId: 5cbf674a70ad0e0b40180dd358398bf498d6f76d
This commit is contained in:
Ilya.Kazakevich
2024-09-11 23:44:51 +02:00
committed by intellij-monorepo-bot
parent 7ca537b422
commit 8457d2ae09
25 changed files with 332 additions and 163 deletions

View File

@@ -318,6 +318,8 @@ python.sdk.switch.to=Switch to {0}
python.sdk.installing=Installing {0}
python.sdk.downloading.package.progress.title=Downloading package from {0}
python.sdk.select.conda.path.title=Select Path to Conda Executable
python.sdk.conda.no.env.selected.error=No Conda enviroment selected
python.sdk.conda.no.base.env.error=No base Conda envrionment available
python.sdk.conda.problem.running=Problem running conda
python.sdk.conda.problem.env.empty.invalid=Environment name is empty or invalid
python.sdk.conda.problem.env.name.used=This name is used already. Please, choose another one.
@@ -570,6 +572,7 @@ remote.interpreter.feature.is.not.available=Remote interpreter feature is not av
# What to display of user entered junk
commandLine.commandNotFound={0}: command not found
commandLine.directoryCantBeAccessed={0}: bad directory
# Window with actions
# "X" button title
@@ -1541,4 +1544,5 @@ progress.text.installing=Installing\u2026
package.install.with.options.dialog.message=Options:
package.install.with.options.dialog.title=Package Install With Options
python.toolwindow.packages.collapse.all.action=Collapse All
django.template.language=Template Language
django.template.language=Template Language
python.error=Error

View File

@@ -38,6 +38,7 @@ import com.jetbrains.python.newProject.promotion.PromoProjectGenerator
import com.jetbrains.python.psi.PyUtil
import com.jetbrains.python.sdk.PyLazySdk
import com.jetbrains.python.sdk.add.v2.PythonAddNewEnvironmentPanel
import com.jetbrains.python.util.ShowingMessageErrorSync
import kotlinx.coroutines.flow.MutableStateFlow
import java.io.File
import java.nio.file.InvalidPathException
@@ -121,7 +122,7 @@ class PythonProjectSpecificSettingsStep<T : PyNewProjectSettings>(
// Instead of setting this type as default, we limit types to it
val onlyAllowedInterpreterTypes = projectGenerator.preferredEnvironmentType?.let { setOf(it) }
val interpreterPanel = PythonAddNewEnvironmentPanel(projectLocationFlow, onlyAllowedInterpreterTypes).also { interpreterPanel = it }
val interpreterPanel = PythonAddNewEnvironmentPanel(projectLocationFlow, onlyAllowedInterpreterTypes, errorSink = ShowingMessageErrorSync).also { interpreterPanel = it }
mainPanel = panel {
row(message("new.project.name")) {

View File

@@ -8,6 +8,7 @@ import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.jetbrains.python.PyBundle
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.newProject.collector.PythonNewProjectWizardCollector.logPythonNewProjectGenerated
import com.jetbrains.python.sdk.ModuleOrProject
import com.jetbrains.python.sdk.add.PySdkCreator
@@ -22,7 +23,7 @@ import kotlinx.coroutines.*
class PyV3BaseProjectSettings(var createGitRepository: Boolean = false) {
lateinit var sdkCreator: PySdkCreator
suspend fun generateAndGetSdk(module: Module, baseDir: VirtualFile): Sdk = coroutineScope {
suspend fun generateAndGetSdk(module: Module, baseDir: VirtualFile): Result<Sdk> = coroutineScope {
val project = module.project
if (createGitRepository) {
launch(CoroutineName("Generating git") + Dispatchers.IO) {
@@ -31,9 +32,7 @@ class PyV3BaseProjectSettings(var createGitRepository: Boolean = false) {
}
}
}
val (sdk, statistics) = withContext(Dispatchers.EDT) {
Pair(sdkCreator.getSdk(ModuleOrProject.ModuleAndProject(module)), sdkCreator.createStatisticsInfo())
}
val (sdk: Sdk, statistics: InterpreterStatisticsInfo?) = getSdkAndInterpreter(module).getOrElse { return@coroutineScope Result.failure(it) }
sdk.setAssociationToModule(module)
module.pythonSdk = sdk
if (statistics != null) {
@@ -42,7 +41,12 @@ class PyV3BaseProjectSettings(var createGitRepository: Boolean = false) {
this::class.java,
emptyList())
}
return@coroutineScope sdk
return@coroutineScope Result.success(sdk)
}
private suspend fun getSdkAndInterpreter(module: Module): Result<Pair<Sdk, InterpreterStatisticsInfo?>> = withContext(Dispatchers.EDT) {
val sdk: Sdk = sdkCreator.getSdk(ModuleOrProject.ModuleAndProject(module)).getOrElse { return@withContext Result.failure(it) }
return@withContext Result.success(Pair<Sdk, InterpreterStatisticsInfo?>(sdk, sdkCreator.createStatisticsInfo()))
}

View File

@@ -2,6 +2,7 @@
package com.jetbrains.python.newProjectWizard
import com.intellij.facet.ui.ValidationResult
import com.intellij.openapi.application.EDT
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.module.Module
@@ -13,9 +14,13 @@ import com.intellij.util.SystemProperties
import com.jetbrains.python.Result
import com.jetbrains.python.newProjectWizard.impl.PyV3GeneratorPeer
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode
import com.jetbrains.python.util.ErrorSink
import com.jetbrains.python.util.ShowingMessageErrorSync
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.Nls
import java.nio.file.Path
@@ -29,6 +34,7 @@ abstract class PyV3ProjectBaseGenerator<TYPE_SPECIFIC_SETTINGS : PyV3ProjectType
private val typeSpecificSettings: TYPE_SPECIFIC_SETTINGS,
private val typeSpecificUI: PyV3ProjectTypeSpecificUI<TYPE_SPECIFIC_SETTINGS>?,
private val allowedInterpreterTypes: Set<PythonInterpreterSelectionMode>? = null,
private val errorSink: ErrorSink = ShowingMessageErrorSync,
) : DirectoryProjectGenerator<PyV3BaseProjectSettings> {
private val baseSettings = PyV3BaseProjectSettings()
private val projectPathFlow = MutableStateFlow(Path.of(SystemProperties.getUserHome()))
@@ -36,7 +42,13 @@ abstract class PyV3ProjectBaseGenerator<TYPE_SPECIFIC_SETTINGS : PyV3ProjectType
override fun generateProject(project: Project, baseDir: VirtualFile, settings: PyV3BaseProjectSettings, module: Module) {
val coroutineScope = project.service<MyService>().coroutineScope
coroutineScope.launch {
typeSpecificSettings.generateProject(module, baseDir, settings.generateAndGetSdk(module, baseDir))
val sdk = settings.generateAndGetSdk(module, baseDir).getOrElse {
withContext(Dispatchers.EDT) {
errorSink.emit(it.localizedMessage)
}
throw it
}
typeSpecificSettings.generateProject(module, baseDir, sdk)
}
}

View File

@@ -11,6 +11,7 @@ import com.jetbrains.python.newProjectWizard.PyV3ProjectTypeSpecificUI
import com.jetbrains.python.sdk.add.PySdkCreator
import com.jetbrains.python.sdk.add.v2.PythonAddNewEnvironmentPanel
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode
import com.jetbrains.python.util.ShowingMessageErrorSync
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import java.nio.file.Path
@@ -24,7 +25,7 @@ internal class Py3VUI<TYPE_SPECIFIC_SETTINGS : PyV3ProjectTypeSpecificSettings>
specificUiAndSettings: Pair<PyV3ProjectTypeSpecificUI<TYPE_SPECIFIC_SETTINGS>, TYPE_SPECIFIC_SETTINGS>?,
allowedInterpreterTypes: Set<PythonInterpreterSelectionMode>? = null,
) {
private val sdkPanel = PythonAddNewEnvironmentPanel(projectPath, allowedInterpreterTypes)
private val sdkPanel = PythonAddNewEnvironmentPanel(projectPath, allowedInterpreterTypes, ShowingMessageErrorSync)
private val _mainPanel = panel {
val checkBoxRow = row {

View File

@@ -28,6 +28,7 @@ import com.jetbrains.python.sdk.add.target.PyAddTargetBasedSdkDialog
import com.jetbrains.python.sdk.add.v2.PythonAddLocalInterpreterDialog
import com.jetbrains.python.sdk.add.v2.PythonAddLocalInterpreterPresenter
import com.jetbrains.python.target.PythonLanguageRuntimeType
import com.jetbrains.python.util.ShowingMessageErrorSync
import java.util.function.Consumer
fun collectAddInterpreterActions(moduleOrProject: ModuleOrProject, onSdkCreated: Consumer<Sdk>): List<AnAction> {
@@ -63,7 +64,7 @@ private class AddLocalInterpreterAction(
) : AnAction(PyBundle.messagePointer("python.sdk.action.add.local.interpreter.text"), AllIcons.Nodes.HomeFolder), DumbAware {
override fun actionPerformed(e: AnActionEvent) {
if (Registry.`is`("python.unified.interpreter.configuration")) {
val dialogPresenter = PythonAddLocalInterpreterPresenter(moduleOrProject).apply {
val dialogPresenter = PythonAddLocalInterpreterPresenter(moduleOrProject, errorSink = ShowingMessageErrorSync).apply {
// Model provides flow, but we need to call Consumer
sdkCreatedFlow.oneShotConsumer(onSdkCreated)
}

View File

@@ -7,8 +7,10 @@ import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.sdk.ModuleOrProject
interface PySdkCreator {
@RequiresEdt
fun getSdk(moduleOrProject: ModuleOrProject): Sdk
/**
* Error is shown to user. Do not catch all exceptions, only return exceptions valuable to user
*/
suspend fun getSdk(moduleOrProject: ModuleOrProject): Result<Sdk>
@RequiresEdt
fun createStatisticsInfo(): InterpreterStatisticsInfo? = null

View File

@@ -18,6 +18,7 @@ import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
import com.intellij.platform.util.progress.RawProgressReporter
import com.jetbrains.extensions.failure
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.flavors.PyFlavorAndData
@@ -112,7 +113,7 @@ suspend fun PyCondaCommand.createCondaSdkAlongWithNewEnv(newCondaEnvInfo: NewCon
val process = PyCondaEnv.createEnv(this, newCondaEnvInfo).getOrElse { return Result.failure(it) }
val error = ProcessHandlerReader(process).runProcessAndGetError(uiContext, reporter)
return error?.let { Result.failure(Exception(it)) }
return error?.let { failure(it) }
?: Result.success(
createCondaSdkFromExistingEnv(newCondaEnvInfo.toIdentity(), existingSdks, project)).apply {
onSuccess {

View File

@@ -7,7 +7,10 @@ import com.intellij.openapi.application.asContextElement
import com.intellij.openapi.observable.util.notEqualsTo
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.openapi.ui.validation.DialogValidationRequestor
import com.intellij.openapi.ui.validation.WHEN_PROPERTY_CHANGED
import com.intellij.openapi.ui.validation.and
import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.bindItem
import com.intellij.ui.layout.predicate
@@ -18,11 +21,13 @@ import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
import com.jetbrains.python.statistics.InterpreterCreationMode
import com.jetbrains.python.statistics.InterpreterType
import kotlinx.coroutines.*
import com.jetbrains.python.util.ErrorSink
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@OptIn(FlowPreview::class)
class CondaExistingEnvironmentSelector(model: PythonAddInterpreterModel) : PythonExistingEnvironmentConfigurator(model) {
class CondaExistingEnvironmentSelector(model: PythonAddInterpreterModel, private val errorSink: ErrorSink) : PythonExistingEnvironmentConfigurator(model) {
private lateinit var envComboBox: ComboBox<PyCondaEnv?>
override fun buildOptions(panel: Panel, validationRequestor: DialogValidationRequestor) {
@@ -31,7 +36,7 @@ class CondaExistingEnvironmentSelector(model: PythonAddInterpreterModel) : Pytho
validationRequestor,
message("sdk.create.custom.venv.executable.path", "conda"),
message("sdk.create.custom.venv.missing.text", "conda"),
createInstallCondaFix(model))
createInstallCondaFix(model, errorSink))
.displayLoaderWhen(model.condaEnvironmentsLoading, scope = model.scope, uiContext = model.uiContext)
row(message("sdk.create.custom.env.creation.type")) {
@@ -41,6 +46,11 @@ class CondaExistingEnvironmentSelector(model: PythonAddInterpreterModel) : Pytho
.bindItem(state.selectedCondaEnv)
.displayLoaderWhen(model.condaEnvironmentsLoading, makeTemporaryEditable = true,
scope = model.scope, uiContext = model.uiContext)
.validationRequestor(validationRequestor and WHEN_PROPERTY_CHANGED(state.condaExecutable))
.validationOnInput {
return@validationOnInput if (it.isVisible && it.selectedItem == null) ValidationInfo(message("python.sdk.conda.no.env.selected.error")) else null
}
.component
link(message("sdk.create.custom.conda.refresh.envs"), action = { onReloadCondaEnvironments() })
@@ -52,7 +62,7 @@ class CondaExistingEnvironmentSelector(model: PythonAddInterpreterModel) : Pytho
private fun onReloadCondaEnvironments() {
model.scope.launch(Dispatchers.EDT + ModalityState.current().asContextElement()) {
model.condaEnvironmentsLoading.value = true
model.detectCondaEnvironments()
model.detectCondaEnvironmentsOrError(errorSink)
model.condaEnvironmentsLoading.value = false
}
}
@@ -95,9 +105,8 @@ class CondaExistingEnvironmentSelector(model: PythonAddInterpreterModel) : Pytho
//}
}
override fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Sdk {
return model.selectCondaEnvironment(state.selectedCondaEnv.get()!!.envIdentity)
}
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Result<Sdk> =
model.selectCondaEnvironment(base = false)
override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo {
//val statisticsTarget = if (presenter.projectLocationContext is WslContext) InterpreterTarget.TARGET_WSL else target.toStatisticsField()
@@ -109,7 +118,7 @@ class CondaExistingEnvironmentSelector(model: PythonAddInterpreterModel) : Pytho
false,
false,
true,
//presenter.projectLocationContext is WslContext,
//presenter.projectLocationContext is WslContext,
false,
InterpreterCreationMode.CUSTOM)
}

View File

@@ -18,12 +18,13 @@ import com.jetbrains.python.sdk.flavors.conda.NewCondaEnvRequest
import com.jetbrains.python.statistics.InterpreterCreationMode
import com.jetbrains.python.statistics.InterpreterType
import com.jetbrains.python.ui.flow.bindText
import com.jetbrains.python.util.ErrorSink
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import java.nio.file.Path
import kotlin.io.path.name
class CondaNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel, private val projectPath: StateFlow<Path>?) : PythonNewEnvironmentCreator(model) {
class CondaNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel, private val projectPath: StateFlow<Path>?, private val errorSink:ErrorSink) : PythonNewEnvironmentCreator(model) {
private lateinit var pythonVersion: ObservableMutableProperty<LanguageLevel>
private lateinit var versionComboBox: ComboBox<LanguageLevel>
@@ -48,7 +49,7 @@ class CondaNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel,
validationRequestor,
message("sdk.create.custom.venv.executable.path", "conda"),
message("sdk.create.custom.venv.missing.text", "conda"),
createInstallCondaFix(model))
createInstallCondaFix(model, errorSink))
.displayLoaderWhen(model.condaEnvironmentsLoading, scope = model.scope, uiContext = model.uiContext)
}
}
@@ -57,7 +58,7 @@ class CondaNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel,
model.state.newCondaEnvName.set(model.projectPath.value.name)
}
override fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Sdk {
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Result<Sdk> {
return model.createCondaEnvironment(NewCondaEnvRequest.EmptyNamedEnv(pythonVersion.get(), model.state.newCondaEnvName.get()))
}

View File

@@ -5,7 +5,6 @@ import com.intellij.openapi.module.Module
import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
import com.intellij.openapi.ui.validation.DialogValidationRequestor
import com.intellij.platform.ide.progress.ModalTaskOwner
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
@@ -49,7 +48,7 @@ abstract class CustomNewEnvironmentCreator(private val name: String, model: Pyth
basePythonComboBox.setItems(model.baseInterpreters)
}
override fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Sdk {
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Result<Sdk> {
savePathToExecutableToProperties()
// todo think about better error handling
@@ -66,8 +65,8 @@ abstract class CustomNewEnvironmentCreator(private val name: String, model: Pyth
model.projectPath.value.toString(),
homePath,
false)!!
SdkConfigurationUtil.addSdk(newSdk)
return newSdk
addSdk(newSdk)
return Result.success(newSdk)
}
override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo =

View File

@@ -13,10 +13,11 @@ import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.sdk.ModuleOrProject
import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.*
import com.jetbrains.python.util.ErrorSink
import kotlinx.coroutines.flow.StateFlow
import java.nio.file.Path
class PythonAddCustomInterpreter(val model: PythonMutableTargetAddInterpreterModel, val moduleOrProject: ModuleOrProject? = null, private val projectPath: StateFlow<Path>? = null) {
class PythonAddCustomInterpreter(val model: PythonMutableTargetAddInterpreterModel, val moduleOrProject: ModuleOrProject? = null, projectPath: StateFlow<Path>? = null, errorSink: ErrorSink) {
private val propertyGraph = model.propertyGraph
private val selectionMethod = propertyGraph.property(PythonInterpreterSelectionMethod.CREATE_NEW)
@@ -27,14 +28,14 @@ class PythonAddCustomInterpreter(val model: PythonMutableTargetAddInterpreterMod
private val newInterpreterCreators = mapOf(
VIRTUALENV to PythonNewVirtualenvCreator(model),
CONDA to CondaNewEnvironmentCreator(model, projectPath),
CONDA to CondaNewEnvironmentCreator(model, projectPath, errorSink),
PIPENV to PipEnvNewEnvironmentCreator(model),
POETRY to PoetryNewEnvironmentCreator(model, moduleOrProject),
)
private val existingInterpreterSelectors = mapOf(
PYTHON to PythonExistingEnvironmentSelector(model),
CONDA to CondaExistingEnvironmentSelector(model),
CONDA to CondaExistingEnvironmentSelector(model, errorSink),
)
val currentSdkManager: PythonAddEnvironment

View File

@@ -12,6 +12,7 @@ import com.intellij.platform.ide.progress.runWithModalProgressBlocking
import com.intellij.ui.dsl.builder.panel
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.jetbrains.python.PyBundle
import com.jetbrains.python.util.ShowingMessageErrorSync
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
@@ -44,19 +45,22 @@ class PythonAddLocalInterpreterDialog(private val dialogPresenter: PythonAddLoca
}
}
override fun createCenterPanel(): JComponent = panel {
model = PythonLocalAddInterpreterModel(PyInterpreterModelParams(service<PythonAddSdkService>().coroutineScope,
// At this moment dialog is not displayed, so there is no modality state
// The whole idea of context passing is doubtful
Dispatchers.EDT + ModalityState.any().asContextElement(), MutableStateFlow(basePath)))
model.navigator.selectionMode = AtomicProperty(PythonInterpreterSelectionMode.CUSTOM)
mainPanel = PythonAddCustomInterpreter(model)
mainPanel.buildPanel(this, WHEN_PROPERTY_CHANGED(AtomicProperty(basePath)))
override fun createCenterPanel(): JComponent {
val errorSink = ShowingMessageErrorSync
return panel {
model = PythonLocalAddInterpreterModel(PyInterpreterModelParams(service<PythonAddSdkService>().coroutineScope,
// At this moment dialog is not displayed, so there is no modality state
// The whole idea of context passing is doubtful
Dispatchers.EDT + ModalityState.any().asContextElement(), MutableStateFlow(basePath)))
model.navigator.selectionMode = AtomicProperty(PythonInterpreterSelectionMode.CUSTOM)
mainPanel = PythonAddCustomInterpreter(model, errorSink = errorSink)
mainPanel.buildPanel(this, WHEN_PROPERTY_CHANGED(AtomicProperty(basePath)))
}.apply {
model.scope.launch(model.uiContext) {
model.initialize()
mainPanel.onShown()
}.apply {
model.scope.launch(model.uiContext) {
model.initialize()
mainPanel.onShown()
}
}
}
}

View File

@@ -1,19 +1,16 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.sdk.add.v2
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.writeIntentReadAction
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.io.toNioPathOrNull
import com.jetbrains.python.sdk.ModuleOrProject
import com.jetbrains.python.sdk.VirtualEnvReader
import com.jetbrains.python.sdk.rootManager
import com.jetbrains.python.sdk.service.PySdkService.Companion.pySdkService
import kotlinx.coroutines.Dispatchers
import com.jetbrains.python.util.ErrorSink
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.withContext
import java.nio.file.Path
/**
@@ -22,7 +19,7 @@ import java.nio.file.Path
*
* @see PythonAddLocalInterpreterDialog
*/
class PythonAddLocalInterpreterPresenter(val moduleOrProject: ModuleOrProject, val envReader: VirtualEnvReader = VirtualEnvReader.Instance) {
class PythonAddLocalInterpreterPresenter(val moduleOrProject: ModuleOrProject, val envReader: VirtualEnvReader = VirtualEnvReader.Instance, val errorSink: ErrorSink) {
/**
* Default path to create virtualenv it
@@ -37,7 +34,10 @@ class PythonAddLocalInterpreterPresenter(val moduleOrProject: ModuleOrProject, v
val sdkCreatedFlow: Flow<Sdk> = _sdkShared.asSharedFlow()
suspend fun okClicked(addEnvironment: PythonAddEnvironment) {
val sdk = withContext(Dispatchers.EDT) { writeIntentReadAction { addEnvironment.getOrCreateSdk(moduleOrProject) } }
val sdk = addEnvironment.getOrCreateSdk(moduleOrProject).getOrElse {
errorSink.emit(it.localizedMessage)
return
}
moduleOrProject.project.pySdkService.persistSdk(sdk)
_sdkShared.emit(sdk)
}

View File

@@ -1,6 +1,7 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.sdk.add.v2
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.asContextElement
@@ -9,9 +10,12 @@ import com.intellij.openapi.observable.properties.PropertyGraph
import com.intellij.openapi.observable.util.and
import com.intellij.openapi.observable.util.notEqualsTo
import com.intellij.openapi.observable.util.or
import com.intellij.openapi.progress.runBlockingCancellable
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.validation.WHEN_PROPERTY_CHANGED
import com.intellij.platform.ide.progress.ModalTaskOwner
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.TopGap
@@ -25,6 +29,8 @@ import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode.*
import com.jetbrains.python.statistics.InterpreterCreationMode
import com.jetbrains.python.statistics.InterpreterTarget
import com.jetbrains.python.statistics.InterpreterType
import com.jetbrains.python.util.ErrorSink
import com.jetbrains.python.util.ShowingMessageErrorSync
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
@@ -36,7 +42,7 @@ import java.nio.file.Path
/**
* If `onlyAllowedInterpreterTypes` then only these types are displayed. All types displayed otherwise
*/
class PythonAddNewEnvironmentPanel(val projectPath: StateFlow<Path>, onlyAllowedInterpreterTypes: Set<PythonInterpreterSelectionMode>? = null) : PySdkCreator {
class PythonAddNewEnvironmentPanel(val projectPath: StateFlow<Path>, onlyAllowedInterpreterTypes: Set<PythonInterpreterSelectionMode>? = null, private val errorSink: ErrorSink) : PySdkCreator {
companion object {
private const val VENV_DIR = ".venv"
@@ -76,7 +82,7 @@ class PythonAddNewEnvironmentPanel(val projectPath: StateFlow<Path>, onlyAllowed
model.navigator.selectionMode = selectedMode
//presenter.controller = model
custom = PythonAddCustomInterpreter(model, projectPath = projectPath)
custom = PythonAddCustomInterpreter(model, projectPath = projectPath, errorSink = ShowingMessageErrorSync)
val validationRequestor = WHEN_PROPERTY_CHANGED(selectedMode)
@@ -104,7 +110,7 @@ class PythonAddNewEnvironmentPanel(val projectPath: StateFlow<Path>, onlyAllowed
validationRequestor,
message("sdk.create.custom.venv.executable.path", "conda"),
message("sdk.create.custom.venv.missing.text", "conda"),
createInstallCondaFix(model))
createInstallCondaFix(model, errorSink))
//.displayLoaderWhen(presenter.detectingCondaExecutable, scope = presenter.scope, uiContext = presenter.uiContext)
}.visibleIf(_baseConda)
@@ -143,19 +149,29 @@ class PythonAddNewEnvironmentPanel(val projectPath: StateFlow<Path>, onlyAllowed
}
@Deprecated("Use one with module or project")
fun getSdk(): Sdk = getSdk(ModuleOrProject.ProjectOnly(ProjectManager.getInstance().defaultProject))
fun getSdk(): Sdk {
val moduleOrProject = ModuleOrProject.ProjectOnly(ProjectManager.getInstance().defaultProject)
return if (ApplicationManager.getApplication().isDispatchThread) {
runWithModalProgressBlocking(ModalTaskOwner.guess(), "...") {
getSdk(moduleOrProject)
}
}
else {
runBlockingCancellable { getSdk(moduleOrProject) }
}.getOrThrow()
}
override fun getSdk(moduleOrProject: ModuleOrProject): Sdk {
override suspend fun getSdk(moduleOrProject: ModuleOrProject): Result<Sdk> {
model.navigator.saveLastState()
return when (selectedMode.get()) {
PROJECT_VENV -> {
val projectPath = projectPath.value
model.setupVirtualenv(projectPath.resolve(VENV_DIR), // todo just keep venv path, all the rest is in the model
projectPath,
projectPath
//pythonBaseVersion.get()!!)
model.state.baseInterpreter.get()!!).getOrThrow()
)
}
BASE_CONDA -> model.selectCondaEnvironment(model.state.baseCondaEnv.get()!!.envIdentity)
BASE_CONDA -> model.selectCondaEnvironment(base = true)
CUSTOM -> custom.currentSdkManager.getOrCreateSdk(moduleOrProject)
}
}

View File

@@ -32,9 +32,9 @@ class PythonExistingEnvironmentSelector(model: PythonAddInterpreterModel) : Pyth
comboBox.setItems(model.allInterpreters)
}
override fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Sdk {
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Result<Sdk> {
// todo error handling, nullability issues
return setupSdkIfDetected(model.state.selectedInterpreter.get()!!, model.existingSdks)!!
return Result.success(setupSdkIfDetected(model.state.selectedInterpreter.get()!!, model.existingSdks)!!)
}
override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo {

View File

@@ -1,26 +1,24 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.sdk.add.v2
import com.intellij.execution.wsl.WslPath.Companion.parseWindowsUncPath
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.asContextElement
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.openapi.ui.validation.DialogValidationRequestor
import com.intellij.openapi.ui.validation.WHEN_PROPERTY_CHANGED
import com.intellij.openapi.ui.validation.and
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.io.OSAgnosticPathUtil
import com.intellij.ui.components.ActionLink
import com.intellij.ui.dsl.builder.*
import com.intellij.ui.dsl.builder.components.validationTooltip
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.intellij.util.ui.showingScope
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.newProject.collector.PythonNewProjectWizardCollector
import com.jetbrains.python.newProjectWizard.validateProjectPathAndGetPath
import com.jetbrains.python.sdk.ModuleOrProject
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMethod.SELECT_EXISTING
import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.PYTHON
@@ -32,6 +30,7 @@ import java.nio.file.InvalidPathException
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.io.path.exists
import kotlin.io.path.isDirectory
class PythonNewVirtualenvCreator(model: PythonMutableTargetAddInterpreterModel) : PythonNewEnvironmentCreator(model) {
private lateinit var versionComboBox: PythonInterpreterComboBox
@@ -77,16 +76,22 @@ class PythonNewVirtualenvCreator(model: PythonMutableTargetAddInterpreterModel)
.component
}
row(message("sdk.create.custom.location")) {
// TODO" Extract this logic to the presenter or view model, do not touch nio from EDT, cover with test
textFieldWithBrowseButton(FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(message("sdk.create.custom.venv.location.browse.title")))
.bindText(model.state.venvPath)
.whenTextChangedFromUi { locationModified = true }
.validationRequestor(validationRequestor and WHEN_PROPERTY_CHANGED(model.state.venvPath))
.cellValidation { textField ->
addInputRule("") {
val pathExists = textField.isVisible && textField.doesPathExist()
addInputRule {
if (!textField.isVisible) return@addInputRule null // We are hidden, hence valid
locationValidationFailed.set(false)
val locationPath = when (val path = validateProjectPathAndGetPath(textField.text)) {
is com.jetbrains.python.Result.Failure -> return@addInputRule ValidationInfo(path.error) // Path is invalid
is com.jetbrains.python.Result.Success -> path.result
}
val pathExists = locationPath.exists()
locationValidationFailed.set(pathExists)
if (pathExists) {
val locationPath = Paths.get(textField.text)
if (locationPath.resolve(pythonInVenvPath).exists()) {
val typedName = locationPath.last().toString()
suggestedVenvName = suggestVenvName(typedName)
@@ -99,12 +104,14 @@ class PythonNewVirtualenvCreator(model: PythonMutableTargetAddInterpreterModel)
locationValidationMessage.set(message("sdk.create.custom.venv.folder.not.empty"))
suggestedVenvName = ".venv"
suggestedLocation = locationPath
val suggestedPath = (if (locationPath.isDirectory()) locationPath else locationPath.parent).resolve(suggestedVenvName)
firstFixLink.text = message("sdk.create.custom.venv.use.different.venv.link",
Paths.get("..", locationPath.last().toString(), suggestedVenvName))
suggestedPath)
secondFixLink.isVisible = false
}
}
pathExists
// Path exists means error
if (pathExists) ValidationInfo(locationValidationMessage.get()) else null
}
}
.align(Align.FILL)
@@ -184,49 +191,15 @@ class PythonNewVirtualenvCreator(model: PythonMutableTargetAddInterpreterModel)
return currentName.removeSuffix(digitSuffix) + newSuffix
}
override fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Sdk {
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Result<Sdk> =
// todo remove project path, or move to controller
return model.setupVirtualenv((Path.of(model.state.venvPath.get())), model.projectPath.value, model.state.baseInterpreter.get()!!).getOrThrow()
}
companion object {
/**
* Checks if [this] field's text points to an existing file or directory. Calls [Path.exists] from EDT with care to avoid freezing the
* UI.
*
* In case of Windows machine, this method skips testing UNC paths on existence (except WSL paths) to prevent [Path.exists] checking the
* network resources. Such checks might freeze the UI for several seconds when performed from EDT. The freezes reveal themselves when
* a user starts typing a WSL path with `\\w` and continues symbol by symbol (`\\ws`, `\\wsl`, etc.) all the way to the root WSL path.
*
* @see [Path.exists]
*/
@RequiresBackgroundThread(generateAssertion = false)
private fun TextFieldWithBrowseButton.doesPathExist(): Boolean =
text.let { probablyIncompletePath ->
if (SystemInfo.isWindows && OSAgnosticPathUtil.isUncPath(probablyIncompletePath)) {
val parseWindowsUncPath = parseWindowsUncPath(probablyIncompletePath)
if (parseWindowsUncPath?.linuxPath?.isNotBlank() == true) {
probablyIncompletePath.safeCheckCorrespondingPathExist()
}
else {
false
}
}
else {
probablyIncompletePath.safeCheckCorrespondingPathExist()
}
}
private fun String.safeCheckCorrespondingPathExist() =
try {
Paths.get(this).exists()
}
catch (e: InvalidPathException) {
false
}
}
try {
val venvPath = Path.of(model.state.venvPath.get())
model.setupVirtualenv(venvPath, model.projectPath.value)
}
catch (e: InvalidPathException) {
Result.failure(e)
}
override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo {
//val statisticsTarget = if (presenter.projectLocationContext is WslContext) InterpreterTarget.TARGET_WSL else target.toStatisticsField()

View File

@@ -15,11 +15,13 @@ import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
import com.intellij.openapi.ui.validation.DialogValidationRequestor
import com.intellij.openapi.wm.IdeFocusManager
import com.intellij.ui.dsl.builder.Panel
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.icons.PythonIcons
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.LOGGER
import com.jetbrains.python.sdk.ModuleOrProject
import com.jetbrains.python.sdk.PythonSdkType
import com.jetbrains.python.sdk.installSdkIfNeeded
import com.jetbrains.python.sdk.pipenv.PIPENV_ICON
import com.jetbrains.python.sdk.poetry.POETRY_ICON
import com.jetbrains.python.statistics.InterpreterTarget
@@ -43,9 +45,10 @@ abstract class PythonAddEnvironment(open val model: PythonAddInterpreterModel) {
/**
* Returns created SDK ready to use
*
* Error is shown to user. Do not catch all exceptions, only return exceptions valuable to user
*/
@RequiresEdt
abstract fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Sdk
abstract suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Result<Sdk>
abstract fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo
}
@@ -53,9 +56,6 @@ abstract class PythonNewEnvironmentCreator(override val model: PythonMutableTarg
abstract class PythonExistingEnvironmentConfigurator(model: PythonAddInterpreterModel) : PythonAddEnvironment(model)
enum class PythonSupportedEnvironmentManagers(val nameKey: String, val icon: Icon) {
VIRTUALENV("sdk.create.custom.virtualenv", PythonIcons.Python.Virtualenv),
CONDA("sdk.create.custom.conda", PythonIcons.Python.Anaconda),
@@ -109,7 +109,7 @@ internal fun installBaseSdk(sdk: Sdk, existingSdks: List<Sdk>): Sdk? {
}
internal fun setupSdkIfDetected(interpreter: PythonSelectableInterpreter, existingSdks: List<Sdk>, targetConfig: TargetEnvironmentConfiguration? = null): Sdk? {
internal suspend fun setupSdkIfDetected(interpreter: PythonSelectableInterpreter, existingSdks: List<Sdk>, targetConfig: TargetEnvironmentConfiguration? = null): Sdk? {
if (interpreter is ExistingSelectableInterpreter) return interpreter.sdk
val homeDir = interpreter.homePath.virtualFileOnTarget(targetConfig) ?: return null // todo handle
@@ -119,6 +119,6 @@ internal fun setupSdkIfDetected(interpreter: PythonSelectableInterpreter, existi
false,
null, // todo create additional data for target
null) ?: return null
SdkConfigurationUtil.addSdk(newSdk)
addSdk(newSdk)
return newSdk
}

View File

@@ -6,10 +6,9 @@ import com.intellij.execution.target.local.LocalTargetEnvironmentRequest
import com.intellij.openapi.application.EDT
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
import com.intellij.platform.ide.progress.ModalTaskOwner
import com.intellij.platform.ide.progress.TaskCancellation
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
import com.intellij.platform.ide.progress.withModalProgress
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.jetbrains.python.PyBundle
import com.jetbrains.python.sdk.PythonSdkAdditionalData
@@ -27,23 +26,22 @@ internal fun PythonAddInterpreterModel.createCondaCommand(): PyCondaCommand =
PyCondaCommand(state.condaExecutable.get().convertToPathOnTarget(targetEnvironmentConfiguration),
targetConfig = targetEnvironmentConfiguration)
@RequiresEdt
internal fun PythonAddInterpreterModel.createCondaEnvironment(request: NewCondaEnvRequest): Sdk {
suspend fun PythonAddInterpreterModel.createCondaEnvironment(request: NewCondaEnvRequest): Result<Sdk> {
val project = ProjectManager.getInstance().defaultProject
val existingSdks = this@createCondaEnvironment.existingSdks
val sdk = runWithModalProgressBlocking(ModalTaskOwner.guess(),
PyBundle.message("sdk.create.custom.conda.create.progress"),
TaskCancellation.nonCancellable()) {
val sdk = withModalProgress(ModalTaskOwner.guess(),
PyBundle.message("sdk.create.custom.conda.create.progress"),
TaskCancellation.nonCancellable()) {
createCondaCommand()
.createCondaSdkAlongWithNewEnv(request,
Dispatchers.EDT,
existingSdks,
project).getOrThrow()
}
project)
}.getOrElse { return Result.failure(it) }
(sdk.sdkType as PythonSdkType).setupSdkPaths(sdk)
SdkConfigurationUtil.addSdk(sdk)
return sdk
addSdk(sdk)
return Result.success(sdk)
}

View File

@@ -11,8 +11,12 @@ 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.intellij.openapi.util.io.FileUtil
import com.intellij.util.SystemProperties
import com.jetbrains.extensions.failure
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.configuration.PyConfigurableInterpreterList
import com.jetbrains.python.newProject.steps.ProjectSpecificSettingsStep
import com.jetbrains.python.psi.LanguageLevel
@@ -23,6 +27,7 @@ import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
import com.jetbrains.python.sdk.pipenv.pipEnvPath
import com.jetbrains.python.sdk.poetry.poetryPath
import com.jetbrains.python.util.ErrorSink
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import java.nio.file.Path
@@ -97,21 +102,26 @@ abstract class PythonAddInterpreterModel(params: PyInterpreterModelParams) {
}
}
suspend fun detectCondaEnvironments() {
/**
* Returns error or `null` if no error
*/
suspend fun detectCondaEnvironments(): @NlsSafe String? =
withContext(Dispatchers.IO) {
val commandExecutor = targetEnvironmentConfiguration.toExecutor()
val environments = PyCondaEnv.getEnvs(commandExecutor, state.condaExecutable.get()).getOrLogException(LOG) ?: emptyList()
val fullCondaPathOnTarget = state.condaExecutable.get()
if (fullCondaPathOnTarget.isBlank()) return@withContext message("python.sdk.conda.no.exec")
val environments = PyCondaEnv.getEnvs(commandExecutor, fullCondaPathOnTarget).getOrElse { return@withContext it.localizedMessage }
val baseConda = environments.find { env -> env.envIdentity.let { it is PyCondaEnvIdentity.UnnamedEnv && it.isBase } }
withContext(uiContext) {
condaEnvironments.value = environments
state.baseCondaEnv.set(baseConda)
}
return@withContext null
}
}
suspend fun initInterpreterList() {
private suspend fun initInterpreterList() {
withContext(Dispatchers.IO) {
val existingSdks = PyConfigurableInterpreterList.getInstance(null).getModel().sdks.toList()
val allValidSdks = ProjectSpecificSettingsStep.getValidPythonSdks(existingSdks)
@@ -182,10 +192,9 @@ abstract class PythonMutableTargetAddInterpreterModel(params: PyInterpreterModel
state.pipenvExecutable.set(savedPath)
}
else {
val modalityState = ModalityState.current().asContextElement()
scope.launch(Dispatchers.IO) {
val detectedExecutable = com.jetbrains.python.sdk.pipenv.detectPipEnvExecutable()
withContext(Dispatchers.EDT + modalityState) {
withContext(Dispatchers.EDT + ModalityState.any().asContextElement()) {
detectedExecutable?.let { state.pipenvExecutable.set(it.path) }
}
}
@@ -259,8 +268,18 @@ class InstallableSelectableInterpreter(val sdk: PySdkToInstall) : PythonSelectab
open class AddInterpreterState(propertyGraph: PropertyGraph) {
val selectedInterpreter: ObservableMutableProperty<PythonSelectableInterpreter?> = propertyGraph.property(null)
val condaExecutable: ObservableMutableProperty<String> = propertyGraph.property("")
/**
* Use [PythonAddInterpreterModel.getBaseCondaOrError]
*/
val selectedCondaEnv: ObservableMutableProperty<PyCondaEnv?> = propertyGraph.property(null)
/**
* Use [PythonAddInterpreterModel.getBaseCondaOrError]
*/
val baseCondaEnv: ObservableMutableProperty<PyCondaEnv?> = propertyGraph.property(null)
}
class MutableTargetState(propertyGraph: PropertyGraph) : AddInterpreterState(propertyGraph) {
@@ -282,4 +301,18 @@ val PythonAddInterpreterModel.baseSdks
fun PythonAddInterpreterModel.findInterpreter(path: String): PythonSelectableInterpreter? {
return allInterpreters.value.asSequence().find { it.homePath == path }
}
internal suspend fun PythonAddInterpreterModel.detectCondaEnvironmentsOrError(errorSink: ErrorSink) {
detectCondaEnvironments()?.let {
errorSink.emit(it)
}
}
internal suspend fun PythonAddInterpreterModel.getBaseCondaOrError(): Result<PyCondaEnv> {
var baseConda = state.baseCondaEnv.get()
if (baseConda != null) return Result.success(baseConda)
detectCondaEnvironments()?.let { return failure(it) }
baseConda = state.baseCondaEnv.get()
return if (baseConda != null) Result.success(baseConda) else failure(PyBundle.message("python.sdk.conda.no.base.env.error"))
}

View File

@@ -2,6 +2,7 @@
package com.jetbrains.python.sdk.add.v2
import com.intellij.execution.ExecutionException
import com.intellij.openapi.application.EDT
import com.intellij.openapi.module.ModuleUtil
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.projectRoots.ProjectJdkTable
@@ -10,21 +11,24 @@ import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
import com.intellij.openapi.vfs.StandardFileSystems
import com.intellij.platform.ide.progress.ModalTaskOwner
import com.intellij.platform.ide.progress.TaskCancellation
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
import com.jetbrains.python.PyBundle
import com.intellij.platform.ide.progress.withModalProgress
import com.jetbrains.extensions.failure
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.sdk.PythonSdkType
import com.jetbrains.python.sdk.PythonSdkUtil
import com.jetbrains.python.sdk.VirtualEnvReader
import com.jetbrains.python.sdk.add.target.conda.createCondaSdkFromExistingEnv
import com.jetbrains.python.sdk.excludeInnerVirtualEnv
import com.jetbrains.python.sdk.flavors.conda.PyCondaCommand
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
import com.jetbrains.python.sdk.suggestAssociatedSdkName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.nio.file.InvalidPathException
import java.nio.file.Path
// todo should it be overriden for targets?
internal fun PythonMutableTargetAddInterpreterModel.setupVirtualenv(venvPath: Path, projectPath: Path, baseSdk: PythonSelectableInterpreter): Result<Sdk> {
suspend fun PythonMutableTargetAddInterpreterModel.setupVirtualenv(venvPath: Path, projectPath: Path): Result<Sdk> {
val baseSdk = state.baseInterpreter.get()!!
val venvPathOnTarget = venvPath.convertToPathOnTarget(targetEnvironmentConfiguration)
@@ -32,35 +36,52 @@ internal fun PythonMutableTargetAddInterpreterModel.setupVirtualenv(venvPath: Pa
is InstallableSelectableInterpreter -> installBaseSdk(baseSdk.sdk, this.existingSdks)?.homePath // todo handle errors
is ExistingSelectableInterpreter -> baseSdk.sdk.homePath
is DetectedSelectableInterpreter, is ManuallyAddedSelectableInterpreter -> baseSdk.homePath
else -> error("Unknown interpreter")
}
createVirtualenv(baseSdkPath!!,
venvPathOnTarget,
projectPath,
inheritSitePackages = state.inheritSitePackages.get())
withContext(Dispatchers.EDT) {
createVirtualenv(baseSdkPath!!,
venvPathOnTarget,
projectPath,
inheritSitePackages = state.inheritSitePackages.get())
}
if (targetEnvironmentConfiguration != null) error("Remote targets aren't supported")
val venvPython = VirtualEnvReader.Instance.findPythonInPythonRoot(Path.of(venvPathOnTarget))?.toString()
val dir = try {
Path.of(venvPathOnTarget)
}
catch (e: InvalidPathException) {
return Result.failure(e)
}
val venvPython = VirtualEnvReader.Instance.findPythonInPythonRoot(dir)?.toString()
if (venvPython == null) {
return failure(message("commandLine.directoryCantBeAccessed", venvPathOnTarget))
}
val homeFile = try {
StandardFileSystems.local().refreshAndFindFileByPath(venvPython!!)!!
StandardFileSystems.local().refreshAndFindFileByPath(venvPython)
}
catch (e: ExecutionException) {
return Result.failure(e)
}
if (homeFile == null) {
return failure(message("commandLine.directoryCantBeAccessed", venvPathOnTarget))
}
val suggestedName = /*suggestedSdkName ?:*/ suggestAssociatedSdkName(homeFile.path, projectPath.toString())
val newSdk = SdkConfigurationUtil.setupSdk(existingSdks.toTypedArray(), homeFile,
PythonSdkType.getInstance(),
false, null, suggestedName)
false, null, suggestedName)!!
SdkConfigurationUtil.addSdk(newSdk!!)
addSdk(newSdk)
// todo check exclude
ProjectManager.getInstance().openProjects
.firstNotNullOfOrNull { ModuleUtil.findModuleForFile(homeFile, it) }
.firstNotNullOfOrNull {
withContext(Dispatchers.IO) {
ModuleUtil.findModuleForFile(homeFile, it)
}
}
?.excludeInnerVirtualEnv(newSdk)
return Result.success(newSdk)
@@ -68,13 +89,24 @@ internal fun PythonMutableTargetAddInterpreterModel.setupVirtualenv(venvPath: Pa
// todo rewrite this
internal fun PythonAddInterpreterModel.selectCondaEnvironment(identity: PyCondaEnvIdentity): Sdk {
/**
* [base] or selected
*/
suspend fun PythonAddInterpreterModel.selectCondaEnvironment(base: Boolean): Result<Sdk> {
val identity = if (base) {
getBaseCondaOrError()
}
else {
state.selectedCondaEnv.get()?.let { Result.success(it) } ?: failure(message("python.sdk.conda.no.env.selected.error"))
}
.getOrElse { return Result.failure(it) }
.envIdentity
val existingSdk = ProjectJdkTable.getInstance().findJdk(identity.userReadableName)
if (existingSdk != null && isCondaSdk(existingSdk)) return existingSdk
if (existingSdk != null && isCondaSdk(existingSdk)) return Result.success(existingSdk)
val sdk = runWithModalProgressBlocking(ModalTaskOwner.guess(),
PyBundle.message("sdk.create.custom.conda.create.progress"),
TaskCancellation.nonCancellable()) {
val sdk = withModalProgress(ModalTaskOwner.guess(),
message("sdk.create.custom.conda.create.progress"),
TaskCancellation.nonCancellable()) {
//PyCondaCommand(condaExecutableOnTarget, targetConfig = targetEnvironmentConfiguration)
PyCondaCommand(state.condaExecutable.get(), targetConfig = targetEnvironmentConfiguration).createCondaSdkFromExistingEnv(identity,
this@selectCondaEnvironment.existingSdks,
@@ -82,8 +114,8 @@ internal fun PythonAddInterpreterModel.selectCondaEnvironment(identity: PyCondaE
}
(sdk.sdkType as PythonSdkType).setupSdkPaths(sdk)
SdkConfigurationUtil.addSdk(sdk)
return sdk
addSdk(sdk)
return Result.success(sdk)
}

View File

@@ -0,0 +1,12 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.sdk.add.v2
import com.intellij.openapi.application.writeAction
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk
internal suspend fun addSdk(sdk: Sdk) {
writeAction {
ProjectJdkTable.getInstance().addJdk(sdk)
}
}

View File

@@ -36,6 +36,7 @@ import com.jetbrains.python.sdk.conda.CondaInstallManager
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.ErrorSink
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.flow.SharedFlow
@@ -447,14 +448,14 @@ fun Panel.executableSelector(
return textFieldCell!!
}
internal fun createInstallCondaFix(model: PythonAddInterpreterModel): ActionLink {
internal fun createInstallCondaFix(model: PythonAddInterpreterModel, errorSink: ErrorSink): ActionLink {
return ActionLink(message("sdk.create.custom.venv.install.fix.title", "Miniconda", "")) {
PythonSdkFlavor.clearExecutablesCache()
CondaInstallManager.installLatest(null)
model.scope.launch(model.uiContext) {
model.condaEnvironmentsLoading.value = true
model.detectCondaExecutable()
model.detectCondaEnvironments()
model.detectCondaEnvironmentsOrError(errorSink)
model.condaEnvironmentsLoading.value = false
}
}

View File

@@ -8,6 +8,7 @@ import com.google.gson.Gson
import com.intellij.execution.target.*
import com.intellij.openapi.progress.*
import com.intellij.openapi.projectRoots.Sdk
import com.jetbrains.extensions.failure
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.sdk.add.target.conda.TargetCommandExecutor
import com.jetbrains.python.sdk.add.target.conda.createCondaSdkFromExistingEnv
@@ -38,7 +39,8 @@ data class PyCondaEnv(
* @return unparsed output of conda info --envs --json
*/
private suspend fun getEnvsInfo(command: TargetCommandExecutor, fullCondaPathOnTarget: FullPathOnTarget): Result<String> {
return runCatching { command.execute(listOf(fullCondaPathOnTarget, "info", "--envs", "--json")).thenApply { it.stdout }.await() }
val output = command.execute(listOf(fullCondaPathOnTarget, "info", "--envs", "--json")).await()
return if (output.exitCode == 0) Result.success(output.stdout) else failure(output.stderr)
}
/**

View File

@@ -0,0 +1,62 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.util
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.asContextElement
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.util.NlsSafe
import com.jetbrains.python.PyBundle
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.withContext
/**
* [FlowCollector.emit] user-readable errors here.
*
* This class should be used by the topmost classes, tightly coupled to the UI.
* For the most business-logic and backend functions please return [Result] or error.
*
* Please do not report *all* exceptions here: This is *not* the class for NPEs and AOOBs:
* do not pass exceptions caught by `catch(e: Exception)` or `runCatching`: only report exceptions user interested in.
* `IOException` or `ExecutionException` are generally ok.
*
* There will be unified sink soon to show and log errors.
* Currently, only [ShowingMessageErrorSync] is a well-known implementation
*
* Example:
* ```kotlin
* suspend fun someLogic(): Result<@NlsSafe String> = withContext(Dispatchers.IO) {
* try {
* Result.success(Path.of("1.txt").readText())
* }
* catch (e: IOException) {
* Result.failure(e)
* }
* }
*
* suspend fun ui(errorSink: ErrorSink) {
* someLogic()
* .onSuccess {
* Messages.showInfoMessage("..", it)
* }
* .onFailure {
* errorSink.emit(it.localizedMessage)
* }
* }
* ```
*/
typealias ErrorSink = FlowCollector<@NlsSafe String>
/**
* Displays error with a message box and writes it to a log.
*/
object ShowingMessageErrorSync : ErrorSink {
override suspend fun emit(value: @NlsSafe String) {
withContext(Dispatchers.EDT + ModalityState.any().asContextElement()) {
thisLogger().warn(value)
Messages.showErrorDialog(value, PyBundle.message("python.error"))
}
}
}