Python: refactor PyError hierarchy, migrate to PyResult.

DO:
For upper-level (public) API use `PyResult`.
(Optionally) for low-level APIs inside your modules use python `Result<S, E>`.
Represent errors as `PyError` whenever possible.
Report `PyError` to `ErrorSink` at the top of your code.

DON'T:
Use `kotlin.Result`
Use `PyExecutionException`
Use any exception to represent user errors.

GitOrigin-RevId: 4ecf69e1fae8be9192cd33b90e0147c725a98964
This commit is contained in:
Ilya.Kazakevich
2025-04-29 00:43:56 +00:00
committed by intellij-monorepo-bot
parent d366245171
commit 803e270d45
74 changed files with 693 additions and 713 deletions
@@ -1,4 +1,4 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.pycharm.community.ide.impl.configuration
import com.intellij.CommonBundle
@@ -32,7 +32,6 @@ import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.intellij.util.ui.JBUI
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PySdkBundle
import com.jetbrains.python.failure
import com.jetbrains.python.packaging.PyPackageManager
import com.jetbrains.python.packaging.PyPackageUtil
import com.jetbrains.python.packaging.PyTargetEnvironmentPackageManager
@@ -67,7 +66,7 @@ class PyRequirementsTxtOrSetupPySdkConfiguration : PyProjectSdkConfigurationExte
val data = askForEnvData(module, existingSdks, source)
if (data == null) {
return failure("askForEnvData is null")
return com.jetbrains.python.failure("askForEnvData is null")
}
val (location, chosenBaseSdk, requirementsTxtOrSetupPy) = data
@@ -181,7 +180,7 @@ class PyRequirementsTxtOrSetupPySdkConfiguration : PyProjectSdkConfigurationExte
override fun createCenterPanel(): JComponent {
return JPanel(BorderLayout()).apply {
val border = IdeBorderFactory.createEmptyBorder(Insets(4, 0, 6, 0))
val border = IdeBorderFactory.createEmptyBorder(JBUI.insets(4, 0, 6, 0))
val message = PyCharmCommunityCustomizationBundle.message("sdk.create.venv.permission", requirementsTxtOrSetupPy.name)
add(
@@ -25,7 +25,8 @@ import com.intellij.python.community.services.systemPython.SystemPythonService
import com.intellij.util.SystemProperties
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.MessageError
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.errorProcessing.failure
import com.jetbrains.python.mapResult
import com.jetbrains.python.projectCreation.createVenvAndSdk
@@ -53,7 +54,7 @@ fun createMiscProject(
confirmInstallation: suspend () -> Boolean,
projectPath: Path = miscProjectDefaultPath.value,
systemPythonService: SystemPythonService = SystemPythonService(),
): Result<Job, PyError> =
): PyResult<Job> =
runWithModalProgressBlocking(ModalTaskOwner.guess(),
PyCharmCommunityCustomizationBundle.message("misc.project.generating.env"),
TaskCancellation.cancellable()) {
@@ -130,7 +131,7 @@ private suspend fun createProjectAndSdk(
projectPath: Path,
confirmInstallation: suspend () -> Boolean,
systemPythonService: SystemPythonService,
): Result<Pair<Project, Sdk>, PyError> {
): PyResult<Pair<Project, Sdk>> {
val vfsProjectPath = createProjectDir(projectPath).getOr { return it }
val project = openProject(projectPath)
val sdk = createVenvAndSdk(project, confirmInstallation, systemPythonService, vfsProjectPath).getOr { return it }
@@ -155,7 +156,7 @@ private suspend fun openProject(projectPath: Path): Project {
/**
* Creating a project != creating a directory for it, but we need a directory to create a template file
*/
private suspend fun createProjectDir(projectPath: Path): Result<VirtualFile, PyError.Message> = withContext(Dispatchers.IO) {
private suspend fun createProjectDir(projectPath: Path): Result<VirtualFile, MessageError> = withContext(Dispatchers.IO) {
try {
projectPath.createDirectories()
}
@@ -10,14 +10,14 @@ import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.pycharm.community.ide.impl.newProjectWizard.welcome.PyWelcome
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.newProjectWizard.PyV3ProjectTypeSpecificSettings
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class PyV3EmptyProjectSettings(var generateWelcomeScript: Boolean = false) : PyV3ProjectTypeSpecificSettings {
override suspend fun generateProject(module: Module, baseDir: VirtualFile, sdk: Sdk): Result<Unit, PyError> {
override suspend fun generateProject(module: Module, baseDir: VirtualFile, sdk: Sdk): PyResult<Unit> {
if (!generateWelcomeScript) return Result.success(Unit)
val sourceRoot = module.rootManager.sourceRoots.firstOrNull() ?: baseDir
@@ -1,2 +1,3 @@
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}
python.execution.cant.start.error={0}\nThe following command could not be started: {1}. Error {2} code: {3}
python.execution.timeout={0}\nThe following command stopped due to timeout: {1}.
@@ -1,4 +1,4 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
// 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.openapi.diagnostic.Logger
@@ -6,6 +6,9 @@ import com.jetbrains.python.Result.Failure
import com.jetbrains.python.Result.Success
/**
* TL;TR: This class is kinda low-level. Use [com.jetbrains.python.errorProcessing.PyResult] in upper-level signatures,
* but use this class as low-level api (preferably internal) inside your modules.
*
* Operation result to be used as `Maybe` instead of checked exceptions.
* Unlike Kotlin `Result`, [ERR] could be anything (i.e [String]).
*
@@ -4,38 +4,14 @@ package com.jetbrains.python.errorProcessing
import kotlinx.coroutines.flow.FlowCollector
/**
* [emit] user-readable errors here.
* [emit] user-readable [PyError] 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 [com.jetbrains.python.Result] or error.
* For the most business-logic and backend functions please return [PyResult] or [PyError].
*
* 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.
* `IOException` or `ExecutionException` are generally ok.
* There will be a unified sink soon to show and log errors.
* Currently, only [com.jetbrains.python.util.ShowingMessageErrorSync] is a well-known implementation.
*
* There will be unified sink soon to show and log errors.
* Currently, only [com.jetbrains.python.util.ShowingMessageErrorSync] is a well-known implementation
*
* Example:
* ```kotlin
* suspend fun someLogic(): Result<@NlsSafe String, IOException> = withContext(Dispatchers.IO) {
* try {
* Result.success(Path.of("1.txt").readText())
* }
* catch (e: IOException) {
* Result.failure(e)
* }
* }
*
* suspend fun ui(errorSink: ErrorSink) {
* someLogic()
* .onSuccess {
* Messages.showInfoMessage("..", it)
* }
* .onFailure {
* errorSink.emit(it.localizedMessage)
* }
* }
* ```
* See [PyError]
*/
typealias ErrorSink = FlowCollector<PyError>
@@ -0,0 +1,85 @@
// 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.errorProcessing
import com.intellij.execution.process.ProcessOutput
import com.intellij.openapi.util.NlsContexts
import com.jetbrains.python.PyCommunityBundle
import org.jetbrains.annotations.Nls
/**
* External process error.
*/
class ExecError(
/**
* I.e ['python', '-v']
*/
val command: Array<String>,
val errorReason: ExecErrorReason,
/**
* optional message to be displayed to the user: Why did we run this process. I.e "running pip to install package".
*/
val additionalMessageToUser: @NlsContexts.DialogTitle String? = null,
) : PyError(getExecErrorMessage(command, additionalMessageToUser, errorReason)) {
val exeAndArgs: Pair<String, Array<String>> = Pair(command[0], command.drop(1).toTypedArray())
init {
assert(command.isNotEmpty()) { "Command can't be empty" }
}
}
sealed interface ExecErrorReason {
/**
* A process failed to start.
* That means the process hasn't been even created.
* Os might return [errNo] (not always possible to fetchit) and [cantExecProcessError]
*/
data class CantStart(val errNo: Int?, val cantExecProcessError: String) : ExecErrorReason
/**
* A process started but failed with an error.
*/
data class UnexpectedProcessTermination(val exitCode: Int, val stdout: String, val stderr: String) : ExecErrorReason
/**
* Process started, but killed due to timeout without returning any useful data
*/
data object Timeout : ExecErrorReason
}
fun ProcessOutput.asExecutionFailed(): ExecErrorReason.UnexpectedProcessTermination =
ExecErrorReason.UnexpectedProcessTermination(exitCode, stdout, stderr)
internal fun getExecErrorMessage(
command: Array<String>,
additionalMessage: @NlsContexts.DialogTitle String?,
execErrorReason: ExecErrorReason,
): @Nls String {
val commandLine = command.joinToString(" ")
return when (val r = execErrorReason) {
is ExecErrorReason.CantStart -> {
PyCommunityBundle.message("python.execution.cant.start.error",
additionalMessage ?: "",
commandLine,
r.cantExecProcessError,
r.errNo ?: "unknown")
}
is ExecErrorReason.UnexpectedProcessTermination -> {
PyCommunityBundle.message("python.execution.error",
additionalMessage ?: "",
commandLine,
r.stdout,
r.stderr,
r.exitCode)
}
ExecErrorReason.Timeout -> {
PyCommunityBundle.message("python.execution.timeout",
additionalMessage ?: "",
commandLine)
}
}
}
@@ -0,0 +1,17 @@
// 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.errorProcessing
import com.intellij.openapi.util.NlsSafe
import com.jetbrains.python.Result
import org.jetbrains.annotations.Nls
/**
* Some "business" error: just a message to be displayed to a user
*/
open class MessageError(message: @NlsSafe String) : PyError(message)
fun failure(message: @Nls String): Result<Nothing, MessageError> = failure(MessageError(message))
suspend fun ErrorSink.emit(message: @Nls String) {
emit(MessageError(message))
}
@@ -2,57 +2,16 @@
package com.jetbrains.python.errorProcessing
import com.intellij.openapi.util.NlsSafe
import com.jetbrains.python.execution.PyExecutionFailure
import com.jetbrains.python.execution.userMessage
import com.jetbrains.python.packaging.PyExecutionException
import org.jetbrains.annotations.Nls
/**
* Error that is interested to user.
* Such errors usually stem from user errors or external process errors (i.e., permissions, network connections).
* Those are *not* NPEs nor OOBs nor various assertions.
* Do *not* use `catch(Exception)` or `runCatching` with this class.
*
* Most probably you will send this error to [ErrorSink].
*/
sealed class PyError(val message: @NlsSafe String) {
/**
* Some "business" error: just a message to be displayed to a user
*/
open class Message(message: @NlsSafe String) : PyError(message)
/**
* Some process can't be executed. To be displayed specially.
*/
open class ExecException(val execFailure: PyExecutionFailure) : PyError(execFailure.userMessage)
override fun toString(): String = message
}
suspend fun ErrorSink.emit(@NlsSafe message: String) {
emit(PyError.Message(message))
}
suspend fun ErrorSink.emit(e: PyExecutionException) {
emit(PyError.ExecException(e))
}
@Deprecated("Migrate to native python result")
fun <T> Result<T>.asPythonResult(): com.jetbrains.python.Result<T, PyError> =
com.jetbrains.python.Result.Companion.success(getOrElse {
return if (it is PyExecutionException) {
failure(it)
}
else {
failure(it.localizedMessage)
}
}
)
@Deprecated("Use python result, not kotlin result")
fun <S, E> com.jetbrains.python.Result<S, E>.asKotlinResult(): Result<S> = when (this) {
is com.jetbrains.python.Result.Failure -> Result.failure(
when (val r = error) {
is Throwable -> r
is PyError.Message -> Exception(r.message)
is PyError.ExecException -> Exception(r.execFailure.userMessage)
else -> Exception(r.toString())
}
)
is com.jetbrains.python.Result.Success -> Result.success(result)
}
fun failure(message: @Nls String): com.jetbrains.python.Result.Failure<PyError.Message> = com.jetbrains.python.Result.Companion.failure(PyError.Message(message))
fun failure(failure: PyExecutionFailure): com.jetbrains.python.Result.Failure<PyError.ExecException> = com.jetbrains.python.Result.failure(PyError.ExecException(failure))
@@ -0,0 +1,25 @@
// 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.errorProcessing
import com.jetbrains.python.packaging.PyExecutionException
/**
* This class is expected to be used as a return value of most PyCharm APIs.
* Use it instead of exceptions and Kotlin Result.
*/
typealias PyResult<T> = com.jetbrains.python.Result<T, PyError>
inline fun <reified T : PyError> failure(pyError: T): com.jetbrains.python.Result.Failure<T> = com.jetbrains.python.Result.Companion.failure(pyError)
@Deprecated("Migrate to native python result")
fun <T> Result<T>.asPythonResult(): com.jetbrains.python.Result<T, PyError> =
com.jetbrains.python.Result.Companion.success(getOrElse {
return if (it is PyExecutionException) {
failure(it.pyError)
}
else {
failure(MessageError(it.localizedMessage))
}
}
)
@@ -1,4 +1,7 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
/**
* See {@link com.jetbrains.python.errorProcessing.PyResultKt} as an entry point.
*/
@ApiStatus.Experimental
package com.jetbrains.python.errorProcessing;
@@ -1,31 +0,0 @@
// 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)
}
}
@@ -1,50 +0,0 @@
// 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
import com.jetbrains.python.PyCommunityBundle
import org.jetbrains.annotations.Nls
/**
* 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
}
/**
* User-readable message about this problem
*/
val PyExecutionFailure.userMessage: @Nls String get() = getUserMessage(command, args, additionalMessage, failureReason)
internal fun getUserMessage(
command: String,
args: List<String>,
additionalMessage: @NlsContexts.DialogTitle String?,
failureReason: FailureReason,
): @Nls String = when (val r = failureReason) {
FailureReason.CantStart -> {
PyCommunityBundle.message("python.execution.cant.start.error",
additionalMessage ?: "",
(listOf(command) + args).joinToString(" "))
}
is FailureReason.ExecutionFailed -> {
PyCommunityBundle.message("python.execution.error",
additionalMessage ?: "",
(listOf(command) + args).joinToString(" "),
r.output.stdout,
r.output.stderr,
r.output.getExitCode())
}
}
@@ -1,147 +0,0 @@
// 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.ExecutionException;
import com.intellij.execution.process.ProcessOutput;
import com.intellij.openapi.util.NlsContexts.DialogMessage;
import com.jetbrains.python.execution.FailureReason;
import com.jetbrains.python.execution.FailureReasonKt;
import com.jetbrains.python.execution.PyExecutionFailure;
import com.jetbrains.python.execution.PyExecutionFailureKt;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.List;
/**
* 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 @NotNull List<? extends PyExecutionFix> myFixes;
private final @DialogMessage @Nullable String myAdditionalMessage;
private final @NotNull FailureReason myError;
/**
* 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());
}
/**
* 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(PyExecutionFailureKt.getUserMessage(command, args, additionalMessage, FailureReason.CantStart.INSTANCE));
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(additionalMessage, command, args, output, Collections.emptyList());
}
/**
* 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(PyExecutionFailureKt.getUserMessage(command, args, additionalMessage, new FailureReason.ExecutionFailed(output)));
myAdditionalMessage = additionalMessage;
myCommand = command;
myArgs = args;
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 @NotNull String getCommand() {
return myCommand;
}
@Override
public @NotNull List<String> getArgs() {
return myArgs;
}
public @NotNull List<? extends PyExecutionFix> getFixes() {
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() {
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);
}
}
@@ -0,0 +1,102 @@
// 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.packaging
import com.intellij.execution.ExecutionException
import com.intellij.execution.process.ProcessOutput
import com.intellij.openapi.util.NlsContexts
import com.jetbrains.python.errorProcessing.*
import org.jetbrains.annotations.ApiStatus
import java.io.IOException
/**
* Wraps [PyError] for cases where [ExecutionException] is used.
* As [PyResult] should be used instead of exceptions, new code should use [PyError]
*
* Migrate to [PyError], please.
*/
@ApiStatus.Obsolete
class PyExecutionException private constructor(
val pyError: PyError,
val fixes: List<PyExecutionFix>,
ioException: IOException? = null,
) : ExecutionException(pyError.message, ioException) {
companion object {
/**
* [com.jetbrains.python.packaging.PyExecutionException] for process died with timeout
*/
@JvmStatic
fun createForTimeout(
additionalMessageToUser: @NlsContexts.DialogMessage String?,
command: String,
args: List<String>,
): PyExecutionException = PyExecutionException(ExecError(arrayOf(command) + args.toTypedArray(), ExecErrorReason.Timeout, additionalMessageToUser))
}
@JvmOverloads
constructor(
pyError: PyError,
fixes: List<PyExecutionFix> = listOf<PyExecutionFix>(),
) : this(pyError, fixes, null)
/**
* System decided not to start a process at all due to [messageToUser]
*/
@JvmOverloads
constructor(
messageToUser: @NlsContexts.DialogMessage String,
fixes: List<PyExecutionFix> = listOf<PyExecutionFix>(),
) : this(
pyError = MessageError(messageToUser),
fixes = fixes)
/**
* A process failed to start, [ExecErrorReason.CantStart]
*
* @param additionalMessage a process start reason for a user
*/
@JvmOverloads
constructor(
startException: IOException,
additionalMessage: @NlsContexts.DialogMessage String?,
command: String,
args: List<String>,
fixes: List<PyExecutionFix> = listOf<PyExecutionFix>(),
) : this(
pyError = ExecError(arrayOf(command) + args.toTypedArray(), ExecErrorReason.CantStart(null, startException.localizedMessage), additionalMessage),
fixes = fixes,
ioException = startException)
/**
* A process started, but failed [com.jetbrains.python.errorProcessing.ExecErrorReason.UnexpectedProcessTermination]
*
* @param additionalMessage a process start reason for a user
* @param output execution output
*/
@JvmOverloads
constructor(
additionalMessage: @NlsContexts.DialogMessage String?,
command: String,
args: List<String>,
output: ProcessOutput,
fixes: List<PyExecutionFix> = listOf<PyExecutionFix>(),
) : this(
pyError = ExecError(arrayOf(command) + args.toTypedArray(), output.asExecutionFailed()),
fixes = fixes)
/**
* A process started, but failed [ExecErrorReason.UnexpectedProcessTermination]
*
* @param additionalMessage a process start reason for a user
*/
constructor(
additionalMessage: @NlsContexts.DialogMessage String?,
command: String,
args: List<String>,
stdout: String,
stderr: String,
exitCode: Int,
fixes: List<PyExecutionFix>,
) : this(additionalMessage, command, args, ProcessOutput(stdout, stderr, exitCode, false, false), fixes)
}
@@ -558,7 +558,7 @@ sdk.create.custom.hatch.environment=Environment:
sdk.create.custom.hatch.environment.loading=Loading environments\u2026
sdk.create.custom.hatch.environment.exists=Environment already exists
sdk.create.custom.hatch.error.no.environments.to.select=Hatch didn't provide any environment to select
sdk.create.custom.hatch.error.execution.failed=Please verify Hatch tool, executed with error: {0} {1}
sdk.create.custom.hatch.error.execution.failed=Please verify Hatch tool, executed with error: {0}
sdk.create.custom.hatch.error.module.is.not.selected=Module is not selected
sdk.create.custom.hatch.error.project.is.not.selected=Project is not selected
sdk.create.custom.hatch.error.environment.is.not.selected=Hatch environment is not selected
@@ -8,7 +8,7 @@ import com.intellij.platform.eel.EelProcess
import com.intellij.python.community.execService.impl.ExecServiceImpl
import com.jetbrains.python.PythonBinary
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyError.ExecException
import com.jetbrains.python.errorProcessing.ExecError
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.CheckReturnValue
import org.jetbrains.annotations.Nls
@@ -17,7 +17,7 @@ import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
/**
* Error is an optional additionalMessage, that will be used instead of a default one for the [ExecException] in the [com.jetbrains.python.execution.PyExecutionFailure].
* Error is an optional additionalMessage, that will be used instead of a default one for the [ExecError] in the [com.jetbrains.python.execution.PyExecutionFailure].
*/
typealias ProcessOutputTransformer<T> = (ProcessOutput) -> Result<T, @NlsSafe String?>
@@ -41,7 +41,7 @@ interface ExecService {
args: List<String> = emptyList(),
options: ExecOptions = ExecOptions(),
eelProcessInteractiveHandler: EelProcessInteractiveHandler<T>,
): Result<T, ExecException>
): Result<T, ExecError>
/**
* Execute [whatToExec] with [args] and get both stdout/stderr outputs if `errorCode != 0`, gets error otherwise.
@@ -57,14 +57,14 @@ interface ExecService {
args: List<String> = emptyList(),
options: ExecOptions = ExecOptions(),
processOutputTransformer: ProcessOutputTransformer<T>,
): Result<T, ExecException>
): Result<T, ExecError>
@CheckReturnValue
suspend fun execGetStdout(
whatToExec: WhatToExec,
args: List<String> = emptyList(),
options: ExecOptions = ExecOptions(),
): Result<String, ExecException> = execute(
): Result<String, ExecError> = execute(
whatToExec = whatToExec,
args = args,
options = options,
@@ -3,31 +3,18 @@ package com.intellij.python.community.execService.impl
import com.intellij.execution.process.ProcessOutput
import com.intellij.openapi.diagnostic.fileLogger
import com.intellij.platform.eel.EelApi
import com.intellij.platform.eel.EelExecApi
import com.intellij.platform.eel.EelProcess
import com.intellij.platform.eel.execute
import com.intellij.platform.eel.getOr
import com.intellij.platform.eel.*
import com.intellij.platform.eel.path.EelPath
import com.intellij.platform.eel.provider.asEelPath
import com.intellij.platform.eel.provider.getEelDescriptor
import com.intellij.platform.eel.provider.utils.EelPathUtils
import com.intellij.platform.eel.provider.utils.EelProcessExecutionResult
import com.intellij.platform.eel.provider.utils.awaitProcessResult
import com.intellij.platform.eel.provider.utils.stderrString
import com.intellij.platform.eel.provider.utils.stdoutString
import com.intellij.python.community.execService.EelProcessInteractiveHandler
import com.intellij.python.community.execService.ExecOptions
import com.intellij.python.community.execService.ExecService
import com.intellij.python.community.execService.ProcessOutputTransformer
import com.intellij.python.community.execService.WhatToExec
import com.intellij.platform.eel.provider.utils.*
import com.intellij.python.community.execService.*
import com.jetbrains.python.PythonHelpersLocator
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyError.ExecException
import com.jetbrains.python.errorProcessing.ExecError
import com.jetbrains.python.errorProcessing.ExecErrorReason
import com.jetbrains.python.errorProcessing.asExecutionFailed
import com.jetbrains.python.errorProcessing.failure
import com.jetbrains.python.execution.FailureReason
import com.jetbrains.python.execution.PyExecutionFailure
import com.jetbrains.python.execution.userMessage
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.withTimeout
import org.jetbrains.annotations.CheckReturnValue
@@ -43,7 +30,7 @@ internal object ExecServiceImpl : ExecService {
args: List<String>,
options: ExecOptions,
eelProcessInteractiveHandler: EelProcessInteractiveHandler<T>,
): Result<T, ExecException> {
): Result<T, ExecError> {
val executableProcess = whatToExec.buildExecutableProcess(args, options)
val eelProcess = executableProcess.run().getOr { return it }
@@ -70,7 +57,7 @@ internal object ExecServiceImpl : ExecService {
args: List<String>,
options: ExecOptions,
processOutputTransformer: ProcessOutputTransformer<T>,
): Result<T, ExecException> {
): Result<T, ExecError> {
val executableProcess = whatToExec.buildExecutableProcess(args, options)
val eelProcess = executableProcess.run().getOr { return it }
@@ -124,7 +111,7 @@ private suspend fun WhatToExec.buildExecutableProcess(args: List<String>, option
}
@CheckReturnValue
private suspend fun EelExecutableProcess.run(): Result<EelProcess, ExecException> {
private suspend fun EelExecutableProcess.run(): Result<EelProcess, ExecError> {
val workDirectoryEelPath = workingDirectory?.let { EelPath.parse(it.toString(), eel.descriptor) }
val executionResult = eel.exec.execute(exe)
.args(args)
@@ -137,41 +124,38 @@ private suspend fun EelExecutableProcess.run(): Result<EelProcess, ExecException
return Result.success(process)
}
private fun EelExecutableProcess.failAsCantStart(executeProcessError: EelExecApi.ExecuteProcessError): Result.Failure<ExecException> {
return PyExecFailureImpl(
command = exe,
args = args,
additionalMessage = PyExecBundle.message("py.exec.start.error", description, executeProcessError.message, executeProcessError.errno),
failureReason = FailureReason.CantStart
private fun EelExecutableProcess.failAsCantStart(executeProcessError: EelExecApi.ExecuteProcessError): Result.Failure<ExecError> {
return ExecError(
command = arrayOf(exe) + args.toTypedArray(),
additionalMessageToUser = PyExecBundle.message("py.exec.start.error", description, executeProcessError.message, executeProcessError.errno),
errorReason = ExecErrorReason.CantStart(executeProcessError.errno, executeProcessError.message)
).logAndFail()
}
private suspend fun EelExecutableProcess.killProcessAndFailAsTimeout(eelProcess: EelProcess, timeout: Duration): Result.Failure<ExecException> {
private suspend fun EelExecutableProcess.killProcessAndFailAsTimeout(eelProcess: EelProcess, timeout: Duration): Result.Failure<ExecError> {
eelProcess.kill()
return PyExecFailureImpl(
command = exe,
args = args,
additionalMessage = PyExecBundle.message("py.exec.timeout.error", description, timeout),
failureReason = FailureReason.CantStart
return ExecError(
command = arrayOf(exe) + args.toTypedArray(),
additionalMessageToUser = PyExecBundle.message("py.exec.timeout.error", description, timeout),
errorReason = ExecErrorReason.Timeout
).logAndFail()
}
private fun EelExecutableProcess.failAsExecutionFailed(processOutput: ProcessOutput, customMessage: @Nls String?): Result.Failure<ExecException> {
private fun EelExecutableProcess.failAsExecutionFailed(processOutput: ProcessOutput, customMessage: @Nls String?): Result.Failure<ExecError> {
val additionalMessage = customMessage ?: run {
PyExecBundle.message("py.exec.exitCode.error", description, processOutput.exitCode)
}
return PyExecFailureImpl(
command = exe,
args = args,
additionalMessage = additionalMessage,
failureReason = FailureReason.ExecutionFailed(processOutput)
return ExecError(
command = arrayOf(exe) + args.toTypedArray(),
additionalMessageToUser = additionalMessage,
errorReason = processOutput.asExecutionFailed()
).logAndFail()
}
private fun PyExecutionFailure.logAndFail(): Result.Failure<ExecException> {
fileLogger().warn(userMessage)
private fun ExecError.logAndFail(): Result.Failure<ExecError> {
fileLogger().warn(message)
return failure(this)
}
@@ -1,16 +0,0 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.community.execService.impl
import com.intellij.openapi.util.NlsContexts
import com.jetbrains.python.execution.FailureReason
import com.jetbrains.python.execution.PyExecutionFailure
import com.jetbrains.python.execution.userMessage
internal data class PyExecFailureImpl(
override val command: String,
override val args: List<String>,
override val additionalMessage: @NlsContexts.DialogTitle String? = null,
override val failureReason: FailureReason,
) : PyExecutionFailure {
override fun toString(): String = userMessage
}
@@ -94,9 +94,8 @@ class ExecServiceShowCaseTest {
is Result.Success -> fail("Execution of bad command should lead to an error")
is Result.Failure -> {
val err = output.error
val failure = err.execFailure
assertEquals(command.command, failure.command, "Wrong command reported")
assertEquals("foo", failure.args[0], "Wrong args reported")
assertEquals(command.command, err.command[0], "Wrong command reported")
assertEquals("foo", err.command[1], "Wrong args reported")
}
}
}
@@ -9,8 +9,8 @@ import com.intellij.python.hatch.PyHatchBundle
import com.intellij.python.hatch.runtime.HatchConstants
import com.intellij.python.hatch.runtime.HatchRuntime
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.PyError.ExecException
import com.jetbrains.python.errorProcessing.ExecError
import com.jetbrains.python.errorProcessing.PyResult
import io.github.z4kn4fein.semver.Version
import io.github.z4kn4fein.semver.VersionFormatException
import java.nio.file.Path
@@ -18,7 +18,7 @@ import java.nio.file.Path
/**
* Handles hatch-specific errors, runs [transformer] only on outputs with codes 0 or 1 without tracebacks.
*/
private suspend fun <T> HatchRuntime.executeAndHandleErrors(vararg arguments: String, transformer: ProcessOutputTransformer<T>): Result<T, ExecException> {
private suspend fun <T> HatchRuntime.executeAndHandleErrors(vararg arguments: String, transformer: ProcessOutputTransformer<T>): Result<T, ExecError> {
val errorHandlerTransformer: ProcessOutputTransformer<T> = { output ->
when {
output.exitCode !in 0..1 -> Result.failure(null)
@@ -38,7 +38,7 @@ private suspend fun <T> HatchRuntime.executeAndMatch(
expectedOutput: Regex,
outputContentSupplier: (ProcessOutput) -> String = ProcessOutput::getStdout,
transformer: (MatchResult) -> Result<T, @NlsSafe String?>,
): Result<T, ExecException> {
): Result<T, ExecError> {
return this.executeAndHandleErrors(*arguments) { processOutput ->
if (processOutput.exitCode != 0) return@executeAndHandleErrors Result.failure(null)
@@ -57,11 +57,11 @@ sealed class HatchCommand(private val command: Array<String>, protected val runt
@Suppress("unused")
constructor(command: String, runtime: HatchRuntime) : this(arrayOf(command), runtime)
protected suspend fun <T> executeAndHandleErrors(vararg arguments: String, transformer: ProcessOutputTransformer<T>): Result<T, ExecException> {
protected suspend fun <T> executeAndHandleErrors(vararg arguments: String, transformer: ProcessOutputTransformer<T>): Result<T, ExecError> {
return runtime.executeAndHandleErrors(*command, *arguments, transformer = transformer)
}
protected suspend fun <T> executeAndMatch(vararg arguments: String, expectedOutput: Regex, transformer: (MatchResult) -> Result<T, @NlsSafe String?>): Result<T, ExecException> {
protected suspend fun <T> executeAndMatch(vararg arguments: String, expectedOutput: Regex, transformer: (MatchResult) -> Result<T, @NlsSafe String?>): Result<T, ExecError> {
return runtime.executeAndMatch(*command, *arguments, expectedOutput = expectedOutput, transformer = transformer)
}
}
@@ -70,12 +70,12 @@ class HatchCli(private val runtime: HatchRuntime) {
/**
* Build a project
*/
fun build(): Result<Unit, ExecException> = TODO()
fun build(): Result<Unit, ExecError> = TODO()
/**
* Remove build artifacts
*/
fun clean(): Result<Unit, ExecException> = TODO()
fun clean(): Result<Unit, ExecError> = TODO()
/**
* Manage the config file
@@ -95,7 +95,7 @@ class HatchCli(private val runtime: HatchRuntime) {
/**
* Format and lint source code
*/
fun fmt(): Result<Unit, ExecException> = TODO()
fun fmt(): Result<Unit, ExecError> = TODO()
/**
* Create or initialize a project.
@@ -114,7 +114,7 @@ class HatchCli(private val runtime: HatchRuntime) {
*
* @param[initExistingProject] Initialize an existing project
*/
suspend fun new(projectName: String, location: Path? = null, initExistingProject: Boolean = false): Result<String, PyError> {
suspend fun new(projectName: String, location: Path? = null, initExistingProject: Boolean = false): PyResult<String> {
val options = listOf(
initExistingProject to "--init",
true to projectName,
@@ -136,7 +136,7 @@ class HatchCli(private val runtime: HatchRuntime) {
/**
* Publish build artifacts
*/
fun publish(): Result<Unit, ExecException> = TODO()
fun publish(): Result<Unit, ExecError> = TODO()
/**
* Manage Python installations
@@ -146,7 +146,7 @@ class HatchCli(private val runtime: HatchRuntime) {
/**
* Run commands within project environments
*/
suspend fun run(envName: String, vararg command: String): Result<String, ExecException> {
suspend fun run(envName: String, vararg command: String): Result<String, ExecError> {
val envRuntime = runtime.withEnv(HatchConstants.AppEnvVars.ENV to envName)
return envRuntime.executeAndHandleErrors("run", *command) { output ->
val scenario = output.stderr.trim()
@@ -170,14 +170,14 @@ class HatchCli(private val runtime: HatchRuntime) {
/**
* Enter a shell within a project's environment
*/
fun shell(): Result<Unit, ExecException> = TODO()
fun shell(): Result<Unit, ExecError> = TODO()
data class HatchStatus(val project: String, val location: Path, val config: Path)
/**
* Show information about the current environment
*/
suspend fun status(): Result<HatchStatus, ExecException> {
suspend fun status(): Result<HatchStatus, ExecError> {
val expectedOutput = """^\[Project] - (.*)\n\[Location] - (.*)\n\[Config] - (.*)\n$""".toRegex()
return runtime.executeAndMatch("status", expectedOutput = expectedOutput, outputContentSupplier = { it.stderr }) { matchResult ->
@@ -194,14 +194,14 @@ class HatchCli(private val runtime: HatchRuntime) {
/**
* Run tests
*/
fun test(): Result<Unit, ExecException> = TODO()
fun test(): Result<Unit, ExecError> = TODO()
/**
* View a project's version.
*
* @return Project Version
*/
suspend fun getVersion(): Result<Version, ExecException> {
suspend fun getVersion(): Result<Version, ExecError> {
return runtime.executeAndHandleErrors("version") { processOutput ->
val output = processOutput.takeIf { it.exitCode == 0 }?.stdout?.trim()
?: return@executeAndHandleErrors Result.failure(null)
@@ -219,7 +219,7 @@ class HatchCli(private val runtime: HatchRuntime) {
*
* @return OldVersion to NewVersion as Pair
*/
suspend fun setVersion(desiredVersion: String): Result<Pair<Version, Version>, PyError> {
suspend fun setVersion(desiredVersion: String): PyResult<Pair<Version, Version>> {
val expectedOutput = """^Old: (.*)\nNew: (.*)\n$""".toRegex()
return runtime.executeAndMatch("version", desiredVersion, expectedOutput = expectedOutput, outputContentSupplier = { it.stderr }) { matchResult ->
@@ -4,7 +4,7 @@ package com.intellij.python.hatch.cli
import com.intellij.python.community.execService.ZeroCodeStdoutTransformer
import com.intellij.python.hatch.runtime.HatchRuntime
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyError.ExecException
import com.jetbrains.python.errorProcessing.ExecError
/**
* Manage environment dependencies
@@ -13,28 +13,28 @@ class HatchConfig(runtime: HatchRuntime) : HatchCommand("config", runtime) {
/**
* Open the config location in your file manager
*/
suspend fun explore(): Result<String, ExecException> {
suspend fun explore(): Result<String, ExecError> {
return executeAndHandleErrors("explore", transformer = ZeroCodeStdoutTransformer)
}
/**
* Show the location of the config file
*/
suspend fun find(): Result<String, ExecException> {
suspend fun find(): Result<String, ExecError> {
return executeAndHandleErrors("find", transformer = ZeroCodeStdoutTransformer)
}
/**
* Restore the config file to default settings
*/
suspend fun restore(): Result<String, ExecException> {
suspend fun restore(): Result<String, ExecError> {
return executeAndHandleErrors("restore", transformer = ZeroCodeStdoutTransformer)
}
/**
* Assign values to config file entries
*/
suspend fun set(key: String, value: String): Result<String, ExecException> {
suspend fun set(key: String, value: String): Result<String, ExecError> {
return executeAndHandleErrors("set", key, value, transformer = ZeroCodeStdoutTransformer)
}
@@ -43,7 +43,7 @@ class HatchConfig(runtime: HatchRuntime) : HatchCommand("config", runtime) {
*
* @param all Do not scrub secret fields
*/
suspend fun show(all: Boolean? = null): Result<String, ExecException> {
suspend fun show(all: Boolean? = null): Result<String, ExecError> {
val options = listOf(all to "--all").makeOptions()
return executeAndHandleErrors("show", *options, transformer = ZeroCodeStdoutTransformer)
}
@@ -51,7 +51,7 @@ class HatchConfig(runtime: HatchRuntime) : HatchCommand("config", runtime) {
/**
* Update the config file with any new fields
*/
suspend fun update(): Result<String, ExecException> {
suspend fun update(): Result<String, ExecError> {
return executeAndHandleErrors("update", transformer = ZeroCodeStdoutTransformer)
}
}
@@ -4,7 +4,7 @@ package com.intellij.python.hatch.cli
import com.intellij.python.community.execService.ZeroCodeStdoutTransformer
import com.intellij.python.hatch.runtime.HatchRuntime
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyError.ExecException
import com.jetbrains.python.errorProcessing.ExecError
enum class Scope(val options: Array<String>) {
All(emptyArray()),
@@ -19,7 +19,7 @@ class HatchDep(runtime: HatchRuntime) : HatchCommand("dep", runtime) {
/**
* Output a hash of the currently defined dependencies
**/
suspend fun hash(scope: Scope = Scope.All): Result<String, ExecException> {
suspend fun hash(scope: Scope = Scope.All): Result<String, ExecError> {
return executeAndHandleErrors("hash", *scope.options, transformer = ZeroCodeStdoutTransformer)
}
@@ -38,7 +38,7 @@ class HatchDepShow(runtime: HatchRuntime) : HatchCommand(arrayOf("dep", "show"),
*
* @param features only show the dependencies of the specified features
*/
suspend fun requirements(scope: Scope = Scope.All, features: List<String>? = null): Result<String, ExecException> {
suspend fun requirements(scope: Scope = Scope.All, features: List<String>? = null): Result<String, ExecError> {
val options = features?.flatMap { listOf("--feature", it) }?.toTypedArray() ?: arrayOf("--all")
return executeAndHandleErrors("requirements", *scope.options, *options, transformer = ZeroCodeStdoutTransformer)
}
@@ -46,7 +46,7 @@ class HatchDepShow(runtime: HatchRuntime) : HatchCommand(arrayOf("dep", "show"),
/**
* Enumerate dependencies in a tabular format.
*/
suspend fun table(scope: Scope = Scope.All): Result<String, ExecException> {
suspend fun table(scope: Scope = Scope.All): Result<String, ExecError> {
val options = listOf(null to "--lines", true to "--ascii").makeOptions()
return executeAndHandleErrors("table", *scope.options, *options, transformer = ZeroCodeStdoutTransformer)
}
@@ -5,7 +5,7 @@ import com.intellij.openapi.util.NlsSafe
import com.intellij.python.hatch.runtime.HatchRuntime
import com.jetbrains.python.PythonHomePath
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyError.ExecException
import com.jetbrains.python.errorProcessing.ExecError
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@@ -127,7 +127,7 @@ class HatchEnv(runtime: HatchRuntime) : HatchCommand("env", runtime) {
*
* @return true if created, false if already exists
*/
suspend fun create(envName: String? = null): Result<CreateResult, ExecException> {
suspend fun create(envName: String? = null): Result<CreateResult, ExecError> {
val arguments = if (envName == null) emptyArray() else arrayOf(envName)
return executeAndHandleErrors("create", *arguments) {
val actualEnvName = envName ?: DEFAULT_ENV_NAME
@@ -145,7 +145,7 @@ class HatchEnv(runtime: HatchRuntime) : HatchCommand("env", runtime) {
*
* @return path to environment
*/
suspend fun find(envName: String? = null): Result<PythonHomePath?, ExecException> {
suspend fun find(envName: String? = null): Result<PythonHomePath?, ExecError> {
val arguments = if (envName == null) emptyArray() else arrayOf(envName)
return executeAndHandleErrors("find", *arguments) {
when (it.exitCode) {
@@ -178,9 +178,9 @@ class HatchEnv(runtime: HatchRuntime) : HatchCommand("env", runtime) {
* - [RemoveResult.NotExists] if the environment does not exist.
* - [RemoveResult.NotDefinedInConfig] if the environment is not defined in the project configuration.
* - [RemoveResult.CantRemoveActiveEnvironment] if the environment cannot be removed because it is currently active.
* - An error wrapped in [ExecException] in case of execution failure.
* - An error wrapped in [ExecError] in case of execution failure.
*/
suspend fun remove(envName: String? = null): Result<RemoveResult, ExecException> {
suspend fun remove(envName: String? = null): Result<RemoveResult, ExecError> {
val arguments = if (envName == null) emptyArray() else arrayOf(envName)
return executeAndHandleErrors("remove", *arguments) {
val actualEnvName = envName ?: DEFAULT_ENV_NAME
@@ -200,9 +200,9 @@ class HatchEnv(runtime: HatchRuntime) : HatchCommand("env", runtime) {
* @param envs A vararg parameter specifying the environment names to be displayed. If not provided, information for all environments is shown.
* @return A [Result] containing:
* - [HatchDetailedEnvironments] if operation is successful.
* - An error wrapped in [ExecException] if an execution failure occurs.
* - An error wrapped in [ExecError] if an execution failure occurs.
*/
suspend fun showWithDetails(vararg envs: String): Result<HatchDetailedEnvironments, ExecException> {
suspend fun showWithDetails(vararg envs: String): Result<HatchDetailedEnvironments, ExecError> {
return executeAndHandleErrors("show", "--json", *envs) { processOutput ->
val output = processOutput.takeIf { it.exitCode == 0 }?.stdout
?: return@executeAndHandleErrors Result.failure(null)
@@ -227,9 +227,9 @@ class HatchEnv(runtime: HatchRuntime) : HatchCommand("env", runtime) {
* @param internal Optional parameter indicating whether to include internal environments. Defaults to false.
* @return A [Result] containing:
* - [HatchDetailedEnvironments] if operation is successful.
* - An error wrapped in [ExecException] if an execution failure occurs.
* - An error wrapped in [ExecError] if an execution failure occurs.
*/
suspend fun show(vararg envs: String, internal: Boolean = false): Result<HatchEnvironments, ExecException> {
suspend fun show(vararg envs: String, internal: Boolean = false): Result<HatchEnvironments, ExecError> {
val options = listOf(internal to "--internal").makeOptions()
return executeAndMatch("show", "--ascii", *options, *envs, expectedOutput = SHOW_RESPONSE_REGEX) { matchResult ->
@@ -3,7 +3,7 @@ package com.intellij.python.hatch.cli
import com.intellij.python.hatch.runtime.HatchRuntime
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyError.ExecException
import com.jetbrains.python.errorProcessing.ExecError
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@@ -61,7 +61,7 @@ class HatchProject(runtime: HatchRuntime) : HatchCommand("project", runtime) {
/**
* Display project metadata
*/
suspend fun metadata(): Result<Metadata, ExecException> {
suspend fun metadata(): Result<Metadata, ExecError> {
return executeAndHandleErrors("metadata") { processOutput ->
val output = processOutput.takeIf { it.exitCode == 0 }?.stdout
?: return@executeAndHandleErrors Result.failure(null)
@@ -3,10 +3,10 @@ package com.intellij.python.hatch.cli
import com.intellij.execution.process.ProcessOutput
import com.intellij.openapi.util.io.NioFiles
import com.intellij.python.hatch.runtime.HatchRuntime
import com.intellij.python.hatch.cli.HatchPython.PythonInstallResponse.AbortReason
import com.intellij.python.hatch.runtime.HatchRuntime
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyError.ExecException
import com.jetbrains.python.errorProcessing.ExecError
import java.nio.file.Path
/**
@@ -24,7 +24,7 @@ class HatchPython(runtime: HatchRuntime) : HatchCommand("python", runtime) {
* @param parent Show the parent directory of the Python binary
* @param dir The directory in which distributions reside
*/
suspend fun find(name: String, parent: Boolean? = null, dir: String? = null): Result<Path?, ExecException> {
suspend fun find(name: String, parent: Boolean? = null, dir: String? = null): Result<Path?, ExecError> {
val options = listOf(parent to "--parent").makeOptions() + buildDirOption(dir)
return executeAndHandleErrors("find", *options, name) { output ->
@@ -127,7 +127,7 @@ class HatchPython(runtime: HatchRuntime) : HatchCommand("python", runtime) {
private: Boolean? = null,
update: Boolean? = null,
dir: String? = null,
): Result<PythonInstallResponse, ExecException> {
): Result<PythonInstallResponse, ExecError> {
val options = listOf(update to "--update", private to "--private").makeOptions() + buildDirOption(dir)
return executeAndHandleErrors("install", *options, *names) { output ->
Result.success(parsePythonInstallCommandOutput(output))
@@ -142,7 +142,7 @@ class HatchPython(runtime: HatchRuntime) : HatchCommand("python", runtime) {
* @param names Distributions to remove, you may select `all` to install all compatible distributions
* @param dir The directory in which distributions reside
*/
suspend fun remove(vararg names: String = ALL_NAMES, dir: String? = null): Result<PythonRemoveResponse, ExecException> {
suspend fun remove(vararg names: String = ALL_NAMES, dir: String? = null): Result<PythonRemoveResponse, ExecError> {
return executeAndHandleErrors("remove", *buildDirOption(dir), *names) { processOutput ->
val output = processOutput.stderr
val notInstalledRegex = Regex("""^Distribution is not installed: (.*)$""", RegexOption.MULTILINE)
@@ -163,7 +163,7 @@ class HatchPython(runtime: HatchRuntime) : HatchCommand("python", runtime) {
* @param dir The directory in which distributions reside
* @return Name to Version as a map
*/
suspend fun show(dir: String? = null): Result<ShowResponse, ExecException> {
suspend fun show(dir: String? = null): Result<ShowResponse, ExecError> {
val nameToVersionRegex = """\|\s+([^|\s]+)\s+\|\s+([^|\s]+)\s+\|""".toRegex()
fun parseNameToVersions(payload: String) = nameToVersionRegex.findAll(payload).associate {
val (name, version) = it.destructured
@@ -198,7 +198,7 @@ class HatchPython(runtime: HatchRuntime) : HatchCommand("python", runtime) {
* @param names Distributions to update, you may select `all` to install all compatible distributions
* @param dir The directory in which distributions reside
*/
suspend fun update(vararg names: String = ALL_NAMES, dir: String? = null): Result<PythonInstallResponse, ExecException> {
suspend fun update(vararg names: String = ALL_NAMES, dir: String? = null): Result<PythonInstallResponse, ExecError> {
return executeAndHandleErrors("update", *buildDirOption(dir), *names) { output ->
Result.success(parsePythonInstallCommandOutput(output))
}
@@ -1,12 +1,12 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.hatch.cli
import com.intellij.python.hatch.runtime.HatchRuntime
import com.intellij.python.hatch.PyHatchBundle
import com.intellij.python.hatch.runtime.HatchRuntime
import com.intellij.util.Url
import com.intellij.util.Urls
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyError.ExecException
import com.jetbrains.python.errorProcessing.ExecError
/**
* Manage environment dependencies
@@ -16,7 +16,7 @@ class HatchSelf(runtime: HatchRuntime) : HatchCommand("self", runtime) {
/**
* Generate a pre-populated GitHub issue.
*/
suspend fun report(): Result<Url, ExecException> {
suspend fun report(): Result<Url, ExecError> {
return executeAndHandleErrors("report", "--no-open") { processOutput ->
val output = processOutput.takeIf { it.exitCode == 0 }?.stdout?.trim()
?: return@executeAndHandleErrors Result.failure(null)
@@ -33,10 +33,10 @@ class HatchSelf(runtime: HatchRuntime) : HatchCommand("self", runtime) {
/**
* Restore the installation
*/
fun restore(): Result<String, ExecException> = TODO()
fun restore(): Result<String, ExecError> = TODO()
/**
* Install the latest version
*/
fun update(): Result<String, ExecException> = TODO()
fun update(): Result<String, ExecError> = TODO()
}
@@ -10,11 +10,12 @@ import com.intellij.python.hatch.service.CliBasedHatchService
import com.jetbrains.python.PythonBinary
import com.jetbrains.python.PythonHomePath
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.MessageError
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.sdk.basePath
import java.nio.file.Path
sealed class HatchError(message: @NlsSafe String) : PyError.Message(message)
sealed class HatchError(message: @NlsSafe String) : MessageError(message)
class HatchExecutableNotFoundHatchError(path: Path?) : HatchError(
PyHatchBundle.message("python.hatch.error.executable.is.not.found", path.toString())
@@ -79,25 +80,25 @@ data class ProjectStructure(
interface HatchService {
fun getWorkingDirectoryPath(): Path
suspend fun syncDependencies(envName: String): Result<String, PyError>
suspend fun syncDependencies(envName: String): PyResult<String>
suspend fun isHatchManagedProject(): Result<Boolean, PyError>
suspend fun isHatchManagedProject(): PyResult<Boolean>
suspend fun createNewProject(projectName: String): Result<ProjectStructure, PyError>
suspend fun createNewProject(projectName: String): PyResult<ProjectStructure>
/**
* param[basePythonBinaryPath] base python for environment, the one on the PATH should be used if null.
* param[envName] environment name to create, 'default' should be used if null.
*/
suspend fun createVirtualEnvironment(basePythonBinaryPath: PythonBinary? = null, envName: String? = null): Result<PythonVirtualEnvironment.Existing, PyError>
suspend fun createVirtualEnvironment(basePythonBinaryPath: PythonBinary? = null, envName: String? = null): PyResult<PythonVirtualEnvironment.Existing>
suspend fun findVirtualEnvironments(): Result<List<HatchVirtualEnvironment>, PyError>
suspend fun findVirtualEnvironments(): PyResult<List<HatchVirtualEnvironment>>
}
/**
* Hatch Service for working directory (where hatch.toml / pyproject.toml is usually placed)
*/
suspend fun Path.getHatchService(hatchExecutablePath: Path? = null): Result<HatchService, PyError> {
suspend fun Path.getHatchService(hatchExecutablePath: Path? = null): PyResult<HatchService> {
return CliBasedHatchService(hatchExecutablePath = hatchExecutablePath, workingDirectoryPath = this)
}
@@ -105,7 +106,7 @@ suspend fun Path.getHatchService(hatchExecutablePath: Path? = null): Result<Hatc
* Hatch Service for Module.
* Working directory considered as the module base path.
*/
suspend fun Module.getHatchService(hatchExecutablePath: Path? = null): Result<HatchService, PyError> {
suspend fun Module.getHatchService(hatchExecutablePath: Path? = null): PyResult<HatchService> {
val workingDirectoryPath = resolveHatchWorkingDirectory(this.project, this).getOr { return it }
return workingDirectoryPath.getHatchService(hatchExecutablePath = hatchExecutablePath)
}
@@ -115,7 +116,7 @@ suspend fun Module.getHatchService(hatchExecutablePath: Path? = null): Result<Ha
*/
fun PythonHomePath.getHatchEnvVirtualProjectPath(): Path = this.parent.parent
fun resolveHatchWorkingDirectory(project: Project, module: Module?): Result<Path, PyError> {
fun resolveHatchWorkingDirectory(project: Project, module: Module?): PyResult<Path> {
val pathString = module?.basePath ?: project.basePath
return when (val path = pathString?.let { Path.of(it) }) {
@@ -12,7 +12,8 @@ import com.intellij.python.hatch.cli.HatchCli
import com.jetbrains.python.PythonBinary
import com.jetbrains.python.PythonHomePath
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.ExecError
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.resolvePythonBinary
import java.nio.file.Path
import kotlin.io.path.isDirectory
@@ -57,15 +58,15 @@ class HatchRuntime(
* Pure execution of [hatchBinary] with command line [arguments] and [execOptions] by [execService]
* Doesn't make any validation of stdout/stderr content.
*/
internal suspend fun <T> execute(vararg arguments: String, processOutputTransformer: ProcessOutputTransformer<T>): Result<T, PyError.ExecException> {
internal suspend fun <T> execute(vararg arguments: String, processOutputTransformer: ProcessOutputTransformer<T>): Result<T, ExecError> {
return execService.execute(hatchBinary, arguments.toList(), execOptions, processOutputTransformer)
}
internal suspend fun <T> executeInteractive(vararg arguments: String, eelProcessInteractiveHandler: EelProcessInteractiveHandler<T>): Result<T, PyError.ExecException> {
internal suspend fun <T> executeInteractive(vararg arguments: String, eelProcessInteractiveHandler: EelProcessInteractiveHandler<T>): Result<T, ExecError> {
return execService.executeInteractive(hatchBinary, arguments.toList(), execOptions, eelProcessInteractiveHandler)
}
internal suspend fun resolvePythonVirtualEnvironment(pythonHomePath: PythonHomePath): Result<PythonVirtualEnvironment, PyError> {
internal suspend fun resolvePythonVirtualEnvironment(pythonHomePath: PythonHomePath): PyResult<PythonVirtualEnvironment> {
val pythonVersion = pythonHomePath.takeIf { it.isDirectory() }?.resolvePythonBinary()?.let { pythonBinaryPath ->
execService.execGetStdout(Binary(pythonBinaryPath), listOf("--version")).getOr { return it }.trim()
}
@@ -15,7 +15,7 @@ import com.intellij.python.hatch.runtime.HatchRuntime
import com.intellij.python.hatch.runtime.createHatchRuntime
import com.jetbrains.python.PythonBinary
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.PyResult
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
@@ -30,7 +30,7 @@ internal class CliBasedHatchService private constructor(
private val hatchRuntime: HatchRuntime,
) : HatchService {
companion object {
suspend operator fun invoke(workingDirectoryPath: Path, hatchExecutablePath: Path?): Result<CliBasedHatchService, PyError> {
suspend operator fun invoke(workingDirectoryPath: Path, hatchExecutablePath: Path?): PyResult<CliBasedHatchService> {
val hatchRuntime = createHatchRuntime(
hatchExecutablePath = hatchExecutablePath,
workingDirectoryPath = workingDirectoryPath,
@@ -51,13 +51,13 @@ internal class CliBasedHatchService private constructor(
override fun getWorkingDirectoryPath(): Path = workingDirectoryPath
override suspend fun syncDependencies(envName: String): Result<String, PyError> {
override suspend fun syncDependencies(envName: String): PyResult<String> {
return withContext(Dispatchers.IO) {
hatchRuntime.hatchCli().run(envName, "python", "--version")
}
}
override suspend fun isHatchManagedProject(): Result<Boolean, PyError> {
override suspend fun isHatchManagedProject(): PyResult<Boolean> {
val isHatchManaged = withContext(Dispatchers.IO) {
when {
workingDirectoryPath.resolve("hatch.toml").exists() -> true
@@ -72,7 +72,7 @@ internal class CliBasedHatchService private constructor(
}
override suspend fun findVirtualEnvironments(): Result<List<HatchVirtualEnvironment>, PyError> {
override suspend fun findVirtualEnvironments(): PyResult<List<HatchVirtualEnvironment>> {
val hatchEnv = hatchRuntime.hatchCli().env()
val environments: HatchEnvironments = hatchEnv.show().getOr { return it }
val virtualEnvironments = environments.getAvailableVirtualHatchEnvironments()
@@ -90,7 +90,7 @@ internal class CliBasedHatchService private constructor(
}
override suspend fun createNewProject(projectName: String): Result<ProjectStructure, PyError> {
override suspend fun createNewProject(projectName: String): PyResult<ProjectStructure> {
val eelApi = workingDirectoryPath.getEelDescriptor().upgrade()
val tempDir = eelApi.fs.createTemporaryDirectory(EelFileSystemApi.CreateTemporaryEntryOptions.Builder().build()).getOr { failure ->
return Result.failure(FileSystemOperationHatchError(failure.error))
@@ -108,7 +108,7 @@ internal class CliBasedHatchService private constructor(
))
}
override suspend fun createVirtualEnvironment(basePythonBinaryPath: PythonBinary?, envName: String?): Result<PythonVirtualEnvironment.Existing, PyError> {
override suspend fun createVirtualEnvironment(basePythonBinaryPath: PythonBinary?, envName: String?): PyResult<PythonVirtualEnvironment.Existing> {
val pythonBasedRuntime = basePythonBinaryPath?.let { path ->
hatchRuntime.withBasePythonBinaryPath(path).getOr { return it }
} ?: hatchRuntime
@@ -1,9 +1,12 @@
package com.jetbrains.python
import com.intellij.openapi.util.NlsSafe
import org.jetbrains.annotations.ApiStatus
import kotlin.Result
/**
* Please use [com.jetbrains.python.Result] (low level) or ([com.jetbrains.python.errorProcessing.PyResult] high level)
* [kotlin.Result] failure with user-readable error
*/
@ApiStatus.Obsolete
fun <T> failure(error: @NlsSafe String): Result<T> = Result.Companion.failure(Throwable(error))
@@ -9,10 +9,10 @@ import com.jetbrains.python.mapResult
import org.apache.tuweni.toml.TomlArray
import org.apache.tuweni.toml.TomlTable
import org.jetbrains.annotations.ApiStatus.Internal
import org.toml.lang.psi.TomlKeyValue as PsiTomlKeyValue
import org.toml.lang.psi.TomlTable as PsiTomlTable
import org.toml.lang.psi.TomlLiteral as PsiTomlLiteral
import kotlin.reflect.KClass
import org.toml.lang.psi.TomlKeyValue as PsiTomlKeyValue
import org.toml.lang.psi.TomlLiteral as PsiTomlLiteral
import org.toml.lang.psi.TomlTable as PsiTomlTable
/**
* The error union used by [TomlTable.safeGet], [TomlTable.safeGetRequired] and [TomlTable.safeGetArr].
@@ -1,22 +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.intellij.python.pyproject
import com.jetbrains.python.Result.Companion.failure
import com.jetbrains.python.Result.Companion.success
import com.jetbrains.python.Result
import org.apache.tuweni.toml.Toml
import org.apache.tuweni.toml.TomlParseError
import org.apache.tuweni.toml.TomlTable
import java.io.InputStream
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.toNioPathOrNull
import com.jetbrains.python.Result
import com.jetbrains.python.Result.Companion.success
import com.jetbrains.python.sdk.basePath
import com.jetbrains.python.sdk.findAmongRoots
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.apache.tuweni.toml.Toml
import org.apache.tuweni.toml.TomlParseError
import org.apache.tuweni.toml.TomlTable
import org.jetbrains.annotations.ApiStatus.Internal
import java.io.InputStream
import java.nio.file.Path
/**
@@ -112,7 +111,7 @@ data class PyProjectToml(
val toml = Toml.parse(inputStream)
if (toml.hasErrors()) {
return failure(toml.errors())
return Result.failure(toml.errors())
}
val projectTable = toml.safeGet<TomlTable>("project").getOrIssue(issues)
@@ -1,12 +1,9 @@
package com.intellij.python.pyproject
import com.intellij.python.pyproject.PyProjectIssue.InvalidContact
import com.intellij.python.pyproject.PyProjectIssue.MissingName
import com.intellij.python.pyproject.PyProjectIssue.MissingVersion
import com.intellij.python.pyproject.PyProjectIssue.SafeGetError
import com.intellij.python.pyproject.PyProjectIssue.*
import com.intellij.python.pyproject.TomlTableSafeGetError.RequiredValueMissing
import com.intellij.python.pyproject.TomlTableSafeGetError.UnexpectedType
import com.jetbrains.python.Result.Failure
import com.jetbrains.python.Result
import com.jetbrains.python.getOrThrow
import com.jetbrains.python.isFailure
import org.apache.tuweni.toml.TomlArray
@@ -29,7 +26,7 @@ class PyProjectTomlTest {
// THEN
assert(result.isFailure)
assert((result as Failure).error.isNotEmpty())
assert((result as Result.Failure).error.isNotEmpty())
}
@Test
@@ -3,7 +3,6 @@ package com.jetbrains.python
import com.intellij.openapi.util.NlsSafe
import com.intellij.platform.eel.EelPlatform
import com.intellij.platform.eel.getOr
import com.intellij.platform.eel.path.EelPath
import com.intellij.platform.eel.provider.getEelDescriptor
import com.intellij.platform.eel.provider.utils.EelProcessExecutionResult
import com.intellij.platform.eel.provider.utils.exec
@@ -9,7 +9,7 @@ import com.intellij.python.community.execService.HelperName
import com.intellij.python.community.execService.WhatToExec
import com.jetbrains.python.PythonBinary
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.errorProcessing.failure
import com.jetbrains.python.sdk.PySdkSettings
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
@@ -36,7 +36,7 @@ suspend fun createVenv(
venvDir: Directory,
inheritSitePackages: Boolean = false,
envReader: VirtualEnvReader = VirtualEnvReader.Instance,
): Result<PythonBinary, PyError> {
): PyResult<PythonBinary> {
val execService = ExecService()
val args = buildList {
if (inheritSitePackages) {
@@ -1,6 +1,7 @@
// 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.CommonBundle
import com.intellij.ide.IdeBundle
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
@@ -9,23 +10,48 @@ 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.concurrency.annotations.RequiresEdt
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 com.jetbrains.python.execution.userMessage
import com.jetbrains.python.errorProcessing.ExecError
import com.jetbrains.python.errorProcessing.ExecErrorReason
import com.jetbrains.python.errorProcessing.MessageError
import com.jetbrains.python.errorProcessing.PyError
import org.jetbrains.annotations.ApiStatus
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
*/
@ApiStatus.Internal
@RequiresEdt
fun showErrorDialog(
project: Project?,
execError: PyError,
) {
when (execError) {
is ExecError -> {
showProcessExecutionErrorDialog(project, execError)
}
is MessageError -> {
Messages.showErrorDialog(execError.message, CommonBundle.message("title.error"))
}
}
}
/**
* @throws IllegalStateException if [project] is not `null` and it is disposed
*/
@ApiStatus.Internal
@RequiresEdt
fun showProcessExecutionErrorDialog(
project: Project?,
exception: PyExecutionFailure,
execError: ExecError,
) {
check(project == null || !project.isDisposed)
@@ -34,15 +60,17 @@ fun showProcessExecutionErrorDialog(
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, "\n", exception.userMessage, null)
val command = execError.command.joinToString(" ")
when (val err = execError.errorReason) {
is ExecErrorReason.CantStart -> {
appendProcessOutput(command, err.cantExecProcessError, execError.message, null)
}
is FailureReason.ExecutionFailed -> {
val output = err.output
appendProcessOutput(command, output.stdout, output.stderr, output.exitCode)
is ExecErrorReason.UnexpectedProcessTermination -> {
appendProcessOutput(command, err.stdout, err.stderr, err.exitCode)
}
ExecErrorReason.Timeout -> {
appendProcessOutput(command, "Timeout", "\n", null)
}
}
@@ -64,7 +92,7 @@ fun showProcessExecutionErrorDialog(
object : DialogWrapper(project) {
init {
init()
title = exception.additionalMessage ?: errorMessageText
title = execError.additionalMessageToUser ?: errorMessageText
}
override fun createActions(): Array<Action> = arrayOf(okAction)
@@ -8,7 +8,7 @@ import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.NlsSafe
import com.intellij.python.hatch.*
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.resolvePythonBinary
import com.jetbrains.python.sdk.createSdk
import com.jetbrains.python.sdk.persist
@@ -19,7 +19,7 @@ import java.nio.file.Path
import kotlin.io.path.name
@ApiStatus.Internal
suspend fun HatchVirtualEnvironment.createSdk(workingDirectoryPath: Path, module: Module?): Result<Sdk, PyError> {
suspend fun HatchVirtualEnvironment.createSdk(workingDirectoryPath: Path, module: Module?): PyResult<Sdk> {
val existingPythonEnvironment = pythonVirtualEnvironment as? PythonVirtualEnvironment.Existing
?: return Result.failure(BasePythonExecutableNotFoundHatchError(null as String?))
val pythonHomePath = pythonVirtualEnvironment?.pythonHomePath
@@ -8,7 +8,7 @@ import com.intellij.openapi.vfs.VirtualFile
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.jetbrains.python.PyBundle
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.sdk.ModuleOrProject
import com.jetbrains.python.sdk.add.v2.PySdkCreator
@@ -24,7 +24,7 @@ import kotlinx.coroutines.launch
class PyV3BaseProjectSettings(var createGitRepository: Boolean = false) {
lateinit var sdkCreator: PySdkCreator
suspend fun generateAndGetSdk(module: Module, baseDir: VirtualFile, supportsNotEmptyModuleStructure: Boolean = false): Result<Pair<Sdk, InterpreterStatisticsInfo>, PyError> = coroutineScope {
suspend fun generateAndGetSdk(module: Module, baseDir: VirtualFile, supportsNotEmptyModuleStructure: Boolean = false): PyResult<Pair<Sdk, InterpreterStatisticsInfo>> = coroutineScope {
val project = module.project
if (createGitRepository) {
launch(CoroutineName("Generating git") + Dispatchers.IO) {
@@ -43,7 +43,7 @@ class PyV3BaseProjectSettings(var createGitRepository: Boolean = false) {
return@coroutineScope Result.success(Pair(sdk, interpreterStatistics))
}
private suspend fun getSdkAndInterpreter(module: Module): Result<Pair<Sdk, InterpreterStatisticsInfo>, PyError> =
private suspend fun getSdkAndInterpreter(module: Module): PyResult<Pair<Sdk, InterpreterStatisticsInfo>> =
sdkCreator.getSdk(ModuleOrProject.ModuleAndProject(module))
@@ -4,8 +4,7 @@ package com.jetbrains.python.newProjectWizard
import com.intellij.openapi.module.Module
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.vfs.VirtualFile
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.PyResult
import org.jetbrains.annotations.CheckReturnValue
/**
@@ -26,5 +25,5 @@ fun interface PyV3ProjectTypeSpecificSettings {
* Returns error if generation failed due to execution error
*/
@CheckReturnValue
suspend fun generateProject(module: Module, baseDir: VirtualFile, sdk: Sdk): Result<Unit, PyError>
suspend fun generateProject(module: Module, baseDir: VirtualFile, sdk: Sdk): PyResult<Unit>
}
@@ -1,4 +1,4 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// 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.packaging;
import com.google.common.collect.Lists;
@@ -9,8 +9,8 @@ import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.jetbrains.python.PySdkBundle;
import com.jetbrains.python.sdk.PythonSdkUtil;
import com.jetbrains.python.venvReader.VirtualEnvReader;
import com.jetbrains.python.sdk.flavors.PyCondaRunKt;
import com.jetbrains.python.venvReader.VirtualEnvReader;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -51,7 +51,7 @@ public class PyCondaPackageManagerImpl extends PyPackageManagerImpl {
final Sdk sdk = getSdk();
final String path = getCondaDirectory();
if (path == null) throw new PyExecutionException(PySdkBundle.message("python.sdk.conda.dialog.empty.conda.name", sdk.getHomePath()), command, arguments);
if (path == null) throw new PyExecutionException(PySdkBundle.message("python.sdk.conda.dialog.empty.conda.name", sdk.getHomePath()));
final ArrayList<String> parameters = Lists.newArrayList(command, "-p", path);
parameters.addAll(arguments);
@@ -123,8 +123,8 @@ public class PyCondaPackageManagerImpl extends PyPackageManagerImpl {
PyPackage pkg = parsePackaging(line,
"=",
false,
PySdkBundle.message("python.sdk.conda.dialog.invalid.conda.output.format"),
"conda");
PySdkBundle.message("python.sdk.conda.dialog.invalid.conda.output.format")
);
if (pkg != null) {
packages.add(pkg);
}
@@ -135,14 +135,13 @@ public class PyCondaPackageManagerImpl extends PyPackageManagerImpl {
public static @NotNull String createVirtualEnv(@Nullable String condaExecutable, @NotNull String destinationDir,
@NotNull String version) throws ExecutionException {
if (condaExecutable == null) {
throw new PyExecutionException(PySdkBundle.message("python.sdk.conda.dialog.cannot.find.conda"), "Conda", Collections.emptyList(),
new ProcessOutput());
throw new PyExecutionException(PySdkBundle.message("python.sdk.conda.dialog.cannot.find.conda"));
}
final ArrayList<String> parameters = Lists.newArrayList("create", "-p", destinationDir, "-y", "python=" + version);
PyCondaRunKt.runConda(condaExecutable, parameters);
final Path binary = VirtualEnvReader.getInstance().findPythonInPythonRoot(Path.of(destinationDir));
final Path binary = VirtualEnvReader.getInstance().findPythonInPythonRoot(Path.of(destinationDir));
final String binaryFallback = destinationDir + File.separator + "bin" + File.separator + "python";
return (binary != null) ? binary.toString() : binaryFallback;
}
@@ -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.packaging
import com.intellij.execution.process.ProcessOutput
import com.jetbrains.python.errorProcessing.ExecError
import com.jetbrains.python.errorProcessing.ExecErrorReason
import com.jetbrains.python.errorProcessing.MessageError
import java.io.IOException
/**
* A temporary hack for some outdated code (see usages), do not use in a new code. Stay away from [PyExecutionException]
*/
internal fun PyExecutionException.copyWith(newCommand: String, newArgs: List<String>): PyExecutionException =
when (val err = pyError) {
is ExecError -> {
when (val reason = err.errorReason) {
is ExecErrorReason.CantStart -> {
PyExecutionException(IOException(reason.cantExecProcessError), err.additionalMessageToUser, newCommand, newArgs, fixes)
}
ExecErrorReason.Timeout -> {
val command = arrayOf(newCommand) + newArgs.toTypedArray()
PyExecutionException(ExecError(command, ExecErrorReason.Timeout, err.additionalMessageToUser))
}
is ExecErrorReason.UnexpectedProcessTermination -> {
val output = ProcessOutput(reason.stdout, reason.stderr, reason.exitCode, false, false)
PyExecutionException(err.additionalMessageToUser, newCommand, newArgs, output, fixes)
}
}
}
is MessageError -> error("Error ${err.message} has no command, command can't be changed")
}
@@ -42,7 +42,8 @@ import static com.jetbrains.python.sdk.PySdkExtKt.showSdkExecutionException;
*/
@Deprecated(forRemoval = true)
public class PyPackageManagerImpl extends PyPackageManagerImplBase {
private static final String LEGACY_VIRTUALENV_ZIPAPP_NAME = "virtualenv-20.13.0.pyz"; // virtualenv used to create virtual environments for python 2.7 & 3.6
private static final String LEGACY_VIRTUALENV_ZIPAPP_NAME = "virtualenv-20.13.0.pyz";
// virtualenv used to create virtual environments for python 2.7 & 3.6
private static final Logger LOG = Logger.getInstance(PyPackageManagerImpl.class);
@@ -108,7 +109,7 @@ public class PyPackageManagerImpl extends PyPackageManagerImplBase {
for (PyRequirement req : requirements) {
simplifiedArgs.addAll(req.getInstallOptions());
}
throw e.copyWith("pip", makeSafeToDisplayCommand(simplifiedArgs));
throw PyExecutionExceptionExtKt.copyWith(e, "pip", makeSafeToDisplayCommand(simplifiedArgs));
}
finally {
LOG.debug("Packages cache is about to be refreshed because these requirements were installed: " + requirements);
@@ -134,7 +135,7 @@ public class PyPackageManagerImpl extends PyPackageManagerImplBase {
getHelperResult(args, !canModify, true);
}
catch (PyExecutionException e) {
throw e.copyWith("pip", args);
throw PyExecutionExceptionExtKt.copyWith(e, "pip", args);
}
finally {
LOG.debug("Packages cache is about to be refreshed because these packages were uninstalled: " + packages);
@@ -197,7 +198,7 @@ public class PyPackageManagerImpl extends PyPackageManagerImplBase {
showSdkExecutionException(sdk, e, PySdkBundle.message("python.creating.venv.failed.title"));
}
final Path binary = VirtualEnvReader.getInstance().findPythonInPythonRoot(Paths.get(destinationDir));
final Path binary = VirtualEnvReader.getInstance().findPythonInPythonRoot(Paths.get(destinationDir));
final String binaryFallback = destinationDir + mySeparator + "bin" + mySeparator + "python";
return (binary != null) ? binary.toString() : binaryFallback;
@@ -240,7 +241,7 @@ public class PyPackageManagerImpl extends PyPackageManagerImplBase {
final ProcessOutput output = getPythonProcessOutput(path, args, askForSudo, showProgress, workingDir, pyArgs);
final int exitCode = output.getExitCode();
if (output.isTimeout()) {
throw new PyExecutionException(PySdkBundle.message("python.sdk.packaging.timed.out"), path, args, output);
throw PyExecutionException.createForTimeout(PySdkBundle.message("python.sdk.packaging.timed.out"), path, args);
}
else if (exitCode != 0) {
throw new PyExecutionException(PySdkBundle.message("python.sdk.packaging.non.zero.exit.code", exitCode), path, args, output);
@@ -309,7 +310,7 @@ public class PyPackageManagerImpl extends PyPackageManagerImplBase {
return result;
}
catch (IOException e) {
throw new PyExecutionException(e.getMessage(), helperPath, args);
throw new PyExecutionException(e, null, helperPath, args);
}
}
}
@@ -1,4 +1,4 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// 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.packaging;
import com.intellij.execution.ExecutionException;
@@ -15,7 +15,8 @@ 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.errorProcessing.ExecError;
import com.jetbrains.python.errorProcessing.ExecErrorReason;
import com.jetbrains.python.packaging.repository.PyPackageRepositoryUtil;
import com.jetbrains.python.psi.LanguageLevel;
import com.jetbrains.python.sdk.PyDetectedSdk;
@@ -35,6 +36,10 @@ import java.util.regex.Pattern;
import static com.intellij.webcore.packaging.PackageVersionComparator.VERSION_COMPARATOR;
/**
* @deprecated TODO: explain
*/
@Deprecated(forRemoval = true)
public abstract class PyPackageManagerImplBase extends PyPackageManager {
protected static final String SETUPTOOLS_VERSION = "44.1.1";
protected static final String PIP_VERSION = "24.3.1";
@@ -125,11 +130,14 @@ public abstract class PyPackageManagerImplBase extends PyPackageManager {
return setuptoolsPackage != null ? setuptoolsPackage : PyPsiPackageUtil.findPackage(packages, PyPackageUtil.DISTRIBUTE);
}
catch (PyExecutionException e) {
var error = e.getFailureReason();
if (error instanceof FailureReason.ExecutionFailed executionFailed) {
int exitCode = executionFailed.getOutput().getExitCode();
if (exitCode == ERROR_NO_SETUPTOOLS) {
return null;
var pyError = e.getPyError();
if (pyError instanceof ExecError error) {
var errorReason = error.getErrorReason();
if (errorReason instanceof ExecErrorReason.UnexpectedProcessTermination unexpectedProcessTermination) {
int exitCode = unexpectedProcessTermination.getExitCode();
if (exitCode == ERROR_NO_SETUPTOOLS) {
return null;
}
}
}
throw e;
@@ -283,8 +291,8 @@ public abstract class PyPackageManagerImplBase extends PyPackageManager {
PyPackage pkg = parsePackaging(line,
"\t",
true,
PySdkBundle.message("python.sdk.packaging.invalid.output.format"),
PACKAGING_TOOL);
PySdkBundle.message("python.sdk.packaging.invalid.output.format")
);
if (pkg != null) {
packages.add(pkg);
@@ -296,11 +304,10 @@ public abstract class PyPackageManagerImplBase extends PyPackageManager {
protected final @Nullable PyPackage parsePackaging(@NotNull @NonNls String line,
@NotNull @NonNls String separator,
boolean useLocation,
@NotNull @Nls String errorMessage,
@NotNull @NonNls String command) throws PyExecutionException {
@NotNull @Nls String errorMessage) throws PyExecutionException {
List<String> fields = StringUtil.split(line, separator);
if (fields.size() < 3) {
throw new PyExecutionException(errorMessage, command, List.of());
throw new PyExecutionException(errorMessage);
}
final String name = fields.get(0);
@@ -1,4 +1,4 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// 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.packaging;
import com.intellij.execution.ExecutionException;
@@ -23,6 +23,8 @@ import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.errorProcessing.ExecError;
import com.jetbrains.python.errorProcessing.PyError;
import com.jetbrains.python.packaging.management.PythonPackagesInstaller;
import com.jetbrains.python.packaging.ui.PyPackageManagementService;
import org.jetbrains.annotations.Nls;
@@ -184,7 +186,15 @@ public final class PyPackageManagerUI {
((InstallTask)this).myRequirements,
req -> ContainerUtil.map(req.getInstallOptions(), option -> Pair.create(option, req.getName()))) : null;
final List<String> packageManagerArguments = exceptions.stream()
.flatMap(e -> (e instanceof PyExecutionException) ? ((PyExecutionException)e).getArgs().stream() : null)
.flatMap(e -> {
if (e instanceof PyExecutionException pyExecutionException) {
PyError pyError = pyExecutionException.getPyError();
if (pyError instanceof ExecError execError) {
return Arrays.stream(execError.getExeAndArgs().getSecond());
}
}
return null;
})
.toList();
final String packageNames = requirements != null ? requirements.stream()
.filter(req -> packageManagerArguments.contains(req.first))
@@ -1,4 +1,4 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// 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.packaging;
import com.intellij.execution.ExecutionException;
@@ -45,7 +45,11 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
public class PyTargetEnvironmentPackageManager extends PyPackageManagerImplBase {
/**
* @deprecated TODO: explain
*/
@Deprecated(forRemoval = true)
public final class PyTargetEnvironmentPackageManager extends PyPackageManagerImplBase {
private static final Logger LOG = Logger.getInstance(PyTargetEnvironmentPackageManager.class);
@Override
@@ -113,7 +117,7 @@ public class PyTargetEnvironmentPackageManager extends PyPackageManagerImplBase
for (PyRequirement req : requirements) {
simplifiedArgs.addAll(req.getInstallOptions());
}
throw e.copyWith("pip", makeSafeToDisplayCommand(simplifiedArgs));
throw PyExecutionExceptionExtKt.copyWith(e, "pip", makeSafeToDisplayCommand(simplifiedArgs));
}
finally {
LOG.debug("Packages cache is about to be refreshed because these requirements were installed: " + requirements);
@@ -158,7 +162,7 @@ public class PyTargetEnvironmentPackageManager extends PyPackageManagerImplBase
getPythonProcessResult(pythonExecution, !canModify, true, targetEnvironmentRequest);
}
catch (PyExecutionException e) {
throw e.copyWith("pip", args);
throw PyExecutionExceptionExtKt.copyWith(e, "pip", args);
}
finally {
LOG.debug("Packages cache is about to be refreshed because these packages were uninstalled: " + packages);
@@ -251,7 +255,7 @@ public class PyTargetEnvironmentPackageManager extends PyPackageManagerImplBase
int exitCode = processOutput.getExitCode();
if (processOutput.isTimeout()) {
// TODO [targets] Make cancellable right away?
throw new PyExecutionException(PySdkBundle.message("python.sdk.packaging.timed.out"), path, args, processOutput);
throw PyExecutionException.createForTimeout(PySdkBundle.message("python.sdk.packaging.timed.out"), path, args);
}
else if (exitCode != 0) {
throw new PyExecutionException(PySdkBundle.message("python.sdk.packaging.non.zero.exit.code", exitCode), path, args, processOutput);
@@ -347,7 +351,7 @@ public class PyTargetEnvironmentPackageManager extends PyPackageManagerImplBase
catch (IOException e) {
String exePath = localCommandLine.getExePath();
List<String> args = localCommandLine.getCommandLineList(exePath);
throw new PyExecutionException(e.getMessage(), exePath, args);
throw new PyExecutionException(e, null, exePath, args);
}
}
@@ -1,4 +1,4 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@file:JvmName("PythonPackageManagerExt")
package com.jetbrains.python.packaging.management
@@ -113,7 +113,7 @@ suspend fun PythonPackageManager.runPackagingTool(
}
if (result.isTimeout) {
throw PyExecutionException(PySdkBundle.message("python.sdk.packaging.timed.out"), helperPath, args, result)
throw PyExecutionException.createForTimeout(PySdkBundle.message("python.sdk.packaging.timed.out"), helperPath, args)
}
return result.stdout
@@ -1,9 +1,8 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// 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.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;
@@ -20,7 +19,8 @@ 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.errorProcessing.ExecError;
import com.jetbrains.python.errorProcessing.ExecErrorReason;
import com.jetbrains.python.packaging.*;
import com.jetbrains.python.packaging.PyPIPackageUtil.PackageDetails;
import com.jetbrains.python.packaging.requirement.PyRequirementRelation;
@@ -33,7 +33,10 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -318,15 +321,17 @@ public class PyPackageManagementService extends PackageManagementServiceEx {
private static @NotNull PyPackageInstallationErrorDescription createDescription(@NotNull ExecutionException e,
@Nullable Sdk sdk,
@Nullable String packageName) {
if (e instanceof PyExecutionException pyExecEx && pyExecEx.getFailureReason() instanceof FailureReason.ExecutionFailed execFailed) {
var ee = execFailed.getOutput();
final String stdout = ee.getStdout();
if (e instanceof PyExecutionException pyExecEx &&
pyExecEx.getPyError() instanceof ExecError execError &&
execError.getErrorReason() instanceof ExecErrorReason.UnexpectedProcessTermination execFailed) {
final String stdout = execFailed.getStdout();
final String stdoutCause = findErrorCause(stdout);
final String stderrCause = findErrorCause(ee.getStderr());
final String stderrCause = findErrorCause(execFailed.getStderr());
final String cause = stdoutCause != null ? stdoutCause : stderrCause;
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(),
final String command = StringUtil.join(execError.getCommand(), " ");
return new PyPackageInstallationErrorDescription(message, command,
stdout.isEmpty() ? execFailed.getStderr() : stdout + "\n" + execFailed.getStderr(),
findErrorSolution(pyExecEx, cause, sdk), packageName, sdk);
}
else {
@@ -334,30 +339,33 @@ public class PyPackageManagementService extends PackageManagementServiceEx {
}
}
private static @Nullable @DetailedDescription String findErrorSolution(@NotNull PyExecutionException e,
private static @Nullable @DetailedDescription String findErrorSolution(@NotNull PyExecutionException executionException,
@Nullable String cause,
@Nullable Sdk sdk) {
if (cause != null) {
if (StringUtil.containsIgnoreCase(cause, "SyntaxError")) {
final LanguageLevel languageLevel = PythonSdkType.getLanguageLevelForSdk(sdk);
return PySdkBundle.message("python.sdk.use.python.version.supported.by.this.package", languageLevel);
if (executionException.getPyError() instanceof ExecError e) {
if (cause != null) {
if (StringUtil.containsIgnoreCase(cause, "SyntaxError")) {
final LanguageLevel languageLevel = PythonSdkType.getLanguageLevelForSdk(sdk);
return PySdkBundle.message("python.sdk.use.python.version.supported.by.this.package", languageLevel);
}
}
if (e.getErrorReason() instanceof ExecErrorReason.UnexpectedProcessTermination unexpectedProcessTermination) {
if (SystemInfo.isLinux && (containsInOutput(unexpectedProcessTermination, "pyconfig.h") || containsInOutput(
unexpectedProcessTermination, "Python.h"))) {
return PySdkBundle.message("python.sdk.check.python.development.packages.installed");
}
}
if ("pip".equals(e.getCommand()[0]) && sdk != null) {
return PySdkBundle.message("python.sdk.try.to.run.command.from.system.terminal", sdk.getHomePath());
}
}
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) {
return PySdkBundle.message("python.sdk.try.to.run.command.from.system.terminal", sdk.getHomePath());
}
return null;
}
private static boolean containsInOutput(@NotNull ProcessOutput e, @NotNull String text) {
private static boolean containsInOutput(@NotNull ExecErrorReason.UnexpectedProcessTermination e, @NotNull String text) {
return StringUtil.containsIgnoreCase(e.getStdout(), text) || StringUtil.containsIgnoreCase(e.getStderr(), text);
}
@@ -16,7 +16,8 @@ import com.intellij.platform.util.progress.withProgressText
import com.intellij.python.community.impl.venv.createVenv
import com.intellij.python.community.services.systemPython.SystemPythonService
import com.jetbrains.python.*
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.MessageError
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.errorProcessing.failure
import com.jetbrains.python.sdk.configurePythonSdk
import com.jetbrains.python.sdk.createSdk
@@ -44,7 +45,7 @@ suspend fun createVenvAndSdk(
confirmInstallation: suspend () -> Boolean = { true },
systemPythonService: SystemPythonService = SystemPythonService(),
explicitProjectPath: VirtualFile? = null,
): Result<Sdk, PyError> {
): PyResult<Sdk> {
val vfsProjectPath = withContext(Dispatchers.IO) {
explicitProjectPath
?: (project.modules.firstOrNull()?.let { module -> ModuleRootManager.getInstance(module).contentRoots.firstOrNull() }
@@ -114,7 +115,7 @@ private suspend fun findExistingVenv(
private suspend fun getSystemPython(
confirmInstallation: suspend () -> Boolean,
pythonService: SystemPythonService,
): Result<PythonBinary, PyError.Message> {
): Result<PythonBinary, MessageError> {
// First, find the latest python according to strategy
@@ -1,4 +1,4 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// 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.sdk
import com.intellij.execution.ExecutionException
@@ -52,7 +52,12 @@ internal suspend fun runCommandLine(commandLine: GeneralCommandLine): Result<Str
)
}
catch (e: ExecutionException) {
return Result.failure(PyExecutionException(e.localizedMessage, commandLine.exePath, commandLine.parametersList.array.toList()))
return Result.failure(PyExecutionException(
startException = e.toIOException(),
additionalMessage = null,
command = commandLine.exePath,
args = commandLine.parametersList.list
))
}
}
@@ -1,4 +1,4 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// 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.sdk.add
import com.intellij.CommonBundle
@@ -25,21 +25,11 @@ import com.intellij.util.ui.JBUI
import com.jetbrains.python.PyBundle
import com.jetbrains.python.icons.PythonIcons
import com.jetbrains.python.packaging.PyExecutionException
import com.jetbrains.python.sdk.PreferredSdkComparator
import com.jetbrains.python.sdk.PythonSdkType
import com.jetbrains.python.sdk.add.v1.CreateSdkInterrupted
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.v1.PyAddSystemWideInterpreterPanel
import com.jetbrains.python.sdk.add.v1.doCreateSouthPanel
import com.jetbrains.python.showProcessExecutionErrorDialog
import com.jetbrains.python.sdk.add.v1.swipe
import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.add.PyAddSdkDialog.Companion.show
import com.jetbrains.python.sdk.add.v1.*
import com.jetbrains.python.sdk.conda.PyCondaSdkCustomizer
import com.jetbrains.python.sdk.detectVirtualEnvs
import com.jetbrains.python.sdk.isAssociatedWithModule
import com.jetbrains.python.sdk.sdkSeemsValid
import com.jetbrains.python.showErrorDialog
import java.awt.CardLayout
import java.awt.event.ActionEvent
import java.io.IOException
@@ -169,7 +159,7 @@ class PyAddSdkDialog private constructor(
panel.addStateListener(object : PyAddSdkStateListener {
override fun onComponentChanged() {
com.jetbrains.python.sdk.add.v1.show(mainPanel, panel.component)
show(mainPanel, panel.component)
selectedPanel?.let { updateWizardActionButtons(it) }
}
@@ -311,7 +301,7 @@ class PyAddSdkDialog private constructor(
catch (e: Exception) {
val cause = ExceptionUtil.findCause(e, PyExecutionException::class.java)
if (cause != null) {
showProcessExecutionErrorDialog(project, cause)
showErrorDialog(project, cause.pyError)
return
}
throw e
@@ -1,4 +1,4 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// 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.sdk.add.v1
import com.intellij.CommonBundle
@@ -33,7 +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.showErrorDialog
import com.jetbrains.python.target.PythonLanguageRuntimeConfiguration
import java.awt.CardLayout
import java.awt.Component
@@ -44,12 +44,14 @@ import javax.swing.JPanel
/**
* The panel that is supposed to be used both for local and non-local target-based versions of "New Interpreter" dialog.
*/
internal class PyAddTargetBasedSdkPanel(private val project: Project?,
private val module: Module?,
private val existingSdks: List<Sdk>,
private val targetSupplier: Supplier<TargetEnvironmentConfiguration>?,
private val config: PythonLanguageRuntimeConfiguration,
private val introspectable: LanguageRuntimeType.Introspectable?) : Disposable {
internal class PyAddTargetBasedSdkPanel(
private val project: Project?,
private val module: Module?,
private val existingSdks: List<Sdk>,
private val targetSupplier: Supplier<TargetEnvironmentConfiguration>?,
private val config: PythonLanguageRuntimeConfiguration,
private val introspectable: LanguageRuntimeType.Introspectable?,
) : Disposable {
private val mainPanel: JPanel = JPanel(JBCardLayout())
private var selectedPanel: PyAddSdkView? = null
@@ -177,7 +179,7 @@ internal class PyAddTargetBasedSdkPanel(private val project: Project?,
Messages.showErrorDialog(e.localizedMessage, CommonBundle.message("title.error"))
}
else {
showProcessExecutionErrorDialog(project, cause)
showErrorDialog(project, cause.pyError)
}
return
}
@@ -19,7 +19,7 @@ import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.PythonHelpersLocator
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.ErrorSink
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.errorProcessing.emit
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.sdk.*
@@ -75,7 +75,7 @@ internal abstract class CustomNewEnvironmentCreator(
}
}
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Result<Sdk, PyError> {
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): PyResult<Sdk> {
savePathToExecutableToProperties(null)
// todo think about better error handling
@@ -201,7 +201,7 @@ internal abstract class CustomNewEnvironmentCreator(
*/
internal abstract fun savePathToExecutableToProperties(path: Path?)
protected abstract suspend fun setupEnvSdk(project: Project, module: Module?, baseSdks: List<Sdk>, projectPath: String, homePath: String?, installPackages: Boolean): Result<Sdk, PyError>
protected abstract suspend fun setupEnvSdk(project: Project, module: Module?, baseSdks: List<Sdk>, projectPath: String, homePath: String?, installPackages: Boolean): PyResult<Sdk>
internal abstract suspend fun detectExecutable()
@@ -1,4 +1,4 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// 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.sdk.add.v2
import com.intellij.ide.util.PropertiesComponent
@@ -7,14 +7,13 @@ import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.util.text.nullize
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.errorProcessing.asPythonResult
import com.jetbrains.python.sdk.pipenv.pipEnvPath
import com.jetbrains.python.sdk.pipenv.setupPipEnvSdkUnderProgress
import com.jetbrains.python.statistics.InterpreterType
import java.nio.file.Path
import kotlin.io.path.pathString
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.asPythonResult
internal class EnvironmentCreatorPip(model: PythonMutableTargetAddInterpreterModel) : CustomNewEnvironmentCreator("pipenv", model) {
override val interpreterType: InterpreterType = InterpreterType.PIPENV
@@ -26,7 +25,7 @@ internal class EnvironmentCreatorPip(model: PythonMutableTargetAddInterpreterMod
PropertiesComponent.getInstance().pipEnvPath = savingPath
}
override suspend fun setupEnvSdk(project: Project, module: Module?, baseSdks: List<Sdk>, projectPath: String, homePath: String?, installPackages: Boolean): Result<Sdk, PyError> =
override suspend fun setupEnvSdk(project: Project, module: Module?, baseSdks: List<Sdk>, projectPath: String, homePath: String?, installPackages: Boolean): PyResult<Sdk> =
setupPipEnvSdkUnderProgress(project, module, baseSdks, projectPath, homePath, installPackages).asPythonResult()
override suspend fun detectExecutable() {
@@ -15,7 +15,7 @@ import com.intellij.ui.dsl.builder.components.validationTooltip
import com.intellij.util.ui.showingScope
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.errorProcessing.ErrorSink
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.errorProcessing.failure
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.newProjectWizard.collector.PythonNewProjectWizardCollector
@@ -155,7 +155,7 @@ class EnvironmentCreatorVenv(model: PythonMutableTargetAddInterpreterModel) : Py
return currentName.removeSuffix(digitSuffix) + newSuffix
}
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): com.jetbrains.python.Result<Sdk, PyError> =
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): PyResult<Sdk> =
// todo remove project path, or move to controller
try {
val venvPath = Path.of(model.state.venvPath.get())
@@ -4,7 +4,7 @@ package com.jetbrains.python.sdk.add.v2
import com.intellij.openapi.module.Module
import com.intellij.openapi.projectRoots.Sdk
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.sdk.ModuleOrProject
@@ -12,7 +12,7 @@ interface PySdkCreator {
/**
* Error is shown to user. Do not catch all exceptions, only return exceptions valuable to user
*/
suspend fun getSdk(moduleOrProject: ModuleOrProject): Result<Pair<Sdk, InterpreterStatisticsInfo>, PyError>
suspend fun getSdk(moduleOrProject: ModuleOrProject): PyResult<Pair<Sdk, InterpreterStatisticsInfo>>
/**
* Creates the Python module structure using tools (uv, poetry, hatch, etc) within the given project module.
@@ -46,6 +46,6 @@ interface PySdkCreator {
* ├── README.md
* └── pyproject.toml
*/
suspend fun createPythonModuleStructure(module: Module): Result<Unit, PyError> = Result.success(Unit)
suspend fun createPythonModuleStructure(module: Module): PyResult<Unit> = Result.success(Unit)
}
@@ -4,11 +4,11 @@ package com.jetbrains.python.sdk.add.v2
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.io.toNioPathOrNull
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.ErrorSink
import com.jetbrains.python.sdk.ModuleOrProject
import com.jetbrains.python.venvReader.VirtualEnvReader
import com.jetbrains.python.sdk.rootManager
import com.jetbrains.python.sdk.service.PySdkService.Companion.pySdkService
import com.jetbrains.python.errorProcessing.ErrorSink
import com.jetbrains.python.venvReader.VirtualEnvReader
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
@@ -25,7 +25,7 @@ import com.intellij.util.ui.showingScope
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.ErrorSink
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.errorProcessing.asPythonResult
import com.jetbrains.python.getOrThrow
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
@@ -206,14 +206,14 @@ internal class PythonAddNewEnvironmentPanel(
}.getOrThrow().first
}
override suspend fun createPythonModuleStructure(module: Module): Result<Unit, PyError> {
override suspend fun createPythonModuleStructure(module: Module): PyResult<Unit> {
return when (selectedMode.get()) {
CUSTOM -> custom.currentSdkManager.createPythonModuleStructure(module)
else -> Result.success(Unit)
}
}
override suspend fun getSdk(moduleOrProject: ModuleOrProject): Result<Pair<Sdk, InterpreterStatisticsInfo>, PyError> {
override suspend fun getSdk(moduleOrProject: ModuleOrProject): PyResult<Pair<Sdk, InterpreterStatisticsInfo>> {
model.navigator.saveLastState()
val sdk = when (selectedMode.get()) {
PROJECT_VENV -> {
@@ -8,7 +8,7 @@ import com.intellij.ui.dsl.builder.Panel
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.ErrorSink
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.sdk.ModuleOrProject
import com.jetbrains.python.sdk.moduleIfExists
@@ -37,7 +37,7 @@ class PythonExistingEnvironmentSelector(model: PythonAddInterpreterModel, privat
comboBox.setItems(model.allInterpreters.map { sortForExistingEnvironment(it, moduleOrProject?.moduleIfExists) })
}
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Result<Sdk, PyError> {
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): PyResult<Sdk> {
// todo error handling, nullability issues
return Result.success(setupSdkIfDetected(model.state.selectedInterpreter.get()!!, model.existingSdks)!!)
}
@@ -18,23 +18,23 @@ import com.intellij.openapi.ui.validation.DialogValidationRequestor
import com.intellij.openapi.ui.validation.WHEN_PROPERTY_CHANGED
import com.intellij.openapi.ui.validation.and
import com.intellij.openapi.wm.IdeFocusManager
import com.intellij.python.hatch.icons.PythonHatchIcons
import com.intellij.ui.dsl.builder.Align
import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.Row
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.ErrorSink
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.icons.PythonIcons
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.psi.icons.PythonPsiApiIcons
import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.pipenv.PIPENV_ICON
import com.jetbrains.python.sdk.poetry.POETRY_ICON
import com.jetbrains.python.sdk.uv.UV_ICON
import com.jetbrains.python.statistics.InterpreterTarget
import com.jetbrains.python.errorProcessing.ErrorSink
import com.jetbrains.python.errorProcessing.PyError
import javax.swing.Icon
import com.intellij.python.hatch.icons.PythonHatchIcons
import com.intellij.ui.dsl.builder.Align
import com.intellij.ui.dsl.builder.Row
import com.jetbrains.python.psi.icons.PythonPsiApiIcons
abstract class PythonAddEnvironment(open val model: PythonAddInterpreterModel) {
@@ -52,9 +52,9 @@ abstract class PythonAddEnvironment(open val model: PythonAddInterpreterModel) {
*
* Error is shown to user. Do not catch all exceptions, only return exceptions valuable to user
*/
abstract suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Result<Sdk, PyError>
abstract suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): PyResult<Sdk>
open suspend fun createPythonModuleStructure(module: Module): Result<Unit, PyError> = Result.success(Unit)
open suspend fun createPythonModuleStructure(module: Module): PyResult<Unit> = Result.success(Unit)
abstract fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo
}
@@ -15,27 +15,16 @@ import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.bindItem
import com.intellij.ui.layout.predicate
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.ErrorSink
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.errorProcessing.asPythonResult
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.sdk.ModuleOrProject
import com.jetbrains.python.sdk.add.v2.*
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
import com.jetbrains.python.statistics.InterpreterCreationMode
import com.jetbrains.python.statistics.InterpreterType
import com.jetbrains.python.errorProcessing.ErrorSink
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.asPythonResult
import com.jetbrains.python.sdk.add.v2.CondaEnvComboBoxListCellRenderer
import com.jetbrains.python.sdk.add.v2.PythonAddInterpreterModel
import com.jetbrains.python.sdk.add.v2.PythonExistingEnvironmentConfigurator
import com.jetbrains.python.sdk.add.v2.PythonInterpreterCreationTargets
import com.jetbrains.python.sdk.add.v2.UNKNOWN_EXECUTABLE
import com.jetbrains.python.sdk.add.v2.createInstallCondaFix
import com.jetbrains.python.sdk.add.v2.detectCondaEnvironmentsOrError
import com.jetbrains.python.sdk.add.v2.displayLoaderWhen
import com.jetbrains.python.sdk.add.v2.executableSelector
import com.jetbrains.python.sdk.add.v2.selectCondaEnvironment
import com.jetbrains.python.sdk.add.v2.toStatisticsField
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
@@ -120,7 +109,7 @@ internal class CondaExistingEnvironmentSelector(model: PythonAddInterpreterModel
//}
}
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Result<Sdk, PyError> =
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): PyResult<Sdk> =
model.selectCondaEnvironment(base = false).asPythonResult()
override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo {
@@ -10,25 +10,18 @@ import com.intellij.ui.dsl.builder.bindItem
import com.intellij.ui.dsl.builder.bindText
import com.intellij.ui.dsl.listCellRenderer.textListCellRenderer
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.ErrorSink
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.errorProcessing.asPythonResult
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.sdk.ModuleOrProject
import com.jetbrains.python.sdk.add.v2.*
import com.jetbrains.python.sdk.conda.condaSupportedLanguages
import com.jetbrains.python.sdk.flavors.conda.NewCondaEnvRequest
import com.jetbrains.python.statistics.InterpreterCreationMode
import com.jetbrains.python.statistics.InterpreterType
import com.jetbrains.python.ui.flow.bindText
import com.jetbrains.python.errorProcessing.ErrorSink
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.asPythonResult
import com.jetbrains.python.sdk.add.v2.PythonInterpreterCreationTargets
import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel
import com.jetbrains.python.sdk.add.v2.PythonNewEnvironmentCreator
import com.jetbrains.python.sdk.add.v2.createInstallCondaFix
import com.jetbrains.python.sdk.add.v2.displayLoaderWhen
import com.jetbrains.python.sdk.add.v2.executableSelector
import com.jetbrains.python.sdk.add.v2.toStatisticsField
internal class CondaNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel) : PythonNewEnvironmentCreator(model) {
@@ -61,7 +54,7 @@ internal class CondaNewEnvironmentCreator(model: PythonMutableTargetAddInterpret
override fun onShown() = Unit
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Result<Sdk, PyError> {
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): PyResult<Sdk> {
return model.createCondaEnvironment(NewCondaEnvRequest.EmptyNamedEnv(pythonVersion.get(), model.state.newCondaEnvName.get())).asPythonResult()
}
@@ -10,7 +10,7 @@ import com.intellij.python.hatch.resolveHatchWorkingDirectory
import com.intellij.ui.dsl.builder.Panel
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.ErrorSink
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.hatch.sdk.createSdk
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.onSuccess
@@ -52,7 +52,7 @@ internal class HatchExistingEnvironmentSelector(
)
}
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Result<Sdk, PyError> {
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): PyResult<Sdk> {
val environment = state.selectedHatchEnv.get()
val existingHatchVenv = environment?.pythonVirtualEnvironment as? PythonVirtualEnvironment.Existing
?: return Result.failure(HatchUIError.HatchEnvironmentIsNotSelected())
@@ -16,7 +16,7 @@ import com.intellij.ui.dsl.builder.Panel
import com.intellij.util.text.nullize
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.ErrorSink
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.hatch.sdk.createSdk
import com.jetbrains.python.onSuccess
import com.jetbrains.python.sdk.add.v2.CustomNewEnvironmentCreator
@@ -58,7 +58,7 @@ internal class HatchNewEnvironmentCreator(
HatchConfiguration.persistPathForTarget(hatchExecutablePath = savingPath)
}
override suspend fun createPythonModuleStructure(module: Module): Result<Unit, PyError> {
override suspend fun createPythonModuleStructure(module: Module): PyResult<Unit> {
val hatchExecutablePath = executable.get().toPath().getOr { return it }
val hatchService = module.getHatchService(hatchExecutablePath = hatchExecutablePath).getOr { return it }
@@ -76,7 +76,7 @@ internal class HatchNewEnvironmentCreator(
return Result.success(Unit)
}
override suspend fun setupEnvSdk(project: Project, module: Module?, baseSdks: List<Sdk>, projectPath: String, homePath: String?, installPackages: Boolean): Result<Sdk, PyError> {
override suspend fun setupEnvSdk(project: Project, module: Module?, baseSdks: List<Sdk>, projectPath: String, homePath: String?, installPackages: Boolean): PyResult<Sdk> {
val hatchEnv = hatchEnvironmentProperty.get()?.hatchEnvironment
?: return Result.failure(HatchUIError.HatchEnvironmentIsNotSelected())
@@ -23,7 +23,10 @@ import com.intellij.ui.dsl.builder.components.validationTooltip
import com.intellij.ui.layout.ComponentPredicate
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.ExecError
import com.jetbrains.python.errorProcessing.MessageError
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.icons.PythonIcons
import com.jetbrains.python.isFailure
import com.jetbrains.python.newProjectWizard.collector.PythonNewProjectWizardCollector
@@ -35,7 +38,7 @@ import org.jetbrains.annotations.Nls
import java.nio.file.Path
import javax.swing.JList
internal sealed class HatchUIError(message: String) : PyError.Message(message) {
internal sealed class HatchUIError(message: String) : MessageError(message) {
class ProjectIsNotSelected : HatchUIError(
message("sdk.create.custom.hatch.error.project.is.not.selected")
)
@@ -53,14 +56,14 @@ internal sealed class HatchUIError(message: String) : PyError.Message(message) {
hatchExecutablePath)
)
class HatchExecutionFailure(execException: ExecException) : HatchUIError(
class HatchExecutionFailure(execError: ExecError) : HatchUIError(
message("sdk.create.custom.hatch.error.execution.failed",
execException.execFailure.command, execException.execFailure.args.joinToString(" ")
execError.command.joinToString(" ")
)
)
}
internal fun String.toPath(): Result<Path, PyError> {
internal fun String.toPath(): PyResult<Path> {
return when (val selectedPath = Path.of(this)) {
null -> Result.failure(HatchUIError.HatchExecutablePathIsNotValid(this))
else -> Result.success(selectedPath)
@@ -148,8 +151,8 @@ private fun Panel.addExecutableSelector(
propertyGraph.dependsOn(hatchErrorMessage, hatchErrorProperty, deleteWhenChildModified = false) {
when (val error = hatchErrorProperty.get()) {
null -> ""
is PyError.Message -> error.message
is PyError.ExecException -> HatchUIError.HatchExecutionFailure(error).message
is MessageError -> error.message
is ExecError -> HatchUIError.HatchExecutionFailure(error).message
}
}
@@ -224,7 +227,7 @@ internal fun Panel.buildHatchFormFields(
@Synchronized
private fun ComboBox<HatchVirtualEnvironment>.syncWithEnvs(
environmentsResult: Result<List<HatchVirtualEnvironment>, PyError>,
environmentsResult: PyResult<List<HatchVirtualEnvironment>>,
isFilterOnlyExisting: Boolean = false,
) {
removeAllItems()
@@ -24,9 +24,8 @@ import com.intellij.python.hatch.getHatchService
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.errorProcessing.ErrorSink
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.errorProcessing.emit
import com.jetbrains.python.failure
import com.jetbrains.python.getOrNull
import com.jetbrains.python.isFailure
import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathFlows
@@ -75,7 +74,7 @@ abstract class PythonAddInterpreterModel(
val manuallyAddedInterpreters: MutableStateFlow<List<PythonSelectableInterpreter>> = MutableStateFlow(emptyList())
private var installable: List<PythonSelectableInterpreter> = emptyList()
val condaEnvironments: MutableStateFlow<List<PyCondaEnv>> = MutableStateFlow(emptyList())
val hatchEnvironmentsResult: MutableStateFlow<com.jetbrains.python.Result<List<HatchVirtualEnvironment>, PyError>?> = MutableStateFlow(
val hatchEnvironmentsResult: MutableStateFlow<PyResult<List<HatchVirtualEnvironment>>?> = MutableStateFlow(
null)
var allInterpreters: StateFlow<List<PythonSelectableInterpreter>> = combine(knownInterpreters, detectedInterpreters,
@@ -145,7 +144,7 @@ abstract class PythonAddInterpreterModel(
suspend fun detectHatchEnvironments(
hatchExecutablePathString: String,
): com.jetbrains.python.Result<List<HatchVirtualEnvironment>, PyError> {
): PyResult<List<HatchVirtualEnvironment>> {
val environmentsResult = withContext(Dispatchers.IO) {
val projectPath = myProjectPathFlows.projectPathWithDefault.first()
val hatchExecutablePath = NioFiles.toPath(hatchExecutablePathString)
@@ -481,7 +480,7 @@ internal suspend fun PythonAddInterpreterModel.detectCondaEnvironmentsOrError(er
internal suspend fun PythonAddInterpreterModel.getBaseCondaOrError(): Result<PyCondaEnv> {
var baseConda = state.baseCondaEnv.get()
if (baseConda != null) return Result.success(baseConda)
detectCondaEnvironments()?.let { return failure(it) }
detectCondaEnvironments()?.let { return com.jetbrains.python.failure(it) }
baseConda = state.baseCondaEnv.get()
return if (baseConda != null) Result.success(baseConda) else failure(message("python.sdk.conda.no.base.env.error"))
return if (baseConda != null) Result.success(baseConda) else com.jetbrains.python.failure(message("python.sdk.conda.no.base.env.error"))
}
@@ -14,33 +14,32 @@ import com.intellij.ui.dsl.builder.bindSelected
import com.intellij.util.text.nullize
import com.jetbrains.python.PyBundle
import com.jetbrains.python.errorProcessing.ErrorSink
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.errorProcessing.asPythonResult
import com.jetbrains.python.newProjectWizard.collector.PythonNewProjectWizardCollector
import com.jetbrains.python.sdk.ModuleOrProject
import com.jetbrains.python.sdk.add.v2.CustomNewEnvironmentCreator
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMethod.SELECT_EXISTING
import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel
import com.jetbrains.python.sdk.add.v2.PythonSelectableInterpreter
import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.POETRY
import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.PYTHON
import com.jetbrains.python.sdk.add.v2.VenvExistenceValidationState.Error
import com.jetbrains.python.sdk.add.v2.VenvExistenceValidationState.Invisible
import com.jetbrains.python.sdk.baseDir
import com.jetbrains.python.sdk.basePath
import com.jetbrains.python.sdk.poetry.*
import com.jetbrains.python.statistics.InterpreterType
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext
import java.nio.file.Path
import kotlin.io.path.pathString
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.asPythonResult
import com.jetbrains.python.newProjectWizard.collector.PythonNewProjectWizardCollector
import com.jetbrains.python.sdk.add.v2.CustomNewEnvironmentCreator
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMethod
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMethod.*
import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel
import com.jetbrains.python.sdk.add.v2.PythonSelectableInterpreter
import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers
import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.*
import com.jetbrains.python.sdk.add.v2.VenvExistenceValidationState.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.io.path.exists
import kotlin.io.path.pathString
internal class EnvironmentCreatorPoetry(model: PythonMutableTargetAddInterpreterModel, private val moduleOrProject: ModuleOrProject?) : CustomNewEnvironmentCreator("poetry", model) {
override val interpreterType: InterpreterType = InterpreterType.POETRY
@@ -108,7 +107,7 @@ internal class EnvironmentCreatorPoetry(model: PythonMutableTargetAddInterpreter
PropertiesComponent.getInstance().poetryPath = savingPath
}
override suspend fun setupEnvSdk(project: Project, module: Module?, baseSdks: List<Sdk>, projectPath: String, homePath: String?, installPackages: Boolean): Result<Sdk, PyError> {
override suspend fun setupEnvSdk(project: Project, module: Module?, baseSdks: List<Sdk>, projectPath: String, homePath: String?, installPackages: Boolean): PyResult<Sdk> {
module?.let { service<PoetryConfigService>().setInProjectEnv(it) }
return setupPoetrySdkUnderProgress(project, module, baseSdks, projectPath, homePath, installPackages).asPythonResult()
}
@@ -141,7 +140,7 @@ internal class EnvironmentCreatorPoetry(model: PythonMutableTargetAddInterpreter
@Service(Service.Level.APP)
@State(name = "PyPoetrySettings", storages = [Storage("pyPoetrySettings.xml")])
private class PoetryConfigService : SerializablePersistentStateComponent<PoetryConfigService.PyPoetrySettingsState>(PyPoetrySettingsState()) {
class PyPoetrySettingsState: BaseState() {
class PyPoetrySettingsState : BaseState() {
var isInProjectEnv = false
}
@@ -7,7 +7,7 @@ import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.vfs.toNioPathOrNull
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.errorProcessing.asPythonResult
import com.jetbrains.python.sdk.ModuleOrProject
import com.jetbrains.python.sdk.add.v2.CustomExistingEnvironmentSelector
@@ -26,7 +26,7 @@ internal class PoetryExistingEnvironmentSelector(model: PythonMutableTargetAddIn
override val executable: ObservableMutableProperty<String> = model.state.poetryExecutable
override val interpreterType: InterpreterType = InterpreterType.POETRY
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Result<Sdk, PyError> {
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): PyResult<Sdk> {
val selectedInterpreter = selectedEnv.get()
ProjectJdkTable.getInstance().allJdks.find { sdk -> sdk.isPoetry && sdk.homePath == selectedInterpreter?.homePath }?.let { return Result.success(it) }
val module = when (moduleOrProject) {
@@ -12,8 +12,7 @@ import com.intellij.platform.ide.progress.TaskCancellation
import com.intellij.platform.ide.progress.withModalProgress
import com.intellij.python.community.impl.venv.createVenv
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.failure
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.conda.createCondaSdkFromExistingEnv
import com.jetbrains.python.sdk.conda.isConda
@@ -24,7 +23,7 @@ import java.nio.file.Path
// todo should it be overriden for targets?
suspend fun PythonMutableTargetAddInterpreterModel.setupVirtualenv(venvPath: Path, projectPath: Path, moduleOrProject: ModuleOrProject?): com.jetbrains.python.Result<Sdk, PyError> {
suspend fun PythonMutableTargetAddInterpreterModel.setupVirtualenv(venvPath: Path, projectPath: Path, moduleOrProject: ModuleOrProject?): PyResult<Sdk> {
val baseSdk = state.baseInterpreter.get()!!
@@ -84,7 +83,8 @@ suspend fun PythonAddInterpreterModel.selectCondaEnvironment(base: Boolean): Res
getBaseCondaOrError()
}
else {
state.selectedCondaEnv.get()?.let { Result.success(it) } ?: failure(message("python.sdk.conda.no.env.selected.error"))
state.selectedCondaEnv.get()?.let { Result.success(it) }
?: com.jetbrains.python.failure(message("python.sdk.conda.no.env.selected.error"))
}
.getOrElse { return Result.failure(it) }
.envIdentity
@@ -6,24 +6,24 @@ import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.util.text.nullize
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.errorProcessing.asPythonResult
import com.jetbrains.python.newProjectWizard.collector.PythonNewProjectWizardCollector
import com.jetbrains.python.sdk.ModuleOrProject
import com.jetbrains.python.sdk.add.v2.CustomNewEnvironmentCreator
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMethod.SELECT_EXISTING
import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel
import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.PYTHON
import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.UV
import com.jetbrains.python.sdk.add.v2.VenvExistenceValidationState
import com.jetbrains.python.sdk.basePath
import com.jetbrains.python.sdk.uv.impl.setUvExecutable
import com.jetbrains.python.sdk.uv.setupNewUvSdkAndEnvUnderProgress
import com.jetbrains.python.statistics.InterpreterType
import com.jetbrains.python.venvReader.tryResolvePath
import java.nio.file.Path
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.asPythonResult
import com.jetbrains.python.newProjectWizard.collector.PythonNewProjectWizardCollector
import com.jetbrains.python.sdk.ModuleOrProject
import com.jetbrains.python.sdk.add.v2.CustomNewEnvironmentCreator
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMethod.*
import com.jetbrains.python.sdk.add.v2.VenvExistenceValidationState
import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel
import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.io.path.exists
@@ -70,10 +70,10 @@ internal class EnvironmentCreatorUv(
setUvExecutable(savingPath)
}
override suspend fun setupEnvSdk(project: Project, module: Module?, baseSdks: List<Sdk>, projectPath: String, homePath: String?, installPackages: Boolean): Result<Sdk, PyError> {
override suspend fun setupEnvSdk(project: Project, module: Module?, baseSdks: List<Sdk>, projectPath: String, homePath: String?, installPackages: Boolean): PyResult<Sdk> {
val workingDir = module?.basePath?.let { tryResolvePath(it) } ?: project.basePath?.let { tryResolvePath(it) }
if (workingDir == null) {
return kotlin.Result.failure<Sdk>(Exception("working dir is not specified for uv environment setup")).asPythonResult()
return Result.failure<Sdk>(Exception("working dir is not specified for uv environment setup")).asPythonResult()
}
val python = homePath?.let { Path.of(it) }
@@ -8,7 +8,7 @@ import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.vfs.toNioPathOrNull
import com.intellij.python.pyproject.PyProjectToml
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.errorProcessing.asPythonResult
import com.jetbrains.python.errorProcessing.failure
import com.jetbrains.python.sdk.ModuleOrProject
@@ -31,7 +31,7 @@ internal class UvExistingEnvironmentSelector(model: PythonMutableTargetAddInterp
override val executable: ObservableMutableProperty<String> = model.state.uvExecutable
override val interpreterType: InterpreterType = InterpreterType.UV
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Result<Sdk, PyError> {
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): PyResult<Sdk> {
val selectedInterpreterPath = tryResolvePath(selectedEnv.get()?.homePath) ?: return failure("No selected interpreter")
val allSdk = ProjectJdkTable.getInstance().allJdks
val existingSdk = allSdk.find { it.homePath == selectedInterpreterPath.pathString }
@@ -18,7 +18,6 @@ import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
import com.intellij.platform.util.progress.RawProgressReporter
import com.jetbrains.python.failure
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.sdk.PythonSdkAdditionalData
import com.jetbrains.python.sdk.PythonSdkType
@@ -129,7 +128,7 @@ suspend fun PyCondaCommand.createCondaSdkAlongWithNewEnv(
val process = PyCondaEnv.createEnv(this, newCondaEnvInfo).getOrElse { return Result.failure(it) }
val error = ProcessHandlerReader(process).runProcessAndGetError(uiContext, reporter)
return error?.let { failure(it) }
return error?.let { com.jetbrains.python.failure(it) }
?: Result.success(
createCondaSdkFromExistingEnv(newCondaEnvInfo.toIdentity(), existingSdks, project)).apply {
onSuccess {
@@ -1,13 +1,15 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// 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.sdk.flavors.conda
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.google.gson.Gson
import com.intellij.execution.target.*
import com.intellij.execution.target.FullPathOnTarget
import com.intellij.execution.target.TargetEnvironmentConfiguration
import com.intellij.execution.target.TargetedCommandLineBuilder
import com.intellij.execution.target.createProcessWithResult
import com.intellij.openapi.projectRoots.Sdk
import com.jetbrains.python.failure
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.sdk.conda.TargetCommandExecutor
import com.jetbrains.python.sdk.conda.createCondaSdkFromExistingEnv
@@ -39,7 +41,7 @@ data class PyCondaEnv(
*/
private suspend fun getEnvsInfo(command: TargetCommandExecutor, fullCondaPathOnTarget: FullPathOnTarget): Result<String> {
val output = command.execute(listOf(fullCondaPathOnTarget, "info", "--envs", "--json")).await()
return if (output.exitCode == 0) Result.success(output.stdout) else failure(output.stderr)
return if (output.exitCode == 0) Result.success(output.stdout) else com.jetbrains.python.failure(output.stderr)
}
/**
@@ -9,6 +9,8 @@ import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.ui.Messages
import com.jetbrains.python.PyBundle
import com.jetbrains.python.errorProcessing.ErrorSink
import com.jetbrains.python.errorProcessing.ExecError
import com.jetbrains.python.errorProcessing.MessageError
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.showProcessExecutionErrorDialog
import kotlinx.coroutines.Dispatchers
@@ -26,10 +28,10 @@ object ShowingMessageErrorSync : ErrorSink {
// Platform doesn't allow dialogs without a lock for now, fix later
writeIntentReadAction {
when (val e = error) {
is PyError.ExecException -> {
showProcessExecutionErrorDialog(null, e.execFailure)
is ExecError -> {
showProcessExecutionErrorDialog(null, e)
}
is PyError.Message -> {
is MessageError -> {
Messages.showErrorDialog(error.message, PyBundle.message("python.error"))
}
}