PY-78762: Activate conda in Powershell.

We used to use `Invoke-Expression` but then migrated to `&`.

However, a conda activation script is a command (code block), not a file, so we need to use `Invoke-Expression` as '&' doesn't support it.

We check if a file exists, and if it does -- we use '&' which is safe and fast. We use `Invoke-Expression` otherwise.


Merge-request: IJ-MR-158505
Merged-by: Ilya Kazakevich <ilya.kazakevich@jetbrains.com>

GitOrigin-RevId: ea0772b3f5f9641a85b542903c44c3b78aed0715
This commit is contained in:
Ilya Kazakevich
2025-03-28 14:51:58 +00:00
committed by intellij-monorepo-bot
parent a6ff3d6fbc
commit ca46fea6b0
5 changed files with 85 additions and 13 deletions
@@ -16,8 +16,12 @@ Get-ChildItem env:_INTELLIJ_FORCE_PREPEND_* | ForEach-Object {
}
# `JEDITERM_SOURCE` is executed in its own scope now. That means, it can only run code, and export env vars. It can't export PS variables.
# It might be better to source it. See MSDN for the difference between "Call operator &" and "Script scope and dot sourcing"
if (($Env:JEDITERM_SOURCE -ne $null) -and (Test-Path $Env:JEDITERM_SOURCE)) {
& $Env:JEDITERM_SOURCE
if ($Env:JEDITERM_SOURCE -ne $null) {
if (Test-Path "$Env:JEDITERM_SOURCE" -ErrorAction SilentlyContinue) {
& "$Env:JEDITERM_SOURCE"
} else { # If file doesn't exist it might be a script
Invoke-Expression "$Env:JEDITERM_SOURCE"
}
Remove-Item "env:JEDITERM_SOURCE"
}
@@ -13,5 +13,7 @@
<orderEntry type="module" module-name="intellij.python.community.testFramework.testEnv.conda" scope="TEST" />
<orderEntry type="library" scope="TEST" name="JUnit5" level="project" />
<orderEntry type="library" scope="TEST" name="jetbrains-annotations" level="project" />
<orderEntry type="module" module-name="intellij.platform.execution" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.core" scope="TEST" />
</component>
</module>
@@ -0,0 +1,30 @@
// 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.psi.LanguageLevel
import com.jetbrains.python.sdk.flavors.conda.NewCondaEnvRequest
import com.jetbrains.python.sdk.flavors.conda.PyCondaCommand
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
import org.jetbrains.annotations.ApiStatus
import java.nio.file.Path
import kotlin.io.path.pathString
@ApiStatus.Internal
/**
* Create conda env in [pathToCreateNewEnvIn] using [existingEnv] as a base
*/
suspend fun createCondaEnv(
existingEnv: PyCondaEnv,
pathToCreateNewEnvIn: Path,
): PyCondaEnv {
val process = 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
}
@@ -25,5 +25,7 @@
<orderEntry type="library" scope="TEST" name="hamcrest" level="project" />
<orderEntry type="module" module-name="intellij.python.community.testFramework.testEnv" scope="TEST" />
<orderEntry type="module" module-name="intellij.python.community.impl.venv" scope="TEST" />
<orderEntry type="module" module-name="intellij.python.community.junit5Tests.framework.conda" scope="TEST" />
<orderEntry type="library" scope="TEST" name="JUnit5Params" level="project" />
</component>
</module>
@@ -2,19 +2,27 @@
package com.intellij.python.junit5Tests.env.terminal
import com.intellij.execution.configurations.PathEnvironmentVariableUtil
import com.intellij.openapi.application.edtWriteAction
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.roots.ModuleRootModificationUtil
import com.intellij.platform.eel.EelExecApi
import com.intellij.platform.eel.getOrThrow
import com.intellij.platform.eel.provider.localEel
import com.intellij.platform.eel.provider.utils.readWholeText
import com.intellij.platform.eel.provider.utils.sendWholeText
import com.intellij.python.community.impl.venv.tests.pyVenvFixture
import com.intellij.python.junit5Tests.framework.env.PyEnvTestCase
import com.intellij.python.community.junit5Tests.framework.conda.CondaEnv
import com.intellij.python.community.junit5Tests.framework.conda.PyEnvTestCaseWithConda
import com.intellij.python.community.junit5Tests.framework.conda.createCondaEnv
import com.intellij.python.junit5Tests.framework.env.pySdkFixture
import com.intellij.python.terminal.PyVirtualEnvTerminalCustomizer
import com.intellij.testFramework.common.timeoutRunBlocking
import com.intellij.testFramework.junit5.fixture.moduleFixture
import com.intellij.testFramework.junit5.fixture.projectFixture
import com.intellij.testFramework.junit5.fixture.tempPathFixture
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
import com.jetbrains.python.sdk.persist
import com.jetbrains.python.venvReader.VirtualEnvReader
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
@@ -24,11 +32,15 @@ import org.jetbrains.plugins.terminal.ShellStartupOptions
import org.jetbrains.plugins.terminal.runner.LocalShellIntegrationInjector
import org.jetbrains.plugins.terminal.util.ShellIntegration
import org.jetbrains.plugins.terminal.util.ShellType
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.condition.EnabledOnOs
import org.junit.jupiter.api.condition.OS
import org.junit.jupiter.api.io.TempDir
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
import java.io.IOException
import java.nio.file.Path
import kotlin.io.path.Path
import kotlin.io.path.name
import kotlin.io.path.pathString
@@ -39,7 +51,7 @@ private const val WHERE_EXE = "where.exe"
/**
* Run `powershell.exe` with a venv activation script and make sure there are no errors and python is correct
*/
@PyEnvTestCase
@PyEnvTestCaseWithConda
class PyVirtualEnvTerminalCustomizerTest {
private val projectFixture = projectFixture()
private val tempDirFixture = tempPathFixture(prefix = "some dir with spaces")
@@ -52,15 +64,38 @@ class PyVirtualEnvTerminalCustomizerTest {
moduleFixture = moduleFixture
)
private var sdkToDelete: Sdk? = null
@AfterEach
fun tearDown(): Unit = timeoutRunBlocking {
sdkToDelete?.let { sdk ->
edtWriteAction {
ProjectJdkTable.getInstance().removeJdk(sdk)
}
}
}
private val powerShell =
PathEnvironmentVariableUtil.findInPath("powershell.exe")?.toPath()
?: Path((System.getenv("SystemRoot") ?: "c:\\windows"), "system32", "WindowsPowerShell", "v1.0", "powershell.exe")
@EnabledOnOs(value = [OS.WINDOWS])
@Test
fun powershellActivationTest(): Unit = timeoutRunBlocking(10.minutes) {
val pythonBinary = VirtualEnvReader.Instance.findPythonInPythonRoot(tempDirFixture.get())!!
@ParameterizedTest
@ValueSource(booleans = [true, false])
fun powershellActivationTest(useConda: Boolean, @CondaEnv condaEnv: PyCondaEnv, @TempDir path: Path): Unit = timeoutRunBlocking(10.minutes) {
val (pythonBinary, venvDirName) =
if (useConda) {
val envDir = path.resolve("some path with spaces")
val sdk = createCondaEnv(condaEnv, envDir).createSdkFromThisEnv(null, emptyList())
sdkToDelete = sdk
sdk.persist()
ModuleRootModificationUtil.setModuleSdk(moduleFixture.get(), sdk)
Pair(Path(sdk.homePath!!), envDir.toRealPath().pathString)
}
else {
val venv = VirtualEnvReader.Instance.findPythonInPythonRoot(tempDirFixture.get())!!
Pair(venv, tempDirFixture.get().name)
}
// binary might be like ~8.3, we need to expand it as venv might report both
val pythonBinaryReal = try {
@@ -69,7 +104,7 @@ class PyVirtualEnvTerminalCustomizerTest {
catch (_: IOException) {
pythonBinary
}
val shellOptions = getShellStartupOptions()
val shellOptions = getShellStartupOptions(pythonBinary.parent)
val command = shellOptions.shellCommand!!
val exe = command[0]
val args = if (command.size == 1) emptyList() else command.subList(1, command.size)
@@ -95,8 +130,7 @@ class PyVirtualEnvTerminalCustomizerTest {
assertThat("We ran `$where`, so we there should be python path", output,
anyOf(hasItem(pythonBinary.pathString), hasItem(pythonBinaryReal.pathString)))
val vendDirName = tempDirFixture.get().name
assertThat("There must be a line with ($vendDirName)", output, hasItem(containsString("($vendDirName)")))
assertThat("There must be a line with ($venvDirName)", output, hasItem(containsString("($venvDirName)")))
process.exitCode.await()
}
@@ -107,12 +141,12 @@ class PyVirtualEnvTerminalCustomizerTest {
}
}
private fun getShellStartupOptions(): ShellStartupOptions {
private fun getShellStartupOptions(workDir: Path): ShellStartupOptions {
val sut = PyVirtualEnvTerminalCustomizer()
val env = mutableMapOf<String, String>()
val command = sut.customizeCommandAndEnvironment(
projectFixture.get(),
tempDirFixture.get().pathString,
workDir.pathString,
arrayOf(powerShell.pathString),
env)