PY-77483, PY-75549, FUS-5195: Misc project pycharm (squashed commits from master)

Lots of files are picked from the master

GitOrigin-RevId: 88dfc699cbfa2be9b11d1645c2c24221d16fbdc4
This commit is contained in:
Ilya.Kazakevich
2024-11-20 21:59:13 +01:00
committed by intellij-monorepo-bot
parent aa93e32d40
commit 9f31575c35
30 changed files with 882 additions and 36 deletions

View File

@@ -10,7 +10,7 @@ import org.jetbrains.intellij.build.io.copyFileToDir
import java.nio.file.Files
import java.nio.file.Path
open class PyCharmCommunityProperties(private val communityHome: Path) : PyCharmPropertiesBase() {
open class PyCharmCommunityProperties(private val communityHome: Path) : PyCharmPropertiesBase(enlargeWelcomeScreen = true) {
override val customProductCode: String
get() = "PC"

View File

@@ -1,6 +1,7 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.intellij.build.pycharm
import kotlinx.collections.immutable.plus
import org.jetbrains.intellij.build.BuildContext
import org.jetbrains.intellij.build.JetBrainsProductProperties
import org.jetbrains.intellij.build.TEST_FRAMEWORK_WITH_JAVA_RT
@@ -11,11 +12,15 @@ import java.util.function.Predicate
const val PYDEVD_PACKAGE: String = "pydevd_package"
abstract class PyCharmPropertiesBase : JetBrainsProductProperties() {
abstract class PyCharmPropertiesBase(enlargeWelcomeScreen: Boolean = false) : JetBrainsProductProperties() {
override val baseFileName: String
get() = "pycharm"
init {
if (enlargeWelcomeScreen) {
additionalVmOptions += "-Dwelcome.screen.defaultWidth=1000"
additionalVmOptions += "-Dwelcome.screen.defaultHeight=720"
}
reassignAltClickToMultipleCarets = true
useSplash = true
productLayout.addPlatformSpec(TEST_FRAMEWORK_WITH_JAVA_RT)

View File

@@ -28,5 +28,6 @@
<orderEntry type="module" module-name="intellij.platform.whatsNew" />
<orderEntry type="module" module-name="intellij.pycharm.community.ide.impl.promotion" scope="RUNTIME" />
<orderEntry type="module" module-name="intellij.platform.util.coroutines" />
<orderEntry type="module" module-name="intellij.platform.experiment" />
</component>
</module>

View File

@@ -2,7 +2,7 @@
<!--Customization code for both Community and Pro PyCharms-->
<dependencies>
<plugin id="PythonCore"/>
<module name="intellij.platform.whatsNew" />
<module name="intellij.platform.whatsNew"/>
</dependencies>
<projectListeners>
@@ -11,7 +11,17 @@
topic="com.intellij.workspaceModel.ide.JpsProjectLoadedListener"/>
</projectListeners>
<extensionPoints>
<extensionPoint interface="com.intellij.pycharm.community.ide.impl.miscProject.MiscFileType"
qualifiedName="Pythonid.miscFileType" dynamic="true"/>
</extensionPoints>
<extensions defaultExtensionNs="com.intellij">
<refactoring.elementListenerProvider implementation="com.intellij.pycharm.community.ide.impl.miscProject.impl.MiscProjectListenerProvider"/>
<statistics.counterUsagesCollector implementationClass="com.intellij.pycharm.community.ide.impl.miscProject.impl.MiscProjectUsageCollector"/>
<experiment.abExperimentOption implementation="com.intellij.pycharm.community.ide.impl.miscProject.impl.PyMiscProjectExperimentOption"/>
<registryKey defaultValue="5" description="Number of primary buttons on welcome screen (other go to 'more actions')"
key="welcome.screen.primaryButtonsCount" restartRequired="true" overrides="true"/>
<applicationInitializedListener implementation="com.intellij.pycharm.community.ide.impl.PyCharmCorePluginConfigurator"/>
<applicationService serviceInterface="com.intellij.lang.IdeLanguageCustomization"
serviceImplementation="com.intellij.pycharm.community.ide.impl.PyCharmPythonIdeLanguageCustomization"
@@ -71,7 +81,8 @@
<directoryIndexExcludePolicy implementation="com.intellij.pycharm.community.ide.impl.PyDirectoryIndexExcludePolicy"/>
<applicationService serviceImplementation="com.intellij.pycharm.community.ide.impl.newProjectWizard.welcome.PyWelcomeSettings"/>
<statistics.counterUsagesCollector implementationClass="com.intellij.pycharm.community.ide.impl.newProjectWizard.welcome.PyWelcomeCollector"/>
<statistics.counterUsagesCollector
implementationClass="com.intellij.pycharm.community.ide.impl.newProjectWizard.welcome.PyWelcomeCollector"/>
<notificationGroup id="PyCharm Professional Advertiser" displayType="STICKY_BALLOON" isLogByDefault="false"
bundle="messages.PyCharmCommunityCustomizationBundle" key="notification.group.pro.advertiser"/>
@@ -103,11 +114,14 @@
</action>
<group id="WelcomeScreen.Platform.NewProject">
<group id="WelcomeScreen.PyScratchFileActionGroup"
class="com.intellij.pycharm.community.ide.impl.miscProject.impl.PyMiscFileActionGroup" compact="true"/>
<group id="WelcomeScreen.CreateDirectoryProject"
class="com.intellij.pycharm.community.ide.impl.newProjectWizard.impl.PyV3NewProjectStepAction"/>
class="com.intellij.pycharm.community.ide.impl.newProjectWizard.impl.PyV3NewProjectStepAction"/>
<reference ref="WelcomeScreen.OpenDirectoryProject"/>
<add-to-group group-id="WelcomeScreen.QuickStart" anchor="first"/>
</group>
</actions>
</idea-plugin>

View File

@@ -59,4 +59,19 @@ sdk.notification.label.set.up.poetry.environment.from.pyproject.toml.dependencie
notification.group.pro.advertiser=PyCharm Professional recommended
new.project.python.group.name=Python
new.project.other.group.name=Other
new.project.other.group.name=Other
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 did not help, install python manually and try again.
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 pythons were found on your system. Please install python manually. Check logs for more info.
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

@@ -0,0 +1,23 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.pycharm.community.ide.impl.miscProject
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.NlsActions
import com.intellij.psi.PsiFile
import javax.swing.Icon
/**
* On a welcome screen user clicks in [icon] to get a project with [fileName] template filled by [fillFile]
*/
interface MiscFileType {
companion object {
val EP: ExtensionPointName<MiscFileType> = ExtensionPointName.create("Pythonid.miscFileType")
}
val title: @NlsActions.ActionText String
val icon: Icon
val fileName: TemplateFileName
suspend fun fillFile(file: PsiFile, sdk: Sdk)
}

View File

@@ -0,0 +1,28 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.pycharm.community.ide.impl.miscProject
import com.intellij.openapi.util.NlsSafe
import org.jetbrains.annotations.NonNls
import org.jetbrains.annotations.NotNull
/**
* [nameWithSuffix] will create `[nameNoExt]`N`.[ext] where `N > 0` if file already exists
*/
data class TemplateFileName(private val nameNoExt: @NotNull String, private val ext: @NonNls String) {
fun nameWithSuffix(suffixCount: Int): @NlsSafe String {
val suffix = if (suffixCount == 0) "" else suffixCount.toString()
return "$nameNoExt$suffix.$ext"
}
companion object {
/**
* Creates instance from `file.ext` ie `notebook.ipynb`
*/
fun parse(fileNameWithExt: @NonNls String): TemplateFileName {
val filePaths = fileNameWithExt.split(".")
assert(filePaths.size == 2) { "$fileNameWithExt must be file.ext" }
return TemplateFileName(filePaths[0], filePaths[1])
}
}
}

View File

@@ -0,0 +1,28 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.pycharm.community.ide.impl.miscProject.impl
import com.intellij.psi.PsiDirectory
import com.intellij.psi.PsiElement
import com.intellij.refactoring.listeners.RefactoringElementListener
import com.intellij.refactoring.listeners.RefactoringElementListenerProvider
import kotlin.io.path.name
/**
* Listens for misc project rename
*/
internal class MiscProjectListenerProvider : RefactoringElementListenerProvider {
override fun getListener(element: PsiElement): RefactoringElementListener? {
if (!miscProjectEnabled.value) return null
val dir = (element as? PsiDirectory) ?: return null
// dir name is enough
return if (dir.name == miscProjectDefaultPath.value.name) MiscProjectRenameReporter else null
}
}
private object MiscProjectRenameReporter : RefactoringElementListener {
override fun elementMoved(newElement: PsiElement) = Unit
override fun elementRenamed(newElement: PsiElement) {
MiscProjectUsageCollector.projectRenamed()
}
}

View File

@@ -0,0 +1,25 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.pycharm.community.ide.impl.miscProject.impl
import com.intellij.internal.statistic.eventLog.EventLogGroup
import com.intellij.internal.statistic.eventLog.events.EventId
import com.intellij.internal.statistic.service.fus.collectors.CounterUsagesCollector
/**
* Logs misc project creation and renaming to FUS
*/
internal object MiscProjectUsageCollector : CounterUsagesCollector() {
private val GROUP: EventLogGroup = EventLogGroup("pycharm.misc.project", 2)
private val MISC_PROJECT_CREATED: EventId = GROUP.registerEvent("misc.project.created", "Misc project created (user clicked on the button on welcome screen)")
private val MISC_PROJECT_RENAMED: EventId = GROUP.registerEvent("misc.project.renamed", "Misc project renamed")
override fun getGroup(): EventLogGroup = GROUP
fun projectCreated() {
MISC_PROJECT_CREATED.log()
}
fun projectRenamed() {
MISC_PROJECT_RENAMED.log()
}
}

View File

@@ -0,0 +1,25 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.pycharm.community.ide.impl.miscProject.impl
import com.intellij.openapi.application.writeAction
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.NlsActions
import com.intellij.psi.PsiFile
import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBundle
import com.intellij.pycharm.community.ide.impl.newProjectWizard.welcome.PyWelcome
import com.intellij.pycharm.community.ide.impl.miscProject.MiscFileType
import com.intellij.pycharm.community.ide.impl.miscProject.TemplateFileName
import com.jetbrains.python.psi.icons.PythonPsiApiIcons
import javax.swing.Icon
object MiscScriptFileType : MiscFileType {
override val title: @NlsActions.ActionText String = PyCharmCommunityCustomizationBundle.message("misc.script.text")
override val icon: Icon = PythonPsiApiIcons.Python_32x32
override val fileName: TemplateFileName = TemplateFileName.parse("script.py")
override suspend fun fillFile(file: PsiFile, sdk: Sdk) {
writeAction {
PyWelcome.writeText(file)
}
}
}

View File

@@ -0,0 +1,26 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.pycharm.community.ide.impl.miscProject.impl
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
import java.nio.file.Path
/**
* How do we get system python?
*/
sealed interface ObtainPythonStrategy {
/**
* Find it on the system. If no python found -- [confirmInstallation] and install
*/
fun interface FindOnSystem : ObtainPythonStrategy {
suspend fun confirmInstallation(): Boolean
}
/**
* Only use [pythons] provided explicitly
*/
data class UseThesePythons(val pythons: List<Pair<PythonSdkFlavor<*>, Collection<Path>>>) : ObtainPythonStrategy {
init {
assert(pythons.flatMap { it.second }.isNotEmpty()) { "When provided explicitly, pythons can't be empty" }
}
}
}

View File

@@ -0,0 +1,54 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.pycharm.community.ide.impl.miscProject.impl
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
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.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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
* Action displayed on welcome screen to create a project by [miscFileType]
*/
internal class PyMiscFileAction(private val miscFileType: MiscFileType) : AnAction(
miscFileType.title,
null,
miscFileType.icon
) {
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
@RequiresEdt
override fun actionPerformed(e: AnActionEvent) {
MiscProjectUsageCollector.projectCreated()
when (val r = createMiscProject(
miscFileType,
obtainPythonStrategy = object : ObtainPythonStrategy.FindOnSystem {
override suspend fun confirmInstallation(): Boolean = withContext(Dispatchers.EDT) {
MessageDialogBuilder.yesNo(
PyCharmCommunityCustomizationBundle.message("misc.no.python.found"),
PyCharmCommunityCustomizationBundle.message("misc.install.python.question")
).ask(e.project)
}
},
scopeProvider = { it.service<MyService>().scope })) {
is Result.Success -> Unit
is Result.Failure -> {
Messages.showErrorDialog(null as Project?, r.error.text, PyCharmCommunityCustomizationBundle.message("misc.project.error.title"))
}
}
}
}
@Service(Service.Level.PROJECT)
private class MyService(val scope: CoroutineScope)

View File

@@ -0,0 +1,23 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.pycharm.community.ide.impl.miscProject.impl
import com.intellij.openapi.actionSystem.ActionGroup
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.pycharm.community.ide.impl.miscProject.MiscFileType
internal class PyMiscFileActionGroup : ActionGroup() {
override fun update(e: AnActionEvent) {
e.presentation.isEnabledAndVisible = miscProjectEnabled.value
}
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
override fun getChildren(e: AnActionEvent?): Array<out AnAction> =
if (miscProjectEnabled.value)
(MiscFileType.EP.extensionList + listOf(MiscScriptFileType)).map { PyMiscFileAction(it) }.toTypedArray()
else
emptyArray()
}

View File

@@ -0,0 +1,19 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.pycharm.community.ide.impl.miscProject.impl
import com.intellij.platform.experiment.ab.impl.experiment.ABExperimentOption
import com.intellij.platform.experiment.ab.impl.experiment.ABExperimentOptionId
import com.intellij.platform.experiment.ab.impl.option.ABExperimentOptionGroupSize
/**
* A/B testing option for misc pycharm project
*/
internal class PyMiscProjectExperimentOption : ABExperimentOption {
override val id: ABExperimentOptionId = ABExperimentOptionId("pycharm.miscProject")
override fun getGroupSizeForIde(isPopularIde: Boolean): ABExperimentOptionGroupSize = ABExperimentOptionGroupSize(128) // half: 256/2
override fun checkIdeIsSuitable(): Boolean = true // This module only goes to PyCharm
override fun checkIdeVersionIsSuitable(): Boolean = true
}

View File

@@ -0,0 +1,385 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.pycharm.community.ide.impl.miscProject.impl
import com.intellij.execution.ExecutionException
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.ide.impl.OpenProjectTask
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.readAction
import com.intellij.openapi.application.writeAction
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.roots.ModuleRootModificationUtil
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.platform.experiment.ab.impl.experiment.ABExperiment
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.pycharm.community.ide.impl.miscProject.impl.ObtainPythonStrategy.FindOnSystem
import com.intellij.pycharm.community.ide.impl.miscProject.impl.ObtainPythonStrategy.UseThesePythons
import com.intellij.util.SystemProperties
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.intellij.util.io.awaitExit
import com.jetbrains.python.LocalizedErrorString
import com.jetbrains.python.PythonModuleTypeBase
import com.jetbrains.python.Result
import com.jetbrains.python.mapResult
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.sdk.PySdkToInstallManager
import com.jetbrains.python.sdk.PythonBinary
import com.jetbrains.python.sdk.VirtualEnvReader
import com.jetbrains.python.sdk.add.v2.createSdk
import com.jetbrains.python.sdk.add.v2.createVirtualenv
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
import com.jetbrains.python.sdk.installer.installBinary
import kotlinx.coroutines.*
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.pathString
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
private val logger = fileLogger()
internal val miscProjectDefaultPath: Lazy<Path> = lazy { Path.of(SystemProperties.getUserHome()).resolve("PyCharmMiscProject") }
internal val miscProjectEnabled: Lazy<Boolean> = lazy { ABExperiment.getABExperimentInstance().isExperimentOptionEnabled(PyMiscProjectExperimentOption::class.java) }
/**
* Creates project in [projectPath] in modal window. Once created, uses [scopeProvider] to get scope
* to launch [miscFileType] generation in background, returns it as a job.
*
* Pythons are obtained with [obtainPythonStrategy]
*/
@RequiresEdt
fun createMiscProject(
miscFileType: MiscFileType,
scopeProvider: (Project) -> CoroutineScope,
obtainPythonStrategy: ObtainPythonStrategy,
projectPath: Path = miscProjectDefaultPath.value,
): Result<Job, LocalizedErrorString> =
runWithModalProgressBlocking(ModalTaskOwner.guess(),
PyCharmCommunityCustomizationBundle.message("misc.project.generating.env"),
TaskCancellation.cancellable()) {
createProjectAndSdk(projectPath, obtainPythonStrategy)
}.mapResult { (project, sdk) ->
Result.Success(scopeProvider(project).launch {
withBackgroundProgress(project, PyCharmCommunityCustomizationBundle.message("misc.project.filling.file")) {
generateAndOpenFile(projectPath, project, miscFileType, sdk)
}
})
}
private suspend fun generateAndOpenFile(projectPath: Path, project: Project, fileType: MiscFileType, sdk: Sdk): PsiFile {
val generateFile = generateFile(projectPath, fileType.fileName)
val psiFile = openFile(project, generateFile)
fileType.fillFile(psiFile, sdk)
return psiFile
}
private suspend fun openFile(project: Project, file: Path): PsiFile {
val vfsFile = withContext(Dispatchers.IO) {
VfsUtil.findFile(file, true) ?: error("Can't find VFS $file")
}
// `navigate` throws `AssertionError` from time to time due to platform API bug.
// We "fix" it by means of retries
return callWithRetry {
withContext(Dispatchers.EDT) {
val psiFile = readAction { PsiManager.getInstance(project).findFile(vfsFile) } ?: error("Can't find PSI for $vfsFile")
psiFile.navigate(true)
return@withContext psiFile
}
}
}
/**
* Retries [code] `10` times if it throws [AssertionError]
*/
private suspend fun <T> callWithRetry(code: suspend () -> T): T {
val logger = fileLogger()
repeat(10) {
try {
return code()
}
catch (e: AssertionError) {
logger.warn(e)
delay(100.milliseconds)
}
}
return code()
}
private suspend fun generateFile(where: Path, templateFileName: TemplateFileName): Path = withContext(Dispatchers.IO) {
repeat(Int.MAX_VALUE) {
val file = where.resolve(templateFileName.nameWithSuffix(it))
try {
file.createFile()
return@withContext file
}
catch (_: FileAlreadyExistsException) {
}
}
error("Too many files in $where")
}
/**
* Creates project with 1 module in [projectPath] and sdk using the highest python.
* Pythons are searched in system ([findPythonsOnSystem]) or provided explicitly (depends on [obtainPythonStrategy]).
* In former case if no python were found, we [installLatestPython] (not in a latter case, though).
*/
private suspend fun createProjectAndSdk(
projectPath: Path,
obtainPythonStrategy: ObtainPythonStrategy,
): Result<Pair<Project, Sdk>, LocalizedErrorString> {
val projectPathVfs = createProjectDir(projectPath).getOr { return it }
val venvDirPath = projectPath.resolve(VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME)
// Find venv in project
var venvPython: PythonBinary? = findExistingVenv(venvDirPath)
if (venvPython == null) {
// No venv found -- find system python to create venv
val systemPythonBinary = getSystemPython(obtainPythonStrategy).getOr { return it }
logger.info("no venv in $venvDirPath, using system python $systemPythonBinary to create venv")
// create venv using this system python
createVenv(systemPythonBinary, venvDirPath = venvDirPath, projectPath = projectPath).getOr { return it }
// try to find venv again
venvPython = findExistingVenv(venvDirPath)
if (venvPython == null) {
// No venv even after venv installation
return Result.failure(LocalizedErrorString(PyCharmCommunityCustomizationBundle.message("misc.project.error.create.venv", "", venvDirPath)))
}
}
logger.info("using venv python $venvPython")
val project = openProject(projectPath)
val sdk = getSdk(venvPython, project)
val module = project.modules.first()
ensureModuleHasRoot(module, projectPathVfs)
ModuleRootModificationUtil.setModuleSdk(module, sdk)
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
}
if (validatePythonAndGetVersion(pythonPath, flavor) == null) {
logger.warn("No version string. python seems to be broken: $pythonPath")
return@withContext null
}
return@withContext pythonPath
}
private suspend fun createVenv(systemPython: PythonBinary, venvDirPath: Path, projectPath: Path): Result<Unit, LocalizedErrorString> =
try {
createVirtualenv(systemPython, venvDirPath, projectPath)
Result.success(Unit)
}
catch (e: ExecutionException) {
Result.failure(LocalizedErrorString(PyCharmCommunityCustomizationBundle.message("misc.project.error.create.venv", e.toString(), venvDirPath)))
}
private suspend fun getSystemPython(obtainPythonStrategy: ObtainPythonStrategy): Result<PythonBinary, LocalizedErrorString> {
// First, find the latest python according to strategy
var systemPythonBinary = filterLatestUsablePython(
when (obtainPythonStrategy) {
is UseThesePythons -> obtainPythonStrategy.pythons
is FindOnSystem -> findPythonsOnSystem()
})
// No python found?
if (systemPythonBinary == null) {
// Only install if pythons weren't provided explicitly, see fun doc
when (obtainPythonStrategy) {
is UseThesePythons -> Unit
is FindOnSystem -> {
// User is ok with installation
if (obtainPythonStrategy.confirmInstallation()) {
// Install
installLatestPython().onFailure { exception ->
// Failed to install python?
logger.warn("Python installation failed", exception)
return Result.Failure(LocalizedErrorString(
PyCharmCommunityCustomizationBundle.message("misc.project.error.install.python", exception.toString())))
}
// Find latest python again, after installation
systemPythonBinary = filterLatestUsablePython(findPythonsOnSystem())
}
}
}
}
return if (systemPythonBinary == null) {
Result.Failure(LocalizedErrorString(PyCharmCommunityCustomizationBundle.message("misc.project.error.all.pythons.bad")))
}
else {
Result.Success(systemPythonBinary)
}
}
private suspend fun openProject(projectPath: Path): Project {
val projectManager = ProjectManagerEx.getInstanceEx()
val project = projectManager.openProjectAsync(projectPath, OpenProjectTask {
runConfigurators = false
}) ?: error("Failed to open project in $projectPath, check logs")
// There are countless number of reasons `openProjectAsync` might return null
if (project.modules.isEmpty()) {
writeAction {
ModuleManager.getInstance(project).newModule(projectPath, PythonModuleTypeBase.getInstance().id)
}
}
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 project != creating directory for it, but we need directory to create template file
*/
private suspend fun createProjectDir(projectPath: Path): Result<VirtualFile, LocalizedErrorString> = withContext(Dispatchers.IO) {
try {
projectPath.createDirectories()
}
catch (e: IOException) {
thisLogger().warn("Couldn't create $projectPath", e)
return@withContext Result.Failure(LocalizedErrorString(
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 = writeAction {
with(module.rootManager.modifiableModel) {
try {
if (root in contentRoots) return@writeAction
addContentEntry(root)
}
finally {
commit()
}
}
}
/**
* Looks for system pythons. Returns flavor and all its pythons.
*/
fun findPythonsOnSystem(): List<Pair<PythonSdkFlavor<*>, Collection<Path>>> =
PythonSdkFlavor.getApplicableFlavors(false) //system=platform dependent (exclude venv)
.map { flavor ->
flavor.dropCaches()
flavor to flavor.suggestLocalHomePaths(null, null)
}
.filter { (_, pythons) ->
pythons.isNotEmpty() // No need to have flavors without pythons
}
suspend fun installLatestPython(): kotlin.Result<Unit> = withContext(Dispatchers.IO) {
val pythonToInstall = PySdkToInstallManager.getAvailableVersionsToInstall().toSortedMap().values.last()
return@withContext withContext(Dispatchers.EDT) {
installBinary(pythonToInstall, null) {
}
}
}
/**
* Looks for the latest python among [flavorsToPythons]: each flavour might have 1 or more pythons.
* Broken pythons are filtered out. If `null` is returned, no python found, you probably need to [installLatestPython]
*/
private suspend fun filterLatestUsablePython(flavorsToPythons: List<Pair<PythonSdkFlavor<*>, Collection<Path>>>): PythonBinary? {
var current: Pair<LanguageLevel, Path>? = null
for ((flavor, paths) in flavorsToPythons) {
for (pythonPath in paths) {
val versionString = validatePythonAndGetVersion(pythonPath, flavor) ?: continue
val languageLevel = flavor.getLanguageLevelFromVersionString(versionString)
// Highest possible, no need to search further
if (languageLevel == LanguageLevel.getLatest()) {
return pythonPath
}
if (current == null || current.first < languageLevel) {
// More recent Python found!
current = Pair(languageLevel, pythonPath)
}
}
}
return current?.second
}
/**
* Ensures that [pythonBinary] is executable and returns its version. `null` if python is broken (reports error to logs).
*
* Some pythons might be broken: they may be executable, even return a version, but still fail to execute it.
* As we need workable pythons, we validate it by executing
*/
private suspend fun validatePythonAndGetVersion(pythonBinary: PythonBinary, flavor: PythonSdkFlavor<*>): String? = withContext(Dispatchers.IO) {
val fileLogger = fileLogger()
val process =
try {
GeneralCommandLine(pythonBinary.toString(), "-c", "print(1)").createProcess()
}
catch (e: ExecutionException) {
fileLogger.warn("$pythonBinary is bad, skipping", e)
return@withContext null
}
val timeout = 5.seconds
return@withContext withTimeoutOrNull(timeout) {
val exitCode = process.awaitExit()
return@withTimeoutOrNull if (exitCode != 0) {
fileLogger.warn("$pythonBinary returned $exitCode, skipping")
null
}
else {
flavor.getVersionString(pythonBinary.pathString)
}
}.also {
if (it == null) {
fileLogger.warn("$pythonBinary didn't return in $timeout, skipping")
process.destroyForcibly()
}
}
}

View File

@@ -0,0 +1,5 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@ApiStatus.Internal
package com.intellij.pycharm.community.ide.impl.miscProject.impl;
import org.jetbrains.annotations.ApiStatus;

View File

@@ -0,0 +1,8 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
/**
* See `PY-75549`
*/
@ApiStatus.Internal
package com.intellij.pycharm.community.ide.impl.miscProject;
import org.jetbrains.annotations.ApiStatus;

View File

@@ -41,12 +41,14 @@ import com.intellij.pycharm.community.ide.impl.newProjectWizard.welcome.PyWelcom
import com.intellij.pycharm.community.ide.impl.newProjectWizard.welcome.PyWelcomeCollector.RunConfigurationResult
import com.intellij.pycharm.community.ide.impl.newProjectWizard.welcome.PyWelcomeCollector.ScriptResult
import com.intellij.pycharm.community.ide.impl.newProjectWizard.welcome.PyWelcomeCollector.logWelcomeRunConfiguration
import com.intellij.util.concurrency.annotations.RequiresReadLock
import com.intellij.util.concurrency.annotations.RequiresWriteLock
import com.intellij.xdebugger.XDebuggerUtil
import com.jetbrains.python.PythonPluginDisposable
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.run.PythonRunConfigurationProducer
import com.jetbrains.python.sdk.pythonSdk
import org.jetbrains.annotations.ApiStatus.Internal
import org.jetbrains.annotations.CalledInAny
import org.jetbrains.concurrency.CancellablePromise
import java.util.concurrent.Callable
@@ -199,7 +201,7 @@ internal object PyWelcome {
val psiFile = PsiManager.getInstance(project).findFile(file) ?: error("File $file was just created, but not found in PSI")
writeText(project, psiFile)?.also { line ->
writeText(psiFile)?.also { line ->
PyWelcomeCollector.logWelcomeScript(project, ScriptResult.CREATED)
XDebuggerUtil.getInstance().toggleLineBreakpoint(project, file, line)
@@ -208,7 +210,10 @@ internal object PyWelcome {
return psiFile
}
private fun writeText(project: Project, psiFile: PsiFile): Int? {
@RequiresWriteLock
internal fun writeText(psiFile: PsiFile): Int? {
val project = psiFile.project
val document = PsiDocumentManager.getInstance(project).getDocument(psiFile)
if (document == null) {
LOG.warn("Unable to get document for ${psiFile.virtualFile}")

View File

@@ -0,0 +1,10 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python
import org.jetbrains.annotations.Nls
/**
* Error string to be used with [Result]
*/
@JvmInline
value class LocalizedErrorString(val text: @Nls String)

View File

@@ -1,18 +1,46 @@
// Copyright 2000-2018 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.jetbrains.python
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.NonNls
import com.jetbrains.python.Result.Failure
import com.jetbrains.python.Result.Success
/**
* Operation result to be used with pattern matching.
* Must be replaced with stdlib solution after [https://github.com/Kotlin/KEEP/blob/master/proposals/stdlib/result.md] completion.
* Operation result to be used as `Maybe` instead of checked exceptions.
* Unlike Kotlin `Result`, [ERR] could be anything (See [LocalizedErrorString]).
*
* Can't be moved to core module because core modules do not support Kotlin and there is no sealed classes in java.
* Typical usages:
*
* ```kotlin
* when(val r = someFun() {
* is Result.Success -> r.result // is ok
* is Result.Failure -> r.error // is error
* }
* ```
* Get result or throw error (I am 100% sure there is no error): [orThrow].
*
* Chain several calls, get latest result or first error (all errors are the same): [mapResult].
*
* When errors are different: [mapResultWithErr]
*
* Fast return: [getOr]
* ```kotlin
* fun foo() {
* val data = getSomeResult().getOr { return }
* }
* ```
*
* Return from function with same error
* ```kotlin
* fun foo():Result<String, Int> {
* // Returns Result<Foo, Int>
* getSomeResult().getOr { return it }
* }
* ```
* See showcase in tests.
*/
sealed class Result<SUCC, ERR> {
data class Failure<SUCC, ERR>(val error: ERR) : Result<SUCC, ERR>()
data class Success<SUCC, ERR>(val result: SUCC) : Result<SUCC, ERR>()
sealed class Result<out SUCC, out ERR> {
data class Failure<out ERR>(val error: ERR) : Result<Nothing, ERR>()
data class Success<out SUCC>(val result: SUCC) : Result<SUCC, Nothing>()
fun <RES> map(map: (SUCC) -> RES): Result<RES, ERR> =
when (this) {
@@ -20,11 +48,70 @@ sealed class Result<SUCC, ERR> {
is Failure -> Failure(error)
}
/***
* ```kotlin
* val data = someFun().getOr { return }
* ```
*/
inline fun getOr(onFailure: (err: Failure<ERR>) -> Nothing): SUCC {
when (this) {
is Failure -> onFailure(this)
is Success -> return result
}
}
/**
* Same as [mapResult] but for different errors
* ```kotlin
* val drinkResultOrFirstError = findBeer()
* .mapResult{ openBeer(it) }
* .mapResultWithErr(
* onSuccess = { drink(it) },
* onErr = { LocalizedErrorString("Oops, ${it.message}") }
* )
* ```
*/
inline fun <NEW_ERR, NEW_S> mapResultWithErr(
onSuccess: (SUCC) -> Result<NEW_S, NEW_ERR>,
onErr: (ERR) -> NEW_ERR,
): Result<NEW_S, NEW_ERR> =
when (this) {
is Success -> onSuccess(result)
is Failure -> Failure(onErr(error))
}
val successOrNull: SUCC? get() = if (this is Success) result else null
fun orThrow(onError: (ERR) -> Throwable = { e -> AssertionError(e) }): SUCC {
/**
* Like Rust `unwrap`: returns result or throws exception. Use when error is unexpected
*/
fun orThrow(onError: (ERR) -> Throwable = { e -> if (e is Throwable) e else AssertionError(e) }): SUCC {
when (this) {
is Success -> return result
is Failure -> throw onError(this.error)
}
}
}
// To be backward compatible with Kotlin result
companion object {
fun <S> success(value: S) = Success(value)
fun <E> failure(error: E) = Failure(error)
}
}
/**
* Maps success result to another one with same error
* ```kotlin
* val drinkResultOrFirstError = findBeer()
* .mapResult{ openBeer(it) }
* .mapResult{ drinkIt(it) }
* ```
*/
fun <SUCC, NEW_S, ERR> Result<SUCC, ERR>.mapResult(map: (SUCC) -> Result<NEW_S, ERR>): Result<NEW_S, ERR> =
when (this) {
is Success -> map(result)
is Failure -> this
}

View File

@@ -26,4 +26,5 @@ public final class PythonPsiApiIcons {
/** 16x16 */ public static final @NotNull Icon PropertyGetter = load("icons/com/jetbrains/python/psi/propertyGetter.svg", 1495604199, 2);
/** 16x16 */ public static final @NotNull Icon PropertySetter = load("icons/com/jetbrains/python/psi/propertySetter.svg", -1451064081, 2);
/** 16x16 */ public static final @NotNull Icon Python = load("icons/com/jetbrains/python/psi/python.svg", 2008591516, 8);
/** 32x32 */ public static final @NotNull Icon Python_32x32 = load("icons/com/jetbrains/python/psi/python@32x32.svg", -2111231783, 2);
}

View File

@@ -0,0 +1,5 @@
<!-- Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 2C22 2 22 4 22 8L22 13C22 14.6569 20.6569 16 19 16H13C10.2386 16 8 18.2386 8 21V22C4 22 2 22 2 16C2 9.99997 4 9.99997 8 9.99997L15 10C15.5523 10 16 9.55228 16 9C16 8.44771 15.5523 8 15 8H10C10 4 10 2 16 2ZM13 6C13.5523 6 14 5.55228 14 5C14 4.44772 13.5523 4 13 4C12.4477 4 12 4.44772 12 5C12 5.55228 12.4477 6 13 6Z" fill="#4682FA"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M24 10V13C24 15.7614 21.7614 18 19 18H13C11.3431 18 10 19.3431 10 21L10 24C9.99893 28 10 30 16 30C22 30 22 28 22 24.0001L17 24C16.4477 24 16 23.5523 16 23C16 22.4477 16.4477 22 17 22L24 22C28 22.0011 30 22 30 16C30 10 27.9999 10 24 10ZM19 28C19.5523 28 20 27.5523 20 27C20 26.4477 19.5523 26 19 26C18.4477 26 18 26.4477 18 27C18 27.5523 18.4477 28 19 28Z" fill="#FFAF0F"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,5 @@
<!-- Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 2C22 2 22 4 22 8L22 13C22 14.6569 20.6569 16 19 16H13C10.2386 16 8 18.2386 8 21V22C4 22 2 22 2 16C2 9.99997 4 9.99997 8 9.99997L15 10C15.5523 10 16 9.55228 16 9C16 8.44771 15.5523 8 15 8H10C10 4 10 2 16 2ZM13 6C13.5523 6 14 5.55228 14 5C14 4.44772 13.5523 4 13 4C12.4477 4 12 4.44772 12 5C12 5.55228 12.4477 6 13 6Z" fill="#548AF7"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M24 10V13C24 15.7614 21.7614 18 19 18H13C11.3431 18 10 19.3431 10 21L10 24C9.99893 28 10 30 16 30C22 30 22 28 22 24.0001L17 24C16.4477 24 16 23.5523 16 23C16 22.4477 16.4477 22 17 22L24 22C28 22.0011 30 22 30 16C30 10 27.9999 10 24 10ZM19 28C19.5523 28 20 27.5523 20 27C20 26.4477 19.5523 26 19 26C18.4477 26 18 26.4477 18 27C18 27.5523 18.4477 28 19 28Z" fill="#F2C55C"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -22,6 +22,7 @@ import com.jetbrains.python.psi.LanguageLevel;
import com.jetbrains.python.psi.icons.PythonPsiApiIcons;
import com.jetbrains.python.run.CommandLinePatcher;
import com.jetbrains.python.sdk.*;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -99,6 +100,14 @@ public abstract class PythonSdkFlavor<D extends PyFlavorData> {
return Collections.emptyList();
}
/**
* Flavor might cache results of {@link #suggestLocalHomePaths(Module, UserDataHolder)}
* This method resets them
*/
@ApiStatus.Internal
public void resetHomePathCache() {
}
/**
* Flavor is added to result in {@link #getApplicableFlavors()} if this method returns true.

View File

@@ -74,7 +74,7 @@ abstract class PyV3ProjectBaseGenerator<TYPE_SPECIFIC_SETTINGS : PyV3ProjectType
override fun validate(baseDirPath: String): ValidationResult =
when (val pathOrError = validatePath(baseDirPath)) {
is Result.Success<Path, *> -> {
is Result.Success -> {
ValidationResult.OK
}
is Result.Failure -> ValidationResult(pathOrError.error)

View File

@@ -7,7 +7,6 @@ import com.intellij.openapi.module.ModuleUtil
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.platform.ide.progress.ModalTaskOwner
import com.intellij.platform.ide.progress.TaskCancellation
@@ -19,7 +18,6 @@ import com.jetbrains.python.sdk.VirtualEnvReader
import com.jetbrains.python.sdk.conda.createCondaSdkFromExistingEnv
import com.jetbrains.python.sdk.excludeInnerVirtualEnv
import com.jetbrains.python.sdk.flavors.conda.PyCondaCommand
import com.jetbrains.python.sdk.suggestAssociatedSdkName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.nio.file.Path
@@ -66,15 +64,7 @@ suspend fun PythonMutableTargetAddInterpreterModel.setupVirtualenv(venvPath: Pat
return failure(message("commandLine.directoryCantBeAccessed", venvPath))
}
// "suggest name" calls external process and can't be called from EDT
val newSdk = withContext(Dispatchers.IO) {
val suggestedName = /*suggestedSdkName ?:*/ suggestAssociatedSdkName(homeFile.path, projectPath.toString())
SdkConfigurationUtil.setupSdk(existingSdks.toTypedArray(), homeFile,
PythonSdkType.getInstance(),
false, null, suggestedName)!!
}
addSdk(newSdk)
val newSdk = createSdk(homeFile, projectPath, existingSdks.toTypedArray())
// todo check exclude
ProjectManager.getInstance().openProjects

View File

@@ -4,7 +4,39 @@ package com.jetbrains.python.sdk.add.v2
import com.intellij.openapi.application.writeAction
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
import com.intellij.openapi.vfs.VirtualFile
import com.jetbrains.python.sdk.PythonSdkType
import com.jetbrains.python.sdk.suggestAssociatedSdkName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.nio.file.Path
/**
* Creates and persists sdk.
* [projectPath] is used to suggest name
*/
suspend fun createSdk(
pythonBinaryPath: VirtualFile,
projectPath: Path?,
existingSdks: Array<Sdk>,
): Sdk {
val newSdk = withContext(Dispatchers.IO) {
// "suggest name" calls external process and can't be called from EDT
val suggestedName = /*suggestedSdkName ?:*/ suggestAssociatedSdkName(pythonBinaryPath.path, projectPath?.toString())
SdkConfigurationUtil.setupSdk(existingSdks, pythonBinaryPath,
PythonSdkType.getInstance(),
null, suggestedName)
}
addSdk(newSdk)
return newSdk
}
/**
* Persists [sdk]
*/
internal suspend fun addSdk(sdk: Sdk) {
writeAction {
ProjectJdkTable.getInstance().addJdk(sdk)

View File

@@ -15,6 +15,7 @@ import com.jetbrains.python.run.PythonExecution
import com.jetbrains.python.run.prepareHelperScriptExecution
import com.jetbrains.python.run.target.HelpersAwareLocalTargetEnvironmentRequest
import com.jetbrains.python.sdk.PySdkSettings
import com.jetbrains.python.sdk.PythonBinary
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus.Internal
@@ -26,7 +27,7 @@ import java.nio.file.Path
@Throws(ExecutionException::class)
@Internal
suspend fun createVirtualenv(
baseInterpreterPath: Path,
baseInterpreterPath: PythonBinary,
venvRoot: Path,
projectBasePath: Path,
inheritSitePackages: Boolean = false,

View File

@@ -84,6 +84,11 @@ public class WinPythonSdkFlavor extends CPythonSdkFlavor<PyFlavorData.Empty> {
candidates.addAll(myAppxCache.getValue());
}
@Override
public final void resetHomePathCache() {
myRegistryCache.drop();
}
@Override
public boolean sdkSeemsValid(@NotNull Sdk sdk,
PyFlavorData.@NotNull Empty flavorData,

View File

@@ -16,6 +16,7 @@ import com.jetbrains.python.sdk.conda.TargetEnvironmentRequestCommandExecutor
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking
import org.jetbrains.annotations.NonNls
import java.nio.file.Path
@@ -33,17 +34,28 @@ typealias PathToPythonBinary = Path
*/
sealed class PythonType<T : Any>(private val tag: @NonNls String) {
suspend fun getTestEnvironment(vararg additionalTags: @NonNls String): Result<Pair<T, AutoCloseable>> =
/**
* Returns all test environments: each must be closed after the test.
*/
suspend fun getTestEnvironments(vararg additionalTags: @NonNls String): Flow<Pair<T, AutoCloseable>> =
PyEnvTestSettings
.fromEnvVariables()
.pythons
.asFlow()
.map { it.toPath() }
.firstOrNull { typeMatchesEnv(it, *additionalTags) }
?.let { envDir ->
Result.success(pythonPathToEnvironment(
.filter { typeMatchesEnv(it, *additionalTags) }
.map { envDir ->
pythonPathToEnvironment(
VirtualEnvReader.Instance.findPythonInPythonRoot(envDir)
?: error("Can't find python binary in $envDir"), envDir)) // This is a misconfiguration, hence an error
?: error("Can't find python binary in $envDir"), envDir) // This is a misconfiguration, hence an error
}
/**
* Returns first (whatever it means) test environment and closable that must be closed after the test
*/
suspend fun getTestEnvironment(vararg additionalTags: @NonNls String): Result<Pair<T, AutoCloseable>> =
getTestEnvironments(*additionalTags).firstOrNull()?.let { Result.success(it) }
?: failure("No python found. See ${PyEnvTestSettings::class} class for more info")