diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties index f44a1f1c207f..a5346072a59e 100644 --- a/python/pluginResources/messages/PyBundle.properties +++ b/python/pluginResources/messages/PyBundle.properties @@ -371,7 +371,6 @@ python.sdk.finish=Finish python.sdk.setting.up.pipenv.sentence=Setting up pipenv environment python.sdk.setting.up.pipenv.title=Setting up Pipenv Environment python.sdk.install.requirements.from.pipenv.lock=Install requirements from Pipfile.lock -python.sdk.pipenv.executable.not.found=Pipenv executable is not found python.sdk.pipenv.executable=Pipenv executable: python.sdk.pipenv.select.executable.title=Select Path to Pipenv Executable python.sdk.pipenv.associated.project=Associated project: @@ -390,6 +389,7 @@ python.sdk.pipenv.pip.file.watcher=Pipfile Watcher python.sdk.new.project.environment=Environment: python.sdk.new.project.environment.type=Environment type: python.sdk.new.error.no.absolute=Path must be absolute +python.sdk.executable.is.not.detected=Executable is not detected # Poetry package manager and SDK python.sdk.dialog.message.creating.virtual.environments.based.on.poetry.environments.not.supported=Creating virtual environments based on Poetry environments is not supported @@ -400,8 +400,6 @@ python.sdk.inspection.message.version.outdated.latest=''{0}'' version {1} is out python.sdk.poetry.associated.module=Associated module: python.sdk.poetry.associated.project=Associated project: python.sdk.poetry.dialog.add.new.environment.in.project.checkbox=Create an in-project environment -python.sdk.poetry.environment.panel.title=Poetry Environment -python.sdk.poetry.executable.not.found=Poetry executable is not found python.sdk.poetry.executable=Poetry executable: python.sdk.poetry.execution.exception.no.poetry.message=Cannot find Poetry python.sdk.poetry.install.packages.from.toml.checkbox.text=Install packages from pyproject.toml diff --git a/python/src/com/jetbrains/python/sdk/add/v2/ValidatedPathField.kt b/python/src/com/jetbrains/python/sdk/add/v2/ValidatedPathField.kt index 0701671ec34e..1d66899ca050 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/ValidatedPathField.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/ValidatedPathField.kt @@ -160,7 +160,11 @@ class ValidatedPathField>( * this method emulates the validation job for the current field when the user switches to another form. */ private fun runJobFor3rdPartyValidation() { - scope.launch { resetValidationJob { delay(1.minutes) } } + scope.launch { + resetValidationJob { + if (backProperty.get() == null) delay(1.minutes) + } + } } private suspend fun resetValidationJob(block: suspend CoroutineScope.() -> Unit) { diff --git a/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchExistingEnvironmentSelector.kt b/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchExistingEnvironmentSelector.kt index b5c0189ab22d..93aa5d0b240d 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchExistingEnvironmentSelector.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchExistingEnvironmentSelector.kt @@ -31,16 +31,10 @@ internal class HatchExistingEnvironmentSelector( override val model: PythonMutableTargetAddInterpreterModel

, ) : PythonExistingEnvironmentConfigurator

