From 5fa4f67865ddb4908958c761057fb98eea7bef19 Mon Sep 17 00:00:00 2001 From: Alexey Katsman Date: Fri, 7 Nov 2025 10:51:13 +0100 Subject: [PATCH] PY-85162 Pre-select tool based on existing envs and other markers GitOrigin-RevId: 3047ea7b0c18bfd591e7551a90c027d543aa5c30 --- .../conda/PyEnvironmentYmlSdkConfiguration.kt | 2 +- .../PyPipfileSdkConfiguration.kt | 2 +- .../configuration/PyVenvSdkConfiguration.kt | 2 +- .../messages/PyBundle.properties | 2 + .../PyProjectSdkConfigurationExtension.kt | 9 ++++ .../jetbrains/python/sdk/add/v2/FileSystem.kt | 3 ++ .../sdk/add/v2/PythonAddCustomInterpreter.kt | 33 ++++++++++++++ .../com/jetbrains/python/sdk/add/v2/common.kt | 22 +++++++--- .../conda/CondaExistingEnvironmentSelector.kt | 4 +- .../python/sdk/add/v2/conda/CondaViewModel.kt | 44 +++++++++++++++++-- .../com/jetbrains/python/sdk/add/v2/models.kt | 2 +- 11 files changed, 109 insertions(+), 16 deletions(-) diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/conda/PyEnvironmentYmlSdkConfiguration.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/conda/PyEnvironmentYmlSdkConfiguration.kt index 28286144b039..a8be546839d0 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/conda/PyEnvironmentYmlSdkConfiguration.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/conda/PyEnvironmentYmlSdkConfiguration.kt @@ -63,7 +63,7 @@ import kotlin.io.path.name @ApiStatus.Internal class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExtension { - override val toolId: ToolId = ToolId("Conda") + override val toolId: ToolId = CONDA_TOOL_ID override suspend fun checkEnvironmentAndPrepareSdkCreator(module: Module): CreateSdkInfo? = prepareSdkCreator( { checkManageableEnv(module, it) } diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPipfileSdkConfiguration.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPipfileSdkConfiguration.kt index b67b306e11d3..7c0bab2aad64 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPipfileSdkConfiguration.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPipfileSdkConfiguration.kt @@ -52,7 +52,7 @@ private val LOGGER = Logger.getInstance(PyPipfileSdkConfiguration::class.java) @ApiStatus.Internal class PyPipfileSdkConfiguration : PyProjectSdkConfigurationExtension { - override val toolId: ToolId = ToolId("pipenv") + override val toolId: ToolId = PIPENV_TOOL_ID override suspend fun checkEnvironmentAndPrepareSdkCreator(module: Module): CreateSdkInfo? = prepareSdkCreator( { checkManageableEnv(module, it) } diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyVenvSdkConfiguration.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyVenvSdkConfiguration.kt index d6bc5b791afa..3aedbcfa057b 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyVenvSdkConfiguration.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyVenvSdkConfiguration.kt @@ -26,7 +26,7 @@ class PyVenvSdkConfiguration : PyProjectSdkConfigurationExtension { private val existingSdks by lazy { PythonSdkUtil.getAllSdks() } private val context = UserDataHolderBase() - override val toolId: ToolId = ToolId("Venv") + override val toolId: ToolId = VENV_TOOL_ID override suspend fun checkEnvironmentAndPrepareSdkCreator(module: Module): CreateSdkInfo? = prepareSdkCreator( { checkManageableEnv(module) } diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties index 9b206b515469..7302af390413 100644 --- a/python/pluginResources/messages/PyBundle.properties +++ b/python/pluginResources/messages/PyBundle.properties @@ -587,6 +587,8 @@ sdk.create.custom.uv=uv sdk.create.custom.hatch=Hatch sdk.create.custom.python=Python +sdk.create.check.environments=Checking Existing Environments + sdk.rendering.detected.grey.text=system sdk.rendering.installable.grey.text=download and install diff --git a/python/python-sdk/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfigurationExtension.kt b/python/python-sdk/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfigurationExtension.kt index 9f23d6cae0e1..ea68b186259b 100644 --- a/python/python-sdk/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfigurationExtension.kt +++ b/python/python-sdk/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfigurationExtension.kt @@ -75,3 +75,12 @@ interface PyProjectSdkConfigurationExtension { * [createSdkInfo] with [toolId] that created it */ data class CreateSdkInfoWithTool(val createSdkInfo: CreateSdkInfo, val toolId: ToolId) + +@ApiStatus.Internal +val VENV_TOOL_ID: ToolId = ToolId("Venv") + +@ApiStatus.Internal +val CONDA_TOOL_ID: ToolId = ToolId("Conda") + +@ApiStatus.Internal +val PIPENV_TOOL_ID: ToolId = ToolId("pipenv") 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 15cce080e6d3..e31d6335a966 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt @@ -57,6 +57,7 @@ internal class VenvAlreadyExistsError

( sealed interface FileSystem

{ val isReadOnly: Boolean val isBrowseable: Boolean + val isLocal: Boolean fun parsePath(raw: String): PyResult

fun validateExecutable(path: P): PyResult @@ -78,6 +79,7 @@ sealed interface FileSystem

{ ) : FileSystem { override val isBrowseable: Boolean = true override val isReadOnly: Boolean = false + override val isLocal: Boolean = eelApi == localEel override fun getBinaryToExec(path: PathHolder.Eel): BinaryToExec { return BinOnEel(path.path) } @@ -197,6 +199,7 @@ sealed interface FileSystem

{ get() = !PythonInterpreterTargetEnvironmentFactory.isMutable(targetEnvironmentConfiguration) override val isBrowseable: Boolean get() = targetEnvironmentConfiguration.getTargetType() is BrowsableTargetEnvironmentType + override val isLocal: Boolean = false private val systemPythonCache = ArrayList>() diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddCustomInterpreter.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddCustomInterpreter.kt index dfb14aa198eb..ac0d3c44700a 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddCustomInterpreter.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddCustomInterpreter.kt @@ -9,6 +9,9 @@ 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.platform.ide.progress.ModalTaskOwner +import com.intellij.platform.ide.progress.TaskCancellation +import com.intellij.platform.ide.progress.withModalProgress import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.bind import com.intellij.ui.dsl.builder.bindItem @@ -29,7 +32,11 @@ import com.jetbrains.python.sdk.add.v2.uv.EnvironmentCreatorUv import com.jetbrains.python.sdk.add.v2.uv.UvExistingEnvironmentSelector import com.jetbrains.python.sdk.add.v2.venv.EnvironmentCreatorVenv import com.jetbrains.python.sdk.add.v2.venv.PythonExistingEnvironmentSelector +import com.jetbrains.python.sdk.configuration.CreateSdkInfo +import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import org.jetbrains.annotations.ApiStatus.Internal class ValidationInfoError(val validationInfo: ValidationInfo) : MessageError(validationInfo.message) @@ -162,6 +169,32 @@ internal class PythonAddCustomInterpreter

( fun onShown(scope: CoroutineScope) { newInterpreterCreators.values.forEach { it.onShown(scope) } existingInterpreterSelectors.values.forEach { it.onShown(scope) } + + scope.launch(Dispatchers.Default) { + val createSdkInfoWithTool = withModalProgress(ModalTaskOwner.guess(), message("sdk.create.check.environments"), TaskCancellation.cancellable()) { + module?.let { PyProjectSdkConfigurationExtension.findAllSortedForModule(it) }?.firstOrNull() + } ?: return@launch + + val (manager, configurators) = when (createSdkInfoWithTool.createSdkInfo) { + is CreateSdkInfo.WillCreateEnv -> { + selectionMethod.set(PythonInterpreterSelectionMethod.CREATE_NEW) + newInterpreterManager to newInterpreterCreators + } + is CreateSdkInfo.ExistingEnv -> { + selectionMethod.set(PythonInterpreterSelectionMethod.SELECT_EXISTING) + existingInterpreterManager to existingInterpreterSelectors + } + } + + val tool = PythonSupportedEnvironmentManagers.entries.singleOrNull { it.toolId == createSdkInfoWithTool.toolId } ?: return@launch + if (tool in configurators) { + manager.set(tool) + } + else { + selectionMethod.set(PythonInterpreterSelectionMethod.CREATE_NEW) + newInterpreterManager.set(tool) + } + } } fun createStatisticsInfo(): InterpreterStatisticsInfo { diff --git a/python/src/com/jetbrains/python/sdk/add/v2/common.kt b/python/src/com/jetbrains/python/sdk/add/v2/common.kt index 05bdd27ca017..d4320a8017e8 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/common.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/common.kt @@ -25,13 +25,17 @@ import com.intellij.platform.ide.progress.ModalTaskOwner import com.intellij.platform.ide.progress.TaskCancellation import com.intellij.platform.ide.progress.withBackgroundProgress import com.intellij.platform.ide.progress.withModalProgress +import com.intellij.python.common.tools.ToolId import com.intellij.python.community.execService.Args import com.intellij.python.community.execService.BinaryToExec import com.intellij.python.community.execService.ExecService import com.intellij.python.community.execService.execGetStdout +import com.intellij.python.community.impl.poetry.common.POETRY_TOOL_ID import com.intellij.python.community.impl.poetry.common.icons.PythonCommunityImplPoetryCommonIcons +import com.intellij.python.community.impl.uv.common.UV_TOOL_ID import com.intellij.python.community.impl.uv.common.icons.PythonCommunityImplUVCommonIcons import com.intellij.python.hatch.icons.PythonHatchIcons +import com.intellij.python.hatch.impl.HATCH_TOOL_ID import com.intellij.ui.dsl.builder.Align import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.Row @@ -44,6 +48,9 @@ import com.jetbrains.python.parser.icons.PythonParserIcons import com.jetbrains.python.psi.LanguageLevel import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory import com.jetbrains.python.sdk.* +import com.jetbrains.python.sdk.configuration.CONDA_TOOL_ID +import com.jetbrains.python.sdk.configuration.PIPENV_TOOL_ID +import com.jetbrains.python.sdk.configuration.VENV_TOOL_ID import com.jetbrains.python.sdk.flavors.PyFlavorAndData import com.jetbrains.python.sdk.flavors.PyFlavorData import com.jetbrains.python.sdk.flavors.VirtualEnvSdkFlavor @@ -137,17 +144,18 @@ abstract class PythonExistingEnvironmentConfigurator

(model: Pyth enum class PythonSupportedEnvironmentManagers( + val toolId: ToolId, val nameKey: String, val icon: Icon, val isFSSupported: (FileSystem<*>) -> Boolean = { (it as? FileSystem.Eel)?.eelApi == localEel }, ) { - VIRTUALENV("sdk.create.custom.virtualenv", PythonIcons.Python.Virtualenv, { true }), - CONDA("sdk.create.custom.conda", PythonIcons.Python.Anaconda, { true }), - POETRY("sdk.create.custom.poetry", PythonCommunityImplPoetryCommonIcons.Poetry), - PIPENV("sdk.create.custom.pipenv", PIPENV_ICON), - UV("sdk.create.custom.uv", PythonCommunityImplUVCommonIcons.UV), - HATCH("sdk.create.custom.hatch", PythonHatchIcons.Logo, { it is FileSystem.Eel }), - PYTHON("sdk.create.custom.python", PythonParserIcons.PythonFile, { true }) + VIRTUALENV(VENV_TOOL_ID, "sdk.create.custom.virtualenv", PythonIcons.Python.Virtualenv, { true }), + CONDA(CONDA_TOOL_ID, "sdk.create.custom.conda", PythonIcons.Python.Anaconda, { true }), + POETRY(POETRY_TOOL_ID, "sdk.create.custom.poetry", PythonCommunityImplPoetryCommonIcons.Poetry), + PIPENV(PIPENV_TOOL_ID, "sdk.create.custom.pipenv", PIPENV_ICON), + UV(UV_TOOL_ID, "sdk.create.custom.uv", PythonCommunityImplUVCommonIcons.UV), + HATCH(HATCH_TOOL_ID, "sdk.create.custom.hatch", PythonHatchIcons.Logo, { it is FileSystem.Eel }), + PYTHON(VENV_TOOL_ID, "sdk.create.custom.python", PythonParserIcons.PythonFile, { true }) } enum class PythonInterpreterSelectionMode(val nameKey: String) { diff --git a/python/src/com/jetbrains/python/sdk/add/v2/conda/CondaExistingEnvironmentSelector.kt b/python/src/com/jetbrains/python/sdk/add/v2/conda/CondaExistingEnvironmentSelector.kt index 696ad1251dcb..bde31c2c3bbd 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/conda/CondaExistingEnvironmentSelector.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/conda/CondaExistingEnvironmentSelector.kt @@ -109,7 +109,9 @@ internal class CondaExistingEnvironmentSelector

(model: PythonAdd scope.launch(Dispatchers.EDT) { model.condaViewModel.condaEnvironmentsResult.collectLatest { environmentsResult -> envComboBox.removeAllItems() - environmentsResult?.successOrNull?.forEach(envComboBox::addItem) + val environments = environmentsResult?.successOrNull ?: return@collectLatest + environments.forEach(envComboBox::addItem) + model.condaViewModel.updateSelection(environments) } } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/conda/CondaViewModel.kt b/python/src/com/jetbrains/python/sdk/add/v2/conda/CondaViewModel.kt index 00e1ca6451be..7e8d511083ff 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/conda/CondaViewModel.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/conda/CondaViewModel.kt @@ -8,10 +8,16 @@ import com.intellij.openapi.diagnostic.fileLogger import com.intellij.openapi.diagnostic.rethrowControlFlowException import com.intellij.openapi.observable.properties.ObservableMutableProperty import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.util.io.toNioPathOrNull +import com.intellij.openapi.vfs.isFile +import com.intellij.openapi.vfs.refreshAndFindVirtualFileOrDirectory import com.intellij.platform.eel.provider.localEel import com.jetbrains.python.PyBundle.message import com.jetbrains.python.conda.loadLocalPythonCondaPath import com.jetbrains.python.errorProcessing.PyResult +import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathFlows +import com.jetbrains.python.packaging.conda.environmentYml.CondaEnvironmentYmlSdkUtils +import com.jetbrains.python.packaging.conda.environmentYml.format.CondaEnvironmentYmlParser import com.jetbrains.python.sdk.add.v2.* import com.jetbrains.python.sdk.conda.TargetEnvironmentRequestCommandExecutor import com.jetbrains.python.sdk.conda.suggestCondaPath @@ -20,14 +26,17 @@ import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.nio.file.Path private val LOG: Logger = fileLogger() class CondaViewModel

( val fileSystem: FileSystem

, propertyGraph: PropertyGraph, + val projectPathFlows: ProjectPathFlows, ) : PythonToolViewModel { val condaExecutable: ObservableMutableProperty?> = propertyGraph.property(null) val condaEnvironmentsResult: MutableStateFlow>?> = MutableStateFlow(null) @@ -94,9 +103,36 @@ class CondaViewModel

( } } - /** - * Returns error or `null` if no error - */ + suspend fun updateSelection(environments: List): Unit = + selectedCondaEnv.set(getBestCondaEnv(environments) ?: environments.firstOrNull()) + + private suspend fun getBestCondaEnv(environments: List): PyCondaEnv? = withContext(Dispatchers.IO) { + val projectPath = projectPathFlows.projectPathWithDefault.first() + val environmentYml = if (fileSystem.isLocal) getEnvironmentYml(projectPath) else null + val envName = environmentYml?.let { CondaEnvironmentYmlParser.readNameFromFile(it) } + val envPrefix = environmentYml?.let { CondaEnvironmentYmlParser.readPrefixFromFile(it) } + val shouldGuessEnvPrefix = envName == null && envPrefix == null + environments.firstOrNull { + val envIdentity = it.envIdentity + when (envIdentity) { + is PyCondaEnvIdentity.NamedEnv -> envIdentity.envName == envName + is PyCondaEnvIdentity.UnnamedEnv -> if (shouldGuessEnvPrefix) { + val envPath = envIdentity.envPath.toNioPathOrNull() + !envIdentity.isBase && projectPath == envPath?.parent + } + else envIdentity.envPath == envPrefix + } + } + } + + private fun getEnvironmentYml(projectPath: Path) = listOf( + CondaEnvironmentYmlSdkUtils.ENV_YAML_FILE_NAME, + CondaEnvironmentYmlSdkUtils.ENV_YML_FILE_NAME, + ).firstNotNullOfOrNull { + val path = projectPath.resolve(it) + path.refreshAndFindVirtualFileOrDirectory()?.takeIf { virtualFile -> virtualFile.isFile } + } + private suspend fun updateCondaEnvironments(): PyResult> = withContext(Dispatchers.IO) { val executable = condaExecutable.get() if (executable == null) return@withContext PyResult.localizedError(message("python.sdk.conda.no.exec")) @@ -108,8 +144,8 @@ class CondaViewModel

( withContext(Dispatchers.UI) { baseCondaEnv.set(baseConda) - selectedCondaEnv.set(environments.firstOrNull()) } + return@withContext PyResult.success(environments) } } 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 a865e970ace5..a238409035b3 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/models.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/models.kt @@ -55,7 +55,7 @@ abstract class PythonAddInterpreterModel

( val navigator: PythonNewEnvironmentDialogNavigator = PythonNewEnvironmentDialogNavigator() open val state: AddInterpreterState

= AddInterpreterState(propertyGraph) - val condaViewModel: CondaViewModel

= CondaViewModel(fileSystem, propertyGraph) + val condaViewModel: CondaViewModel

= CondaViewModel(fileSystem, propertyGraph, projectPathFlows) val uvViewModel: UvViewModel

= UvViewModel(fileSystem, propertyGraph) val pipenvViewModel: PipenvViewModel

= PipenvViewModel(fileSystem, propertyGraph) val poetryViewModel: PoetryViewModel

= PoetryViewModel(fileSystem, propertyGraph)