mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
cleanup [python]: Make VirtualEnvReader support eel.
GitOrigin-RevId: 5855162b20bda624f764a0c75733a28b334c85ef
This commit is contained in:
committed by
intellij-monorepo-bot
parent
b3ad3600d2
commit
a523b7d4d2
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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?,
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user