Python: extract python-specific extensions from exec service to simplify API and make it extendable for intepreters.

Use `ExecService` `api.kt` to exec any binary and extensions from `execService.python/api.kt` for python-specific things (i.e helpers)

GitOrigin-RevId: bb217798a9d1ee886c4b12220ec1f66a5ef08336
This commit is contained in:
Ilya.Kazakevich
2025-06-07 05:00:09 +02:00
committed by intellij-monorepo-bot
parent fd2ad62299
commit 2e14347844
55 changed files with 606 additions and 229 deletions

1
.idea/modules.xml generated
View File

@@ -1045,6 +1045,7 @@
<module fileurl="file://$PROJECT_DIR$/python/intellij.python.community.communityOnly/intellij.python.community.communityOnly.iml" filepath="$PROJECT_DIR$/python/intellij.python.community.communityOnly/intellij.python.community.communityOnly.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-core-impl/intellij.python.community.core.impl.iml" filepath="$PROJECT_DIR$/python/python-core-impl/intellij.python.community.core.impl.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-exec-service/intellij.python.community.execService.iml" filepath="$PROJECT_DIR$/python/python-exec-service/intellij.python.community.execService.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-exec-service/execService.python/intellij.python.community.execService.python.iml" filepath="$PROJECT_DIR$/python/python-exec-service/execService.python/intellij.python.community.execService.python.iml" />
<module fileurl="file://$PROJECT_DIR$/python/impl.helperLocator/intellij.python.community.helpersLocator.iml" filepath="$PROJECT_DIR$/python/impl.helperLocator/intellij.python.community.helpersLocator.iml" />
<module fileurl="file://$PROJECT_DIR$/python/intellij.python.community.impl.iml" filepath="$PROJECT_DIR$/python/intellij.python.community.impl.iml" />
<module fileurl="file://$PROJECT_DIR$/python/huggingFace/intellij.python.community.impl.huggingFace.iml" filepath="$PROJECT_DIR$/python/huggingFace/intellij.python.community.impl.huggingFace.iml" />

View File

@@ -4,19 +4,12 @@
package com.intellij.platform.eel.provider.utils
import com.intellij.openapi.util.IntellijInternalApi
import com.intellij.platform.eel.*
import com.intellij.platform.eel.provider.getEelDescriptor
import com.intellij.platform.eel.EelProcess
import com.intellij.util.io.computeDetached
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
import org.jetbrains.annotations.ApiStatus
import java.io.ByteArrayOutputStream
import java.nio.file.Path
import kotlin.io.path.pathString
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
/**
* To simplify [EelProcessExecutionResult] delegation
@@ -67,24 +60,3 @@ suspend fun EelProcess.awaitProcessResult(): EelProcessExecutionResult {
}
}
}
/**
* Given [this] is a binary, executes it with [args] and returns either [EelExecApi.ExecuteProcessError] (couldn't execute) or
* [ProcessOutput] as a result of the execution.
* If [timeout] elapsed then return value is an error with `null`.
* ```kotlin
* withTimeout(10.seconds) {python.exec("-v")}.getOr{return it}
* ```
*/
@ThrowsChecked(ExecuteProcessException::class)
@ApiStatus.Internal
@ApiStatus.Experimental
suspend fun Path.exec(vararg args: String, timeout: Duration = Int.MAX_VALUE.days): EelProcessExecutionResult {
val process = getEelDescriptor().toEelApi().exec.spawnProcess(pathString, *args).eelIt()
return withTimeoutOrNull(timeout) {
process.awaitProcessResult()
} ?: run {
process.kill()
throw ExecuteProcessException(-1, "Timeout exceeded: $timeout")
}
}

View File

@@ -282,6 +282,7 @@ jvm_library(
"@lib//:http-client",
"@lib//:commons-lang3",
"//python/impl.helperLocator:community-helpersLocator",
"//python/python-exec-service/execService.python",
],
exports = [
"//python/openapi:community",

View File

@@ -180,5 +180,6 @@
<orderEntry type="library" name="http-client" level="project" />
<orderEntry type="library" name="commons-lang3" level="project" />
<orderEntry type="module" module-name="intellij.python.community.helpersLocator" />
<orderEntry type="module" module-name="intellij.python.community.execService.python" />
</component>
</module>

View File

@@ -1,5 +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 org.jetbrains.annotations.ApiStatus
import java.nio.file.Path
/**
@@ -11,3 +13,11 @@ typealias PythonBinary = Path
* python home directory, virtual environment or a base one.
*/
typealias PythonHomePath = Path
/**
* `
* python --version
` *
*/
@ApiStatus.Internal
const val PYTHON_VERSION_ARG: String = "--version"

View File

@@ -48,6 +48,7 @@
- name: intellij.python.community
- name: intellij.python.community.impl
- name: intellij.python.community.execService
- name: intellij.python.community.execService.python
- name: intellij.python.community.impl.installer
- name: intellij.python.pydev
- name: intellij.python.community.impl.venv

View File

@@ -45,6 +45,7 @@ The Python plug-in provides smart editing for Python scripts. The feature set of
<module name="intellij.python.community" loading="embedded"/>
<module name="intellij.python.community.impl" loading="embedded"/>
<module name="intellij.python.community.execService" loading="embedded"/>
<module name="intellij.python.community.execService.python" loading="embedded"/>
<module name="intellij.python.community.impl.installer" loading="embedded"/>
<module name="intellij.python.pydev" loading="embedded"/>
<module name="intellij.python.community.impl.venv" loading="embedded"/>

View File

@@ -4,6 +4,8 @@
<module name="intellij.python.sdk"/>
<module name="intellij.python.community"/>
<module name="intellij.python.community.helpersLocator"/>
<module name="intellij.python.community.execService"/>
<module name="intellij.python.community.execService.python"/>
</dependencies>
<resource-bundle>messages.PyBundle</resource-bundle>

View File

@@ -23,7 +23,6 @@ jvm_library(
"//platform/core-api:core",
"//platform/eel",
"//platform/util/progress",
"//python/impl.helperLocator:community-helpersLocator",
],
runtime_deps = [":community-execService_resources"]
)
@@ -54,7 +53,6 @@ jvm_library(
"@lib//:junit5Pioneer",
"//platform/testFramework/common",
"//platform/util/progress",
"//python/impl.helperLocator:community-helpersLocator",
],
runtime_deps = [":community-execService_resources"]
)

View File

@@ -0,0 +1,2 @@
Execution services to run code locally or remotely. Python-agnostic
Start with `api.kt`, do not touch "advanced" functions.

View File

@@ -0,0 +1,66 @@
### auto-generated section `build intellij.python.community.execService.python` start
load("@rules_jvm//:jvm.bzl", "jvm_library", "jvm_resources", "jvm_test")
jvm_resources(
name = "execService.python_resources",
files = glob(["resources/**/*"]),
strip_prefix = "resources"
)
jvm_library(
name = "execService.python",
module_name = "intellij.python.community.execService.python",
visibility = ["//visibility:public"],
srcs = glob(["src/**/*.kt", "src/**/*.java"], allow_empty = True),
deps = [
"@lib//:kotlin-stdlib",
"//python/python-exec-service:community-execService",
"//python/openapi:community",
"@lib//:jetbrains-annotations",
"@lib//:kotlinx-coroutines-core",
"//platform/eel",
"//platform/eel-provider",
"//python/impl.helperLocator:community-helpersLocator",
"//platform/util",
"//platform/core-api:core",
"//python/python-sdk:sdk",
],
runtime_deps = [":execService.python_resources"]
)
jvm_library(
name = "execService.python_test_lib",
visibility = ["//visibility:public"],
srcs = glob(["tests/**/*.kt", "tests/**/*.java"], allow_empty = True),
associates = [":execService.python"],
deps = [
"@lib//:kotlin-stdlib",
"//python/python-exec-service:community-execService",
"//python/python-exec-service:community-execService_test_lib",
"//python/openapi:community",
"//python/openapi:community_test_lib",
"@lib//:jetbrains-annotations",
"@lib//:kotlinx-coroutines-core",
"//platform/eel",
"//platform/eel-provider",
"//python/impl.helperLocator:community-helpersLocator",
"@lib//:junit5",
"//platform/testFramework/junit5",
"//platform/testFramework/junit5:junit5_test_lib",
"//python/junit5Tests-framework:community-junit5Tests-framework_test_lib",
"//platform/testFramework/junit5/eel",
"//platform/testFramework/junit5/eel:eel_test_lib",
"@lib//:junit5Params",
"//platform/util",
"//platform/core-api:core",
"//python/python-sdk:sdk",
"//python/python-sdk:sdk_test_lib",
],
runtime_deps = [":execService.python_resources"]
)
jvm_test(
name = "execService.python_test",
runtime_deps = [":execService.python_test_lib"]
)
### auto-generated section `build intellij.python.community.execService.python` end

