From edaad5131e640ce942c322251c26acfd6d06649d Mon Sep 17 00:00:00 2001 From: Alexey Katsman Date: Thu, 19 Feb 2026 15:27:17 +0100 Subject: [PATCH] PY-87789 Fix existing venv alert for uv The alert for existing venv in SDK creation dialog was missing. This commit adds it back, also tying it together with the venv path field validator. In particular, we now support existing venv alert for any venv typed in the field (including on remotes). Also, it's now possible to override existing venv. Also these changes fix broken alert for in-project poetry environment. (cherry picked from commit 3cf7202ab0a1393c510abb07e2e0d5676cff9a69) IJ-MR-192485 GitOrigin-RevId: 79b6852ddd6edef14a1c808a1a5bf83e2ac5fc31 --- .../python/venvReader/VirtualEnvReader.kt | 24 ++++++++++------- .../jetbrains/python/sdk/add/v2/FileSystem.kt | 19 +++++++++++--- .../python/sdk/add/v2/ValidatedPathField.kt | 23 +++++++++++++--- .../add/v2/poetry/EnvironmentCreatorPoetry.kt | 5 ++-- .../sdk/add/v2/uv/EnvironmentCreatorUv.kt | 26 +++++++++++++++++++ .../add/v2/venvExistenceValidationAlert.kt | 11 ++++---- 6 files changed, 83 insertions(+), 25 deletions(-) diff --git a/python/openapi/src/com/jetbrains/python/venvReader/VirtualEnvReader.kt b/python/openapi/src/com/jetbrains/python/venvReader/VirtualEnvReader.kt index ee051ccf65c5..9299f5b5e6f1 100644 --- a/python/openapi/src/com/jetbrains/python/venvReader/VirtualEnvReader.kt +++ b/python/openapi/src/com/jetbrains/python/venvReader/VirtualEnvReader.kt @@ -181,7 +181,7 @@ class VirtualEnvReader private constructor( } } - fun getVenvRootPath(path: Path): Path? { + fun getVenvName(path: Path): String? { val bin = path.parent val binFolderName = when (forcedOs ?: path.osFamily) { @@ -194,18 +194,24 @@ class VirtualEnvReader private constructor( } val venv = bin.parent + return venv?.name + } - if (venv == null) { + fun getVenvNameForTarget(path: FullPathOnTarget, platform: Platform): String? { + val separator = platform.fileSeparator + val bin = path.substringBeforeLast(separator) + + val binFolderName = when (platform) { + Platform.UNIX -> "bin" + Platform.WINDOWS -> "Scripts" + } + + if (bin.substringAfterLast(separator) != binFolderName) { return null } - val root = venv.parent - - if (root == null) { - return null - } - - return root + val venv = bin.substringBeforeLast(separator) + return venv.substringAfterLast(separator).takeIf { it.isNotBlank() } } /** diff --git a/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt b/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt index 6a7bd3c981ec..61f023b7570d 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt @@ -62,6 +62,7 @@ import java.nio.file.Path import kotlin.io.path.Path import kotlin.io.path.exists import kotlin.io.path.isDirectory +import kotlin.io.path.name private val LOG: Logger = fileLogger() @@ -92,7 +93,8 @@ sealed interface FileSystem

