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:
Ilya.Kazakevich
2025-01-22 21:55:36 +01:00
committed by intellij-monorepo-bot
parent 4b22bc0b72
commit ca2148932f
21 changed files with 373 additions and 67 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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" />

View File

@@ -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}

View 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)
}
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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);
}
}

View File

@@ -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.

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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.*

View File

@@ -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))
}