PY-82119 Packaging: Rewrite Conda to use EEL

GitOrigin-RevId: 8329d24191bdc1411775dc8e58a65172a2804788
This commit is contained in:
Nikita Ashihmin
2025-06-19 17:30:40 +00:00
committed by intellij-monorepo-bot
parent 693c5014a8
commit 2c3a5a9f7a
36 changed files with 473 additions and 551 deletions
@@ -1,7 +1,6 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.pycharm.community.ide.impl
import com.intellij.concurrency.SensitiveProgressWrapper
import com.intellij.ide.trustedProjects.TrustedProjects
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.runInEdt
@@ -9,9 +8,6 @@ import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.diagnostic.debug
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.projectRoots.Sdk
@@ -24,16 +20,23 @@ import com.intellij.openapi.util.UserDataHolderBase
import com.intellij.openapi.util.use
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.platform.DirectoryProjectConfigurator
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.intellij.platform.util.progress.reportRawProgress
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PySdkBundle
import com.jetbrains.python.orLogException
import com.jetbrains.python.packaging.utils.PyPackageCoroutine
import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.conda.PyCondaSdkCustomizer
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfiguration.setReadyToUseSdk
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfiguration.setSdkUsingExtension
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfiguration.suppressTipAndInspectionsFor
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension
import org.jetbrains.annotations.ApiStatus.Internal
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus
import kotlin.coroutines.coroutineContext
/**
* @see [PyConfigureSdkOnWslTest]
@@ -51,9 +54,6 @@ class PythonSdkConfigurator : DirectoryProjectConfigurator {
return ProjectRootManager.getInstance(ProjectManager.getInstance().defaultProject).projectSdk?.takeIf { it.sdkType is PythonSdkType }
}
private fun <T> guardIndicator(indicator: ProgressIndicator, computable: () -> T): T {
return ProgressManager.getInstance().runProcess(computable, SensitiveProgressWrapper(indicator))
}
}
override fun configureProject(project: Project, baseDir: VirtualFile, moduleRef: Ref<Module>, isProjectCreatedWithWizard: Boolean) {
@@ -67,73 +67,73 @@ class PythonSdkConfigurator : DirectoryProjectConfigurator {
}
val module = getModule(moduleRef, project) ?: return
val extension = findExtension(module)
val lifetime = extension?.let { suppressTipAndInspectionsFor(module, it) }
StartupManager.getInstance(project).runWhenProjectIsInitialized {
ProgressManager.getInstance().run(
object : Task.Backgroundable(project, PySdkBundle.message("python.configuring.interpreter.progress"), extension == null) {
override fun run(indicator: ProgressIndicator) = lifetime.use { configureSdk(project, module, extension, indicator) }
PyPackageCoroutine.launch(project) {
val extension = findExtension(module)
val lifetime = extension?.let { suppressTipAndInspectionsFor(module, it) }
withBackgroundProgress(project, PySdkBundle.message("python.configuring.interpreter.progress"), extension == null) {
lifetime.use { configureSdk(project, module, extension) }
}
)
}
}
}
private fun findExtension(module: Module): PyProjectSdkConfigurationExtension? {
return if (!TrustedProjects.isProjectTrusted(module.project) || ApplicationManager.getApplication().isUnitTestMode) {
private suspend fun findExtension(module: Module): PyProjectSdkConfigurationExtension? = withContext(Dispatchers.Default) {
if (!TrustedProjects.isProjectTrusted(module.project) || ApplicationManager.getApplication().isUnitTestMode) {
null
}
else PyProjectSdkConfigurationExtension.EP_NAME.findFirstSafe {
else PyProjectSdkConfigurationExtension.EP_NAME.extensionsIfPointIsRegistered.firstOrNull {
it.getIntention(module) != null && (!ApplicationManager.getApplication().isHeadlessEnvironment || it.supportsHeadlessModel())
}
}
// TODO: PythonInterpreterService: detect and validate system python
@Internal
fun configureSdk(
@ApiStatus.Internal
suspend fun configureSdk(
project: Project,
module: Module,
extension: PyProjectSdkConfigurationExtension?,
indicator: ProgressIndicator,
) {
): Unit = reportRawProgress { indicator ->
// please keep this method in sync with com.jetbrains.python.inspections.PyInterpreterInspection.Visitor.getSuitableSdkFix
indicator.isIndeterminate = true
indicator.fraction(null)
val context = UserDataHolderBase()
if (indicator.isCanceled) return
if (!coroutineContext.isActive) return@reportRawProgress
indicator.text = PyBundle.message("looking.for.inner.venvs")
indicator.text(PyBundle.message("looking.for.inner.venvs"))
LOGGER.debug("Looking for inner virtual environments")
guardIndicator(indicator) {
detectAssociatedEnvironments(module, emptyList(), context).filter { it.isLocatedInsideModule(module) }.takeIf { it.isNotEmpty() }
}?.let {
detectAssociatedEnvironments(module, emptyList(), context).filter { it.isLocatedInsideModule(module) }.takeIf { it.isNotEmpty() }?.let {
runInEdt { it.forEach { module.excludeInnerVirtualEnv(it) } }
}
if (!TrustedProjects.isProjectTrusted(project)) {
// com.jetbrains.python.inspections.PyInterpreterInspection will ask for confirmation
LOGGER.info("Python interpreter has not been configured since project is not trusted")
return
return@reportRawProgress
}
val existingSdks = ProjectSdksModel().apply { reset(project) }.sdks.filter { it.sdkType is PythonSdkType }
if (indicator.isCanceled) return
if (!coroutineContext.isActive) return@reportRawProgress
indicator.text = PyBundle.message("looking.for.previous.interpreter")
indicator.text(PyBundle.message("looking.for.previous.interpreter"))
LOGGER.debug("Looking for the previously used interpreter")
guardIndicator(indicator) { mostPreferred(filterAssociatedSdks(module, existingSdks)) }?.let {
mostPreferred(filterAssociatedSdks(module, existingSdks))?.let {
LOGGER.debug { "The previously used interpreter: $it" }
setReadyToUseSdk(project, module, it)
return
return@reportRawProgress
}
if (indicator.isCanceled) return
if (!coroutineContext.isActive) return@reportRawProgress
indicator.text = PyBundle.message("looking.for.related.venv")
indicator.text(PyBundle.message("looking.for.related.venv"))
LOGGER.debug("Looking for a virtual environment related to the project")
guardIndicator(indicator) { detectAssociatedEnvironments(module, existingSdks, context).firstOrNull() }?.let {
detectAssociatedEnvironments(module, existingSdks, context).firstOrNull()?.let {
LOGGER.debug { "Detected virtual environment related to the project: $it" }
val newSdk = it.setupAssociated(existingSdks, module.basePath, true).orLogException(LOGGER) ?: return
@@ -144,52 +144,52 @@ fun configureSdk(
setReadyToUseSdk(project, module, newSdk)
}
return
return@reportRawProgress
}
if (indicator.isCanceled) return
if (!coroutineContext.isActive) return@reportRawProgress
if (extension != null) {
indicator.text = ""
indicator.text("")
setSdkUsingExtension(module, extension) { extension.createAndAddSdkForConfigurator(module) }
return
return@reportRawProgress
}
if (indicator.isCanceled) return
if (!coroutineContext.isActive) return@reportRawProgress
if (PyCondaSdkCustomizer.instance.suggestSharedCondaEnvironments) {
indicator.text = PyBundle.message("looking.for.shared.conda.environment")
guardIndicator(indicator) { mostPreferred(filterSharedCondaEnvs(module, existingSdks)) }?.let {
indicator.text(PyBundle.message("looking.for.shared.conda.environment"))
mostPreferred(filterSharedCondaEnvs(module, existingSdks))?.let {
setReadyToUseSdk(project, module, it)
return
return@reportRawProgress
}
if (indicator.isCanceled) return
if (!coroutineContext.isActive) return@reportRawProgress
}
indicator.text = PyBundle.message("looking.for.default.interpreter")
indicator.text(PyBundle.message("looking.for.default.interpreter"))
LOGGER.debug("Looking for the default interpreter setting for a new project")
guardIndicator(indicator) { getDefaultProjectSdk() }?.let {
getDefaultProjectSdk()?.let {
LOGGER.debug { "Default interpreter setting for a new project: $it" }
setReadyToUseSdk(project, module, it)
return
return@reportRawProgress
}
if (indicator.isCanceled) return
if (!coroutineContext.isActive) return@reportRawProgress
indicator.text = PyBundle.message("looking.for.previous.system.interpreter")
indicator.text(PyBundle.message("looking.for.previous.system.interpreter"))
LOGGER.debug("Looking for the previously used system-wide interpreter")
guardIndicator(indicator) { mostPreferred(filterSystemWideSdks(existingSdks)) }?.let {
mostPreferred(filterSystemWideSdks(existingSdks))?.let {
LOGGER.debug { "Previously used system-wide interpreter: $it" }
setReadyToUseSdk(project, module, it)
return
return@reportRawProgress
}
if (indicator.isCanceled) return
if (!coroutineContext.isActive) return@reportRawProgress
indicator.text = PyBundle.message("looking.for.system.interpreter")
indicator.text(PyBundle.message("looking.for.system.interpreter"))
LOGGER.debug("Looking for a system-wide interpreter")
guardIndicator(indicator) { detectSystemWideSdks(module, existingSdks, context).firstOrNull() }?.let {
detectSystemWideSdks(module, existingSdks, context).firstOrNull()?.let {
LOGGER.debug { "Detected system-wide interpreter: $it" }
runInEdt {
SdkConfigurationUtil.createAndAddSDK(it.homePath!!, PythonSdkType.getInstance())?.apply {
@@ -2,7 +2,6 @@
package com.intellij.pycharm.community.ide.impl.configuration
import com.intellij.codeInspection.util.IntentionName
import com.intellij.execution.ExecutionException
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.runInEdt
@@ -31,18 +30,24 @@ import com.intellij.webcore.packaging.PackageManagementService
import com.intellij.webcore.packaging.PackagesNotificationPanel
import com.jetbrains.python.PyBundle
import com.jetbrains.python.configuration.PyConfigurableInterpreterList
import com.jetbrains.python.getOrNull
import com.jetbrains.python.onFailure
import com.jetbrains.python.packaging.conda.environmentYml.CondaEnvironmentYmlSdkUtils
import com.jetbrains.python.pathValidation.PlatformAndRoot
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.PythonSdkUpdater
import com.jetbrains.python.sdk.basePath
import com.jetbrains.python.sdk.conda.PyCondaSdkCustomizer
import com.jetbrains.python.sdk.conda.createCondaSdkAlongWithNewEnv
import com.jetbrains.python.sdk.conda.execution.CondaExecutor
import com.jetbrains.python.sdk.conda.suggestCondaPath
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension
import com.jetbrains.python.sdk.findAmongRoots
import com.jetbrains.python.sdk.flavors.conda.CondaEnvSdkFlavor
import com.jetbrains.python.sdk.flavors.conda.NewCondaEnvRequest
import com.jetbrains.python.sdk.flavors.conda.PyCondaCommand
import com.jetbrains.python.sdk.flavors.listCondaEnvironments
import com.jetbrains.python.sdk.setAssociationToModuleAsync
import com.jetbrains.python.util.ShowingMessageErrorSync
import kotlinx.coroutines.Dispatchers
import java.awt.BorderLayout
import java.nio.file.Path
@@ -56,23 +61,20 @@ private val LOGGER = Logger.getInstance(PyEnvironmentYmlSdkConfiguration::class.
* TODO: Support remote target (ie \\wsl)
*/
internal class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExtension {
@RequiresBackgroundThread
override fun createAndAddSdkForConfigurator(module: Module): Sdk? = createAndAddSdk(module, Source.CONFIGURATOR)
override suspend fun createAndAddSdkForConfigurator(module: Module): Sdk? = createAndAddSdk(module, Source.CONFIGURATOR)
override fun getIntention(module: Module): @IntentionName String? = getEnvironmentYml(module)?.let {
override suspend fun getIntention(module: Module): @IntentionName String? = getEnvironmentYml(module)?.let {
PyCharmCommunityCustomizationBundle.message("sdk.create.condaenv.suggestion")
}
@RequiresBackgroundThread
override fun createAndAddSdkForInspection(module: Module): Sdk? = createAndAddSdk(module, Source.INSPECTION)
override suspend fun createAndAddSdkForInspection(module: Module): Sdk? = createAndAddSdk(module, Source.INSPECTION)
private fun getEnvironmentYml(module: Module) = listOf(
CondaEnvironmentYmlSdkUtils.ENV_YAML_FILE_NAME,
CondaEnvironmentYmlSdkUtils.ENV_YML_FILE_NAME,
).firstNotNullOfOrNull { findAmongRoots(module, it) }
@RequiresBackgroundThread
private fun createAndAddSdk(module: Module, source: Source): Sdk? {
private suspend fun createAndAddSdk(module: Module, source: Source): Sdk? {
val targetConfig = PythonInterpreterTargetEnvironmentFactory.getTargetModuleResidesOn(module)
if (targetConfig != null) {
// Remote targets aren't supported yet
@@ -112,7 +114,7 @@ internal class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExten
return if (permitted) envData else null
}
private fun createAndAddCondaEnv(module: Module, condaExecutable: String, environmentYml: String): Sdk? {
private suspend fun createAndAddCondaEnv(module: Module, condaExecutable: String, environmentYml: String): Sdk? {
ProgressManager.progress(PyBundle.message("python.sdk.creating.conda.environment.sentence"))
LOGGER.debug("Creating conda environment")
@@ -138,15 +140,13 @@ internal class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExten
return if (condaExecutable.isNullOrBlank()) InputData.NOT_FILLED else InputData.SPECIFIED
}
private fun createCondaEnv(project: Project, condaExecutable: String, environmentYml: String): Sdk? {
private suspend fun createCondaEnv(project: Project, condaExecutable: String, environmentYml: String): Sdk? {
val condaEnvironmentsBefore = safelyListCondaEnvironments(project, condaExecutable) ?: return null
val sdk = runBlockingCancellable {
val existingSdks = PyConfigurableInterpreterList.getInstance(project).model.sdks
val newCondaEnvInfo = NewCondaEnvRequest.LocalEnvByLocalEnvironmentFile(Path.of(environmentYml))
PyCondaCommand(condaExecutable, null)
.createCondaSdkAlongWithNewEnv(newCondaEnvInfo, Dispatchers.EDT, existingSdks.toList(), project)
}.getOr {
val existingSdks = PyConfigurableInterpreterList.getInstance(project).model.sdks
val newCondaEnvInfo = NewCondaEnvRequest.LocalEnvByLocalEnvironmentFile(Path.of(environmentYml))
val sdk = PyCondaCommand(condaExecutable, null)
.createCondaSdkAlongWithNewEnv(newCondaEnvInfo, Dispatchers.EDT, existingSdks.toList(), project).getOr {
PySdkConfigurationCollector.logCondaEnv(project, CondaEnvResult.CREATION_FAILURE)
LOGGER.warn("Exception during creating conda environment $it")
PackageManagementService.ErrorDescription.fromMessage(it.error.message)?.let { description ->
@@ -183,15 +183,14 @@ internal class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExten
}
private fun safelyListCondaEnvironments(project: Project, condaExecutable: String): List<String>? {
return try {
listCondaEnvironments(condaExecutable)
}
catch (e: ExecutionException) {
PySdkConfigurationCollector.logCondaEnv(project, CondaEnvResult.LISTING_FAILURE)
LOGGER.warn("Exception during listing conda environments", e)
showSdkExecutionException(null, e, PyCharmCommunityCustomizationBundle.message("sdk.detect.condaenv.exception.dialog.title"))
null
val listEnvs = runBlockingCancellable {
CondaExecutor.listEnvs(Path.of(condaExecutable)).onFailure {
PySdkConfigurationCollector.logCondaEnv(project, CondaEnvResult.LISTING_FAILURE)
it.addMessage(PyCharmCommunityCustomizationBundle.message("sdk.detect.condaenv.exception.dialog.title"))
ShowingMessageErrorSync.emit(it)
}
}
return listEnvs.getOrNull()?.envs?.toList()
}
private class Dialog(module: Module, condaBinary: VirtualFile?, environmentYml: VirtualFile) : DialogWrapper(module.project, false,
@@ -5,11 +5,11 @@ import com.intellij.codeInspection.util.IntentionName
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.module.Module
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.platform.util.progress.reportRawProgress
import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBundle
import com.intellij.python.hatch.HatchVirtualEnvironment
import com.intellij.python.hatch.cli.HatchEnvironment
import com.intellij.python.hatch.getHatchService
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.jetbrains.python.getOrLogException
import com.jetbrains.python.hatch.sdk.createSdk
import com.jetbrains.python.orLogException
@@ -21,12 +21,10 @@ internal class PyHatchSdkConfiguration : PyProjectSdkConfigurationExtension {
private val LOGGER = Logger.getInstance(PyHatchSdkConfiguration::class.java)
}
override fun getIntention(module: Module): @IntentionName String? {
val isReadyAndHaveOwnership = runWithModalBlockingOrInBackground(
project = module.project,
msg = PyCharmCommunityCustomizationBundle.message("sdk.set.up.hatch.project.analysis")
) {
val hatchService = module.getHatchService().getOr { return@runWithModalBlockingOrInBackground false }
override suspend fun getIntention(module: Module): @IntentionName String? {
val isReadyAndHaveOwnership = reportRawProgress {
it.text(PyCharmCommunityCustomizationBundle.message("sdk.set.up.hatch.project.analysis"))
val hatchService = module.getHatchService().getOr { return@reportRawProgress false }
hatchService.isHatchManagedProject().getOrLogException(LOGGER) == true
}
@@ -49,10 +47,9 @@ internal class PyHatchSdkConfiguration : PyProjectSdkConfigurationExtension {
val sdk = hatchVenv.createSdk(hatchService.getWorkingDirectoryPath(), module).orLogException(LOGGER)
sdk
}
@RequiresBackgroundThread
override fun createAndAddSdkForConfigurator(module: Module): Sdk? = createSdk(module)
override suspend fun createAndAddSdkForConfigurator(module: Module): Sdk? = createSdk(module)
override fun createAndAddSdkForInspection(module: Module): Sdk? = createSdk(module)
override suspend fun createAndAddSdkForInspection(module: Module): Sdk? = createSdk(module)
override fun supportsHeadlessModel(): Boolean = true
}
@@ -6,7 +6,6 @@ import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.application.EDT
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.module.Module
import com.intellij.openapi.progress.runBlockingCancellable
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
@@ -20,7 +19,6 @@ import com.intellij.pycharm.community.ide.impl.configuration.PySdkConfigurationC
import com.intellij.pycharm.community.ide.impl.configuration.PySdkConfigurationCollector.Source
import com.intellij.ui.IdeBorderFactory
import com.intellij.ui.components.JBLabel
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.intellij.util.ui.JBUI
import com.jetbrains.python.PyBundle
import com.jetbrains.python.errorProcessing.PyResult
@@ -46,14 +44,11 @@ internal class PyPipfileSdkConfiguration : PyProjectSdkConfigurationExtension {
private val LOGGER = Logger.getInstance(PyPipfileSdkConfiguration::class.java)
@RequiresBackgroundThread
override fun createAndAddSdkForConfigurator(module: Module): Sdk? = runBlockingCancellable { createAndAddSDk(module, Source.CONFIGURATOR) }
override suspend fun createAndAddSdkForConfigurator(module: Module): Sdk? = createAndAddSDk(module, Source.CONFIGURATOR)
@RequiresBackgroundThread
override fun getIntention(module: Module): @IntentionName String? = findAmongRoots(module, PIP_FILE)?.let { PyCharmCommunityCustomizationBundle.message("sdk.create.pipenv.suggestion", it.name) }
override suspend fun getIntention(module: Module): @IntentionName String? = findAmongRoots(module, PIP_FILE)?.let { PyCharmCommunityCustomizationBundle.message("sdk.create.pipenv.suggestion", it.name) }
@RequiresBackgroundThread
override fun createAndAddSdkForInspection(module: Module): Sdk? = runBlockingCancellable { createAndAddSDk(module, Source.INSPECTION) }
override suspend fun createAndAddSdkForInspection(module: Module): Sdk? = createAndAddSDk(module, Source.INSPECTION)
private suspend fun createAndAddSDk(module: Module, source: Source): Sdk? {
val pipEnvExecutable = askForEnvData(module, source) ?: return null
@@ -6,19 +6,19 @@ import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.EDT
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.module.Module
import com.intellij.openapi.progress.runBlockingCancellable
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.intellij.platform.util.progress.reportRawProgress
import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBundle
import com.intellij.python.community.impl.poetry.poetryPath
import com.intellij.ui.IdeBorderFactory
import com.intellij.ui.components.JBLabel
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.intellij.util.ui.JBUI
import com.jetbrains.python.PyBundle
import com.jetbrains.python.errorProcessing.PyResult
@@ -30,7 +30,6 @@ import com.jetbrains.python.sdk.findAmongRoots
import com.jetbrains.python.sdk.poetry.*
import com.jetbrains.python.sdk.poetry.ui.PyAddNewPoetryFromFilePanel
import com.jetbrains.python.sdk.setAssociationToModule
import com.jetbrains.python.util.runWithModalBlockingOrInBackground
import com.jetbrains.python.venvReader.VirtualEnvReader
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -45,26 +44,26 @@ internal class PyPoetrySdkConfiguration : PyProjectSdkConfigurationExtension {
private val LOGGER = Logger.getInstance(PyPoetrySdkConfiguration::class.java)
}
override fun getIntention(module: Module): String? =
runWithModalBlockingOrInBackground(module.project, PyBundle.message("python.sdk.validating.environment")) {
val toml = findAmongRoots(module, PY_PROJECT_TOML)
if (toml == null) {
return@runWithModalBlockingOrInBackground null
}
val isPoetry = getPyProjectTomlForPoetry(toml) != null
if (!isPoetry) {
return@runWithModalBlockingOrInBackground null
}
return@runWithModalBlockingOrInBackground PyCharmCommunityCustomizationBundle.message("sdk.set.up.poetry.environment", toml.name)
@NlsSafe
override suspend fun getIntention(module: Module): String? = reportRawProgress {
it.text(PyBundle.message("python.sdk.validating.environment"))
val toml = findAmongRoots(module, PY_PROJECT_TOML)
if (toml == null) {
return@reportRawProgress null
}
@RequiresBackgroundThread
override fun createAndAddSdkForConfigurator(module: Module): Sdk? = runBlockingCancellable { createAndAddSDk(module, false) }
val isPoetry = getPyProjectTomlForPoetry(toml) != null
if (!isPoetry) {
return@reportRawProgress null
}
@RequiresBackgroundThread
override fun createAndAddSdkForInspection(module: Module): Sdk? = runBlockingCancellable { createAndAddSDk(module, true) }
return@reportRawProgress PyCharmCommunityCustomizationBundle.message("sdk.set.up.poetry.environment", toml.name)
}
override suspend fun createAndAddSdkForConfigurator(module: Module): Sdk? = createAndAddSDk(module, false)
override suspend fun createAndAddSdkForInspection(module: Module): Sdk? = createAndAddSDk(module, true)
override fun supportsHeadlessModel(): Boolean = true
@@ -25,9 +25,9 @@ import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBund
import com.intellij.pycharm.community.ide.impl.configuration.PySdkConfigurationCollector.InputData
import com.intellij.pycharm.community.ide.impl.configuration.PySdkConfigurationCollector.Source
import com.intellij.pycharm.community.ide.impl.configuration.PySdkConfigurationCollector.VirtualEnvResult
import com.intellij.pycharm.community.ide.impl.configuration.ui.PyAddNewVirtualEnvFromFilePanel
import com.intellij.ui.IdeBorderFactory
import com.intellij.ui.components.JBLabel
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.intellij.util.ui.JBUI
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PySdkBundle
@@ -35,7 +35,6 @@ import com.jetbrains.python.packaging.PyPackageManager
import com.jetbrains.python.packaging.PyPackageUtil
import com.jetbrains.python.packaging.PyTargetEnvironmentPackageManager
import com.jetbrains.python.requirements.RequirementsFileType
import com.intellij.pycharm.community.ide.impl.configuration.ui.PyAddNewVirtualEnvFromFilePanel
import com.jetbrains.python.sdk.basePath
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension
import com.jetbrains.python.sdk.configuration.createVirtualEnvAndSdkSynchronously
@@ -50,14 +49,12 @@ import kotlin.io.path.Path
private val LOGGER = fileLogger()
class PyRequirementsTxtOrSetupPySdkConfiguration : PyProjectSdkConfigurationExtension {
@RequiresBackgroundThread
override fun createAndAddSdkForConfigurator(module: Module): Sdk? = createAndAddSdk(module, Source.CONFIGURATOR)
override suspend fun createAndAddSdkForConfigurator(module: Module): Sdk? = createAndAddSdk(module, Source.CONFIGURATOR)
override fun getIntention(module: Module): @IntentionName String? =
override suspend fun getIntention(module: Module): @IntentionName String? =
getRequirementsTxtOrSetupPy(module)?.let { PyCharmCommunityCustomizationBundle.message("sdk.create.venv.suggestion", it.name) }
@RequiresBackgroundThread
override fun createAndAddSdkForInspection(module: Module): Sdk? = createAndAddSdk(module, Source.INSPECTION)
override suspend fun createAndAddSdkForInspection(module: Module): Sdk? = createAndAddSdk(module, Source.INSPECTION)
private fun createAndAddSdk(module: Module, source: Source): Sdk? {
val existingSdks = ProjectJdkTable.getInstance().allJdks.asList()
@@ -5,14 +5,12 @@ import com.intellij.codeInspection.util.IntentionName
import com.intellij.openapi.application.EDT
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.module.Module
import com.intellij.openapi.progress.runBlockingCancellable
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
import com.intellij.openapi.util.registry.Registry
import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBundle
import com.intellij.python.pyproject.PY_PROJECT_TOML
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.jetbrains.python.errorProcessing.MessageError
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.onSuccess
@@ -31,30 +29,21 @@ class PyUvSdkConfiguration : PyProjectSdkConfigurationExtension {
private val LOGGER = Logger.getInstance(PyUvSdkConfiguration::class.java)
}
@RequiresBackgroundThread
override fun getIntention(module: Module): @IntentionName String? {
override suspend fun getIntention(module: Module): @IntentionName String? {
return findAmongRoots(module, PY_PROJECT_TOML)?.let { toml ->
getUvExecutable()?.let { PyCharmCommunityCustomizationBundle.message("sdk.set.up.uv.environment", toml.name) }
}
}
@RequiresBackgroundThread
override fun createAndAddSdkForConfigurator(module: Module): Sdk? {
return runBlockingCancellable {
createUv(module).getOr {
LOGGER.warn(it.error.message)
return@runBlockingCancellable null
}
}
override suspend fun createAndAddSdkForConfigurator(module: Module): Sdk? = createUv(module).getOr {
LOGGER.warn(it.error.message)
return null
}
@RequiresBackgroundThread
override fun createAndAddSdkForInspection(module: Module): Sdk? {
return runBlockingCancellable {
createUv(module).getOr {
LOGGER.warn(it.error.message)
return@runBlockingCancellable null
}
override suspend fun createAndAddSdkForInspection(module: Module): Sdk? {
return createUv(module).getOr {
LOGGER.warn(it.error.message)
return null
}
}
@@ -1,7 +1,6 @@
// 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.community.junit5Tests.framework.conda
import com.intellij.execution.processTools.getResultStdoutStr
import com.jetbrains.python.getOrThrow
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.sdk.flavors.conda.NewCondaEnvRequest
@@ -20,11 +19,10 @@ suspend fun createCondaEnv(
existingEnv: PyCondaEnv,
pathToCreateNewEnvIn: Path,
): PyCondaEnv {
val process = PyCondaEnv.createEnv(
PyCondaEnv.createEnv(
PyCondaCommand(existingEnv.fullCondaPathOnTarget, null),
NewCondaEnvRequest.EmptyUnnamedEnv(LanguageLevel.PYTHON311, pathToCreateNewEnvIn.pathString)
).getOrThrow()
process.getResultStdoutStr().getOrThrow()
val env = PyCondaEnv(PyCondaEnvIdentity.UnnamedEnv(pathToCreateNewEnvIn.pathString, false), existingEnv.fullCondaPathOnTarget)
return env
@@ -5,6 +5,10 @@ import com.intellij.openapi.diagnostic.Logger
import com.jetbrains.python.Result.Failure
import com.jetbrains.python.Result.Success
import com.jetbrains.python.errorProcessing.MessageError
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.packaging.PyExecutionException
import kotlinx.coroutines.CancellationException
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nls
/**
@@ -113,6 +117,23 @@ sealed class Result<out SUCC, out ERR> {
}
}
@Suppress("UsagesOfObsoleteApi")
@ApiStatus.Internal
fun <T> pyRunCatching(body: () -> T): PyResult<T> {
return try {
PyResult.success(body())
}
catch (t: PyExecutionException) {
PyResult.failure(t.pyError)
}
catch (t: CancellationException) {
throw t
}
catch (t: IllegalArgumentException) {
//Parse deserialization exceptions
PyResult.localizedError(t.localizedMessage)
}
}
/**
* Maps success result to another one with same error
@@ -1192,6 +1192,7 @@ python.add.sdk.panel.name.system.interpreter=System interpreter
python.add.sdk.panel.name.virtualenv.environment=Virtualenv environment
python.add.sdk.conda.executable.path.is.empty=Conda executable path is empty
python.add.sdk.conda.executable.path.is.not.found=Conda executable is not found
python.requirements.version.label=Version in requirements:
python.requirements.version.separator.no.version=Don't specify version
@@ -4,6 +4,7 @@ package com.jetbrains.python.sdk.configuration
import com.intellij.codeInspection.util.IntentionName
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.module.Module
import com.intellij.openapi.progress.runBlockingMaybeCancellable
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import org.jetbrains.annotations.ApiStatus
@@ -23,8 +24,10 @@ interface PyProjectSdkConfigurationExtension {
val EP_NAME = ExtensionPointName.create<PyProjectSdkConfigurationExtension>("Pythonid.projectSdkConfigurationExtension")
@JvmStatic
fun findForModule(module: Module): Pair<@IntentionName String, PyProjectSdkConfigurationExtension>? =
EP_NAME.computeSafeIfAny { ext -> ext.getIntention(module)?.let { Pair(it, ext) } }
@RequiresBackgroundThread
fun findForModule(module: Module): Pair<@IntentionName String, PyProjectSdkConfigurationExtension>? = runBlockingMaybeCancellable {
EP_NAME.extensionsIfPointIsRegistered.firstNotNullOfOrNull { ext -> ext.getIntention(module)?.let { Pair(it, ext) } }
}
}
/**
@@ -33,8 +36,15 @@ interface PyProjectSdkConfigurationExtension {
*
* Rule of thumb is to explicitly ask a user if sdk creation is desired and allowed.
*/
@RequiresBackgroundThread
fun createAndAddSdkForConfigurator(module: Module): Sdk?
suspend fun createAndAddSdkForConfigurator(module: Module): Sdk?
/**
* An implementation is responsible for interpreter setup and registration in IDE.
* In case of failures `null` should be returned, the implementation is responsible for errors displaying.
*
* You're free here to create sdk immediately, without any user permission since quick fix is explicitly clicked.
*/
suspend fun createAndAddSdkForInspection(module: Module): Sdk?
/**
* Called by sdk configurator and interpreter inspection
@@ -51,16 +61,7 @@ interface PyProjectSdkConfigurationExtension {
* Example: `Create a virtual environment using requirements.txt`.
*/
@IntentionName
fun getIntention(module: Module): String?
/**
* An implementation is responsible for interpreter setup and registration in IDE.
* In case of failures `null` should be returned, the implementation is responsible for errors displaying.
*
* You're free here to create sdk immediately, without any user permission since quick fix is explicitly clicked.
*/
@RequiresBackgroundThread
fun createAndAddSdkForInspection(module: Module): Sdk?
suspend fun getIntention(module: Module): String?
/**
* If headless supported implementation is responsible for interpreter setup and registration
@@ -1,91 +0,0 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.sdk.flavors
import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import com.google.gson.annotations.SerializedName
import com.intellij.execution.ExecutionException
import com.intellij.execution.RunCanceledByUserException
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.CapturingProcessHandler
import com.intellij.execution.process.ProcessOutput
import com.intellij.openapi.progress.EmptyProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.text.StringUtil
import com.jetbrains.python.PySdkBundle
import com.jetbrains.python.packaging.IndicatedProcessOutputListener
import com.jetbrains.python.packaging.PyCondaPackageService
import com.jetbrains.python.packaging.PyExecutionException
import com.jetbrains.python.packaging.getCondaBasePython
import com.jetbrains.python.sdk.PySdkUtil
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
@Deprecated("Use Sdk.configureBuilderToRunPythonOnTarget")
@Throws(ExecutionException::class)
fun runConda(condaExecutable: String, arguments: List<String>): ProcessOutput {
return run(condaExecutable, arguments, readCondaEnv(condaExecutable))
}
@ApiStatus.Internal
@Deprecated("Use Sdk.configureBuilderToRunPythonOnTarget")
@Throws(ExecutionException::class)
fun runConda(sdk: Sdk?, arguments: List<String>): ProcessOutput {
val condaExecutable = findCondaExecutable(sdk)
val environment = if (sdk != null) {
PySdkUtil.activateVirtualEnv(sdk)
}
else {
readCondaEnv(condaExecutable)
}
return run(condaExecutable, arguments, environment)
}
private fun run(executable: String, arguments: List<String>, env: Map<String, String>?): ProcessOutput {
val commandLine = GeneralCommandLine(executable).withParameters(arguments).withEnvironment(env)
val indicator = ProgressManager.getInstance().progressIndicator ?: EmptyProgressIndicator()
val handler = CapturingProcessHandler(commandLine).apply {
addProcessListener(IndicatedProcessOutputListener(indicator))
}
return handler.runProcessWithProgressIndicator(indicator).apply {
if (isCancelled) throw RunCanceledByUserException()
checkExitCode(executable, arguments)
}
}
private fun readCondaEnv(condaExecutable: String): Map<String, String>? {
return getCondaBasePython(condaExecutable)?.let { PySdkUtil.activateVirtualEnv(it) }
}
@Throws(ExecutionException::class)
private fun findCondaExecutable(sdk: Sdk?): String {
return PyCondaPackageService.getCondaExecutable(sdk?.homePath) ?: throw ExecutionException(
PySdkBundle.message("python.sdk.flavor.cannot.find.conda"))
}
@Throws(PyExecutionException::class)
private fun ProcessOutput.checkExitCode(executable: String, arguments: List<String>) {
if (exitCode != 0) {
val message = if (StringUtil.isEmptyOrSpaces(stdout) && StringUtil.isEmptyOrSpaces(stderr))
PySdkBundle.message("python.conda.permission.denied")
else PySdkBundle.message("python.conda.non.zero.exit.code")
throw PyExecutionException(message, executable, arguments, this)
}
}
@ApiStatus.Internal
@Deprecated("Use PyCondaEnv")
@Throws(ExecutionException::class, JsonSyntaxException::class)
fun listCondaEnvironments(condaExecutable: String): List<String> {
val output = runConda(condaExecutable, listOf("env", "list", "--json"))
val text = output.stdout
val envList = Gson().fromJson(text, CondaEnvironmentsList::class.java)
return envList.envs
}
private data class CondaEnvironmentsList(@SerializedName("envs") var envs: List<String>)
@@ -1,7 +1,6 @@
package com.intellij.python.community.testFramework.testEnv.conda
import com.intellij.execution.processTools.getResultStdout
import com.intellij.execution.target.local.LocalTargetEnvironmentRequest
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.python.community.testFramework.testEnv.PythonType
@@ -9,7 +8,6 @@ import com.jetbrains.python.PythonBinary
import com.jetbrains.python.getOrThrow
import com.jetbrains.python.packaging.PyCondaPackageService
import com.jetbrains.python.packaging.findCondaExecutableRelativeToEnv
import com.jetbrains.python.sdk.conda.TargetEnvironmentRequestCommandExecutor
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
import kotlinx.coroutines.Dispatchers
@@ -52,8 +50,8 @@ data object TypeConda : PythonType<PyCondaEnv>("conda") {
}
private suspend fun getCondaNames(condaPath: Path) =
PyCondaEnv.getEnvs(TargetEnvironmentRequestCommandExecutor(LocalTargetEnvironmentRequest()),
condaPath.toString()).getOrThrow()
PyCondaEnv.getEnvs(
condaPath.toString()).getOrThrow()
.map { it.envIdentity.userReadableName }
.toMutableSet()
}
@@ -138,7 +138,6 @@ public final class PyInterpreterInspection extends PyInspection {
if (fixData != null) {
fixes.add(fixData.getQuickFix());
// noinspection HardCodedStringLiteral
registerProblemWithCommonFixes(node, fixData.getMessage(), module, sdk, fixes, pyCharm);
return;
}
@@ -493,8 +492,7 @@ public final class PyInterpreterInspection extends PyInspection {
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
PyProjectSdkConfiguration.INSTANCE.configureSdkUsingExtension(myModule, myExtension,
() -> myExtension.createAndAddSdkForInspection(myModule));
PyProjectSdkConfiguration.INSTANCE.configureSdkUsingExtension(myModule, myExtension);
}
@Override
@@ -1,29 +0,0 @@
// 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.packaging.conda
import com.intellij.execution.ExecutionException
import com.jetbrains.python.PySdkBundle
import com.jetbrains.python.packaging.PyExecutionException
import com.jetbrains.python.sdk.flavors.runConda
import com.jetbrains.python.venvReader.VirtualEnvReader.Companion.Instance
import java.io.File
import java.nio.file.Path
object CondaEnvCreator {
@Throws(ExecutionException::class)
fun createVirtualEnv(
condaExecutable: String?, destinationDir: String,
version: String,
): String {
if (condaExecutable == null) {
throw PyExecutionException(PySdkBundle.message("python.sdk.conda.dialog.cannot.find.conda"))
}
val parameters = listOf("create", "-p", destinationDir, "-y", "python=" + version)
runConda(condaExecutable, parameters)
val binary = Instance.findPythonInPythonRoot(Path.of(destinationDir))
val binaryFallback = destinationDir + File.separator + "bin" + File.separator + "python"
return binary?.toString() ?: binaryFallback
}
}
@@ -71,7 +71,7 @@ internal class CondaPackageCache : PythonPackageCache<String> {
val targetConfig = sdk.targetEnvConfiguration
val targetRequest = targetConfig?.createEnvironmentRequest(project) ?: LocalTargetEnvironmentRequest()
val commandExecutor = TargetEnvironmentRequestCommandExecutor(targetRequest)
val baseConda = PyCondaEnv.getEnvs(commandExecutor, pathOnTarget).getOrThrow()
val baseConda = PyCondaEnv.getEnvs(pathOnTarget).getOrThrow()
.first { it.envIdentity is PyCondaEnvIdentity.UnnamedEnv && it.envIdentity.isBase }
val helpersAware = PythonInterpreterTargetEnvironmentFactory.findPythonTargetInterpreter(sdk, project)
@@ -25,7 +25,7 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
class CondaPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(project, sdk) {
private val condaPackageEngine = CondaPackageManagerEngine(project, sdk)
private val condaPackageEngine = CondaPackageManagerEngine(sdk)
private val condaRepositoryManger = CondaRepositoryManger(project, sdk)
private val pipRepositoryManger = PipRepositoryManager(project)
private val pipPackageEngine = PipPackageManagerEngine(project, sdk)
@@ -33,12 +33,12 @@ class CondaPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(pro
override var repositoryManager: PythonRepositoryManager = CompositePythonRepositoryManager(project,
listOf(condaRepositoryManger, pipRepositoryManger))
override fun getDependencyManager(): PythonDependenciesManager? {
override fun getDependencyManager(): PythonDependenciesManager {
return CondaEnvironmentYmlManager.getInstance(project, sdk)
}
override suspend fun syncCommand(): PyResult<Unit> {
val requirementsFile = getDependencyManager()?.getDependenciesFile()
val requirementsFile = getDependencyManager().getDependenciesFile()
?: return PyResult.localizedError(PyBundle.message("python.sdk.conda.requirements.file.not.found"))
return updateEnv(requirementsFile)
}
@@ -1,147 +1,62 @@
// 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.packaging.conda
import com.intellij.execution.target.TargetProgressIndicator
import com.intellij.execution.target.TargetedCommandLineBuilder
import com.intellij.execution.target.local.LocalTargetEnvironmentRequest
import com.intellij.execution.target.value.targetPath
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.toNioPathOrNull
import com.intellij.platform.eel.provider.asEelPath
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.Result.Companion.success
import com.jetbrains.python.errorProcessing.ExecError
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.errorProcessing.asExecutionFailed
import com.jetbrains.python.errorProcessing.failure
import com.jetbrains.python.packaging.PyExecutionException
import com.jetbrains.python.packaging.common.PythonOutdatedPackage
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification
import com.jetbrains.python.packaging.management.PythonPackageInstallRequest
import com.jetbrains.python.packaging.management.PythonPackageManagerEngine
import com.jetbrains.python.packaging.management.PythonPackageManagerRunner
import com.jetbrains.python.sdk.conda.execution.CondaExecutor
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
import com.jetbrains.python.sdk.flavors.conda.PyCondaFlavorData
import com.jetbrains.python.sdk.getOrCreateAdditionalData
import com.jetbrains.python.sdk.targetEnvConfiguration
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.nio.file.Path
import kotlin.io.path.Path
internal class CondaPackageManagerEngine(
private val project: Project,
private val sdk: Sdk,
) : PythonPackageManagerEngine {
internal class CondaPackageManagerEngine(private val sdk: Sdk) : PythonPackageManagerEngine {
suspend fun updateFromEnvironmentFile(envFile: VirtualFile): PyResult<Unit> {
return runConda("env", listOf("update", "--file", envFile.name, "--prune"),
addCondaEnv = false, workingPath = envFile.parent?.toNioPathOrNull())
.mapSuccess { }
val env = getEnvData()
return CondaExecutor.updateFromEnvironmentFile(env.condaPath, envFile.path)
}
suspend fun exportToEnvironmentFile(): PyResult<String> {
return runConda("export", listOf("--from-history"))
val env = getEnvData()
return CondaExecutor.exportEnvironmentFile(env.condaPath, env.envIdentity)
}
override suspend fun loadOutdatedPackagesCommand(): PyResult<List<PythonOutdatedPackage>> {
val jsonPyResult = runConda("update", listOf("--dry-run", "--all", "--json")
).getOr { return it }
val parsed = withContext(Dispatchers.Default) {
CondaParseUtils.parseOutdatedOutputs(jsonPyResult)
}
return PyResult.success(parsed)
val env = getEnvData()
return CondaExecutor.listOutdatedPackages(env.condaPath, env.envIdentity)
}
override suspend fun installPackageCommand(installRequest: PythonPackageInstallRequest, options: List<String>): PyResult<Unit> {
val installationArgs = installRequest.buildInstallationArguments().getOr { return it }
val result = runConda("install", installationArgs + "-y" + options)
return result.mapSuccess { }
val env = getEnvData()
return CondaExecutor.installPackages(env.condaPath, env.envIdentity, installationArgs, options)
}
override suspend fun updatePackageCommand(vararg specifications: PythonRepositoryPackageSpecification): PyResult<Unit> {
val packages = specifications.map { it.name }
val result = runConda("install", packages + listOf("-y")
)
return result.mapSuccess { }
val env = getEnvData()
return CondaExecutor.installPackages(env.condaPath, env.envIdentity, packages, emptyList())
}
override suspend fun uninstallPackageCommand(vararg pythonPackages: String): PyResult<Unit> {
if (pythonPackages.isEmpty())
return PyResult.success(Unit)
val result = runConda("uninstall", pythonPackages.toList() + listOf("-y")
)
return result.mapSuccess { }
val env = getEnvData()
return CondaExecutor.uninstallPackages(env.condaPath, env.envIdentity, pythonPackages.toList())
}
override suspend fun loadPackagesCommand(): PyResult<List<PythonPackage>> {
val result = runConda("list", emptyList())
return result.mapSuccess {
parseCondaPackageList(it)
}
}
private fun parseCondaPackageList(text: String): List<CondaPackage> {
return text.lineSequence()
.filterNot { it.startsWith("#") }
.map { line -> line.split(listLineParser) }
.filterNot { it.size < 2 }
//TODO: fix
.map { CondaPackage(it[0], it[1], editableMode = false, installedWithPip = (it.size >= 4 && it[3] == "pypi")) }
.sortedWith(compareBy(CondaPackage::name))
.toList()
val env = getEnvData()
return CondaExecutor.listPackages(env.condaPath, env.envIdentity)
}
private suspend fun runConda(
operation: String, arguments: List<String>,
addCondaEnv: Boolean = true,
workingPath: Path? = null,
): PyResult<String> {
return withContext(Dispatchers.IO) {
val targetConfig = sdk.targetEnvConfiguration
val targetReq = targetConfig?.createEnvironmentRequest(project) ?: LocalTargetEnvironmentRequest()
val commandLineBuilder = TargetedCommandLineBuilder(targetReq)
val targetEnv = targetReq.prepareEnvironment(TargetProgressIndicator.EMPTY)
val env = (sdk.getOrCreateAdditionalData().flavorAndData.data as PyCondaFlavorData).env
commandLineBuilder.setExePath(env.fullCondaPathOnTarget)
commandLineBuilder.addParameter(operation)
if (addCondaEnv) {
env.addCondaEnvironmentToTargetBuilder(commandLineBuilder)
}
arguments.forEach(commandLineBuilder::addParameter)
if (workingPath != null) {
val workingDir = targetPath(workingPath)
val appliedWorkingDir = workingDir.apply(targetEnv)
commandLineBuilder.setWorkingDirectory(appliedWorkingDir)
}
val targetedCommandLine = commandLineBuilder.build()
val process = targetEnv.createProcess(targetedCommandLine)
val commandLine = targetedCommandLine.collectCommandsSynchronously()
val commandLineString = StringUtil.join(commandLine, " ")
val result = PythonPackageManagerRunner.runProcess(process, commandLineString)
result.checkSuccess(thisLogger())
if (result.isTimeout) throw PyExecutionException(message("conda.packaging.exception.timeout"), operation, arguments, result)
if (result.exitCode != 0) {
val error = ExecError(Path(env.fullCondaPathOnTarget).asEelPath(), (listOf(operation) + arguments).toTypedArray(), result.asExecutionFailed())
failure(error)
}
else {
success(result.stdout)
}
}
}
private fun getEnvData(): PyCondaEnv = (sdk.getOrCreateAdditionalData().flavorAndData.data as PyCondaFlavorData).env
private fun PythonPackageInstallRequest.buildInstallationArguments(): PyResult<List<String>> = when (this) {
is PythonPackageInstallRequest.ByLocation -> PyResult.localizedError("CondaManager does not support installing from location uri")
@@ -160,6 +75,6 @@ internal class CondaPackageManagerEngine(
}
companion object {
private val listLineParser = "\\s+".toRegex()
}
}
@@ -6,17 +6,18 @@ import com.intellij.openapi.project.Project
import com.intellij.platform.util.coroutines.childScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.jetbrains.annotations.ApiStatus
import kotlin.coroutines.CoroutineContext
@Service(Service.Level.PROJECT)
@ApiStatus.Internal
internal class PyPackageCoroutine(val project: Project, val coroutineScope: CoroutineScope) {
val ioScope = coroutineScope.childScope("Jupyter IO scope", context = Dispatchers.IO)
class PyPackageCoroutine(val project: Project, val coroutineScope: CoroutineScope) {
val ioScope: CoroutineScope = coroutineScope.childScope("Jupyter IO scope", context = Dispatchers.IO)
companion object {
fun launch(project: Project, context: CoroutineContext = Dispatchers.Main, body: suspend CoroutineScope.() -> Unit) =
fun launch(project: Project, context: CoroutineContext = Dispatchers.Main, body: suspend CoroutineScope.() -> Unit): Job =
project.service<PyPackageCoroutine>().coroutineScope.launch(context, block = body)
fun getIoScope(project: Project): CoroutineScope = project.service<PyPackageCoroutine>().ioScope
@@ -26,10 +26,10 @@ import kotlin.time.Duration.Companion.minutes
* @return A [Result] object containing the output of the command execution.
*/
@Internal
suspend fun runExecutableWithProgress(executable: Path, workDir: Path?, timeout: Duration = 10.minutes, vararg args: String): PyExecResult<String> {
suspend fun runExecutableWithProgress(executable: Path, workDir: Path?, timeout: Duration = 10.minutes, env: Map<String,String> = emptyMap(), vararg args: String): PyExecResult<String> {
val ansiDecoder = AnsiEscapeDecoder()
reportRawProgress { reporter ->
return ExecService().execGetStdout(executable, args.toList(), ExecOptions(workingDirectory = workDir, timeout = timeout), procListener = {
return ExecService().execGetStdout(executable, args.toList(), ExecOptions(workingDirectory = workDir, timeout = timeout, env = env), procListener = {
when (it) {
is ProcessEvent.ProcessStarted, is ProcessEvent.ProcessEnded -> Unit
is ProcessEvent.ProcessOutput -> {
@@ -151,10 +151,9 @@ abstract class PythonAddInterpreterModel(
* Returns error or `null` if no error
*/
suspend fun detectCondaEnvironments(): PyResult<Unit> = withContext(Dispatchers.IO) {
val commandExecutor = targetEnvironmentConfiguration.toExecutor()
val fullCondaPathOnTarget = state.condaExecutable.get()
if (fullCondaPathOnTarget.isBlank()) return@withContext PyResult.localizedError(message("python.sdk.conda.no.exec"))
val environments = PyCondaEnv.getEnvs(commandExecutor, fullCondaPathOnTarget).getOr { return@withContext it }
val environments = PyCondaEnv.getEnvs(fullCondaPathOnTarget).getOr { return@withContext it }
val baseConda = environments.find { env -> env.envIdentity.let { it is PyCondaEnvIdentity.UnnamedEnv && it.isBase } }
withContext(Dispatchers.EDT) {
@@ -127,12 +127,7 @@ suspend fun PyCondaCommand.createCondaSdkAlongWithNewEnv(
project: Project,
reporter: RawProgressReporter? = null,
): PyResult<Sdk> {
val process = PyCondaEnv.createEnv(this, newCondaEnvInfo).getOr { return it }
ProcessHandlerReader(process).runProcessAndGetError(uiContext, reporter)?.let {
return PyResult.localizedError(it)
}
PyCondaEnv.createEnv(this, newCondaEnvInfo).getOr { return it }
val sdk = createCondaSdkFromExistingEnv(newCondaEnvInfo.toIdentity(), existingSdks, project)
saveLocalPythonCondaPath(Path.of(this@createCondaSdkAlongWithNewEnv.fullCondaPathOnTarget))
@@ -1,13 +1,31 @@
// 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.packaging.conda
package com.jetbrains.python.sdk.conda.execution
import com.jetbrains.python.packaging.common.PythonOutdatedPackage
import com.jetbrains.python.packaging.conda.CondaPackage
import com.jetbrains.python.sdk.conda.execution.models.CondaEnvInfo
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
internal object CondaParseUtils {
internal object CondaExecutionParser {
private val json = kotlinx.serialization.json.Json { ignoreUnknownKeys = true }
fun parseCondaPackageList(text: String): List<CondaPackage> {
return text.lineSequence()
.filterNot { it.startsWith("#") }
.map { line -> line.split(listLineParser) }
.filterNot { it.size < 2 }
//TODO: fix
.map { CondaPackage(it[0], it[1], editableMode = false, installedWithPip = (it.size >= 4 && it[3] == "pypi")) }
.sortedWith(compareBy(CondaPackage::name))
.toList()
}
fun parseListEnvironmentsOutput(jsonContent: String): CondaEnvInfo {
return json.decodeFromString<CondaEnvInfo>(jsonContent)
}
fun parseOutdatedOutputs(jsonContent: String): List<PythonOutdatedPackage> {
val json = kotlinx.serialization.json.Json { ignoreUnknownKeys = true }
val condaOutput = json.decodeFromString<CondaOutput>(jsonContent)
val prevVersions = condaOutput.actions.unlink.associateBy { it.name }
val newVersions = condaOutput.actions.link.associateBy { it.name }
@@ -33,4 +51,6 @@ internal object CondaParseUtils {
@Serializable
private data class CondaPackageInfo(val name: String, val version: String)
private val listLineParser = "\\s+".toRegex()
}
@@ -0,0 +1,148 @@
// 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.conda.execution
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.platform.eel.isWindows
import com.intellij.platform.eel.provider.getEelDescriptor
import com.intellij.util.EnvReader
import com.jetbrains.python.PyBundle
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.packaging.common.PythonOutdatedPackage
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.pyRunCatching
import com.jetbrains.python.sdk.conda.execution.models.CondaEnvInfo
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
import com.jetbrains.python.sdk.runExecutableWithProgress
import org.jetbrains.annotations.ApiStatus
import java.io.IOException
import java.nio.file.Path
import kotlin.io.path.*
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
@ApiStatus.Internal
object CondaExecutor {
suspend fun createNamedEnv(condaPath: Path, envName: String, pythonVersion: String): PyResult<Unit> {
val args = listOf("create", "-y", "-n", envName, "python=${pythonVersion}")
return runConda(condaPath, args, null).mapSuccess { }
}
suspend fun createUnnamedEnv(condaPath: Path, envPrefix: String, pythonVersion: String): PyResult<Unit> {
val args = listOf("create", "-y", "-p", envPrefix, "python=${pythonVersion}")
return runConda(condaPath, args, null).mapSuccess { }
}
suspend fun createFileEnv(condaPath: Path, environmentYaml: Path): PyResult<Unit> {
val args = listOf("env", "create", "-y", "-f", environmentYaml.pathString)
return runConda(condaPath, args, null).mapSuccess { }
}
suspend fun updateFromEnvironmentFile(condaPath: Path, envYmlPath: String): PyResult<Unit> {
val args = listOf("env", "update", "--file", envYmlPath, "--prune")
return runConda(condaPath, args, null).mapSuccess { }
}
suspend fun listEnvs(condaPath: Path): PyResult<CondaEnvInfo> {
val args = listOf("env", "list", "--json")
val json = runConda(condaPath, args, null).getOr { return it }
return pyRunCatching {
CondaExecutionParser.parseListEnvironmentsOutput(json)
}
}
suspend fun exportEnvironmentFile(condaPath: Path, envIdentity: PyCondaEnvIdentity): PyResult<String> {
return runConda(condaPath, listOf("export") + listOf("--from-history"), envIdentity)
}
suspend fun listPackages(condaPath: Path, envIdentity: PyCondaEnvIdentity): PyResult<List<PythonPackage>> {
return runConda(condaPath, listOf("list"), envIdentity).mapSuccess {
CondaExecutionParser.parseCondaPackageList(it)
}
}
suspend fun installPackages(condaPath: Path, envIdentity: PyCondaEnvIdentity, packages: List<String>, options: List<String>): PyResult<Unit> {
return runConda(condaPath, listOf("install") + packages + listOf("-y") + options, envIdentity).mapSuccess { }
}
suspend fun uninstallPackages(condaPath: Path, envIdentity: PyCondaEnvIdentity, packages: List<String>): PyResult<Unit> {
return runConda(condaPath, listOf("uninstall") + packages + "-y", envIdentity).mapSuccess { }
}
suspend fun listOutdatedPackages(condaPath: Path, envIdentity: PyCondaEnvIdentity): PyResult<List<PythonOutdatedPackage>> {
val jsonPyResult = runConda(condaPath, listOf("update", "--dry-run", "--all", "--json"), envIdentity).getOr {
return it
}
return pyRunCatching {
CondaExecutionParser.parseOutdatedOutputs(jsonPyResult)
}
}
private suspend fun runConda(condaPath: Path, args: List<String>, condaEnvIdentity: PyCondaEnvIdentity?, timeout: Duration = 15.minutes): PyResult<String> {
val condaEnv = when (condaEnvIdentity) {
is PyCondaEnvIdentity.UnnamedEnv -> {
if (condaEnvIdentity.isBase)
listOf()
else
listOf("-p", condaEnvIdentity.envPath)
}
is PyCondaEnvIdentity.NamedEnv -> {
listOf("-n", condaEnvIdentity.envName)
}
null -> emptyList()
}
val envs = getFixedEnvs(condaPath).getOr {
return it
}
val runArgs = (condaEnv + args).toTypedArray()
return runExecutableWithProgress(condaPath, null, timeout, env = envs, *runArgs)
}
private fun getFixedEnvs(condaPath: Path): PyResult<Map<String, String>> {
val osFamily = condaPath.getEelDescriptor().osFamily
if (!osFamily.isWindows) return PyResult.success(emptyMap())
if (!condaPath.exists()) {
return PyResult.localizedError(PyBundle.message("python.add.sdk.conda.executable.path.is.not.found"))
}
val activateBat = condaPath.resolveSibling("activate.bat")
if (!activateBat.isExecutable()) {
thisLogger().warn("$activateBat doesn't exist or can't be read")
return PyResult.success(emptyMap())
}
try {
val envs = EnvReader().readBatEnv(activateBat, emptyList())
val transformed = transformEnvVars(envs, condaPath)
return PyResult.success(transformed)
}
catch (e: IOException) {
thisLogger().warn("Can't read env vars", e)
return PyResult.success(emptyMap())
}
}
/**
* Special fix for conda PATH.
* conda itself runs base python under the hood. When run non-base env, only non-base env gets activated, so conda
* first runs non-activated base python. It loads ``sitecustomize.py`` and it leads to mkl loading that throws warning
* due to path issue (DLL not found).
* We add this folder to conda path
*/
private fun transformEnvVars(envs: Map<String, String>, condaPathOnTarget: Path): Map<String, String> {
val extraPath = condaPathOnTarget.parent?.parent?.resolve("Library")?.resolve("Bin")
if (extraPath == null || !extraPath.exists() || !extraPath.isDirectory()) {
thisLogger().warn("$extraPath doesn't exist")
}
thisLogger().info("Patching envs")
return envs.map { (key, value) ->
val fixedVal = if (key.equals("Path", ignoreCase = true) && extraPath != null) {
value + ";" + extraPath.absolutePathString()
}
else value
key to fixedVal.toString()
}.toMap()
}
}
@@ -0,0 +1,17 @@
// 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.conda.execution.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
@Serializable
data class CondaEnvInfo(
@SerialName("envs")
val envs: List<String>,
@SerialName("envs_dirs")
val envsDirs: List<String>,
@SerialName("conda_prefix")
val condaPrefix: String,
)
@@ -10,21 +10,20 @@ import com.intellij.openapi.Disposable
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.module.Module
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task.Backgroundable
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.isNotificationSilentMode
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.util.use
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PySdkBundle
import com.jetbrains.python.PythonPluginDisposable
import com.jetbrains.python.inspections.PyInspectionExtension
import com.jetbrains.python.inspections.requirement.RunningPackagingTasksListener
import com.jetbrains.python.packaging.utils.PyPackageCoroutine
import com.jetbrains.python.projectModel.uv.UvProjectModelService
import com.jetbrains.python.psi.PyFile
import com.jetbrains.python.sdk.PySdkPopupFactory
@@ -35,21 +34,22 @@ object PyProjectSdkConfiguration {
private val LOGGER = Logger.getInstance(PyProjectSdkConfiguration::class.java)
fun configureSdkUsingExtension(module: Module, extension: PyProjectSdkConfigurationExtension, supplier: () -> Sdk?) {
fun configureSdkUsingExtension(module: Module, extension: PyProjectSdkConfigurationExtension) {
val lifetime = suppressTipAndInspectionsFor(module, extension)
ProgressManager.getInstance().run(
object : Backgroundable(module.project, PySdkBundle.message("python.configuring.interpreter.progress"), false) {
override fun run(indicator: ProgressIndicator) {
indicator.isIndeterminate = true
lifetime.use { setSdkUsingExtension(module, extension, supplier) }
val project = module.project
PyPackageCoroutine.launch(project) {
withBackgroundProgress(project, PySdkBundle.message("python.configuring.interpreter.progress"), false) {
lifetime.use {
setSdkUsingExtension(module, extension) {
extension.createAndAddSdkForInspection(module)
}
}
}
)
}
}
@RequiresBackgroundThread
fun setSdkUsingExtension(module: Module, extension: PyProjectSdkConfigurationExtension, supplier: () -> Sdk?) {
suspend fun setSdkUsingExtension(module: Module, extension: PyProjectSdkConfigurationExtension, supplier: suspend () -> Sdk?) {
ProgressManager.progress("")
LOGGER.debug("Configuring sdk with ${extension.javaClass.canonicalName} extension")
@@ -60,7 +60,7 @@ object PyProjectSdkConfiguration {
val ws = UvProjectModelService.findWorkspace(module)
if (ws != null) {
for (wsModule in ws.members + ws.root) {
setReadyToUseSdk(wsModule.project, wsModule, sdk)
setReadyToUseSdk(wsModule.project, wsModule, sdk)
}
return
}
@@ -173,7 +173,7 @@ private class PyInterpreterInspectionSuppressor : PyInspectionExtension() {
}
}
private class PyPackageRequirementsInspectionSuppressor(module: Module): Disposable {
private class PyPackageRequirementsInspectionSuppressor(module: Module) : Disposable {
private val listener = RunningPackagingTasksListener(module)
@@ -10,6 +10,8 @@ import com.jetbrains.python.pathValidation.PlatformAndRoot.Companion.getPlatform
import com.jetbrains.python.pathValidation.ValidationRequest
import com.jetbrains.python.pathValidation.validateExecutableFile
import org.jetbrains.annotations.ApiStatus
import java.nio.file.Path
import kotlin.io.path.Path
/**
* Encapsulates conda binary command to simplify target request creation
@@ -22,6 +24,7 @@ class PyCondaCommand(
internal val project: Project? = null,
internal val indicator: TargetProgressIndicator = TargetProgressIndicator.EMPTY
) {
fun getCondaPath(): Path = Path(fullCondaPathOnTarget)
@RequiresBackgroundThread
private fun createRequest(): PyResult<TargetEnvironmentRequest> {
@@ -4,26 +4,24 @@ package com.jetbrains.python.sdk.flavors.conda
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.google.gson.Gson
import com.intellij.execution.target.FullPathOnTarget
import com.intellij.execution.target.TargetEnvironmentConfiguration
import com.intellij.execution.target.TargetedCommandLineBuilder
import com.intellij.execution.target.createProcessWithResult
import com.intellij.openapi.projectRoots.Sdk
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.errorProcessing.asPythonResult
import com.jetbrains.python.psi.LanguageLevel
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.Dispatchers
import kotlinx.coroutines.future.await
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.NonNls
import java.nio.file.Path
import java.util.*
import kotlin.io.path.Path
import kotlin.io.path.exists
import kotlin.io.path.name
import kotlin.io.path.pathString
/**
* TODO: Once we get rid of [TargetCommandExecutor] and have access to [com.intellij.execution.target.TargetEnvironmentConfiguration] use it validate conda binary in [getEnvs]
@@ -35,78 +33,54 @@ data class PyCondaEnv(
val envIdentity: PyCondaEnvIdentity,
val fullCondaPathOnTarget: FullPathOnTarget,
) {
val condaPath: Path
get() = Path(fullCondaPathOnTarget)
companion object {
/**
* @return unparsed output of conda info --envs --json
*/
private suspend fun getEnvsInfo(command: TargetCommandExecutor, fullCondaPathOnTarget: FullPathOnTarget): PyResult<String> {
val output = command.execute(listOf(fullCondaPathOnTarget, "info", "--envs", "--json")).await()
return if (output.exitCode == 0) PyResult.success(output.stdout) else PyResult.localizedError(output.stderr)
}
/**
* @return list of conda's envs_dirs directories
*/
@ApiStatus.Internal
suspend fun getEnvsDirs(
command: TargetCommandExecutor,
fullCondaPathOnTarget: FullPathOnTarget,
): PyResult<Collection<String>> = withContext(Dispatchers.IO) {
val json = getEnvsInfo(command, fullCondaPathOnTarget).getOr { return@withContext it }
runCatching { // External command may return junk
val info = Gson().fromJson(json, CondaInfoJson::class.java)
info.envs_dirs
}.asPythonResult()
): PyResult<Collection<String>> {
val info = CondaExecutor.listEnvs(Path(fullCondaPathOnTarget)).getOr { return it }
return PyResult.success(info.envsDirs)
}
/**
* @return list of conda environments
*/
@ApiStatus.Internal
suspend fun getEnvs(
command: TargetCommandExecutor,
fullCondaPathOnTarget: FullPathOnTarget,
): PyResult<List<PyCondaEnv>> = withContext(Dispatchers.IO) {
val json = getEnvsInfo(command, fullCondaPathOnTarget).getOr { return@withContext it }
return@withContext kotlin.runCatching { // External command may return junk
val info = Gson().fromJson(json, CondaInfoJson::class.java)
val fileSeparator = command.targetPlatform.await().platform.fileSeparator
info.envs.distinctBy { it.trim().lowercase(Locale.getDefault()) }.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 envName = if (info.envs_dirs.any {
if (command.local) Path.of(it) == Path.of(envPath).parent
else envPath.startsWith(it)
}) envPath.split(fileSeparator).last()
else null
val base = envPath.equals(info.conda_prefix, ignoreCase = true)
PyCondaEnv(envName?.let { PyCondaEnvIdentity.NamedEnv(it) } ?: PyCondaEnvIdentity.UnnamedEnv(envPath, base),
fullCondaPathOnTarget)
suspend fun getEnvs(condaPath: String): PyResult<List<PyCondaEnv>> {
val info = CondaExecutor.listEnvs(Path(condaPath)).getOr { return it }
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
}
}.asPythonResult()
val envName = if (isEnvName)
Path.of(envPath).name
else
null
val base = envPath.equals(info.condaPrefix, ignoreCase = true)
val identity = if (envName != null) {
PyCondaEnvIdentity.NamedEnv(envName)
}
else {
PyCondaEnvIdentity.UnnamedEnv(envPath, base)
}
PyCondaEnv(identity, condaPath)
}
return PyResult.success(identities)
}
suspend fun createEnv(command: PyCondaCommand, newCondaEnvInfo: NewCondaEnvRequest): PyResult<Process> {
val (_, env, commandLineBuilder) = withContext(Dispatchers.IO) {
command.createRequestEnvAndCommandLine()
}.getOr { return it }
val commandLine = commandLineBuilder.apply {
//conda create -y -n myenv python=3.9
//addParameters("create", "-y", "-n", envName, *newCondaEnvInfo.createEnvArguments)
addParameters(*newCondaEnvInfo.createEnvArguments)
}.build()
val processResult = env.createProcessWithResult(commandLine)
return processResult.asPythonResult()
suspend fun createEnv(command: PyCondaCommand, newCondaEnvInfo: NewCondaEnvRequest): PyResult<Unit> {
return newCondaEnvInfo.create(command.getCondaPath())
}
}
suspend fun createSdkFromThisEnv(targetConfig: TargetEnvironmentConfiguration?, existingSdk: List<Sdk>): Sdk =
@@ -135,43 +109,10 @@ data class PyCondaEnv(
}
}
/**
* Add conda prefix to [targetedCommandLineBuilder] without specifying the 'run' command
*/
fun addCondaEnvironmentToTargetBuilder(targetedCommandLineBuilder: TargetedCommandLineBuilder) {
targetedCommandLineBuilder.apply {
when (val identity = this@PyCondaEnv.envIdentity) {
is PyCondaEnvIdentity.UnnamedEnv -> {
addParameter("-p")
addParameter(identity.envPath) // TODO: Escape. Shouldn't target have something like "addEscaped"?
}
is PyCondaEnvIdentity.NamedEnv -> {
addParameter("-n")
addParameter(identity.envName)
}
}
targetedCommandLineBuilder.fixCondaPathEnvIfNeeded(fullCondaPathOnTarget)
}
}
override fun toString(): String = "$envIdentity@$fullCondaPathOnTarget"
}
private class CondaInfoJson {
lateinit var envs: Collection<String>
private set
@Suppress("PropertyName") // JSON conda format
lateinit var envs_dirs: Collection<String>
private set
@Suppress("PropertyName") // JSON conda format
lateinit var conda_prefix: String
private set
}
/**
* Request to create new conda environment.
* Conda binary must be run with [createEnvArguments]
@@ -179,22 +120,30 @@ private class CondaInfoJson {
sealed class NewCondaEnvRequest {
abstract val envName: @NonNls String
abstract val createEnvArguments: Array<String>
@ApiStatus.Internal
abstract suspend fun create(condaPath: Path): PyResult<Unit>
/**
* Create empty environment with [langLevel]
*/
class EmptyNamedEnv(langLevel: LanguageLevel, @NonNls override val envName: String) : NewCondaEnvRequest() {
override val createEnvArguments: Array<String> = arrayOf("create", "-y", "-n", envName, "python=${langLevel.toPythonVersion()}")
class EmptyNamedEnv(private val langLevel: LanguageLevel, @get:NonNls override val envName: String) : NewCondaEnvRequest() {
@ApiStatus.Internal
override suspend fun create(condaPath: Path): PyResult<Unit> {
return CondaExecutor.createNamedEnv(condaPath, envName, langLevel.toPythonVersion())
}
}
/**
* Create empty environment with [langlevel] in a specific directory
* Create empty environment with [langLevel] in a specific directory
*/
class EmptyUnnamedEnv(langLevel: LanguageLevel, private val envPrefix: String) : NewCondaEnvRequest() {
class EmptyUnnamedEnv(private val langLevel: LanguageLevel, private val envPrefix: String) : NewCondaEnvRequest() {
override val envName: String get() = envPrefix
override val createEnvArguments: Array<String> = arrayOf("create", "-y", "-p", envPrefix, "python=${langLevel.toPythonVersion()}")
@ApiStatus.Internal
override suspend fun create(condaPath: Path): PyResult<Unit> {
return CondaExecutor.createUnnamedEnv(condaPath, envPrefix, langLevel.toPythonVersion())
}
}
/**
@@ -209,10 +158,13 @@ sealed class NewCondaEnvRequest {
}
private val lazyName = lazy {
ObjectMapper(YAMLFactory()).readValue(environmentYaml.toFile(), ObjectNode::class.java).get("name").asText()
ObjectMapper(YAMLFactory()).readValue(environmentYaml.pathString, ObjectNode::class.java).get("name").asText()
}
override val envName: String get() = lazyName.value
override val createEnvArguments: Array<String> = arrayOf("env", "create", "-y", "-f", environmentYaml.toFile().path)
@ApiStatus.Internal
override suspend fun create(condaPath: Path): PyResult<Unit> {
return CondaExecutor.createFileEnv(condaPath, environmentYaml)
}
}
}
@@ -26,7 +26,7 @@ import kotlin.time.Duration.Companion.minutes
@Internal
suspend fun runPipEnv(dirPath: Path?, vararg args: String): PyResult<String> {
val executable = getPipEnvExecutable().getOr { return it }
return runExecutableWithProgress(executable, dirPath, 10.minutes, *args)
return runExecutableWithProgress(executable, dirPath, 10.minutes, args = args)
}
/**
@@ -99,7 +99,7 @@ suspend fun setupPipEnvSdkUnderProgress(
setUpPipEnv(projectPath, python, installPackages)
}.getOr { return it }
return createSdk(pythonExecutablePath, existingSdks, projectPath, suggestedSdkName(projectPath),PyPipEnvSdkAdditionalData())
return createSdk(pythonExecutablePath, existingSdks, projectPath, suggestedSdkName(projectPath), PyPipEnvSdkAdditionalData())
}
/**
@@ -55,7 +55,7 @@ private val VERSION_2 = "2.0.0".toVersion()
@Internal
suspend fun runPoetry(projectPath: Path?, vararg args: String): PyResult<String> {
val executable = getPoetryExecutable().getOr { return it }
return runExecutableWithProgress(executable, projectPath, 10.minutes, *args)
return runExecutableWithProgress(executable, projectPath, 10.minutes, args = args)
}
@@ -98,7 +98,8 @@ suspend fun validatePoetryExecutable(poetryExecutable: Path?): ValidationInfo? =
*/
@Internal
suspend fun runPoetryWithSdk(sdk: Sdk, vararg args: String): PyResult<String> {
val projectPath = sdk.associatedModulePath?.let { Path.of(it) } ?: return PyResult.localizedError(poetryNotFoundException) // Choose a correct sdk
val projectPath = sdk.associatedModulePath?.let { Path.of(it) }
?: return PyResult.localizedError(poetryNotFoundException) // Choose a correct sdk
runPoetry(projectPath, "env", "use", sdk.homePath!!)
return runPoetry(projectPath, *args)
}
@@ -41,7 +41,7 @@ private fun validateUvExecutable(uvPath: Path?): ValidationInfo? {
}
private suspend fun runUv(uv: Path, workingDir: Path, vararg args: String): PyExecResult<String> {
return runExecutableWithProgress(uv, workingDir, 10.minutes, *args)
return runExecutableWithProgress(uv, workingDir, 10.minutes, args = args)
}
private class UvCliImpl(val dispatcher: CoroutineDispatcher, uvPath: Path?) : UvCli {
@@ -147,7 +147,7 @@ class PyAddCondaPanelModel(val targetConfiguration: TargetEnvironmentConfigurati
reporter: RawProgressReporter? = null): PyResult<List<PyCondaEnv>> = withContext(uiContext) {
val path = condaPathTextBoxRwProp.get()
reporter?.text(PyBundle.message("python.sdk.conda.getting.list.envs"))
val envs = PyCondaEnv.getEnvs(targetCommandExecutor, path.trim())
val envs = PyCondaEnv.getEnvs(path.trim())
.onFailure {
condaEnvModel.removeAllElements()
showCondaActionsPanelRoProp.set(false)
@@ -2,7 +2,6 @@
package com.jetbrains.env.python.conda
import com.intellij.execution.processTools.getResultStdoutStr
import com.intellij.execution.processTools.mapFlat
import com.intellij.execution.target.TargetProgressIndicator
import com.intellij.execution.target.TargetedCommandLineBuilder
import com.intellij.execution.target.local.LocalTargetEnvironmentRequest
@@ -32,8 +31,7 @@ internal class CondaYamlFileRule(private val condaRule: LocalCondaRule,
val command = PyCondaCommand(fullPathOnTarget, null, null)
val condaEnvRequest = NewCondaEnvRequest.EmptyNamedEnv(languageLevel, envName)
runBlocking {
val process = PyCondaEnv.createEnv(command, condaEnvRequest).getOrThrow()
Result.success(process).mapFlat { it.getResultStdoutStr() }.getOrThrow()
PyCondaEnv.createEnv(command, condaEnvRequest).getOrThrow()
}
val targetReq = LocalTargetEnvironmentRequest()
val builder = TargetedCommandLineBuilder(targetReq).apply {
@@ -102,7 +102,7 @@ class PyAddCondaPanelModelTest {
// Create env
PyCondaEnv.createEnv(condaRule.condaCommand,
NewCondaEnvRequest.EmptyNamedEnv(LanguageLevel.PYTHON38, name)).asKotlinResult().map { it.getResultStdoutStr() }.getOrThrow()
NewCondaEnvRequest.EmptyNamedEnv(LanguageLevel.PYTHON38, name)).getOrThrow()
val model = PyAddCondaPanelModel(null, emptyList(), projectRule.project)
@@ -68,7 +68,7 @@ internal class PyCondaSdkTest {
private suspend fun createCondaEnv(): PyCondaEnv {
val name = "condaForTests"
PyCondaEnv.createEnv(condaRule.condaCommand, NewCondaEnvRequest.EmptyNamedEnv(LanguageLevel.PYTHON38, name)).getOrThrow().waitFor()
PyCondaEnv.createEnv(condaRule.condaCommand, NewCondaEnvRequest.EmptyNamedEnv(LanguageLevel.PYTHON38, name)).getOrThrow()
return PyCondaEnv(PyCondaEnvIdentity.NamedEnv(name), condaRule.condaPathOnTarget)
}
@@ -86,7 +86,7 @@ internal class PyCondaSdkTest {
""".trimIndent())
Assume.assumeTrue("Windows only", SystemInfoRt.isWindows)
val condaEnvs = PyCondaEnv.getEnvs(condaRule.commandExecutor, condaRule.condaPathOnTarget).getOrThrow()
val condaEnvs = PyCondaEnv.getEnvs(condaRule.condaPathOnTarget).getOrThrow()
val baseEnv = condaEnvs.first { (it.envIdentity as? PyCondaEnvIdentity.UnnamedEnv)?.isBase == true }
val nonBaseEnv = condaEnvs.firstOrNull { it.envIdentity is PyCondaEnvIdentity.NamedEnv } ?: createCondaEnv()
@@ -116,7 +116,7 @@ internal class PyCondaSdkTest {
@Test
fun testExecuteCommandOnSdk(): Unit = runTest(timeout = 20.seconds) {
val condaEnv = PyCondaEnv.getEnvs(condaRule.commandExecutor, condaRule.condaPathOnTarget).getOrThrow().first()
val condaEnv = PyCondaEnv.getEnvs(condaRule.condaPathOnTarget).getOrThrow().first()
val sdk = condaRule.condaCommand.createCondaSdkFromExistingEnv(condaEnv.envIdentity, emptyList(), projectRule.project)
val request = LocalTargetEnvironmentRequest()
@@ -139,7 +139,7 @@ internal class PyCondaSdkTest {
@Test
fun testCreateFromExisting() = runTest {
val env = PyCondaEnv.getEnvs(condaRule.commandExecutor, condaRule.condaPathOnTarget).getOrThrow().first()
val env = PyCondaEnv.getEnvs(condaRule.condaPathOnTarget).getOrThrow().first()
val sdk = condaRule.condaCommand.createCondaSdkFromExistingEnv(env.envIdentity, emptyList(), projectRule.project)
Assert.assertEquals(sdk.getOrCreateAdditionalData().flavor, CondaEnvSdkFlavor.getInstance())
Assert.assertTrue(env.toString(), getPythonVersion(sdk, LocalTargetEnvironmentRequest())?.isNotBlank() == true)
+10 -10
View File
@@ -64,7 +64,7 @@ internal class PyCondaTest {
@Test
fun testBasePython(): Unit = runTest {
val baseConda = PyCondaEnv.getEnvs(condaRule.commandExecutor, condaRule.condaPathOnTarget).getOrThrow()
val baseConda = PyCondaEnv.getEnvs(condaRule.condaPathOnTarget).getOrThrow()
.first { (it.envIdentity as? PyCondaEnvIdentity.UnnamedEnv)?.isBase == true }
val targetRequest = LocalTargetEnvironmentRequest()
val commandLineBuilder = TargetedCommandLineBuilder(targetRequest)
@@ -87,8 +87,8 @@ internal class PyCondaTest {
@Test
fun testCondaCreateByYaml() = runTest(timeout = 60.seconds) {
PyCondaEnv.createEnv(condaRule.condaCommand,
LocalEnvByLocalEnvironmentFile(yamlRule.yamlFilePath)).asKotlinResult().mapFlat { it.getResultStdoutStr() }.getOrThrow()
val condaEnv = PyCondaEnv.getEnvs(condaRule.commandExecutor, condaRule.condaPathOnTarget)
LocalEnvByLocalEnvironmentFile(yamlRule.yamlFilePath)).getOrThrow()
val condaEnv = PyCondaEnv.getEnvs(condaRule.condaPathOnTarget)
.getOrThrow().first { (it.envIdentity as? PyCondaEnvIdentity.NamedEnv)?.envName == yamlRule.envName }
// Python version contains word "Python", LanguageLevel doesn't expect it
@@ -100,14 +100,14 @@ internal class PyCondaTest {
fun testCondaCreateEnv(): Unit = runTest(timeout = 20.seconds) {
val envName = "myNewEnvForTests"
PyCondaEnv.createEnv(condaRule.condaCommand,
EmptyNamedEnv(LanguageLevel.PYTHON39, envName)).asKotlinResult().mapFlat { it.getResultStdout() }
PyCondaEnv.getEnvs(condaRule.commandExecutor, condaRule.condaPathOnTarget)
EmptyNamedEnv(LanguageLevel.PYTHON39, envName)).getOrThrow()
PyCondaEnv.getEnvs(condaRule.condaPathOnTarget)
.getOrThrow().first { (it.envIdentity as? PyCondaEnvIdentity.NamedEnv)?.envName == envName }
}
@Test
fun testCondaListEnvs(): Unit = runTest {
val condaEnvs = PyCondaEnv.getEnvs(condaRule.commandExecutor, condaRule.condaPathOnTarget).getOrThrow()
val condaEnvs = PyCondaEnv.getEnvs(condaRule.condaPathOnTarget).getOrThrow()
Assert.assertTrue("No environments returned", condaEnvs.isNotEmpty())
var baseFound = false
@@ -130,20 +130,20 @@ internal class PyCondaTest {
@Test
fun testCondaListUnnamedEnvs(): Unit = runTest(timeout = 90.seconds) {
val envsDirs = Path.of(PyCondaEnv.getEnvsDirs(condaRule.commandExecutor, condaRule.condaPathOnTarget).getOrThrow().first())
val envsDirs = Path.of(PyCondaEnv.getEnvsDirs(condaRule.condaPathOnTarget).getOrThrow().first())
val childDir = envsDirs.resolve("child")
val childEnvPrefix = childDir.resolve("childEnv").toString()
val siblingDir = envsDirs.resolveSibling("${envsDirs.fileName}Sibling")
val siblingEnvPrefix = siblingDir.resolve("siblingEnv").toString()
PyCondaEnv.createEnv(condaRule.condaCommand,
NewCondaEnvRequest.EmptyUnnamedEnv(LanguageLevel.PYTHON39, childEnvPrefix)).asKotlinResult().mapFlat { it.getResultStdout() }
NewCondaEnvRequest.EmptyUnnamedEnv(LanguageLevel.PYTHON39, childEnvPrefix)).getOrThrow()
PyCondaEnv.createEnv(condaRule.condaCommand,
NewCondaEnvRequest.EmptyUnnamedEnv(LanguageLevel.PYTHON39, siblingEnvPrefix)).asKotlinResult().mapFlat { it.getResultStdout() }
NewCondaEnvRequest.EmptyUnnamedEnv(LanguageLevel.PYTHON39, siblingEnvPrefix)).getOrThrow()
// Important to check that envIdentity is UnnamedEnv as this is testing to make sure that
// getEnvs doesn't mistakenly return a NamedEnv for an environment that isn't a direct child of envsDirs
val envs = PyCondaEnv.getEnvs(condaRule.commandExecutor, condaRule.condaPathOnTarget).getOrThrow()
val envs = PyCondaEnv.getEnvs(condaRule.condaPathOnTarget).getOrThrow()
.map { it.envIdentity }
.filterIsInstance<PyCondaEnvIdentity.UnnamedEnv>()
Assert.assertTrue("No child $childEnvPrefix in $envs", envs.any { it.envPath == childEnvPrefix })