{ suspend fun wrapSdk(sdk: Sdk): SdkWrapper

suspend fun detectSelectableVenv(projectPathPrefix: Path): List> fun preferredInterpreterBasePath(): P? = null - suspend fun resolvePythonBinary(pythonHome: P): P? + fun resolvePythonBinary(pythonHome: P): P? + fun getVenvName(pythonHome: P): String? fun getBinaryToExec(path: P): BinaryToExec suspend fun which(cmd: String): P? @@ -236,10 +238,14 @@ sealed interface FileSystem

{ else -> null } - override suspend fun resolvePythonBinary(pythonHome: PathHolder.Eel): PathHolder.Eel? { + override fun resolvePythonBinary(pythonHome: PathHolder.Eel): PathHolder.Eel? { return pythonHome.path.resolvePythonBinary()?.let { PathHolder.Eel(it) } } + override fun getVenvName(pythonHome: PathHolder.Eel): String? { + return resolvePythonBinary(pythonHome)?.let { VirtualEnvReader().getVenvName(it.path) } + } + override suspend fun which(cmd: String): PathHolder.Eel? = detectTool(cmd, eelApi)?.let { PathHolder.Eel(it) } override suspend fun getHomePath(): PathHolder.Eel = PathHolder.Eel(eelApi.userInfo.home.asNioPath()) @@ -353,12 +359,19 @@ sealed interface FileSystem

{ return listOf(systemPython) } - override suspend fun resolvePythonBinary(pythonHome: PathHolder.Target): PathHolder.Target { + override fun resolvePythonBinary(pythonHome: PathHolder.Target): PathHolder.Target { val pythonHomeString = pythonHome.pathString val platform = targetEnvironmentConfiguration.getPlatformAndRoot().platform return PathHolder.Target(VirtualEnvReader().findPythonInPythonRootForTarget(pythonHomeString, platform)) } + override fun getVenvName(pythonHome: PathHolder.Target): String? { + val pythonBinary = resolvePythonBinary(pythonHome) + val pythonBinaryString = pythonBinary.pathString + val platform = targetEnvironmentConfiguration.getPlatformAndRoot().platform + return VirtualEnvReader().getVenvNameForTarget(pythonBinaryString, platform) + } + override suspend fun which(cmd: String): PathHolder.Target? { val binaryPathString = executeCommand("which $cmd") ?: return null val binaryPathOnFS = parsePath(binaryPathString).getOr { return null } 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 5b8f7f29dacf..4ea5ef5319fd 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/ValidatedPathField.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/ValidatedPathField.kt @@ -10,6 +10,7 @@ import com.intellij.openapi.actionSystem.CustomShortcutSet import com.intellij.openapi.application.EDT import com.intellij.openapi.fileChooser.FileChooserDescriptor import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.observable.properties.AtomicProperty import com.intellij.openapi.observable.properties.ObservableMutableProperty import com.intellij.openapi.observable.properties.ObservableProperty import com.intellij.openapi.observable.util.and @@ -317,6 +318,7 @@ internal fun > Panel.validatablePath missingExecutableText: @Nls String?, installAction: ActionLink? = null, isFileSelectionMode: Boolean = true, + venvExistenceValidationState: ObservableProperty? = null, ): ValidatedPathField { val validatedPathField = ValidatedPathField( @@ -335,16 +337,29 @@ internal fun > Panel.validatablePath ).visibleIf(pathValidator.backProperty.transform { it?.pathHolder == null }.and(pathValidator.isDirtyValue.not())) } + val initialValidationRequestor = (validationRequestor + and WHEN_PROPERTY_CHANGED(pathValidator.isDirtyValue) + and WHEN_PROPERTY_CHANGED(pathValidator.backProperty)) + + val finalValidationRequestor = if (venvExistenceValidationState != null) { + initialValidationRequestor and WHEN_PROPERTY_CHANGED(venvExistenceValidationState) + } + else initialValidationRequestor + row(labelText) { cell(validatedPathField) .align(AlignX.FILL) - .validationRequestor(validationRequestor - and WHEN_PROPERTY_CHANGED(pathValidator.isDirtyValue) - and WHEN_PROPERTY_CHANGED(pathValidator.backProperty) - ) + .validationRequestor(finalValidationRequestor) .validationOnInput { component -> if (!component.isVisible) return@validationOnInput null + val isVenvOverridden = when (venvExistenceValidationState?.get()) { + is VenvExistenceValidationState.Warning -> true + is VenvExistenceValidationState.Invisible, is VenvExistenceValidationState.Error, null -> false + } + + if (isVenvOverridden) return@validationOnInput null + val pyErrorMessage = pathValidator.backProperty.get()?.validationResult?.errorOrNull?.message when { diff --git a/python/src/com/jetbrains/python/sdk/add/v2/poetry/EnvironmentCreatorPoetry.kt b/python/src/com/jetbrains/python/sdk/add/v2/poetry/EnvironmentCreatorPoetry.kt index 2918330c70ed..e3533f9f8330 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/poetry/EnvironmentCreatorPoetry.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/poetry/EnvironmentCreatorPoetry.kt @@ -49,7 +49,6 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.nio.file.Path -import java.nio.file.Paths import kotlin.io.path.exists internal class EnvironmentCreatorPoetry

( @@ -108,7 +107,7 @@ internal class EnvironmentCreatorPoetry

( venvExistenceValidationState.set( if (venvPath.exists()) - Error(Paths.get(VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME)) + Error(VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME) else Invisible ) @@ -145,7 +144,7 @@ internal class EnvironmentCreatorPoetry

( with(panel) { row("") { checkBox(PyBundle.message("python.sdk.poetry.dialog.add.new.environment.in.project.checkbox")) - .bindSelected(service().state::isInProjectEnv) + .bindSelected(isInProjectEnvProp) } } } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/uv/EnvironmentCreatorUv.kt b/python/src/com/jetbrains/python/sdk/add/v2/uv/EnvironmentCreatorUv.kt index e3aad4acebe1..1ff43ebf9de6 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/uv/EnvironmentCreatorUv.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/uv/EnvironmentCreatorUv.kt @@ -30,6 +30,8 @@ import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.UV import com.jetbrains.python.sdk.add.v2.ToolValidator import com.jetbrains.python.sdk.add.v2.ValidatedPath import com.jetbrains.python.sdk.add.v2.ValidatedPathField +import com.jetbrains.python.sdk.add.v2.VenvAlreadyExistsError +import com.jetbrains.python.sdk.add.v2.VenvExistenceValidationState import com.jetbrains.python.sdk.add.v2.savePathForEelOnly import com.jetbrains.python.sdk.add.v2.validatablePathField import com.jetbrains.python.sdk.uv.impl.createUvCli @@ -38,6 +40,7 @@ import com.jetbrains.python.sdk.uv.impl.setUvExecutableLocal import com.jetbrains.python.sdk.uv.setupNewUvSdkAndEnv import com.jetbrains.python.statistics.InterpreterType import com.jetbrains.python.util.ShowingMessageErrorSync +import com.jetbrains.python.venvReader.VirtualEnvReader import io.github.z4kn4fein.semver.Version import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -79,12 +82,28 @@ internal class EnvironmentCreatorUv

( savePathForEelOnly(pathHolder) { path -> setUvExecutableLocal(path) } } + private val venvAlreadyExistsError = propertyGraph.property?>(null) private val loading = AtomicBooleanProperty(false) init { model.uvViewModel.uvExecutable.afterChange { executableFlow.value = it } + + venvAlreadyExistsError.afterChange { + if (it == null) { + venvExistenceValidationState.set(VenvExistenceValidationState.Invisible) + } + else { + val venvName = model.fileSystem.getVenvName(it.detectedSelectableInterpreter.homePath) + ?: VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME + venvExistenceValidationState.set(VenvExistenceValidationState.Error(venvName)) + } + } + + propertyGraph.dependsOn(venvAlreadyExistsError, model.uvViewModel.uvVenvPath, deleteWhenChildModified = false) { + model.uvViewModel.uvVenvPath.get()?.validationResult?.errorOrNull as? VenvAlreadyExistsError

+ } } override fun setupUI(panel: Panel, validationRequestor: DialogValidationRequestor) { @@ -120,7 +139,14 @@ internal class EnvironmentCreatorUv

( labelText = message("sdk.create.custom.location"), missingExecutableText = null, isFileSelectionMode = false, + venvExistenceValidationState = venvExistenceValidationState, ) + + row("") { + venvExistenceValidationAlert(validationRequestor) { + onVenvSelectExisting() + } + } } } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/venvExistenceValidationAlert.kt b/python/src/com/jetbrains/python/sdk/add/v2/venvExistenceValidationAlert.kt index d2a4bca87885..85fc2bd43800 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/venvExistenceValidationAlert.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/venvExistenceValidationAlert.kt @@ -12,18 +12,17 @@ import com.intellij.ui.dsl.builder.components.ValidationType import com.intellij.ui.dsl.builder.components.validationTooltip import com.intellij.ui.dsl.gridLayout.UnscaledGaps import com.jetbrains.python.PyBundle.message -import java.nio.file.Path import javax.swing.JComponent internal sealed class VenvExistenceValidationState { data object Invisible : VenvExistenceValidationState() data class Warning( - val venvPath: Path, + val venvName: String, ) : VenvExistenceValidationState() data class Error( - val venvPath: Path, + val venvName: String, ) : VenvExistenceValidationState() } @@ -50,15 +49,15 @@ internal fun Row.venvExistenceValidationAlert( is VenvExistenceValidationState.Warning -> listOfNotNull(selectExitingEnvironment) is VenvExistenceValidationState.Error -> listOfNotNull( ActionLink(message("sdk.create.custom.override.action")) { - observableState.set(VenvExistenceValidationState.Warning(state.venvPath)) + observableState.set(VenvExistenceValidationState.Warning(state.venvName)) }, selectExitingEnvironment, ) } val text = when (state) { VenvExistenceValidationState.Invisible -> "" - is VenvExistenceValidationState.Warning -> message("sdk.create.custom.override.warning", state.venvPath.toString()) - is VenvExistenceValidationState.Error -> message("sdk.create.custom.override.error", state.venvPath.toString()) + is VenvExistenceValidationState.Warning -> message("sdk.create.custom.override.warning", state.venvName) + is VenvExistenceValidationState.Error -> message("sdk.create.custom.override.error", state.venvName) } noticePlaceholder.component =