Skip to content

Commit 992a0f8

Browse files
authored
Make Python dependency optional to fix marketplace verification (#122)
1 parent da33fd9 commit 992a0f8

6 files changed

Lines changed: 257 additions & 30 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.github.pyvenvmanage
2+
3+
import com.intellij.notification.NotificationAction
4+
import com.intellij.notification.NotificationGroupManager
5+
import com.intellij.notification.NotificationType
6+
import com.intellij.openapi.extensions.PluginId
7+
import com.intellij.openapi.project.Project
8+
import com.intellij.openapi.startup.ProjectActivity
9+
10+
import com.github.pyvenvmanage.settings.PyVenvManageSettings
11+
12+
class PythonRequiredStartupActivity : ProjectActivity {
13+
override suspend fun execute(project: Project) {
14+
if (isPythonPluginAvailable()) return
15+
if (PyVenvManageSettings.getInstance().dismissedPythonWarning) return
16+
NotificationGroupManager
17+
.getInstance()
18+
.getNotificationGroup("PyVenv Manage")
19+
.createNotification(
20+
"PyVenv Manage requires Python support",
21+
"Please install the Python plugin or use PyCharm for full functionality.",
22+
NotificationType.WARNING,
23+
).addAction(
24+
NotificationAction.createExpiring("Don't show again") { _, notification ->
25+
PyVenvManageSettings.getInstance().dismissedPythonWarning = true
26+
notification.expire()
27+
},
28+
).notify(project)
29+
}
30+
31+
private fun isPythonPluginAvailable(): Boolean =
32+
com.intellij.ide.plugins.PluginManagerCore
33+
.getPlugin(
34+
PluginId.getId("com.intellij.modules.python"),
35+
)?.isEnabled == true ||
36+
com.intellij.ide.plugins.PluginManagerCore
37+
.getPlugin(
38+
PluginId.getId("PythonCore"),
39+
)?.isEnabled == true
40+
}

src/main/kotlin/com/github/pyvenvmanage/settings/PyVenvManageSettings.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class PyVenvManageSettings : PersistentStateComponent<PyVenvManageSettings.Setti
3030
var suffix: String = "]",
3131
var separator: String = " - ",
3232
var fields: List<String> = DecorationField.entries.map { it.name },
33+
var dismissedPythonWarning: Boolean = false,
3334
)
3435

3536
override fun getState(): SettingsState = state
@@ -62,6 +63,12 @@ class PyVenvManageSettings : PersistentStateComponent<PyVenvManageSettings.Setti
6263
state.fields = value.map { it.name }
6364
}
6465

