From b3a87dc49ecd21fc54fa3970ec3378e1d6c63893 Mon Sep 17 00:00:00 2001 From: Ilya Kazakevich Date: Thu, 17 Apr 2025 12:13:41 +0000 Subject: [PATCH] PY-78817: Compound cherry-pick for PyCharm and training API changes. Reviewed by training API devs: IJ-MR-160099. Merge-request: IJ-MR-160630 Merged-by: Ilya Kazakevich GitOrigin-RevId: aa1b2848c8292dafb2f42a805f173ddd505430dc --- .../util/intellij.platform.util.tests.iml | 1 + .../training/project/ProjectUtilsTest.kt | 53 +++++ .../src/training/lang/LangSupport.kt | 10 + .../src/training/project/ProjectUtils.kt | 93 +++++--- .../intellij.pycharm.community.ide.impl.iml | 3 + ...armCommunityCustomizationBundle.properties | 8 +- .../PyEnvironmentYmlSdkConfiguration.kt | 4 +- .../configuration/PyHatchSdkConfiguration.kt | 3 +- ...equirementsTxtOrSetupPySdkConfiguration.kt | 19 +- .../PyTemporarilyIgnoredFileProvider.kt | 11 +- .../impl/miscProject/impl/PyMiscFileAction.kt | 9 +- .../ide/impl/miscProject/impl/lib.kt | 151 ++----------- .../PyTemporarilyIgnoredFileProviderTest.kt | 33 +++ .../messages/PyBundle.properties | 16 +- .../intellij.python.featuresTrainer.iml | 5 + .../ift/PythonBasedLangSupport.kt | 204 ------------------ .../featuresTrainer/ift/PythonLangSupport.kt | 189 +++++++++++++++- .../featuresTrainer/ift/package-info.java | 4 + .../PythonLessonsAndTipsIntegrationTest.kt | 7 +- .../projectCreation/venvWithSdkCreator.kt | 176 +++++++++++++++ .../PyProjectVirtualEnvConfiguration.kt | 7 + .../python/util/ShowingMessageErrorSync.kt | 8 +- 22 files changed, 611 insertions(+), 403 deletions(-) create mode 100644 platform/util/testSrc/training/project/ProjectUtilsTest.kt create mode 100644 python/ide/impl/tests/com/intellij/python/junit5Tests/unit/PyTemporarilyIgnoredFileProviderTest.kt 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 create mode 100644 python/src/com/jetbrains/python/projectCreation/venvWithSdkCreator.kt diff --git a/platform/util/intellij.platform.util.tests.iml b/platform/util/intellij.platform.util.tests.iml index da2beed33791..3e0db7f53f8c 100644 --- a/platform/util/intellij.platform.util.tests.iml +++ b/platform/util/intellij.platform.util.tests.iml @@ -46,5 +46,6 @@ + \ No newline at end of file diff --git a/platform/util/testSrc/training/project/ProjectUtilsTest.kt b/platform/util/testSrc/training/project/ProjectUtilsTest.kt new file mode 100644 index 000000000000..8ed385c0ad0d --- /dev/null +++ b/platform/util/testSrc/training/project/ProjectUtilsTest.kt @@ -0,0 +1,53 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package training.project + +import com.intellij.openapi.project.ProjectManager +import com.intellij.testFramework.common.timeoutRunBlocking +import com.intellij.testFramework.junit5.TestApplication +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import org.mockito.Mockito +import org.mockito.Mockito.`when` +import org.mockito.kotlin.any +import training.lang.LangManager +import training.lang.LangSupport +import java.nio.file.Path +import kotlin.io.path.* + +@TestApplication +class ProjectUtilsTest { + private companion object { + private const val LANG = "FakeLang" + } + + // Ensure that a project is cleared, but protected files aren't removed + @Test + fun testRestoreProject(@TempDir root: Path): Unit = timeoutRunBlocking { + withContext(Dispatchers.IO) { + val protectedFile = root.resolve("excludedDir").resolve("excludedFile").createParentDirectories().createFile() + + val lang = Mockito.mock() + `when`(lang.getLearningProjectPath(any())).thenReturn(root) + `when`(lang.getContentRootPath(any())).thenReturn(root) + `when`(lang.getProtectedDirs(any())).thenReturn(setOf(protectedFile.parent)) + `when`(lang.primaryLanguage).thenReturn(LANG) + `when`(lang.contentRootDirectoryName).thenReturn(LANG) + + val badFile = root.resolve("junk_folder").resolve("file.txt").createParentDirectories().createFile() + val goodFiles = arrayOf( + protectedFile, + root.resolve("venv").createDirectory(), + root.resolve(".git").resolve("file").createParentDirectories().createFile()) + + LangManager.getInstance().setLearningProjectPath(lang, root.pathString) + ProjectUtils.restoreProject(lang, ProjectManager.getInstance().defaultProject) + assertFalse(badFile.exists(), "$badFile should have been deleted") + for (goodFile in goodFiles) { + assertTrue(goodFile.exists(), "$goodFile shouldn't have been deleted") + } + } + } +} \ No newline at end of file diff --git a/plugins/ide-features-trainer/src/training/lang/LangSupport.kt b/plugins/ide-features-trainer/src/training/lang/LangSupport.kt index a81c043c5cd9..c3764ef293b1 100644 --- a/plugins/ide-features-trainer/src/training/lang/LangSupport.kt +++ b/plugins/ide-features-trainer/src/training/lang/LangSupport.kt @@ -7,6 +7,7 @@ import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.wm.ToolWindowAnchor import com.intellij.util.concurrency.annotations.RequiresBackgroundThread +import com.intellij.util.concurrency.annotations.RequiresReadLock import training.dsl.LessonContext import training.learn.course.KLesson import training.learn.exceptons.InvalidSdkException @@ -114,4 +115,13 @@ interface LangSupport { fun getContentRootPath(projectPath: Path): Path { return projectPath } + + /** + * When a project dir is cleared using [training.project.ProjectUtils.restoreProject], + * these paths along with their descenders are protected. + * + * For example: `.venv` in Python shouldn't be deleted as it has SDK. + */ + @RequiresReadLock + fun getProtectedDirs(project: Project): Set = emptySet() } diff --git a/plugins/ide-features-trainer/src/training/project/ProjectUtils.kt b/plugins/ide-features-trainer/src/training/project/ProjectUtils.kt index 82695fadcc6a..397b75e32b59 100644 --- a/plugins/ide-features-trainer/src/training/project/ProjectUtils.kt +++ b/plugins/ide-features-trainer/src/training/project/ProjectUtils.kt @@ -20,8 +20,11 @@ import com.intellij.openapi.roots.ModuleRootManager import com.intellij.openapi.ui.Messages import com.intellij.openapi.vfs.* import com.intellij.util.Consumer +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.intellij.util.io.createDirectories import com.intellij.util.io.delete +import org.jetbrains.annotations.ApiStatus +import org.jetbrains.annotations.TestOnly import training.lang.LangManager import training.lang.LangSupport import training.learn.LearnBundle @@ -30,21 +33,34 @@ import java.io.File import java.io.FileFilter import java.io.IOException import java.io.PrintWriter +import java.nio.file.FileVisitResult +import java.nio.file.FileVisitor import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths +import java.nio.file.attribute.BasicFileAttributes import java.util.concurrent.CompletableFuture import kotlin.io.path.exists import kotlin.io.path.getLastModifiedTime import kotlin.io.path.isDirectory import kotlin.io.path.name +import kotlin.io.path.pathString object ProjectUtils { private const val LEARNING_PROJECT_MODIFICATION = "LEARNING_PROJECT_MODIFICATION" private const val FEATURE_TRAINER_VERSION = "feature-trainer-version.txt" + private val protectedDirNames = hashSetOf("idea", ".git", "git", "venv") + + + @ApiStatus.Internal + @Volatile + var customSystemPath: Path? = null + @TestOnly + set val learningProjectsPath: Path - get() = Paths.get(PathManager.getSystemPath(), "demo") + get() = Paths.get(customSystemPath?.pathString ?: PathManager.getSystemPath(), "demo") + /** * For example: @@ -134,10 +150,12 @@ object ProjectUtils { ?: error("Cannot to convert $projectPath to virtual file") } - fun simpleInstallAndOpenLearningProject(contentRoot: Path, - langSupport: LangSupport, - openProjectTask: OpenProjectTask, - postInitCallback: (learnProject: Project) -> Unit) { + fun simpleInstallAndOpenLearningProject( + contentRoot: Path, + langSupport: LangSupport, + openProjectTask: OpenProjectTask, + postInitCallback: (learnProject: Project) -> Unit, + ) { val actualContentRoot = copyLearningProjectFiles(contentRoot, langSupport) if (actualContentRoot == null) return createVersionFile(actualContentRoot) @@ -151,10 +169,12 @@ object ProjectUtils { PropertiesComponent.getInstance(it).setValue(LEARNING_PROJECT_MODIFICATION, System.currentTimeMillis().toString()) } - private fun openOrImportLearningProject(contentRoot: Path, - openProjectTask: OpenProjectTask, - langSupport: LangSupport, - postInitCallback: (learnProject: Project) -> Unit) { + private fun openOrImportLearningProject( + contentRoot: Path, + openProjectTask: OpenProjectTask, + langSupport: LangSupport, + postInitCallback: (learnProject: Project) -> Unit, + ) { val projectDirectoryVirtualFile = LocalFileSystem.getInstance().refreshAndFindFileByNioFile( langSupport.getLearningProjectPath(contentRoot) ) ?: error("Copied Learn project folder is null") @@ -270,6 +290,7 @@ object ProjectUtils { project.save() } + @RequiresBackgroundThread fun restoreProject(languageSupport: LangSupport, project: Project) { val done = CompletableFuture() AppUIExecutor.onWriteThread().withDocumentsCommitted(project).submit { @@ -280,29 +301,44 @@ object ProjectUtils { val directories = mutableListOf() val root = getProjectRoot(languageSupport) val contentRootPath = languageSupport.getContentRootPath(root.toNioPath()) + val protectedPaths = languageSupport.getProtectedDirs(project) - for (path in Files.walk(contentRootPath)) { - if (contentRootPath.relativize(path).any { file -> - file.name == ".idea" || - file.name == "git" || - file.name == ".git" || - file.name == ".gitignore" || - file.name == "venv" || - file.name == FEATURE_TRAINER_VERSION || - file.name.endsWith(".iml") - }) continue - if (path.isDirectory()) { - directories.add(path) - } - else { - if (path.getLastModifiedTime().toMillis() > stamp) { - needReplace.add(path) + + Files.walkFileTree(contentRootPath, object : FileVisitor { + override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult { + if (dir == contentRootPath) { + return FileVisitResult.CONTINUE + } + return if (dir.name in protectedDirNames || protectedPaths.any { it.startsWith(dir) }) { + FileVisitResult.SKIP_SUBTREE } else { - validContent.add(path) + directories.add(dir) + FileVisitResult.CONTINUE } } - } + + override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult { + val fileName = file.name + if (fileName == FEATURE_TRAINER_VERSION || + fileName.startsWith(".git") || + protectedPaths.contains(file) || + fileName.endsWith(".iml")) { + return FileVisitResult.CONTINUE + } + if (file.getLastModifiedTime().toMillis() > stamp) { + needReplace.add(file) + } + else { + validContent.add(file) + } + return FileVisitResult.CONTINUE + } + + override fun visitFileFailed(file: Path?, exc: IOException): FileVisitResult = FileVisitResult.CONTINUE + + override fun postVisitDirectory(dir: Path?, exc: IOException?): FileVisitResult = FileVisitResult.CONTINUE + }) var modified = false @@ -348,4 +384,5 @@ object ProjectUtils { } return false } -} \ No newline at end of file +} + diff --git a/python/ide/impl/intellij.pycharm.community.ide.impl.iml b/python/ide/impl/intellij.pycharm.community.ide.impl.iml index acecef717105..a2736ee52a04 100644 --- a/python/ide/impl/intellij.pycharm.community.ide.impl.iml +++ b/python/ide/impl/intellij.pycharm.community.ide.impl.iml @@ -5,6 +5,7 @@ + @@ -39,5 +40,7 @@ + + \ No newline at end of file diff --git a/python/ide/impl/resources/messages/PyCharmCommunityCustomizationBundle.properties b/python/ide/impl/resources/messages/PyCharmCommunityCustomizationBundle.properties index 579d15c4ad6b..b8b10f5d1473 100644 --- a/python/ide/impl/resources/messages/PyCharmCommunityCustomizationBundle.properties +++ b/python/ide/impl/resources/messages/PyCharmCommunityCustomizationBundle.properties @@ -70,13 +70,7 @@ misc.script.text=New Script misc.project.generating.env=Preparing environment misc.project.filling.file=Filling the template file -misc.project.error.title=Error Creating Scratch File -misc.project.error.install.python=Python could not be installed {0}. Try restarting PyCharm. If it does not help, install Python manually and try again. -misc.project.error.install.not.supported=Python is not installed and this target does not support installation. Please, install it manually. -misc.project.error.create.venv=Failed to create virtual environment: {0}. Please delete {1} if it exists, and try again. You can also create virtual environment in {1} and try again. Check logs for more info. -misc.project.error.create.dir=Could not create {0} because of {1}. Please create it manually, and try again. - -misc.project.error.all.pythons.bad=No usable Python installations were found on your system. Please install Python manually. Check logs for more info. +misc.project.error.create.dir=Could not create {0} because of {1}. Please create it manually and try again. misc.no.python.found=No Python Interpreter Found on Your System. misc.install.python.question=Do you want to install the latest Python? \ No newline at end of file diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyEnvironmentYmlSdkConfiguration.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyEnvironmentYmlSdkConfiguration.kt index 782d42107d6a..257851169928 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyEnvironmentYmlSdkConfiguration.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyEnvironmentYmlSdkConfiguration.kt @@ -56,13 +56,13 @@ import javax.swing.JPanel */ internal class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExtension { private val LOGGER = Logger.getInstance(PyEnvironmentYmlSdkConfiguration::class.java) - + @RequiresBackgroundThread override fun createAndAddSdkForConfigurator(module: Module): Sdk? = createAndAddSdk(module, Source.CONFIGURATOR) override fun getIntention(module: Module): @IntentionName String? = getEnvironmentYml(module)?.let { PyCharmCommunityCustomizationBundle.message("sdk.create.condaenv.suggestion") } - + @RequiresBackgroundThread override fun createAndAddSdkForInspection(module: Module): Sdk? = createAndAddSdk(module, Source.INSPECTION) private fun getEnvironmentYml(module: Module) = PyUtil.findInRoots(module, "environment.yml") diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyHatchSdkConfiguration.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyHatchSdkConfiguration.kt index f3069c241511..c4135f77c239 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyHatchSdkConfiguration.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyHatchSdkConfiguration.kt @@ -9,6 +9,7 @@ import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBund import com.intellij.python.hatch.HatchVirtualEnvironment import com.intellij.python.hatch.cli.HatchEnvironment import com.intellij.python.hatch.getHatchService +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.jetbrains.python.getOrNull import com.jetbrains.python.hatch.sdk.createSdk import com.jetbrains.python.orLogException @@ -48,7 +49,7 @@ internal class PyHatchSdkConfiguration : PyProjectSdkConfigurationExtension { val sdk = hatchVenv.createSdk(hatchService.getWorkingDirectoryPath(), module).orLogException(LOGGER) sdk } - + @RequiresBackgroundThread override fun createAndAddSdkForConfigurator(module: Module): Sdk? = createSdk(module) override fun createAndAddSdkForInspection(module: Module): Sdk? = createSdk(module) diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyRequirementsTxtOrSetupPySdkConfiguration.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyRequirementsTxtOrSetupPySdkConfiguration.kt index f34e806ba736..8ea8c3da2218 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyRequirementsTxtOrSetupPySdkConfiguration.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyRequirementsTxtOrSetupPySdkConfiguration.kt @@ -19,7 +19,6 @@ import com.intellij.openapi.ui.DialogWrapper import com.intellij.openapi.ui.ValidationInfo import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.NlsSafe -import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.util.use import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.VirtualFile @@ -29,33 +28,39 @@ import com.intellij.pycharm.community.ide.impl.configuration.PySdkConfigurationC import com.intellij.pycharm.community.ide.impl.configuration.PySdkConfigurationCollector.VirtualEnvResult import com.intellij.ui.IdeBorderFactory import com.intellij.ui.components.JBLabel +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.intellij.util.ui.JBUI -import com.jetbrains.python.failure import com.jetbrains.python.PyBundle import com.jetbrains.python.PySdkBundle +import com.jetbrains.python.failure import com.jetbrains.python.packaging.PyPackageManager import com.jetbrains.python.packaging.PyPackageUtil import com.jetbrains.python.packaging.PyTargetEnvironmentPackageManager import com.jetbrains.python.requirements.RequirementsFileType -import com.jetbrains.python.sdk.* import com.jetbrains.python.sdk.add.v1.PyAddNewVirtualEnvFromFilePanel +import com.jetbrains.python.sdk.basePath import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension import com.jetbrains.python.sdk.configuration.createVirtualEnvSynchronously +import com.jetbrains.python.sdk.isTargetBased +import com.jetbrains.python.sdk.showSdkExecutionException import java.awt.BorderLayout import java.awt.Insets import java.nio.file.Paths import javax.swing.JComponent import javax.swing.JPanel +import kotlin.io.path.Path private val LOGGER = fileLogger() -class PyRequirementsTxtOrSetupPySdkConfiguration : PyProjectSdkConfigurationExtension { - override fun createAndAddSdkForConfigurator(module: Module) = createAndAddSdk(module, Source.CONFIGURATOR).getOrLogException(LOGGER) +class PyRequirementsTxtOrSetupPySdkConfiguration : PyProjectSdkConfigurationExtension { + @RequiresBackgroundThread + override fun createAndAddSdkForConfigurator(module: Module): Sdk? = createAndAddSdk(module, Source.CONFIGURATOR).getOrLogException(LOGGER) override fun getIntention(module: Module): @IntentionName String? = getRequirementsTxtOrSetupPy(module)?.let { PyCharmCommunityCustomizationBundle.message("sdk.create.venv.suggestion", it.name) } - override fun createAndAddSdkForInspection(module: Module) = createAndAddSdk(module, Source.INSPECTION).getOrLogException(LOGGER) + @RequiresBackgroundThread + override fun createAndAddSdkForInspection(module: Module): Sdk? = createAndAddSdk(module, Source.INSPECTION).getOrLogException(LOGGER) private fun createAndAddSdk(module: Module, source: Source): Result { val existingSdks = ProjectJdkTable.getInstance().allJdks.asList() @@ -66,7 +71,7 @@ class PyRequirementsTxtOrSetupPySdkConfiguration : PyProjectSdkConfigurationExte } val (location, chosenBaseSdk, requirementsTxtOrSetupPy) = data - val systemIndependentLocation = FileUtil.toSystemIndependentName(location) + val systemIndependentLocation = Path(location) val projectPath = module.basePath ?: module.project.basePath ProgressManager.progress(PySdkBundle.message("python.creating.venv.sentence")) diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyTemporarilyIgnoredFileProvider.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyTemporarilyIgnoredFileProvider.kt index 38c8c87c7d56..06e2838df74e 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyTemporarilyIgnoredFileProvider.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyTemporarilyIgnoredFileProvider.kt @@ -5,20 +5,19 @@ import com.intellij.openapi.Disposable import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer -import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.vcs.FilePath import com.intellij.openapi.vcs.changes.IgnoredFileDescriptor import com.intellij.openapi.vcs.changes.IgnoredFileProvider import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBundle -import org.jetbrains.annotations.SystemIndependent +import java.nio.file.Path internal class PyTemporarilyIgnoredFileProvider : IgnoredFileProvider { companion object { private val LOGGER = Logger.getInstance(PyTemporarilyIgnoredFileProvider::class.java) - private val IGNORED_ROOTS = mutableSetOf() + private val IGNORED_ROOTS = mutableSetOf() - internal fun ignoreRoot(path: @SystemIndependent String, parent: Disposable) { + internal fun ignoreRoot(path: Path, parent: Disposable) { Disposer.register( parent, { @@ -33,8 +32,8 @@ internal class PyTemporarilyIgnoredFileProvider : IgnoredFileProvider { } override fun isIgnoredFile(project: Project, filePath: FilePath): Boolean { - val path = filePath.path - return IGNORED_ROOTS.any { FileUtil.isAncestor(it, path, false) } + val path = Path.of(filePath.path) + return IGNORED_ROOTS.any { path.startsWith(it) } } override fun getIgnoredFiles(project: Project): Set = emptySet() diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/miscProject/impl/PyMiscFileAction.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/miscProject/impl/PyMiscFileAction.kt index c6fe7ff3ba3a..96cb9f1924a9 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/miscProject/impl/PyMiscFileAction.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/miscProject/impl/PyMiscFileAction.kt @@ -7,13 +7,14 @@ import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.application.EDT import com.intellij.openapi.components.Service import com.intellij.openapi.components.service -import com.intellij.openapi.project.Project import com.intellij.openapi.ui.MessageDialogBuilder -import com.intellij.openapi.ui.Messages +import com.intellij.platform.ide.progress.ModalTaskOwner +import com.intellij.platform.ide.progress.runWithModalProgressBlocking import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBundle import com.intellij.pycharm.community.ide.impl.miscProject.MiscFileType import com.intellij.util.concurrency.annotations.RequiresEdt import com.jetbrains.python.Result +import com.jetbrains.python.util.ShowingMessageErrorSync import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -44,7 +45,9 @@ internal class PyMiscFileAction(private val miscFileType: MiscFileType) : AnActi scopeProvider = { it.service().scope })) { is Result.Success -> Unit is Result.Failure -> { - Messages.showErrorDialog(null as Project?, r.error, PyCharmCommunityCustomizationBundle.message("misc.project.error.title")) + runWithModalProgressBlocking(ModalTaskOwner.guess(), "..") { + ShowingMessageErrorSync.emit(r.error) + } } } } diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/miscProject/impl/lib.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/miscProject/impl/lib.kt index f9ec6e5b4264..f25f226e172e 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/miscProject/impl/lib.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/miscProject/impl/lib.kt @@ -6,17 +6,10 @@ import com.intellij.ide.trustedProjects.TrustedProjects import com.intellij.ide.trustedProjects.TrustedProjectsLocator.Companion.locateProject import com.intellij.openapi.application.EDT import com.intellij.openapi.application.readAction -import com.intellij.openapi.application.edtWriteAction import com.intellij.openapi.diagnostic.fileLogger import com.intellij.openapi.diagnostic.thisLogger -import com.intellij.openapi.module.Module -import com.intellij.openapi.module.ModuleManager import com.intellij.openapi.project.Project -import com.intellij.openapi.project.ProjectBundle import com.intellij.openapi.project.ex.ProjectManagerEx -import com.intellij.openapi.project.modules -import com.intellij.openapi.project.rootManager -import com.intellij.openapi.projectRoots.ProjectJdkTable import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.VirtualFile @@ -24,34 +17,27 @@ import com.intellij.platform.ide.progress.ModalTaskOwner import com.intellij.platform.ide.progress.TaskCancellation import com.intellij.platform.ide.progress.runWithModalProgressBlocking import com.intellij.platform.ide.progress.withBackgroundProgress -import com.intellij.platform.util.progress.withProgressText import com.intellij.psi.PsiFile import com.intellij.psi.PsiManager import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBundle import com.intellij.pycharm.community.ide.impl.miscProject.MiscFileType import com.intellij.pycharm.community.ide.impl.miscProject.TemplateFileName -import com.intellij.python.community.impl.venv.createVenv import com.intellij.python.community.services.systemPython.SystemPythonService import com.intellij.util.SystemProperties import com.intellij.util.concurrency.annotations.RequiresEdt -import com.jetbrains.python.* -import com.jetbrains.python.sdk.configurePythonSdk -import com.jetbrains.python.sdk.createSdk -import com.jetbrains.python.sdk.flavors.PythonSdkFlavor -import com.jetbrains.python.sdk.getOrCreateAdditionalData -import com.jetbrains.python.venvReader.VirtualEnvReader +import com.jetbrains.python.Result +import com.jetbrains.python.errorProcessing.PyError +import com.jetbrains.python.errorProcessing.failure +import com.jetbrains.python.mapResult +import com.jetbrains.python.projectCreation.createVenvAndSdk import kotlinx.coroutines.* -import org.jetbrains.annotations.Nls import java.io.IOException import java.nio.file.FileAlreadyExistsException import java.nio.file.Path import kotlin.io.path.createDirectories import kotlin.io.path.createFile -import kotlin.io.path.name import kotlin.time.Duration.Companion.milliseconds -private val logger = fileLogger() - internal val miscProjectDefaultPath: Lazy = lazy { Path.of(SystemProperties.getUserHome()).resolve("PyCharmMiscProject") } /** @@ -68,7 +54,7 @@ fun createMiscProject( confirmInstallation: suspend () -> Boolean, projectPath: Path = miscProjectDefaultPath.value, systemPythonService: SystemPythonService = SystemPythonService(), -): Result = +): Result = runWithModalProgressBlocking(ModalTaskOwner.guess(), PyCharmCommunityCustomizationBundle.message("misc.project.generating.env"), TaskCancellation.cancellable()) { @@ -135,6 +121,7 @@ private suspend fun generateFile(where: Path, templateFileName: TemplateFileName error("Too many files in $where") } + /** * Creates a project with one module in [projectPath] and sdk using the highest python. * Pythons are searched using [systemPythonService]. @@ -144,95 +131,13 @@ private suspend fun createProjectAndSdk( projectPath: Path, confirmInstallation: suspend () -> Boolean, systemPythonService: SystemPythonService, -): Result, @Nls String> { - val projectPathVfs = createProjectDir(projectPath).getOr { return it } - val venvDirPath = projectPath.resolve(VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME) - - // Find venv in a project - var venvPython: PythonBinary? = findExistingVenv(venvDirPath) - - if (venvPython == null) { - // No venv found -- find system python to create venv - val systemPythonBinary = getSystemPython(confirmInstallation = confirmInstallation, systemPythonService).getOr { return it } - logger.info("no venv in $venvDirPath, using system python $systemPythonBinary to create venv") - // create venv using this system python - venvPython = createVenv(systemPythonBinary, venvDir = venvDirPath).getOr { - return Result.failure(PyCharmCommunityCustomizationBundle.message("misc.project.error.create.venv", it.error.message, venvDirPath)) - } - } - - logger.info("using venv python $venvPython") +): Result, PyError> { + val vfsProjectPath = createProjectDir(projectPath).getOr { return it } val project = openProject(projectPath) - val sdk = getSdk(venvPython, project) - val module = project.modules.first() - ensureModuleHasRoot(module, projectPathVfs) - withContext(Dispatchers.IO) { - // generated files should be readable by VFS - VfsUtil.markDirtyAndRefresh(false, true, true, projectPathVfs) - } - configurePythonSdk(project, module, sdk) - sdk.getOrCreateAdditionalData().associateWithModule(module) - return Result.Success(Pair(project, sdk)) + val sdk = createVenvAndSdk(project, confirmInstallation, systemPythonService, vfsProjectPath).getOr { return it } + return Result.success(Pair(project, sdk)) } -/** - * Search for existing venv in [venvDirPath] and make sure it is usable. - * `null` means no venv or venv is broken (it doesn't report its version) - */ -private suspend fun findExistingVenv( - venvDirPath: Path, -): PythonBinary? = withContext(Dispatchers.IO) { - val pythonPath = VirtualEnvReader.Instance.findPythonInPythonRoot(venvDirPath) ?: return@withContext null - val flavor = PythonSdkFlavor.tryDetectFlavorByLocalPath(pythonPath.toString()) - if (flavor == null) { - logger.warn("No flavor found for $pythonPath") - return@withContext null - } - return@withContext when (val p = pythonPath.validatePythonAndGetVersion()) { - is Result.Success -> pythonPath - is Result.Failure -> { - logger.warn("No version string. python seems to be broken: $pythonPath. ${p.error}") - null - } - } -} - - -private suspend fun getSystemPython(confirmInstallation: suspend () -> Boolean, pythonService: SystemPythonService): Result { - - - // First, find the latest python according to strategy - var systemPythonBinary = pythonService.findSystemPythons().firstOrNull() - - // No python found? - if (systemPythonBinary == null) { - // Install it - val installer = pythonService.getInstaller() - ?: return Result.failure(PyCharmCommunityCustomizationBundle.message("misc.project.error.install.not.supported")) - if (confirmInstallation()) { - // Install - when (val r = installer.installLatestPython()) { - is Result.Failure -> { - val error = r.error - logger.warn("Python installation failed $error") - return Result.Failure( - PyCharmCommunityCustomizationBundle.message("misc.project.error.install.python", error)) - } - is Result.Success -> { - // Find the latest python again, after installation - systemPythonBinary = pythonService.findSystemPythons().firstOrNull() - } - } - } - } - - return if (systemPythonBinary == null) { - Result.Failure(PyCharmCommunityCustomizationBundle.message("misc.project.error.all.pythons.bad")) - } - else { - Result.Success(systemPythonBinary.pythonBinary) - } -} private suspend fun openProject(projectPath: Path): Project { TrustedProjects.setProjectTrusted(locateProject(projectPath, null), isTrusted = true) @@ -242,51 +147,25 @@ private suspend fun openProject(projectPath: Path): Project { isProjectCreatedWithWizard = true }) ?: error("Failed to open project in $projectPath, check logs") - // There are countless number of reasons `openProjectAsync` might return null - if (project.modules.isEmpty()) { - edtWriteAction { - ModuleManager.getInstance(project).newModule(projectPath.resolve("${projectPath.name}.iml"), PythonModuleTypeBase.getInstance().id) - } - } + // There are countless numbers of reasons `openProjectAsync` might return null + return project } -private suspend fun getSdk(pythonPath: PythonBinary, project: Project): Sdk = - withProgressText(ProjectBundle.message("progress.text.configuring.sdk")) { - val allJdks = ProjectJdkTable.getInstance().allJdks - val currentSdk = allJdks.firstOrNull { sdk -> sdk.homeDirectory?.toNioPath() == pythonPath } - if (currentSdk != null) return@withProgressText currentSdk - - val localPythonVfs = withContext(Dispatchers.IO) { VfsUtil.findFile(pythonPath, true)!! } - return@withProgressText createSdk(localPythonVfs, project.basePath?.let { Path.of(it) }, allJdks) - } - /** * Creating a project != creating a directory for it, but we need a directory to create a template file */ -private suspend fun createProjectDir(projectPath: Path): Result = withContext(Dispatchers.IO) { +private suspend fun createProjectDir(projectPath: Path): Result = withContext(Dispatchers.IO) { try { projectPath.createDirectories() } catch (e: IOException) { thisLogger().warn("Couldn't create $projectPath", e) - return@withContext Result.Failure( + return@withContext failure( PyCharmCommunityCustomizationBundle.message("misc.project.error.create.dir", projectPath, e.localizedMessage)) } val projectPathVfs = VfsUtil.findFile(projectPath, true) ?: error("Can't find VFS $projectPath") return@withContext Result.Success(projectPathVfs) } - -private suspend fun ensureModuleHasRoot(module: Module, root: VirtualFile): Unit = edtWriteAction { - with(module.rootManager.modifiableModel) { - try { - if (root in contentRoots) return@edtWriteAction - addContentEntry(root) - } - finally { - commit() - } - } -} diff --git a/python/ide/impl/tests/com/intellij/python/junit5Tests/unit/PyTemporarilyIgnoredFileProviderTest.kt b/python/ide/impl/tests/com/intellij/python/junit5Tests/unit/PyTemporarilyIgnoredFileProviderTest.kt new file mode 100644 index 000000000000..8caee32e900b --- /dev/null +++ b/python/ide/impl/tests/com/intellij/python/junit5Tests/unit/PyTemporarilyIgnoredFileProviderTest.kt @@ -0,0 +1,33 @@ +// 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.unit + +import com.intellij.openapi.Disposable +import com.intellij.openapi.vcs.LocalFilePath +import com.intellij.pycharm.community.ide.impl.configuration.PyTemporarilyIgnoredFileProvider +import com.intellij.testFramework.junit5.TestApplication +import com.intellij.testFramework.junit5.TestDisposable +import com.intellij.testFramework.junit5.fixture.projectFixture +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path +import kotlin.io.path.pathString + +@TestApplication +class PyTemporarilyIgnoredFileProviderTest { + private val project = projectFixture() + + @Test + fun testExclude(@TempDir path: Path, @TestDisposable disable: Disposable) { + + val ignored = path.resolve("ignored") + + PyTemporarilyIgnoredFileProvider.ignoreRoot(ignored, disable) + val sut = PyTemporarilyIgnoredFileProvider() + + assertTrue(sut.isIgnoredFile(project.get(), LocalFilePath(ignored.pathString, true))) + assertTrue(sut.isIgnoredFile(project.get(), LocalFilePath(ignored.resolve("1.txt").pathString, false))) + assertFalse(sut.isIgnoredFile(project.get(), LocalFilePath(path.pathString, true))) + } +} \ No newline at end of file diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties index 069088d52a65..07d69576d0c5 100644 --- a/python/pluginResources/messages/PyBundle.properties +++ b/python/pluginResources/messages/PyBundle.properties @@ -1634,4 +1634,18 @@ filter.install.package=Install Package sdk.create.custom.override.warning=Existing environment at "{0}" will be overridden sdk.create.custom.override.error=Environment at "{0}" already exists -sdk.create.custom.override.action=Override existing environment \ No newline at end of file +sdk.create.custom.override.action=Override existing environment + +python.project.model.progress.title.syncing.all.poetry.projects=Syncing all Poetry projects +python.project.model.progress.title.syncing.poetry.projects.at=Syncing Poetry projects at {0} +python.project.model.progress.title.unlinking.poetry.projects.at=Unlinking Poetry projects at {0} +python.project.model.activity.key.poetry.link=Poetry link +python.project.model.activity.key.poetry.sync=Poetry sync +python.project.model.progress.title.discovering.poetry.projects=Discovering Poetry projects +python.project.model.poetry=Poetry +action.Python.PoetrySync.text=Sync All Poetry Projects +action.Python.PoetryLink.text=Link All Poetry Projects + +project.error.install.python=Python could not be installed {0}. Try restarting PyCharm. If it does not help, install Python manually and try again. +project.error.install.not.supported=Python is not installed and this target does not support installation. Please, install it manually. +project.error.all.pythons.bad=No usable Python installations were found on your system. Please install Python manually. Check logs for more info. \ No newline at end of file diff --git a/python/python-features-trainer/intellij.python.featuresTrainer.iml b/python/python-features-trainer/intellij.python.featuresTrainer.iml index 0c00522ef9ac..5e8b5e8fea21 100644 --- a/python/python-features-trainer/intellij.python.featuresTrainer.iml +++ b/python/python-features-trainer/intellij.python.featuresTrainer.iml @@ -24,5 +24,10 @@ + + + + + \ No newline at end of file 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 e82961d04168..000000000000 --- a/python/python-features-trainer/src/com/intellij/python/featuresTrainer/ift/PythonBasedLangSupport.kt +++ /dev/null @@ -1,204 +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.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.createVirtualEnvSynchronously -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 = createVirtualEnvSynchronously(preferredSdk, existingSdks, venvRoot, project.basePath, project, module, project) - return venvSdk.also { - SdkConfigurationUtil.addSdk(it) - } - } - - 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..163b419ededd 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,58 @@ // 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.project.Project +import com.intellij.openapi.project.ProjectManager +import com.intellij.openapi.projectRoots.Sdk +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.vfs.VirtualFile +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.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.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 +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(private val errorSink: ErrorSink = ShowingMessageErrorSync) : AbstractLangSupport() { -class PythonLangSupport : PythonBasedLangSupport() { override val contentRootDirectoryName = "PyCharmLearningProject" override val primaryLanguage = "Python" @@ -20,11 +65,149 @@ 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) + } + + @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() + } + + + @RequiresEdt + override fun applyProjectSdk(sdk: Sdk, project: Project) { + } + + + 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) + } + } + } + } + + @RequiresReadLock + override fun getProtectedDirs(project: Project): Set { + return project.getSdks().mapNotNull { it.homePath }.map { Path.of(it) }.toSet() + } +} + +@RequiresReadLock +private fun Project.getSdks(): Set { + val projectSdk = ProjectRootManager.getInstance(this).projectSdk + val moduleSdks = modules.mapNotNull { + ModuleRootManager.getInstance(it).sdk + } + return (moduleSdks + (projectSdk?.let { listOf(it) } ?: emptyList())).toSet() } 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 diff --git a/python/src/com/jetbrains/python/projectCreation/venvWithSdkCreator.kt b/python/src/com/jetbrains/python/projectCreation/venvWithSdkCreator.kt new file mode 100644 index 000000000000..4645a86de2ce --- /dev/null +++ b/python/src/com/jetbrains/python/projectCreation/venvWithSdkCreator.kt @@ -0,0 +1,176 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.projectCreation + +import com.intellij.openapi.application.edtWriteAction +import com.intellij.openapi.application.writeAction +import com.intellij.openapi.diagnostic.fileLogger +import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleManager +import com.intellij.openapi.project.* +import com.intellij.openapi.projectRoots.ProjectJdkTable +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.roots.ModuleRootManager +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.platform.util.progress.withProgressText +import com.intellij.python.community.impl.venv.createVenv +import com.intellij.python.community.services.systemPython.SystemPythonService +import com.jetbrains.python.* +import com.jetbrains.python.errorProcessing.PyError +import com.jetbrains.python.errorProcessing.failure +import com.jetbrains.python.sdk.configurePythonSdk +import com.jetbrains.python.sdk.createSdk +import com.jetbrains.python.sdk.flavors.PythonSdkFlavor +import com.jetbrains.python.sdk.getOrCreateAdditionalData +import com.jetbrains.python.sdk.pythonSdk +import com.jetbrains.python.venvReader.VirtualEnvReader +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.nio.file.Path + +private val logger = fileLogger() + +/** + * Create a venv in a [project] (or in [explicitProjectPath]) and SDK out of it (existing venv will be used if valid). + * The best python os chosen automatically using [SystemPythonService], but if there is no python one will be installed with + * [confirmInstallation]. + * If a project has no module -- one will be created. + * + * Use this function as a high-level API for various quick project creation wizards like Misc and Tour. + * + * If you only need venv (no SDK), use [createVenv] + */ +suspend fun createVenvAndSdk( + project: Project, + confirmInstallation: suspend () -> Boolean = { true }, + systemPythonService: SystemPythonService = SystemPythonService(), + explicitProjectPath: VirtualFile? = null, +): Result { + val vfsProjectPath = withContext(Dispatchers.IO) { + explicitProjectPath + ?: (project.modules.firstOrNull()?.let { module -> ModuleRootManager.getInstance(module).contentRoots.firstOrNull() } + ?: project.guessProjectDir() + ?: error("no path provided and can't guess path for $project")) + } + + + val projectPath = vfsProjectPath.toNioPath() + val venvDirPath = vfsProjectPath.toNioPath().resolve(VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME) + + + // Find venv in a project + var venvPython: PythonBinary? = findExistingVenv(venvDirPath) + + if (venvPython == null) { + // No venv found -- find system python to create venv + val systemPythonBinary = getSystemPython(confirmInstallation = confirmInstallation, systemPythonService).getOr { return it } + logger.info("no venv in $venvDirPath, using system python $systemPythonBinary to create venv") + // create venv using this system python + venvPython = createVenv(systemPythonBinary, venvDir = venvDirPath).getOr { + return it + } + } + + logger.info("using venv python $venvPython") + val sdk = getSdk(venvPython, project) + if (project.modules.isEmpty()) { + writeAction { + val file = projectPath.resolve("${projectPath.fileName}.iml") + ModuleManager.getInstance(project).newModule(file, PythonModuleTypeBase.getInstance().id) + } + } + val module = project.modules.first() + ensureModuleHasRoot(module, vfsProjectPath) + withContext(Dispatchers.IO) { + // generated files should be readable by VFS + VfsUtil.markDirtyAndRefresh(false, true, true, vfsProjectPath) + } + configurePythonSdk(project, module, sdk) + sdk.getOrCreateAdditionalData().associateWithModule(module) + module.pythonSdk + return Result.success(sdk) +} + +/** + * Search for existing venv in [venvDirPath] and make sure it is usable. + * `null` means no venv or venv is broken (it doesn't report its version) + */ +private suspend fun findExistingVenv( + venvDirPath: Path, +): PythonBinary? = withContext(Dispatchers.IO) { + val pythonPath = VirtualEnvReader.Instance.findPythonInPythonRoot(venvDirPath) ?: return@withContext null + val flavor = PythonSdkFlavor.tryDetectFlavorByLocalPath(pythonPath.toString()) + if (flavor == null) { + logger.warn("No flavor found for $pythonPath") + return@withContext null + } + return@withContext when (val p = pythonPath.validatePythonAndGetVersion()) { + is Result.Success -> pythonPath + is Result.Failure -> { + logger.warn("No version string. python seems to be broken: $pythonPath. ${p.error}") + null + } + } +} + +private suspend fun getSystemPython( + confirmInstallation: suspend () -> Boolean, + pythonService: SystemPythonService, +): Result { + + + // First, find the latest python according to strategy + var systemPythonBinary = pythonService.findSystemPythons().firstOrNull() + + // No python found? + if (systemPythonBinary == null) { + // Install it + val installer = pythonService.getInstaller() + ?: return failure(PyBundle.message("project.error.install.not.supported")) + if (confirmInstallation()) { + // Install + when (val r = installer.installLatestPython()) { + is Result.Failure -> { + val error = r.error + logger.warn("Python installation failed $error") + return failure( + PyBundle.message("project.error.install.python", error)) + } + is Result.Success -> { + // Find the latest python again, after installation + systemPythonBinary = pythonService.findSystemPythons().firstOrNull() + } + } + } + } + + return if (systemPythonBinary == null) { + return failure(PyBundle.message("project.error.all.pythons.bad")) + } + else { + Result.Success(systemPythonBinary.pythonBinary) + } +} + + +private suspend fun ensureModuleHasRoot(module: Module, root: VirtualFile): Unit = edtWriteAction { + with(module.rootManager.modifiableModel) { + try { + if (root in contentRoots) return@edtWriteAction + addContentEntry(root) + } + finally { + commit() + } + } +} + +private suspend fun getSdk(pythonPath: PythonBinary, project: Project): Sdk = + withProgressText(ProjectBundle.message("progress.text.configuring.sdk")) { + val allJdks = ProjectJdkTable.getInstance().allJdks + val currentSdk = allJdks.firstOrNull { sdk -> sdk.homeDirectory?.toNioPath() == pythonPath } + if (currentSdk != null) return@withProgressText currentSdk + + val localPythonVfs = withContext(Dispatchers.IO) { VfsUtil.findFile(pythonPath, true)!! } + return@withProgressText createSdk(localPythonVfs, project.basePath?.let { Path.of(it) }, allJdks) + } diff --git a/python/src/com/jetbrains/python/sdk/configuration/PyProjectVirtualEnvConfiguration.kt b/python/src/com/jetbrains/python/sdk/configuration/PyProjectVirtualEnvConfiguration.kt index 52afb47b0798..89714d812353 100644 --- a/python/src/com/jetbrains/python/sdk/configuration/PyProjectVirtualEnvConfiguration.kt +++ b/python/src/com/jetbrains/python/sdk/configuration/PyProjectVirtualEnvConfiguration.kt @@ -33,7 +33,14 @@ import com.jetbrains.python.sdk.flavors.PyFlavorAndData import com.jetbrains.python.sdk.flavors.PyFlavorData import com.jetbrains.python.target.PyTargetAwareAdditionalData import com.jetbrains.python.target.getInterpreterVersion +import org.jetbrains.annotations.ApiStatus +/** + * Use [com.jetbrains.python.projectCreation.createVenvAndSdk] unless you need the Targets API. + * + * If you need venv only, please use [com.intellij.python.community.impl.venv.createVenv]: it is cleaner and suspend. + */ +@ApiStatus.Internal @RequiresEdt fun createVirtualEnvSynchronously( baseSdk: Sdk, diff --git a/python/src/com/jetbrains/python/util/ShowingMessageErrorSync.kt b/python/src/com/jetbrains/python/util/ShowingMessageErrorSync.kt index 5badaf609af2..718ae9d00101 100644 --- a/python/src/com/jetbrains/python/util/ShowingMessageErrorSync.kt +++ b/python/src/com/jetbrains/python/util/ShowingMessageErrorSync.kt @@ -13,15 +13,17 @@ import com.jetbrains.python.errorProcessing.PyError import com.jetbrains.python.showProcessExecutionErrorDialog import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.jetbrains.annotations.ApiStatus /** - * Displays error with a message box and writes it to a log. + * Displays the error with a message box and writes it to a log. */ -internal object ShowingMessageErrorSync : ErrorSink { +@ApiStatus.Internal +object ShowingMessageErrorSync : ErrorSink { override suspend fun emit(error: PyError) { withContext(Dispatchers.EDT + ModalityState.any().asContextElement()) { thisLogger().warn(error.message) - // Platform doesn't allow dialogs without lock for now, fix later + // Platform doesn't allow dialogs without a lock for now, fix later writeIntentReadAction { when (val e = error) { is PyError.ExecException -> {