Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk
Expand All @@ -16,44 +15,30 @@ import com.jetbrains.python.sdk.PythonSdkUtil
import com.jetbrains.python.statistics.executionType
import com.jetbrains.python.statistics.interpreterType

import com.github.pyvenvmanage.VenvUtils
import com.github.pyvenvmanage.sdk.EnvironmentDetector
import com.github.pyvenvmanage.sdk.PythonEnvironmentType
import com.github.pyvenvmanage.sdk.SdkFactory

abstract class ConfigurePythonActionAbstract : AnAction() {
companion object {
private val LOG = Logger.getInstance(ConfigurePythonActionAbstract::class.java)
}

override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT

override fun update(e: AnActionEvent) {
val selectedPath = e.getData(CommonDataKeys.VIRTUAL_FILE)
LOG.info("Update called for path: $selectedPath")
val isValid =
selectedPath?.let { path ->
val dir = if (path.isDirectory) path else path.parent
LOG.info("Checking directory: ${dir.path}")
PythonSdkUtil.getPythonExecutable(dir.path)?.let { pythonExe ->
LOG.info("Found python executable: $pythonExe")
val envType = EnvironmentDetector.detectEnvironmentType(pythonExe)
val icon = SdkFactory.getIconForEnvironmentType(envType)
LOG.info("Setting icon for type $envType: $icon")
e.presentation.icon = icon
true
} ?: false.also { LOG.info("No python executable found") }
} ?: false.also { LOG.info("No selected path") }

e.presentation.isEnabledAndVisible = isValid
LOG.info("Action visible: $isValid")
// Offer the action only on directories that are actual virtual environments, using the same
// pyvenv.cfg check as VenvProjectViewNodeDecorator. getPythonExecutable alone also matches
// non-venv directories (e.g. a project root with a configured interpreter), which surfaced the
// action on ordinary files via their parent directory.
val venvDir = e.getData(CommonDataKeys.VIRTUAL_FILE)?.takeIf { VenvUtils.getPyVenvCfg(it) != null }
venvDir?.let { PythonSdkUtil.getPythonExecutable(it.path) }?.let { pythonExecutable ->
val envType = EnvironmentDetector.detectEnvironmentType(pythonExecutable)
e.presentation.icon = SdkFactory.getIconForEnvironmentType(envType)
}
e.presentation.isEnabledAndVisible = venvDir != null
}

override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return
val selectedPath =
e.getData(CommonDataKeys.VIRTUAL_FILE)?.let {
if (it.isDirectory) it else it.parent
} ?: return
val selectedPath = e.getData(CommonDataKeys.VIRTUAL_FILE)?.takeIf { it.isDirectory } ?: return

val pythonExecutable = PythonSdkUtil.getPythonExecutable(selectedPath.path)
if (pythonExecutable == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import com.intellij.python.venv.icons.PythonVenvIcons
import com.jetbrains.python.configuration.PyConfigurableInterpreterList
import com.jetbrains.python.sdk.PythonSdkUtil

import com.github.pyvenvmanage.VenvUtils
import com.github.pyvenvmanage.sdk.EnvironmentDetector
import com.github.pyvenvmanage.sdk.PythonEnvironmentType
import com.github.pyvenvmanage.sdk.SdkFactory
Expand Down Expand Up @@ -68,6 +69,7 @@ class ConfigurePythonActionAbstractTest {
@BeforeEach
fun setUpMocks() {
mockkStatic(PythonSdkUtil::class)
mockkObject(VenvUtils)
mockkObject(EnvironmentDetector)
mockkObject(SdkFactory)
}
Expand All @@ -87,10 +89,10 @@ class ConfigurePythonActionAbstractTest {
}

@Test
fun `enables action for directory with Python executable`() {
fun `enables action for virtualenv directory`() {
every { event.getData(CommonDataKeys.VIRTUAL_FILE) } returns virtualFile
every { virtualFile.isDirectory } returns true
every { virtualFile.path } returns "/some/venv"
every { VenvUtils.getPyVenvCfg(virtualFile) } returns Path.of("/some/venv/pyvenv.cfg")
every { PythonSdkUtil.getPythonExecutable("/some/venv") } returns "/some/venv/bin/python"
every { EnvironmentDetector.detectEnvironmentType("/some/venv/bin/python") } returns
PythonEnvironmentType.UV
Expand All @@ -103,22 +105,31 @@ class ConfigurePythonActionAbstractTest {
}

@Test
fun `disables action for directory without Python executable`() {
fun `disables action for non-venv directory`() {
every { event.getData(CommonDataKeys.VIRTUAL_FILE) } returns virtualFile
every { virtualFile.isDirectory } returns true
every { virtualFile.path } returns "/some/dir"
every { PythonSdkUtil.getPythonExecutable("/some/dir") } returns null
every { VenvUtils.getPyVenvCfg(virtualFile) } returns null

action.update(event)

verify { presentation.isEnabledAndVisible = false }
}

@Test
fun `disables action for non-directory selection`() {
every { event.getData(CommonDataKeys.VIRTUAL_FILE) } returns virtualFile
every { VenvUtils.getPyVenvCfg(virtualFile) } returns null

action.update(event)

verify { presentation.isEnabledAndVisible = false }
verify(exactly = 0) { PythonSdkUtil.getPythonExecutable(any()) }
}

@Test
fun `sets icon based on detected environment type`() {
every { event.getData(CommonDataKeys.VIRTUAL_FILE) } returns virtualFile
every { virtualFile.isDirectory } returns true
every { virtualFile.path } returns "/some/venv"
every { VenvUtils.getPyVenvCfg(virtualFile) } returns Path.of("/some/venv/pyvenv.cfg")
every { PythonSdkUtil.getPythonExecutable("/some/venv") } returns "/some/venv/bin/python"
every { EnvironmentDetector.detectEnvironmentType("/some/venv/bin/python") } returns
PythonEnvironmentType.UV
Expand All @@ -133,7 +144,6 @@ class ConfigurePythonActionAbstractTest {

@Nested
inner class ActionPerformedTest {
private lateinit var parentDir: VirtualFile
private lateinit var interpreterList: PyConfigurableInterpreterList
private lateinit var notificationGroupManager: NotificationGroupManager
private lateinit var notificationGroup: NotificationGroup
Expand All @@ -142,7 +152,6 @@ class ConfigurePythonActionAbstractTest {

@BeforeEach
fun setUpMocks() {
parentDir = mockk(relaxed = true)
interpreterList = mockk(relaxed = true)
notificationGroupManager = mockk(relaxed = true)
notificationGroup = mockk(relaxed = true)
Expand Down Expand Up @@ -191,16 +200,13 @@ class ConfigurePythonActionAbstractTest {
}

@Test
fun `uses parent directory when file is not a directory`() {
fun `ignores non-directory selection`() {
every { event.getData(CommonDataKeys.VIRTUAL_FILE) } returns virtualFile
every { virtualFile.isDirectory } returns false
every { virtualFile.parent } returns parentDir
every { parentDir.path } returns "/some/venv"
every { PythonSdkUtil.getPythonExecutable("/some/venv") } returns null

action.actionPerformed(event)

verify { PythonSdkUtil.getPythonExecutable("/some/venv") }
verify(exactly = 0) { PythonSdkUtil.getPythonExecutable(any()) }
}

@Test
Expand Down
Loading