From ec660d11c281963a072514d47dcbfbba81a2272f Mon Sep 17 00:00:00 2001 From: "Ilya.Kazakevich" Date: Tue, 22 Jul 2025 23:38:25 +0200 Subject: [PATCH] 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 --- .../ide/impl/PyIdeCommonOptionsForm.java | 2 +- .../jetbrains/python/sdk/PythonSdkUtil.java | 26 +++++++++++-- .../PyActiveSdkConfigurable.java | 36 ++++++++++++++---- .../PyConfigurableInterpreterList.java | 8 ++-- .../python/configuration/PyPanelWithPromo.kt | 25 ++++++++++++ .../PythonInterpreterMasterDetails.kt | 2 +- .../com/jetbrains/python/createPromoPanel.kt | 38 +++++++++++++++++++ .../python/run/PythonCommandLineState.java | 16 ++++---- .../run/PythonLockedRunConfiguration.kt | 30 +-------------- .../jetbrains/python/sdk/PySdkPopupFactory.kt | 2 +- 10 files changed, 131 insertions(+), 54 deletions(-) create mode 100644 python/src/com/jetbrains/python/configuration/PyPanelWithPromo.kt create mode 100644 python/src/com/jetbrains/python/createPromoPanel.kt diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/PyIdeCommonOptionsForm.java b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/PyIdeCommonOptionsForm.java index 414d42a9667e..ac068e0ac8d3 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/PyIdeCommonOptionsForm.java +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/PyIdeCommonOptionsForm.java @@ -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; diff --git a/python/python-psi-impl/src/com/jetbrains/python/sdk/PythonSdkUtil.java b/python/python-psi-impl/src/com/jetbrains/python/sdk/PythonSdkUtil.java index ce21d75412a4..272ef23f8036 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/sdk/PythonSdkUtil.java +++ b/python/python-psi-impl/src/com/jetbrains/python/sdk/PythonSdkUtil.java @@ -71,20 +71,38 @@ public final class PythonSdkUtil { private static final Key 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 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) { diff --git a/python/src/com/jetbrains/python/configuration/PyActiveSdkConfigurable.java b/python/src/com/jetbrains/python/configuration/PyActiveSdkConfigurable.java index 8c6ca4fc7395..254a718f2c8e 100644 --- a/python/src/com/jetbrains/python/configuration/PyActiveSdkConfigurable.java +++ b/python/src/com/jetbrains/python/configuration/PyActiveSdkConfigurable.java @@ -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 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 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 getAvailableSdks() { - return myInterpreterList.getAllPythonSdks(myProject, myModule); + return myInterpreterList.getAllPythonSdks(myProject, myModule, true); } private void updateSdkListAndSelect(@Nullable Sdk selectedSdk) { diff --git a/python/src/com/jetbrains/python/configuration/PyConfigurableInterpreterList.java b/python/src/com/jetbrains/python/configuration/PyConfigurableInterpreterList.java index 95be031afb1a..8d3fac49962e 100644 --- a/python/src/com/jetbrains/python/configuration/PyConfigurableInterpreterList.java +++ b/python/src/com/jetbrains/python/configuration/PyConfigurableInterpreterList.java @@ -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 getAllPythonSdks(final @Nullable Project project, @Nullable Module module) { + @ApiStatus.Internal + public @NotNull List getAllPythonSdks(final @Nullable Project project, @Nullable Module module, boolean allowRemoteInFreeTier) { var targetModuleSitsOn = (module != null) ? PythonInterpreterTargetEnvironmentFactory.Companion.getTargetModuleResidesOn(module) : null; List 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 getAllPythonSdks() { - return getAllPythonSdks(null, null); + return getAllPythonSdks(null, null, false); } private static class PyInterpreterComparator implements Comparator { diff --git a/python/src/com/jetbrains/python/configuration/PyPanelWithPromo.kt b/python/src/com/jetbrains/python/configuration/PyPanelWithPromo.kt new file mode 100644 index 000000000000..3dbc43c9639f --- /dev/null +++ b/python/src/com/jetbrains/python/configuration/PyPanelWithPromo.kt @@ -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()) + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/configuration/PythonInterpreterMasterDetails.kt b/python/src/com/jetbrains/python/configuration/PythonInterpreterMasterDetails.kt index f3138b6447ee..d9f1f104f143 100644 --- a/python/src/com/jetbrains/python/configuration/PythonInterpreterMasterDetails.kt +++ b/python/src/com/jetbrains/python/configuration/PythonInterpreterMasterDetails.kt @@ -103,7 +103,7 @@ internal class PythonInterpreterMasterDetails(private val moduleOrProject: Modul } private val allPythonSdksInEdit: List - get() = pythonConfigurableInterpreterList.getAllPythonSdks(project, module) + get() = pythonConfigurableInterpreterList.getAllPythonSdks(project, module, false) override fun reset() { pythonPathsModified = false diff --git a/python/src/com/jetbrains/python/createPromoPanel.kt b/python/src/com/jetbrains/python/createPromoPanel.kt new file mode 100644 index 000000000000..1a1c4cef2f5d --- /dev/null +++ b/python/src/com/jetbrains/python/createPromoPanel.kt @@ -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) + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/run/PythonCommandLineState.java b/python/src/com/jetbrains/python/run/PythonCommandLineState.java index c4e6bcab89db..0dab54a68d1d 100644 --- a/python/src/com/jetbrains/python/run/PythonCommandLineState.java +++ b/python/src/com/jetbrains/python/run/PythonCommandLineState.java @@ -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; - } } /** diff --git a/python/src/com/jetbrains/python/run/PythonLockedRunConfiguration.kt b/python/src/com/jetbrains/python/run/PythonLockedRunConfiguration.kt index 1161e0122e34..89d6a14adcb7 100644 --- a/python/src/com/jetbrains/python/run/PythonLockedRunConfiguration.kt +++ b/python/src/com/jetbrains/python/run/PythonLockedRunConfiguration.kt @@ -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