diff --git a/python/intellij.python.community.impl.iml b/python/intellij.python.community.impl.iml index 1405e070e978..60b5a0d59d65 100644 --- a/python/intellij.python.community.impl.iml +++ b/python/intellij.python.community.impl.iml @@ -148,5 +148,6 @@ + \ No newline at end of file diff --git a/python/python-venv/src/com/intellij/python/community/impl/venv/venv.kt b/python/python-venv/src/com/intellij/python/community/impl/venv/venv.kt index 4be91d02c279..2507d6a1507d 100644 --- a/python/python-venv/src/com/intellij/python/community/impl/venv/venv.kt +++ b/python/python-venv/src/com/intellij/python/community/impl/venv/venv.kt @@ -20,7 +20,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.jetbrains.annotations.ApiStatus.Internal import org.jetbrains.annotations.CheckReturnValue -import java.nio.file.Path import kotlin.io.path.pathString import kotlin.time.Duration.Companion.minutes @@ -37,7 +36,7 @@ suspend fun createVenv( venvDir: Directory, inheritSitePackages: Boolean = false, envReader: VirtualEnvReader = VirtualEnvReader.Instance, -): Result { +): Result { val execService = ExecService() val args = buildList { if (inheritSitePackages) { diff --git a/python/src/com/jetbrains/python/projectCreation/venvWithSdkCreator.kt b/python/src/com/jetbrains/python/projectCreation/venvWithSdkCreator.kt index 4a15d828c1a5..7ebd6b6b8925 100644 --- a/python/src/com/jetbrains/python/projectCreation/venvWithSdkCreator.kt +++ b/python/src/com/jetbrains/python/projectCreation/venvWithSdkCreator.kt @@ -21,7 +21,6 @@ import com.jetbrains.python.errorProcessing.failure import com.jetbrains.python.sdk.configurePythonSdk import com.jetbrains.python.sdk.createSdk import com.jetbrains.python.sdk.flavors.PythonSdkFlavor -import com.jetbrains.python.sdk.pythonSdk import com.jetbrains.python.sdk.setAssociationToModuleAsync import com.jetbrains.python.venvReader.VirtualEnvReader import kotlinx.coroutines.Dispatchers @@ -87,7 +86,6 @@ suspend fun createVenvAndSdk( } configurePythonSdk(project, module, sdk) sdk.setAssociationToModuleAsync(module) - module.pythonSdk return Result.success(sdk) } diff --git a/python/src/com/jetbrains/python/sdk/ModuleOrProject.kt b/python/src/com/jetbrains/python/sdk/ModuleOrProject.kt index c91fbe92d2db..374e0cccbdbe 100644 --- a/python/src/com/jetbrains/python/sdk/ModuleOrProject.kt +++ b/python/src/com/jetbrains/python/sdk/ModuleOrProject.kt @@ -1,4 +1,4 @@ -// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.jetbrains.python.sdk import com.intellij.openapi.module.Module @@ -35,6 +35,12 @@ sealed class ModuleOrProject(val project: Project) { class ModuleAndProject(val module: Module) : ModuleOrProject(module.project) } +val ModuleOrProject.moduleIfExists: Module? + get() = when (this) { + is ModuleOrProject.ModuleAndProject -> module + is ModuleOrProject.ProjectOnly -> null + } + val ModuleOrProject.destructured: Pair get() = when (this) { is ModuleOrProject.ProjectOnly -> project to null diff --git a/python/src/com/jetbrains/python/sdk/add/v2/CustomExistingEnvironmentSelector.kt b/python/src/com/jetbrains/python/sdk/add/v2/CustomExistingEnvironmentSelector.kt index 9a4111577e56..37e983506f2e 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/CustomExistingEnvironmentSelector.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/CustomExistingEnvironmentSelector.kt @@ -9,12 +9,14 @@ import com.intellij.python.community.services.shared.PythonWithLanguageLevel import com.intellij.ui.dsl.builder.Align import com.intellij.ui.dsl.builder.Panel import com.jetbrains.python.PyBundle.message +import com.jetbrains.python.errorProcessing.ErrorSink import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo import com.jetbrains.python.sdk.ModuleOrProject +import com.jetbrains.python.sdk.moduleIfExists import com.jetbrains.python.statistics.InterpreterCreationMode import com.jetbrains.python.statistics.InterpreterType -import com.jetbrains.python.errorProcessing.ErrorSink import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.jetbrains.annotations.ApiStatus.Internal import java.nio.file.Path @@ -64,7 +66,7 @@ internal abstract class CustomExistingEnvironmentSelector(private val name: Stri } override fun onShown() { - comboBox.setItems(existingEnvironments) + comboBox.setItems(existingEnvironments.map { sortForExistingEnvironment(it, moduleOrProject.moduleIfExists) }) } override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo { diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonExistingEnvironmentSelector.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonExistingEnvironmentSelector.kt index 3f1a62519921..8ceca4cbc1e9 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PythonExistingEnvironmentSelector.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonExistingEnvironmentSelector.kt @@ -11,8 +11,10 @@ import com.jetbrains.python.errorProcessing.ErrorSink import com.jetbrains.python.errorProcessing.PyError import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo import com.jetbrains.python.sdk.ModuleOrProject +import com.jetbrains.python.sdk.moduleIfExists import com.jetbrains.python.statistics.InterpreterCreationMode import com.jetbrains.python.statistics.InterpreterType +import kotlinx.coroutines.flow.map class PythonExistingEnvironmentSelector(model: PythonAddInterpreterModel, private val moduleOrProject: ModuleOrProject?) : PythonExistingEnvironmentConfigurator(model) { @@ -32,7 +34,7 @@ class PythonExistingEnvironmentSelector(model: PythonAddInterpreterModel, privat } override fun onShown() { - comboBox.setItems(model.allInterpreters) + comboBox.setItems(model.allInterpreters.map { sortForExistingEnvironment(it, moduleOrProject?.moduleIfExists) }) } override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Result { 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 cd79871b744a..25787839bcfb 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/models.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/models.kt @@ -45,6 +45,7 @@ import com.jetbrains.python.sdk.uv.impl.getUvExecutable import com.jetbrains.python.venvReader.tryResolvePath import kotlinx.coroutines.* import kotlinx.coroutines.flow.* +import org.apache.commons.lang3.builder.CompareToBuilder import java.nio.file.InvalidPathException import java.nio.file.Path import kotlin.io.path.Path @@ -355,7 +356,7 @@ class PythonLocalAddInterpreterModel(params: PyInterpreterModelParams) : PythonM } -sealed class PythonSelectableInterpreter { +sealed class PythonSelectableInterpreter : Comparable { /** * Base python is some system python (not venv) which can be used as a base for venv. * In terms of flavors we call it __not__ [PythonSdkFlavor.isPlatformIndependent] @@ -368,6 +369,12 @@ sealed class PythonSelectableInterpreter { abstract val languageLevel: LanguageLevel open val uiCustomization: UICustomization? = null override fun toString(): String = "PythonSelectableInterpreter(homePath='$homePath')" + + override fun compareTo(other: PythonSelectableInterpreter): Int = + CompareToBuilder() + .append(uiCustomization?.hashCode(), other.uiCustomization?.hashCode()) + .append(other.languageLevel, languageLevel) + .toComparison() } class ExistingSelectableInterpreter( @@ -379,7 +386,13 @@ class ExistingSelectableInterpreter( !sdk.sdkFlavor.isPlatformIndependent } + override fun toString(): String { + return "ExistingSelectableInterpreter(sdk=$sdk, languageLevel=$languageLevel, isSystemWide=$isSystemWide, homePath='$homePath')" + } + override val homePath = sdk.homePath!! // todo is it safe + + } /** @@ -390,14 +403,12 @@ class DetectedSelectableInterpreter( override val languageLevel: LanguageLevel, private val isBase: Boolean, override val uiCustomization: UICustomization? = null, -) : PythonSelectableInterpreter(), Comparable { +) : PythonSelectableInterpreter() { override suspend fun isBasePython(): Boolean = isBase - - - override fun compareTo(other: DetectedSelectableInterpreter): Int { // First by type - val byType = (uiCustomization?.title ?: "").compareTo(other.uiCustomization?.title ?: "") // Then from the highest python to the lowest - return if (byType != 0) byType else (languageLevel.compareTo(other.languageLevel) * -1) + override fun toString(): String { + return "DetectedSelectableInterpreter(homePath='$homePath', languageLevel=$languageLevel, isBase=$isBase, uiCustomization=$uiCustomization)" } + } class ManuallyAddedSelectableInterpreter( @@ -405,6 +416,12 @@ class ManuallyAddedSelectableInterpreter( override val languageLevel: LanguageLevel, ) : PythonSelectableInterpreter() { constructor(python: PythonWithLanguageLevel) : this(python.pythonBinary.pathString, python.languageLevel) + + override fun toString(): String { + return "ManuallyAddedSelectableInterpreter(homePath='$homePath', languageLevel=$languageLevel)" + } + + } class InstallableSelectableInterpreter(val sdk: PySdkToInstall) : PythonSelectableInterpreter() { diff --git a/python/src/com/jetbrains/python/sdk/add/v2/sortForExistingEnvironment.kt b/python/src/com/jetbrains/python/sdk/add/v2/sortForExistingEnvironment.kt new file mode 100644 index 000000000000..526695ea37e6 --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/add/v2/sortForExistingEnvironment.kt @@ -0,0 +1,98 @@ +// Copyright 2000-2025 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.diagnostic.fileLogger +import com.intellij.openapi.module.Module +import com.jetbrains.python.sdk.associatedModulePath +import com.jetbrains.python.sdk.getOrCreateAdditionalData +import com.jetbrains.python.sdk.isAssociatedWithAnotherModule +import com.jetbrains.python.sdk.isAssociatedWithModule +import com.jetbrains.python.venvReader.VirtualEnvReader +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.jetbrains.annotations.ApiStatus +import java.nio.file.Path + + +private val LOG = fileLogger() + +/** + * Python order, see PY-78035 + */ +private enum class Group { + /** + * SDK venvs, associated with a current module + */ + ASSOC_WITH_PROJ_ROOT, + + /** + * SDK venvs no associated with any project + */ + SHARED_VENVS, + + /** + * Venvs in a user home + */ + VENVS_IN_USER_HOME, + + /** + * System (both vanilla and from providers) + */ + OTHER, + + /** + * Those, which should be filtered out + */ + REDUNDANT +} + +/** + * Sorts and filters [pythons] to show them in "existing pythons" combobox in v2. + * If [module] is set, v2 is displayed for the certain module. + */ +@ApiStatus.Internal +suspend fun sortForExistingEnvironment( + pythons: Collection, + module: Module?, + venvReader: VirtualEnvReader = VirtualEnvReader.Companion.Instance, +): List { + val venvRoot = withContext(Dispatchers.IO) { + venvReader.getVEnvRootDir() + } + return withContext(Dispatchers.Default) { + val groupedPythons = pythons.groupBy { + when (it) { + is InstallableSelectableInterpreter -> error("$it is unexpected") + is DetectedSelectableInterpreter, is ManuallyAddedSelectableInterpreter -> Unit // Those are pythons, and not SDKs + is ExistingSelectableInterpreter -> { //SDKs + if (module != null) { + if (it.sdk.isAssociatedWithModule(module)) { + return@groupBy Group.ASSOC_WITH_PROJ_ROOT + } + else if (it.sdk.isAssociatedWithAnotherModule(module)) { + return@groupBy Group.REDUNDANT // Foreign SDK + } + } + else if (it.sdk.getOrCreateAdditionalData().associatedModulePath != null) { + // module == null, associated path != null: associated sdk can't be used without a module + return@groupBy Group.REDUNDANT + } + if (it.sdk.associatedModulePath == null) { // Shared SDK + return@groupBy Group.SHARED_VENVS + } + } + } + return@groupBy if (Path.of(it.homePath).startsWith(venvRoot)) Group.VENVS_IN_USER_HOME else Group.OTHER + } + if (LOG.isDebugEnabled) { + LOG.debug(groupedPythons.map { (group, pythons) -> + "$group: (${pythons.joinToString()})" + }.joinToString("\n")) + } + + groupedPythons.filterKeys { it != Group.REDUNDANT }.toSortedMap().flatMap { (_, pythons) -> + pythons.sorted() + }.distinctBy { it.homePath } + } +} +