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 }
+ }
+}
+