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:
Ilya Kazakevich
2025-04-17 12:13:41 +00:00
committed by intellij-monorepo-bot
parent cbd1a11c19
commit b3a87dc49e
22 changed files with 611 additions and 403 deletions

View File

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

View 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")
}
}
}
}

View File

@@ -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()
}

View File

@@ -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
}
}
}

View File

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

View File

@@ -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?

View File

@@ -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")

View File

@@ -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)

View File

@@ -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"))

View File

@@ -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()

View File

@@ -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)
}
}
}
}

View File

@@ -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()
}
}
}

View File

@@ -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)))
}
}

View File

@@ -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.

View File

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

View File

@@ -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)
}
}
}
}
}

View File

@@ -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()
}

View File

@@ -0,0 +1,4 @@
@ApiStatus.Internal
package com.intellij.python.featuresTrainer.ift;
import org.jetbrains.annotations.ApiStatus;

View File

@@ -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()
}

View File

@@ -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)
}

View File

@@ -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,

View File

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