PY-78817: Migrate a learning project to createVenvAndSdk, cover with test.

This is a top-level function used by a Misc project.

GitOrigin-RevId: 64f334813091cfc8b12bf1dfe85f1ed1ecb8637a
This commit is contained in:
Ilya.Kazakevich
2025-04-15 20:54:50 +02:00
committed by intellij-monorepo-bot
parent 80f39095c8
commit 63282e2d43
5 changed files with 153 additions and 55 deletions

View File

@@ -24,5 +24,10 @@
<orderEntry type="library" name="kotlinx-serialization-json" level="project" />
<orderEntry type="module" module-name="intellij.platform.testFramework" scope="TEST" />
<orderEntry type="module" module-name="intellij.pycharm.community" scope="TEST" />
<orderEntry type="module" module-name="intellij.python.community.services.systemPython" />
<orderEntry type="module" module-name="intellij.python.community.junit5Tests.framework" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.testFramework.junit5" scope="TEST" />
<orderEntry type="module" module-name="intellij.python.community.impl.venv" scope="TEST" />
<orderEntry type="module" module-name="intellij.python.community.services.internal.impl" scope="TEST" />
</component>
</module>

View File

@@ -4,36 +4,40 @@ package com.intellij.python.featuresTrainer.ift
import com.intellij.ide.impl.OpenProjectTask
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.invokeLater
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.util.UserDataHolder
import com.intellij.openapi.util.UserDataHolderBase
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.intellij.util.concurrency.annotations.RequiresReadLock
import com.intellij.util.ui.FormBuilder
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PySdkBundle
import com.jetbrains.python.Result
import com.jetbrains.python.configuration.PyConfigurableInterpreterList
import com.jetbrains.python.errorProcessing.ErrorSink
import com.jetbrains.python.inspections.PyInterpreterInspection
import com.jetbrains.python.newProject.steps.ProjectSpecificSettingsStep
import com.jetbrains.python.sdk.*
import com.jetbrains.python.projectCreation.createVenvAndSdk
import com.jetbrains.python.sdk.PySdkToInstall
import com.jetbrains.python.sdk.add.PySdkPathChoosingComboBox
import com.jetbrains.python.sdk.add.addBaseInterpretersAsync
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfiguration.setReadyToUseSdk
import com.jetbrains.python.sdk.configuration.createVirtualEnvAndSdkSynchronously
import com.jetbrains.python.sdk.configuration.findPreferredVirtualEnvBaseSdk
import com.jetbrains.python.sdk.findBaseSdks
import com.jetbrains.python.sdk.pythonSdk
import com.jetbrains.python.statistics.modules
import com.jetbrains.python.util.ShowingMessageErrorSync
import training.dsl.LessonContext
import training.lang.AbstractLangSupport
import training.learn.CourseManager
import training.learn.course.KLesson
import training.learn.exceptons.NoSdkException
import training.project.ProjectUtils
import training.project.ReadMeCreator
import training.statistic.LearningInternalProblems
@@ -47,7 +51,7 @@ import javax.swing.JComponent
import javax.swing.JLabel
import kotlin.math.max
internal class PythonLangSupport : AbstractLangSupport() {
internal class PythonLangSupport(private val errorSink: ErrorSink = ShowingMessageErrorSync) : AbstractLangSupport() {
override val contentRootDirectoryName = "PyCharmLearningProject"
@@ -68,9 +72,11 @@ internal class PythonLangSupport : AbstractLangSupport() {
override fun blockProjectFileModification(project: Project, file: VirtualFile): Boolean = true
override val readMeCreator = ReadMeCreator()
override fun installAndOpenLearningProject(contentRoot: Path,
projectToClose: Project?,
postInitCallback: (learnProject: Project) -> Unit) {
override fun installAndOpenLearningProject(
contentRoot: Path,
projectToClose: Project?,
postInitCallback: (learnProject: Project) -> Unit,
) {
// if we open project with isProjectCreatedFromWizard flag as true, PythonSdkConfigurator will not run and configure our sdks,
// and we will configure it individually without any race conditions
val openProjectTask = OpenProjectTask {
@@ -80,56 +86,23 @@ internal class PythonLangSupport : AbstractLangSupport() {
ProjectUtils.simpleInstallAndOpenLearningProject(contentRoot, this, openProjectTask, postInitCallback)
}
override fun getSdkForProject(project: Project, selectedSdk: Sdk?): Sdk? {
if (selectedSdk != null) {
val module = project.modules.first()
val existingSdks = getExistingSdks()
return applyBaseSdk(project, selectedSdk, existingSdks, module)
}
if (project.pythonSdk != null) return null // sdk already configured
// Run in parallel, because we can not wait for SDK here
ApplicationManager.getApplication().executeOnPooledThread {
createAndSetVenvSdk(project)
}
return null
}
@RequiresBackgroundThread
private fun createAndSetVenvSdk(project: Project) {
val module = project.modules.first()
val existingSdks = getExistingSdks()
val baseSdks = findBaseSdks(existingSdks, module, project)
val preferredSdk = findPreferredVirtualEnvBaseSdk(baseSdks) ?: return
invokeLater {
val venvSdk = applyBaseSdk(project, preferredSdk, existingSdks, module)
if (venvSdk != null) {
applyProjectSdk(venvSdk, project)
@Throws(NoSdkException::class)
@RequiresEdt
override fun getSdkForProject(project: Project, selectedSdk: Sdk?): Sdk = runWithModalProgressBlocking(project, "...") {
when (val r = createVenvAndSdk(project)) {
is Result.Failure -> {
errorSink.emit(r.error)
null
}
}
is Result.Success -> r.result
} ?: throw NoSdkException()
}
private fun applyBaseSdk(project: Project,
preferredSdk: Sdk,
existingSdks: List<Sdk>,
module: Module?): Sdk? {
val venvRoot = FileUtil.toSystemDependentName(PySdkSettings.instance.getPreferredVirtualEnvBasePath(project.basePath))
val venvSdk = createVirtualEnvAndSdkSynchronously(preferredSdk, existingSdks, venvRoot, project.basePath, project, module, project)
return venvSdk.also {
SdkConfigurationUtil.addSdk(it)
}
}
@RequiresEdt
override fun applyProjectSdk(sdk: Sdk, project: Project) {
setReadyToUseSdk(project, project.modules.first(), sdk)
}
private fun getExistingSdks(): List<Sdk> {
return PyConfigurableInterpreterList.getInstance(null).allPythonSdks
.sortedWith(PreferredSdkComparator.INSTANCE)
}
override fun checkSdk(sdk: Sdk?, project: Project) {
}
@@ -223,4 +196,18 @@ internal class PythonLangSupport : AbstractLangSupport() {
}
}
}
@RequiresReadLock
override fun getProtectedDirs(project: Project): Set<Path> {
return project.getSdks().mapNotNull { it.homePath }.map { Path.of(it) }.toSet()
}
}
@RequiresReadLock
private fun Project.getSdks(): Set<Sdk> {
val projectSdk = ProjectRootManager.getInstance(this).projectSdk
val moduleSdks = modules.mapNotNull {
ModuleRootManager.getInstance(it).sdk
}
return (moduleSdks + (projectSdk?.let { listOf(it) } ?: emptyList())).toSet()
}

View File

@@ -0,0 +1,93 @@
package com.intellij.python.junit5Tests.env
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.writeAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.python.community.impl.venv.createVenv
import com.intellij.python.community.services.internal.impl.PythonWithLanguageLevelImpl
import com.intellij.python.featuresTrainer.ift.PythonLangSupport
import com.intellij.python.junit5Tests.framework.env.PyEnvTestCase
import com.intellij.python.junit5Tests.framework.env.PythonBinaryPath
import com.intellij.testFramework.common.timeoutRunBlocking
import com.jetbrains.python.PythonBinary
import com.jetbrains.python.errorProcessing.ErrorSink
import com.jetbrains.python.sdk.pythonSdk
import com.jetbrains.python.venvReader.VirtualEnvReader.Companion.DEFAULT_VIRTUALENV_DIRNAME
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.io.TempDir
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
import training.learn.NewLearnProjectUtil
import training.project.ProjectUtils
import java.nio.file.Path
import kotlin.time.Duration.Companion.minutes
@PyEnvTestCase
class PythonLangSupportTest {
companion object {
@JvmStatic
@TempDir
lateinit var temporarySystemPath: Path
@JvmStatic
@BeforeAll
fun setUp() {
ProjectUtils.customSystemPath = temporarySystemPath
}
@JvmStatic
@AfterAll
fun tearDown() {
ProjectUtils.customSystemPath = null
}
}
@ParameterizedTest
@ValueSource(booleans = [true, false])
fun ensureVenvCreatedTest(venvAlreadyExists: Boolean, @PythonBinaryPath python: PythonBinary): Unit = timeoutRunBlocking(10.minutes) {
val learningProjectsPath = ProjectUtils.learningProjectsPath
assert(learningProjectsPath.startsWith(temporarySystemPath)) { "$learningProjectsPath must reside in $temporarySystemPath" }
val sut = PythonLangSupport(ErrorSink {
Assertions.fail(it.message)
})
if (venvAlreadyExists) {
val venvPath = learningProjectsPath.resolve(DEFAULT_VIRTUALENV_DIRNAME)
createVenv(python, venvPath).orThrow()
}
val sema = CompletableDeferred<Project>()
withContext(Dispatchers.EDT) {
NewLearnProjectUtil.createLearnProject(null, sut, null) { project ->
sema.complete(project)
}
}
val project = sema.await()
withContext(Dispatchers.IO) {
sut.cleanupBeforeLessons(project)
}
val sdk = project.pythonSdk!!
try {
val pythonBinary = Path.of(sdk.homePath!!)
Assertions.assertTrue(PythonWithLanguageLevelImpl.createByPythonBinary(pythonBinary).orThrow().languageLevel.isPy3K, "Sdk is broken")
}
finally {
writeAction {
ProjectJdkTable.getInstance().removeJdk(sdk)
}
withContext(Dispatchers.EDT) {
ProjectManager.getInstance().closeAndDispose(project)
}
}
}
}