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 =