[python] (PY-78749) single dialog for all types of Python SDKs

+ old dialogs were removed

GitOrigin-RevId: 00ed85ba578c6419373d8605ed50954e7aa58f0a
This commit is contained in:
Vitaly Legchilkin
2025-10-13 22:54:32 +00:00
committed by intellij-monorepo-bot
parent ab8455f916
commit 5e2028d129
99 changed files with 2693 additions and 3331 deletions
@@ -1,6 +1,7 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.env.python.conda
import com.intellij.python.community.execService.BinaryToExec
import com.intellij.util.io.delete
import com.jetbrains.python.getOrThrow
import com.jetbrains.python.psi.LanguageLevel
@@ -27,11 +28,12 @@ internal class CondaYamlFileRule(private val condaRule: LocalCondaRule,
override fun before() {
val fullPathOnTarget = condaRule.condaPathOnTarget
val command = PyCondaCommand(fullPathOnTarget, null, null)
val condaEnvRequest = NewCondaEnvRequest.EmptyNamedEnv(languageLevel, envName)
val env = PyCondaEnvIdentity.NamedEnv(condaEnvRequest.envName)
val yamlFileText = runBlocking {
PyCondaEnv.createEnv(command, condaEnvRequest).getOrThrow()
CondaExecutor.exportEnvironmentFile(Path.of(fullPathOnTarget), env)
CondaExecutor.exportEnvironmentFile(command.asBinaryToExec(), env)
}.getOrThrow()
val file = File.createTempFile("ijconda", ".yaml")
@@ -2,7 +2,12 @@
package com.jetbrains.env.python.conda
import com.intellij.execution.target.FullPathOnTarget
import com.intellij.python.community.execService.BinOnEel
import com.intellij.python.community.execService.BinaryToExec
import com.intellij.python.community.testFramework.testEnv.conda.TypeConda
import com.jetbrains.python.getOrThrow
import com.jetbrains.python.sdk.add.v2.Version
import com.jetbrains.python.sdk.add.v2.conda.getCondaVersion
import com.jetbrains.python.sdk.flavors.conda.PyCondaCommand
import kotlinx.coroutines.runBlocking
import org.junit.AssumptionViolatedException
@@ -20,6 +25,9 @@ class LocalCondaRule : ExternalResource() {
lateinit var condaPath: Path
private set
lateinit var condaVersion: Version
private set
private lateinit var autoCloseable: AutoCloseable
val condaPathOnTarget: FullPathOnTarget get() = condaPath.toString()
@@ -35,10 +43,16 @@ class LocalCondaRule : ExternalResource() {
if (!condaPath.isExecutable()) {
throw AssumptionViolatedException("$condaPath is not executable")
}
condaVersion = runBlocking { BinOnEel(condaPath).getCondaVersion().getOrThrow() }
this.autoCloseable = autoCloseable
}
override fun after() {
autoCloseable.close()
}
fun getCondaBinaryToExec(): BinaryToExec {
return BinOnEel(Path.of(condaPathOnTarget))
}
}
@@ -1,162 +0,0 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.env.python.conda
import com.intellij.execution.target.local.LocalTargetEnvironmentRequest
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.util.registry.Registry
import com.intellij.testFramework.ProjectRule
import com.intellij.testFramework.common.timeoutRunBlocking
import com.jetbrains.getPythonVersion
import com.jetbrains.python.PyBundle
import com.jetbrains.python.getOrThrow
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.sdk.flavors.conda.NewCondaEnvRequest
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
import com.jetbrains.python.sdk.flavors.conda.PyCondaFlavorData
import com.jetbrains.python.sdk.getOrCreateAdditionalData
import com.jetbrains.python.target.ui.PyAddCondaPanelModel
import org.hamcrest.MatcherAssert
import org.hamcrest.Matchers.hasItem
import org.hamcrest.Matchers.not
import org.junit.Assert
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import java.nio.file.Path
import kotlin.io.path.exists
import kotlin.time.Duration.Companion.seconds
@RunWith(Parameterized::class)
class PyAddCondaPanelModelTest {
@JvmField
@Rule
val condaRule: LocalCondaRule = LocalCondaRule()
@JvmField
@Rule
val projectRule: ProjectRule = ProjectRule()
@Parameterized.Parameter(0)
@JvmField
var useLegacy: Boolean = false
companion object {
private val timeout = 60.seconds
@JvmStatic
@Parameterized.Parameters
fun data(): Collection<Array<Any>> = listOf(arrayOf(false), arrayOf(true))
}
@Before
fun before() {
Registry.get("use.python.for.local.conda").setValue(useLegacy)
Logger.getInstance(PyCondaSdkTest::class.java).info("Legacy: $useLegacy")
}
@Test
fun testCondaDetection(): Unit = timeoutRunBlocking(timeout) {
val model = PyAddCondaPanelModel(null, emptyList(), projectRule.project)
model.detectConda(coroutineContext)
val detectedPath = model.condaPathTextBoxRwProp.get()
if (detectedPath.isNotEmpty()) {
Assert.assertEquals("Wrong path detected", condaRule.condaPathOnTarget, detectedPath)
}
}
@Test
fun testCondaCreateNewEnv(): Unit = timeoutRunBlocking(timeout) {
val condaName = "someNewCondaEnv"
val model = PyAddCondaPanelModel(null, emptyList(), projectRule.project)
model.condaPathTextBoxRwProp.set(condaRule.condaPath.toString())
model.condaActionCreateNewEnvRadioRwProp.set(true)
model.condaActionUseExistingEnvRadioRwProp.set(false)
MatcherAssert.assertThat("No 3.9 suggested", model.languageLevels, hasItem(LanguageLevel.PYTHON39))
MatcherAssert.assertThat("2.6 suggested", model.languageLevels, not(hasItem(LanguageLevel.PYTHON26)))
model.newEnvLanguageLevelRwProperty.set(LanguageLevel.PYTHON38)
Assert.assertNotNull("Empty conda env name didn't lead to validation", model.getValidationError())
model.newEnvNameRwProperty.set("d f --- ")
Assert.assertNotNull("Bad conda name didn't lead to validation", model.getValidationError())
model.newEnvNameRwProperty.set(condaName)
val sdk = model.onCondaCreateSdkClicked(coroutineContext, null, targetConfiguration = null).getOrThrow()
val newName = ((sdk.getOrCreateAdditionalData().flavorAndData.data as PyCondaFlavorData).env.envIdentity as PyCondaEnvIdentity.NamedEnv).envName
Assert.assertEquals("Wrong conda name", condaName, newName)
}
@Test
fun testCondaCantUseNameUsedAlready(): Unit = timeoutRunBlocking(timeout) {
val name = "cond_env_" + Math.random().toString().replace('.', '_')
// Create env
PyCondaEnv.createEnv(condaRule.condaCommand,
NewCondaEnvRequest.EmptyNamedEnv(LanguageLevel.PYTHON38, name)).getOrThrow()
val model = PyAddCondaPanelModel(null, emptyList(), projectRule.project)
// Trying to create env with same name
model.condaPathTextBoxRwProp.set(condaRule.condaPath.toString())
model.onLoadEnvsClicked(coroutineContext)
model.condaActionCreateNewEnvRadioRwProp.set(true)
model.condaActionUseExistingEnvRadioRwProp.set(false)
model.newEnvLanguageLevelRwProperty.set(LanguageLevel.PYTHON38)
model.newEnvNameRwProperty.set(name)
Assert.assertEquals("Name duplicate should lead to error", PyBundle.message("python.sdk.conda.problem.env.name.used"),
model.getValidationError())
}
@Test
fun testCondaUseExistingEnv(): Unit = timeoutRunBlocking(timeout) {
val model = PyAddCondaPanelModel(null, emptyList(), projectRule.project)
model.condaPathTextBoxRwProp.set(condaRule.condaPath.toString())
model.onLoadEnvsClicked(coroutineContext)
model.condaActionUseExistingEnvRadioRwProp.set(true)
model.condaActionCreateNewEnvRadioRwProp.set(false)
model.condaEnvModel.selectedItem = model.condaEnvModel.getElementAt(0)
val sdk = model.onCondaCreateSdkClicked(coroutineContext, null, targetConfiguration = null).getOrThrow()
Assert.assertTrue(getPythonVersion(sdk, LocalTargetEnvironmentRequest())!!.isNotBlank())
Assert.assertTrue(Path.of(sdk.homePath!!).exists())
}
@Test
fun testCondaModelValidation(): Unit = timeoutRunBlocking(timeout) {
val model = PyAddCondaPanelModel(null, emptyList(), projectRule.project)
Assert.assertNotNull("No validation error, even though path not set", model.getValidationError())
Assert.assertFalse(model.showCondaPathSetOkButtonRoProp.get())
model.condaPathTextBoxRwProp.set("Some random path that doesn't exist and doesn't contain conda")
Assert.assertFalse("Wrong path, but button not disabled", model.showCondaPathSetOkButtonRoProp.get())
model.condaPathTextBoxRwProp.set("foo/conda")
Assert.assertTrue("Path that passes regex, but button disabled", model.showCondaPathSetOkButtonRoProp.get())
model.onLoadEnvsClicked(coroutineContext)
Assert.assertNotNull("No validation error, but path is incorrect", model.getValidationError())
model.condaPathTextBoxRwProp.set(condaRule.condaPath.toString())
model.onLoadEnvsClicked(coroutineContext)
Assert.assertNull("Unexpected validation error", model.getValidationError())
Assert.assertTrue(model.showCondaActionsPanelRoProp.get())
model.condaActionCreateNewEnvRadioRwProp.set(true)
Assert.assertTrue("No conda envs loaded", model.condaEnvModel.size > 0)
model.condaActionCreateNewEnvRadioRwProp.set(true)
model.condaActionUseExistingEnvRadioRwProp.set(false)
Assert.assertEquals("Env name not set", projectRule.project.name, model.newEnvNameRwProperty.get())
model.newEnvNameRwProperty.set("")
Assert.assertNotNull("No validation error, but conda env name not set", model.getValidationError())
model.newEnvNameRwProperty.set("SomeEnv-Name")
Assert.assertNull("Unexpected error", model.getValidationError())
}
}
@@ -25,9 +25,14 @@ class PyCondaAdditionalDataTest {
@Test
fun testSerialize() {
val flavorData = PyCondaFlavorData(PyCondaEnv(PyCondaEnvIdentity.NamedEnv("D"), "foo"))
val flavorData = PyCondaFlavorData(
env = PyCondaEnv(
envIdentity = PyCondaEnvIdentity.NamedEnv("D"),
fullCondaPathOnTarget = "foo",
)
)
val data = PythonSdkAdditionalData(
PyFlavorAndData(flavorData, CondaEnvSdkFlavor.getInstance()))
PyFlavorAndData(flavorData, CondaEnvSdkFlavor))
val rootElement = Element("root")
data.save(rootElement)
@@ -36,7 +41,7 @@ class PyCondaAdditionalDataTest {
val reloadedData = PythonSdkAdditionalData.loadFromElement(rootElement)
Assert.assertEquals(reloadedData.flavor, CondaEnvSdkFlavor.getInstance())
Assert.assertEquals(reloadedData.flavor, CondaEnvSdkFlavor)
Assert.assertEquals(reloadedData.flavorAndData.data, flavorData)
}
}
@@ -70,7 +70,10 @@ internal class PyCondaSdkTest {
private suspend fun createCondaEnv(): PyCondaEnv {
val name = "condaForTests"
PyCondaEnv.createEnv(condaRule.condaCommand, NewCondaEnvRequest.EmptyNamedEnv(LanguageLevel.PYTHON38, name)).getOrThrow()
return PyCondaEnv(PyCondaEnvIdentity.NamedEnv(name), condaRule.condaPathOnTarget)
return PyCondaEnv(
envIdentity = PyCondaEnvIdentity.NamedEnv(name),
fullCondaPathOnTarget = condaRule.condaPathOnTarget,
)
}
/**
@@ -87,12 +90,16 @@ internal class PyCondaSdkTest {
""".trimIndent())
Assume.assumeTrue("Windows only", SystemInfoRt.isWindows)
val condaEnvs = PyCondaEnv.getEnvs(condaRule.condaPathOnTarget).getOrThrow()
val condaEnvs = PyCondaEnv.getEnvs(condaRule.getCondaBinaryToExec()).getOrThrow()
val baseEnv = condaEnvs.first { (it.envIdentity as? PyCondaEnvIdentity.UnnamedEnv)?.isBase == true }
val nonBaseEnv = condaEnvs.firstOrNull { it.envIdentity is PyCondaEnvIdentity.NamedEnv } ?: createCondaEnv()
for (condaEnv in arrayOf(nonBaseEnv, baseEnv)) {
val condaSdk = condaRule.condaCommand.createCondaSdkFromExistingEnv(condaEnv.envIdentity, emptyList(), projectRule.project)
val condaSdk = condaRule.condaCommand.createCondaSdkFromExistingEnv(
condaIdentity = condaEnv.envIdentity,
existingSdks = emptyList(),
project = projectRule.project,
)
val request = LocalTargetEnvironmentRequest()
val targetEnvironment = LocalTargetEnvironment(request)
@@ -117,8 +124,12 @@ internal class PyCondaSdkTest {
@Test
fun testExecuteCommandOnSdk(): Unit = timeoutRunBlocking(60.seconds) {
val condaEnv = PyCondaEnv.getEnvs(condaRule.condaPathOnTarget).getOrThrow().first()
val sdk = condaRule.condaCommand.createCondaSdkFromExistingEnv(condaEnv.envIdentity, emptyList(), projectRule.project)
val condaEnv = PyCondaEnv.getEnvs(condaRule.getCondaBinaryToExec()).getOrThrow().first()
val sdk = condaRule.condaCommand.createCondaSdkFromExistingEnv(
condaIdentity = condaEnv.envIdentity,
existingSdks = emptyList(),
project = projectRule.project,
)
val request = LocalTargetEnvironmentRequest()
repeat(10) { // To measure time to compare legacy and local
@@ -140,9 +151,14 @@ internal class PyCondaSdkTest {
@Test
fun testCreateFromExisting() = timeoutRunBlocking(10.minutes) {
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())
val env = PyCondaEnv.getEnvs(condaRule.getCondaBinaryToExec()).getOrThrow().first()
val sdk = condaRule.condaCommand.createCondaSdkFromExistingEnv(
condaIdentity = env.envIdentity,
existingSdks = emptyList(),
project = projectRule.project,
)
Assert.assertEquals(sdk.getOrCreateAdditionalData().flavor, CondaEnvSdkFlavor)
Assert.assertTrue(env.toString(), getPythonVersion(sdk, LocalTargetEnvironmentRequest())?.isNotBlank() == true)
Assert.assertTrue("Bad home path", Files.isExecutable(Path.of(sdk.homePath!!)))
@@ -63,7 +63,7 @@ internal class PyCondaTest {
@Test
fun testBasePython(): Unit = timeoutRunBlocking(10.minutes) {
val baseConda = PyCondaEnv.getEnvs(condaRule.condaPathOnTarget).getOrThrow()
val baseConda = PyCondaEnv.getEnvs(condaRule.getCondaBinaryToExec()).getOrThrow()
.first { (it.envIdentity as? PyCondaEnvIdentity.UnnamedEnv)?.isBase == true }
val targetRequest = LocalTargetEnvironmentRequest()
val commandLineBuilder = TargetedCommandLineBuilder(targetRequest)
@@ -87,7 +87,7 @@ internal class PyCondaTest {
fun testCondaCreateByYaml() = timeoutRunBlocking(60.seconds) {
PyCondaEnv.createEnv(condaRule.condaCommand,
LocalEnvByLocalEnvironmentFile(yamlRule.yamlFilePath, emptyList())).getOrThrow()
val condaEnv = PyCondaEnv.getEnvs(condaRule.condaPathOnTarget)
val condaEnv = PyCondaEnv.getEnvs(condaRule.getCondaBinaryToExec())
.getOrThrow().first { (it.envIdentity as? PyCondaEnvIdentity.NamedEnv)?.envName == yamlRule.envName }
// Python version contains word "Python", LanguageLevel doesn't expect it
@@ -100,13 +100,13 @@ internal class PyCondaTest {
val envName = "myNewEnvForTests"
PyCondaEnv.createEnv(condaRule.condaCommand,
EmptyNamedEnv(LanguageLevel.PYTHON39, envName)).getOrThrow()
PyCondaEnv.getEnvs(condaRule.condaPathOnTarget)
PyCondaEnv.getEnvs(condaRule.getCondaBinaryToExec())
.getOrThrow().first { (it.envIdentity as? PyCondaEnvIdentity.NamedEnv)?.envName == envName }
}
@Test
fun testCondaListEnvs(): Unit = timeoutRunBlocking(10.minutes) {
val condaEnvs = PyCondaEnv.getEnvs(condaRule.condaPathOnTarget).getOrThrow()
val condaEnvs = PyCondaEnv.getEnvs(condaRule.getCondaBinaryToExec()).getOrThrow()
Assert.assertTrue("No environments returned", condaEnvs.isNotEmpty())
var baseFound = false
@@ -129,7 +129,7 @@ internal class PyCondaTest {
@Test
fun testCondaListUnnamedEnvs(): Unit = timeoutRunBlocking(90.seconds) {
val envDirs = CondaExecutor.listEnvs(Path.of(condaRule.condaPathOnTarget)).mapSuccess { it.envsDirs }.getOrThrow()
val envDirs = CondaExecutor.listEnvs(condaRule.getCondaBinaryToExec()).mapSuccess { it.envsDirs }.getOrThrow()
val envsDirs = Path.of(envDirs.first())
val childDir = envsDirs.resolve("child")
val childEnvPrefix = childDir.resolve("childEnv").toString()
@@ -143,7 +143,7 @@ internal class PyCondaTest {
// 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.condaPathOnTarget).getOrThrow()
val envs = PyCondaEnv.getEnvs(condaRule.getCondaBinaryToExec()).getOrThrow()
.map { it.envIdentity }
.filterIsInstance<PyCondaEnvIdentity.UnnamedEnv>()
Assert.assertTrue("No child $childEnvPrefix in $envs", envs.any { it.envPath == childEnvPrefix })
@@ -154,6 +154,6 @@ internal class PyCondaTest {
val req = LocalTargetEnvironmentRequest()
val commandLine = TargetedCommandLineBuilder(req).also { condaEnv.addCondaToTargetBuilder(it) }
commandLine.addParameter("python")
return getPythonVersion(commandLine, CondaEnvSdkFlavor.getInstance(), req) ?: error("No version for $condaEnv")
return getPythonVersion(commandLine, CondaEnvSdkFlavor, req) ?: error("No version for $condaEnv")
}
}