mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-85634: Make PyInterpreterInspection async for PyCharm
PyInterpreterInspection uses SDK configurators to find existing environments and find the most suitable SDK. This operation takes time (especially with conda on Windows), and on top it's performed on every file change, making UI freeze all the time. This change makes inspection asynchronous by caching the result and triggerring inspection again to show changes on the UI. GitOrigin-RevId: 6a64101c5b4e5c36841f0d7c046c5fce1c5fd50b
This commit is contained in:
committed by
intellij-monorepo-bot
parent
3221788f65
commit
ab0b2158ed
@@ -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
|
||||
|
||||
@@ -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<LocalQuickFix>,
|
||||
) {
|
||||
private val cache: LoadingCache<Module, Deferred<List<LocalQuickFix>>> = Caffeine.newBuilder()
|
||||
.expireAfterWrite(cacheTtl.toJavaDuration())
|
||||
.weakKeys()
|
||||
.build { module ->
|
||||
val project = module.project
|
||||
project.service<InspectionRunnerService>().scope.async {
|
||||
withBackgroundProgress(module.project, progressTitle) {
|
||||
cacheLoader(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun runInspection(node: PyFile, module: Module): List<LocalQuickFix>? {
|
||||
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<InspectionRunnerService>().scope.launch {
|
||||
backgroundWriteAction {
|
||||
DaemonCodeAnalyzer.getInstance(project).restart(node, "$progressTitle finished")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@Service(Service.Level.PROJECT)
|
||||
private class InspectionRunnerService(val scope: CoroutineScope)
|
||||
+39
-32
@@ -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 (?<version>\\d\\.\\d+)\\s*(\\((?<name>.+?)\\))?")
|
||||
|
||||
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<Sdk> {
|
||||
@@ -155,7 +162,7 @@ private fun detectAssociatedViaRootNameEnv(
|
||||
): PyDetectedSdk? = findAssociatedViaRootNameEnv(associatedName, detectVirtualEnvs(module, existingSdks, context))
|
||||
|
||||
private fun detectSystemWideSdk(version: String, module: Module, existingSdks: List<Sdk>, 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")
|
||||
|
||||
|
||||
@@ -39,7 +39,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
|
||||
@@ -298,7 +297,8 @@ val Sdk.associatedModuleNioPath: Path?
|
||||
get() =
|
||||
try {
|
||||
associatedModulePath?.let { Path(it) }
|
||||
}catch (e: InvalidPathException) {
|
||||
}
|
||||
catch (e: InvalidPathException) {
|
||||
if (getUserData(SDK_ERROR_REPORTED) != true) {
|
||||
LOGGER.warn("Can't convert ${associatedModulePath} to path", e)
|
||||
putUserData(SDK_ERROR_REPORTED, true)
|
||||
@@ -325,17 +325,6 @@ fun PyDetectedSdk.setup(existingSdks: List<Sdk>): Sdk? {
|
||||
return SdkConfigurationUtil.setupSdk(existingSdks.toTypedArray(), homeDir, PythonSdkType.getInstance(), null, null)
|
||||
}
|
||||
|
||||
// For Java only
|
||||
internal fun PyDetectedSdk.setupSdkLaunch(
|
||||
module: Module,
|
||||
existingSdks: List<Sdk>,
|
||||
doAssociate: Boolean,
|
||||
) {
|
||||
PyPackageCoroutine.launch(project = module.project) {
|
||||
setupSdk(module, existingSdks, doAssociate)
|
||||
}
|
||||
}
|
||||
|
||||
@Internal
|
||||
suspend fun PyDetectedSdk.setupSdk(
|
||||
module: Module,
|
||||
@@ -464,7 +453,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
|
||||
|
||||
Reference in New Issue
Block a user