cleanup [python]: Make VirtualEnvReader support eel.

GitOrigin-RevId: 5855162b20bda624f764a0c75733a28b334c85ef
This commit is contained in:
Ilya.Kazakevich
2025-12-26 19:40:48 +00:00
committed by intellij-monorepo-bot
parent b3ad3600d2
commit a523b7d4d2
8 changed files with 106 additions and 59 deletions
@@ -1,42 +1,54 @@
// 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.venvReader
import com.intellij.openapi.util.SystemInfoRt
import com.intellij.openapi.progress.runBlockingMaybeCancellable
import com.intellij.openapi.util.io.toCanonicalPath
import com.intellij.util.SystemProperties
import com.intellij.platform.eel.EelApi
import com.intellij.platform.eel.EelOsFamily
import com.intellij.platform.eel.environmentVariables
import com.intellij.platform.eel.provider.asNioPath
import com.intellij.platform.eel.provider.getEelDescriptor
import com.intellij.platform.eel.provider.localEel
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.jetbrains.python.PythonBinary
import com.jetbrains.python.PythonHomePath
import com.jetbrains.python.venvReader.VirtualEnvReader.Companion.Instance
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.NonNls
import org.jetbrains.annotations.TestOnly
import java.io.IOException
import java.nio.file.Path
import kotlin.io.path.*
typealias Directory = Path
/**
* Use [Instance]. Provide "forced" vars to ctor for tests only.
*/
@ApiStatus.Internal
class VirtualEnvReader(
private val envs: Map<@NonNls String, @NonNls String> = System.getenv(),
isWindows: Boolean = SystemInfoRt.isWindows,
class VirtualEnvReader private constructor(
private val forcedVars: Map<@NonNls String, @NonNls String>?,
private val forcedOs: EelOsFamily? = null,
) {
private val pythonNames = if (isWindows)
setOf("pypy.exe", "python.exe")
else
setOf("pypy", "python")
private val binFolderName = if (isWindows) {
"Scripts"
} else {
"bin"
}
@TestOnly
constructor(
forcedVars: Map<@NonNls String, @NonNls String>? = null,
isWindows: Boolean? = null,
) : this(forcedVars, when (isWindows) {
true -> EelOsFamily.Windows
false -> EelOsFamily.Posix
null -> null
})
private constructor() : this(forcedVars = null, forcedOs = null)
/**
* Dir with virtual envs
*/
@RequiresBackgroundThread
fun getVEnvRootDir(): Directory {
return resolveDirFromEnvOrElseGetDirInHomePath("WORKON_HOME", DEFAULT_VIRTUALENVS_DIR)
fun getVEnvRootDir(eel: EelApi = localEel): Directory {
return resolveDirFromEnvOrElseGetDirInHomePath(eel, "WORKON_HOME", DEFAULT_VIRTUALENVS_DIR)
}
/**
@@ -47,8 +59,8 @@ class VirtualEnvReader(
findLocalInterpreters(getVEnvRootDir())
@RequiresBackgroundThread
fun getPyenvRootDir(): Directory {
return resolveDirFromEnvOrElseGetDirInHomePath("PYENV_ROOT", ".pyenv")
fun getPyenvRootDir(eel: EelApi = localEel): Directory {
return resolveDirFromEnvOrElseGetDirInHomePath(eel, "PYENV_ROOT", ".pyenv")
}
@RequiresBackgroundThread
@@ -127,6 +139,11 @@ class VirtualEnvReader(
fun getVenvRootPath(path: Path): Path? {
val bin = path.parent
val binFolderName = when (forcedOs ?: path.getEelDescriptor().osFamily) {
EelOsFamily.Posix -> "bin"
EelOsFamily.Windows -> "Scripts"
}
if (bin == null || bin.fileName.pathString != binFolderName) {
return null
}
@@ -150,13 +167,20 @@ class VirtualEnvReader(
* Looks for python binary among directory entries
*/
@RequiresBackgroundThread
private fun findInterpreter(dir: Path): PythonBinary? =
dir.listDirectoryEntries().firstOrNull { it.isRegularFile() && it.name.lowercase() in pythonNames }
private fun findInterpreter(dir: Path): PythonBinary? {
val pythonNames = when (forcedOs ?: dir.getEelDescriptor().osFamily) {
EelOsFamily.Posix -> setOf("pypy", "python")
EelOsFamily.Windows -> setOf("pypy.exe", "python.exe")
}
return dir.listDirectoryEntries().firstOrNull { it.isRegularFile() && it.name.lowercase() in pythonNames }
}
@RequiresBackgroundThread
private fun resolveDirFromEnvOrElseGetDirInHomePath(env: String, dirName: String): Path =
envs[env]?.let { tryResolvePath(it) }
?: Path.of(SystemProperties.getUserHome(), dirName)
private fun resolveDirFromEnvOrElseGetDirInHomePath(eel: EelApi, env: String, dirName: String): Path {
val envs = forcedVars ?: runBlockingMaybeCancellable { eel.exec.environmentVariables().eelIt().await() }
return envs[env]?.let { tryResolvePath(it, eel.descriptor) }
?: eel.userInfo.home.asNioPath().resolve(dirName)
}
companion object {
@@ -1,24 +1,33 @@
// 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.venvReader
import com.intellij.platform.eel.EelDescriptor
import com.intellij.platform.eel.path.EelPathException
import com.intellij.platform.eel.provider.localEel
import com.intellij.platform.eel.provider.utils.Path
import com.jetbrains.python.sdk.CustomSdkHomePattern
import org.jetbrains.annotations.ApiStatus
import java.nio.file.InvalidPathException
import java.nio.file.Path
import kotlin.io.path.Path
/**
* Converts [str] to [Path] of str is real nio path. Returns null otherwise
*/
@JvmOverloads
@ApiStatus.Internal
fun tryResolvePath(str: String?): Path? {
if (str == null || CustomSdkHomePattern.isCustomPythonSdkHomePath(str)) {
fun tryResolvePath(str: String?, eelDescriptor: EelDescriptor? = null): Path? {
val eelIsLocal = eelDescriptor == null || eelDescriptor == localEel.descriptor
if (str == null || eelIsLocal && CustomSdkHomePattern.isCustomPythonSdkHomePath(str)) {
return null
}
try {
return Path.of(str)
return if (eelIsLocal) Path(str) else Path(str, eelDescriptor)
}
catch (_: InvalidPathException) {
}
catch (_: EelPathException) {
}
return null
}
@@ -21,5 +21,5 @@ fun PythonBinary.resolvePythonHome(): PythonHomePath = when (getEelDescriptor().
@RequiresBackgroundThread
@ApiStatus.Internal
fun PythonHomePath.resolvePythonBinary(): PythonBinary? {
return VirtualEnvReader(isWindows = getEelDescriptor().osFamily.isWindows).findPythonInPythonRoot(this)
return VirtualEnvReader.Instance.findPythonInPythonRoot(this)
}
+2
View File
@@ -30,6 +30,7 @@ jvm_library(
"//platform/util",
"//platform/core-api:core",
"//python/python-exec-service/execService.python",
"//platform/eel-provider",
]
)
@@ -61,6 +62,7 @@ jvm_library(
"//platform/core-api:core",
"//python/python-exec-service/execService.python",
"//python/python-exec-service/execService.python:execService.python_test_lib",
"//platform/eel-provider",
]
)
### auto-generated section `build intellij.python.community.impl.venv` end
@@ -26,5 +26,6 @@
<orderEntry type="module" module-name="intellij.platform.execution" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="module" module-name="intellij.python.community.execService.python" />
<orderEntry type="module" module-name="intellij.platform.eel.provider" />
</component>
</module>
@@ -3,6 +3,7 @@ package com.intellij.python.community.impl.venv
import com.intellij.openapi.application.EDT
import com.intellij.openapi.diagnostic.fileLogger
import com.intellij.platform.eel.provider.asEelPath
import com.intellij.python.community.execService.BinaryToExec
import com.intellij.python.community.execService.ExecOptions
import com.intellij.python.community.execService.ExecService
@@ -40,7 +41,7 @@ suspend fun createVenv(
inheritSitePackages: Boolean = false,
envReader: VirtualEnvReader = VirtualEnvReader.Instance,
): PyResult<PythonBinary> {
createVenv(python.asBinToExec(), venvDir.pathString, inheritSitePackages).getOr { return it }
createVenv(python.asBinToExec(), venvDir.asEelPath().toString(), inheritSitePackages).getOr { return it }
val venvPython = withContext(Dispatchers.IO) {
envReader.findPythonInPythonRoot(venvDir)
@@ -56,7 +56,7 @@ internal class UvExistingEnvironmentSelector<P : PathHolder>(model: PythonMutabl
}
val workingDirectory =
VirtualEnvReader().getVenvRootPath(selectedInterpreterPath.path)
VirtualEnvReader.Instance.getVenvRootPath(selectedInterpreterPath.path)
?: tryResolvePath(existingSdk?.associatedModulePath)
?: projectDir
@@ -2,12 +2,14 @@
package com.jetbrains.python.sdk
import com.intellij.grazie.grammar.assertIsEmpty
import com.intellij.openapi.util.SystemInfoRt
import com.intellij.openapi.util.io.FileUtilRt
import com.intellij.testFramework.junit5.TestApplication
import com.intellij.testFramework.utils.io.deleteRecursively
import com.jetbrains.python.venvReader.VirtualEnvReader
import com.jetbrains.python.venvReader.tryResolvePath
import org.junit.Assert
import org.junit.Test
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource
@@ -16,6 +18,7 @@ import java.nio.file.Path
import kotlin.io.path.absolutePathString
import kotlin.io.path.exists
@TestApplication
class VirtualEnvReaderTest {
inner class Bootstrap {
val PYENV_ROOT = "PYENV_ROOT"
@@ -24,7 +27,7 @@ class VirtualEnvReaderTest {
val pyenv = cwd.resolve(".pyenv")
val env = HashMap<String, String>()
val virtualEnvReader = VirtualEnvReader(env, isWindows = false)
val virtualEnvReader = VirtualEnvReader(env)
fun setupPyenv(versions: List<String>, binary: String) {
for (version in versions) {
@@ -99,62 +102,69 @@ class VirtualEnvReaderTest {
val bootstrap = Bootstrap()
// just version
bootstrap.setupPyenv(listOf("3.1.1"), "python")
val binary = if (SystemInfoRt.isWindows) "python.exe" else "python"
bootstrap.setupPyenv(listOf("3.1.1"), binary)
var interpreters = bootstrap.virtualEnvReader.findPyenvInterpreters()
Assert.assertEquals(1, interpreters.size)
Assertions.assertEquals(1, interpreters.size)
assert(interpreters[0].absolutePathString().startsWith(bootstrap.pyenv.absolutePathString()))
assert(interpreters[0].absolutePathString().endsWith("python"))
assert(interpreters[0].absolutePathString().endsWith(binary))
// another version w/o match
bootstrap.setupPyenv(listOf("3.2.1"), "xxx")
interpreters = bootstrap.virtualEnvReader.findPyenvInterpreters()
Assert.assertEquals(1, interpreters.size)
Assertions.assertEquals(1, interpreters.size)
// both in names
bootstrap.setupPyenv(listOf("3.2.2"), "pypy")
val pypyBinary = if (SystemInfoRt.isWindows) "pypy.exe" else "pypy"
bootstrap.setupPyenv(listOf("3.2.2"), pypyBinary)
interpreters = bootstrap.virtualEnvReader.findPyenvInterpreters()
Assert.assertEquals(2, interpreters.size)
Assertions.assertEquals(2, interpreters.size)
assert(interpreters[0] != interpreters[1])
bootstrap.removeVersion(bootstrap.pyenv, "3.2.2")
interpreters = bootstrap.virtualEnvReader.findPyenvInterpreters()
Assert.assertEquals(1, interpreters.size)
assert(interpreters[0].absolutePathString().endsWith("python"))
Assertions.assertEquals(1, interpreters.size)
assert(interpreters[0].absolutePathString().endsWith(binary))
}
@Test
fun testIsPyenvSdk() {
val bootstrap = Bootstrap()
Assert.assertFalse(bootstrap.virtualEnvReader.isPyenvSdk(null as String?))
Assert.assertFalse(bootstrap.virtualEnvReader.isPyenvSdk(""))
Assert.assertFalse(bootstrap.virtualEnvReader.isPyenvSdk("aa\u0000bb"))
Assert.assertFalse(bootstrap.virtualEnvReader.isPyenvSdk("a/b/c/d"))
Assert.assertFalse(bootstrap.virtualEnvReader.isPyenvSdk(bootstrap.cwd))
Assert.assertFalse(bootstrap.virtualEnvReader.isPyenvSdk(bootstrap.cwd.resolve("smthg")))
Assertions.assertFalse(bootstrap.virtualEnvReader.isPyenvSdk(null as String?))
Assertions.assertFalse(bootstrap.virtualEnvReader.isPyenvSdk(""))
Assertions.assertFalse(bootstrap.virtualEnvReader.isPyenvSdk("aa\u0000bb"))
Assertions.assertFalse(bootstrap.virtualEnvReader.isPyenvSdk("a/b/c/d"))
Assertions.assertFalse(bootstrap.virtualEnvReader.isPyenvSdk(bootstrap.cwd))
Assertions.assertFalse(bootstrap.virtualEnvReader.isPyenvSdk(bootstrap.cwd.resolve("smthg")))
bootstrap.setupPyenv(listOf("3.2.1"), "xxxx")
Assert.assertFalse(bootstrap.virtualEnvReader.isPyenvSdk(null as String?))
Assert.assertFalse(bootstrap.virtualEnvReader.isPyenvSdk(""))
Assert.assertFalse(bootstrap.virtualEnvReader.isPyenvSdk("aa\u0000bb"))
Assert.assertFalse(bootstrap.virtualEnvReader.isPyenvSdk("a/b/c/d"))
Assert.assertFalse(bootstrap.virtualEnvReader.isPyenvSdk(bootstrap.cwd))
Assert.assertFalse(bootstrap.virtualEnvReader.isPyenvSdk(bootstrap.cwd.resolve("smthg")))
Assertions.assertFalse(bootstrap.virtualEnvReader.isPyenvSdk(null as String?))
Assertions.assertFalse(bootstrap.virtualEnvReader.isPyenvSdk(""))
Assertions.assertFalse(bootstrap.virtualEnvReader.isPyenvSdk("aa\u0000bb"))
Assertions.assertFalse(bootstrap.virtualEnvReader.isPyenvSdk("a/b/c/d"))
Assertions.assertFalse(bootstrap.virtualEnvReader.isPyenvSdk(bootstrap.cwd))
Assertions.assertFalse(bootstrap.virtualEnvReader.isPyenvSdk(bootstrap.cwd.resolve("smthg")))
// particularly any path inside pyenv root will work
Assert.assertTrue(bootstrap.virtualEnvReader.isPyenvSdk(bootstrap.pyenv.resolve("xxx")))
Assertions.assertTrue(bootstrap.virtualEnvReader.isPyenvSdk(bootstrap.pyenv.resolve("xxx")))
// should resolve symlinks
val link = bootstrap.cwd.resolve("smthg")
val target = bootstrap.pyenv.resolve("xxx")
// hanging links, should not resolve it
Files.createSymbolicLink(link, target)
Assert.assertFalse(bootstrap.virtualEnvReader.isPyenvSdk(link))
if (!SystemInfoRt.isWindows) {
// links require UAC on Windows
Files.createSymbolicLink(link, target)
}
Assertions.assertFalse(bootstrap.virtualEnvReader.isPyenvSdk(link))
Files.createFile(target)
Assert.assertTrue(bootstrap.virtualEnvReader.isPyenvSdk(link))
if (!SystemInfoRt.isWindows) {
Files.createFile(target)
Assertions.assertTrue(bootstrap.virtualEnvReader.isPyenvSdk(link))
}
}
@ParameterizedTest(name = "{0}")
@@ -162,7 +172,7 @@ class VirtualEnvReaderTest {
fun getVenvRootPathTests(name: String, isWindows: Boolean, path: Path, expectedReturnValue: Path?) {
val result = VirtualEnvReader(isWindows = isWindows).getVenvRootPath(path)
Assert.assertEquals(expectedReturnValue, result)
Assertions.assertEquals(expectedReturnValue, result)
}
companion object {
@@ -231,7 +241,7 @@ class VirtualEnvReaderTest {
val name: String,
val isWindows: Boolean,
val path: String,
val expectedReturnValue: String?
val expectedReturnValue: String?,
)
}
}