mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-85903 Implement a cache for conda environments
GitOrigin-RevId: 78338ac3ffc7f25a6ba980cd017f0c8c0602bc6b
This commit is contained in:
committed by
intellij-monorepo-bot
parent
ec20742361
commit
ddc3157c60
+1
-1
@@ -199,7 +199,7 @@ internal class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExten
|
||||
|
||||
private suspend fun createCondaEnv(project: Project, condaExecutable: String, environmentYml: String): PyResult<Sdk> {
|
||||
val binaryToExec = BinOnEel(Path.of(condaExecutable))
|
||||
val existingEnvs = PyCondaEnv.getEnvs(binaryToExec).getOrNull() ?: emptyList()
|
||||
val existingEnvs = PyCondaEnv.getEnvs(binaryToExec, forceRefresh = true).getOrNull() ?: emptyList()
|
||||
|
||||
val existingSdks = PyConfigurableInterpreterList.getInstance(project).model.sdks
|
||||
val newCondaEnvInfo = NewCondaEnvRequest.LocalEnvByLocalEnvironmentFile(Path.of(environmentYml),
|
||||
|
||||
@@ -35,7 +35,9 @@
|
||||
implementationClass="com.jetbrains.python.sdk.poetry.PoetryPackageVersionsInspection"
|
||||
key="INSP.poetry.package.versions.display.name" bundle="messages.PyBundle" groupKey="INSP.GROUP.python"
|
||||
suppressId="PoetryPackageVersions" shortName="PoetryPackageVersionsInspection"/>
|
||||
<registryKey defaultValue="10" description="How ofter system pythons must be refreshed (minutes)" key="python.system.refresh.minutes"/>
|
||||
<registryKey defaultValue="10" description="How often system pythons must be refreshed (minutes)" key="python.system.refresh.minutes" restartRequired="true"/>
|
||||
<registryKey defaultValue="20" description="How often conda environments cache must be refreshed (seconds)" key="python.conda.envs.refresh.seconds" restartRequired="true"/>
|
||||
<registryKey defaultValue="60" description="TTL of conda environments cache (seconds)" key="python.conda.envs.cache.ttl.seconds" restartRequired="true"/>
|
||||
<!-- Only modules with `baseDir` support Python SDKs -->
|
||||
<registryKey defaultValue="true" description="Show non-python modules(different type, no python facet) in Python configurable when they have base dir" key="python.show.modules.with.base.dir" restartRequired="true"/>
|
||||
<fileType name="Requirements.txt"
|
||||
|
||||
@@ -26,6 +26,7 @@ suspend fun <T> runExecutableWithProgress(
|
||||
env: Map<String, String> = emptyMap(),
|
||||
vararg args: String,
|
||||
transformer: ProcessOutputTransformer<T>,
|
||||
execService: ExecService = ExecService(),
|
||||
processWeight: ConcurrentProcessWeight = ConcurrentProcessWeight.LIGHT
|
||||
): PyResult<T> {
|
||||
val execOptions = ExecOptions(timeout = timeout, env = env, weight = processWeight)
|
||||
@@ -37,7 +38,7 @@ suspend fun <T> runExecutableWithProgress(
|
||||
}
|
||||
}
|
||||
|
||||
return ExecService().execute(
|
||||
return execService.execute(
|
||||
binary = binaryToExec,
|
||||
args = Args(*args),
|
||||
options = execOptions,
|
||||
|
||||
+1
-1
@@ -122,7 +122,7 @@ internal class CondaExistingEnvironmentSelector<P : PathHolder>(model: PythonAdd
|
||||
|
||||
reloadLink.action = object : AbstractAction(message("sdk.create.custom.conda.refresh.envs")) {
|
||||
override fun actionPerformed(e: ActionEvent?) {
|
||||
model.condaViewModel.detectCondaEnvironments()
|
||||
model.condaViewModel.detectCondaEnvironments(forceRefresh = true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -89,15 +89,15 @@ class CondaViewModel<P : PathHolder>(
|
||||
baseCondaEnv.set(null)
|
||||
|
||||
if (condaExecutable?.validationResult?.successOrNull != null) {
|
||||
detectCondaEnvironments()
|
||||
detectCondaEnvironments(forceRefresh = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun detectCondaEnvironments() {
|
||||
fun detectCondaEnvironments(forceRefresh: Boolean) {
|
||||
condaEnvironmentsLoading.value = true
|
||||
scope.launch(Dispatchers.UI) {
|
||||
condaEnvironmentsResult.value = updateCondaEnvironments()
|
||||
condaEnvironmentsResult.value = updateCondaEnvironments(forceRefresh)
|
||||
}.invokeOnCompletion {
|
||||
condaEnvironmentsLoading.value = false
|
||||
}
|
||||
@@ -133,13 +133,13 @@ class CondaViewModel<P : PathHolder>(
|
||||
path.refreshAndFindVirtualFileOrDirectory()?.takeIf { virtualFile -> virtualFile.isFile }
|
||||
}
|
||||
|
||||
private suspend fun updateCondaEnvironments(): PyResult<List<PyCondaEnv>> = withContext(Dispatchers.IO) {
|
||||
private suspend fun updateCondaEnvironments(forceRefresh: Boolean): PyResult<List<PyCondaEnv>> = withContext(Dispatchers.IO) {
|
||||
val executable = condaExecutable.get()
|
||||
if (executable == null) return@withContext PyResult.localizedError(message("python.sdk.conda.no.exec"))
|
||||
executable.validationResult.getOr { return@withContext it }
|
||||
|
||||
val binaryToExec = executable.pathHolder?.let { fileSystem.getBinaryToExec(it) }!!
|
||||
val environments = PyCondaEnv.getEnvs(binaryToExec).getOr { return@withContext it }
|
||||
val environments = PyCondaEnv.getEnvs(binaryToExec, forceRefresh).getOr { return@withContext it }
|
||||
val baseConda = environments.find { env -> env.envIdentity.let { it is PyCondaEnvIdentity.UnnamedEnv && it.isBase } }
|
||||
|
||||
withContext(Dispatchers.UI) {
|
||||
|
||||
@@ -57,10 +57,11 @@ object CondaExecutor {
|
||||
) { PyResult.success(Unit) }
|
||||
}
|
||||
|
||||
suspend fun listEnvs(binaryToExec: BinaryToExec): PyResult<CondaEnvInfo> {
|
||||
suspend fun listEnvs(binaryToExec: BinaryToExec, execService: ExecService = ExecService()): PyResult<CondaEnvInfo> {
|
||||
val args = listOf("env", "list", "--json")
|
||||
return runConda(
|
||||
binaryToExec, args, null,
|
||||
execService = execService,
|
||||
transformer = ZeroCodeJsonParserTransformer { CondaExecutionParser.parseListEnvironmentsOutput(it) }
|
||||
)
|
||||
}
|
||||
@@ -129,11 +130,20 @@ object CondaExecutor {
|
||||
args: List<String>,
|
||||
condaEnvIdentity: PyCondaEnvIdentity?,
|
||||
timeout: Duration = 15.minutes,
|
||||
execService: ExecService = ExecService(),
|
||||
transformer: ProcessOutputTransformer<T>,
|
||||
): PyResult<T> {
|
||||
val envs = getFixedEnvs(binaryToExec).getOr { return it }
|
||||
val runArgs = prepareCondaRunArgs(args, emptyList(), condaEnvIdentity).toTypedArray()
|
||||
return runExecutableWithProgress(binaryToExec, timeout, env = envs, *runArgs, transformer = transformer, processWeight = ConcurrentProcessWeight.HEAVY)
|
||||
return runExecutableWithProgress(
|
||||
binaryToExec,
|
||||
timeout,
|
||||
env = envs,
|
||||
*runArgs,
|
||||
transformer = transformer,
|
||||
execService = execService,
|
||||
processWeight = ConcurrentProcessWeight.HEAVY
|
||||
)
|
||||
}
|
||||
|
||||
private fun getFixedEnvs(binaryToExec: BinaryToExec): PyResult<Map<String, String>> {
|
||||
|
||||
@@ -4,27 +4,28 @@ package com.jetbrains.python.sdk.flavors.conda
|
||||
import com.intellij.execution.target.FullPathOnTarget
|
||||
import com.intellij.execution.target.TargetEnvironmentConfiguration
|
||||
import com.intellij.execution.target.TargetedCommandLineBuilder
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.python.community.execService.BinOnEel
|
||||
import com.intellij.python.community.execService.BinOnTarget
|
||||
import com.intellij.openapi.util.IntellijInternalApi
|
||||
import com.intellij.openapi.util.registry.RegistryManager
|
||||
import com.intellij.python.community.execService.BinaryToExec
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.sdk.conda.TargetCommandExecutor
|
||||
import com.jetbrains.python.sdk.conda.createCondaSdkFromExistingEnv
|
||||
import com.jetbrains.python.sdk.conda.execution.CondaExecutor
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv.Companion.getEnvs
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.async
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import java.nio.file.Path
|
||||
import java.util.*
|
||||
import kotlin.io.path.name
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* TODO: Once we get rid of [TargetCommandExecutor] and have access to [TargetEnvironmentConfiguration] use it validate conda binary in [getEnvs]
|
||||
* @see `PyCondaTest`
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
|
||||
data class PyCondaEnv(
|
||||
val envIdentity: PyCondaEnvIdentity,
|
||||
val fullCondaPathOnTarget: FullPathOnTarget,
|
||||
@@ -32,39 +33,20 @@ data class PyCondaEnv(
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* The logic is the following:
|
||||
*
|
||||
* - If an explicit refresh is triggered, ask the cache to reload the value
|
||||
* - If a value is present in the cache, a refresh is triggered if the refresh interval has passed, and the old value is returned:
|
||||
* - If it's an error, let's try to reload (we may succeed this time)
|
||||
* - If it's a success, return this value
|
||||
* - If a value is not present in the cache, it will be calculated
|
||||
*
|
||||
* @return list of conda environments
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
suspend fun getEnvs(binaryToExec: BinaryToExec): PyResult<List<PyCondaEnv>> {
|
||||
val condaPath = when (binaryToExec) {
|
||||
is BinOnEel -> binaryToExec.path.toString()
|
||||
is BinOnTarget -> binaryToExec.getLocalExePath().value
|
||||
}
|
||||
val info = CondaExecutor.listEnvs(binaryToExec).getOr { return it }
|
||||
val condaPrefix = info.condaPrefix ?: condaPath.removeSuffix("/bin/conda")
|
||||
val envs = info.envs.distinctBy { it.trim().lowercase(Locale.getDefault()) }
|
||||
val identities = envs.map { envPath ->
|
||||
// Env name is the basename for envs inside of default location
|
||||
// envPath should be direct child of envs_dirs to be a NamedEnv
|
||||
val isEnvName = info.envsDirs.any {
|
||||
Path.of(it) == Path.of(envPath).parent
|
||||
}
|
||||
val envName = if (isEnvName)
|
||||
Path.of(envPath).name
|
||||
else
|
||||
null
|
||||
val base = envPath.equals(condaPrefix, ignoreCase = true)
|
||||
val identity = if (envName != null) {
|
||||
PyCondaEnvIdentity.NamedEnv(envName)
|
||||
}
|
||||
else {
|
||||
PyCondaEnvIdentity.UnnamedEnv(envPath, base)
|
||||
}
|
||||
PyCondaEnv(identity, condaPath)
|
||||
}
|
||||
|
||||
return PyResult.success(identities)
|
||||
}
|
||||
@JvmOverloads
|
||||
suspend fun getEnvs(binaryToExec: BinaryToExec, forceRefresh: Boolean = false): PyResult<List<PyCondaEnv>> =
|
||||
service<CondaEnvService>().getEnvs(binaryToExec, forceRefresh)
|
||||
|
||||
suspend fun createEnv(command: PyCondaCommand, newCondaEnvInfo: NewCondaEnvRequest): PyResult<Unit> {
|
||||
return newCondaEnvInfo.create(command.asBinaryToExec())
|
||||
@@ -98,4 +80,19 @@ data class PyCondaEnv(
|
||||
}
|
||||
|
||||
override fun toString(): String = "$envIdentity@$fullCondaPathOnTarget"
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(IntellijInternalApi::class)
|
||||
@Service(Service.Level.APP)
|
||||
private class CondaEnvService(scope: CoroutineScope) {
|
||||
private val _condaEnvProviderImpl: Deferred<PyCondaEnvProvider> = scope.async {
|
||||
PyCondaEnvProvider(
|
||||
refreshInterval = RegistryManager.getInstanceAsync().intValue("python.conda.envs.refresh.seconds").seconds,
|
||||
ttlAfterWrite = RegistryManager.getInstanceAsync().intValue("python.conda.envs.cache.ttl.seconds").seconds,
|
||||
)
|
||||
}
|
||||
private suspend fun condaEnvProvider() = _condaEnvProviderImpl.await()
|
||||
|
||||
suspend fun getEnvs(binaryToExec: BinaryToExec, forceRefresh: Boolean): PyResult<List<PyCondaEnv>> =
|
||||
condaEnvProvider().getEnvs(binaryToExec, forceRefresh)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// 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.sdk.flavors.conda
|
||||
|
||||
import com.github.benmanes.caffeine.cache.AsyncLoadingCache
|
||||
import com.github.benmanes.caffeine.cache.Caffeine
|
||||
import com.intellij.openapi.progress.runBlockingMaybeCancellable
|
||||
import com.intellij.openapi.util.IntellijInternalApi
|
||||
import com.intellij.python.community.execService.BinOnEel
|
||||
import com.intellij.python.community.execService.BinOnTarget
|
||||
import com.intellij.python.community.execService.BinaryToExec
|
||||
import com.intellij.python.community.execService.ExecService
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.isFailure
|
||||
import com.jetbrains.python.isSuccess
|
||||
import com.jetbrains.python.sdk.conda.execution.CondaExecutor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.asExecutor
|
||||
import kotlinx.coroutines.future.asDeferred
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import java.nio.file.Path
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executor
|
||||
import kotlin.io.path.name
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.toJavaDuration
|
||||
|
||||
@ApiStatus.Internal
|
||||
@IntellijInternalApi
|
||||
class PyCondaEnvProvider(
|
||||
refreshInterval: Duration,
|
||||
ttlAfterWrite: Duration,
|
||||
executor: Executor = Dispatchers.Default.asExecutor(),
|
||||
private val execService: ExecService = ExecService(),
|
||||
) {
|
||||
private val cache: AsyncLoadingCache<BinaryToExec, PyResult<List<PyCondaEnv>>> = Caffeine.newBuilder()
|
||||
.executor(executor)
|
||||
.refreshAfterWrite(refreshInterval.toJavaDuration())
|
||||
.expireAfterWrite(ttlAfterWrite.toJavaDuration())
|
||||
.buildAsync { binaryToExec ->
|
||||
runBlockingMaybeCancellable {
|
||||
getEnvsInternal(binaryToExec)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getEnvs(binaryToExec: BinaryToExec, forceRefresh: Boolean): PyResult<List<PyCondaEnv>> {
|
||||
if (forceRefresh) {
|
||||
return cache.synchronous().refresh(binaryToExec).asDeferred().await()
|
||||
}
|
||||
|
||||
val currentValue = cache.getIfPresent(binaryToExec)?.asDeferred()?.await()
|
||||
return when {
|
||||
currentValue?.isSuccess == true -> currentValue
|
||||
currentValue?.isFailure == true -> cache.synchronous().refresh(binaryToExec).asDeferred().await()
|
||||
else -> cache[binaryToExec].asDeferred().await()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getEnvsInternal(binaryToExec: BinaryToExec): PyResult<List<PyCondaEnv>> {
|
||||
val condaPath = when (binaryToExec) {
|
||||
is BinOnEel -> binaryToExec.path.toString()
|
||||
is BinOnTarget -> binaryToExec.getLocalExePath().value
|
||||
}
|
||||
val info = CondaExecutor.listEnvs(binaryToExec, execService).getOr { return it }
|
||||
val condaPrefix = info.condaPrefix ?: condaPath.removeSuffix("/bin/conda")
|
||||
val envs = info.envs.distinctBy { it.trim().lowercase(Locale.getDefault()) }
|
||||
val identities = envs.map { envPath ->
|
||||
// Env name is the basename for envs inside of default location
|
||||
// envPath should be direct child of envs_dirs to be a NamedEnv
|
||||
val isEnvName = info.envsDirs.any {
|
||||
Path.of(it) == Path.of(envPath).parent
|
||||
}
|
||||
val envName = if (isEnvName)
|
||||
Path.of(envPath).name
|
||||
else
|
||||
null
|
||||
val base = envPath.equals(condaPrefix, ignoreCase = true)
|
||||
val identity = if (envName != null) {
|
||||
PyCondaEnvIdentity.NamedEnv(envName)
|
||||
}
|
||||
else {
|
||||
PyCondaEnvIdentity.UnnamedEnv(envPath, base)
|
||||
}
|
||||
PyCondaEnv(identity, condaPath)
|
||||
}
|
||||
|
||||
return PyResult.success(identities)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user