[python]: PY-85585 : Do not display vens as system pythons in "Add new interpreter" window.

Instead of old `addManuallyAddedInterpreter` we now have two functions: one that requires system python and one that doesn't.

Both functions register system python if provided, but the latter one accepts any python (venv included).

Various "selectors" use these functions.

We also make sure no non-system python is set to `baseInterpreters`: base are always system!

As a bonus, we show "system" or "virtual env" title near interpreter.

It now checks that python is system (see `ensureSystemPython`).

Non-system pythons are never reported, and `registerSystemPython` also returns an error for non-system pythons

We need `execGetBoolFromStdout` for the further changes

Merge-request: IJ-MR-182415
Merged-by: Ilya Kazakevich <ilya.kazakevich@jetbrains.com>

(cherry picked from commit 2950f5f0cd2745c12987a92e40774d366568f312)

GitOrigin-RevId: f30e9a6cd7b5d103454d66f26a0c2282d7c587fc
This commit is contained in:
Ilya.Kazakevich
2025-11-26 13:52:46 +00:00
committed by intellij-monorepo-bot
parent ed0fda649a
commit 3388536d2c
25 changed files with 267 additions and 105 deletions
@@ -35,6 +35,7 @@ jvm_library(
"//python/installer",
"//python/python-venv:community-impl-venv",
"//python/python-psi-impl:psi-impl",
"//python/python-exec-service/execService.python",
],
exports = ["//python/services/shared"]
)
@@ -75,6 +76,8 @@ jvm_library(
"//platform/testFramework",
"//platform/testFramework:testFramework_test_lib",
"//python/python-psi-impl:psi-impl",
"//python/python-exec-service/execService.python",
"//python/python-exec-service/execService.python:execService.python_test_lib",
],
exports = [
"//python/services/shared",
@@ -33,5 +33,6 @@
<orderEntry type="module" module-name="intellij.python.community.testFramework.testEnv" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.testFramework" scope="TEST" />
<orderEntry type="module" module-name="intellij.python.psi.impl" />
<orderEntry type="module" module-name="intellij.python.community.execService.python" />
</component>
</module>
@@ -1 +1,2 @@
py.system.python.service.python.is.broken=Python {0} is broken
py.system.python.service.python.is.broken=Python {0} is broken
py.system.python.service.python.is.not.system={0} is not a system python
@@ -9,10 +9,16 @@ import com.intellij.python.community.impl.venv.createVenv
import com.intellij.python.community.services.shared.PythonInfoWithUiComparator
import com.intellij.python.community.services.shared.PythonWithUi
import com.intellij.python.community.services.shared.VanillaPythonWithPythonInfo
import com.intellij.python.community.services.systemPython.impl.PySystemPythonBundle
import com.intellij.python.community.services.systemPython.impl.asSysPythonRegisterError
import com.intellij.python.community.services.systemPython.impl.ensureSystemPython
import com.jetbrains.python.PyToolUIInfo
import com.jetbrains.python.PythonBinary
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.MessageError
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.mapError
import com.jetbrains.python.venvReader.Directory
import com.jetbrains.python.venvReader.VirtualEnvReader
import org.jetbrains.annotations.ApiStatus
@@ -32,9 +38,9 @@ interface SystemPythonService {
/**
* When user provides a path to the python binary, use this method to the [SystemPython].
* @return either [SystemPython] or an error if python is broken.
* @return either [SystemPython] or an error if python is broken or not a system python.
*/
suspend fun registerSystemPython(pythonPath: PythonBinary): PyResult<SystemPython>
suspend fun registerSystemPython(pythonPath: PythonBinary): Result<SystemPython, SysPythonRegisterError>
/**
* @return tool to install python on OS If [eelApi] supports python installation
@@ -42,6 +48,35 @@ interface SystemPythonService {
fun getInstaller(eelApi: EelApi = localEel): PythonInstallerService?
}
/**
* System python has an error.
* It is either [NotASystemPython] (think: virtual env) or [PythonIsBroken] (and completely unusable)
*/
sealed interface SysPythonRegisterError {
val asPyError: PyError
/**
* Virtual env, not a system python
*/
class NotASystemPython private constructor(val notSystemPython: VanillaPythonWithPythonInfo, override val asPyError: PyError) : SysPythonRegisterError {
companion object : suspend (VanillaPythonWithPythonInfo) -> NotASystemPython {
override suspend fun invoke(notSystemPython: VanillaPythonWithPythonInfo): NotASystemPython = NotASystemPython(
notSystemPython = notSystemPython,
asPyError = MessageError(PySystemPythonBundle.message("py.system.python.service.python.is.not.system", notSystemPython.getReadableName()))
)
}
override fun toString(): String = "NotASystemPython(notSystemPython=$notSystemPython, asPyError=$asPyError)"
}
/**
* Python failed during execution
*/
data class PythonIsBroken(override val asPyError: PyError) : SysPythonRegisterError
}
/**
* Creates an instance of this service
*/
@@ -56,10 +91,19 @@ fun SystemPythonService(): SystemPythonService = ApplicationManager.getApplicati
*
* Instances could be obtained with [SystemPythonService]
*/
class SystemPython internal constructor(private val delegate: VanillaPythonWithPythonInfo, override val ui: PyToolUIInfo?) : VanillaPythonWithPythonInfo by delegate, PythonWithUi, Comparable<SystemPython> {
class SystemPython private constructor(private val delegate: VanillaPythonWithPythonInfo, override val ui: PyToolUIInfo?) : VanillaPythonWithPythonInfo by delegate, PythonWithUi, Comparable<SystemPython> {
private companion object {
internal companion object {
val comparator = PythonInfoWithUiComparator<SystemPython>()
internal suspend fun create(delegate: VanillaPythonWithPythonInfo, ui: PyToolUIInfo?): Result<SystemPython, SysPythonRegisterError> {
val isSystemPython = ensureSystemPython(delegate).mapError { it.asSysPythonRegisterError() }.getOr { return it }
return if (isSystemPython) {
Result.success(SystemPython(delegate, ui))
}
else {
Result.failure(SysPythonRegisterError.NotASystemPython(delegate))
}
}
}
override fun equals(other: Any?): Boolean {
@@ -0,0 +1,24 @@
// 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.systemPython.impl
import com.intellij.python.community.execService.python.execGetBoolFromStdout
import com.intellij.python.community.services.shared.VanillaPythonWithPythonInfo
import com.intellij.python.community.services.systemPython.SysPythonRegisterError
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.PyResult
import org.intellij.lang.annotations.Language
internal fun PyError.asSysPythonRegisterError(): SysPythonRegisterError.PythonIsBroken = SysPythonRegisterError.PythonIsBroken(this)
@Language("Python")
internal const val ENSURE_SYSTEM_PYTHON_CMD = "import sys; print(sys.prefix == sys.base_prefix)"
internal suspend fun ensureSystemPython(python: VanillaPythonWithPythonInfo): PyResult<Boolean> {
if (python.pythonInfo.languageLevel.isPython2) {
// there is no obvious check for venv in py2.7. Nobody uses it, anyway. Let it be.
return Result.success(true)
}
val systemPython = python.asExecutablePython.execGetBoolFromStdout(ENSURE_SYSTEM_PYTHON_CMD).getOr { return it }
return Result.success(systemPython)
}
@@ -15,13 +15,9 @@ import com.intellij.python.community.services.internal.impl.VanillaPythonWithPyt
import com.intellij.python.community.services.systemPython.SystemPythonServiceImpl.MyServiceState
import com.intellij.python.community.services.systemPython.impl.Cache
import com.intellij.python.community.services.systemPython.impl.PySystemPythonBundle
import com.jetbrains.python.NON_INTERACTIVE_ROOT_TRACE_CONTEXT
import com.jetbrains.python.PyToolUIInfo
import com.jetbrains.python.PythonBinary
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyResult
import com.intellij.python.community.services.systemPython.impl.asSysPythonRegisterError
import com.jetbrains.python.*
import com.jetbrains.python.errorProcessing.getOr
import com.jetbrains.python.getOrNull
import com.jetbrains.python.sdk.installer.installBinary
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
@@ -65,10 +61,10 @@ internal class SystemPythonServiceImpl(scope: CoroutineScope) : SystemPythonServ
}
}
override suspend fun registerSystemPython(pythonPath: PythonBinary): PyResult<SystemPython> {
override suspend fun registerSystemPython(pythonPath: PythonBinary): Result<SystemPython, SysPythonRegisterError> {
val pythonWithLangLevel = VanillaPythonWithPythonInfoImpl.createByPythonBinary(pythonPath)
.getOr(PySystemPythonBundle.message("py.system.python.service.python.is.broken", pythonPath)) { return it }
val systemPython = SystemPython(pythonWithLangLevel, null)
.getOr(PySystemPythonBundle.message("py.system.python.service.python.is.broken", pythonPath)) { return Result.failure(it.error.asSysPythonRegisterError()) }
val systemPython = SystemPython.create(pythonWithLangLevel, null).getOr { return it }
state.userProvidedPythons.add(pythonPath.pathString)
cache()?.get(pythonPath.getEelDescriptor())?.add(systemPython)
return Result.success(systemPython)
@@ -126,10 +122,14 @@ internal class SystemPythonServiceImpl(scope: CoroutineScope) : SystemPythonServ
val result = VanillaPythonWithPythonInfoImpl.createByPythonBinaries(pythons.toSet())
.mapNotNull { (python, r) ->
when (r) {
is Result.Success -> SystemPython(r.result, pythonsUi[r.result.pythonBinary])
val sysPython = r.mapSuccessError(
onSuccess = { r -> SystemPython.create(r, pythonsUi[r.pythonBinary]) },
onErr = { it.asSysPythonRegisterError() }
)
when (sysPython) {
is Result.Success -> sysPython.result
is Result.Failure -> {
fileLogger().warn("Skipping $python : ${r.error}")
fileLogger().warn("Skipping $python : ${sysPython.error.asPyError}")
badPythons.add(python)
null
}
@@ -68,28 +68,6 @@ class SystemPythonServiceShowCaseTest {
SystemPythonService().registerSystemPython(randomBinary).assertFail()
}
@Test
fun testCustomPythonSunnyDay(@PythonBinaryPath python: Path, @TempDir venvPath: Path): Unit = timeoutRunBlocking(10.minutes) {
createVenv(python, venvPath).getOrThrow()
val python = VirtualEnvReader.Instance.findPythonInPythonRoot(venvPath) ?: error("no python in $venvPath")
val newPython = SystemPythonService().registerSystemPython(python).orThrow()
var allPythons = SystemPythonService().findSystemPythons()
assertThat("No newly registered python returned", allPythons, hasItem(newPython))
if (SystemInfo.isWindows) {
deleteCheckLocking(python)
}
else {
python.deleteExisting()
}
allPythons = SystemPythonService().findSystemPythons(forceRefresh = true)
assertThat("Broken python returned", allPythons, not(hasItem(newPython)))
if (SystemInfo.isWindows) {
deleteCheckLocking(venvPath)
}
}
@Test
fun testRefresh(@TestDisposable disposable: Disposable): Unit = timeoutRunBlocking(10.minutes) {
val provider = CountingTestProvider(Result.failure(MessageError("...")))