66+
var dismissedPythonWarning: Boolean
67+
get() = state.dismissedPythonWarning
68+
set(value) {
69+
state.dismissedPythonWarning = value
70+
}
71+
6572
fun formatDecoration(info: VenvInfo): String {
6673
val values =
6774
fields.mapNotNull { field ->

src/main/resources/META-INF/plugin.xml

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,39 +3,13 @@
33
<name>PyVenv Manage 2</name>
44
<vendor url="https://github.com/pyvenvmanage/PyVenvManage">pyvenvmanage</vendor>
55

6-
<depends>com.intellij.modules.python</depends>
76
<depends>com.intellij.modules.platform</depends>
7+
<depends optional="true" config-file="pyvenvmanage-python.xml">com.intellij.modules.python</depends>
88

99
<extensions defaultExtensionNs="com.intellij">
10-
<notificationGroup id="Python SDK change"
11-
displayType="BALLOON"/>
12-
<projectViewNodeDecorator implementation="com.github.pyvenvmanage.VenvProjectViewNodeDecorator"/>
13-
<applicationConfigurable
14-
parentId="tools"
15-
instance="com.github.pyvenvmanage.settings.PyVenvManageConfigurable"
16-
id="com.github.pyvenvmanage.settings.PyVenvManageConfigurable"
17-
displayName="PyVenv Manage"/>
10+
<notificationGroup id="PyVenv Manage"
11+
displayType="STICKY_BALLOON"/>
12+
<postStartupActivity implementation="com.github.pyvenvmanage.PythonRequiredStartupActivity"/>
1813
</extensions>
1914

20-
<actions>
21-
<action
22-
id="com.github.pyvenvmanage.actions.ConfigurePythonActionProject"
23-
class="com.github.pyvenvmanage.actions.ConfigurePythonActionProject"
24-
text="Set as Project Interpreter"
25-
description="Configure this Python to be the projects interpreter."
26-
icon="com.jetbrains.python.icons.PythonIcons.Python.Virtualenv"
27-
>
28-
<add-to-group group-id="ProjectViewPopupMenu" anchor="last"/>
29-
</action>
30-
<action
31-
id="com.github.pyvenvmanage.actions.ConfigurePythonActionModule"
32-
class="com.github.pyvenvmanage.actions.ConfigurePythonActionModule"
33-
text="Set as Module Interpreter"
34-
description="Configure this Python to be the current modules interpreter."
35-
icon="com.jetbrains.python.icons.PythonIcons.Python.Virtualenv"
36-
>
37-
<add-to-group group-id="ProjectViewPopupMenu" anchor="last"/>
38-
</action>
39-
</actions>
40-
4115
</idea-plugin>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<idea-plugin>
2+
<extensions defaultExtensionNs="com.intellij">
3+
<notificationGroup id="Python SDK change"
4+
displayType="BALLOON"/>
5+
<projectViewNodeDecorator implementation="com.github.pyvenvmanage.VenvProjectViewNodeDecorator"/>
6+
<applicationConfigurable
7+
parentId="tools"
8+
instance="com.github.pyvenvmanage.settings.PyVenvManageConfigurable"
9+
id="com.github.pyvenvmanage.settings.PyVenvManageConfigurable"
10+
displayName="PyVenv Manage"/>
11+
</extensions>
12+
13+
<actions>
14+
<action
15+
id="com.github.pyvenvmanage.actions.ConfigurePythonActionProject"
16+
class="com.github.pyvenvmanage.actions.ConfigurePythonActionProject"
17+
text="Set as Project Interpreter"
18+
description="Configure this Python to be the projects interpreter."
19+
icon="com.jetbrains.python.icons.PythonIcons.Python.Virtualenv"
20+
>
21+
<add-to-group group-id="ProjectViewPopupMenu" anchor="last"/>
22+
</action>
23+
<action
24+
id="com.github.pyvenvmanage.actions.ConfigurePythonActionModule"
25+
class="com.github.pyvenvmanage.actions.ConfigurePythonActionModule"
26+
text="Set as Module Interpreter"
27+
description="Configure this Python to be the current modules interpreter."
28+
icon="com.jetbrains.python.icons.PythonIcons.Python.Virtualenv"
29+
>
30+
<add-to-group group-id="ProjectViewPopupMenu" anchor="last"/>
31+
</action>
32+
</actions>
33+
</idea-plugin>
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package com.github.pyvenvmanage
2+
3+
import io.mockk.every
4+
import io.mockk.mockk
5+
import io.mockk.mockkObject
6+
import io.mockk.mockkStatic
7+
import io.mockk.slot
8+
import io.mockk.unmockkObject
9+
import io.mockk.unmockkStatic
10+
import io.mockk.verify
11+
import kotlinx.coroutines.runBlocking
12+
import org.junit.jupiter.api.AfterEach
13+
import org.junit.jupiter.api.BeforeEach
14+
import org.junit.jupiter.api.Test
15+
16+
import com.intellij.ide.plugins.IdeaPluginDescriptor
17+
import com.intellij.ide.plugins.PluginManagerCore
18+
import com.intellij.notification.Notification
19+
import com.intellij.notification.NotificationAction
20+
import com.intellij.notification.NotificationGroup
21+
import com.intellij.notification.NotificationGroupManager
22+
import com.intellij.notification.NotificationType
23+
import com.intellij.openapi.extensions.PluginId
24+
import com.intellij.openapi.project.Project
25+
26+
import com.github.pyvenvmanage.settings.PyVenvManageSettings
27+
28+
class PythonRequiredStartupActivityTest {
29+
private lateinit var project: Project
30+
private lateinit var notificationGroupManager: NotificationGroupManager
31+
private lateinit var notificationGroup: NotificationGroup
32+
private lateinit var notification: Notification
33+
private lateinit var settings: PyVenvManageSettings
34+
35+
@BeforeEach
36+
fun setUp() {
37+
project = mockk(relaxed = true)
38+
notificationGroupManager = mockk(relaxed = true)
39+
notificationGroup = mockk(relaxed = true)
40+
notification = mockk(relaxed = true)
41+
settings = mockk(relaxed = true)
42+
43+
mockkStatic(NotificationGroupManager::class)
44+
mockkStatic(PluginManagerCore::class)
45+
mockkObject(PyVenvManageSettings)
46+
47+
every { NotificationGroupManager.getInstance() } returns notificationGroupManager
48+
every { notificationGroupManager.getNotificationGroup("PyVenv Manage") } returns notificationGroup
49+
every {
50+
notificationGroup.createNotification(any<String>(), any<String>(), any<NotificationType>())
51+
} returns notification
52+
every { notification.addAction(any<NotificationAction>()) } returns notification
53+
every { PyVenvManageSettings.getInstance() } returns settings
54+
every { settings.dismissedPythonWarning } returns false
55+
}
56+
57+
@AfterEach
58+
fun tearDown() {
59+
unmockkStatic(NotificationGroupManager::class)
60+
unmockkStatic(PluginManagerCore::class)
61+
unmockkObject(PyVenvManageSettings)
62+
}
63+
64+
@Test
65+
fun `does not show notification when python module is available`(): Unit =
66+
runBlocking {
67+
val pythonPlugin: IdeaPluginDescriptor = mockk(relaxed = true)
68+
every { pythonPlugin.isEnabled } returns true
69+
every {
70+
PluginManagerCore.getPlugin(PluginId.getId("com.intellij.modules.python"))
71+
} returns pythonPlugin
72+
73+
PythonRequiredStartupActivity().execute(project)
74+
75+
verify(exactly = 0) { notification.notify(any()) }
76+
}
77+
78+
@Test
79+
fun `does not show notification when PythonCore plugin is available`(): Unit =
80+
runBlocking {
81+
val pythonCorePlugin: IdeaPluginDescriptor = mockk(relaxed = true)
82+
every { pythonCorePlugin.isEnabled } returns true
83+
every { PluginManagerCore.getPlugin(PluginId.getId("com.intellij.modules.python")) } returns null
84+
every { PluginManagerCore.getPlugin(PluginId.getId("PythonCore")) } returns pythonCorePlugin
85+
86+
PythonRequiredStartupActivity().execute(project)
87+
88+
verify(exactly = 0) { notification.notify(any()) }
89+
}
90+
91+
@Test
92+
fun `shows warning notification when python is not available`(): Unit =
93+
runBlocking {
94+
every { PluginManagerCore.getPlugin(PluginId.getId("com.intellij.modules.python")) } returns null
95+
every { PluginManagerCore.getPlugin(PluginId.getId("PythonCore")) } returns null
96+
97+
PythonRequiredStartupActivity().execute(project)
98+
99+
verify {
100+
notificationGroup.createNotification(
101+
"PyVenv Manage requires Python support",
102+
"Please install the Python plugin or use PyCharm for full functionality.",
103+
NotificationType.WARNING,
104+
)
105+
}
106+
verify { notification.notify(project) }
107+
}
108+
109+
@Test
110+
fun `shows warning when python plugin exists but is disabled`(): Unit =
111+
runBlocking {
112+
val disabledPlugin: IdeaPluginDescriptor = mockk(relaxed = true)
113+
every { disabledPlugin.isEnabled } returns false
114+
every { PluginManagerCore.getPlugin(PluginId.getId("com.intellij.modules.python")) } returns disabledPlugin
115+
every { PluginManagerCore.getPlugin(PluginId.getId("PythonCore")) } returns null
116+
117+
PythonRequiredStartupActivity().execute(project)
118+
119+
verify { notification.notify(project) }
120+
}
121+
122+
@Test
123+
fun `does not show notification when warning was dismissed`(): Unit =
124+
runBlocking {
125+
every { PluginManagerCore.getPlugin(PluginId.getId("com.intellij.modules.python")) } returns null
126+
every { PluginManagerCore.getPlugin(PluginId.getId("PythonCore")) } returns null
127+
every { settings.dismissedPythonWarning } returns true
128+
129+
PythonRequiredStartupActivity().execute(project)
130+
131+
verify(exactly = 0) { notification.notify(any()) }
132+
}
133+
134+
@Test
135+
fun `notification includes dont show again action`(): Unit =
136+
runBlocking {
137+
every { PluginManagerCore.getPlugin(PluginId.getId("com.intellij.modules.python")) } returns null
138+
every { PluginManagerCore.getPlugin(PluginId.getId("PythonCore")) } returns null
139+
140+
PythonRequiredStartupActivity().execute(project)
141+
142+
verify { notification.addAction(any<NotificationAction>()) }
143+
}
144+
145+
@Test
146+
fun `dont show again action sets dismissed flag and expires notification`(): Unit =
147+
runBlocking {
148+
every { PluginManagerCore.getPlugin(PluginId.getId("com.intellij.modules.python")) } returns null
149+
every { PluginManagerCore.getPlugin(PluginId.getId("PythonCore")) } returns null
150+
151+
val actionSlot = slot<NotificationAction>()
152+
every { notification.addAction(capture(actionSlot)) } returns notification
153+
154+
PythonRequiredStartupActivity().execute(project)
155+
156+
val event: com.intellij.openapi.actionSystem.AnActionEvent = mockk(relaxed = true)
157+
actionSlot.captured.actionPerformed(event, notification)
158+
159+
verify { settings.dismissedPythonWarning = true }
160+
verify { notification.expire() }
161+
}
162+
}

src/test/kotlin/com/github/pyvenvmanage/settings/PyVenvManageSettingsTest.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,17 @@ class PyVenvManageSettingsTest {
173173
assertEquals(listOf(DecorationField.VERSION, DecorationField.IMPLEMENTATION), settings.fields)
174174
}
175175

176+
@Test
177+
fun `default dismissedPythonWarning is false`() {
178+
assertEquals(false, settings.dismissedPythonWarning)
179+
}
180+
181+
@Test
182+
fun `dismissedPythonWarning can be set`() {
183+
settings.dismissedPythonWarning = true
184+
assertEquals(true, settings.dismissedPythonWarning)
185+
}
186+
176187
@Test
177188
fun `getInstance returns settings instance`() {
178189
val application: Application = mockk(relaxed = true)

0 commit comments

Comments
 (0)