PY-78033/Sort-Base-interpreter-list

After this refactoring we how have `comparators.kt` with sorting logic that is used both by `SystemPython` and `PythonSelectableInterpreter` (ViewModel thing for V2).

Tests are also added.

GitOrigin-RevId: 9d04405829f31874d15cb893d9261c7997cb2dd5
This commit is contained in:
Ilya.Kazakevich
2025-06-09 18:20:09 +02:00
committed by intellij-monorepo-bot
parent 02118dde3c
commit 93d8518b8d
18 changed files with 172 additions and 34 deletions

View File

@@ -97,6 +97,7 @@ jvm_library(
"//python/services/system-python:system-python_test_lib",
"//platform/eel",
"//python/services/shared",
"//python/services/shared:shared_test_lib",
"//python/poetry",
"//python/python-venv:community-impl-venv",
"//python/python-venv:community-impl-venv_test_lib",

View File

@@ -35,6 +35,7 @@ jvm_library(
"@lib//:kotlin-stdlib",
"//python/python-parser:parser",
"//python/services/shared",
"//python/services/shared:shared_test_lib",
"//platform/eel-provider",
"@lib//:jetbrains-annotations",
"//python/openapi:community",

View File

@@ -7,6 +7,8 @@ import com.intellij.platform.eel.provider.getEelDescriptor
import com.intellij.python.community.execService.python.validatePythonAndGetVersion
import com.intellij.python.community.services.internal.impl.VanillaPythonWithLanguageLevelImpl.Companion.concurrentLimit
import com.intellij.python.community.services.internal.impl.VanillaPythonWithLanguageLevelImpl.Companion.createByPythonBinary
import com.intellij.python.community.services.shared.LanguageLevelComparator
import com.intellij.python.community.services.shared.PythonWithLanguageLevel
import com.intellij.python.community.services.shared.VanillaPythonWithLanguageLevel
import com.jetbrains.python.PythonBinary
import com.jetbrains.python.Result
@@ -26,7 +28,7 @@ import kotlin.io.path.relativeTo
class VanillaPythonWithLanguageLevelImpl internal constructor(
override val pythonBinary: PythonBinary,
override val languageLevel: LanguageLevel,
) : VanillaPythonWithLanguageLevel {
) : VanillaPythonWithLanguageLevel, Comparable<PythonWithLanguageLevel> {
companion object {
@@ -82,4 +84,5 @@ class VanillaPythonWithLanguageLevelImpl internal constructor(
return "$pythonString ($languageLevel)"
}
override fun compareTo(other: PythonWithLanguageLevel): Int = LanguageLevelComparator.compare(this, other)
}

View File

@@ -1,5 +1,5 @@
### auto-generated section `build intellij.python.community.services.shared` start
load("@rules_jvm//:jvm.bzl", "jvm_library", "jvm_resources")
load("@rules_jvm//:jvm.bzl", "jvm_library", "jvm_resources", "jvm_test")
jvm_resources(
name = "shared_resources",
@@ -21,4 +21,31 @@ jvm_library(
],
runtime_deps = [":shared_resources"]
)
jvm_library(
name = "shared_test_lib",
visibility = ["//visibility:public"],
srcs = glob(["tests/**/*.kt", "tests/**/*.java"], allow_empty = True),
associates = [":shared"],
deps = [
"@lib//:kotlin-stdlib",
"//python/openapi:community",
"//python/openapi:community_test_lib",
"@lib//:jetbrains-annotations",
"//platform/eel-provider",
"@lib//:junit5",
"@lib//:kotlinx-coroutines-core",
"//platform/testFramework/junit5",
"//platform/testFramework/junit5:junit5_test_lib",
"@lib//:hamcrest",
"//python/python-exec-service/execService.python",
"//python/python-exec-service/execService.python:execService.python_test_lib",
],
runtime_deps = [":shared_resources"]
)
jvm_test(
name = "shared_test",
runtime_deps = [":shared_test_lib"]
)
### auto-generated section `build intellij.python.community.services.shared` end

View File

@@ -5,6 +5,7 @@
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />

View File

@@ -0,0 +1,11 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.community.services.shared
import com.jetbrains.python.psi.LanguageLevel
/**
* Something with language level
*/
interface LanguageLevelHolder {
val languageLevel: LanguageLevel
}

View File

@@ -2,21 +2,15 @@
package com.intellij.python.community.services.shared
import com.intellij.python.community.execService.python.advancedApi.ExecutablePython
import com.jetbrains.python.psi.LanguageLevel
/**
* Python (vanilla, conda, whatever) with known language level.
*/
interface PythonWithLanguageLevel : Comparable<PythonWithLanguageLevel>, PythonWithName {
val languageLevel: LanguageLevel
interface PythonWithLanguageLevel : PythonWithName, LanguageLevelHolder {
/**
* Convert python to something that can be executed on [java.util.concurrent.ExecutorService]
*/
val asExecutablePython: ExecutablePython
// Backward: first python is the highest
override fun compareTo(other: PythonWithLanguageLevel): Int = languageLevel.compareTo(other.languageLevel) * -1
}

View File

@@ -1,6 +1,7 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.community.services.shared
interface PythonWithUi : PythonWithLanguageLevel {
val ui: UICustomization?
}
/**
* Python that has both [languageLevel] and [ui]
*/
interface PythonWithUi : PythonWithLanguageLevel, UiHolder

View File

@@ -10,4 +10,6 @@ data class UICustomization(
*/
val title: @Nls String,
val icon: Icon? = null,
)
) : Comparable<UICustomization> {
override fun compareTo(other: UICustomization): Int = title.compareTo(other.title)
}

View File

@@ -0,0 +1,9 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.community.services.shared
interface UiHolder {
/**
* UI hints on how to display this python to the end user: icon, title, etc
*/
val ui: UICustomization?
}

View File

@@ -0,0 +1,21 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.community.services.shared
import java.util.*
object LanguageLevelComparator : Comparator<LanguageLevelHolder> {
override fun compare(o1: LanguageLevelHolder, o2: LanguageLevelHolder): Int =
// Backward: first python is the highest
o1.languageLevel.compareTo(o2.languageLevel) * -1
}
object UiComparator : Comparator<UiHolder> {
override fun compare(o1: UiHolder, o2: UiHolder): Int =
Objects.compare(o1.ui, o2.ui, Comparator.nullsFirst(UICustomization::compareTo))
}
class LanguageLevelWithUiComparator<T> : Comparator<T> where T : LanguageLevelHolder, T : UiHolder {
override fun compare(o1: T, o2: T): Int =
LanguageLevelComparator.compare(o1, o2) * 10 + UiComparator.compare(o1, o2)
}

View File

@@ -0,0 +1,8 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
/**
* Various details for pythons, not intended to be used directly (except for comparators).
*/
@ApiStatus.Internal
package com.intellij.python.community.services.shared;
import org.jetbrains.annotations.ApiStatus;

View File

@@ -0,0 +1,44 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.junit5Tests.unit.comparators
import com.intellij.python.community.services.shared.LanguageLevelHolder
import com.intellij.python.community.services.shared.LanguageLevelWithUiComparator
import com.intellij.python.community.services.shared.UICustomization
import com.intellij.python.community.services.shared.UiHolder
import com.jetbrains.python.psi.LanguageLevel
import org.hamcrest.MatcherAssert
import org.hamcrest.Matchers
import org.junit.jupiter.api.Test
import java.util.*
class ComparatorsTest {
@Test
fun testComparators() {
val mocks = arrayOf(
MockLevel(LanguageLevel.PYTHON314),
MockLevel(LanguageLevel.PYTHON310),
MockLevel(LanguageLevel.PYTHON310, ui = UICustomization("A")),
MockLevel(LanguageLevel.PYTHON310, ui = UICustomization("Z")),
MockLevel(LanguageLevel.PYTHON310, ui = UICustomization("B")),
MockLevel(LanguageLevel.PYTHON27),
MockLevel(LanguageLevel.PYTHON313),
)
val set = TreeSet(LanguageLevelWithUiComparator<MockLevel>())
set.addAll(mocks)
MatcherAssert.assertThat("", set, Matchers.contains(
MockLevel(LanguageLevel.PYTHON314),
MockLevel(LanguageLevel.PYTHON313),
MockLevel(LanguageLevel.PYTHON310),
MockLevel(LanguageLevel.PYTHON310, ui = UICustomization("A")),
MockLevel(LanguageLevel.PYTHON310, ui = UICustomization("B")),
MockLevel(LanguageLevel.PYTHON310, ui = UICustomization("Z")),
MockLevel(LanguageLevel.PYTHON27)
))
}
}
private data class MockLevel(
override val languageLevel: LanguageLevel,
override val ui: UICustomization? = null,
) : LanguageLevelHolder, UiHolder

View File

@@ -61,6 +61,7 @@ jvm_library(
"//python/testFramework",
"@lib//:hamcrest",
"//python/services/shared",
"//python/services/shared:shared_test_lib",
"//python/services/internal-impl:python-community-services-internal-impl",
"//python/services/internal-impl:python-community-services-internal-impl_test_lib",
"//platform/util",

View File

@@ -6,6 +6,7 @@ import com.intellij.openapi.components.service
import com.intellij.platform.eel.EelApi
import com.intellij.platform.eel.provider.localEel
import com.intellij.python.community.impl.venv.createVenv
import com.intellij.python.community.services.shared.LanguageLevelWithUiComparator
import com.intellij.python.community.services.shared.PythonWithUi
import com.intellij.python.community.services.shared.UICustomization
import com.intellij.python.community.services.shared.VanillaPythonWithLanguageLevel
@@ -55,7 +56,11 @@ fun SystemPythonService(): SystemPythonService = ApplicationManager.getApplicati
*
* Instances could be obtained with [SystemPythonService]
*/
class SystemPython internal constructor(private val impl: VanillaPythonWithLanguageLevel, override val ui: UICustomization?) : VanillaPythonWithLanguageLevel by impl, PythonWithUi {
class SystemPython internal constructor(private val delegate: VanillaPythonWithLanguageLevel, override val ui: UICustomization?) : VanillaPythonWithLanguageLevel by delegate, PythonWithUi, Comparable<SystemPython> {
private companion object {
val comparator = LanguageLevelWithUiComparator<SystemPython>()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
@@ -63,21 +68,23 @@ class SystemPython internal constructor(private val impl: VanillaPythonWithLangu
other as SystemPython
if (impl != other.impl) return false
if (delegate != other.delegate) return false
if (ui != other.ui) return false
return true
}
override fun hashCode(): Int {
var result = impl.hashCode()
var result = delegate.hashCode()
result = 31 * result + (ui?.hashCode() ?: 0)
return result
}
override fun toString(): String {
return "SystemPython(impl=$impl, ui=$ui)"
return "SystemPython(delegate=$delegate, ui=$ui)"
}
override fun compareTo(other: SystemPython): Int = comparator.compare(this, other)
}
/**

View File

@@ -84,7 +84,7 @@ internal class SystemPythonServiceImpl(scope: CoroutineScope) : SystemPythonServ
else {
cache.get(eelApi.descriptor)
}.sorted()
} ?: searchPythonsPhysicallyNoCache(eelApi)
} ?: searchPythonsPhysicallyNoCache(eelApi).sorted()
class MyServiceState : BaseState() {

View File

@@ -15,8 +15,7 @@ import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.io.NioFiles
import com.intellij.openapi.vfs.toNioPathOrNull
import com.intellij.python.community.services.internal.impl.VanillaPythonWithLanguageLevelImpl
import com.intellij.python.community.services.shared.UICustomization
import com.intellij.python.community.services.shared.VanillaPythonWithLanguageLevel
import com.intellij.python.community.services.shared.*
import com.intellij.python.community.services.systemPython.SystemPython
import com.intellij.python.community.services.systemPython.SystemPythonService
import com.intellij.python.hatch.HatchConfiguration
@@ -48,7 +47,8 @@ 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 org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.TestOnly
import java.nio.file.InvalidPathException
import java.nio.file.Path
import kotlin.io.path.Path
@@ -83,6 +83,12 @@ abstract class PythonAddInterpreterModel(
val interpreterLoading: MutableStateFlow<Boolean> = MutableStateFlow(true)
val condaEnvironmentsLoading: MutableStateFlow<Boolean> = MutableStateFlow(true)
@TestOnly
@ApiStatus.Internal
fun addDetected(detected: DetectedSelectableInterpreter) {
_detectedInterpreters.value += detected
}
// If the project is provided, sdks associated with it will be kept in the list of interpreters. If not, then they will be filtered out.
open fun initialize(scope: CoroutineScope) {
merge(
@@ -111,7 +117,7 @@ abstract class PythonAddInterpreterModel(
manuallyAddedInterpreters,
) { known, detected, added ->
added + known + detected
}.stateIn(scope, started = SharingStarted.Eagerly, initialValue = emptyList())
}.map { it.sorted() }.stateIn(scope, started = SharingStarted.Eagerly, initialValue = emptyList())
this.baseInterpreters = allInterpreters.map { all ->
all.filter { it.isBasePython() }
@@ -365,7 +371,11 @@ class PythonLocalAddInterpreterModel(projectPathFlows: ProjectPathFlows) : Pytho
}
sealed class PythonSelectableInterpreter : Comparable<PythonSelectableInterpreter> {
sealed class PythonSelectableInterpreter : Comparable<PythonSelectableInterpreter>, UiHolder, LanguageLevelHolder {
companion object {
private val comparator = LanguageLevelWithUiComparator<PythonSelectableInterpreter>()
}
/**
* 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]
@@ -375,15 +385,12 @@ sealed class PythonSelectableInterpreter : Comparable<PythonSelectableInterprete
}
abstract val homePath: String
abstract val languageLevel: LanguageLevel
open val uiCustomization: UICustomization? = null
abstract override val languageLevel: LanguageLevel
override val ui: 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()
comparator.compare(this, other)
}
class ExistingSelectableInterpreter(
@@ -411,11 +418,11 @@ class DetectedSelectableInterpreter(
override val homePath: String,
override val languageLevel: LanguageLevel,
private val isBase: Boolean,
override val uiCustomization: UICustomization? = null,
override val ui: UICustomization? = null,
) : PythonSelectableInterpreter() {
override suspend fun isBasePython(): Boolean = isBase
override fun toString(): String {
return "DetectedSelectableInterpreter(homePath='$homePath', languageLevel=$languageLevel, isBase=$isBase, uiCustomization=$uiCustomization)"
return "DetectedSelectableInterpreter(homePath='$homePath', languageLevel=$languageLevel, isBase=$isBase, uiCustomization=$ui)"
}
}
@@ -436,7 +443,7 @@ class ManuallyAddedSelectableInterpreter(
class InstallableSelectableInterpreter(val sdk: PySdkToInstall) : PythonSelectableInterpreter() {
override suspend fun isBasePython(): Boolean = true
override val homePath: String = ""
override val languageLevel = PySdkUtil.getLanguageLevelForSdk(sdk)
override val languageLevel: LanguageLevel = PySdkUtil.getLanguageLevelForSdk(sdk)
}

View File

@@ -159,8 +159,8 @@ internal fun SimpleColoredComponent.customizeForPythonInterpreter(isLoading: Boo
when (interpreter) {
is DetectedSelectableInterpreter, is ManuallyAddedSelectableInterpreter -> {
icon = IconLoader.getTransparentIcon(interpreter.uiCustomization?.icon ?: PythonPsiApiIcons.Python)
val title = interpreter.uiCustomization?.title ?: message("sdk.rendering.detected.grey.text")
icon = IconLoader.getTransparentIcon(interpreter.ui?.icon ?: PythonPsiApiIcons.Python)
val title = interpreter.ui?.title ?: message("sdk.rendering.detected.grey.text")
append(String.format("Python %-4s", interpreter.languageLevel))
append(" (" + replaceHomePathToTilde(interpreter.homePath) + ") $title", SimpleTextAttributes.GRAYED_SMALL_ATTRIBUTES)
}