View File

@@ -0,0 +1,2 @@
Execution service extensions for python-specific things like helpers.
Start with `api.kt`, do not touch "advanced" functions.

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" packagePrefix="com.intellij.python.community.execService.python" />
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="module" module-name="intellij.python.community.execService" />
<orderEntry type="module" module-name="intellij.python.community" />
<orderEntry type="library" name="jetbrains-annotations" level="project" />
<orderEntry type="library" name="kotlinx-coroutines-core" level="project" />
<orderEntry type="module" module-name="intellij.platform.eel" />
<orderEntry type="module" module-name="intellij.platform.eel.provider" />
<orderEntry type="module" module-name="intellij.python.community.helpersLocator" />
<orderEntry type="library" scope="TEST" name="JUnit5" level="project" />
<orderEntry type="module" module-name="intellij.platform.testFramework.junit5" scope="TEST" />
<orderEntry type="module" module-name="intellij.python.community.junit5Tests.framework" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.testFramework.junit5.eel" scope="TEST" />
<orderEntry type="library" scope="TEST" name="JUnit5Params" level="project" />
<orderEntry type="module" module-name="intellij.platform.util" />
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="module" module-name="intellij.python.sdk" />
</component>
</module>

View File

@@ -0,0 +1,6 @@
<idea-plugin>
<dependencies>
<module name="intellij.python.community.execService"/>
<module name="intellij.python.community.helpersLocator"/>
</dependencies>
</idea-plugin>

View File

@@ -0,0 +1,5 @@
py.exec.defaultName.process=Process {0}
python.get.version.error={0} returned error: {1}
python.get.version.too.long={0} took too long
python.get.version.wrong.version={0} has a wrong version: {1}

View File

@@ -1,5 +1,5 @@
// 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
package com.intellij.python.community.execService.python
/**
* Name of the helper file in helpers module

View File

@@ -0,0 +1,21 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.community.execService.python.advancedApi
import com.jetbrains.python.PythonBinary
import java.nio.file.Path
/**
* Something that can execute python code (vanilla cpython, conda).
* `[binary] [args]` <python-args-go-here> (i.e `-m foo.py`).
* For [VanillaExecutablePython] it is `python` without arguments, but for conda it might be `conda run` etc
*/
interface ExecutablePython {
val binary: Path
val args: List<String>
companion object {
class VanillaExecutablePython(override val binary: PythonBinary) : ExecutablePython {
override val args: List<String> = emptyList()
}
}
}

View File

@@ -0,0 +1,55 @@
// 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.python.advancedApi
import com.intellij.python.community.execService.*
import com.intellij.python.community.execService.impl.transformerToHandler
import com.intellij.python.community.execService.python.HelperName
import com.intellij.python.community.execService.python.impl.validatePythonAndGetVersionImpl
import com.intellij.python.community.helpersLocator.PythonHelpersLocator
import com.jetbrains.python.errorProcessing.PyExecResult
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.psi.LanguageLevel
import org.jetbrains.annotations.ApiStatus
// This in advanced API, most probably you need "api.kt"
/**
* Execute [python]
*/
suspend fun <T> ExecService.executePythonAdvanced(
python: ExecutablePython,
argsBuilder: suspend ArgsBuilder.() -> Unit = {},
options: ExecOptions = ExecOptions(),
processInteractiveHandler: ProcessInteractiveHandler<T>,
): PyExecResult<T> =
executeAdvanced(python.binary, {
addArgs(*python.args.toTypedArray())
argsBuilder()
}, options, processInteractiveHandler)
/**
* Execute [helper] on [python]. For remote eels, [helper] is copied (but only one file!).
*/
suspend fun <T> ExecService.executeHelperAdvanced(
python: ExecutablePython,
helper: HelperName,
args: List<String> = emptyList(),
options: ExecOptions = ExecOptions(),
procListener: PyProcessListener? = null,
processOutputTransformer: ProcessOutputTransformer<T>,
): PyExecResult<T> = executePythonAdvanced(python, {
addLocalFile(PythonHelpersLocator.findPathInHelpers(helper))
addArgs(*args.toTypedArray())
}, options, transformerToHandler(procListener, processOutputTransformer))
/**
* Ensures that this python is executable and returns its version. Error if python is broken.
*
* Some pythons might be broken: they may be executable, even return a version, but still fail to execute it.
* As we need workable pythons, we validate it by executing
*/
@ApiStatus.Internal
suspend fun ExecService.validatePythonAndGetVersion(python: ExecutablePython): PyResult<LanguageLevel> =
validatePythonAndGetVersionImpl(python)

View File

@@ -0,0 +1,5 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@ApiStatus.Internal
package com.intellij.python.community.execService.python.advancedApi;
import org.jetbrains.annotations.ApiStatus;

View File

@@ -0,0 +1,41 @@
// 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.python
import com.intellij.python.community.execService.ExecOptions
import com.intellij.python.community.execService.ExecService
import com.intellij.python.community.execService.PyProcessListener
import com.intellij.python.community.execService.ZeroCodeStdoutTransformer
import com.intellij.python.community.execService.python.advancedApi.ExecutablePython.Companion.VanillaExecutablePython
import com.intellij.python.community.execService.python.advancedApi.executeHelperAdvanced
import com.intellij.python.community.execService.python.advancedApi.validatePythonAndGetVersion
import com.jetbrains.python.PythonBinary
import com.jetbrains.python.errorProcessing.PyExecResult
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.psi.LanguageLevel
import org.jetbrains.annotations.ApiStatus
/**
* Execute [helper] on [python]. For remote eels, [helper] is copied (but only one file!).
* Returns `stdout`
*/
suspend fun ExecService.executeHelper(
python: PythonBinary,
helper: HelperName,
args: List<String> = emptyList(),
options: ExecOptions = ExecOptions(),
procListener: PyProcessListener? = null,
): PyExecResult<String> =
executeHelperAdvanced(VanillaExecutablePython(python), helper, args, options, procListener, ZeroCodeStdoutTransformer)
/**
* Ensures that this python is executable and returns its version. Error if python is broken.
*
* Some pythons might be broken: they may be executable, even return a version, but still fail to execute it.
* As we need workable pythons, we validate it by executing
*/
@ApiStatus.Internal
suspend fun ExecService.validatePythonAndGetVersion(python: PythonBinary): PyResult<LanguageLevel> =
validatePythonAndGetVersion(VanillaExecutablePython(python))
suspend fun PythonBinary.validatePythonAndGetVersion(): PyResult<LanguageLevel> = ExecService().validatePythonAndGetVersion(this)

View File

