PY-80049: Check Python2 version correctly.

We execute `--version` to find python version.
Since 3.4 Python uses stdout for output, but before that it used stderr.

We now check both: stdout and stderr.

GitOrigin-RevId: ca9059b71cabbd1a94e26523192cdf9eeb8c1eb1
This commit is contained in:
Ilya.Kazakevich
2025-03-28 12:59:26 +01:00
committed by intellij-monorepo-bot
parent f313e649fd
commit 92b5532dc1
6 changed files with 41 additions and 15 deletions

View File

@@ -4,6 +4,7 @@ import com.intellij.openapi.util.NlsSafe
import com.intellij.platform.eel.getOr
import com.intellij.platform.eel.path.EelPath
import com.intellij.platform.eel.provider.getEelDescriptor
import com.intellij.platform.eel.provider.utils.EelProcessExecutionResult
import com.intellij.platform.eel.provider.utils.exec
import com.intellij.platform.eel.provider.utils.stderrString
import com.intellij.platform.eel.provider.utils.stdoutString
@@ -22,7 +23,6 @@ import kotlin.io.path.pathString
import kotlin.time.Duration.Companion.seconds
// TODO: PythonInterpreterService: validate system python
/**
* Ensures that this python is executable and returns its version. Error if python is broken.
*
@@ -31,24 +31,25 @@ import kotlin.time.Duration.Companion.seconds
*/
@ApiStatus.Internal
suspend fun PythonBinary.validatePythonAndGetVersion(): Result<LanguageLevel, @NlsSafe String> = withContext(Dispatchers.IO) {
val smokeTestOutput = executeWithResult("-c", "print(1)").getOr { return@withContext it }.trim()
val smokeTestOutput = executeWithResult("-c", "print(1)").getOr { return@withContext it }.stdoutString.trim()
if (smokeTestOutput != "1") {
return@withContext failure(message("python.get.version.error", pathString, smokeTestOutput))
}
val versionString = executeWithResult(PYTHON_VERSION_ARG).getOr { return@withContext it }
val versionOutput = executeWithResult(PYTHON_VERSION_ARG).getOr { return@withContext it }
// Python 2 might return version as stderr, see https://bugs.python.org/issue18338
val versionString = versionOutput.stdoutString.let { it.ifBlank { versionOutput.stderrString } }
val languageLevel = getLanguageLevelFromVersionStringStaticSafe(versionString.trim())
if (languageLevel == null) {
return@withContext failure(message("python.get.version.wrong.version", pathString, versionString))
return@withContext failure(message("python.get.version.wrong.version", pathString, versionOutput))
}
return@withContext Result.success(languageLevel)
}
/**
* Executes [this] with [args], returns either stdout or error (if execution failed or exit code != 0)
* Executes [this] with [args], returns either output or error (if execution failed or exit code != 0)
*/
@ApiStatus.Internal
suspend fun PythonBinary.executeWithResult(vararg args: String): Result<@NlsSafe String, @NlsSafe String> {
private suspend fun PythonBinary.executeWithResult(vararg args: String): Result<@NlsSafe EelProcessExecutionResult, @NlsSafe String> {
val output = exec(*args, timeout = 5.seconds).getOr {
val text = it.error?.message ?: message("python.get.version.too.long", pathString)
return failure(text)
@@ -57,7 +58,7 @@ suspend fun PythonBinary.executeWithResult(vararg args: String): Result<@NlsSafe
failure(message("python.get.version.error", pathString, "code ${output.exitCode}, ${output.stderrString}"))
}
else {
Result.success(output.stdoutString)
Result.success(output)
}
}

View File

@@ -0,0 +1,19 @@
// 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.env.systemPython
import com.intellij.python.community.services.systemPython.SystemPythonService
import com.intellij.python.junit5Tests.framework.env.PyEnvTestCase
import com.intellij.testFramework.common.timeoutRunBlocking
import com.jetbrains.python.getOrThrow
import com.jetbrains.python.psi.LanguageLevel
import org.junit.jupiter.api.Test
@PyEnvTestCase
class Py27Test {
@Test
fun testPy27(): Unit = timeoutRunBlocking {
val testEnvironments = SystemPythonService().findSystemPythons()
val python27 = testEnvironments.firstOrNull { it.languageLevel == LanguageLevel.PYTHON27 } ?: error("No 2.7 found in $testEnvironments")
SystemPythonService().registerSystemPython(python27.pythonBinary).getOrThrow()
}
}

View File

@@ -38,6 +38,7 @@ import org.junit.jupiter.api.io.TempDir
import java.nio.file.Path
import kotlin.io.path.deleteExisting
import kotlin.io.path.pathString
import kotlin.time.Duration.Companion.minutes
@PyEnvTestCase
class SystemPythonServiceShowCaseTest {
@@ -49,7 +50,7 @@ class SystemPythonServiceShowCaseTest {
val eelApi = systemPython.pythonBinary.getEelDescriptor().upgrade()
val process = eelApi.exec.executeProcess(systemPython.pythonBinary.pathString, "--version").getOrThrow()
val output = async {
process.stdout.readWholeText().getOrThrow()
(if (systemPython.languageLevel.isPy3K) process.stdout else process.stderr).readWholeText().getOrThrow()
}
Assertions.assertEquals(0, process.exitCode.await(), "Wrong exit code")
val versionString = PythonSdkFlavor.getLanguageLevelFromVersionStringStaticSafe(output.await())!!

View File

@@ -6,9 +6,11 @@ import com.intellij.openapi.util.Disposer
import com.intellij.platform.eel.EelApi
import com.intellij.platform.eel.provider.localEel
import com.intellij.python.community.services.systemPython.SystemPythonProvider
import com.intellij.python.community.testFramework.testEnv.TypeVanillaPython
import com.intellij.python.community.testFramework.testEnv.TypeVanillaPython3
import com.jetbrains.python.PythonBinary
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.toSet
/**
@@ -18,8 +20,8 @@ internal class EnvTestPythonProvider : SystemPythonProvider {
override suspend fun findSystemPythons(eelApi: EelApi): Result<Set<PythonBinary>> {
var pythons = emptySet<PythonBinary>()
if (eelApi == localEel) {
pythons = TypeVanillaPython3
.getTestEnvironments()
// Add Py27 temporary to test Py27
pythons = merge(TypeVanillaPython3.getTestEnvironments(), TypeVanillaPython2.getTestEnvironments())
.map { (python, closeable) ->
Disposer.register(ApplicationManager.getApplication()) {
closeable.close()
@@ -31,4 +33,6 @@ internal class EnvTestPythonProvider : SystemPythonProvider {
return Result.success(pythons)
}
}
}
private object TypeVanillaPython2 : TypeVanillaPython("python2.7")

View File

@@ -41,7 +41,6 @@ abstract class PythonType<T : Any>(private val tag: @NonNls String) {
?: error("Can't get language level for $flavor , $binary")
}
.sortedByDescending { (_, languageLevel) -> languageLevel }
.filter { (_, languageLevel) -> languageLevel.isPy3K }
.map { (path, _) -> path }
}
return pythons.asFlow()

View File

@@ -11,7 +11,7 @@ import com.jetbrains.python.PyNames
import com.jetbrains.python.PythonBinary
import java.nio.file.Path
data object TypeVanillaPython3 : PythonType<PythonBinary>("python3") {
open class TypeVanillaPython(tag: String) : PythonType<PythonBinary>(tag) {
override suspend fun createSdkFor(python: PythonBinary): Sdk = createSdk(python)
fun createSdk(python: PythonBinary): Sdk =
@@ -27,4 +27,6 @@ data object TypeVanillaPython3 : PythonType<PythonBinary>("python3") {
Disposer.dispose(disposable)
})
}
}
}
object TypeVanillaPython3 : TypeVanillaPython("python3")