diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties index e3185a30d5d6..ee9313ea7d56 100644 --- a/python/pluginResources/messages/PyBundle.properties +++ b/python/pluginResources/messages/PyBundle.properties @@ -1049,6 +1049,7 @@ form.integrated.tools.analyze.python.code.in.docstrings=Analyze Python code in d form.integrated.tools.default.test.runner=Default test runner: form.integrated.tools.docstring.format=Docstring format: form.integrated.tools.package.requirements.file=Package requirements file for SDK: +form.integrated.tools.package.requirements.file.invalid.path=Invalid package requirements file path: {0} form.integrated.tools.path.to.pipenv.executable=Path to Pipenv executable: form.integrated.tools.render.external.documentation.for.stdlib=Render external documentation for stdlib form.integrated.tools.sphinx.working.directory=Sphinx working directory: diff --git a/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.java b/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.java index c48f096cd81c..5a238f9edce4 100644 --- a/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.java +++ b/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.java @@ -3,12 +3,10 @@ package com.jetbrains.python.configuration; import com.google.common.collect.Lists; import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer; -import com.intellij.execution.ExecutionException; import com.intellij.facet.impl.ui.FacetErrorPanel; import com.intellij.facet.ui.FacetConfigurationQuickFix; import com.intellij.facet.ui.FacetEditorValidator; import com.intellij.facet.ui.ValidationResult; -import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory; import com.intellij.openapi.fileTypes.PlainTextFileType; import com.intellij.openapi.module.Module; @@ -39,9 +37,8 @@ import com.jetbrains.python.PythonFileType; import com.jetbrains.python.ReSTService; import com.jetbrains.python.documentation.PyDocumentationSettings; import com.jetbrains.python.documentation.docstrings.DocStringFormat; -import com.jetbrains.python.packaging.PyPackageManagerUI; import com.jetbrains.python.packaging.PyPackageRequirementsSettings; -import com.jetbrains.python.packaging.PyRequirementsKt; +import com.jetbrains.python.packaging.management.ui.PythonPackageManagerUI; import com.jetbrains.python.packaging.requirementsTxt.PythonRequirementTxtSdkUtils; import com.jetbrains.python.sdk.PythonSdkAdditionalData; import com.jetbrains.python.sdk.legacy.PythonSdkUtil; @@ -63,6 +60,7 @@ import javax.swing.JPanel; import java.awt.BorderLayout; import java.awt.Insets; import java.lang.reflect.Method; +import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; @@ -314,7 +312,7 @@ public class PyIntegratedToolsConfigurable implements SearchableConfigurable { return path; } - private void setRequirementsPath(String requirementsPath) { + private void setRequirementsPath(String requirementsPath) throws ConfigurationException { if (myModule == null) { return; } @@ -326,8 +324,8 @@ public class PyIntegratedToolsConfigurable implements SearchableConfigurable { try { PythonRequirementTxtSdkUtils.saveRequirementsTxtPath(myModule.getProject(), sdk, Path.of(requirementsPath)); } - catch (Throwable t) { - Logger.getInstance(PyIntegratedToolsConfigurable.class).warn("Failed to save requirements path", t); + catch (InvalidPathException e) { + throw new ConfigurationException(PyBundle.message("form.integrated.tools.package.requirements.file.invalid.path", e.getMessage())); } } @@ -359,18 +357,8 @@ public class PyIntegratedToolsConfigurable implements SearchableConfigurable { return new FacetConfigurationQuickFix() { @Override public void run(JComponent place) { - final PyPackageManagerUI ui = new PyPackageManagerUI(myProject, sdk, new PyPackageManagerUI.Listener() { - @Override - public void started() { } - - @Override - public void finished(List exceptions) { - if (exceptions.isEmpty()) { - facetErrorPanel.getValidatorsManager().validate(); - } - } - }); - ui.install(Collections.singletonList(PyRequirementsKt.pyRequirement(name, null)), Collections.emptyList()); + PythonPackageManagerUI.forSdk(myProject, sdk).installPackagesWithModalProgressBlocking(name); + facetErrorPanel.getValidatorsManager().validate(); } }; } diff --git a/python/src/com/jetbrains/python/packaging/management/ui/PythonPackageManagerUI.kt b/python/src/com/jetbrains/python/packaging/management/ui/PythonPackageManagerUI.kt index 980f8fc28e3c..36a89c8f2657 100644 --- a/python/src/com/jetbrains/python/packaging/management/ui/PythonPackageManagerUI.kt +++ b/python/src/com/jetbrains/python/packaging/management/ui/PythonPackageManagerUI.kt @@ -3,15 +3,22 @@ package com.jetbrains.python.packaging.management.ui import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk +import com.intellij.platform.ide.progress.runWithModalProgressBlocking +import com.intellij.util.concurrency.annotations.RequiresBlockingContext +import com.intellij.util.concurrency.annotations.RequiresEdt import com.jetbrains.python.PyBundle import com.jetbrains.python.errorProcessing.ErrorSink import com.jetbrains.python.errorProcessing.PyResult +import com.jetbrains.python.errorProcessing.emit +import com.jetbrains.python.getOrNull +import com.jetbrains.python.onFailure import com.jetbrains.python.packaging.PyRequirement import com.jetbrains.python.packaging.common.PythonPackage import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification import com.jetbrains.python.packaging.management.PyWorkspaceMember import com.jetbrains.python.packaging.management.PythonPackageInstallRequest import com.jetbrains.python.packaging.management.PythonPackageManager +import com.jetbrains.python.packaging.management.findPackageSpecification import com.jetbrains.python.packaging.pyRequirement import com.jetbrains.python.statistics.PyPackagesUsageCollector import com.jetbrains.python.util.ShowingMessageErrorSync @@ -150,7 +157,37 @@ class PythonPackageManagerUI(val manager: PythonPackageManager, val sink: ErrorS operation() } + /** + * Installs packages by name using modal progress, blocking the calling EDT thread. + * + * Resolves package specifications from repository, then installs them while showing + * a modal progress dialog. Errors are reported via [sink]. + * + * Intended for use from modal dialogs (e.g., Settings) where background progress is not visible. + * + * @return list of all installed packages after installation, or null if the operation failed + */ + @RequiresEdt + @RequiresBlockingContext + fun installPackagesWithModalProgressBlocking(vararg packages: String): List? { + val specifications = runWithModalProgressBlocking(project, PyBundle.message("python.packaging.installing.packages")) { + packages.mapNotNull { + manager.findPackageSpecification(it) + } + } + + val installRequest = PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications(specifications) + val title = getProgressTitle(installRequest) + return runWithModalProgressBlocking(project, title) { + manager.installPackage(installRequest, emptyList()).onFailure { + sink.emit(it, project) + }.getOrNull() + } + } + companion object { + @JvmStatic + @JvmOverloads @ApiStatus.Internal fun forSdk(project: Project, sdk: Sdk, sink: ErrorSink = ShowingMessageErrorSync): PythonPackageManagerUI { val packageManager = PythonPackageManager.forSdk(project, sdk) diff --git a/python/src/com/jetbrains/python/sdk/PythonSdkUpdateProjectActivity.kt b/python/src/com/jetbrains/python/sdk/PythonSdkUpdateProjectActivity.kt index 8cc86c120fdf..26528bcd6075 100644 --- a/python/src/com/jetbrains/python/sdk/PythonSdkUpdateProjectActivity.kt +++ b/python/src/com/jetbrains/python/sdk/PythonSdkUpdateProjectActivity.kt @@ -14,6 +14,7 @@ import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.newvfs.RefreshQueue import com.jetbrains.python.packaging.common.PythonPackageManagementListener import com.jetbrains.python.packaging.management.PythonPackageManager +import com.jetbrains.python.packaging.utils.PyPackageCoroutine import com.jetbrains.python.sdk.skeleton.PySkeletonUtil.getSitePackagesDirectory import org.jetbrains.annotations.ApiStatus @@ -56,7 +57,9 @@ suspend fun refreshPaths(project: Project, sdk: Sdk) { RefreshQueue.getInstance().refresh(true, listOfNotNull(getSitePackagesDirectory(sdk), sdk.associatedModuleDir)) } - PythonSdkUpdater.scheduleUpdate(sdk, project, false) + PyPackageCoroutine.launch(project) { + PythonSdkUpdater.scheduleUpdate(sdk, project, false) + } } internal fun dropUpdaterInHeadless(): Boolean {