diff --git a/python/python-pyproject/src/com/intellij/python/pyproject/model/api/sdkSuggestionTools.kt b/python/python-pyproject/src/com/intellij/python/pyproject/model/api/sdkSuggestionTools.kt index c7cdf24398ef..b84a89065347 100644 --- a/python/python-pyproject/src/com/intellij/python/pyproject/model/api/sdkSuggestionTools.kt +++ b/python/python-pyproject/src/com/intellij/python/pyproject/model/api/sdkSuggestionTools.kt @@ -1,8 +1,12 @@ package com.intellij.python.pyproject.model.api import com.intellij.openapi.module.Module +import com.intellij.openapi.project.guessModuleDir import com.intellij.python.common.tools.ToolId import com.intellij.python.pyproject.model.internal.suggestSdkImpl +import com.jetbrains.python.sdk.configuration.CreateSdkInfo +import com.jetbrains.python.sdk.configuration.CreateSdkInfoWithTool +import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension import com.jetbrains.python.venvReader.Directory @@ -24,4 +28,36 @@ sealed interface SuggestedSdk { * Suggests how to configure SDK for a certain module. * `null` means this module is not `pyproject.toml` based */ -suspend fun Module.suggestSdk(): SuggestedSdk? = suggestSdkImpl(this) \ No newline at end of file +suspend fun Module.suggestSdk(): SuggestedSdk? = suggestSdkImpl(this) + + +/** + * For multiple calls, pull [configuratorsByTool] up not to create it each time + */ +suspend fun Module.getModuleInfo( + configuratorsByTool: Map = PyProjectSdkConfigurationExtension.createMap(), +): ModuleCreateInfo? = // Save on module level + when (val r = suggestSdk()) { + is SuggestedSdk.PyProjectIndependent -> { + val tools = r.preferTools.map { configuratorsByTool[it]!! } + tools.firstNotNullOfOrNull { tool -> + val createInfo = (tool.asPyProjectTomlSdkConfigurationExtension()?.createSdkWithoutPyProjectTomlChecks(this) + ?: tool.checkEnvironmentAndPrepareSdkCreator(this)) ?: return@firstNotNullOfOrNull null + CreateSdkInfoWithTool(createInfo, tool.toolId).asDTO(r.moduleDir) + } + } + is SuggestedSdk.SameAs -> { + ModuleCreateInfo.SameAs(r.parentModule) + } + null -> null + } // No tools or not pyproject.toml at all? Use EP as a fallback + ?: PyProjectSdkConfigurationExtension.findAllSortedForModule(this).firstOrNull() + ?.let { CreateSdkInfoWithTool(it.createSdkInfo, it.toolId).asDTO(guessModuleDir()?.toNioPath()) } + +sealed interface ModuleCreateInfo { + data class CreateSdkInfoWrapper(val createSdkInfo: CreateSdkInfo, val toolId: ToolId, val moduleDir: Directory?) : ModuleCreateInfo + data class SameAs(val parentModule: Module) : ModuleCreateInfo +} + +private fun CreateSdkInfoWithTool.asDTO(moduleDir: Directory?): ModuleCreateInfo = + ModuleCreateInfo.CreateSdkInfoWrapper(createSdkInfo, toolId, moduleDir) diff --git a/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/workspaceBridge/workspaceTools.kt b/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/workspaceBridge/workspaceTools.kt index 4fe5dbbbc859..273ef488841f 100644 --- a/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/workspaceBridge/workspaceTools.kt +++ b/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/workspaceBridge/workspaceTools.kt @@ -112,20 +112,23 @@ internal fun relocateUserDefinedModuleSdk(storage: MutableEntityStorage, transfe } } -// TODO: DOC +/** + * Return [entires_to_create_modules_from, dirs_to_exclude] + */ private suspend fun generatePyProjectTomlEntries( fsInfo: FSWalkInfoWithToml, ): Pair, Set> = withContext(Dispatchers.Default) { val (files, allExcludeDirs) = fsInfo val entries = ArrayList() val usedNamed = mutableSetOf() + val tools = Tool.EP.extensionList // Any tool that helped us somehow must be tracked here for ((tomlFile, toml) in files.entries) { val participatedTools = mutableSetOf() val root = tomlFile.parent var projectNameAsString = toml.project?.name if (projectNameAsString == null) { - val toolAndName = getNameFromEP(toml) + val toolAndName = tools.getNameFromEP(toml) if (toolAndName != null) { projectNameAsString = toolAndName.second participatedTools.add(toolAndName.first.id) @@ -142,6 +145,15 @@ private suspend fun generatePyProjectTomlEntries( val sourceRoots = sourceRootsAndTools.map { it.second }.toSet() + findSrc(root) participatedTools.addAll(sourceRootsAndTools.map { it.first.id }) val excludedDirs = allExcludeDirs.filter { it.startsWith(root) } + if (participatedTools.isEmpty()) { + // Try to use build-tool as last resort + toml.toml.getString("build-system.build-backend")?.let { buildBackend -> + tools.firstOrNull { it.id.id in buildBackend }?.let { buildTool -> + participatedTools.add(buildTool.id) + } + } + } + val relationsWithTools: List = participatedTools.map { PyProjectTomlToolRelation.SimpleRelation(it) } val entry = PyProjectTomlBasedEntryImpl(tomlFile, HashSet(relationsWithTools), @@ -175,9 +187,10 @@ private suspend fun generatePyProjectTomlEntries( return@withContext Pair(entries.toSet(), allExcludeDirs) } -private suspend fun getNameFromEP(projectToml: PyProjectToml): Pair? = withContext(Dispatchers.Default) { - Tool.EP.extensionList.firstNotNullOfOrNull { tool -> tool.getProjectName(projectToml.toml)?.let { Pair(tool, it) } } -} +private suspend fun Iterable.getNameFromEP(projectToml: PyProjectToml): Pair? = + withContext(Dispatchers.Default) { + firstNotNullOfOrNull { tool -> tool.getProjectName(projectToml.toml)?.let { Pair(tool, it) } } + } private suspend fun createEntityStorage( graph: Set, diff --git a/python/python-sdk-configurator/backend/src/impl/ModulesSdkConfigurator.kt b/python/python-sdk-configurator/backend/src/impl/ModulesSdkConfigurator.kt index fc6b55343b59..d77826f8bfa3 100644 --- a/python/python-sdk-configurator/backend/src/impl/ModulesSdkConfigurator.kt +++ b/python/python-sdk-configurator/backend/src/impl/ModulesSdkConfigurator.kt @@ -5,14 +5,14 @@ import com.intellij.openapi.diagnostic.fileLogger import com.intellij.openapi.diagnostic.trace import com.intellij.openapi.module.Module import com.intellij.openapi.project.Project -import com.intellij.openapi.project.guessModuleDir import com.intellij.openapi.project.modules import com.intellij.openapi.roots.ModuleRootModificationUtil import com.intellij.openapi.util.Key import com.intellij.openapi.util.removeUserData import com.intellij.platform.ide.progress.withBackgroundProgress -import com.intellij.python.common.tools.ToolId +import com.intellij.python.pyproject.model.api.ModuleCreateInfo import com.intellij.python.pyproject.model.api.SuggestedSdk +import com.intellij.python.pyproject.model.api.getModuleInfo import com.intellij.python.pyproject.model.api.suggestSdk import com.intellij.python.sdkConfigurator.backend.impl.ModulesSdkConfigurator.Companion.create import com.intellij.python.sdkConfigurator.backend.impl.ModulesSdkConfigurator.Companion.popModulesSDKConfigurator @@ -21,12 +21,10 @@ import com.intellij.python.sdkConfigurator.common.impl.ModuleName import com.jetbrains.python.PathShortener import com.jetbrains.python.Result import com.jetbrains.python.sdk.configuration.CreateSdkInfo -import com.jetbrains.python.sdk.configuration.CreateSdkInfoWithTool import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension import com.jetbrains.python.sdk.getOrCreateAdditionalData import com.jetbrains.python.sdk.legacy.PythonSdkUtil import com.jetbrains.python.sdk.setAssociationToPath -import com.jetbrains.python.venvReader.Directory import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -60,7 +58,7 @@ internal class ModulesSdkConfigurator private constructor( when (createInfo) { is ModuleCreateInfo.CreateSdkInfoWrapper -> Unit is ModuleCreateInfo.SameAs -> { - children.getOrPut(createInfo.parentModuleName) { HashSet() }.add(moduleName) + children.getOrPut(createInfo.parentModule.name) { HashSet() }.add(moduleName) } } } @@ -111,7 +109,7 @@ internal class ModulesSdkConfigurator private constructor( val now = System.currentTimeMillis() val resultDef = project.modules.filter { PythonSdkUtil.findPythonSdk(it) == null }.map { module -> async { - val moduleInfo = getModuleInfo(module, tools) ?: return@async null + val moduleInfo = module.getModuleInfo(tools) ?: return@async null Pair(module, moduleInfo) } } @@ -154,7 +152,8 @@ internal class ModulesSdkConfigurator private constructor( } } is ModuleCreateInfo.SameAs -> { - val parent = modulesMap[createInfo.parentModuleName] ?: error("No parent module named ${createInfo.parentModuleName}") + val parentModuleName = createInfo.parentModule.name + val parent = modulesMap[parentModuleName] ?: error("No parent module named $parentModuleName") modulesWithSameSdk[module] = parent } } @@ -193,28 +192,7 @@ suspend fun configureSdkAutomatically(project: Project): Unit = withContext(Disp 1 -> { // Single-module project, associate first module only val module = modules.first() if (PythonSdkUtil.findPythonSdk(module) != null) return@withContext // Already has SDK - when (val moduleInfo = getModuleInfo(module)) { - is ModuleCreateInfo.CreateSdkInfoWrapper -> { - when (val info = moduleInfo.createSdkInfo) { - is CreateSdkInfo.ExistingEnv -> { - when (val r = info.sdkCreator(false)) { - is Result.Failure -> { - logger.trace { "Failed to create sdk for ${module.name} : ${r.error}" } - } - is Result.Success -> { - val sdk = r.result!! // It can't be null: this is an old buggy API that will be fixed soon - ModuleRootModificationUtil.setModuleSdk(module, sdk) - logger.trace { "SDK creation result for ${module.name} : $sdk" } - } - } - } - is CreateSdkInfo.WillCreateEnv -> { - logger.trace { "${module.name} can't be configured automatically: no venv for ${info.intentionName}" } - } - } - } - is ModuleCreateInfo.SameAs, null -> Unit - } + configureSdkForModuleAutomatically(module, createEnvIfNeeded = false) } else -> { // Multi-module project, only associate workspace members val modulesToSdk = modules.filter { PythonSdkUtil.findPythonSdk(it) == null }.associateWith { it.suggestSdk() } @@ -224,9 +202,7 @@ suspend fun configureSdkAutomatically(project: Project): Unit = withContext(Disp logger.trace { "${module.name} skipped in multimodule project autoconfig" } } is SuggestedSdk.SameAs -> { - val parentSdk = PythonSdkUtil.findPythonSdk(sdkSuggestion.parentModule) ?: continue - logger.trace { "${module.name} seeds the same sdk as ${sdkSuggestion.parentModule} : ${parentSdk}" } - ModuleRootModificationUtil.setModuleSdk(module, parentSdk) + setModuleSdkAsParent(parentModule = sdkSuggestion.parentModule, module = module) } } } @@ -234,37 +210,52 @@ suspend fun configureSdkAutomatically(project: Project): Unit = withContext(Disp } } - -private sealed interface ModuleCreateInfo { - data class CreateSdkInfoWrapper(val createSdkInfo: CreateSdkInfo, val toolId: ToolId, val moduleDir: Directory?) : ModuleCreateInfo - data class SameAs(val parentModuleName: ModuleName) : ModuleCreateInfo -} - -private val logger = fileLogger() - -/** - * For multiple calls, pull [configuratorsByTool] up not to create it each time - */ -private suspend fun getModuleInfo( - module: Module, - configuratorsByTool: Map = PyProjectSdkConfigurationExtension.createMap(), -): ModuleCreateInfo? = // Save on module level - when (val r = module.suggestSdk()) { - is SuggestedSdk.PyProjectIndependent -> { - val tools = r.preferTools.map { configuratorsByTool[it]!! } - tools.firstNotNullOfOrNull { tool -> - val createInfo = (tool.asPyProjectTomlSdkConfigurationExtension()?.createSdkWithoutPyProjectTomlChecks(module) - ?: tool.checkEnvironmentAndPrepareSdkCreator(module)) ?: return@firstNotNullOfOrNull null - CreateSdkInfoWithTool(createInfo, tool.toolId).asDTO(r.moduleDir) +private suspend fun configureSdkForModuleAutomatically(module: Module, createEnvIfNeeded: Boolean) { + when (val moduleInfo = module.getModuleInfo()) { + is ModuleCreateInfo.CreateSdkInfoWrapper -> { + when (val info = moduleInfo.createSdkInfo) { + is CreateSdkInfo.ExistingEnv -> { + info.createAndSetToModule(module) + } + is CreateSdkInfo.WillCreateEnv -> { + if (createEnvIfNeeded) { + info.createAndSetToModule(module) + } + else { + logger.trace { "${module.name} can't be configured automatically: no venv for ${info.intentionName}" } + } + } } } - is SuggestedSdk.SameAs -> { - ModuleCreateInfo.SameAs(r.parentModule.name) + is ModuleCreateInfo.SameAs -> { + val parentModule = moduleInfo.parentModule + setModuleSdkAsParent(parentModule = parentModule, module = module) } - null -> null - } // No tools or not pyproject.toml at all? Use EP as a fallback - ?: PyProjectSdkConfigurationExtension.findAllSortedForModule(module).firstOrNull() - ?.let { CreateSdkInfoWithTool(it.createSdkInfo, it.toolId).asDTO(module.guessModuleDir()?.toNioPath()) } + null -> Unit + } +} -private fun CreateSdkInfoWithTool.asDTO(moduleDir: Directory?): ModuleCreateInfo = - ModuleCreateInfo.CreateSdkInfoWrapper(createSdkInfo, toolId, moduleDir) +private suspend fun CreateSdkInfo.createAndSetToModule(module: Module) { + when (val r = sdkCreator(false)) { + is Result.Failure -> { + logger.trace { "Failed to create sdk for ${module.name} : ${r.error}" } + } + is Result.Success -> { + val sdk = r.result!! // It can't be null: this is an old buggy API that will be fixed soon + ModuleRootModificationUtil.setModuleSdk(module, sdk) + logger.trace { "SDK creation result for ${module.name} : $sdk" } + } + } +} + +private fun setModuleSdkAsParent( + parentModule: Module, + module: Module, +) { + val parentSdk = PythonSdkUtil.findPythonSdk(parentModule) ?: return + logger.trace { "${module.name} seeds the same sdk as ${parentModule} : ${parentSdk}" } + ModuleRootModificationUtil.setModuleSdk(module, parentSdk) +} + + +private val logger = fileLogger() diff --git a/python/python-sdk-configurator/backend/src/impl/rpcBridge/SdkConfiguratorApiImpl.kt b/python/python-sdk-configurator/backend/src/impl/rpcBridge/SdkConfiguratorApiImpl.kt index e8a0e205d35b..42da70fe2339 100644 --- a/python/python-sdk-configurator/backend/src/impl/rpcBridge/SdkConfiguratorApiImpl.kt +++ b/python/python-sdk-configurator/backend/src/impl/rpcBridge/SdkConfiguratorApiImpl.kt @@ -2,9 +2,7 @@ package com.intellij.python.sdkConfigurator.backend.impl.rpcBridge import com.intellij.platform.project.ProjectId import com.intellij.platform.project.findProject -import com.intellij.python.sdkConfigurator.backend.impl.ModuleConfigurationMode import com.intellij.python.sdkConfigurator.backend.impl.ModulesSdkConfigurator.Companion.popModulesSDKConfigurator -import com.intellij.python.sdkConfigurator.backend.impl.configureSdk import com.intellij.python.sdkConfigurator.common.impl.ModuleName import com.intellij.python.sdkConfigurator.common.impl.SdkConfiguratorBackEndApi @@ -12,8 +10,4 @@ internal object SdkConfiguratorApiImpl : SdkConfiguratorBackEndApi { override suspend fun configureSdkForModules(projectId: ProjectId, onlyModules: Set) { projectId.findProject().popModulesSDKConfigurator().configureSdks(onlyModules) } - - override suspend fun configureAskingUser(projectId: ProjectId) { - configureSdk(projectId.findProject(), mode = ModuleConfigurationMode.INTERACTIVE) - } } \ No newline at end of file diff --git a/python/python-sdk-configurator/common/src/impl/SdkConfiguratorBackEndApi.kt b/python/python-sdk-configurator/common/src/impl/SdkConfiguratorBackEndApi.kt index 17442d888598..32adce0aa6f3 100644 --- a/python/python-sdk-configurator/common/src/impl/SdkConfiguratorBackEndApi.kt +++ b/python/python-sdk-configurator/common/src/impl/SdkConfiguratorBackEndApi.kt @@ -17,10 +17,6 @@ interface SdkConfiguratorBackEndApi : RemoteApi { */ suspend fun configureSdkForModules(projectId: ProjectId, onlyModules: Set) - /** - * Ask user about modules, then call [configureSdkForModules] - */ - suspend fun configureAskingUser(projectId: ProjectId) } diff --git a/python/python-sdk-configurator/common/src/publicApi.kt b/python/python-sdk-configurator/common/src/publicApi.kt index 76689c4f8acf..d054734c723f 100644 --- a/python/python-sdk-configurator/common/src/publicApi.kt +++ b/python/python-sdk-configurator/common/src/publicApi.kt @@ -1,9 +1,6 @@ package com.intellij.python.sdkConfigurator.common -import com.intellij.openapi.project.Project import com.intellij.openapi.util.registry.Registry -import com.intellij.platform.project.projectId -import com.intellij.python.sdkConfigurator.common.impl.SdkConfiguratorBackEndApi /** @@ -11,10 +8,3 @@ import com.intellij.python.sdkConfigurator.common.impl.SdkConfiguratorBackEndApi */ val enableSDKAutoConfigurator: Boolean get() = Registry.`is`("intellij.python.sdkConfigurator.auto") - -/** - * it might ask user for list of modules and then detect SDK for them - */ -suspend fun detectSdkForModulesIn(project: Project) { - SdkConfiguratorBackEndApi().configureAskingUser(project.projectId()) -} diff --git a/python/src/com/jetbrains/python/inspections/interpreter/PyInterpreterInspection.kt b/python/src/com/jetbrains/python/inspections/interpreter/PyInterpreterInspection.kt index b88b85b9029c..75a03ca4b4b5 100644 --- a/python/src/com/jetbrains/python/inspections/interpreter/PyInterpreterInspection.kt +++ b/python/src/com/jetbrains/python/inspections/interpreter/PyInterpreterInspection.kt @@ -20,8 +20,8 @@ import com.intellij.openapi.roots.ProjectRootManager import com.intellij.openapi.roots.ui.configuration.projectRoot.ProjectSdksModel import com.intellij.openapi.util.UserDataHolderBase import com.intellij.psi.PsiElementVisitor -import com.intellij.python.sdkConfigurator.common.detectSdkForModulesIn -import com.intellij.python.sdkConfigurator.common.enableSDKAutoConfigurator +import com.intellij.python.pyproject.model.api.ModuleCreateInfo +import com.intellij.python.pyproject.model.api.getModuleInfo import com.intellij.util.PathUtil import com.jetbrains.python.PyPsiBundle import com.jetbrains.python.PythonIdeLanguageCustomization @@ -37,7 +37,6 @@ import com.jetbrains.python.sdk.* import com.jetbrains.python.sdk.conda.PyCondaSdkCustomizer import com.jetbrains.python.sdk.configuration.CreateSdkInfoWithTool import com.jetbrains.python.sdk.configuration.PyProjectSdkConfiguration -import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension import com.jetbrains.python.sdk.legacy.PythonSdkUtil import com.jetbrains.python.ui.PyUiUtil import kotlinx.coroutines.CoroutineScope @@ -109,8 +108,9 @@ private suspend fun getSuitableSdkFix(name: String?, module: Module): LocalQuick if (associatedSdk != null) return@withContext UseExistingInterpreterFix(associatedSdk, module) val context = UserDataHolderBase() - val createSdkInfo = PyProjectSdkConfigurationExtension.findAllSortedForModule(module).firstOrNull() - if (createSdkInfo != null) return@withContext UseProvidedInterpreterFix(module, createSdkInfo) + + val quickFixBySdkSuggestion = module.getQuickFixBySdkSuggestion() + if (quickFixBySdkSuggestion != null) return@withContext quickFixBySdkSuggestion if (name != null) { val matcher = NAME.matcher(name) @@ -204,13 +204,8 @@ private class UseProvidedInterpreterFix(private val myModule: Module, private va override fun getName(): String = myCreateSdkInfo.createSdkInfo.intentionName override fun applyFix(project: Project, descriptor: ProblemDescriptor) { - if (!enableSDKAutoConfigurator) { - return PyProjectSdkConfiguration.configureSdkUsingCreateSdkInfo(myModule, myCreateSdkInfo) - } - - project.service().scope.launch { - detectSdkForModulesIn(project) - } + PyProjectSdkConfiguration.configureSdkUsingCreateSdkInfo(myModule, myCreateSdkInfo) + PyUiUtil.clearFileLevelInspectionResults(descriptor.psiElement.containingFile) } override fun generatePreview(project: Project, previewDescriptor: ProblemDescriptor): IntentionPreviewInfo { @@ -253,4 +248,23 @@ private class UseDetectedInterpreterFix( } @Service(Service.Level.PROJECT) -private class MyService(val scope: CoroutineScope) \ No newline at end of file +private class MyService(val scope: CoroutineScope) + +private suspend fun Module.getQuickFixBySdkSuggestion(): LocalQuickFix? = when (val i = getModuleInfo()) { + is ModuleCreateInfo.CreateSdkInfoWrapper -> { + val tool = CreateSdkInfoWithTool(i.createSdkInfo, i.toolId) + UseProvidedInterpreterFix(this, tool) + } + is ModuleCreateInfo.SameAs -> { + val parentModuleSdk = i.parentModule.pythonSdk + if (parentModuleSdk != null) { + UseExistingInterpreterFix(parentModuleSdk, this) + } + else { + // Parent has no SDK, configure it first + // TODO: Check for SO + i.parentModule.getQuickFixBySdkSuggestion() + } + } + null -> null +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfiguration.kt b/python/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfiguration.kt index af1111c4afa3..b879b8a9a06a 100644 --- a/python/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfiguration.kt +++ b/python/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfiguration.kt @@ -8,7 +8,6 @@ import com.intellij.openapi.Disposable import com.intellij.openapi.application.EDT import com.intellij.openapi.diagnostic.thisLogger import com.intellij.openapi.module.Module -import com.intellij.openapi.progress.runBlockingMaybeCancellable import com.intellij.openapi.project.Project import com.intellij.openapi.project.isNotificationSilentMode import com.intellij.openapi.projectRoots.Sdk @@ -58,12 +57,6 @@ object PyProjectSdkConfiguration { true } - fun setReadyToUseSdkSync(project: Project, module: Module, sdk: Sdk) { - runBlockingMaybeCancellable { - setReadyToUseSdk(project, module, sdk) - } - } - suspend fun setReadyToUseSdk(project: Project, module: Module, sdk: Sdk) { if (module.isDisposed) { return