From bea035291c0c07037b30ca2b095abdc232b9766d Mon Sep 17 00:00:00 2001 From: "Ilya.Kazakevich" Date: Thu, 21 Mar 2024 01:35:26 +0000 Subject: [PATCH] [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 GitOrigin-RevId: 1e1a0121972119d36cd3bb53e876b3af86c09f8c --- .../python/newProject/FrameworkInstaller.kt | 53 +++++++++++++++ .../newProject/PythonProjectGenerator.java | 64 ++++++++++++------- 2 files changed, 95 insertions(+), 22 deletions(-) create mode 100644 python/src/com/jetbrains/python/newProject/FrameworkInstaller.kt diff --git a/python/src/com/jetbrains/python/newProject/FrameworkInstaller.kt b/python/src/com/jetbrains/python/newProject/FrameworkInstaller.kt new file mode 100644 index 000000000000..902c3ed97479 --- /dev/null +++ b/python/src/com/jetbrains/python/newProject/FrameworkInstaller.kt @@ -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().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") + } + } + } +} diff --git a/python/src/com/jetbrains/python/newProject/PythonProjectGenerator.java b/python/src/com/jetbrains/python/newProject/PythonProjectGenerator.java index 49a40e31e2ed..acc90bcfc03e 100644 --- a/python/src/com/jetbrains/python/newProject/PythonProjectGenerator.java +++ b/python/src/com/jetbrains/python/newProject/PythonProjectGenerator.java @@ -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 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 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 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 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(); 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 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 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 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