diff --git a/.idea/modules.xml b/.idea/modules.xml
index 27ad19ae9e8f..ec315aad9b90 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -943,7 +943,6 @@
-
@@ -957,6 +956,7 @@
+
diff --git a/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/impl/CondaPythonEnvExtension.kt b/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/impl/CondaPythonEnvExtension.kt
index 8666f5beec12..942fc3800f5b 100644
--- a/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/impl/CondaPythonEnvExtension.kt
+++ b/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/impl/CondaPythonEnvExtension.kt
@@ -1,11 +1,11 @@
// 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.junit5Tests.framework.env.impl
-import com.intellij.python.community.testFramework.testEnv.PythonType
+import com.intellij.python.community.testFramework.testEnv.TypeConda
import com.intellij.python.junit5Tests.framework.env.CondaEnv
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
/**
* Mark [PyCondaEnv] param with [CondaEnv] annotation and register this extension with [com.intellij.python.junit5Tests.framework.env.PyEnvTestCaseWithConda]
*/
-internal class CondaPythonEnvExtension : PythonEnvExtensionBase(CondaEnv::class, PythonType.Conda, PyCondaEnv::class)
\ No newline at end of file
+internal class CondaPythonEnvExtension : PythonEnvExtensionBase(CondaEnv::class, TypeConda, PyCondaEnv::class)
\ No newline at end of file
diff --git a/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/impl/PythonEnvExtensionBase.kt b/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/impl/PythonEnvExtensionBase.kt
index ba07f4a277b1..594c64aff5ec 100644
--- a/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/impl/PythonEnvExtensionBase.kt
+++ b/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/impl/PythonEnvExtensionBase.kt
@@ -52,8 +52,8 @@ internal abstract class PythonEnvExtensionBase {
- val (env, autoClosable) = runBlocking {
- pythonType.getTestEnvironment(*additionalTags).getOrElse {
+ val (_, autoClosable, env) = runBlocking {
+ pythonType.createSdkClosableEnv(*additionalTags).getOrElse {
// Logging due to IDEA-356206
LOG.warn(it)
val message = "Couldn't find python to run test against ($pythonType , ${additionalTags.toList()})"
diff --git a/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/impl/VanillaPythonEnvExtension.kt b/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/impl/VanillaPythonEnvExtension.kt
index 9a42fe095728..113d664ea409 100644
--- a/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/impl/VanillaPythonEnvExtension.kt
+++ b/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/impl/VanillaPythonEnvExtension.kt
@@ -6,7 +6,7 @@ import com.intellij.execution.process.CapturingProcessHandler
import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.util.SystemInfoRt
import com.intellij.python.community.impl.poetry.poetryPath
-import com.intellij.python.community.testFramework.testEnv.PythonType
+import com.intellij.python.community.testFramework.testEnv.TypeVanillaPython3
import com.intellij.python.junit5Tests.framework.env.PythonBinaryPath
import com.jetbrains.python.PythonBinary
import java.nio.file.Path
@@ -16,9 +16,9 @@ import java.nio.file.Path
*
* It also searches for poetry and stores it in [PropertiesComponent]
*/
-internal class VanillaPythonEnvExtension : PythonEnvExtensionBase(
+internal class VanillaPythonEnvExtension : PythonEnvExtensionBase(
annotation = PythonBinaryPath::class,
- pythonType = PythonType.VanillaPython3,
+ pythonType = TypeVanillaPython3,
envType = PythonBinary::class,
lazy = false,
additionalTags = arrayOf("poetry")
diff --git a/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/sdkFixture.kt b/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/sdkFixture.kt
index 8b84eddadc33..89c27bff7a78 100644
--- a/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/sdkFixture.kt
+++ b/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/sdkFixture.kt
@@ -8,6 +8,7 @@ import com.intellij.openapi.module.Module
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.python.community.testFramework.testEnv.PythonType
+import com.intellij.python.community.testFramework.testEnv.TypeVanillaPython3
import com.intellij.testFramework.junit5.fixture.TestFixture
import com.intellij.testFramework.junit5.fixture.testFixture
import com.jetbrains.python.sdk.configuration.createVirtualEnvSynchronously
@@ -25,7 +26,7 @@ typealias PySdkFixture = TestFixture
* Creates [Sdk] (if you only need a python path, use [PythonBinaryPath] or [CondaEnv])
*/
fun pySdkFixture(
- pythonType: PythonType<*> = PythonType.VanillaPython3,
+ pythonType: PythonType<*> = TypeVanillaPython3,
): PySdkFixture = testFixture {
val (sdk, autoClosable) = com.jetbrains.python.tools.createSdk(LocalPython(pythonType))
sdk.persist()
diff --git a/python/services/system-python/tests/com/intellij/python/junit5Tests/env/systemPython/impl/EnvTestPythonProvider.kt b/python/services/system-python/tests/com/intellij/python/junit5Tests/env/systemPython/impl/EnvTestPythonProvider.kt
index dd3043083c86..f43c11e06472 100644
--- a/python/services/system-python/tests/com/intellij/python/junit5Tests/env/systemPython/impl/EnvTestPythonProvider.kt
+++ b/python/services/system-python/tests/com/intellij/python/junit5Tests/env/systemPython/impl/EnvTestPythonProvider.kt
@@ -5,7 +5,7 @@ import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.util.Disposer
import com.intellij.platform.eel.EelApi
import com.intellij.platform.eel.provider.localEel
-import com.intellij.python.community.testFramework.testEnv.PythonType
+import com.intellij.python.community.testFramework.testEnv.TypeVanillaPython3
import com.jetbrains.python.PythonBinary
import com.jetbrains.python.systemPythonSpi.SystemPythonProvider
import kotlinx.coroutines.flow.map
@@ -17,7 +17,7 @@ import kotlinx.coroutines.flow.toSet
internal class EnvTestPythonProvider : SystemPythonProvider {
override suspend fun findSystemPythons(eelApi: EelApi): Set {
if (eelApi != localEel) return emptySet()
- return PythonType.VanillaPython3
+ return TypeVanillaPython3
.getTestEnvironments()
.map { (python, closeable) ->
Disposer.register(ApplicationManager.getApplication()) {
diff --git a/python/setup-test-environment/intellij.python.community.testFramework.testEnv.iml b/python/setup-test-environment/intellij.python.community.testFramework.testEnv.iml
index 1d54991f73f5..e10464c00407 100644
--- a/python/setup-test-environment/intellij.python.community.testFramework.testEnv.iml
+++ b/python/setup-test-environment/intellij.python.community.testFramework.testEnv.iml
@@ -15,5 +15,6 @@
+
\ No newline at end of file
diff --git a/python/setup-test-environment/src/com/intellij/python/community/testFramework/testEnv/PythonType.kt b/python/setup-test-environment/src/com/intellij/python/community/testFramework/testEnv/PythonType.kt
index 34d711fc7674..bfe9d9289e96 100644
--- a/python/setup-test-environment/src/com/intellij/python/community/testFramework/testEnv/PythonType.kt
+++ b/python/setup-test-environment/src/com/intellij/python/community/testFramework/testEnv/PythonType.kt
@@ -1,32 +1,22 @@
// 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.testFramework.testEnv
-import com.intellij.execution.processTools.getResultStdout
-import com.intellij.execution.target.local.LocalTargetEnvironmentRequest
-import com.intellij.openapi.diagnostic.logger
-import com.intellij.openapi.util.Disposer
-import com.intellij.openapi.vfs.newvfs.impl.VfsRootAccess
+import com.intellij.openapi.projectRoots.Sdk
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.jetbrains.python.PythonBinary
-import com.jetbrains.python.packaging.findCondaExecutableRelativeToEnv
-import com.jetbrains.python.sdk.conda.TargetEnvironmentRequestCommandExecutor
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
-import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
-import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
import com.jetbrains.python.venvReader.VirtualEnvReader
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
-import kotlinx.coroutines.runBlocking
import org.jetbrains.annotations.NonNls
import java.nio.file.Path
import java.util.concurrent.ConcurrentHashMap
import kotlin.Result.Companion.failure
/**
- * Gradle script installs two types of python: conda and vanilla. Env could be obtained by [getTestEnvironment] which also provides closable
+ * Gradle script installs two types of python: conda and vanilla. Env could be obtained by [createSdkClosableEnv] which also provides closable
* to clean up after the usage
*/
-sealed class PythonType(private val tag: @NonNls String? = null) {
+abstract class PythonType(private val tag: @NonNls String) {
private companion object {
val cache: MutableMap, List> = ConcurrentHashMap()
}
@@ -65,72 +55,23 @@ sealed class PythonType(private val tag: @NonNls String? = null) {
/**
- * Returns first (whatever it means) test environment and closable that must be closed after the test
+ * Returns sdk, (whatever it means) test environment and closable that must be closed after the test
*/
- suspend fun getTestEnvironment(vararg additionalTags: @NonNls String): Result> =
- getTestEnvironments(*additionalTags).firstOrNull()?.let { Result.success(it) }
+ suspend fun createSdkClosableEnv(vararg additionalTags: @NonNls String): Result> =
+ getTestEnvironments(*additionalTags).firstOrNull()?.let { (env, closable) ->
+ Result.success(Triple(createSdkFor(env), closable, env))
+ }
?: failure(AssertionError("No python found. See ${PyEnvTestSettings::class} class for more info"))
+ protected abstract suspend fun createSdkFor(t: T): Sdk
+
protected abstract suspend fun pythonPathToEnvironment(pythonBinary: PythonBinary, envDir: Path): Pair
- data object VanillaPython3 : PythonType() {
- // Python is directly executable
- override suspend fun pythonPathToEnvironment(pythonBinary: PythonBinary, envDir: Path): Pair {
- val disposable = Disposer.newDisposable("Python tests disposable for VfsRootAccess")
- // We might have python installation outside the project root, but we still need to have access to it.
- VfsRootAccess.allowRootAccess(disposable, pythonBinary.parent.toString())
- return Pair(pythonBinary, AutoCloseable {
- Disposer.dispose(disposable)
- })
- }
- }
-
-
- data object Conda : PythonType("conda") {
-
- override suspend fun pythonPathToEnvironment(pythonBinary: PythonBinary, envDir: Path): Pair {
- // First, find python binary, then calculate conda from it as env stores "conda" as a regular env
- val condaPath = findCondaExecutableRelativeToEnv(pythonBinary) ?: error("Conda root $pythonBinary doesn't have conda binary")
-
- // We'll remove then on close
- val condaEnvsBeforeTest = getCondaNames(condaPath)
-
- val cleanupCondas = AutoCloseable {
- runBlocking(Dispatchers.IO) {
- val condasToRemove = getCondaNames(condaPath)
- condasToRemove.removeAll(condaEnvsBeforeTest)
- for (envName in condasToRemove) {
- println("Removing $envName")
-
- for (arg in arrayOf("--name", "-p")) {
- val args = arrayOf(condaPath.toString(), "remove", arg, envName, "--all", "-y")
- Runtime.getRuntime().exec(args).getResultStdout().getOrElse {
- logger().warn(it)
- }
- }
- }
- }
- }
- return Pair(PyCondaEnv(PyCondaEnvIdentity.UnnamedEnv(envDir.toString(), isBase = true), condaPath.toString()), cleanupCondas)
- }
-
- private suspend fun getCondaNames(condaPath: Path) =
- PyCondaEnv.getEnvs(TargetEnvironmentRequestCommandExecutor(LocalTargetEnvironmentRequest()),
- condaPath.toString()).getOrThrow()
- .map { it.envIdentity.userReadableName }
- .toMutableSet()
- }
-
-
@RequiresBackgroundThread
private fun typeMatchesEnv(env: Path, vararg additionalTags: @NonNls String): Boolean {
val envTags = loadEnvTags(env)
-
- for (badTag in PythonType::class.sealedSubclasses.filterNot { it.isInstance(this) }.map { it.objectInstance!!.tag }) {
- if (badTag != null && badTag in envTags) return false
- }
- return (tag == null || tag in envTags) && additionalTags.all { it in envTags }
+ return tag in envTags && additionalTags.all { it in envTags }
}
}
\ No newline at end of file
diff --git a/python/setup-test-environment/src/com/intellij/python/community/testFramework/testEnv/TypeConda.kt b/python/setup-test-environment/src/com/intellij/python/community/testFramework/testEnv/TypeConda.kt
new file mode 100644
index 000000000000..2653b4130dd3
--- /dev/null
+++ b/python/setup-test-environment/src/com/intellij/python/community/testFramework/testEnv/TypeConda.kt
@@ -0,0 +1,52 @@
+// 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.testFramework.testEnv
+
+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.jetbrains.python.PythonBinary
+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
+import kotlinx.coroutines.runBlocking
+import java.nio.file.Path
+
+data object TypeConda : PythonType("conda") {
+ override suspend fun createSdkFor(env: PyCondaEnv): Sdk =
+ env.createSdkFromThisEnv(null, emptyList())
+
+ override suspend fun pythonPathToEnvironment(pythonBinary: PythonBinary, envDir: Path): Pair {
+ // First, find python binary, then calculate conda from it as env stores "conda" as a regular env
+ val condaPath = findCondaExecutableRelativeToEnv(pythonBinary) ?: error("Conda root $pythonBinary doesn't have conda binary")
+
+ // We'll remove then on close
+ val condaEnvsBeforeTest = getCondaNames(condaPath)
+
+ val cleanupCondas = AutoCloseable {
+ runBlocking(Dispatchers.IO) {
+ val condasToRemove = getCondaNames(condaPath)
+ condasToRemove.removeAll(condaEnvsBeforeTest)
+ for (envName in condasToRemove) {
+ println("Removing $envName")
+
+ for (arg in arrayOf("--name", "-p")) {
+ val args = arrayOf(condaPath.toString(), "remove", arg, envName, "--all", "-y")
+ Runtime.getRuntime().exec(args).getResultStdout().getOrElse {
+ logger().warn(it)
+ }
+ }
+ }
+ }
+ }
+ return Pair(PyCondaEnv(PyCondaEnvIdentity.UnnamedEnv(envDir.toString(), isBase = true), condaPath.toString()), cleanupCondas)
+ }
+
+ private suspend fun getCondaNames(condaPath: Path) =
+ PyCondaEnv.Companion.getEnvs(TargetEnvironmentRequestCommandExecutor(LocalTargetEnvironmentRequest()),
+ condaPath.toString()).getOrThrow()
+ .map { it.envIdentity.userReadableName }
+ .toMutableSet()
+}
\ No newline at end of file
diff --git a/python/setup-test-environment/src/com/intellij/python/community/testFramework/testEnv/TypeVanillaPython3.kt b/python/setup-test-environment/src/com/intellij/python/community/testFramework/testEnv/TypeVanillaPython3.kt
new file mode 100644
index 000000000000..10b592a9aa8e
--- /dev/null
+++ b/python/setup-test-environment/src/com/intellij/python/community/testFramework/testEnv/TypeVanillaPython3.kt
@@ -0,0 +1,28 @@
+// 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.testFramework.testEnv
+
+import com.intellij.openapi.projectRoots.Sdk
+import com.intellij.openapi.projectRoots.SdkType
+import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
+import com.intellij.openapi.util.Disposer
+import com.intellij.openapi.vfs.newvfs.impl.VfsRootAccess
+import com.intellij.testFramework.utils.vfs.refreshAndGetVirtualFile
+import com.jetbrains.python.PyNames
+import com.jetbrains.python.PythonBinary
+import java.nio.file.Path
+
+data object TypeVanillaPython3 : PythonType("python3") {
+ override suspend fun createSdkFor(python: PythonBinary): Sdk =
+ SdkConfigurationUtil.setupSdk(emptyArray(), python.refreshAndGetVirtualFile(),
+ SdkType.findByName(PyNames.PYTHON_SDK_ID_NAME)!!, null, null)
+
+ // Python is directly executable
+ override suspend fun pythonPathToEnvironment(pythonBinary: PythonBinary, envDir: Path): Pair {
+ val disposable = Disposer.newDisposable("Python tests disposable for VfsRootAccess")
+ // We might have python installation outside the project root, but we still need to have access to it.
+ VfsRootAccess.allowRootAccess(disposable, pythonBinary.parent.toString())
+ return Pair(pythonBinary, AutoCloseable {
+ Disposer.dispose(disposable)
+ })
+ }
+}
\ No newline at end of file
diff --git a/python/testFramework/src/com/jetbrains/python/tools/sdk.kt b/python/testFramework/src/com/jetbrains/python/tools/sdk.kt
index 792bad30926a..f004a43c8ac3 100644
--- a/python/testFramework/src/com/jetbrains/python/tools/sdk.kt
+++ b/python/testFramework/src/com/jetbrains/python/tools/sdk.kt
@@ -6,10 +6,9 @@ import com.intellij.execution.target.TargetEnvironmentConfiguration
import com.intellij.openapi.application.writeAction
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk
-import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
import com.intellij.python.community.testFramework.testEnv.PythonType
+import com.intellij.python.community.testFramework.testEnv.TypeVanillaPython3
import com.intellij.remote.RemoteSdkException
-import com.intellij.testFramework.utils.vfs.refreshAndGetVirtualFile
import com.jetbrains.python.sdk.PythonSdkType
import com.jetbrains.python.sdk.flavors.PyFlavorAndData
import com.jetbrains.python.sdk.flavors.PyFlavorData
@@ -24,7 +23,7 @@ import org.junit.Assume
* To be used with [createSdk]
*/
sealed class SdkCreationRequest {
- data class LocalPython(val pythonType: PythonType<*> = PythonType.VanillaPython3) : SdkCreationRequest()
+ data class LocalPython(val pythonType: PythonType<*> = TypeVanillaPython3) : SdkCreationRequest()
data class RemotePython(val targetConfig: TargetEnvironmentConfiguration) : SdkCreationRequest()
}
@@ -34,17 +33,8 @@ sealed class SdkCreationRequest {
suspend fun createSdk(request: SdkCreationRequest): Pair = withContext(Dispatchers.IO) {
when (request) {
is SdkCreationRequest.LocalPython -> {
- when (val pythonType = request.pythonType) {
- is PythonType.Conda -> {
- val (env, closable) = pythonType.getTestEnvironment().getOrThrow()
- Pair(env.createSdkFromThisEnv(null, emptyList()), closable)
- }
- is PythonType.VanillaPython3 -> {
- val (python, closable) = pythonType.getTestEnvironment().getOrThrow()
- Pair(SdkConfigurationUtil.setupSdk(emptyArray(), python.refreshAndGetVirtualFile(),
- PythonSdkType.getInstance(), null, null), closable)
- }
- }
+ val (sdk, closable, _) = request.pythonType.createSdkClosableEnv().getOrThrow()
+ Pair(sdk, closable)
}
is SdkCreationRequest.RemotePython -> {
val targetData = PyTargetAwareAdditionalData(PyFlavorAndData(PyFlavorData.Empty, UnixPythonSdkFlavor.getInstance()),
diff --git a/python/testSrc/com/jetbrains/env/python/conda/LocalCondaRule.kt b/python/testSrc/com/jetbrains/env/python/conda/LocalCondaRule.kt
index 14137f58da9a..9c2fb7100888 100644
--- a/python/testSrc/com/jetbrains/env/python/conda/LocalCondaRule.kt
+++ b/python/testSrc/com/jetbrains/env/python/conda/LocalCondaRule.kt
@@ -3,7 +3,7 @@ package com.jetbrains.env.python.conda
import com.intellij.execution.target.FullPathOnTarget
import com.intellij.execution.target.local.LocalTargetEnvironmentRequest
-import com.intellij.python.community.testFramework.testEnv.PythonType
+import com.intellij.python.community.testFramework.testEnv.TypeConda
import com.jetbrains.python.sdk.conda.TargetCommandExecutor
import com.jetbrains.python.sdk.conda.TargetEnvironmentRequestCommandExecutor
import com.jetbrains.python.sdk.flavors.conda.PyCondaCommand
@@ -34,7 +34,7 @@ class LocalCondaRule : ExternalResource() {
override fun before() {
super.before()
- val (condaPathEnv, autoCloseable) = runBlocking { PythonType.Conda.getTestEnvironment().getOrElse { throw AssumptionViolatedException("No conda found, run gradle script to install test env") } }
+ val (_, autoCloseable, condaPathEnv) = runBlocking { TypeConda.createSdkClosableEnv().getOrElse { throw AssumptionViolatedException("No conda found, run gradle script to install test env") } }
condaPath = Path.of(condaPathEnv.fullCondaPathOnTarget)
if (!condaPath.isExecutable()) {