diff --git a/python/python-psi-impl/resources/messages/PyPsiBundle.properties b/python/python-psi-impl/resources/messages/PyPsiBundle.properties index 877fdb5dbd31..575fa786d14e 100644 --- a/python/python-psi-impl/resources/messages/PyPsiBundle.properties +++ b/python/python-psi-impl/resources/messages/PyPsiBundle.properties @@ -616,6 +616,7 @@ INSP.interpreter.python.has.reached.its.end.of.life.and.is.no.longer.supported.i INSP.interpreter.python.has.reached.its.end.life.and.is.no.longer.supported.in.python.plugin=Python {0} has reached its end-of-life date and it is no longer supported in the Python plugin INSP.interpreter.no.python.interpreter.configured.for.project=No Python interpreter configured for the project INSP.interpreter.no.python.interpreter.configured.for.module=No Python interpreter configured for the module +INSP.interpreter.checking.existing.environments=Checking existing environments # ReturnValueFromInitInspection INSP.NAME.init.return=__init__ method that returns a value diff --git a/python/src/com/jetbrains/python/inspections/PyAsyncFileInspectionRunner.kt b/python/src/com/jetbrains/python/inspections/PyAsyncFileInspectionRunner.kt new file mode 100644 index 000000000000..96758640a12c --- /dev/null +++ b/python/src/com/jetbrains/python/inspections/PyAsyncFileInspectionRunner.kt @@ -0,0 +1,66 @@ +// 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.inspections + +import com.github.benmanes.caffeine.cache.Caffeine +import com.github.benmanes.caffeine.cache.LoadingCache +import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.openapi.application.backgroundWriteAction +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.module.Module +import com.intellij.openapi.util.NlsContexts +import com.intellij.platform.ide.progress.withBackgroundProgress +import com.intellij.util.FileContentUtilCore +import com.jetbrains.python.psi.PyFile +import kotlinx.coroutines.* +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import kotlin.time.toJavaDuration + +/** + * This class is intended for async file inspections. It should have the same lifecycle as the inspection, the best + * way is to create the instance of this runner as a property of the inspection itself. + */ +internal class PyAsyncFileInspectionRunner( + @NlsContexts.ProgressTitle private val progressTitle: String, + cacheTtl: Duration = 20.seconds, + cacheLoader: suspend (Module) -> List, +) { + private val cache: LoadingCache>> = Caffeine.newBuilder() + .expireAfterWrite(cacheTtl.toJavaDuration()) + .weakKeys() + .build { module -> + val project = module.project + project.service().scope.async { + withBackgroundProgress(module.project, progressTitle) { + cacheLoader(module) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + fun runInspection(node: PyFile, module: Module): List? { + val cached = cache.getIfPresent(module) != null + val fixes = cache.get(module) + if (fixes.isCompleted) { + return fixes.getCompleted() + } + + if (!cached) { + fixes.invokeOnCompletion { + val project = module.project + project.service().scope.launch { + backgroundWriteAction { + DaemonCodeAnalyzer.getInstance(project).restart(node, "$progressTitle finished") + } + } + } + } + + return null + } +} + +@Service(Service.Level.PROJECT) +private class InspectionRunnerService(val scope: CoroutineScope) diff --git a/python/src/com/jetbrains/python/inspections/interpreter/PyInterpreterInspection.kt b/python/src/com/jetbrains/python/inspections/interpreter/PyInterpreterInspection.kt index 4481df2800ee..b88b85b9029c 100644 --- a/python/src/com/jetbrains/python/inspections/interpreter/PyInterpreterInspection.kt +++ b/python/src/com/jetbrains/python/inspections/interpreter/PyInterpreterInspection.kt @@ -13,7 +13,7 @@ import com.intellij.openapi.components.Service import com.intellij.openapi.components.service import com.intellij.openapi.module.Module import com.intellij.openapi.module.ModuleUtilCore -import com.intellij.openapi.progress.runBlockingMaybeCancellable +import com.intellij.openapi.project.DumbAware import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.roots.ProjectRootManager @@ -25,6 +25,7 @@ import com.intellij.python.sdkConfigurator.common.enableSDKAutoConfigurator import com.intellij.util.PathUtil import com.jetbrains.python.PyPsiBundle import com.jetbrains.python.PythonIdeLanguageCustomization +import com.jetbrains.python.inspections.PyAsyncFileInspectionRunner import com.jetbrains.python.inspections.PyInspection import com.jetbrains.python.inspections.PyInspectionExtension import com.jetbrains.python.inspections.PyInspectionVisitor @@ -40,19 +41,32 @@ import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension import com.jetbrains.python.sdk.legacy.PythonSdkUtil import com.jetbrains.python.ui.PyUiUtil import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.util.regex.Pattern private val NAME: Pattern = Pattern.compile("Python (?\\d\\.\\d+)\\s*(\\((?.+?)\\))?") -class PyInterpreterInspection : PyInspection() { +class PyInterpreterInspection : PyInspection(), DumbAware { + private val asyncFileInspectionRunner = PyAsyncFileInspectionRunner( + PyPsiBundle.message("INSP.interpreter.checking.existing.environments") + ) { module -> + buildList { + val sdkName = ProjectRootManager.getInstance(module.project).projectSdkName + getSuitableSdkFix(sdkName, module)?.let { add(it) } + add(ConfigureInterpreterFix()) + } + } + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor = - PyInterpreterInspectionVisitor(holder, PyInspectionVisitor.getContext(session)) + PyInterpreterInspectionVisitor(holder, PyInspectionVisitor.getContext(session), asyncFileInspectionRunner) } -class PyInterpreterInspectionVisitor( +private class PyInterpreterInspectionVisitor( holder: ProblemsHolder?, context: TypeEvalContext, + private val asyncFileInspectionRunner: PyAsyncFileInspectionRunner, ) : PyInspectionVisitor(holder, context) { override fun visitPyFile(node: PyFile) { @@ -74,35 +88,29 @@ class PyInterpreterInspectionVisitor( } private fun registerProblemWithCommonFixes(node: PyFile, @InspectionMessage message: String, module: Module?, pyCharm: Boolean) { - val fixes = buildList { - if (module != null && pyCharm) { - val sdkName = ProjectRootManager.getInstance(node.project).projectSdkName - getSuitableSdkFix(sdkName, module)?.let { add(it) } - add(ConfigureInterpreterFix()) - } - else { - add(InterpreterSettingsQuickFix(module)) + if (module != null && pyCharm) { + asyncFileInspectionRunner.runInspection(node, module)?.let { fixes -> + registerProblem(node, message, *fixes.toTypedArray()) } } - - registerProblem(node, message, *fixes.toTypedArray()) + else { + registerProblem(node, message, InterpreterSettingsQuickFix(module)) + } } } -private fun getSuitableSdkFix(name: String?, module: Module): LocalQuickFix? { +private suspend fun getSuitableSdkFix(name: String?, module: Module): LocalQuickFix? = withContext(Dispatchers.Default) { // this method is based on com.jetbrains.python.sdk.PySdkExtKt.suggestAssociatedSdkName // please keep it in sync with the mentioned method and com.jetbrains.python.PythonSdkConfigurator.configureSdk val existingSdks = getExistingSdks() val associatedSdk = mostPreferred(filterAssociatedSdks(module, existingSdks)) - if (associatedSdk != null) return UseExistingInterpreterFix(associatedSdk, module) + if (associatedSdk != null) return@withContext UseExistingInterpreterFix(associatedSdk, module) val context = UserDataHolderBase() - val createSdkInfo = runBlockingMaybeCancellable { - PyProjectSdkConfigurationExtension.findAllSortedForModule(module).firstOrNull() - } - if (createSdkInfo != null) return UseProvidedInterpreterFix(module, createSdkInfo) + val createSdkInfo = PyProjectSdkConfigurationExtension.findAllSortedForModule(module).firstOrNull() + if (createSdkInfo != null) return@withContext UseProvidedInterpreterFix(module, createSdkInfo) if (name != null) { val matcher = NAME.matcher(name) @@ -111,13 +119,13 @@ private fun getSuitableSdkFix(name: String?, module: Module): LocalQuickFix? { if (venvName != null) { val detectedAssociatedViaRootNameEnv = detectAssociatedViaRootNameEnv(venvName, module, existingSdks, context) if (detectedAssociatedViaRootNameEnv != null) { - return UseDetectedInterpreterFix(detectedAssociatedViaRootNameEnv, existingSdks, true, module) + return@withContext UseDetectedInterpreterFix(detectedAssociatedViaRootNameEnv, existingSdks, true, module) } } else { val detectedSystemWideSdk = detectSystemWideSdk(matcher.group("version"), module, existingSdks, context) if (detectedSystemWideSdk != null) { - return UseDetectedInterpreterFix(detectedSystemWideSdk, existingSdks, false, module) + return@withContext UseDetectedInterpreterFix(detectedSystemWideSdk, existingSdks, false, module) } } } @@ -125,23 +133,22 @@ private fun getSuitableSdkFix(name: String?, module: Module): LocalQuickFix? { if (PyCondaSdkCustomizer.instance.suggestSharedCondaEnvironments) { val sharedCondaEnv = mostPreferred(filterSharedCondaEnvs(module, existingSdks)) - if (sharedCondaEnv != null) return UseExistingInterpreterFix(sharedCondaEnv, module) + if (sharedCondaEnv != null) return@withContext UseExistingInterpreterFix(sharedCondaEnv, module) } // TODO: We should use SystemPythonService here as well, postponing as it's quite unlikely we get here (although we can) val systemWideSdk = mostPreferred(filterSystemWideSdks(existingSdks)) - if (systemWideSdk != null) return UseExistingInterpreterFix(systemWideSdk, module) + if (systemWideSdk != null) return@withContext UseExistingInterpreterFix(systemWideSdk, module) - val configurator = PyCondaSdkCustomizer.instance.fallbackConfigurator - if (configurator != null) { - val fallbackCreateSdkInfo = PyCondaSdkCustomizer.checkEnvironmentAndPrepareSdkCreatorBlocking(configurator, module) - if (fallbackCreateSdkInfo != null) return UseProvidedInterpreterFix(module, fallbackCreateSdkInfo) + val fallbackCreateSdkInfo = PyCondaSdkCustomizer.instance.fallbackConfigurator?.let { configurator -> + configurator.checkEnvironmentAndPrepareSdkCreator(module)?.let { CreateSdkInfoWithTool(it, configurator.toolId) } } + if (fallbackCreateSdkInfo != null) return@withContext UseProvidedInterpreterFix(module, fallbackCreateSdkInfo) val detectedSystemWideSdk = detectSystemWideSdks(module, existingSdks).firstOrNull() - if (detectedSystemWideSdk != null) return UseDetectedInterpreterFix(detectedSystemWideSdk, existingSdks, false, module) + if (detectedSystemWideSdk != null) return@withContext UseDetectedInterpreterFix(detectedSystemWideSdk, existingSdks, false, module) - return null + null } private fun getExistingSdks(): List { @@ -155,7 +162,7 @@ private fun detectAssociatedViaRootNameEnv( ): PyDetectedSdk? = findAssociatedViaRootNameEnv(associatedName, detectVirtualEnvs(module, existingSdks, context)) private fun detectSystemWideSdk(version: String, module: Module, existingSdks: List, context: UserDataHolderBase): PyDetectedSdk? { - val parsedVersion = LanguageLevel.fromPythonVersion(version)!! + val parsedVersion = LanguageLevel.fromPythonVersion(version) return if (parsedVersion.toString() == version) { detectSystemWideSdks(module, existingSdks, context).firstOrNull { it.guessedLanguageLevel == parsedVersion } @@ -176,7 +183,7 @@ private fun isFileIgnored(pyFile: PyFile): Boolean = PyInspectionExtension.EP_NAME.extensionList.any { it.ignoreInterpreterWarnings(pyFile) } -class ConfigureInterpreterFix : LocalQuickFix { +private class ConfigureInterpreterFix : LocalQuickFix { @IntentionFamilyName override fun getFamilyName(): String = PyPsiBundle.message("INSP.interpreter.configure.python.interpreter") diff --git a/python/src/com/jetbrains/python/sdk/PySdkExt.kt b/python/src/com/jetbrains/python/sdk/PySdkExt.kt index 5f7aa3cbfe23..22d94900d72f 100644 --- a/python/src/com/jetbrains/python/sdk/PySdkExt.kt +++ b/python/src/com/jetbrains/python/sdk/PySdkExt.kt @@ -38,7 +38,6 @@ import com.jetbrains.python.errorProcessing.emit import com.jetbrains.python.isCondaVirtualEnv import com.jetbrains.python.isVirtualEnv import com.jetbrains.python.packaging.ui.PyPackageManagementService -import com.jetbrains.python.packaging.utils.PyPackageCoroutine import com.jetbrains.python.psi.LanguageLevel import com.jetbrains.python.remote.PyRemoteSdkAdditionalData import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory @@ -315,17 +314,6 @@ fun PyDetectedSdk.setup(existingSdks: List): Sdk? { return SdkConfigurationUtil.setupSdk(existingSdks.toTypedArray(), homeDir, PythonSdkType.getInstance(), null, null) } -// For Java only -internal fun PyDetectedSdk.setupSdkLaunch( - module: Module, - existingSdks: List, - doAssociate: Boolean, -) { - PyPackageCoroutine.launch(project = module.project) { - setupSdk(module, existingSdks, doAssociate) - } -} - @Internal suspend fun PyDetectedSdk.setupSdk( module: Module, @@ -454,7 +442,7 @@ fun getInnerVirtualEnvRoot(sdk: Sdk): VirtualFile? { } internal suspend fun suggestAssociatedSdkName(sdkHome: String, associatedPath: String?): String? = withContext(Dispatchers.IO) { - // please don't forget to update com.jetbrains.python.inspections.interpreter.PyInterpreterInspection.Visitor#getSuitableSdkFix + // please don't forget to update com.jetbrains.python.inspections.interpreter.PyInterpreterInspection#getSuitableSdkFix // after changing this method val baseSdkName = PythonSdkType.suggestBaseSdkName(sdkHome) ?: return@withContext null