@@ -0,0 +1,29 @@
// 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.python.impl
import com.intellij.DynamicBundle
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.NonNls
import org.jetbrains.annotations.PropertyKey
import java.util.function.Supplier
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
internal object PyExecPythonBundle {
private const val BUNDLE_FQN: @NonNls String = "messages.PyExecPythonBundle"
private val BUNDLE = DynamicBundle(PyExecPythonBundle::class.java, BUNDLE_FQN)
fun message(
key: @PropertyKey(resourceBundle = BUNDLE_FQN) String,
vararg params: Any,
): @Nls String {
return BUNDLE.getMessage(key, *params)
}
fun messagePointer(
key: @PropertyKey(resourceBundle = BUNDLE_FQN) String,
vararg params: Any,
): Supplier<String> {
return BUNDLE.getLazyMessage(key, *params)
}
}

View File

@@ -0,0 +1,70 @@
// 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.python.impl
import com.intellij.openapi.util.NlsSafe
import com.intellij.platform.eel.provider.utils.EelProcessExecutionResult
import com.intellij.platform.eel.provider.utils.stderrString
import com.intellij.platform.eel.provider.utils.stdoutString
import com.intellij.python.community.execService.ExecOptions
import com.intellij.python.community.execService.ExecService
import com.intellij.python.community.execService.ZeroCodeStdoutTransformer
import com.intellij.python.community.execService.impl.transformerToHandler
import com.intellij.python.community.execService.python.advancedApi.ExecutablePython
import com.intellij.python.community.execService.python.advancedApi.executePythonAdvanced
import com.intellij.python.community.execService.python.impl.PyExecPythonBundle.message
import com.jetbrains.python.PYTHON_VERSION_ARG
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.ExecError
import com.jetbrains.python.errorProcessing.ExecErrorReason
import com.jetbrains.python.errorProcessing.MessageError
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor.getLanguageLevelFromVersionStringStaticSafe
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus
import kotlin.io.path.pathString
import kotlin.time.Duration.Companion.minutes
@ApiStatus.Internal
internal suspend fun ExecService.validatePythonAndGetVersionImpl(python: ExecutablePython): PyResult<LanguageLevel> = withContext(Dispatchers.IO) {
val options = ExecOptions(timeout = 1.minutes)
val result: PyResult<LanguageLevel> = run {
val smokeTestOutput = executePythonAdvanced(python, { addArgs("-c", "print(1)") }, processInteractiveHandler = transformerToHandler(null, ZeroCodeStdoutTransformer), options = options).getOr { return@run it }.trim()
if (smokeTestOutput != "1") {
return@run PyResult.localizedError(message("python.get.version.error", python.userReadableName, smokeTestOutput))
}
val versionOutput: EelProcessExecutionResult = executePythonAdvanced(python, options = options, argsBuilder = { addArgs(PYTHON_VERSION_ARG) }, processInteractiveHandler = transformerToHandler<EelProcessExecutionResult>(null, { r ->
if (r.exitCode == 0) Result.success(r) else Result.failure(message("python.get.version.error", python.userReadableName, r.exitCode))
})).getOr { return@run it }
// Python 2 might return version as stderr, see https://bugs.python.org/issue18338
val versionString = versionOutput.stdoutString.let { it.ifBlank { versionOutput.stderrString } }
val languageLevel = getLanguageLevelFromVersionStringStaticSafe(versionString.trim())
if (languageLevel == null) {
return@run PyResult.localizedError(message("python.get.version.wrong.version", python.userReadableName, versionOutput))
}
return@run Result.success(languageLevel)
}
// Return more readable error to user in case of timeout
when (result) {
is Result.Success -> Unit
is Result.Failure -> {
when (val e = result.error) {
is ExecError -> {
when (e.errorReason) {
is ExecErrorReason.CantStart, is ExecErrorReason.UnexpectedProcessTermination -> Unit
ExecErrorReason.Timeout -> {
return@withContext Result.localizedError(message("python.get.version.too.long", python.userReadableName))
}
}
}
is MessageError -> Unit
}
}
}
return@withContext result
}
private val ExecutablePython.userReadableName: @NlsSafe String get() = (listOf(binary.pathString) + args).joinToString(" ")

View File

@@ -0,0 +1,5 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@ApiStatus.Internal
package com.intellij.python.community.execService.python.impl;
import org.jetbrains.annotations.ApiStatus;

View File

@@ -0,0 +1,5 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@ApiStatus.Internal
package com.intellij.python.community.execService.python;
import org.jetbrains.annotations.ApiStatus;

View File

@@ -0,0 +1,64 @@
// 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.junit5Tests.env
import com.intellij.platform.eel.provider.asNioPath
import com.intellij.platform.eel.provider.localEel
import com.intellij.platform.testFramework.junit5.eel.params.api.*
import com.intellij.python.community.execService.ExecService
import com.intellij.python.community.execService.python.executeHelper
import com.intellij.python.community.execService.python.validatePythonAndGetVersion
import com.intellij.python.community.helpersLocator.PythonHelpersLocator
import com.intellij.python.junit5Tests.framework.env.PyEnvTestCase
import com.intellij.python.junit5Tests.framework.env.PythonBinaryPath
import com.jetbrains.python.PythonBinary
import com.jetbrains.python.getOrThrow
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.condition.OS
import org.junit.jupiter.params.ParameterizedTest
import java.nio.file.Path
import kotlin.io.path.deleteExisting
import kotlin.io.path.name
import kotlin.io.path.writeText
@TestApplicationWithEel(osesMayNotHaveRemoteEels = [OS.WINDOWS, OS.MAC, OS.LINUX])
@PyEnvTestCase
class HelpersShowCaseTest() {
@WslTest("ubuntu", mandatory = false)
@DockerTest(image = "python:3.13.4", mandatory = false)
@EelSource
@ParameterizedTest
fun testHelpersWinExample(
eelHolder: EelHolder,
@PythonBinaryPath localPython: PythonBinary,
): Unit = runBlocking {
val eel = eelHolder.eel
// We might not have local python, so we use one from tests
// Unlike docker/wsl, we can't choose local environment
val python: Path = if (eel == localEel) localPython else eel.exec.findExeFilesInPath("python3").first().asNioPath()
val helper = PythonHelpersLocator.getCommunityHelpersRoot().resolve("file.py")
val hello = "hello"
try {
withContext(Dispatchers.IO) {
helper.writeText("""
print("$hello")
""".trimIndent())
}
val output = ExecService().executeHelper(python, helper.name, listOf("--version")).orThrow().trim()
Assertions.assertEquals(hello, output, "wrong helper output")
val langLevel = ExecService().validatePythonAndGetVersion(python).getOrThrow()
Assertions.assertTrue(langLevel.isPy3K, "Wrong lang level:$langLevel")
}
finally {
withContext(Dispatchers.IO) {
helper.deleteExisting()
}
}
}
}

View File

@@ -0,0 +1,33 @@
// 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.junit5Tests.env
import com.intellij.python.community.execService.python.validatePythonAndGetVersion
import com.intellij.python.junit5Tests.framework.env.PyEnvTestCase
import com.intellij.python.junit5Tests.framework.env.PythonBinaryPath
import com.intellij.python.junit5Tests.randomBinary
import com.jetbrains.python.PythonBinary
import com.jetbrains.python.Result
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
@PyEnvTestCase
class PythonBinaryValidationTest {
@Test
fun sunnyDayTest(@PythonBinaryPath python: PythonBinary): Unit = runBlocking {
val level = python.validatePythonAndGetVersion().orThrow()
Assertions.assertNotNull(level, "Failed to get python level")
}
@Test
fun rainyDayTest(): Unit = runBlocking {
when (val r = randomBinary.validatePythonAndGetVersion()) {
is Result.Success -> {
Assertions.fail("${randomBinary} isn't a python, should fail, but got ${r.result}")
}
is Result.Failure -> {
Assertions.assertTrue(r.error.message.isNotBlank(), "No error returned")
}
}
}
}

View File

@@ -26,6 +26,5 @@
<orderEntry type="library" scope="TEST" name="JUnit5Pioneer" level="project" />
<orderEntry type="module" module-name="intellij.platform.testFramework.common" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.util.progress" />
<orderEntry type="module" module-name="intellij.python.community.helpersLocator" />
</component>
</module>

