PY-80882 Introduce support for "Run with" tools in Python run configurations. Introduce PyRunToolProvider and UvRunToolProvider to enable uv run for UV SDKs and update Python run configuration logic accordingly

GitOrigin-RevId: 0323bba73b2f9d3530280c5d03e749c664adb425
This commit is contained in:
Timur Malanin
2025-10-13 19:57:17 +00:00
committed by intellij-monorepo-bot
parent 373afacd68
commit defbf5a1d0
13 changed files with 249 additions and 9 deletions
@@ -19,6 +19,7 @@ import com.jetbrains.python.run.AbstractPyCommonOptionsForm;
import com.jetbrains.python.run.PyCommonOptionsFormData;
import com.jetbrains.python.sdk.PySdkListCellRenderer;
import com.jetbrains.python.sdk.legacy.PythonSdkUtil;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -162,6 +163,18 @@ public class PyIdeCommonOptionsForm implements AbstractPyCommonOptionsForm {
return null;
}
@ApiStatus.Internal
@Override
public @Nullable Boolean getUseRunTool() {
return null;
}
@ApiStatus.Internal
@Override
public void setUseRunTool(@Nullable Boolean useRunTool) {
}
@Override
public void setModule(Module module) {
content.moduleCombo.setSelectedModule(module);
@@ -17,6 +17,7 @@ import com.jetbrains.python.run.AbstractPyCommonOptionsForm;
import com.jetbrains.python.run.PyCommonOptionsFormData;
import com.jetbrains.python.sdk.PreferredSdkComparator;
import com.jetbrains.python.sdk.legacy.PythonSdkUtil;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -170,6 +171,18 @@ public class PyPluginCommonOptionsForm implements AbstractPyCommonOptionsForm {
return content.moduleComboBox.getSelectedModule();
}
@ApiStatus.Internal
@Override
public @Nullable Boolean getUseRunTool() {
return null;
}
@ApiStatus.Internal
@Override
public void setUseRunTool(@Nullable Boolean useRunTool) {
}
@Override
public String getModuleName() {
Module module = getModule();
@@ -810,9 +810,14 @@
<extensionPoint qualifiedName="Pythonid.PythonPackageRequirementsTreeExtractorProvider"
interface="com.jetbrains.python.packaging.packageRequirements.PythonPackageRequirementsTreeExtractorProvider"
dynamic="true"/>
<extensionPoint qualifiedName="Pythonid.pyRunToolProvider"
interface="com.jetbrains.python.run.features.PyRunToolProvider"
dynamic="true"/>
</extensionPoints>
<extensions defaultExtensionNs="Pythonid">
<pyRunToolProvider implementation="com.jetbrains.python.sdk.uv.run.UvRunToolProvider"/>
<evoTreeElementProvider id="UvEvoProvider"
order="first"
implementation="com.jetbrains.python.uv.sdk.evolution.UvSelectSdkProvider"/>
@@ -1105,6 +1105,7 @@ form.tox.configuration.runcfg.tox=tox
py.module.dependencies.configurable.list.title=Project depends on these projects:
py.sdk.editor.python.interpreter.label.text=Python Interpreter:
python.run.configuration.fragments.run.with.tool=Run with `{0}`
python.run.configuration.fragments.chooser.hint=Run Python script or module
python.run.configuration.fragments.script.path.hint=Path to a Python script
python.run.configuration.fragments.module.name.hint=Name of a module
@@ -1139,6 +1140,7 @@ python.run.configuration.fastapi.name=FastAPI
python.run.configuration.is.not.editable.in.this.mode=This run configuration is unavailable in the free mode and cannot be edited.
python.run.configuration.is.not.runnable.in.this.mode=This run configuration is unavailable in the free mode and cannot be run.
dialog.message.failed.to.resolve.original.executable.for.run.tool=Failed to resolve the original executable for run tool ''{0}''. Please check your interpreter and tool configuration.
# Python reStructuredText forms
rest.configuration.editor.open.output.file.in.browser.label.text=Open output file in browser
@@ -1717,4 +1719,4 @@ tracecontext.new.project.wizard=New Project Wizard
tracecontext.loading.interpreter.list=Loading Interpreter List
tracecontext.detecting.conda.executable.and.environments=Detecting Conda Executable and environments
evolution.uv.executable.is.not.found=uv executable is not found
evolution.pyproject.toml.file.is.required.for.poetry=pyproject.toml file is required for Poetry
evolution.pyproject.toml.file.is.required.for.poetry=pyproject.toml file is required for Poetry
@@ -32,6 +32,7 @@ import com.jetbrains.python.sdk.PythonEnvUtil;
import com.jetbrains.python.sdk.PythonSdkType;
import com.jetbrains.python.sdk.legacy.PythonSdkUtil;
import org.jdom.Element;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -45,6 +46,8 @@ import static com.jetbrains.python.run.PythonScriptCommandLineState.getExpandedW
*/
public abstract class AbstractPythonRunConfiguration<T extends AbstractPythonRunConfiguration<T>> extends AbstractRunConfiguration
implements AbstractPythonRunConfigurationParams, CommandLinePatcher, RunProfileWithCompileBeforeLaunchOption {
private static final String RUN_TOOL = "RUN_TOOL";
private @Nullable Boolean useRunTool = null;
private String myInterpreterOptions = "";
private String myWorkingDirectory = "";
private String mySdkHome = "";
@@ -281,6 +284,8 @@ public abstract class AbstractPythonRunConfiguration<T extends AbstractPythonRun
setMappingSettings(PathMappingSettings.readExternal(element));
// extension settings:
PythonRunConfigurationExtensionsManager.Companion.getInstance().readExternal(this, element);
String runToolValue = JDOMExternalizerUtil.readField(element, RUN_TOOL);
useRunTool = StringUtil.isEmpty(runToolValue) ? null : Boolean.parseBoolean(runToolValue);
}
protected void readEnvs(Element element) {
@@ -313,6 +318,7 @@ public abstract class AbstractPythonRunConfiguration<T extends AbstractPythonRun
PythonRunConfigurationExtensionsManager.Companion.getInstance().writeExternal(this, element);
PathMappingSettings.writeExternal(element, getMappingSettings());
JDOMExternalizerUtil.writeField(element, RUN_TOOL, useRunTool == null ? null : Boolean.toString(useRunTool));
}
protected void writeEnvs(Element element) {
@@ -400,6 +406,7 @@ public abstract class AbstractPythonRunConfiguration<T extends AbstractPythonRun
target.setMappingSettings(source.getMappingSettings());
target.setAddContentRoots(source.shouldAddContentRoots());
target.setAddSourceRoots(source.shouldAddSourceRoots());
target.setUseRunTool(source.getUseRunTool());
}
/**
@@ -494,6 +501,18 @@ public abstract class AbstractPythonRunConfiguration<T extends AbstractPythonRun
return module != null ? module.getName() : null;
}
@ApiStatus.Internal
@Override
public final @Nullable Boolean getUseRunTool() {
return useRunTool;
}
@ApiStatus.Internal
@Override
public final void setUseRunTool(@Nullable Boolean useRunTool) {
this.useRunTool = useRunTool;
}
@Override
public boolean isBuildBeforeLaunchAddedByDefault() {
return false;
@@ -2,6 +2,7 @@
package com.jetbrains.python.run;
import com.intellij.openapi.module.Module;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
/**
@@ -10,4 +11,10 @@ import org.jetbrains.annotations.Nullable;
public interface AbstractPythonRunConfigurationParams extends PythonRunParams {
@Nullable
Module getModule();
@ApiStatus.Internal
@Nullable Boolean getUseRunTool();
@ApiStatus.Internal
void setUseRunTool(@Nullable Boolean useRunTool);
}
@@ -67,6 +67,8 @@ import com.jetbrains.python.library.PythonLibraryType;
import com.jetbrains.python.packaging.PyExecutionException;
import com.jetbrains.python.remote.PyRemotePathMapper;
import com.jetbrains.python.remote.PyRemoteSdkAdditionalData;
import com.jetbrains.python.run.features.PyRunToolParameters;
import com.jetbrains.python.run.features.PyRunToolProvider;
import com.jetbrains.python.run.target.HelpersAwareTargetEnvironmentRequest;
import com.jetbrains.python.run.target.PySdkTargetPaths;
import com.jetbrains.python.run.target.PythonCommandLineTargetEnvironmentProvider;
@@ -90,6 +92,7 @@ import java.util.function.Function;
import static com.intellij.execution.util.EnvFilesUtilKt.configureEnvsFromFiles;
import static com.jetbrains.python.run.PythonScriptCommandLineState.getExpandedWorkingDir;
import static com.jetbrains.python.run.features.PyRunToolExtKt.useRunTool;
/**
* Since this state is async, any method could be called on any thread
@@ -342,10 +345,21 @@ public abstract class PythonCommandLineState extends CommandLineState {
helpersAwareTargetRequest.getTargetEnvironmentRequest().prepareEnvironment(TargetProgressIndicator.EMPTY);
// TODO Detect and discard existing overrides of configured parameters.
List<String> allInterpreterParameters = Streams.concat(getConfiguredInterpreterParameters().stream(),
realPythonExecution.getAdditionalInterpreterParameters().stream()).toList();
List<String> allInterpreterParameters =
Streams.concat(getConfiguredInterpreterParameters().stream(), realPythonExecution.getAdditionalInterpreterParameters().stream())
.toList();
PyRunToolParameters runToolParameters = null;
if (sdk != null) {
PyRunToolProvider runToolProvider = PyRunToolProvider.forSdk(sdk);
if (runToolProvider != null && useRunTool(myConfig, sdk)) {
runToolParameters = runToolProvider.getRunToolParameters();
}
}
TargetedCommandLine targetedCommandLine =
PythonScripts.buildTargetedCommandLine(realPythonExecution, targetEnvironment, sdk, allInterpreterParameters, myRunWithPty);
PythonScripts.buildTargetedCommandLine(realPythonExecution, targetEnvironment, sdk, allInterpreterParameters, myRunWithPty,
runToolParameters);
// TODO [Targets API] `myConfig.isPassParentEnvs` must be handled (at least for the local case)
ProcessHandler processHandler = doStartProcess(targetEnvironment, targetedCommandLine, progressIndicator);
@@ -61,7 +61,7 @@ public class PythonRunConfiguration extends AbstractPythonRunConfiguration<Pytho
public static PythonRunConfiguration createRunConfigurationForTests(Project project, ConfigurationFactory factory) {
return new PythonRunConfiguration(project, factory);
}
@Override
protected boolean isNewUiSupported() {
return true;
@@ -22,9 +22,11 @@ import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.util.io.FileUtil
import com.jetbrains.python.HelperPackage
import com.jetbrains.python.PyBundle
import com.jetbrains.python.debugger.PyDebugRunner
import com.jetbrains.python.packaging.PyExecutionException
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.run.features.PyRunToolParameters
import com.jetbrains.python.run.target.HelpersAwareTargetEnvironmentRequest
import com.jetbrains.python.run.target.PathMapping
import com.jetbrains.python.run.target.tryResolveAsPythonHelperDir
@@ -43,10 +45,13 @@ private val LOG = Logger.getInstance("#com.jetbrains.python.run.PythonScripts")
@JvmOverloads
@ApiStatus.Internal
fun PythonExecution.buildTargetedCommandLine(targetEnvironment: TargetEnvironment,
sdk: Sdk?,
interpreterParameters: List<String>,
isUsePty: Boolean = false): TargetedCommandLine {
fun PythonExecution.buildTargetedCommandLine(
targetEnvironment: TargetEnvironment,
sdk: Sdk?,
interpreterParameters: List<String>,
isUsePty: Boolean = false,
runTool: PyRunToolParameters? = null,
): TargetedCommandLine {
val commandLineBuilder = TargetedCommandLineBuilder(targetEnvironment.request)
workingDir?.apply(targetEnvironment)?.let { commandLineBuilder.setWorkingDirectory(it) }
commandLineBuilder.charset = charset
@@ -65,6 +70,10 @@ fun PythonExecution.buildTargetedCommandLine(targetEnvironment: TargetEnvironmen
}
}
if (runTool != null) {
applyRunToolAsync(commandLineBuilder, runTool)
}
when (this) {
is PythonScriptExecution -> pythonScriptPath?.let { commandLineBuilder.addParameter(it.apply(targetEnvironment)) }
?: throw IllegalArgumentException("Python script path must be set")
@@ -113,6 +122,20 @@ fun PythonExecution.buildTargetedCommandLine(targetEnvironment: TargetEnvironmen
return commandLineBuilder.build()
}
private fun applyRunToolAsync(
commandLineBuilder: TargetedCommandLineBuilder,
runTool: PyRunToolParameters,
) = commandLineBuilder.exePath.localValue
.onSuccess { originalExe: String? ->
commandLineBuilder.exePath = TargetValue.fixed(runTool.exe)
commandLineBuilder.addFixedParametersAt(0, runTool.args)
if (originalExe != null) {
commandLineBuilder.addParameterAt(runTool.args.size, originalExe)
}
}
.onError {
throw ExecutionException(PyBundle.message("dialog.message.failed.to.resolve.original.executable.for.run.tool", runTool.exe))
}
@ApiStatus.Internal
data class Upload(val localPath: String, val targetPath: TargetEnvironmentFunction<String>)
@@ -4,12 +4,15 @@ package com.jetbrains.python.run.configuration
import com.intellij.diagnostic.logging.LogsGroupFragment
import com.intellij.execution.ui.*
import com.intellij.openapi.components.service
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.ui.RawCommandLineEditor
import com.intellij.ui.components.TextComponentEmptyText
import com.jetbrains.python.PyBundle
import com.jetbrains.python.run.AbstractPythonRunConfiguration
import com.jetbrains.python.run.PyCommonFragmentsBuilder
import com.jetbrains.python.run.PythonRunConfigurationExtensionsManager
import com.jetbrains.python.run.features.PyRunToolProvider
import com.jetbrains.python.run.features.useRunTool
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
@@ -28,6 +31,10 @@ abstract class AbstractPythonConfigurationFragmentedEditor<T : AbstractPythonRun
addInterpreterOptions(fragments)
addContentSourceRoots(fragments)
runConfiguration.sdk?.let { sdk ->
createRunToolTag(sdk)?.let { fragments.add(it) }
}
customizeFragments(fragments)
fragments.add(PyEditorExtensionFragment())
fragments.add(LogsGroupFragment())
@@ -41,6 +48,18 @@ abstract class AbstractPythonConfigurationFragmentedEditor<T : AbstractPythonRun
abstract fun customizeFragments(fragments: MutableList<SettingsEditorFragment<T, *>>)
private fun createRunToolTag(sdk: Sdk): SettingsEditorFragment<T, *>? {
val provider = PyRunToolProvider.forSdk(sdk) ?: return null
val toolData = provider.runToolData
return SettingsEditorFragment.createTag(
toolData.id.value,
PyBundle.message("python.run.configuration.fragments.run.with.tool", toolData.name),
toolData.group,
{ runConfiguration.useRunTool(sdk) },
{ config, value -> config.useRunTool = value }
)
}
private fun <T : AbstractPythonRunConfiguration<*>> addInterpreterOptions(fragments: MutableList<SettingsEditorFragment<T, *>>) {
val interpreterOptionsField = RawCommandLineEditor()
val interpreterOptionsFragment: SettingsEditorFragment<T, RawCommandLineEditor> = SettingsEditorFragment<T, RawCommandLineEditor>(
@@ -0,0 +1,13 @@
// 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.run.features
import com.intellij.openapi.projectRoots.Sdk
import com.jetbrains.python.run.AbstractPythonRunConfiguration
import com.jetbrains.python.run.features.PyRunToolProvider.Companion.forSdk
internal fun AbstractPythonRunConfiguration<*>.useRunTool(sdk: Sdk): Boolean {
val explicit = this.useRunTool
if (explicit != null) return explicit
val provider = forSdk(sdk) ?: return false
return provider.initialToolState
}
@@ -0,0 +1,81 @@
// 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.run.features
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.projectRoots.Sdk
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.NonNls
@JvmInline
@ApiStatus.Internal
value class PyRunToolId(@param:NonNls val value: String)
/**
* Metadata describing a concrete Python "Run with …" tool option.
*
* - id: a unique identifier of the tool.
* - name: user‑visible action/configuration name (localized).
* - group: user‑visible group name under which the action is shown in UI (localized).
*/
@ApiStatus.Internal
data class PyRunToolData(
@param:NonNls val id: PyRunToolId,
@param:Nls val name: String,
@param:Nls val group: String
)
/**
* Represents the parameters needed to run a Python tool or executable.
*
* @property exe The path to the Python executable or script.
* @property args A list of arguments to be passed to the executable.
*/
@ApiStatus.Internal
data class PyRunToolParameters(
val exe: String,
val args: List<String>,
)
/**
* Represents a provider for Python "Run with …" tools. This interface defines
* the metadata, parameters, and state management for tools available under the
* "Run with" menu in Python environments.
*/
@ApiStatus.Internal
interface PyRunToolProvider {
/**
* Represents the metadata of a specific Python "Run with …" tool option.
* This property provides details such as the unique identifier, user-visible name,
* and UI group for the tool associated with a `PyRunToolProvider`.
*/
val runToolData: PyRunToolData
/**
* Represents the parameters required to configure and run a Python tool.
* This includes the path to the executable and a list of associated arguments.
*/
val runToolParameters: PyRunToolParameters
/**
* Represents the initial state of the tool, determining whether it is enabled or not by default.
*/
val initialToolState: Boolean
/**
* Checks if the tool is available for the given SDK.
*
* @param sdk the SDK to check the availability for
* @return true if the tool is available for the specified SDK, false otherwise
*/
fun isAvailable(sdk: Sdk): Boolean
companion object {
@JvmField
val EP: ExtensionPointName<PyRunToolProvider> = ExtensionPointName.create("Pythonid.pyRunToolProvider")
@JvmStatic
fun forSdk(sdk: Sdk): PyRunToolProvider? = EP.extensionList.firstOrNull { it.isAvailable(sdk) }
}
}
@@ -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.sdk.uv.run
import com.intellij.openapi.projectRoots.Sdk
import com.jetbrains.python.PyBundle
import com.jetbrains.python.run.features.PyRunToolData
import com.jetbrains.python.run.features.PyRunToolId
import com.jetbrains.python.run.features.PyRunToolParameters
import com.jetbrains.python.run.features.PyRunToolProvider
import com.jetbrains.python.sdk.uv.isUv
/**
* PyRunToolProvider implementation that runs scripts/modules using `uv run`.
*/
private class UvRunToolProvider : PyRunToolProvider {
override val runToolData: PyRunToolData = PyRunToolData(
PyRunToolId("py.run.with.uv"),
PyBundle.message("uv.run.configuration.type.display.name"),
PyBundle.message("python.run.configuration.fragments.python.group"),
)
override val runToolParameters: PyRunToolParameters = PyRunToolParameters(
"uv",
listOf("run")
)
override val initialToolState: Boolean = true
override fun isAvailable(sdk: Sdk): Boolean = sdk.isUv
}