[python] PY-84315: Use SDK configurators for inspection

There was a problem that we can detect uv environment as a virtualenv in
PyInterpreterInspection. The problem is that we don't use the knowledge
about the existing environment anyhow.

This change relies on SDK configurators to tell whether environment is
present or can be configured.

GitOrigin-RevId: 27f55c7d1b4712b0b8c3d39b1b8f7d8643680910
This commit is contained in:
Alexey Katsman
2025-10-22 13:10:20 +00:00
committed by intellij-monorepo-bot
parent 033d7cc704
commit 9140cb4e7b
5 changed files with 18 additions and 105 deletions
@@ -85,7 +85,7 @@ class PythonSdkConfigurator : DirectoryProjectConfigurator {
emptyList()
}
else {
PyProjectSdkConfigurationExtension.EP_NAME.extensionsIfPointIsRegistered.mapNotNull { it.checkEnvironmentAndPrepareSdkCreator(module) }.sorted()
PyProjectSdkConfigurationExtension.findAllSortedForModule(module)
}
}
@@ -91,12 +91,6 @@
</extensions>
<projectListeners>
<listener
class="com.jetbrains.python.inspections.PyInterpreterInspection$Visitor$CacheCleaner"
topic="com.intellij.platform.backend.workspace.WorkspaceModelChangeListener"/>
<listener
class="com.jetbrains.python.inspections.PyInterpreterInspection$Visitor$CacheCleaner"
topic="com.intellij.openapi.projectRoots.ProjectJdkTable$Listener"/>
<listener class="com.jetbrains.python.packaging.PyDependencyCollectorListener"
topic="com.jetbrains.python.packaging.common.PythonPackageManagementListener"/>
<listener class="com.jetbrains.python.statistics.PyPackageDaemonListener"
@@ -36,14 +36,12 @@ private class AutoconfigSelectSdkProvider() : EvoSelectSdkProvider {
text = PySdkUiBundle.message("evo.sdk.status.bar.popup.shortcuts.best.options"),
icon = AllIcons.General.Layout
) {
val createSdkInfos = PyProjectSdkConfigurationExtension.EP_NAME.extensionsIfPointIsRegistered.mapNotNull {
it.checkEnvironmentAndPrepareSdkCreator(evoModuleSdk.module)
}
val createSdkInfoWithTools = PyProjectSdkConfigurationExtension.findAllSortedForModule(evoModuleSdk.module)
val section = EvoTreeSection(
label = null,
elements = createSdkInfos.mapIndexed { idx, createSdkInfo ->
EvoTreeLeafElement(RunConfiguratorAction(createSdkInfo.intentionName, idx))
elements = createSdkInfoWithTools.mapIndexed { idx, createSdkInfoWithTool ->
EvoTreeLeafElement(RunConfiguratorAction(createSdkInfoWithTool.intentionName, idx))
}
)
@@ -27,6 +27,17 @@ interface PyProjectSdkConfigurationExtension {
fun findForModule(module: Module): CreateSdkInfo? = runBlockingMaybeCancellable {
EP_NAME.extensionsIfPointIsRegistered.firstNotNullOfOrNull { ext -> ext.checkEnvironmentAndPrepareSdkCreator(module) }
}
@JvmStatic
@RequiresBackgroundThread
/**
* We return all configurators in a sorted order. The order is determined by extensions order, but existing environments have a
* higher priority. That means we first have all existing envs, and only after SDK creators that extensions can manage.
*/
fun findAllSortedForModule(module: Module): List<CreateSdkInfo> = runBlockingMaybeCancellable {
EP_NAME.extensionsIfPointIsRegistered
.mapNotNull { e -> e.checkEnvironmentAndPrepareSdkCreator(module) }.sorted()
}
}
val toolInfo: PyToolUIInfo
@@ -1,8 +1,6 @@
// 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.inspections;
import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo;
import com.intellij.codeInspection.LocalInspectionToolSession;
import com.intellij.codeInspection.LocalQuickFix;
@@ -13,32 +11,23 @@ import com.intellij.codeInspection.util.IntentionFamilyName;
import com.intellij.codeInspection.util.IntentionName;
import com.intellij.ide.actions.ShowSettingsUtilImpl;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.options.ex.ConfigurableExtensionPointUtil;
import com.intellij.openapi.options.ex.ConfigurableVisitor;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.ProjectJdkTable;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.roots.ui.configuration.ProjectSettingsService;
import com.intellij.openapi.roots.ui.configuration.projectRoot.ProjectSdksModel;
import com.intellij.openapi.util.UserDataHolderBase;
import com.intellij.platform.backend.workspace.WorkspaceModelChangeListener;
import com.intellij.platform.workspace.jps.entities.ModuleEntity;
import com.intellij.platform.workspace.storage.EntityChange;
import com.intellij.platform.workspace.storage.VersionedStorageChange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.util.PathUtil;
import com.intellij.util.PlatformUtils;
import com.intellij.util.concurrency.AppExecutorUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.workspaceModel.ide.impl.legacyBridge.module.ModuleEntityUtils;
import com.jetbrains.python.PyPsiBundle;
import com.jetbrains.python.PythonIdeLanguageCustomization;
import com.jetbrains.python.psi.LanguageLevel;
@@ -61,13 +50,10 @@ import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import java.io.File;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -77,8 +63,6 @@ import static com.intellij.python.sdkConfigurator.common.PublicApiKt.detectSdkFo
public final class PyInterpreterInspection extends PyInspection {
private static final @NotNull Logger LOGGER = Logger.getInstance(PyInterpreterInspection.class);
private static final @NotNull Pattern NAME = Pattern.compile("Python (?<version>\\d\\.\\d+)\\s*(\\((?<name>.+?)\\))?");
@Override
@@ -89,21 +73,6 @@ public final class PyInterpreterInspection extends PyInspection {
}
public static class Visitor extends PyInspectionVisitor {
/** Invalidated by {@link CacheCleaner}. */
private static final AsyncLoadingCache<@NotNull Module, @NotNull List<PyDetectedSdk>> DETECTED_ASSOCIATED_ENVS_CACHE =
Caffeine.newBuilder().executor(AppExecutorUtil.getAppExecutorService())
// Even though various listeners invalidate the cache on many actions, it's unfeasible to track for venv/conda interpreters
// creation performed outside the IDE.
// 20 seconds timeout is taken at random.
.expireAfterWrite(Duration.ofSeconds(20))
.weakKeys()
.buildAsync(module -> {
final List<Sdk> existingSdks = getExistingSdks();
final UserDataHolderBase context = new UserDataHolderBase();
return PySdkExtKt.detectAssociatedEnvironments(module, existingSdks, context);
});
public Visitor(@Nullable ProblemsHolder holder,
@NotNull TypeEvalContext context) {
@@ -161,30 +130,9 @@ public final class PyInterpreterInspection extends PyInspection {
final UserDataHolderBase context = new UserDataHolderBase();
List<PyDetectedSdk> detectedAssociatedEnvs = Collections.emptyList();
while (true) {
try {
// Beware that this thread holds the read lock. Shouldn't wait too much.
detectedAssociatedEnvs = DETECTED_ASSOCIATED_ENVS_CACHE.get(module).get(10, TimeUnit.MILLISECONDS);
break;
}
catch (InterruptedException | TimeoutException ignored) {
ProgressManager.checkCanceled();
}
catch (Exception e) {
LOGGER.warn("Failed to get suitable sdk fix for name " + name + " and module " + module, e);
break;
}
}
final var detectedAssociatedSdk = ContainerUtil.getFirstItem(detectedAssociatedEnvs);
if (detectedAssociatedSdk != null) {
return new UseDetectedInterpreterFix(detectedAssociatedSdk, existingSdks, true, module);
}
final CreateSdkInfo createSdkInfo = PyProjectSdkConfigurationExtension.findForModule(module);
if (createSdkInfo != null) {
return new UseProvidedInterpreterFix(module, createSdkInfo);
final List<CreateSdkInfo> createSdkInfos = PyProjectSdkConfigurationExtension.findAllSortedForModule(module);
if (!createSdkInfos.isEmpty()) {
return new UseProvidedInterpreterFix(module, createSdkInfos.getFirst());
}
if (name != null) {
@@ -290,44 +238,6 @@ public final class PyInterpreterInspection extends PyInspection {
private static @Nullable String getEnvRootName(@Nullable File envRoot) {
return envRoot == null ? null : PathUtil.getFileName(envRoot.getPath());
}
private static class CacheCleaner implements WorkspaceModelChangeListener, ProjectJdkTable.Listener {
@Override
public void jdkAdded(@NotNull Sdk jdk) {
invalidate();
}
@Override
public void jdkRemoved(@NotNull Sdk jdk) {
invalidate();
}
@Override
public void jdkNameChanged(@NotNull Sdk jdk, @NotNull String previousName) {
invalidate();
}
/**
* Invalidates the cache for the modules that were changed in any way. Especially interesting are
* content roots changes and current Python interpreter changes.
*/
@Override
public void beforeChanged(@NotNull VersionedStorageChange event) {
for (EntityChange<ModuleEntity> change : event.getChanges(ModuleEntity.class)) {
ModuleEntity entity = change.getOldEntity();
if (entity != null) {
var module = ModuleEntityUtils.findModule(entity, event.getStorageBefore());
if (module != null) {
DETECTED_ASSOCIATED_ENVS_CACHE.synchronous().invalidate(module);
}
}
}
}
private static void invalidate() {
DETECTED_ASSOCIATED_ENVS_CACHE.synchronous().invalidateAll();
}
}
}
private static boolean isFileIgnored(@NotNull PyFile pyFile) {