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 -> {