(model) { val interpreterType: InterpreterType = InterpreterType.HATCH - val executable: ObservableMutableProperty?> = propertyGraph.property(model.state.hatchExecutable.get()) + val executable: ObservableMutableProperty?> = model.state.hatchExecutable private lateinit var hatchFormFields: HatchFormFields

- init { - propertyGraph.dependsOn(executable, model.state.hatchExecutable, deleteWhenChildModified = false) { - model.state.hatchExecutable.get() - } - } - override fun setupUI(panel: Panel, validationRequestor: DialogValidationRequestor) { hatchFormFields = panel.buildHatchFormFields( model = model, @@ -53,13 +47,13 @@ internal class HatchExistingEnvironmentSelector( override fun onShown(scope: CoroutineScope) { hatchFormFields.onShown(scope, model, state, isFilterOnlyExisting = true) - model.state.hatchExecutable.afterChange { commonExecutable -> - if (commonExecutable == null) { + executable.afterChange { hatchExecutable -> + if (hatchExecutable?.validationResult?.successOrNull == null) { model.hatchEnvironmentsResult.value = null return@afterChange } - val binaryToExec = commonExecutable.pathHolder?.let { model.fileSystem.getBinaryToExec(it) } + val binaryToExec = hatchExecutable.pathHolder?.let { model.fileSystem.getBinaryToExec(it) } ?: return@afterChange scope.launch(Dispatchers.IO) { model.detectHatchEnvironments(binaryToExec).also { diff --git a/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchNewEnvironmentCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchNewEnvironmentCreator.kt index f673eb089538..86c07f42e9f9 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchNewEnvironmentCreator.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchNewEnvironmentCreator.kt @@ -31,16 +31,10 @@ internal class HatchNewEnvironmentCreator( errorSink: ErrorSink, ) : CustomNewEnvironmentCreator

("hatch", model, errorSink) { override val interpreterType: InterpreterType = InterpreterType.HATCH - override val executable: ObservableMutableProperty?> = propertyGraph.property(model.state.hatchExecutable.get()) + override val executable: ObservableMutableProperty?> = model.state.hatchExecutable private val hatchEnvironmentProperty: ObservableMutableProperty = propertyGraph.property(null) private lateinit var hatchFormFields: HatchFormFields

- init { - propertyGraph.dependsOn(executable, model.state.hatchExecutable, deleteWhenChildModified = false) { - model.state.hatchExecutable.get() - } - } - override fun setupUI(panel: Panel, validationRequestor: DialogValidationRequestor) { hatchFormFields = panel.buildHatchFormFields( model = model, diff --git a/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchUIComponents.kt b/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchUIComponents.kt index a1d85b0cdb86..2b08c0ca9932 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchUIComponents.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchUIComponents.kt @@ -5,6 +5,7 @@ import com.intellij.icons.AllIcons import com.intellij.openapi.application.EDT import com.intellij.openapi.observable.properties.AtomicBooleanProperty import com.intellij.openapi.observable.properties.ObservableMutableProperty +import com.intellij.openapi.observable.util.transform import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.ValidationInfo import com.intellij.openapi.ui.validation.DialogValidationRequestor @@ -20,6 +21,7 @@ import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.bindItem import com.intellij.ui.dsl.builder.components.ValidationType import com.intellij.ui.dsl.builder.components.validationTooltip +import com.intellij.util.lateinitVal import com.intellij.util.ui.JBUI import com.jetbrains.python.PyBundle.message import com.jetbrains.python.Result @@ -215,24 +217,28 @@ internal fun

Panel.buildHatchFormFields( val binaryToExec = model.fileSystem.getBinaryToExec(it) ValidatedPath.Executable(it, binaryToExec.getToolVersion("hatch")) } - - val environmentComboBox = addEnvironmentComboBox( - model = model, - hatchEnvironmentProperty = hatchEnvironmentProperty, - validationRequestor = validationRequestor, - isValidateOnlyNotExisting = isGenerateNewMode - ) - + var environmentComboBox: ComboBox by lateinitVal() var basePythonComboBox: PythonInterpreterComboBox

? = null - if (isGenerateNewMode) { - basePythonComboBox = pythonInterpreterComboBox( - model.fileSystem, - title = message("sdk.create.custom.base.python"), - selectedSdkProperty = model.state.baseInterpreter, + + rowsRange { + environmentComboBox = addEnvironmentComboBox( + model = model, + hatchEnvironmentProperty = hatchEnvironmentProperty, validationRequestor = validationRequestor, - onPathSelected = model::addManuallyAddedInterpreter, + isValidateOnlyNotExisting = isGenerateNewMode ) - } + + if (isGenerateNewMode) { + basePythonComboBox = pythonInterpreterComboBox( + model.fileSystem, + title = message("sdk.create.custom.base.python"), + selectedSdkProperty = model.state.baseInterpreter, + validationRequestor = validationRequestor, + onPathSelected = model::addManuallyAddedInterpreter, + ) + } + }.visibleIf(hatchExecutableProperty.transform { it?.validationResult?.successOrNull != null }) + return HatchFormFields(environmentComboBox, basePythonComboBox, executablePath) } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/models.kt b/python/src/com/jetbrains/python/sdk/add/v2/models.kt index b188c133b127..7be856ce416e 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/models.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/models.kt @@ -14,7 +14,7 @@ import com.intellij.platform.eel.provider.localEel import com.intellij.python.community.execService.BinOnEel import com.intellij.python.community.execService.BinaryToExec import com.intellij.python.community.services.shared.* -import com.intellij.python.hatch.HatchConfiguration +import com.intellij.python.hatch.HatchConfiguration.getOrDetectHatchExecutablePath import com.intellij.python.hatch.HatchVirtualEnvironment import com.intellij.python.hatch.getHatchService import com.intellij.python.pyproject.PyProjectToml @@ -195,53 +195,62 @@ abstract class PythonMutableTargetAddInterpreterModel

(projectPat } } - suspend fun detectPoetryExecutable(): Unit = withContext(Dispatchers.IO) { - if ((fileSystem as? FileSystem.Eel)?.eelApi != localEel) return@withContext // getPoetryExecutable() works only with localEel currently + suspend fun detectPoetryExecutable() { + if ((fileSystem as? FileSystem.Eel)?.eelApi != localEel) return // getPoetryExecutable() works only with localEel currently - getPoetryExecutable().getOrNull()?.let { - val binaryToExec = fileSystem.getBinaryToExec(PathHolder.Eel(it)) - val version = binaryToExec.getToolVersion("poetry") - withContext(Dispatchers.EDT) { - state.poetryExecutable.set(ValidatedPath.Executable(PathHolder.Eel(it) as P, version)) - } + state.poetryExecutable.autodetectWithVersionProbe("poetry") { + getPoetryExecutable().getOrNull()?.let { PathHolder.Eel(it) } as P? } } - suspend fun detectPipEnvExecutable(): Unit = withContext(Dispatchers.IO) { - if ((fileSystem as? FileSystem.Eel)?.eelApi != localEel) return@withContext // getPipEnvExecutable() works only with localEel currently + suspend fun detectPipEnvExecutable() { + if ((fileSystem as? FileSystem.Eel)?.eelApi != localEel) return // getPipEnvExecutable() works only with localEel currently - getPipEnvExecutable().getOrNull()?.let { - val binaryToExec = fileSystem.getBinaryToExec(PathHolder.Eel(it)) - val version = binaryToExec.getToolVersion("pipenv") - withContext(Dispatchers.EDT) { - state.pipenvExecutable.set(ValidatedPath.Executable(PathHolder.Eel(it) as P, version)) - } + state.pipenvExecutable.autodetectWithVersionProbe("pipenv") { + getPipEnvExecutable().getOrNull()?.let { PathHolder.Eel(it) } as P? } } - suspend fun detectUvExecutable(): Unit = withContext(Dispatchers.IO) { - if ((fileSystem as? FileSystem.Eel)?.eelApi != localEel) return@withContext // getUvExecutable() works only with localEel currently + suspend fun detectUvExecutable() { + if ((fileSystem as? FileSystem.Eel)?.eelApi != localEel) return // getUvExecutable() works only with localEel currently - getUvExecutable()?.let { - val binaryToExec = fileSystem.getBinaryToExec(PathHolder.Eel(it)) - val version = binaryToExec.getToolVersion("uv") - withContext(Dispatchers.EDT) { - state.uvExecutable.set(ValidatedPath.Executable(PathHolder.Eel(it) as P, version)) - } + state.uvExecutable.autodetectWithVersionProbe("uv") { + getUvExecutable()?.let { PathHolder.Eel(it) } as P? } } - suspend fun detectHatchExecutable(): Unit = withContext(Dispatchers.IO) { - if (fileSystem !is FileSystem.Eel) return@withContext // getOrDetectHatchExecutablePath() works only with eel filesystem currently + suspend fun detectHatchExecutable() { + if (fileSystem !is FileSystem.Eel) return // getOrDetectHatchExecutablePath() works only with eel filesystem currently - HatchConfiguration.getOrDetectHatchExecutablePath(fileSystem.eelApi).getOrNull()?.let { - val binaryToExec = fileSystem.getBinaryToExec(PathHolder.Eel(it)) - val version = binaryToExec.getToolVersion("hatch") - withContext(Dispatchers.EDT) { - state.hatchExecutable.set(ValidatedPath.Executable(PathHolder.Eel(it) as P, version)) - } + state.hatchExecutable.autodetectWithVersionProbe("hatch") { + getOrDetectHatchExecutablePath(fileSystem.eelApi).getOrNull()?.let { PathHolder.Eel(it) } as P? } } + + private suspend fun ObservableMutableProperty?>.autodetectWithVersionProbe( + toolVersionPrefix: String, + toolPathSupplier: suspend () -> P?, + ): Unit = withContext(Dispatchers.IO) { + val path = toolPathSupplier.invoke() + val validatedPath = path?.validateToolExecutableByVersionProbe(toolVersionPrefix) + ?: ValidatedPath.Executable( + pathHolder = path, + validationResult = PyResult.localizedError(message("python.sdk.executable.is.not.detected")) + ) + + withContext(Dispatchers.EDT) { + set(validatedPath) + } + } + + private suspend fun P.validateToolExecutableByVersionProbe(toolVersionPrefix: String): ValidatedPath.Executable

{ + val binaryToExec = fileSystem.getBinaryToExec(this) + val validationResult = binaryToExec.getToolVersion(toolVersionPrefix) + return ValidatedPath.Executable( + pathHolder = this, + validationResult = validationResult + ) + } } class PythonLocalAddInterpreterModel

(projectPathFlows: ProjectPathFlows, fileSystem: FileSystem

) : PythonMutableTargetAddInterpreterModel

(projectPathFlows, fileSystem) {