mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-03-22 15:19:59 +07:00
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 <ilya.kazakevich@jetbrains.com> GitOrigin-RevId: aa1b2848c8292dafb2f42a805f173ddd505430dc
This commit is contained in:
committed by
intellij-monorepo-bot
parent
cbd1a11c19
commit
b3a87dc49e
@@ -46,5 +46,6 @@
|
||||
<orderEntry type="library" scope="TEST" name="mockito" level="project" />
|
||||
<orderEntry type="library" scope="TEST" name="mockito-kotlin" level="project" />
|
||||
<orderEntry type="library" scope="TEST" name="jimfs" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.featuresTrainer" scope="TEST" />
|
||||
</component>
|
||||
</module>
|
||||
53
platform/util/testSrc/training/project/ProjectUtilsTest.kt
Normal file
53
platform/util/testSrc/training/project/ProjectUtilsTest.kt
Normal file
@@ -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<LangSupport>()
|
||||
`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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Path> = emptySet()
|
||||
}
|
||||
|
||||
@@ -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<Boolean>()
|
||||
AppUIExecutor.onWriteThread().withDocumentsCommitted(project).submit {
|
||||
@@ -280,29 +301,44 @@ object ProjectUtils {
|
||||
val directories = mutableListOf<Path>()
|
||||
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<Path> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
@@ -39,5 +40,7 @@
|
||||
<orderEntry type="library" name="jetbrains-annotations" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.python.pyproject" />
|
||||
<orderEntry type="module" module-name="intellij.python.hatch" />
|
||||
<orderEntry type="module" module-name="intellij.platform.testFramework.junit5" scope="TEST" />
|
||||
<orderEntry type="library" scope="TEST" name="JUnit5" level="project" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -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?
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<Sdk> {
|
||||
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"))
|
||||
|
||||
@@ -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<String>()
|
||||
private val IGNORED_ROOTS = mutableSetOf<Path>()
|
||||
|
||||
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<IgnoredFileDescriptor> = emptySet()
|
||||
|
||||
@@ -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<MyService>().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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Path> = lazy { Path.of(SystemProperties.getUserHome()).resolve("PyCharmMiscProject") }
|
||||
|
||||
/**
|
||||
@@ -68,7 +54,7 @@ fun createMiscProject(
|
||||
confirmInstallation: suspend () -> Boolean,
|
||||
projectPath: Path = miscProjectDefaultPath.value,
|
||||
systemPythonService: SystemPythonService = SystemPythonService(),
|
||||
): Result<Job, @Nls String> =
|
||||
): Result<Job, PyError> =
|
||||
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<Pair<Project, Sdk>, @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<Pair<Project, Sdk>, 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<PythonBinary, @Nls String> {
|
||||
|
||||
|
||||
// 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<VirtualFile, @Nls String> = withContext(Dispatchers.IO) {
|
||||
private suspend fun createProjectDir(projectPath: Path): Result<VirtualFile, PyError.Message> = 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
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.
|
||||
@@ -24,5 +24,10 @@
|
||||
<orderEntry type="library" name="kotlinx-serialization-json" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.platform.testFramework" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.pycharm.community" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.python.community.services.systemPython" />
|
||||
<orderEntry type="module" module-name="intellij.python.community.junit5Tests.framework" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.platform.testFramework.junit5" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.python.community.impl.venv" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.python.community.services.internal.impl" scope="TEST" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -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<Sdk>,
|
||||
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<Sdk> {
|
||||
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<Sdk>, 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 = "<html>$installationWarning</html>"
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Sdk>, 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 = "<html>$installationWarning</html>"
|
||||
}
|
||||
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<Path> {
|
||||
return project.getSdks().mapNotNull { it.homePath }.map { Path.of(it) }.toSet()
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresReadLock
|
||||
private fun Project.getSdks(): Set<Sdk> {
|
||||
val projectSdk = ProjectRootManager.getInstance(this).projectSdk
|
||||
val moduleSdks = modules.mapNotNull {
|
||||
ModuleRootManager.getInstance(it).sdk
|
||||
}
|
||||
return (moduleSdks + (projectSdk?.let { listOf(it) } ?: emptyList())).toSet()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
@ApiStatus.Internal
|
||||
package com.intellij.python.featuresTrainer.ift;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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<Sdk, PyError> {
|
||||
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<PythonBinary, PyError.Message> {
|
||||
|
||||
|
||||
// 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)
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 -> {
|
||||
|
||||
Reference in New Issue
Block a user