View File

@@ -1,5 +1,4 @@
py.exec.defaultName.process=Process
py.exec.defaultName.helper=Helper
py.exec.defaultName.process=Process {0}
py.exec.start.error={0} Failed to Start: {1} (Code {2})
py.exec.timeout.error={0} Timed out (Run More Than {1})

View File

@@ -3,6 +3,7 @@ package com.intellij.python.community.execService
import kotlinx.coroutines.flow.FlowCollector
import org.jetbrains.annotations.ApiStatus
import java.nio.file.Path
/**
* Listens for start/stop/std{out,err} events
@@ -10,7 +11,7 @@ import org.jetbrains.annotations.ApiStatus
typealias PyProcessListener = FlowCollector<ProcessEvent>
sealed interface ProcessEvent {
data class ProcessStarted @ApiStatus.Internal constructor(val whatToExec: WhatToExec, val args: List<String>) : ProcessEvent
data class ProcessStarted @ApiStatus.Internal constructor(val binary: Path, val args: List<String>) : ProcessEvent
data class ProcessOutput @ApiStatus.Internal constructor(val stream: OutputType, val line: String) : ProcessEvent
data class ProcessEnded @ApiStatus.Internal constructor(val exitCode: Int) : ProcessEvent

View File

@@ -11,6 +11,7 @@ import kotlinx.coroutines.Deferred
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.CheckReturnValue
import org.jetbrains.annotations.Nls
import java.nio.file.Path
// This is an advanced API, consider using basic api.kt
@@ -25,16 +26,19 @@ import org.jetbrains.annotations.Nls
interface ExecService {
/**
* TL;TR: Use extension functions from `api.kt`, do not use it directly!
*
* Execute code in a so-called "interactive" mode.
* This is a quite advanced mode where *you* are responsible for converting a process to output.
* You must listen for process stdout/stderr e.t.c.
* Use it if you need to get some info from a process before it ends or to interact (i.e. write into stdin).
* See [ProcessInteractiveHandler] and [processSemiInteractiveHandler]
* See [ProcessInteractiveHandler] and [processSemiInteractiveHandler].
* [argsBuilder] is a lambda to build args, see [ArgsBuilder]
*/
@CheckReturnValue
suspend fun <T> execute(
whatToExec: WhatToExec,
args: List<String> = emptyList(),
suspend fun <T> executeAdvanced(
binary: Path,
argsBuilder: suspend ArgsBuilder.() -> Unit = {},
options: ExecOptions = ExecOptions(),
processInteractiveHandler: ProcessInteractiveHandler<T>,
): PyExecResult<T>
@@ -56,7 +60,7 @@ fun interface ProcessInteractiveHandler<T> {
* In latter case returns [EelProcessExecutionResult] (created out of collected output) and optional error message.
* If no message returned -- the default one is used.
*/
suspend fun getResultFromProcess(whatToExec: WhatToExec, args: List<String>, process: EelProcess): Result<T, Pair<EelProcessExecutionResult, CustomErrorMessage?>>
suspend fun getResultFromProcess(binary: Path, args: List<String>, process: EelProcess): Result<T, Pair<EelProcessExecutionResult, CustomErrorMessage?>>
}
@@ -71,3 +75,18 @@ typealias ProcessSemiInteractiveFun<T> = suspend (EelSendChannel, Deferred<EelPr
* So, you can only *write* something to process.
*/
fun <T> processSemiInteractiveHandler(pyProcessListener: PyProcessListener? = null, code: ProcessSemiInteractiveFun<T>): ProcessInteractiveHandler<T> = ProcessSemiInteractiveHandlerImpl(pyProcessListener, code)
/**
* ```kotlin
* addLocalFile(helper)
* addTextArgs("-v")
* ```
*/
interface ArgsBuilder {
fun addArgs(vararg args: String)
/**
* This file will be copied to eel and its remote name will be added to the list of arguments
*/
suspend fun addLocalFile(localFile: Path)
}

View File

@@ -9,7 +9,7 @@ import com.intellij.platform.eel.provider.utils.EelProcessExecutionResult
import com.intellij.platform.eel.provider.utils.stdoutString
import com.intellij.python.community.execService.impl.ExecServiceImpl
import com.intellij.python.community.execService.impl.PyExecBundle
import com.jetbrains.python.PythonBinary
import com.intellij.python.community.execService.impl.transformerToHandler
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.ExecError
import com.jetbrains.python.errorProcessing.PyExecResult
@@ -35,7 +35,14 @@ suspend fun ExecService.execGetStdout(
args: List<String> = emptyList(),
options: ExecOptions = ExecOptions(),
procListener: PyProcessListener? = null,
): PyExecResult<String> = execGetStdout(WhatToExec.Binary(binary), args, options, procListener)
): PyExecResult<String> = execute(
binary = binary,
args = args,
options = options,
processOutputTransformer = ZeroCodeStdoutTransformer,
procListener = procListener
)
/**
* Execute [binaryName] on [eelApi].
@@ -48,9 +55,9 @@ suspend fun ExecService.execGetStdout(
options: ExecOptions = ExecOptions(),
procListener: PyProcessListener? = null,
): PyResult<String> {
val whatToExec = WhatToExec.Binary.fromRelativeName(eelApi, binaryName)
val binary = eelApi.exec.findExeFilesInPath(binaryName).firstOrNull()?.asNioPath()
?: return PyResult.localizedError(PyExecBundle.message("py.exec.fileNotFound", binaryName, eelApi.descriptor.userReadableDescription))
return execGetStdout(whatToExec, args, options, procListener)
return execGetStdout(binary, args, options, procListener)
}
@@ -66,11 +73,11 @@ suspend fun ExecService.execGetStdoutInShell(
procListener: PyProcessListener? = null,
): PyExecResult<String> {
val (shell, arg) = eelApi.exec.getShell()
return execGetStdout(WhatToExec.Binary(shell.asNioPath()), listOf(arg, commandForShell) + args, options, procListener)
return execGetStdout(shell.asNioPath(), listOf(arg, commandForShell) + args, options, procListener)
}
/**
* Execute [whatToExec] with [args] and get both stdout/stderr outputs if `errorCode != 0`, returns error otherwise.
* Execute [binary] with [args] and get both stdout/stderr outputs if `errorCode != 0`, returns error otherwise.
* Function collects output lines and reports them to [procListener] if set
*
* @param[args] command line arguments
@@ -79,31 +86,13 @@ suspend fun ExecService.execGetStdoutInShell(
*/
@CheckReturnValue
suspend fun <T> ExecService.execute(
whatToExec: WhatToExec,
binary: Path,
args: List<String> = emptyList(),
options: ExecOptions = ExecOptions(),
procListener: PyProcessListener? = null,
processOutputTransformer: ProcessOutputTransformer<T>,
): PyExecResult<T> = execute(whatToExec, args, options, processSemiInteractiveHandler(procListener) { _, result ->
processOutputTransformer(result.await())
})
): PyExecResult<T> = executeAdvanced(binary, { addArgs(*args.toTypedArray()) }, options, transformerToHandler(procListener, processOutputTransformer))
/**
* See [ExecService.execute]
*/
@CheckReturnValue
suspend fun ExecService.execGetStdout(
whatToExec: WhatToExec,
args: List<String> = emptyList(),
options: ExecOptions = ExecOptions(),
procListener: PyProcessListener? = null,
): PyExecResult<String> = execute(
whatToExec = whatToExec,
args = args,
options = options,
processOutputTransformer = ZeroCodeStdoutTransformer,
procListener = procListener
)
/**
* Error is an optional additionalMessage, that will be used instead of a default one for the [ExecError]
@@ -128,24 +117,3 @@ data class ExecOptions(
val processDescription: @Nls String? = null,
val timeout: Duration = 1.minutes,
)
sealed interface WhatToExec {
/**
* [binary] (can reside on local or remote Eel, [EelApi] is calculated out of it)
*/
data class Binary(val binary: Path) : WhatToExec {
companion object {
/**
* Resolves relative name to the full name or `null` if [relativeBinName] can't be found in the path.
*/
suspend fun fromRelativeName(eel: EelApi, relativeBinName: String): Binary? =
eel.exec.findExeFilesInPath(relativeBinName).firstOrNull()?.let { Binary(it.asNioPath()) }
}
}
/**
* Execute [helper] on [python]. If [python] resides on remote Eel -- helper is copied there.
* Note, that only **one** helper file is copied, not all helpers.
*/
data class Helper(val python: PythonBinary, val helper: HelperName) : WhatToExec
}

