PY-85365 Detect conda environments without environment.yml

GitOrigin-RevId: 8a51ab264a4810dda6bbdb2ad40aa25489b1ed49
This commit is contained in:
Alexey Katsman
2025-11-06 23:06:43 +00:00
committed by intellij-monorepo-bot
parent 47fc499a0d
commit c3650beaf7
@@ -8,7 +8,9 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBundle
import com.intellij.pycharm.community.ide.impl.configuration.PySdkConfigurationCollector
@@ -19,6 +21,7 @@ import com.intellij.pycharm.community.ide.impl.configuration.ui.PyAddNewCondaEnv
import com.intellij.pycharm.community.ide.impl.findEnvOrNull
import com.intellij.python.community.execService.BinOnEel
import com.intellij.python.sdk.ui.icons.PythonSdkUIIcons
import com.intellij.util.FileName
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PyToolUIInfo
@@ -51,6 +54,7 @@ import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.SystemDependent
import java.nio.file.Path
import kotlin.io.path.name
/**
@@ -72,10 +76,7 @@ class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExtension {
private suspend fun checkManageableEnv(module: Module, checkExistence: CheckExistence): EnvCheckerResult = withBackgroundProgress(module.project, PyBundle.message("python.sdk.validating.environment")) {
val condaPath = withContext(Dispatchers.IO) {
if (getEnvironmentYml(module) != null) {
suggestCondaPath()?.let { LocalFileSystem.getInstance().findFileByPath(it) }
}
else null
suggestCondaPath()?.let { LocalFileSystem.getInstance().findFileByPath(it) }
}
val canManage = condaPath != null
val intentionName = PyCharmCommunityCustomizationBundle.message("sdk.create.condaenv.suggestion")
@@ -88,7 +89,7 @@ class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExtension {
CondaExecutor.getPythonInfo(binaryToExec, env).findEnvOrNull(intentionName)
} ?: envNotFound
}
canManage -> envNotFound
canManage -> if (getEnvironmentYml(module) != null) envNotFound else EnvCheckerResult.CannotConfigure
else -> EnvCheckerResult.CannotConfigure
}
}
@@ -105,22 +106,22 @@ class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExtension {
return PyResult.localizedError(PyCharmCommunityCustomizationBundle.message("sdk.remote.target.are.not.supported.for.conda.environment"))
}
val (condaExecutable, environmentYml) = askForEnvData(module, source, envExists) ?: return PyResult.success(null)
return createAndAddCondaEnv(module, condaExecutable, environmentYml, envExists).onSuccess { sdk ->
sdk.let { PythonSdkUpdater.scheduleUpdate(it, module.project) }
}
}
private suspend fun askForEnvData(module: Module, source: Source, envExists: Boolean) = withContext(Dispatchers.Default) {
val environmentYml = getEnvironmentYml(module) ?: return@withContext null
// Again: only local conda is supported for now
val condaExecutable = suggestCondaPath()?.let { LocalFileSystem.getInstance().findFileByPath(it) }
if ((envExists || source == Source.INSPECTION) && validateCondaPath(condaExecutable?.path, PlatformAndRoot.local) == null) {
val sdk = if ((envExists || source == Source.INSPECTION) && validateCondaPath(condaExecutable?.path, PlatformAndRoot.local) == null) {
PySdkConfigurationCollector.logCondaEnvDialogSkipped(module.project, source, executableToEventField(condaExecutable?.path))
return@withContext PyAddNewCondaEnvFromFilePanel.Data(condaExecutable!!.path, environmentYml.path)
createAndAddCondaEnv(module, condaExecutable!!.path, null)
}
else {
val envData = askForEnvData(module, source, condaExecutable) ?: return PyResult.success(null)
createAndAddCondaEnv(module, envData.condaPath, envData.environmentYmlPath)
}
return sdk.onSuccess { sdk -> sdk.let { PythonSdkUpdater.scheduleUpdate(it, module.project) } }
}
private suspend fun askForEnvData(module: Module, source: Source, condaExecutable: VirtualFile?) = withContext(Dispatchers.Default) {
val environmentYml = getEnvironmentYml(module) ?: return@withContext null
var permitted = false
var envData: PyAddNewCondaEnvFromFilePanel.Data? = null
@@ -137,12 +138,10 @@ class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExtension {
if (permitted) envData else null
}
private suspend fun createAndAddCondaEnv(
module: Module, condaExecutable: String, environmentYml: String, envExists: Boolean,
): PyResult<Sdk> {
private suspend fun createAndAddCondaEnv(module: Module, condaExecutable: String, environmentYml: String?): PyResult<Sdk> {
thisLogger().debug("Creating conda environment")
val sdk = if (envExists) {
val sdk = if (environmentYml == null) {
useExistingCondaEnv(module, condaExecutable)
}
else {
@@ -181,15 +180,21 @@ class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExtension {
}
private suspend fun getCondaEnvIdentity(module: Module, condaExecutable: String): PyCondaEnvIdentity? {
val environmentYml = getEnvironmentYml(module) ?: return null
val envName = CondaEnvironmentYmlParser.readNameFromFile(environmentYml)
val envPrefix = CondaEnvironmentYmlParser.readPrefixFromFile(environmentYml)
val environmentYml = getEnvironmentYml(module)
val envName = environmentYml?.let { CondaEnvironmentYmlParser.readNameFromFile(it) }
val envPrefix = environmentYml?.let { CondaEnvironmentYmlParser.readPrefixFromFile(it) }
val shouldGuessEnvPrefix = envName == null && envPrefix == null
val binaryToExec = BinOnEel(Path.of(condaExecutable))
return PyCondaEnv.getEnvs(binaryToExec).getOr { return null }.firstOrNull {
val envIdentity = it.envIdentity
when (envIdentity) {
is PyCondaEnvIdentity.NamedEnv -> envIdentity.envName == envName
is PyCondaEnvIdentity.UnnamedEnv -> envIdentity.envPath == envPrefix
is PyCondaEnvIdentity.UnnamedEnv -> if (shouldGuessEnvPrefix) {
val envPath = Path.of(envIdentity.envPath)
val sameModule = module.basePath?.toNioPathOrNull() == envPath.parent
!envIdentity.isBase && sameModule && module.findAmongRoots(FileName(Path.of(envIdentity.envPath).name)) != null
}
else envIdentity.envPath == envPrefix
}
}?.envIdentity
}