[python] Convert "install package in background" tools to futures.

fixup! Fix after review: use another `install` signature

[python] Convert "install package in background" tools to futures.

For things like PY-64402 we might need lots of packages to be installed in the background.

This change substitutes callback spaghetti with `Future` that could be used for `suspend` function using `await`.

It introduces "facade" named `FrameworkInstaller` to do that.

Unlike callbacks that weren't called in case of error,
`Future` completes with error, and thrown exception caught and logged by IJ platform.

`DsKnownPackages` enum is also added not to hardcode string literals


Merge-request: IJ-MR-129535
Merged-by: Ilya Kazakevich <ilya.kazakevich@jetbrains.com>

GitOrigin-RevId: 1e1a0121972119d36cd3bb53e876b3af86c09f8c
This commit is contained in:
Ilya.Kazakevich
2024-03-21 01:35:26 +00:00
committed by intellij-monorepo-bot
parent b39dd25583
commit bea035291c
2 changed files with 95 additions and 22 deletions
@@ -0,0 +1,53 @@
// 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.newProject
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.asContextElement
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.util.io.await
import com.jetbrains.python.newProject.FrameworkInstaller.Companion.installFrameworksInBackground
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.NonNls
/**
* `suspend`-friendly frontend for [PythonProjectGenerator]
* Installs frameworks in the background, shows errors as messages.
* Use [installFrameworksInBackground]
*/
@Service(Service.Level.PROJECT)
@ApiStatus.Internal
class FrameworkInstaller private constructor(private val projectScope: CoroutineScope) {
companion object {
private val log = Logger.getInstance(FrameworkInstaller::class.java)
/**
* Install python [frameworks] (as in `requirements.txt`) on [sdk] as background a task.
* `null` in [sdk] will lead to an error displayed in UI (for ar backward compatibility).
*
* This function is a wrapper for [PythonProjectGenerator.installFrameworkInBackground]
*/
fun installFrameworksInBackground(project: Project, sdk: Sdk?, vararg frameworks: @NonNls String) {
val taskName = "Python framework installer for ${frameworks.joinToString(",")}"
log.info("Started $taskName")
// Do not wait for a modal dialog to close
val context = Dispatchers.EDT + ModalityState.current().asContextElement() + CoroutineName(taskName)
project.service<FrameworkInstaller>().projectScope.launch(context) {
for (framework in frameworks) {
log.info("installing $framework")
PythonProjectGenerator.installFrameworkInBackground(project, framework, framework, sdk, true, null).await()
log.info("$framework installed")
}
log.info("Completed $taskName")
}
}
}
}
@@ -59,6 +59,8 @@ import java.awt.event.MouseListener;
import java.io.File;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.function.Consumer;
@@ -360,36 +362,40 @@ public abstract class PythonProjectGenerator<T extends PyNewProjectSettings> ext
* @param requirement name of requirement to install (i.e. "django")
* @param forceInstallFramework pass true if you are sure required framework is missing
* @param callback to be called after installation (or instead of is framework is installed) on AWT thread
* @return future to be used instead of callback.
*/
public static void installFrameworkIfNeeded(@NotNull final Project project,
@NotNull final String frameworkName,
@NotNull final String requirement,
@Nullable final Sdk sdk,
final boolean forceInstallFramework,
@Nullable final Runnable callback) {
installFrameworkIfNeeded(project, frameworkName, requirement, sdk, forceInstallFramework, false, callback);
public static @NotNull Future<Void> installFrameworkIfNeeded(@NotNull final Project project,
@NotNull final String frameworkName,
@NotNull final String requirement,
@Nullable final Sdk sdk,
final boolean forceInstallFramework,
@Nullable final Runnable callback) {
return installFrameworkIfNeeded(project, frameworkName, requirement, sdk, forceInstallFramework, false, callback);
}
public static void installFrameworkInBackground(@NotNull final Project project,
@NotNull final String frameworkName,
@NotNull final String requirement,
@Nullable final Sdk sdk,
final boolean forceInstallFramework,
@Nullable final Runnable callback) {
installFrameworkIfNeeded(project, frameworkName, requirement, sdk, forceInstallFramework, true, callback);
public static @NotNull Future<Void> installFrameworkInBackground(@NotNull final Project project,
@NotNull final String frameworkName,
@NotNull final String requirement,
@Nullable final Sdk sdk,
final boolean forceInstallFramework,
@Nullable final Runnable callback) {
return installFrameworkIfNeeded(project, frameworkName, requirement, sdk, forceInstallFramework, true, callback);
}
private static void installFrameworkIfNeeded(@NotNull final Project project,
@NotNull final String frameworkName,
@NotNull final String requirement,
@Nullable final Sdk sdk,
final boolean forceInstallFramework,
boolean asBackgroundTask,
@Nullable final Runnable callback) {
@NotNull
private static Future<Void> installFrameworkIfNeeded(@NotNull final Project project,
@NotNull final String frameworkName,
@NotNull final String requirement,
@Nullable final Sdk sdk,
final boolean forceInstallFramework,
boolean asBackgroundTask,
@Nullable final Runnable callback) {
var future = new CompletableFuture<Void>();
if (sdk == null) {
reportPackageInstallationFailure(frameworkName, null);
return;
future.completeExceptionally(new RuntimeException(("No SDK provided")));
return future;
}
// For remote SDK we are not sure if framework exists or not, so we'll check it anyway
@@ -403,8 +409,14 @@ public abstract class PythonProjectGenerator<T extends PyNewProjectSettings> ext
installPackages(frameworkName, forceInstallFramework, indicator, requirement, sdk);
}
@Override
public void onThrowable(@NotNull Throwable error) {
future.completeExceptionally(error);
}
@Override
public void onSuccess() {
future.complete(null);
// Installed / checked successfully, call callback on AWT
if (callback != null) {
callback.run();
@@ -420,8 +432,14 @@ public abstract class PythonProjectGenerator<T extends PyNewProjectSettings> ext
installPackages(frameworkName, forceInstallFramework, indicator, requirement, sdk);
}
@Override
public void onThrowable(@NotNull Throwable error) {
future.completeExceptionally(error);
}
@Override
public void onSuccess() {
future.complete(null);
// Installed / checked successfully, call callback on AWT
if (callback != null) {
callback.run();
@@ -431,12 +449,14 @@ public abstract class PythonProjectGenerator<T extends PyNewProjectSettings> ext
}
}
else {
future.complete(null);
// No need to install, but still need to call callback on AWT
if (callback != null) {
assert SwingUtilities.isEventDispatchThread();
callback.run();
}
}
return future;
}
@Nullable