View File

@@ -2,6 +2,7 @@
package com.intellij.python.community.execService.impl
import com.intellij.openapi.diagnostic.fileLogger
import com.intellij.platform.eel.EelApi
import com.intellij.platform.eel.EelProcess
import com.intellij.platform.eel.ExecuteProcessException
import com.intellij.platform.eel.path.EelPath
@@ -9,11 +10,10 @@ 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.spawnProcess
import com.intellij.python.community.execService.ArgsBuilder
import com.intellij.python.community.execService.ExecOptions
import com.intellij.python.community.execService.ExecService
import com.intellij.python.community.execService.ProcessInteractiveHandler
import com.intellij.python.community.execService.WhatToExec
import com.intellij.python.community.helpersLocator.PythonHelpersLocator
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.ExecError
import com.jetbrains.python.errorProcessing.ExecErrorReason
@@ -26,22 +26,24 @@ import kotlinx.coroutines.withTimeout
import org.jetbrains.annotations.CheckReturnValue
import org.jetbrains.annotations.Nls
import java.nio.file.Path
import java.util.concurrent.CopyOnWriteArrayList
import kotlin.io.path.exists
import kotlin.time.Duration
internal object ExecServiceImpl : ExecService {
override suspend fun <T> execute(
whatToExec: WhatToExec,
args: List<String>,
options: ExecOptions,
processInteractiveHandler: ProcessInteractiveHandler<T>,
): PyExecResult<T> {
val executableProcess = whatToExec.buildExecutableProcess(args, options)
override suspend fun <T> executeAdvanced(binary: Path, argsBuilder: suspend ArgsBuilder.() -> Unit, options: ExecOptions, processInteractiveHandler: ProcessInteractiveHandler<T>): PyExecResult<T> {
val args = ArgsBuilderImpl(binary.getEelDescriptor().toEelApi()).apply { argsBuilder() }.args
val description = options.processDescription
?: PyExecBundle.message("py.exec.defaultName.process", (listOf(toString()) + args).joinToString(" "))
val executableProcess = EelExecutableProcess(binary.asEelPath(), args, options.env, options.workingDirectory, description)
val eelProcess = executableProcess.run().getOr { return it }
val result = try {
withTimeout(options.timeout) {
val interactiveResult = processInteractiveHandler.getResultFromProcess(whatToExec, args, eelProcess)
val interactiveResult = processInteractiveHandler.getResultFromProcess(binary, args, eelProcess)
val successResult = interactiveResult.getOr { failure ->
val (output, customErrorMessage) = failure.error
@@ -66,28 +68,6 @@ private data class EelExecutableProcess(
val description: @Nls String,
)
private suspend fun WhatToExec.buildExecutableProcess(args: List<String>, options: ExecOptions): EelExecutableProcess {
val (exe, args) = when (this) {
is WhatToExec.Binary -> Pair(binary, args)
is WhatToExec.Helper -> {
val eel = python.getEelDescriptor().toEelApi()
val localHelper = withContext(Dispatchers.IO) { PythonHelpersLocator.findPathInHelpers(helper) }
val remoteHelper = EelPathUtils.transferLocalContentToRemote(
source = localHelper,
target = EelPathUtils.TransferTarget.Temporary(eel.descriptor)
).asEelPath().toString()
Pair(python, listOf(remoteHelper) + args)
}
}
val description = options.processDescription ?: when (this) {
is WhatToExec.Binary -> PyExecBundle.message("py.exec.defaultName.process")
is WhatToExec.Helper -> PyExecBundle.message("py.exec.defaultName.helper")
}
return EelExecutableProcess(exe.asEelPath(), args, options.env, options.workingDirectory, description)
}
@CheckReturnValue
private suspend fun EelExecutableProcess.run(): PyExecResult<EelProcess> {
val workingDirectory = if (workingDirectory != null && !workingDirectory.isAbsolute) workingDirectory.toRealPath() else workingDirectory
@@ -98,7 +78,8 @@ private suspend fun EelExecutableProcess.run(): PyExecResult<EelProcess> {
.workingDirectory(workingDirectory?.asEelPath()).eelIt()
return Result.success(executionResult)
} catch (e: ExecuteProcessException) {
}
catch (e: ExecuteProcessException) {
return failAsCantStart(e)
}
}
@@ -140,3 +121,20 @@ private fun ExecError.logAndFail(): Result.Failure<ExecError> {
fileLogger().warn(message)
return failure(this)
}
private class ArgsBuilderImpl(private val eel: EelApi) : ArgsBuilder {
private val _args = CopyOnWriteArrayList<String>()
val args: List<String> = _args
override fun addArgs(vararg args: String) {
_args.addAll(args)
}
override suspend fun addLocalFile(localFile: Path): Unit = withContext(Dispatchers.IO) {
assert(localFile.exists()) { "No file $localFile, be sure to check it before calling" }
val remoteFile = EelPathUtils.transferLocalContentToRemote(
source = localFile,
target = EelPathUtils.TransferTarget.Temporary(eel.descriptor)
).asEelPath().toString()
_args.add(remoteFile)
}
}

View File

@@ -8,14 +8,15 @@ import com.jetbrains.python.Result
import com.jetbrains.python.mapError
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import java.nio.file.Path
internal class ProcessSemiInteractiveHandlerImpl<T>(
private val pyProcessListener: PyProcessListener?,
private val code: ProcessSemiInteractiveFun<T>,
) : ProcessInteractiveHandler<T> {
override suspend fun getResultFromProcess(whatToExec: WhatToExec, args: List<String>, process: EelProcess): Result<T, Pair<EelProcessExecutionResult, CustomErrorMessage?>> =
override suspend fun getResultFromProcess(binary: Path, args: List<String>, process: EelProcess): Result<T, Pair<EelProcessExecutionResult, CustomErrorMessage?>> =
coroutineScope {
pyProcessListener?.emit(ProcessEvent.ProcessStarted(whatToExec, args))
pyProcessListener?.emit(ProcessEvent.ProcessStarted(binary, args))
val processOutput = async { process.awaitWithReporting(pyProcessListener) }
val result = code(process.stdin, processOutput)
pyProcessListener?.emit(ProcessEvent.ProcessEnded(process.exitCode.await()))

View File

@@ -0,0 +1,16 @@
// 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.python.community.execService.ProcessInteractiveHandler
import com.intellij.python.community.execService.ProcessOutputTransformer
import com.intellij.python.community.execService.PyProcessListener
import com.intellij.python.community.execService.processSemiInteractiveHandler
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
fun <T> transformerToHandler(
procListener: PyProcessListener?,
processOutputTransformer: ProcessOutputTransformer<T>,
): ProcessInteractiveHandler<T> = processSemiInteractiveHandler(procListener) { _, result ->
processOutputTransformer(result.await())
}

View File

@@ -89,7 +89,7 @@ class ExecServiceShowCaseTest {
val (shell, execArg) = eel.exec.getShell()
val args = listOf(execArg, "echo Alice,25 && echo Bob,48")
val records = ExecService().execute(WhatToExec.Binary(shell.asNioPath()), args) { output ->
val records = ExecService().execute((shell.asNioPath()), args) { output ->
val stdout = output.stdoutString.trim()
when {
output.exitCode == 123 -> {
@@ -133,9 +133,9 @@ class ExecServiceShowCaseTest {
}
}
val whatToExec = WhatToExec.Binary.fromRelativeName(eel, binaryName) ?: error("Can't find $binaryName")
val whatToExec = eel.exec.findExeFilesInPath(binaryName).firstOrNull() ?: error("Can't find $binaryName")
val output = execService.execGetStdout(whatToExec, args.toList()).getOrThrow()
val output = execService.execGetStdout(whatToExec.asNioPath(), args.toList()).getOrThrow()
assertThat("Command doesn't have expected output", output, CoreMatchers.containsString(expectedPhrase))
}
@@ -143,8 +143,8 @@ class ExecServiceShowCaseTest {
@EelSource
fun testInteractive(eelHolder: EelHolder): Unit = timeoutRunBlocking {
val string = "abc123"
val shell = eelHolder.eel.exec.getShell().first
val output = ExecService().execute(WhatToExec.Binary(shell.asNioPath()), emptyList(), processInteractiveHandler = ProcessInteractiveHandler<String> { _, _, process ->
val shell = eelHolder.eel.exec.getShell().first.asNioPath()
val output = ExecService().executeAdvanced(shell, {}, processInteractiveHandler = ProcessInteractiveHandler<String> { _, _, process ->
val stdout = async {
process.stdout.readWholeText()
}
@@ -161,8 +161,8 @@ class ExecServiceShowCaseTest {
@CartesianTest.Values(booleans = [true, false]) sunny: Boolean,
): Unit = timeoutRunBlocking {
val messageToUser = "abc123"
val shell = eelHolder.eel.exec.getShell().first
val result = ExecService().execute(WhatToExec.Binary(shell.asNioPath()), emptyList(), processInteractiveHandler = processSemiInteractiveHandler<Unit> { channel, exitCode ->
val shell = eelHolder.eel.exec.getShell().first.asNioPath()
val result = ExecService().executeAdvanced(shell, {}, processInteractiveHandler = processSemiInteractiveHandler<Unit> { channel, exitCode ->
channel.sendWholeText("exit\n")
assertEquals(0, exitCode.await().exitCode, "Wrong exit code")
if (sunny) {
@@ -176,7 +176,7 @@ class ExecServiceShowCaseTest {
is Result.Failure -> {
assertFalse(sunny, "Unexpected failure ${result.error}")
assertEquals(messageToUser, result.error.additionalMessageToUser, "Wrong message to user")
assertEquals(shell, result.error.exe, "Wrong exe")
assertEquals(shell, result.error.exe.asNioPath(), "Wrong exe")
}
is Result.Success -> {
assertTrue(sunny, "Unexpected success")
@@ -190,12 +190,11 @@ class ExecServiceShowCaseTest {
val eel = eelHolder.eel
val binary = eel.fs.user.home.asNioPath().resolve("Some_command_that_never_exists_on_any_machine${Math.random()}")
val arg = "foo"
val command = WhatToExec.Binary(binary)
when (val output = ExecService().execGetStdout(command, listOf(arg))) {
when (val output = ExecService().execGetStdout(binary, listOf(arg))) {
is Result.Success -> fail("Execution of bad command should lead to an error")
is Result.Failure -> {
val err = output.error
assertEquals(command.binary, err.exe.asNioPath(), "Wrong command reported")
assertEquals(binary, err.exe.asNioPath(), "Wrong command reported")
assertEquals("foo", err.args[0], "Wrong args reported")
}
}
@@ -206,12 +205,10 @@ class ExecServiceShowCaseTest {
@EelSource
fun testProgress(eelHolder: EelHolder): Unit = timeoutRunBlocking(10.minutes) {
val eel = eelHolder.eel
val shell = eel.exec.getShell().first
val shell = eel.exec.getShell().first.asNioPath()
val text = "Once there was a captain brave".split(" ").toTypedArray()
val whatToExec = WhatToExec.Binary(shell.asNioPath())
var processStartEvent = false
var processEndEvent = false
@@ -219,7 +216,7 @@ class ExecServiceShowCaseTest {
val progressCapturer = PyProcessListener { event ->
when (event) {
is ProcessEvent.ProcessStarted -> {
assertEquals(whatToExec, event.whatToExec, "Wrong args for start event")
assertEquals(shell, event.binary, "Wrong args for start event")
processStartEvent = true
}
is ProcessEvent.ProcessOutput -> {
@@ -231,7 +228,7 @@ class ExecServiceShowCaseTest {
}
}
ExecService().execute(whatToExec, args = emptyList(), processInteractiveHandler = processSemiInteractiveHandler<Unit>(progressCapturer) { stdin, exitCode ->
ExecService().executeAdvanced(shell, argsBuilder = {}, processInteractiveHandler = processSemiInteractiveHandler<Unit>(progressCapturer) { stdin, _ ->
for (string in text) {
stdin.sendWholeText("echo $string\n")
delay(500)
@@ -267,7 +264,7 @@ class ExecServiceShowCaseTest {
is ProcessEvent.ProcessEnded, is ProcessEvent.ProcessStarted -> Unit
}
}
val output = ExecService().execGetStdout(WhatToExec.Binary(shell.asNioPath()), listOf(arg, "echo $text"), procListener = listener).getOrThrow()
val output = ExecService().execGetStdout(shell.asNioPath(), listOf(arg, "echo $text"), procListener = listener).getOrThrow()
assertTrue(stdoutReported, "No stdout reported")
assertEquals(text, output.trim(), "Wrong result")

View File

@@ -3,7 +3,6 @@ package com.intellij.python.hatch.runtime
import com.intellij.platform.eel.EelApi
import com.intellij.platform.eel.provider.localEel
import com.intellij.python.community.execService.*
import com.intellij.python.community.execService.WhatToExec.Binary
import com.intellij.python.hatch.*
import com.intellij.python.hatch.cli.HatchCli
import com.jetbrains.python.PythonBinary
@@ -17,7 +16,7 @@ import kotlin.io.path.isDirectory
import kotlin.io.path.isExecutable
class HatchRuntime(
val hatchBinary: Binary,
val hatchBinary: Path,
val execOptions: ExecOptions,
private val execService: ExecService = ExecService(),
) {
@@ -60,12 +59,12 @@ class HatchRuntime(
}
internal suspend fun <T> executeInteractive(vararg arguments: String, processSemiInteractiveFun: ProcessSemiInteractiveFun<T>): PyExecResult<T> {
return execService.execute(hatchBinary, arguments.toList(), execOptions, processSemiInteractiveHandler(code = processSemiInteractiveFun))
return execService.executeAdvanced(hatchBinary, { addArgs(*arguments) }, execOptions, processSemiInteractiveHandler(code = processSemiInteractiveFun))
}
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()
execService.execGetStdout(pythonBinaryPath, listOf("--version")).getOr { return it }.trim()
}
val pythonVirtualEnvironment = when {
pythonVersion == null -> PythonVirtualEnvironment.NotExisting(pythonHomePath)
@@ -99,7 +98,7 @@ suspend fun createHatchRuntime(
val actualEnvVars = defaultVariables + envVars
val runtime = HatchRuntime(
hatchBinary = Binary(actualHatchExecutable),
hatchBinary = actualHatchExecutable,
execOptions = ExecOptions(
env = actualEnvVars,
workingDirectory = workingDirectoryPath

View File

@@ -83,7 +83,3 @@ path.validation.ends.with.whitespace=Path ends with a whitespace
path.validation.file.not.found=File {0} is not found
path.validation.invalid=Path is invalid: {0}
path.validation.inaccessible=Path is inaccessible
python.get.version.error={0} returned error: {1}
python.get.version.too.long={0} took too long
python.get.version.wrong.version={0} has a wrong version: {1}

View File

@@ -1,68 +1,13 @@
package com.jetbrains.python
import com.intellij.openapi.util.NlsSafe
import com.intellij.platform.eel.EelPlatform
import com.intellij.platform.eel.ExecuteProcessException
import com.intellij.platform.eel.provider.getEelDescriptor
import com.intellij.platform.eel.provider.utils.EelProcessExecutionResult
import com.intellij.platform.eel.provider.utils.exec
import com.intellij.platform.eel.provider.utils.stderrString
import com.intellij.platform.eel.provider.utils.stdoutString
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.jetbrains.python.PySdkBundle.message
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor.PYTHON_VERSION_ARG
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor.getLanguageLevelFromVersionStringStaticSafe
import com.jetbrains.python.venvReader.VirtualEnvReader
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus
import kotlin.io.path.name
import kotlin.io.path.pathString
import kotlin.time.Duration.Companion.seconds
/**
* Ensures that this python is executable and returns its version. Error if python is broken.
*
* Some pythons might be broken: they may be executable, even return a version, but still fail to execute it.
* As we need workable pythons, we validate it by executing
*/
@ApiStatus.Internal
suspend fun PythonBinary.validatePythonAndGetVersion(): PyResult<LanguageLevel> = withContext(Dispatchers.IO) {
val smokeTestOutput = executeWithResult("-c", "print(1)").getOr { return@withContext it }.stdoutString.trim()
if (smokeTestOutput != "1") {
return@withContext PyResult.localizedError(message("python.get.version.error", pathString, smokeTestOutput))
}
val versionOutput = executeWithResult(PYTHON_VERSION_ARG).getOr { return@withContext it }
// Python 2 might return version as stderr, see https://bugs.python.org/issue18338
val versionString = versionOutput.stdoutString.let { it.ifBlank { versionOutput.stderrString } }
val languageLevel = getLanguageLevelFromVersionStringStaticSafe(versionString.trim())
if (languageLevel == null) {
return@withContext PyResult.localizedError(message("python.get.version.wrong.version", pathString, versionOutput))
}
return@withContext Result.success(languageLevel)
}
/**
* Executes [this] with [args], returns either output or error (if execution failed or exit code != 0)
*/
private suspend fun PythonBinary.executeWithResult(vararg args: String): PyResult<@NlsSafe EelProcessExecutionResult> {
try {
val output = exec(*args, timeout = 5.seconds)
return if (output.exitCode != 0) {
PyResult.localizedError(message("python.get.version.error", pathString, "code ${output.exitCode}, ${output.stderrString}"))
}
else {
Result.success(output)
}
} catch (e : ExecuteProcessException) {
return PyResult.localizedError(e.localizedMessage)
}
}
@RequiresBackgroundThread
@ApiStatus.Internal
fun PythonBinary.resolvePythonHome(): PythonHomePath = when (getEelDescriptor().platform) {

View File

@@ -34,6 +34,7 @@ import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static com.jetbrains.python.PythonBinaryKt.PYTHON_VERSION_ARG;
import static com.jetbrains.python.sdk.flavors.PySdkFlavorUtilKt.getFileExecutionError;
import static com.jetbrains.python.sdk.flavors.PySdkFlavorUtilKt.getFileExecutionErrorOnEdt;
import static com.jetbrains.python.venvReader.ResolveUtilKt.tryResolvePath;
@@ -65,12 +66,6 @@ public abstract class PythonSdkFlavor<D extends PyFlavorData> {
private static final Pattern VERSION_RE = Pattern.compile("(Python \\S+).*");
private static final Logger LOG = Logger.getInstance(PythonSdkFlavor.class);
/**
* <code>
* python --version
* </code>
*/
public static final String PYTHON_VERSION_ARG = "--version";
/**

View File

@@ -28,6 +28,7 @@ jvm_library(
"//platform/projectModel-api:projectModel",
"//platform/util",
"//platform/core-api:core",
"//python/python-exec-service/execService.python",
],
runtime_deps = [":community-impl-venv_resources"]
)
@@ -57,6 +58,8 @@ jvm_library(
"//platform/ide-core-impl",
"//platform/execution",
"//platform/core-api:core",
"//python/python-exec-service/execService.python",
"//python/python-exec-service/execService.python:execService.python_test_lib",
],
runtime_deps = [
":community-impl-venv_resources",

View File

@@ -25,5 +25,6 @@
<orderEntry type="module" module-name="intellij.platform.ide.core.impl" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.execution" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="module" module-name="intellij.python.community.execService.python" />
</component>
</module>

View File

@@ -2,6 +2,7 @@
<dependencies>
<module name="intellij.python.community"/>
<module name="intellij.python.community.execService"/>
<module name="intellij.python.community.execService.python"/>
<module name="intellij.python.sdk"/>
</dependencies>
</idea-plugin>

View File

@@ -4,13 +4,15 @@ package com.intellij.python.community.impl.venv
import com.intellij.openapi.application.EDT
import com.intellij.openapi.diagnostic.fileLogger
import com.intellij.python.community.execService.*
import com.intellij.python.community.execService.python.HelperName
import com.intellij.python.community.execService.python.executeHelper
import com.intellij.python.community.execService.python.validatePythonAndGetVersion
import com.jetbrains.python.PythonBinary
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.sdk.PySdkSettings
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
import com.jetbrains.python.validatePythonAndGetVersion
import com.jetbrains.python.venvReader.Directory
import com.jetbrains.python.venvReader.VirtualEnvReader
import kotlinx.coroutines.Dispatchers
@@ -43,7 +45,7 @@ suspend fun createVenv(
}
val version = python.validatePythonAndGetVersion().getOr { return it }
val helper = if (version.isAtLeast(LanguageLevel.PYTHON38)) VIRTUALENV_ZIPAPP_NAME else LEGACY_VIRTUALENV_ZIPAPP_NAME
execService.execGetStdout(WhatToExec.Helper(python, helper = helper), args, ExecOptions(timeout = 3.minutes)).getOr { return it }
execService.executeHelper(python, helper, args, ExecOptions(timeout = 3.minutes)).getOr { return it }
val venvPython = withContext(Dispatchers.IO) {

View File

@@ -21,6 +21,7 @@ jvm_library(
"//python/openapi:community",
"@lib//:kotlinx-coroutines-core",
"//python/python-sdk:sdk",
"//python/python-exec-service/execService.python",
],
runtime_deps = [":python-community-services-internal-impl_resources"]
)
@@ -46,6 +47,8 @@ jvm_library(
"//python/python-sdk:sdk",
"//python/python-sdk:sdk_test_lib",
"//python/junit5Tests-framework:community-junit5Tests-framework_test_lib",
"//python/python-exec-service/execService.python",
"//python/python-exec-service/execService.python:execService.python_test_lib",
],
runtime_deps = [":python-community-services-internal-impl_resources"]
)

View File

@@ -21,5 +21,6 @@
<orderEntry type="library" name="kotlinx-coroutines-core" level="project" />
<orderEntry type="module" module-name="intellij.python.sdk" />
<orderEntry type="module" module-name="intellij.python.community.junit5Tests.framework" scope="TEST" />
<orderEntry type="module" module-name="intellij.python.community.execService.python" />
</component>
</module>

View File

@@ -3,5 +3,6 @@
<module name="intellij.python.community"/>
<module name="intellij.python.psi.impl"/>
<module name="intellij.python.sdk"/>
<module name="intellij.python.community.execService.python"/>
</dependencies>
</idea-plugin>

View File

@@ -4,6 +4,7 @@ package com.intellij.python.community.services.internal.impl
import com.intellij.platform.eel.EelPlatform
import com.intellij.platform.eel.provider.asNioPath
import com.intellij.platform.eel.provider.getEelDescriptor
import com.intellij.python.community.execService.python.validatePythonAndGetVersion
import com.intellij.python.community.services.internal.impl.PythonWithLanguageLevelImpl.Companion.concurrentLimit
import com.intellij.python.community.services.internal.impl.PythonWithLanguageLevelImpl.Companion.createByPythonBinary
import com.intellij.python.community.services.shared.PythonWithLanguageLevel
@@ -11,7 +12,6 @@ import com.jetbrains.python.PythonBinary
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.validatePythonAndGetVersion
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope

View File

@@ -13,6 +13,7 @@ import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.platform.util.progress.withProgressText
import com.intellij.python.community.execService.python.validatePythonAndGetVersion
import com.intellij.python.community.impl.venv.createVenv
import com.intellij.python.community.services.systemPython.SystemPythonService
import com.jetbrains.python.*

View File

@@ -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.remote;
import com.google.common.collect.Lists;
@@ -25,6 +25,8 @@ import java.io.Closeable;
import java.io.IOException;
import java.io.UncheckedIOException;
import static com.jetbrains.python.PythonBinaryKt.PYTHON_VERSION_ARG;
public final class PyRemoteInterpreterUtil {
/**
* @param nullForUnparsableVersion if version returns by python can't be parsed -- return null instead of exception
@@ -45,7 +47,7 @@ public final class PyRemoteInterpreterUtil {
ProcessOutput processOutput;
try {
try {
String[] command = {data.getInterpreterPath(), PythonSdkFlavor.PYTHON_VERSION_ARG};
String[] command = {data.getInterpreterPath(), PYTHON_VERSION_ARG};
processOutput = PyRemoteProcessStarterManagerUtil.getManager(data).executeRemoteProcess(myProject, command, null,
data, new PyRemotePathMapper());
if (processOutput.getExitCode() == 0) {

View File

@@ -4,7 +4,10 @@ package com.jetbrains.python.sdk
import com.intellij.execution.process.AnsiEscapeDecoder
import com.intellij.execution.process.ProcessOutputTypes
import com.intellij.platform.util.progress.reportRawProgress
import com.intellij.python.community.execService.*
import com.intellij.python.community.execService.ExecOptions
import com.intellij.python.community.execService.ExecService
import com.intellij.python.community.execService.ProcessEvent
import com.intellij.python.community.execService.execGetStdout
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyExecResult
import org.jetbrains.annotations.ApiStatus.Internal
@@ -26,7 +29,7 @@ import kotlin.time.Duration.Companion.minutes
suspend fun runExecutableWithProgress(executable: Path, workDir: Path?, timeout: Duration = 10.minutes, vararg args: String): PyExecResult<String> {
val ansiDecoder = AnsiEscapeDecoder()
reportRawProgress { reporter ->
return ExecService().execGetStdout(WhatToExec.Binary(executable), args.toList(), ExecOptions(workingDirectory = workDir, timeout = timeout), procListener = {
return ExecService().execGetStdout(executable, args.toList(), ExecOptions(workingDirectory = workDir, timeout = timeout), procListener = {
when (it) {
is ProcessEvent.ProcessStarted, is ProcessEvent.ProcessEnded -> Unit
is ProcessEvent.ProcessOutput -> {

View File

@@ -3,8 +3,7 @@ package com.jetbrains.python.sdk
import com.intellij.openapi.util.SystemInfo
import com.intellij.python.community.execService.ExecService
import com.intellij.python.community.execService.WhatToExec
import com.intellij.python.community.execService.execGetStdout
import com.intellij.python.community.execService.python.executeHelper
import com.jetbrains.python.PythonBinary
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyResult
@@ -28,6 +27,6 @@ fun getPythonExecutableString(): String = if (SystemInfo.isWindows) "py" else "p
*/
@Internal
suspend fun installExecutableViaPythonScript(pythonExecutable: PythonBinary, vararg args: String): PyResult<Path> {
val output = ExecService().execGetStdout(WhatToExec.Helper(pythonExecutable, "pycharm_package_installer.py"), args.toList()).getOr { return it }
val output = ExecService().executeHelper(pythonExecutable, "pycharm_package_installer.py", args.toList()).getOr { return it }
return Result.success(Path.of(output.split("\n").last()))
}

View File

@@ -11,6 +11,7 @@ import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.Disposer
import com.jetbrains.python.PYTHON_VERSION_ARG
import com.jetbrains.python.PythonHelper
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
import com.jetbrains.python.run.buildTargetedCommandLine
@@ -41,7 +42,7 @@ class PyTargetsIntrospectionFacade(val sdk: Sdk, val project: Project) {
val cmdBuilder = TargetedCommandLineBuilder(targetEnvRequest)
sdk.configureBuilderToRunPythonOnTarget(cmdBuilder)
sdk.sdkFlavor
cmdBuilder.addParameter(PythonSdkFlavor.PYTHON_VERSION_ARG)
cmdBuilder.addParameter(PYTHON_VERSION_ARG)
val cmd = cmdBuilder.build()
val environment = targetEnvRequest.prepareEnvironment(TargetProgressIndicatorAdapter(indicator))

View File

@@ -11,7 +11,6 @@ import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.registry.Registry
import com.intellij.python.community.execService.ExecOptions
import com.intellij.python.community.execService.ExecService
import com.intellij.python.community.execService.WhatToExec
import com.intellij.python.community.execService.execGetStdout
import com.intellij.python.community.impl.poetry.poetryPath
import com.intellij.util.SystemProperties
@@ -115,7 +114,7 @@ suspend fun setupPoetry(projectPath: Path, python: String?, installPackages: Boo
if (init) {
runPoetry(projectPath, *listOf("init", "-n").toTypedArray())
if (python != null) { // Replace a python version in toml
ExecService().execGetStdout(WhatToExec.Binary(Path.of(python)), listOf("-c", REPLACE_PYTHON_VERSION), ExecOptions(workingDirectory = projectPath)).getOr { return it }
ExecService().execGetStdout(Path.of(python), listOf("-c", REPLACE_PYTHON_VERSION), ExecOptions(workingDirectory = projectPath)).getOr { return it }
}
}
when {

View File

@@ -1,4 +1,4 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. 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.
@file:JvmName("PyInterpreterVersionUtil")
package com.jetbrains.python.target
@@ -15,6 +15,7 @@ import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.util.Ref
import com.intellij.remote.RemoteSdkException
import com.intellij.util.ui.UIUtil
import com.jetbrains.python.PYTHON_VERSION_ARG
import com.jetbrains.python.PyBundle
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
@@ -39,7 +40,7 @@ fun PyTargetAwareAdditionalData.getInterpreterVersion(project: Project?,
try {
val targetedCommandLineBuilder = TargetedCommandLineBuilder(targetEnvironmentRequest)
targetedCommandLineBuilder.setExePath(interpreterPath)
targetedCommandLineBuilder.addParameter(PythonSdkFlavor.PYTHON_VERSION_ARG)
targetedCommandLineBuilder.addParameter(PYTHON_VERSION_ARG)
val targetEnvironment = targetEnvironmentRequest.prepareEnvironment(TargetProgressIndicatorAdapter(indicator))
val targetedCommandLine = targetedCommandLineBuilder.build()
val process = targetEnvironment.createProcess(targetedCommandLine, indicator)

View File

@@ -1,4 +1,4 @@
// 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
import com.intellij.execution.processTools.getBareExecutionResult
@@ -10,6 +10,7 @@ import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.util.concurrency.ThreadingAssertions
import com.jetbrains.python.PYTHON_VERSION_ARG
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
import com.jetbrains.python.sdk.configureBuilderToRunPythonOnTarget
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
@@ -29,7 +30,7 @@ internal suspend fun getPythonVersion(sdk: Sdk, request: TargetEnvironmentReques
internal suspend fun getPythonVersion(commandLineBuilder: TargetedCommandLineBuilder,
flavor: PythonSdkFlavor<*>,
request: TargetEnvironmentRequest): String? {
commandLineBuilder.addParameter(PythonSdkFlavor.PYTHON_VERSION_ARG)
commandLineBuilder.addParameter(PYTHON_VERSION_ARG)
val commandLine = commandLineBuilder.build()
val result = request
.prepareEnvironment(TargetProgressIndicator.EMPTY)