mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-04-21 05:51:25 +07:00
PY-78649: Improve error handling.
There were three problems: 1. There was `PyExecutionException` with an optional message which was intended to be displayed via dialog using a message as a title. It breaks an exception contract (code that isn't aware of this particular class should still be able to fetch info, and a message is usually blank). 2. Moreover, `ErrorSink` wasn't aware of it either and displayed it as a plain text. 3. `PyExecutionException` didn't distinguish between "process can't be started" and "process died with error code != 0". Those are different cases. This change: 1. Fixes `PyExecutionException` for 1 and 3. 2. Introduces API in `ErrorSink.kt` to display `PyExecutionException` GitOrigin-RevId: a8d835afb086b23c73ced15f243d2b27b59dcf82
This commit is contained in:
committed by
intellij-monorepo-bot
parent
4b22bc0b72
commit
ca2148932f
@@ -2525,8 +2525,7 @@ tab.title.preview.only=Preview
|
||||
tab.title.editor.and.preview=Editor and Preview
|
||||
tab.title.text=Text
|
||||
tooltip.hide=Hide
|
||||
dialog.message.command.could.not.complete={0} could not complete successfully. \
|
||||
Please see the command''s output for information about resolving this problem.
|
||||
|
||||
border.title.command.output=Command output
|
||||
notification.content.updated.plugin.to.version=Updated {0} plugin to version {1}
|
||||
notification.content.updated.plugins=Updated {0} plugins
|
||||
|
||||
@@ -3,13 +3,13 @@ package com.intellij.python.junit5Tests.framework
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.jetbrains.python.newProjectWizard.PyV3UIServices
|
||||
import com.jetbrains.python.util.ErrorSink
|
||||
import com.jetbrains.python.util.PyError
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.job
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.annotations.Nls
|
||||
import javax.swing.JComponent
|
||||
|
||||
/**
|
||||
@@ -17,12 +17,12 @@ import javax.swing.JComponent
|
||||
* Collect user errors from [errors], check [projectTreeExpanded] and [kotlinx.coroutines.Job.cancel] the [job] at the end
|
||||
*/
|
||||
class PyV3UIServicesMock(private val coroutineScope: CoroutineScope) : PyV3UIServices {
|
||||
private val _errors = MutableSharedFlow<@Nls String>()
|
||||
private val _errors = MutableSharedFlow<PyError>()
|
||||
|
||||
/**
|
||||
* [com.jetbrains.python.util.ErrorSink] errors
|
||||
*/
|
||||
val errors: Flow<@Nls String> = _errors
|
||||
val errors: Flow<PyError> = _errors
|
||||
|
||||
@Volatile
|
||||
var projectTreeExpanded: Boolean = false
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
python.execution.error={0}\nThe following command finished with error: {1}\nOutput: {2}\nError: {3}\Exit code: {4}
|
||||
python.execution.cant.start.error={0}\nThe following command could not be started: {1}
|
||||
27
python/openapi/src/com/jetbrains/python/PyCommunityBundle.kt
Normal file
27
python/openapi/src/com/jetbrains/python/PyCommunityBundle.kt
Normal file
@@ -0,0 +1,27 @@
|
||||
// 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
|
||||
|
||||
import com.intellij.DynamicBundle
|
||||
import org.jetbrains.annotations.Nls
|
||||
import org.jetbrains.annotations.NonNls
|
||||
import org.jetbrains.annotations.PropertyKey
|
||||
import java.util.function.Supplier
|
||||
|
||||
internal object PyCommunityBundle {
|
||||
private const val BUNDLE_FQN: @NonNls String = "messages.PyCommunityBundle"
|
||||
private val BUNDLE = DynamicBundle(PyCommunityBundle::class.java, BUNDLE_FQN)
|
||||
|
||||
fun message(
|
||||
key: @PropertyKey(resourceBundle = BUNDLE_FQN) String,
|
||||
vararg params: Any
|
||||
): @Nls String {
|
||||
return BUNDLE.getMessage(key, *params)
|
||||
}
|
||||
|
||||
fun messagePointer(
|
||||
key: @PropertyKey(resourceBundle = BUNDLE_FQN) String,
|
||||
vararg params: Any
|
||||
): Supplier<String> {
|
||||
return BUNDLE.getLazyMessage(key, *params)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// 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.execution
|
||||
|
||||
import com.intellij.execution.process.ProcessOutput
|
||||
import com.jetbrains.python.packaging.PyExecutionException
|
||||
|
||||
/**
|
||||
* Types of execution error for [PyExecutionFailure]
|
||||
*/
|
||||
sealed interface FailureReason {
|
||||
/**
|
||||
* A process failed to start, or the code that ought to start it decided not to run it.
|
||||
* That means the process hasn't been even created.
|
||||
*/
|
||||
data object CantStart : FailureReason
|
||||
|
||||
/**
|
||||
* A process started but failed with an error. See [output] for the result
|
||||
*/
|
||||
data class ExecutionFailed(val output: ProcessOutput) : FailureReason
|
||||
}
|
||||
|
||||
internal fun copyWith(ex: PyExecutionException, newCommand: String, newArgs: List<String>): PyExecutionException =
|
||||
when (val err = ex.failureReason) {
|
||||
FailureReason.CantStart -> {
|
||||
PyExecutionException(ex.additionalMessage, newCommand, newArgs, ex.fixes)
|
||||
}
|
||||
is FailureReason.ExecutionFailed -> {
|
||||
PyExecutionException(ex.additionalMessage, newCommand, newArgs, err.output, ex.fixes)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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.execution
|
||||
|
||||
import com.intellij.openapi.util.NlsContexts
|
||||
|
||||
/**
|
||||
* Some command can't be executed
|
||||
*/
|
||||
interface PyExecutionFailure {
|
||||
val command: String
|
||||
|
||||
val args: List<String>
|
||||
|
||||
/**
|
||||
* optional message to be displayed to the user
|
||||
*/
|
||||
val additionalMessage: @NlsContexts.DialogTitle String?
|
||||
|
||||
|
||||
val failureReason: FailureReason
|
||||
}
|
||||
@@ -1,63 +1,124 @@
|
||||
// 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.packaging;
|
||||
|
||||
import com.intellij.execution.ExecutionExceptionWithAttachments;
|
||||
import com.intellij.execution.ExecutionException;
|
||||
import com.intellij.execution.process.ProcessOutput;
|
||||
import com.intellij.openapi.util.NlsContexts.DialogMessage;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.jetbrains.python.PyCommunityBundle;
|
||||
import com.jetbrains.python.execution.FailureReason;
|
||||
import com.jetbrains.python.execution.FailureReasonKt;
|
||||
import com.jetbrains.python.execution.PyExecutionFailure;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class PyExecutionException extends ExecutionExceptionWithAttachments {
|
||||
/**
|
||||
* Process execution failed.
|
||||
* There are two cases, see {@link FailureReason}.
|
||||
* Each constructor represents one or another.
|
||||
*
|
||||
* @see FailureReason
|
||||
*/
|
||||
public final class PyExecutionException extends ExecutionException implements PyExecutionFailure {
|
||||
private final @NotNull String myCommand;
|
||||
private final @NotNull List<String> myArgs;
|
||||
private final int myExitCode;
|
||||
private final @NotNull List<? extends PyExecutionFix> myFixes;
|
||||
private final @DialogMessage @Nullable String myAdditionalMessage;
|
||||
private final @NotNull FailureReason myError;
|
||||
|
||||
public PyExecutionException(@DialogMessage @NotNull String message, @NotNull String command, @NotNull List<String> args) {
|
||||
this(message, command, args, "", "", 0, Collections.emptyList());
|
||||
/**
|
||||
* A process failed to start, {@link FailureReason.CantStart}
|
||||
*
|
||||
* @param additionalMessage a process start reason for a user
|
||||
*/
|
||||
public PyExecutionException(@DialogMessage @Nullable String additionalMessage,
|
||||
@NotNull String command,
|
||||
@NotNull List<String> args) {
|
||||
this(additionalMessage, command, args, Collections.emptyList());
|
||||
}
|
||||
|
||||
public PyExecutionException(@DialogMessage @NotNull String message,
|
||||
/**
|
||||
* A process failed to start, {@link FailureReason.CantStart}
|
||||
*
|
||||
* @param additionalMessage a process start reason for a user
|
||||
*/
|
||||
public PyExecutionException(@DialogMessage @Nullable String additionalMessage,
|
||||
@NotNull String command,
|
||||
@NotNull List<String> args,
|
||||
@NotNull List<? extends PyExecutionFix> fixes) {
|
||||
super(PyCommunityBundle.INSTANCE.message("python.execution.cant.start.error",
|
||||
additionalMessage != null ? additionalMessage : "",
|
||||
command + " " + StringUtil.join(args, " ")));
|
||||
myAdditionalMessage = additionalMessage;
|
||||
myCommand = command;
|
||||
myArgs = args;
|
||||
myFixes = fixes;
|
||||
myError = FailureReason.CantStart.INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* A process started, but failed {@link FailureReason.ExecutionFailed}
|
||||
*
|
||||
* @param additionalMessage a process start reason for a user
|
||||
* @param output execution output
|
||||
*/
|
||||
public PyExecutionException(@DialogMessage @Nullable String additionalMessage,
|
||||
@NotNull String command,
|
||||
@NotNull List<String> args,
|
||||
@NotNull ProcessOutput output) {
|
||||
this(message, command, args, output.getStdout(), output.getStderr(), output.getExitCode(), Collections.emptyList());
|
||||
this(additionalMessage, command, args, output, Collections.emptyList());
|
||||
}
|
||||
|
||||
public PyExecutionException(@DialogMessage @NotNull String message, @NotNull String command, @NotNull List<String> args,
|
||||
@NotNull String stdout, @NotNull String stderr, int exitCode,
|
||||
/**
|
||||
* A process started, but failed {@link FailureReason.ExecutionFailed}
|
||||
*
|
||||
* @param additionalMessage a process start reason for a user
|
||||
* @param output execution output
|
||||
*/
|
||||
public PyExecutionException(@DialogMessage @Nullable String additionalMessage,
|
||||
@NotNull String command,
|
||||
@NotNull List<String> args,
|
||||
@NotNull ProcessOutput output,
|
||||
@NotNull List<? extends PyExecutionFix> fixes) {
|
||||
super(message, stdout, stderr);
|
||||
super(PyCommunityBundle.INSTANCE.message("python.execution.error",
|
||||
additionalMessage != null ? additionalMessage : "",
|
||||
command + " " + StringUtil.join(args, " "),
|
||||
output.getStdout(),
|
||||
output.getStderr(),
|
||||
output.getExitCode()
|
||||
));
|
||||
myAdditionalMessage = additionalMessage;
|
||||
myCommand = command;
|
||||
myArgs = args;
|
||||
myExitCode = exitCode;
|
||||
myFixes = fixes;
|
||||
myError = new FailureReason.ExecutionFailed(output);
|
||||
}
|
||||
|
||||
/**
|
||||
* A process started, but failed {@link FailureReason.ExecutionFailed}
|
||||
*
|
||||
* @param additionalMessage a process start reason for a user
|
||||
*/
|
||||
public PyExecutionException(@DialogMessage @Nullable String additionalMessage,
|
||||
@NotNull String command,
|
||||
@NotNull List<String> args,
|
||||
@NotNull String stdout,
|
||||
@NotNull String stderr,
|
||||
int exitCode,
|
||||
@NotNull List<? extends PyExecutionFix> fixes) {
|
||||
this(additionalMessage, command, args, new ProcessOutput(stdout, stderr, exitCode, false, false), fixes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder b = new StringBuilder();
|
||||
b.append("The following command was executed:\n\n");
|
||||
final String command = getCommand() + " " + StringUtil.join(getArgs(), " ");
|
||||
b.append(command);
|
||||
b.append("\n\n");
|
||||
b.append("The exit code: ").append(myExitCode).append("\n");
|
||||
b.append("The error output of the command:\n\n");
|
||||
b.append(getStdout());
|
||||
b.append("\n");
|
||||
b.append(getStderr());
|
||||
b.append("\n");
|
||||
b.append(getMessage());
|
||||
return b.toString();
|
||||
}
|
||||
|
||||
public @NotNull String getCommand() {
|
||||
return myCommand;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull List<String> getArgs() {
|
||||
return myArgs;
|
||||
}
|
||||
@@ -66,7 +127,30 @@ public class PyExecutionException extends ExecutionExceptionWithAttachments {
|
||||
return myFixes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getAdditionalMessage() {
|
||||
return myAdditionalMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull FailureReason getFailureReason() {
|
||||
return myError;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use {@link #getFailureReason()} and match it as when process failed to start there is no exit code
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
public int getExitCode() {
|
||||
return myExitCode;
|
||||
if (getFailureReason() instanceof FailureReason.ExecutionFailed executionFailed) {
|
||||
return executionFailed.getOutput().getExitCode();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
@NotNull
|
||||
public PyExecutionException copyWith(@NotNull String newCommand, @NotNull List<@NotNull String> newArgs) {
|
||||
return FailureReasonKt.copyWith(this, newCommand, newArgs);
|
||||
}
|
||||
}
|
||||
@@ -1593,3 +1593,5 @@ python.survey.user.job.dialog.blocks.checkbox.web=Web development
|
||||
python.survey.user.job.dialog.blocks.checkbox.scripts=Writing automation scripts / parsers / tests / system administration
|
||||
python.survey.user.job.dialog.blocks.checkbox.other=Other
|
||||
validation.invalid.name=Invalid name
|
||||
dialog.message.command.could.not.complete=Command could not be completed successfully. \
|
||||
Please see the command's output for information about resolving this problem.
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
// 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
|
||||
|
||||
import com.intellij.ide.IdeBundle
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.ui.DialogWrapper
|
||||
import com.intellij.openapi.ui.Messages
|
||||
import com.intellij.ui.IdeBorderFactory
|
||||
import com.intellij.ui.JBColor
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
import com.intellij.util.ui.FormBuilder
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import com.intellij.util.ui.components.BorderLayoutPanel
|
||||
import com.jetbrains.python.execution.FailureReason
|
||||
import com.jetbrains.python.execution.PyExecutionFailure
|
||||
import java.awt.Dimension
|
||||
import java.awt.Font
|
||||
import javax.swing.*
|
||||
import javax.swing.text.StyleConstants
|
||||
|
||||
/**
|
||||
* @throws IllegalStateException if [project] is not `null` and it is disposed
|
||||
*/
|
||||
fun showProcessExecutionErrorDialog(
|
||||
project: Project?,
|
||||
exception: PyExecutionFailure,
|
||||
) {
|
||||
check(project == null || !project.isDisposed)
|
||||
|
||||
val errorMessageText = PyBundle.message("dialog.message.command.could.not.complete")
|
||||
// HTML format for text in `JBLabel` enables text wrapping
|
||||
val errorMessageLabel = JBLabel(UIUtil.toHtml(errorMessageText), Messages.getErrorIcon(), SwingConstants.LEFT)
|
||||
|
||||
val commandOutputTextPane = JTextPane().apply {
|
||||
val command = (listOf(exception.command) + exception.args).joinToString(" ")
|
||||
when (val err = exception.failureReason) {
|
||||
FailureReason.CantStart -> {
|
||||
appendProcessOutput(command, exception.toString(), "", null)
|
||||
|
||||
}
|
||||
is FailureReason.ExecutionFailed -> {
|
||||
val output = err.output
|
||||
appendProcessOutput(command, output.stdout, output.stderr, output.exitCode)
|
||||
}
|
||||
}
|
||||
|
||||
background = JBColor.WHITE
|
||||
isEditable = false
|
||||
}
|
||||
|
||||
val commandOutputPanel = BorderLayoutPanel().apply {
|
||||
border = IdeBorderFactory.createTitledBorder(IdeBundle.message("border.title.command.output"), false)
|
||||
|
||||
addToCenter(
|
||||
JBScrollPane(commandOutputTextPane, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER))
|
||||
}
|
||||
|
||||
val formBuilder = FormBuilder()
|
||||
.addComponent(errorMessageLabel)
|
||||
.addComponentFillVertically(commandOutputPanel, UIUtil.DEFAULT_VGAP)
|
||||
|
||||
object : DialogWrapper(project) {
|
||||
init {
|
||||
init()
|
||||
title = exception.additionalMessage ?: errorMessageText
|
||||
}
|
||||
|
||||
override fun createActions(): Array<Action> = arrayOf(okAction)
|
||||
|
||||
override fun createCenterPanel(): JComponent = formBuilder.panel.apply {
|
||||
preferredSize = Dimension(600, 300)
|
||||
}
|
||||
}.showAndGet()
|
||||
}
|
||||
|
||||
private fun JTextPane.appendProcessOutput(command: String, stdout: String, stderr: String, exitCode: Int?) {
|
||||
val stdoutStyle = addStyle(null, null)
|
||||
StyleConstants.setFontFamily(stdoutStyle, Font.MONOSPACED)
|
||||
|
||||
val stderrStyle = addStyle(null, stdoutStyle)
|
||||
StyleConstants.setForeground(stderrStyle, JBColor.RED)
|
||||
|
||||
document.apply {
|
||||
insertString(0, command + "\n", stdoutStyle)
|
||||
arrayOf(stdout to stdoutStyle, stderr to stderrStyle).forEach { (std, style) ->
|
||||
if (std.isNotEmpty()) insertString(length, std + "\n", style)
|
||||
}
|
||||
if (exitCode != null) {
|
||||
insertString(length, "Process finished with exit code $exitCode", stdoutStyle)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,8 +20,11 @@ import com.jetbrains.python.newProjectWizard.collector.PythonNewProjectWizardCol
|
||||
import com.jetbrains.python.newProjectWizard.impl.PyV3GeneratorPeer
|
||||
import com.jetbrains.python.newProjectWizard.impl.PyV3UIServicesProd
|
||||
import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathFlows.Companion.validatePath
|
||||
import com.jetbrains.python.packaging.PyExecutionException
|
||||
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode
|
||||
import com.jetbrains.python.statistics.version
|
||||
import com.jetbrains.python.util.PyError
|
||||
import com.jetbrains.python.util.emit
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -65,7 +68,8 @@ abstract class PyV3ProjectBaseGenerator<TYPE_SPECIFIC_SETTINGS : PyV3ProjectType
|
||||
coroutineScope.launch {
|
||||
val (sdk, interpreterStatistics) = settings.generateAndGetSdk(module, baseDir).getOrElse {
|
||||
withContext(Dispatchers.EDT) {
|
||||
uiServices.errorSink.emit(it.localizedMessage) // Show error generation to user
|
||||
// TODO: Migrate to python Result using PyError as exception not to make this dynamic check
|
||||
uiServices.errorSink.emit(if (it is PyExecutionException) PyError.ExecException(it) else PyError.Message(it.localizedMessage))
|
||||
}
|
||||
return@launch // Since we failed to generate project, we do not need to go any further
|
||||
}
|
||||
@@ -86,7 +90,10 @@ abstract class PyV3ProjectBaseGenerator<TYPE_SPECIFIC_SETTINGS : PyV3ProjectType
|
||||
// Either base settings (which create venv) might generate some or type specific settings (like Django) may.
|
||||
// So we expand it right after SDK generation, but if there are no files yet, we do it again after project generation
|
||||
uiServices.expandProjectTreeView(project)
|
||||
typeSpecificSettings.generateProject(module, baseDir, sdk).onFailure { uiServices.errorSink.emit(it.localizedMessage) }
|
||||
typeSpecificSettings.generateProject(module, baseDir, sdk).onFailure {
|
||||
// TODO: Migrate to python Result using PyError as exception not to make this dynamic check
|
||||
uiServices.errorSink.emit(if (it is PyExecutionException) PyError.ExecException(it) else PyError.Message(it.localizedMessage))
|
||||
}
|
||||
uiServices.expandProjectTreeView(project)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,8 +104,7 @@ public class PyPackageManagerImpl extends PyPackageManagerImplBase {
|
||||
for (PyRequirement req : requirements) {
|
||||
simplifiedArgs.addAll(req.getInstallOptions());
|
||||
}
|
||||
throw new PyExecutionException(e.getMessage(), "pip", makeSafeToDisplayCommand(simplifiedArgs),
|
||||
e.getStdout(), e.getStderr(), e.getExitCode(), e.getFixes());
|
||||
throw e.copyWith("pip", makeSafeToDisplayCommand(simplifiedArgs));
|
||||
}
|
||||
finally {
|
||||
LOG.debug("Packages cache is about to be refreshed because these requirements were installed: " + requirements);
|
||||
@@ -131,7 +130,7 @@ public class PyPackageManagerImpl extends PyPackageManagerImplBase {
|
||||
getHelperResult(args, !canModify, true);
|
||||
}
|
||||
catch (PyExecutionException e) {
|
||||
throw new PyExecutionException(e.getMessage(), "pip", args, e.getStdout(), e.getStderr(), e.getExitCode(), e.getFixes());
|
||||
throw e.copyWith("pip", args);
|
||||
}
|
||||
finally {
|
||||
LOG.debug("Packages cache is about to be refreshed because these packages were uninstalled: " + packages);
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.intellij.util.net.HttpConfigurable;
|
||||
import com.jetbrains.python.PyPsiPackageUtil;
|
||||
import com.jetbrains.python.PySdkBundle;
|
||||
import com.jetbrains.python.PythonHelpersLocator;
|
||||
import com.jetbrains.python.execution.FailureReason;
|
||||
import com.jetbrains.python.packaging.repository.PyPackageRepositoryUtil;
|
||||
import com.jetbrains.python.psi.LanguageLevel;
|
||||
import com.jetbrains.python.sdk.PyDetectedSdk;
|
||||
@@ -124,8 +125,12 @@ public abstract class PyPackageManagerImplBase extends PyPackageManager {
|
||||
return setuptoolsPackage != null ? setuptoolsPackage : PyPsiPackageUtil.findPackage(packages, PyPackageUtil.DISTRIBUTE);
|
||||
}
|
||||
catch (PyExecutionException e) {
|
||||
if (e.getExitCode() == ERROR_NO_SETUPTOOLS) {
|
||||
return null;
|
||||
var error = e.getFailureReason();
|
||||
if (error instanceof FailureReason.ExecutionFailed executionFailed) {
|
||||
int exitCode = executionFailed.getOutput().getExitCode();
|
||||
if (exitCode == ERROR_NO_SETUPTOOLS) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -113,8 +113,7 @@ public class PyTargetEnvironmentPackageManager extends PyPackageManagerImplBase
|
||||
for (PyRequirement req : requirements) {
|
||||
simplifiedArgs.addAll(req.getInstallOptions());
|
||||
}
|
||||
throw new PyExecutionException(e.getMessage(), "pip", makeSafeToDisplayCommand(simplifiedArgs),
|
||||
e.getStdout(), e.getStderr(), e.getExitCode(), e.getFixes());
|
||||
throw e.copyWith("pip", makeSafeToDisplayCommand(simplifiedArgs));
|
||||
}
|
||||
finally {
|
||||
LOG.debug("Packages cache is about to be refreshed because these requirements were installed: " + requirements);
|
||||
@@ -159,7 +158,7 @@ public class PyTargetEnvironmentPackageManager extends PyPackageManagerImplBase
|
||||
getPythonProcessResult(pythonExecution, !canModify, true, targetEnvironmentRequest);
|
||||
}
|
||||
catch (PyExecutionException e) {
|
||||
throw new PyExecutionException(e.getMessage(), "pip", args, e.getStdout(), e.getStderr(), e.getExitCode(), e.getFixes());
|
||||
throw e.copyWith("pip", args);
|
||||
}
|
||||
finally {
|
||||
LOG.debug("Packages cache is about to be refreshed because these packages were uninstalled: " + packages);
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.jetbrains.python.packaging.ui;
|
||||
|
||||
import com.intellij.execution.ExecutionException;
|
||||
import com.intellij.execution.RunCanceledByUserException;
|
||||
import com.intellij.execution.process.ProcessOutput;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.projectRoots.Sdk;
|
||||
import com.intellij.openapi.util.NlsContexts;
|
||||
@@ -19,6 +20,7 @@ import com.intellij.webcore.packaging.PackageManagementServiceEx;
|
||||
import com.intellij.webcore.packaging.RepoPackage;
|
||||
import com.jetbrains.python.PyBundle;
|
||||
import com.jetbrains.python.PySdkBundle;
|
||||
import com.jetbrains.python.execution.FailureReason;
|
||||
import com.jetbrains.python.packaging.*;
|
||||
import com.jetbrains.python.packaging.PyPIPackageUtil.PackageDetails;
|
||||
import com.jetbrains.python.packaging.requirement.PyRequirementRelation;
|
||||
@@ -326,22 +328,25 @@ public class PyPackageManagementService extends PackageManagementServiceEx {
|
||||
private static @NotNull PyPackageInstallationErrorDescription createDescription(@NotNull ExecutionException e,
|
||||
@Nullable Sdk sdk,
|
||||
@Nullable String packageName) {
|
||||
if (e instanceof PyExecutionException ee) {
|
||||
if (e instanceof PyExecutionException pyExecEx && pyExecEx.getFailureReason() instanceof FailureReason.ExecutionFailed execFailed) {
|
||||
var ee = execFailed.getOutput();
|
||||
final String stdout = ee.getStdout();
|
||||
final String stdoutCause = findErrorCause(stdout);
|
||||
final String stderrCause = findErrorCause(ee.getStderr());
|
||||
final String cause = stdoutCause != null ? stdoutCause : stderrCause;
|
||||
final String message = cause != null ? cause : ee.getMessage();
|
||||
final String command = ee.getCommand() + " " + StringUtil.join(ee.getArgs(), " ");
|
||||
final String message = cause != null ? cause : pyExecEx.getMessage();
|
||||
final String command = pyExecEx.getCommand() + " " + StringUtil.join(pyExecEx.getArgs(), " ");
|
||||
return new PyPackageInstallationErrorDescription(message, command, stdout.isEmpty() ? ee.getStderr() : stdout + "\n" + ee.getStderr(),
|
||||
findErrorSolution(ee, cause, sdk), packageName, sdk);
|
||||
findErrorSolution(pyExecEx, cause, sdk), packageName, sdk);
|
||||
}
|
||||
else {
|
||||
return new PyPackageInstallationErrorDescription(e.getMessage(), null, null, null, packageName, sdk);
|
||||
}
|
||||
}
|
||||
|
||||
private static @Nullable @DetailedDescription String findErrorSolution(@NotNull PyExecutionException e, @Nullable String cause, @Nullable Sdk sdk) {
|
||||
private static @Nullable @DetailedDescription String findErrorSolution(@NotNull PyExecutionException e,
|
||||
@Nullable String cause,
|
||||
@Nullable Sdk sdk) {
|
||||
if (cause != null) {
|
||||
if (StringUtil.containsIgnoreCase(cause, "SyntaxError")) {
|
||||
final LanguageLevel languageLevel = PythonSdkType.getLanguageLevelForSdk(sdk);
|
||||
@@ -349,8 +354,10 @@ public class PyPackageManagementService extends PackageManagementServiceEx {
|
||||
}
|
||||
}
|
||||
|
||||
if (SystemInfo.isLinux && (containsInOutput(e, "pyconfig.h") || containsInOutput(e, "Python.h"))) {
|
||||
return PySdkBundle.message("python.sdk.check.python.development.packages.installed");
|
||||
if (e.getFailureReason() instanceof FailureReason.ExecutionFailed executionFailed) {
|
||||
if (SystemInfo.isLinux && (containsInOutput(executionFailed.getOutput(), "pyconfig.h") || containsInOutput(executionFailed.getOutput(), "Python.h"))) {
|
||||
return PySdkBundle.message("python.sdk.check.python.development.packages.installed");
|
||||
}
|
||||
}
|
||||
|
||||
if ("pip".equals(e.getCommand()) && sdk != null) {
|
||||
@@ -360,7 +367,7 @@ public class PyPackageManagementService extends PackageManagementServiceEx {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean containsInOutput(@NotNull PyExecutionException e, @NotNull String text) {
|
||||
private static boolean containsInOutput(@NotNull ProcessOutput e, @NotNull String text) {
|
||||
return StringUtil.containsIgnoreCase(e.getStdout(), text) || StringUtil.containsIgnoreCase(e.getStderr(), text);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,11 +32,9 @@ import com.jetbrains.python.sdk.add.v1.PyAddExistingCondaEnvPanel
|
||||
import com.jetbrains.python.sdk.add.v1.PyAddExistingVirtualEnvPanel
|
||||
import com.jetbrains.python.sdk.add.v1.PyAddNewCondaEnvPanel
|
||||
import com.jetbrains.python.sdk.add.v1.PyAddNewVirtualEnvPanel
|
||||
import com.jetbrains.python.sdk.add.PyAddSdkDialogFlowAction
|
||||
import com.jetbrains.python.sdk.add.PyAddSdkPanel
|
||||
import com.jetbrains.python.sdk.add.v1.PyAddSystemWideInterpreterPanel
|
||||
import com.jetbrains.python.sdk.add.v1.doCreateSouthPanel
|
||||
import com.jetbrains.python.sdk.add.v1.showProcessExecutionErrorDialog
|
||||
import com.jetbrains.python.showProcessExecutionErrorDialog
|
||||
import com.jetbrains.python.sdk.add.v1.swipe
|
||||
import com.jetbrains.python.sdk.conda.PyCondaSdkCustomizer
|
||||
import com.jetbrains.python.sdk.detectVirtualEnvs
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.jetbrains.python.sdk.conda.PyCondaSdkCustomizer
|
||||
import com.jetbrains.python.sdk.pipenv.ui.PyAddPipEnvPanel
|
||||
import com.jetbrains.python.sdk.poetry.ui.createPoetryPanel
|
||||
import com.jetbrains.python.sdk.sdkSeemsValid
|
||||
import com.jetbrains.python.showProcessExecutionErrorDialog
|
||||
import com.jetbrains.python.target.PythonLanguageRuntimeConfiguration
|
||||
import java.awt.CardLayout
|
||||
import java.awt.Component
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.add.v1
|
||||
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.SystemInfoRt
|
||||
import com.intellij.ui.JBCardLayout
|
||||
import com.intellij.ui.components.panels.NonOpaquePanel
|
||||
import com.intellij.ui.messages.showProcessExecutionErrorDialog
|
||||
import com.intellij.util.ui.GridBag
|
||||
import com.intellij.util.ui.JBInsets
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import com.jetbrains.python.packaging.PyExecutionException
|
||||
import java.awt.*
|
||||
import javax.swing.BorderFactory
|
||||
import javax.swing.Box
|
||||
@@ -65,6 +62,3 @@ internal fun show(panel: JPanel, stepContent: Component) {
|
||||
panel.add(stepContentName, stepContent)
|
||||
(panel.layout as CardLayout).show(panel, stepContentName)
|
||||
}
|
||||
|
||||
fun showProcessExecutionErrorDialog(project: Project?, e: PyExecutionException) =
|
||||
showProcessExecutionErrorDialog(project, e.localizedMessage.orEmpty(), e.command, e.stdout, e.stderr, e.exitCode)
|
||||
@@ -3,11 +3,14 @@ package com.jetbrains.python.sdk.add.v2
|
||||
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.util.io.toNioPathOrNull
|
||||
import com.jetbrains.python.packaging.PyExecutionException
|
||||
import com.jetbrains.python.sdk.ModuleOrProject
|
||||
import com.jetbrains.python.sdk.VirtualEnvReader
|
||||
import com.jetbrains.python.sdk.rootManager
|
||||
import com.jetbrains.python.sdk.service.PySdkService.Companion.pySdkService
|
||||
import com.jetbrains.python.util.ErrorSink
|
||||
import com.jetbrains.python.util.PyError
|
||||
import com.jetbrains.python.util.emit
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
@@ -35,7 +38,8 @@ class PythonAddLocalInterpreterPresenter(val moduleOrProject: ModuleOrProject, v
|
||||
|
||||
suspend fun okClicked(addEnvironment: PythonAddEnvironment) {
|
||||
val sdk = addEnvironment.getOrCreateSdk(moduleOrProject).getOrElse {
|
||||
errorSink.emit(it.localizedMessage.ifBlank { it.toString() })
|
||||
// TODO: Migrate to python Result with PyError as error, not to check type dynamically
|
||||
errorSink.emit(if (it is PyExecutionException) PyError.ExecException(it) else PyError.Message(it.localizedMessage))
|
||||
return
|
||||
}
|
||||
moduleOrProject.project.pySdkService.persistSdk(sdk)
|
||||
|
||||
@@ -24,6 +24,7 @@ import com.jetbrains.python.sdk.pipenv.getPipEnvExecutable
|
||||
import com.jetbrains.python.sdk.poetry.getPoetryExecutable
|
||||
import com.jetbrains.python.sdk.uv.impl.getUvExecutable
|
||||
import com.jetbrains.python.util.ErrorSink
|
||||
import com.jetbrains.python.util.emit
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
@@ -9,6 +9,9 @@ import com.intellij.openapi.diagnostic.thisLogger
|
||||
import com.intellij.openapi.ui.Messages
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.execution.PyExecutionFailure
|
||||
import com.jetbrains.python.packaging.PyExecutionException
|
||||
import com.jetbrains.python.showProcessExecutionErrorDialog
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -17,7 +20,7 @@ import kotlinx.coroutines.withContext
|
||||
* [FlowCollector.emit] user-readable errors here.
|
||||
*
|
||||
* This class should be used by the topmost classes, tightly coupled to the UI.
|
||||
* For the most business-logic and backend functions please return [Result] or error.
|
||||
* For the most business-logic and backend functions please return [com.jetbrains.python.Result] or error.
|
||||
*
|
||||
* Please do not report *all* exceptions here: This is *not* the class for NPEs and AOOBs:
|
||||
* do not pass exceptions caught by `catch(e: Exception)` or `runCatching`: only report exceptions user interested in.
|
||||
@@ -28,7 +31,7 @@ import kotlinx.coroutines.withContext
|
||||
*
|
||||
* Example:
|
||||
* ```kotlin
|
||||
* suspend fun someLogic(): Result<@NlsSafe String> = withContext(Dispatchers.IO) {
|
||||
* suspend fun someLogic(): Result<@NlsSafe String, IOException> = withContext(Dispatchers.IO) {
|
||||
* try {
|
||||
* Result.success(Path.of("1.txt").readText())
|
||||
* }
|
||||
@@ -48,19 +51,47 @@ import kotlinx.coroutines.withContext
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
typealias ErrorSink = FlowCollector<@NlsSafe String>
|
||||
typealias ErrorSink = FlowCollector<PyError>
|
||||
|
||||
|
||||
sealed class PyError(val message: @NlsSafe String) {
|
||||
/**
|
||||
* Some "business" error: just a message to be displayed to a user
|
||||
*/
|
||||
class Message(message: @NlsSafe String) : PyError(message)
|
||||
|
||||
/**
|
||||
* Some process can't be executed. To be displayed specially.
|
||||
*/
|
||||
data class ExecException(val execFailure: PyExecutionFailure) : PyError(execFailure.toString())
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays error with a message box and writes it to a log.
|
||||
*/
|
||||
internal object ShowingMessageErrorSync : ErrorSink {
|
||||
override suspend fun emit(value: @NlsSafe String) {
|
||||
override suspend fun emit(error: PyError) {
|
||||
withContext(Dispatchers.EDT + ModalityState.any().asContextElement()) {
|
||||
thisLogger().warn(value)
|
||||
thisLogger().warn(error.message)
|
||||
// Platform doesn't allow dialogs without lock for now, fix later
|
||||
writeIntentReadAction {
|
||||
Messages.showErrorDialog(value, PyBundle.message("python.error"))
|
||||
when (val e = error) {
|
||||
is PyError.ExecException -> {
|
||||
showProcessExecutionErrorDialog(null, e.execFailure)
|
||||
}
|
||||
is PyError.Message -> {
|
||||
Messages.showErrorDialog(error.message, PyBundle.message("python.error"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun ErrorSink.emit(@NlsSafe message: String) {
|
||||
emit(PyError.Message(message))
|
||||
}
|
||||
|
||||
suspend fun ErrorSink.emit(e: PyExecutionException) {
|
||||
emit(PyError.ExecException(e))
|
||||
}
|
||||
Reference in New Issue
Block a user