From f21b818c35a327f72dda88a1829866804b2960ce Mon Sep 17 00:00:00 2001 From: "Ilya.Kazakevich" Date: Fri, 4 Apr 2025 03:48:24 +0200 Subject: [PATCH] Python: tour: two classes merged as prep for PY-78817. No need to have two classes GitOrigin-RevId: 2da0b9b4c9ac7354cb3d165177f717581a6f4ff4 --- .../ift/PythonBasedLangSupport.kt | 206 ------------------ .../featuresTrainer/ift/PythonLangSupport.kt | 202 ++++++++++++++++- .../featuresTrainer/ift/package-info.java | 4 + .../PythonLessonsAndTipsIntegrationTest.kt | 7 +- 4 files changed, 208 insertions(+), 211 deletions(-) delete mode 100644 python/python-features-trainer/src/com/intellij/python/featuresTrainer/ift/PythonBasedLangSupport.kt create mode 100644 python/python-features-trainer/src/com/intellij/python/featuresTrainer/ift/package-info.java diff --git a/python/python-features-trainer/src/com/intellij/python/featuresTrainer/ift/PythonBasedLangSupport.kt b/python/python-features-trainer/src/com/intellij/python/featuresTrainer/ift/PythonBasedLangSupport.kt deleted file mode 100644 index bd442c8fdf13..000000000000 --- a/python/python-features-trainer/src/com/intellij/python/featuresTrainer/ift/PythonBasedLangSupport.kt +++ /dev/null @@ -1,206 +0,0 @@ -// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -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.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.util.concurrency.annotations.RequiresBackgroundThread -import com.intellij.util.concurrency.annotations.RequiresEdt -import com.intellij.util.ui.FormBuilder -import com.jetbrains.python.PyBundle -import com.jetbrains.python.PySdkBundle -import com.jetbrains.python.configuration.PyConfigurableInterpreterList -import com.jetbrains.python.inspections.PyInterpreterInspection -import com.jetbrains.python.newProject.steps.ProjectSpecificSettingsStep -import com.jetbrains.python.sdk.* -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.statistics.modules -import training.dsl.LessonContext -import training.lang.AbstractLangSupport -import training.learn.CourseManager -import training.learn.course.KLesson -import training.project.ProjectUtils -import training.project.ReadMeCreator -import training.statistic.LearningInternalProblems -import training.statistic.LessonStartingWay -import training.ui.LearningUiManager -import training.util.isLearningProject -import java.awt.Dimension -import java.nio.file.Path -import javax.swing.JComponent -import javax.swing.JLabel -import kotlin.math.max - -abstract class PythonBasedLangSupport : AbstractLangSupport() { - override val readMeCreator = ReadMeCreator() - - 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 { - this.projectToClose = projectToClose - isProjectCreatedWithWizard = true - } - 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) - } - } - } - - private fun applyBaseSdk(project: Project, - preferredSdk: Sdk, - existingSdks: List, - 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 { - return PyConfigurableInterpreterList.getInstance(null).allPythonSdks - .sortedWith(PreferredSdkComparator.INSTANCE) - } - - override fun checkSdk(sdk: Sdk?, project: Project) { - } - - override val sampleFilePath = "src/sandbox.py" - - override fun startFromWelcomeFrame(startCallback: (Sdk?) -> Unit) { - val allExistingSdks = listOf(*PyConfigurableInterpreterList.getInstance(null).model.sdks) - val existingSdks = ProjectSpecificSettingsStep.getValidPythonSdks(allExistingSdks) - - ApplicationManager.getApplication().executeOnPooledThread { - val context = UserDataHolderBase() - val baseSdks = findBaseSdks(existingSdks, null, context) - - invokeLater { - if (baseSdks.isEmpty()) { - val sdk = showSdkChoosingDialog(existingSdks, context) - if (sdk != null) { - startCallback(sdk) - } - } - else startCallback(null) - } - } - } - - private fun showSdkChoosingDialog(existingSdks: List, context: UserDataHolder): Sdk? { - val baseSdkField = PySdkPathChoosingComboBox() - - val warningPlaceholder = JLabel() - val formPanel = FormBuilder.createFormBuilder() - .addComponent(warningPlaceholder) - .addLabeledComponent(PySdkBundle.message("python.venv.base.label"), baseSdkField) - .panel - - formPanel.preferredSize = Dimension(max(formPanel.preferredSize.width, 500), formPanel.preferredSize.height) - val dialog = object : DialogWrapper(ProjectManager.getInstance().defaultProject) { - override fun createCenterPanel(): JComponent = formPanel - - init { - title = PyBundle.message("sdk.select.path") - init() - } - } - - addBaseInterpretersAsync(baseSdkField, existingSdks, null, context) { - val selectedSdk = baseSdkField.selectedSdk - if (selectedSdk is PySdkToInstall) { - val installationWarning = selectedSdk.getInstallationWarning(Messages.getOkButton()) - warningPlaceholder.text = "$installationWarning" - } - else { - warningPlaceholder.text = "" - } - } - - dialog.title = PythonLessonsBundle.message("choose.python.sdk.to.start.learning.header") - return if (dialog.showAndGet()) { - baseSdkField.selectedSdk - } - else null - } - - override fun isSdkConfigured(project: Project): Boolean = project.pythonSdk != null - - override val sdkConfigurationTasks: LessonContext.(lesson: KLesson) -> Unit = { lesson -> - task { - stateCheck { - isSdkConfigured(project) - } - val configureCallbackId = LearningUiManager.addCallback { - val module = project.modules.singleOrNull() - PyInterpreterInspection.InterpreterSettingsQuickFix.showPythonInterpreterSettings(project, module) - } - if (useUserProjects || isLearningProject(project, primaryLanguage)) { - showWarning(PythonLessonsBundle.message("no.interpreter.in.learning.project", configureCallbackId), - problem = LearningInternalProblems.NO_SDK_CONFIGURED) { - !isSdkConfigured(project) - } - } - else { - // for Scratch lessons in the non-learning project - val openCallbackId = LearningUiManager.addCallback { - CourseManager.instance.openLesson(project, lesson, LessonStartingWay.NO_SDK_RESTART, - forceStartLesson = true, - forceOpenLearningProject = true) - } - showWarning(PythonLessonsBundle.message("no.interpreter.in.user.project", openCallbackId, configureCallbackId)) { - !isSdkConfigured(project) - } - } - } - } -} diff --git a/python/python-features-trainer/src/com/intellij/python/featuresTrainer/ift/PythonLangSupport.kt b/python/python-features-trainer/src/com/intellij/python/featuresTrainer/ift/PythonLangSupport.kt index c2e79dd225be..f1901cb44dd5 100644 --- a/python/python-features-trainer/src/com/intellij/python/featuresTrainer/ift/PythonLangSupport.kt +++ b/python/python-features-trainer/src/com/intellij/python/featuresTrainer/ift/PythonLangSupport.kt @@ -1,13 +1,54 @@ // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 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.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.util.concurrency.annotations.RequiresEdt +import com.intellij.util.ui.FormBuilder +import com.jetbrains.python.PyBundle +import com.jetbrains.python.PySdkBundle +import com.jetbrains.python.configuration.PyConfigurableInterpreterList +import com.jetbrains.python.inspections.PyInterpreterInspection +import com.jetbrains.python.newProject.steps.ProjectSpecificSettingsStep +import com.jetbrains.python.sdk.* +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.statistics.modules +import training.dsl.LessonContext +import training.lang.AbstractLangSupport +import training.learn.CourseManager +import training.learn.course.KLesson import training.project.ProjectUtils import training.project.ReadMeCreator +import training.statistic.LearningInternalProblems +import training.statistic.LessonStartingWay +import training.ui.LearningUiManager import training.util.getFeedbackLink +import training.util.isLearningProject +import java.awt.Dimension +import java.nio.file.Path +import javax.swing.JComponent +import javax.swing.JLabel +import kotlin.math.max + +internal class PythonLangSupport : AbstractLangSupport() { -class PythonLangSupport : PythonBasedLangSupport() { override val contentRootDirectoryName = "PyCharmLearningProject" override val primaryLanguage = "Python" @@ -20,11 +61,166 @@ class PythonLangSupport : PythonBasedLangSupport() { override val langCourseFeedback get() = getFeedbackLink(this, false) - override val readMeCreator = ReadMeCreator() - override fun applyToProjectAfterConfigure(): (Project) -> Unit = { project -> ProjectUtils.markDirectoryAsSourcesRoot(project, sourcesDirectoryName) } override fun blockProjectFileModification(project: Project, file: VirtualFile): Boolean = true + override val readMeCreator = ReadMeCreator() + + 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 { + this.projectToClose = projectToClose + isProjectCreatedWithWizard = true + } + 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) + } + } + } + + private fun applyBaseSdk(project: Project, + preferredSdk: Sdk, + existingSdks: List, + 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 { + return PyConfigurableInterpreterList.getInstance(null).allPythonSdks + .sortedWith(PreferredSdkComparator.INSTANCE) + } + + override fun checkSdk(sdk: Sdk?, project: Project) { + } + + override val sampleFilePath = "src/sandbox.py" + + override fun startFromWelcomeFrame(startCallback: (Sdk?) -> Unit) { + val allExistingSdks = listOf(*PyConfigurableInterpreterList.getInstance(null).model.sdks) + val existingSdks = ProjectSpecificSettingsStep.getValidPythonSdks(allExistingSdks) + + ApplicationManager.getApplication().executeOnPooledThread { + val context = UserDataHolderBase() + val baseSdks = findBaseSdks(existingSdks, null, context) + + invokeLater { + if (baseSdks.isEmpty()) { + val sdk = showSdkChoosingDialog(existingSdks, context) + if (sdk != null) { + startCallback(sdk) + } + } + else startCallback(null) + } + } + } + + private fun showSdkChoosingDialog(existingSdks: List, context: UserDataHolder): Sdk? { + val baseSdkField = PySdkPathChoosingComboBox() + + val warningPlaceholder = JLabel() + val formPanel = FormBuilder.createFormBuilder() + .addComponent(warningPlaceholder) + .addLabeledComponent(PySdkBundle.message("python.venv.base.label"), baseSdkField) + .panel + + formPanel.preferredSize = Dimension(max(formPanel.preferredSize.width, 500), formPanel.preferredSize.height) + val dialog = object : DialogWrapper(ProjectManager.getInstance().defaultProject) { + override fun createCenterPanel(): JComponent = formPanel + + init { + title = PyBundle.message("sdk.select.path") + init() + } + } + + addBaseInterpretersAsync(baseSdkField, existingSdks, null, context) { + val selectedSdk = baseSdkField.selectedSdk + if (selectedSdk is PySdkToInstall) { + val installationWarning = selectedSdk.getInstallationWarning(Messages.getOkButton()) + warningPlaceholder.text = "$installationWarning" + } + else { + warningPlaceholder.text = "" + } + } + + dialog.title = PythonLessonsBundle.message("choose.python.sdk.to.start.learning.header") + return if (dialog.showAndGet()) { + baseSdkField.selectedSdk + } + else null + } + + override fun isSdkConfigured(project: Project): Boolean = project.pythonSdk != null + + override val sdkConfigurationTasks: LessonContext.(lesson: KLesson) -> Unit = { lesson -> + task { + stateCheck { + isSdkConfigured(project) + } + val configureCallbackId = LearningUiManager.addCallback { + val module = project.modules.singleOrNull() + PyInterpreterInspection.InterpreterSettingsQuickFix.showPythonInterpreterSettings(project, module) + } + if (useUserProjects || isLearningProject(project, primaryLanguage)) { + showWarning(PythonLessonsBundle.message("no.interpreter.in.learning.project", configureCallbackId), + problem = LearningInternalProblems.NO_SDK_CONFIGURED) { + !isSdkConfigured(project) + } + } + else { + // for Scratch lessons in the non-learning project + val openCallbackId = LearningUiManager.addCallback { + CourseManager.instance.openLesson(project, lesson, LessonStartingWay.NO_SDK_RESTART, + forceStartLesson = true, + forceOpenLearningProject = true) + } + showWarning(PythonLessonsBundle.message("no.interpreter.in.user.project", openCallbackId, configureCallbackId)) { + !isSdkConfigured(project) + } + } + } + } } diff --git a/python/python-features-trainer/src/com/intellij/python/featuresTrainer/ift/package-info.java b/python/python-features-trainer/src/com/intellij/python/featuresTrainer/ift/package-info.java new file mode 100644 index 000000000000..dbc08f9cb823 --- /dev/null +++ b/python/python-features-trainer/src/com/intellij/python/featuresTrainer/ift/package-info.java @@ -0,0 +1,4 @@ +@ApiStatus.Internal +package com.intellij.python.featuresTrainer.ift; + +import org.jetbrains.annotations.ApiStatus; \ No newline at end of file diff --git a/python/python-features-trainer/testSrc/com/jetbrains/python/featureTraining/ift/PythonLessonsAndTipsIntegrationTest.kt b/python/python-features-trainer/testSrc/com/jetbrains/python/featureTraining/ift/PythonLessonsAndTipsIntegrationTest.kt index c0e72439dda0..d81a40f43d0e 100644 --- a/python/python-features-trainer/testSrc/com/jetbrains/python/featureTraining/ift/PythonLessonsAndTipsIntegrationTest.kt +++ b/python/python-features-trainer/testSrc/com/jetbrains/python/featureTraining/ift/PythonLessonsAndTipsIntegrationTest.kt @@ -1,12 +1,15 @@ -package com.intellij.python.featuresTrainer.ift +package com.jetbrains.python.featureTraining.ift +import com.intellij.python.featuresTrainer.ift.PythonLangSupport +import com.intellij.python.featuresTrainer.ift.PythonLearningCourse import org.junit.runner.RunWith import org.junit.runners.JUnit4 +import training.lang.LangSupport import training.simple.LessonsAndTipsIntegrationTest @RunWith(JUnit4::class) class PythonLessonsAndTipsIntegrationTest : LessonsAndTipsIntegrationTest() { override val languageId = "Python" - override val languageSupport = PythonLangSupport() + override val languageSupport: LangSupport? = PythonLangSupport() override val learningCourse = PythonLearningCourse() } \ No newline at end of file