PY-78995: (finished): Show promo in interpreter settings, do not throw exception from run configurations.

Open Interpreter settings, try remote and local in free mode.
Try running run config with remote interpreter in free tier

GitOrigin-RevId: e6c611826e942c49f5b576ea769a044357cf6146
This commit is contained in:
Ilya.Kazakevich
2025-07-22 23:02:26 +00:00
committed by intellij-monorepo-bot
parent 115d00ef1c
commit ec660d11c2
10 changed files with 131 additions and 54 deletions
@@ -178,7 +178,7 @@ public class PyIdeCommonOptionsForm implements AbstractPyCommonOptionsForm {
}
public void updateSdkList(boolean preserveSelection, PyConfigurableInterpreterList myInterpreterList) {
myPythonSdks = myInterpreterList.getAllPythonSdks(myProject, null);
myPythonSdks = myInterpreterList.getAllPythonSdks(myProject, null, false);
Sdk selection = preserveSelection ? (Sdk)content.interpreterComboBox.getSelectedItem() : null;
if (!myPythonSdks.contains(selection)) {
selection = null;
@@ -71,20 +71,38 @@ public final class PythonSdkUtil {
private static final Key<PySkeletonHeader> CACHED_SKELETON_HEADER = Key.create("CACHED_SKELETON_HEADER");
public static boolean isPythonSdk(@NotNull Sdk sdk) {
return isPythonSdk(sdk, false);
}
@ApiStatus.Internal
public static boolean isPythonSdk(@NotNull Sdk sdk, boolean allowRemoteInFreeTier) {
if (!PyNames.PYTHON_SDK_ID_NAME.equals(sdk.getSdkType().getName())) {
return false;
}
// PY-79923: Should explicitly filter sdks created while pro was active
if (PlatformUtils.isPyCharm() && !PlatformUtils.isDataSpell()) {
return !isRemote(sdk) || !PluginManagerCore.isDisabled(ULTIMATE_PLUGIN_ID);
if (isFreeTier()) {
return allowRemoteInFreeTier || (!isRemote(sdk));
}
return true;
}
public static @Unmodifiable List<Sdk> getAllSdks() {
return ContainerUtil.filter(ProjectJdkTable.getInstance().getAllJdks(), PythonSdkUtil::isPythonSdk);
/**
* @return PyCharm with Pro mode disabled
*/
@ApiStatus.Internal
public static boolean isFreeTier() {
return PlatformUtils.isPyCharm() && (!PlatformUtils.isDataSpell()) && PluginManagerCore.isDisabled(ULTIMATE_PLUGIN_ID);
}
public static @Unmodifiable @NotNull List<@NotNull Sdk> getAllSdks() {
return getAllSdks(false);
}
@ApiStatus.Internal
public static @Unmodifiable @NotNull List<@NotNull Sdk> getAllSdks(boolean allowRemoteInFreeTier) {
return ContainerUtil.filter(ProjectJdkTable.getInstance().getAllJdks(), sdk -> isPythonSdk(sdk, allowRemoteInFreeTier));
}
private static @Nullable PySkeletonHeader readSkeletonHeader(@NotNull VirtualFile file, @NotNull Sdk pythonSdk) {
@@ -4,7 +4,6 @@ package com.jetbrains.python.configuration;
import com.intellij.icons.AllIcons;
import com.intellij.ide.DataManager;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.application.ApplicationManager;
@@ -24,7 +23,8 @@ import com.intellij.openapi.ui.popup.ListPopup;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Pair;
import com.intellij.ui.*;
import com.intellij.ui.CollectionComboBoxModel;
import com.intellij.ui.ComboboxSpeedSearch;
import com.intellij.ui.components.DropDownLink;
import com.intellij.util.NullableConsumer;
import com.intellij.util.ui.JBUI;
@@ -47,6 +47,7 @@ import java.util.Map;
import java.util.function.Consumer;
import static com.jetbrains.python.sdk.PySdkRenderingKt.groupModuleSdksByTypes;
import static com.jetbrains.python.sdk.PythonSdkUtil.isRemote;
public class PyActiveSdkConfigurable implements UnnamedConfigurable {
@@ -65,6 +66,7 @@ public class PyActiveSdkConfigurable implements UnnamedConfigurable {
private final @NotNull ComboBox<Object> mySdkCombo;
private final @NotNull PyInstalledPackagesPanel myPackagesPanel;
private final @Nullable PyPanelWithPromo myPanelWithPromo;
private final @Nullable Disposable myDisposable;
@@ -85,6 +87,8 @@ public class PyActiveSdkConfigurable implements UnnamedConfigurable {
final PackagesNotificationPanel packagesNotificationPanel = new PyPackagesNotificationPanel();
myPackagesPanel = new PyInstalledPackagesPanel(myProject, packagesNotificationPanel);
myPackagesPanel.setShowGrid(false);
boolean freeTier = PythonSdkUtil.isFreeTier();
myPanelWithPromo = freeTier ? new PyPanelWithPromo(myPackagesPanel) : null;
final PyCustomSdkUiProvider customUiProvider = PyCustomSdkUiProvider.getInstance();
myDisposable = customUiProvider == null ? null : Disposer.newDisposable();
@@ -95,7 +99,9 @@ public class PyActiveSdkConfigurable implements UnnamedConfigurable {
additionalAction = new DropDownLink<>(PyBundle.message("active.sdk.dialog.link.add.interpreter.text"),
link -> createAddInterpreterPopup(project, module, link, this::updateSdkListAndSelect));
myMainPanel = buildPanel(project, mySdkCombo, additionalAction, myPackagesPanel, packagesNotificationPanel, customizer);
myMainPanel =
buildPanel(project, mySdkCombo, additionalAction, freeTier ? myPanelWithPromo.getPanel() : myPackagesPanel, packagesNotificationPanel,
customizer);
myInterpreterList = PyConfigurableInterpreterList.getInstance(myProject);
myProjectSdksModel = myInterpreterList.getModel();
@@ -160,9 +166,10 @@ public class PyActiveSdkConfigurable implements UnnamedConfigurable {
private static @NotNull JPanel buildPanel(@NotNull Project project,
@NotNull ComboBox<?> sdkComboBox,
@NotNull JComponent additionalAction,
@NotNull PyInstalledPackagesPanel installedPackagesPanel,
@NotNull JPanel promotionPanel,
@NotNull PackagesNotificationPanel packagesNotificationPanel,
@Nullable Pair<PyCustomSdkUiProvider, Disposable> customizer) {
final JPanel result = new JPanel(new GridBagLayout());
final GridBagConstraints c = new GridBagConstraints();
@@ -196,7 +203,7 @@ public class PyActiveSdkConfigurable implements UnnamedConfigurable {
c.gridwidth = 3;
c.gridheight = GridBagConstraints.RELATIVE;
c.fill = GridBagConstraints.BOTH;
result.add(installedPackagesPanel, c);
result.add(promotionPanel, c);
c.gridheight = GridBagConstraints.REMAINDER;
c.gridx = 0;
@@ -218,6 +225,18 @@ public class PyActiveSdkConfigurable implements UnnamedConfigurable {
protected void onSdkSelected() {
final Sdk sdk = getOriginalSelectedSdk();
if (sdk != null) {
// Non-null means we are in free tier mode, so must switch between packages and promo panel
if (myPanelWithPromo != null) {
boolean remote = isRemote(sdk);
myPanelWithPromo.setPromoMode(remote);
if (remote) {
return;
}
}
}
refreshPackages(sdk);
}
@@ -273,8 +292,9 @@ public class PyActiveSdkConfigurable implements UnnamedConfigurable {
Sdk sdk = null;
if (myModule == null) {
sdk = ProjectRootManager.getInstance(myProject).getProjectSdk();
} else {
sdk = ModuleRootManager.getInstance(myModule).getSdk();
}
else {
sdk = ModuleRootManager.getInstance(myModule).getSdk();
}
if (sdk != null && PythonSdkUtil.isPythonSdk(sdk)) {
@@ -315,7 +335,7 @@ public class PyActiveSdkConfigurable implements UnnamedConfigurable {
}
protected @NotNull List<Sdk> getAvailableSdks() {
return myInterpreterList.getAllPythonSdks(myProject, myModule);
return myInterpreterList.getAllPythonSdks(myProject, myModule, true);
}
private void updateSdkListAndSelect(@Nullable Sdk selectedSdk) {
@@ -13,6 +13,7 @@ import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory;
import com.jetbrains.python.run.TargetConfigurationWithLocalFsAccessExKt;
import com.jetbrains.python.sdk.*;
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -57,14 +58,15 @@ public final class PyConfigurableInterpreterList {
/**
* @param module if not null and module resides on certain target, returns only SDKs for this target
*/
public @NotNull List<Sdk> getAllPythonSdks(final @Nullable Project project, @Nullable Module module) {
@ApiStatus.Internal
public @NotNull List<Sdk> getAllPythonSdks(final @Nullable Project project, @Nullable Module module, boolean allowRemoteInFreeTier) {
var targetModuleSitsOn = (module != null)
? PythonInterpreterTargetEnvironmentFactory.Companion.getTargetModuleResidesOn(module)
: null;
List<Sdk> result = new ArrayList<>();
for (Sdk sdk : getModel().getSdks()) {
if (!PythonSdkUtil.isPythonSdk(sdk)) {
if (!PythonSdkUtil.isPythonSdk(sdk, allowRemoteInFreeTier)) {
continue;
}
@@ -81,7 +83,7 @@ public final class PyConfigurableInterpreterList {
}
public @NotNull List<Sdk> getAllPythonSdks() {
return getAllPythonSdks(null, null);
return getAllPythonSdks(null, null, false);
}
private static class PyInterpreterComparator implements Comparator<Sdk> {
@@ -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.configuration
import com.intellij.openapi.observable.properties.PropertyGraph
import com.intellij.openapi.observable.util.not
import com.intellij.ui.dsl.builder.*
import com.jetbrains.python.createPromoPanel
import javax.swing.JPanel
/**
* Shows either promo-panel (if [promoMode]) or [whenPromoDisabled]
*/
internal class PyPanelWithPromo(private val whenPromoDisabled: JPanel) {
private val root = PropertyGraph()
private val _promoMode = root.property(false)
var promoMode: Boolean by _promoMode
val panel: JPanel = panel {
row {
cell(createPromoPanel()).resizableColumn().align(AlignY.CENTER + AlignX.FILL).applyToComponent { putClientProperty(DslComponentProperty.VERTICAL_COMPONENT_GAP, VerticalComponentGap(top = false, bottom = false)) }
}.resizableRow().visibleIf(_promoMode)
row {
cell(whenPromoDisabled).resizableColumn().align(AlignY.FILL + AlignX.FILL).applyToComponent { putClientProperty(DslComponentProperty.VERTICAL_COMPONENT_GAP, VerticalComponentGap(top = false, bottom = false)) }
}.resizableRow().visibleIf(_promoMode.not())
}
}
@@ -103,7 +103,7 @@ internal class PythonInterpreterMasterDetails(private val moduleOrProject: Modul
}
private val allPythonSdksInEdit: List<Sdk>
get() = pythonConfigurableInterpreterList.getAllPythonSdks(project, module)
get() = pythonConfigurableInterpreterList.getAllPythonSdks(project, module, false)
override fun reset() {
pythonPathsModified = false
@@ -0,0 +1,38 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python
import com.intellij.ide.browsers.BrowserLauncher
import com.intellij.ide.ui.laf.darcula.ui.DarculaButtonUI
import com.intellij.ui.dsl.builder.*
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.intellij.util.ui.JBFont
import com.jetbrains.python.icons.PythonIcons
import javax.swing.JPanel
/**
* Panel that suggests user to buy Pro version
*/
@RequiresEdt
internal fun createPromoPanel(): JPanel = panel {
row {
icon(PythonIcons.Python.Pycharm32).resizableColumn().align(AlignX.RIGHT + AlignY.TOP)
panel {
row {
@Suppress("DialogTitleCapitalization") // PyCharm Pro is literally how we want to see it
text(PyBundle.message("pycharm.free.mode.upgrade.title")).align(AlignY.BOTTOM).applyToComponent {
font = JBFont.h1()
}
}
row {
text(PyBundle.message("pycharm.free.mode.upgrade.body")).align(AlignY.TOP).applyToComponent { putClientProperty(DslComponentProperty.VERTICAL_COMPONENT_GAP, VerticalComponentGap(top = false, bottom = false)) }
}
row {
button(PyBundle.message("pycharm.free.mode.upgrade.button")) {
BrowserLauncher.instance.open("https://www.jetbrains.com/pycharm/buy/?section=commercial&billing=yearly")
}.applyToComponent {
putClientProperty(DarculaButtonUI.DEFAULT_STYLE_KEY, true)
}
}
}.resizableColumn().align(AlignX.LEFT)
}
}
@@ -64,6 +64,7 @@ import com.jetbrains.python.debugger.PyTargetPathMapper;
import com.jetbrains.python.facet.LibraryContributingFacet;
import com.jetbrains.python.facet.PythonPathContributingFacet;
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.target.HelpersAwareTargetEnvironmentRequest;
@@ -1047,22 +1048,21 @@ public abstract class PythonCommandLineState extends CommandLineState {
return interpreterPath;
}
private @NotNull HelpersAwareTargetEnvironmentRequest getPythonTargetInterpreter() {
return getPythonTargetInterpreter(myConfig.getProject(), getSdk());
private @NotNull HelpersAwareTargetEnvironmentRequest getPythonTargetInterpreter() throws ExecutionException {
Sdk sdk = getSdk();
if (sdk == null) {
throw new PyExecutionException(PyBundle.message("runcfg.error.message.cannot.find.python.interpreter"));
}
return getPythonTargetInterpreter(myConfig.getProject(), sdk);
}
public static @NotNull HelpersAwareTargetEnvironmentRequest getPythonTargetInterpreter(@NotNull Project project, @Nullable Sdk sdk) {
if (sdk == null) {
throw new IllegalStateException("SDK is not defined for Run Configuration");
}
else {
public static @NotNull HelpersAwareTargetEnvironmentRequest getPythonTargetInterpreter(@NotNull Project project, @NotNull Sdk sdk) {
HelpersAwareTargetEnvironmentRequest helpersAwareTargetRequest =
PythonInterpreterTargetEnvironmentFactory.findPythonTargetInterpreter(sdk, project);
if (helpersAwareTargetRequest == null) {
throw new IllegalStateException("Cannot find execution environment for SDK " + sdk);
}
return helpersAwareTargetRequest;
}
}
/**
@@ -11,9 +11,7 @@ import com.intellij.execution.runners.RunConfigurationWithSuppressedDefaultRunAc
import com.intellij.facet.impl.invalid.FacetIgnorer
import com.intellij.facet.impl.invalid.InvalidFacet
import com.intellij.icons.AllIcons
import com.intellij.ide.browsers.BrowserLauncher
import com.intellij.ide.plugins.PluginManager
import com.intellij.ide.ui.laf.darcula.ui.DarculaButtonUI
import com.intellij.openapi.extensions.ExtensionNotApplicableException
import com.intellij.openapi.extensions.PluginId
import com.intellij.openapi.options.SettingsEditor
@@ -21,12 +19,10 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.util.JDOMUtil
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.util.WriteExternalException
import com.intellij.ui.dsl.builder.*
import com.intellij.util.PlatformUtils
import com.intellij.util.ui.JBFont
import com.jetbrains.python.PYTHON_PROF_PLUGIN_ID
import com.jetbrains.python.PyBundle
import com.jetbrains.python.icons.PythonIcons
import com.jetbrains.python.createPromoPanel
import org.jdom.Element
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.NonNls
@@ -40,29 +36,7 @@ private class PythonLockedRunConfigurationEditor : SettingsEditor<PythonLockedRu
override fun applyEditorTo(s: PythonLockedRunConfiguration) {}
protected override fun createEditor(): JComponent = panel {
row {
icon(PythonIcons.Python.Pycharm32).resizableColumn().align(AlignX.RIGHT + AlignY.TOP)
panel {
row {
@Suppress("DialogTitleCapitalization") // PyCharm Pro is literally how we want to see it
text(PyBundle.message("pycharm.free.mode.upgrade.title")).align(AlignY.BOTTOM).applyToComponent {
font = JBFont.h1()
}
}
row {
text(PyBundle.message("pycharm.free.mode.upgrade.body")).align(AlignY.TOP).applyToComponent { putClientProperty(DslComponentProperty.VERTICAL_COMPONENT_GAP, VerticalComponentGap(top = false, bottom = false)) }
}
row {
button(PyBundle.message("pycharm.free.mode.upgrade.button")) {
BrowserLauncher.instance.open("https://www.jetbrains.com/pycharm/buy/?section=commercial&billing=yearly")
}.applyToComponent {
putClientProperty(DarculaButtonUI.DEFAULT_STYLE_KEY, true)
}
}
}.resizableColumn().align(AlignX.LEFT)
}
}
protected override fun createEditor(): JComponent = createPromoPanel()
}
private class PythonLockedRunConfiguration(val configProject: Project, val configFactory: ConfigurationFactory)
@@ -56,7 +56,7 @@ class PySdkPopupFactory(val module: Module) {
val interpreterList = PyConfigurableInterpreterList.getInstance(module.project)
val moduleSdksByTypes = SlowOperations.knownIssue("PY-76167").use {
groupModuleSdksByTypes(interpreterList.getAllPythonSdks(module.project, module), module) {
groupModuleSdksByTypes(interpreterList.getAllPythonSdks(module.project, module, false), module) {
!it.sdkSeemsValid ||
PythonSdkType.hasInvalidRemoteCredentials(it) ||
PythonSdkType.isIncompleteRemote(it) ||