PY-82119 Packaging: Fix bugs in Conda

Signed-off-by: Nikita.Ashihmin <nikita.ashihmin@jetbrains.com>

GitOrigin-RevId: 0ae5236e0d1c72de889f15563bc08a541cec834f
This commit is contained in:
Nikita.Ashihmin
2025-06-25 14:14:22 +00:00
committed by intellij-monorepo-bot
parent b219e8ca22
commit a2b962df79
6 changed files with 46 additions and 33 deletions
@@ -18,7 +18,7 @@ import com.jetbrains.python.sdk.getOrCreateAdditionalData
internal class CondaPackageManagerEngine(private val sdk: Sdk) : PythonPackageManagerEngine {
suspend fun updateFromEnvironmentFile(envFile: VirtualFile): PyResult<Unit> {
val env = getEnvData()
return CondaExecutor.updateFromEnvironmentFile(env.condaPath, envFile.path)
return CondaExecutor.updateFromEnvironmentFile(env.condaPath, envFile.path, env.envIdentity)
}
suspend fun exportToEnvironmentFile(): PyResult<String> {
@@ -74,8 +74,4 @@ internal class CondaPackageManagerEngine(private val sdk: Sdk) : PythonPackageMa
PyResult.success(quoted)
}
}
companion object {
}
}
@@ -15,6 +15,14 @@ import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
object CondaEnvironmentYmlParser {
fun readNameFromFile(file: VirtualFile): String? {
val text = FileDocumentManager.getInstance().getDocument(file)?.text ?: return null
val yaml = Yaml(configuration = YamlConfiguration(strictMode = false))
val environment: YamlMap = yaml.parseToYamlNode(text).yamlMap
return environment.get<YamlScalar>("name")?.yamlScalar?.content
}
fun fromFile(file: VirtualFile): List<PyRequirement>? {
val pyRequirements = runCatching { readDeps(file) }.onFailure {
thisLogger().info("Cannot parse deps from ${file.readText()}", it)
@@ -18,18 +18,20 @@ import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
import com.intellij.platform.util.progress.RawProgressReporter
import com.jetbrains.python.conda.loadLocalPythonCondaPath
import com.jetbrains.python.conda.saveLocalPythonCondaPath
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.errorProcessing.asPythonResult
import com.jetbrains.python.getOrThrow
import com.jetbrains.python.onFailure
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.sdk.PythonSdkAdditionalData
import com.jetbrains.python.sdk.PythonSdkType
import com.jetbrains.python.conda.loadLocalPythonCondaPath
import com.jetbrains.python.conda.saveLocalPythonCondaPath
import com.jetbrains.python.sdk.flavors.PyFlavorAndData
import com.jetbrains.python.sdk.flavors.conda.*
import com.jetbrains.python.sdk.getOrCreateAdditionalData
import com.jetbrains.python.target.PyTargetAwareAdditionalData
import com.jetbrains.python.util.ShowingMessageErrorSync
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.future.await
import kotlinx.coroutines.withContext
@@ -48,8 +50,9 @@ internal val condaSupportedLanguages: List<LanguageLevel>
.asReversed()
.filter { it < LanguageLevel.PYTHON313 }
val condaLatestSupportedLanguage: LanguageLevel @ApiStatus.Internal get() =
condaSupportedLanguages.maxWith(LanguageLevel.VERSION_COMPARATOR)
val condaLatestSupportedLanguage: LanguageLevel
@ApiStatus.Internal get() =
condaSupportedLanguages.maxWith(LanguageLevel.VERSION_COMPARATOR)
/**
* See [com.jetbrains.env.conda.PyCondaSdkTest]
@@ -73,7 +76,9 @@ suspend fun PyCondaCommand.createCondaSdkFromExistingEnv(
sdkModificator.sdkAdditionalData = additionalData
// homePath is not required by conda, but used by lots of tools all over the code and required by CondaPathFix
// Because homePath is not set yet, CondaPathFix does not work
sdkModificator.homePath = getCondaPythonBinaryPath(project, condaEnv, targetConfig).getOrThrow()
sdkModificator.homePath = getCondaPythonBinaryPath(project, condaEnv, targetConfig).onFailure {
ShowingMessageErrorSync.emit(it)
}.getOrThrow()
edtWriteAction {
sdkModificator.commitChanges()
}
@@ -6,16 +6,22 @@ import com.jetbrains.python.packaging.conda.CondaPackage
import com.jetbrains.python.sdk.conda.execution.models.CondaEnvInfo
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
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")) }
val parsed = json.parseToJsonElement(text).jsonArray
return parsed.map {
val jsonObject = it.jsonObject
val name = jsonObject["name"]?.jsonPrimitive?.content ?: ""
val version = jsonObject["version"]?.jsonPrimitive?.content ?: ""
val channel = jsonObject["channel"]?.jsonPrimitive?.content ?: ""
val isPypi = channel == "pypi"
CondaPackage(name, version, editableMode = false, installedWithPip = isPypi)
}
.sortedWith(compareBy(CondaPackage::name))
.toList()
}
@@ -51,6 +57,4 @@ internal object CondaExecutionParser {
@Serializable
private data class CondaPackageInfo(val name: String, val version: String)
private val listLineParser = "\\s+".toRegex()
}
@@ -37,9 +37,9 @@ object CondaExecutor {
return runConda(condaPath, args, null).mapSuccess { }
}
suspend fun updateFromEnvironmentFile(condaPath: Path, envYmlPath: String): PyResult<Unit> {
suspend fun updateFromEnvironmentFile(condaPath: Path, envYmlPath: String, envIdentity: PyCondaEnvIdentity): PyResult<Unit> {
val args = listOf("env", "update", "--file", envYmlPath, "--prune")
return runConda(condaPath, args, null).mapSuccess { }
return runConda(condaPath, args, envIdentity).mapSuccess { }
}
suspend fun listEnvs(condaPath: Path): PyResult<CondaEnvInfo> {
@@ -55,7 +55,7 @@ object CondaExecutor {
}
suspend fun listPackages(condaPath: Path, envIdentity: PyCondaEnvIdentity): PyResult<List<PythonPackage>> {
return runConda(condaPath, listOf("list"), envIdentity).mapSuccess {
return runConda(condaPath, listOf("list", "--json"), envIdentity).mapSuccess {
CondaExecutionParser.parseCondaPackageList(it)
}
}
@@ -97,7 +97,7 @@ object CondaExecutor {
return it
}
val runArgs = (condaEnv + args).toTypedArray()
val runArgs = (args + condaEnv).toTypedArray()
return runExecutableWithProgress(condaPath, null, timeout, env = envs, *runArgs)
}
@@ -142,7 +142,7 @@ object CondaExecutor {
}
else value
key to fixedVal.toString()
key to fixedVal
}.toMap()
}
}
@@ -1,14 +1,14 @@
// 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.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.intellij.execution.target.FullPathOnTarget
import com.intellij.execution.target.TargetEnvironmentConfiguration
import com.intellij.execution.target.TargetedCommandLineBuilder
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.vfs.VirtualFileManager
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.packaging.conda.environmentYml.format.CondaEnvironmentYmlParser
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.sdk.conda.TargetCommandExecutor
import com.jetbrains.python.sdk.conda.createCondaSdkFromExistingEnv
@@ -21,10 +21,9 @@ 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]
* TODO: Once we get rid of [TargetCommandExecutor] and have access to [TargetEnvironmentConfiguration] use it validate conda binary in [getEnvs]
* @see `PyCondaTest`
*/
@ApiStatus.Internal
@@ -115,7 +114,6 @@ data class PyCondaEnv(
/**
* Request to create new conda environment.
* Conda binary must be run with [createEnvArguments]
*/
sealed class NewCondaEnvRequest {
abstract val envName: @NonNls String
@@ -157,14 +155,16 @@ sealed class NewCondaEnvRequest {
assert(environmentYaml.exists()) { "$environmentYaml doesn't exist" }
}
private val lazyName = lazy {
ObjectMapper(YAMLFactory()).readValue(environmentYaml.pathString, ObjectNode::class.java).get("name").asText()
}
override val envName: String get() = lazyName.value
override val envName: String = readEnvName()
@ApiStatus.Internal
override suspend fun create(condaPath: Path): PyResult<Unit> {
return CondaExecutor.createFileEnv(condaPath, environmentYaml)
}
private fun readEnvName(): String = runReadAction {
val virtualFile = VirtualFileManager.getInstance().findFileByNioPath(environmentYaml) ?: return@runReadAction "default"
CondaEnvironmentYmlParser.readNameFromFile(virtualFile) ?: "default"
}
}
}