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
22 changes: 13 additions & 9 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ repositories {
mavenCentral()
intellijPlatform {
defaultRepositories()
// platformVersion targets a 2026.2 EAP build, which lives in the snapshots channel.
snapshots()
}
}

Expand All @@ -65,7 +67,11 @@ dependencies {
testImplementation(libs.remoteRobot)
testImplementation(libs.remoteRobotFixtures)
intellijPlatform {
pycharm(platformVersion)
// platformVersion is a 2026.2 EAP build, available only as a snapshot maven artifact
// (no installer at download.jetbrains.com), so resolve it from the repository.
// Community (not Professional) carries every Python SDK API the plugin uses and has no
// EAP evaluation-login wall, which would otherwise block the headless UI tests.
pycharmCommunity(platformVersion) { useInstaller = false }
bundledPlugin("PythonCore")
pluginVerifier()
zipSigner()
Expand Down Expand Up @@ -103,10 +109,7 @@ intellijPlatform {

ideaVersion {
sinceBuild = providers.gradleProperty("pluginSinceBuild")
// The 2.3.x line targets the 2026.1 (261) Python SDK API. Build 262 changed
// UvSdkAdditionalData, VirtualEnvSdkFlavor, PythonSdkUtil.isVirtualEnv and the uv
// icon package incompatibly, so cap here and ship 262 support from the 2.4.x line.
untilBuild = "261.*"
untilBuild = provider { null }
}
}

Expand All @@ -130,10 +133,11 @@ intellijPlatform {
}
pluginVerification {
// The verifier's ignoredProblemsFile filters CompatibilityProblem instances only,
// not ApiUsage (which is what INTERNAL_API_USAGES is). The 21 internal usages from
// not ApiUsage (which is what INTERNAL_API_USAGES is). The internal usages from
// SdkFactory/EnvironmentDetector reach into per-tool PyCharm SDK packages
// (uv, hatch.sdk, poetry, pipenv) and per-tool icon classes, all sealed behind
// @ApiStatus.Internal package-info markers with no public alternative on 261.
// (uv, hatch.sdk, poetry, pipenv) and per-tool icon classes, plus the
// PluginManagerCore plugin lookup, all sealed behind @ApiStatus.Internal with no
// public alternative on 262.
failureLevel =
listOf(
FailureLevel.COMPATIBILITY_PROBLEMS,
Expand Down Expand Up @@ -161,7 +165,7 @@ intellijPlatform {
IntelliJPlatformType.PyCharmProfessional,
)
}
ideTypes.forEach { create(it, platformVersion) }
ideTypes.forEach { create(it, platformVersion) { useInstaller = false } }
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ nl.littlerobots.vcu.resolver=true
org.gradle.caching=true
org.gradle.configuration-cache=true
org.gradle.welcome=never
platformVersion=2026.1
platformVersion=262.6653.28-EAP-SNAPSHOT
pluginGroup=com.github.pyvenvmanage
pluginName=PyVenv Manage 2
pluginRepositoryUrl=https://github.com/pyvenvmanage/PyVenvManage
pluginSinceBuild=261
pluginVersion=2.3.2-dev
pluginSinceBuild=262
pluginVersion=2.4.0-dev
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.util.SystemInfo
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread

import com.jetbrains.python.sdk.PythonSdkUtil

enum class PythonEnvironmentType {
UV,
CONDA,
Expand Down Expand Up @@ -71,7 +69,7 @@ object EnvironmentDetector {
PythonEnvironmentType.PIPENV
}

PythonSdkUtil.isVirtualEnv(pythonExecutablePath).also { LOG.info("isVirtualEnv: $it") } -> {
isVirtualEnv(venvRoot).also { LOG.info("isVirtualEnv: $it") } -> {
PythonEnvironmentType.VIRTUALENV
}

Expand Down Expand Up @@ -131,6 +129,8 @@ object EnvironmentDetector {
return dirs.any { venvRoot.startsWith(it) }
}

private fun isVirtualEnv(venvRoot: Path): Boolean = venvRoot.resolve("pyvenv.cfg").exists()

private fun computePoetryDirs(): List<Path> {
val dirs = mutableListOf<Path>()

Expand Down
8 changes: 4 additions & 4 deletions src/main/kotlin/com/github/pyvenvmanage/sdk/SdkFactory.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
import com.intellij.python.community.impl.conda.icons.PythonCommunityImplCondaIcons
import com.intellij.python.community.impl.pipenv.PIPENV_ICON
import com.intellij.python.community.impl.poetry.common.icons.PythonCommunityImplPoetryCommonIcons
import com.intellij.python.community.impl.uv.common.icons.PythonCommunityImplUVCommonIcons
import com.intellij.python.hatch.icons.PythonHatchIcons
import com.intellij.python.uv.common.icons.PythonUvCommonIcons
import com.intellij.python.venv.icons.PythonVenvIcons
import com.intellij.python.venv.sdk.flavors.VirtualEnvSdkFlavor

import com.jetbrains.python.hatch.sdk.HatchSdkAdditionalData
import com.jetbrains.python.sdk.PythonSdkAdditionalData
import com.jetbrains.python.sdk.PythonSdkType
import com.jetbrains.python.sdk.flavors.PyFlavorAndData
import com.jetbrains.python.sdk.flavors.PyFlavorData
import com.jetbrains.python.sdk.flavors.VirtualEnvSdkFlavor
import com.jetbrains.python.sdk.pipenv.PyPipEnvSdkFlavor
import com.jetbrains.python.sdk.poetry.PyPoetrySdkFlavor
import com.jetbrains.python.sdk.uv.UvSdkAdditionalData
Expand Down Expand Up @@ -73,7 +73,7 @@ object SdkFactory {
PythonEnvironmentType.UV -> {
val uvWorkingDir = findUvWorkingDir(projectBasePath)
val venvPath = Path.of(pythonExecutable).parent?.parent
UvSdkAdditionalData(uvWorkingDir, null, venvPath, null)
UvSdkAdditionalData(uvWorkingDir, null, venvPath?.toString(), null)
}

PythonEnvironmentType.POETRY -> {
Expand Down Expand Up @@ -129,7 +129,7 @@ object SdkFactory {
PythonEnvironmentType.CONDA -> PythonCommunityImplCondaIcons.Anaconda
PythonEnvironmentType.POETRY -> PythonCommunityImplPoetryCommonIcons.Poetry
PythonEnvironmentType.HATCH -> PythonHatchIcons.Logo
PythonEnvironmentType.UV -> PythonCommunityImplUVCommonIcons.UV
PythonEnvironmentType.UV -> PythonUvCommonIcons.UV
PythonEnvironmentType.PIPENV -> PIPENV_ICON
PythonEnvironmentType.VIRTUALENV -> PythonVenvIcons.VirtualEnv
PythonEnvironmentType.SYSTEM -> PythonVenvIcons.VirtualEnv
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.python.community.impl.uv.common.icons.PythonCommunityImplUVCommonIcons
import com.intellij.python.uv.common.icons.PythonUvCommonIcons
import com.intellij.python.venv.icons.PythonVenvIcons

import com.jetbrains.python.configuration.PyConfigurableInterpreterList
Expand Down Expand Up @@ -123,11 +123,11 @@ class ConfigurePythonActionAbstractTest {
every { EnvironmentDetector.detectEnvironmentType("/some/venv/bin/python") } returns
PythonEnvironmentType.UV
every { SdkFactory.getIconForEnvironmentType(PythonEnvironmentType.UV) } returns
PythonCommunityImplUVCommonIcons.UV
PythonUvCommonIcons.UV

action.update(event)

verify { presentation.icon = PythonCommunityImplUVCommonIcons.UV }
verify { presentation.icon = PythonUvCommonIcons.UV }
}
}

Expand Down Expand Up @@ -269,7 +269,7 @@ class ConfigurePythonActionAbstractTest {
} returns
newSdk
every { SdkFactory.getIconForEnvironmentType(PythonEnvironmentType.UV) } returns
PythonCommunityImplUVCommonIcons.UV
PythonUvCommonIcons.UV
every { newSdk.name } returns "Python 3.11 (venv)"

action.actionPerformed(event)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,11 @@ import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import kotlin.io.path.createDirectories
import kotlin.io.path.writeText
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import com.jetbrains.python.sdk.PythonSdkUtil

class EnvironmentDetectorTest {
@TempDir
lateinit var tempDir: Path
Expand All @@ -32,13 +29,6 @@ class EnvironmentDetectorTest {

binDir.createDirectories()
Files.createFile(pythonExe)

mockkStatic(PythonSdkUtil::class)
}

@AfterEach
fun tearDown() {
unmockkStatic(PythonSdkUtil::class)
}

@Test
Expand Down Expand Up @@ -84,9 +74,8 @@ class EnvironmentDetectorTest {
}

@Test
fun `detects virtualenv as fallback`() {
fun `detects virtualenv from pyvenv cfg`() {
venvRoot.resolve("pyvenv.cfg").writeText("home = /usr/bin")
every { PythonSdkUtil.isVirtualEnv(pythonExe.toString()) } returns true

val result = EnvironmentDetector.detectEnvironmentType(pythonExe.toString())

Expand All @@ -102,8 +91,6 @@ class EnvironmentDetectorTest {

@Test
fun `returns SYSTEM when not a venv`() {
every { PythonSdkUtil.isVirtualEnv(pythonExe.toString()) } returns false

val result = EnvironmentDetector.detectEnvironmentType(pythonExe.toString())

assertEquals(PythonEnvironmentType.SYSTEM, result)
Expand All @@ -113,7 +100,6 @@ class EnvironmentDetectorTest {
fun `UV takes precedence over virtualenv`() {
val pyvenvCfg = venvRoot.resolve("pyvenv.cfg")
pyvenvCfg.writeText("home = /usr/bin\nuv = 0.1.0")
every { PythonSdkUtil.isVirtualEnv(pythonExe.toString()) } returns true

val result = EnvironmentDetector.detectEnvironmentType(pythonExe.toString())

Expand All @@ -124,36 +110,32 @@ class EnvironmentDetectorTest {
fun `conda takes precedence over virtualenv`() {
venvRoot.resolve("conda-meta").createDirectories()
venvRoot.resolve("pyvenv.cfg").writeText("home = /usr/bin")
every { PythonSdkUtil.isVirtualEnv(pythonExe.toString()) } returns true

val result = EnvironmentDetector.detectEnvironmentType(pythonExe.toString())

assertEquals(PythonEnvironmentType.CONDA, result)
}

@Test
fun `pyvenv cfg without uv marker is not UV`() {
fun `pyvenv cfg without uv marker is virtualenv`() {
venvRoot.resolve("pyvenv.cfg").writeText("home = /usr/bin\nversion = 3.14")
every { PythonSdkUtil.isVirtualEnv(pythonExe.toString()) } returns true

val result = EnvironmentDetector.detectEnvironmentType(pythonExe.toString())

assertEquals(PythonEnvironmentType.VIRTUALENV, result)
}

@Test
fun `missing pyvenv cfg is not UV`() {
every { PythonSdkUtil.isVirtualEnv(pythonExe.toString()) } returns true

fun `missing pyvenv cfg is SYSTEM`() {
val result = EnvironmentDetector.detectEnvironmentType(pythonExe.toString())

assertEquals(PythonEnvironmentType.VIRTUALENV, result)
assertEquals(PythonEnvironmentType.SYSTEM, result)
}

@Test
fun `gitignore without Hatch marker is not Hatch`() {
fun `gitignore without Hatch marker falls back to virtualenv`() {
venvRoot.resolve(".gitignore").writeText("*.pyc\n__pycache__/\n")
every { PythonSdkUtil.isVirtualEnv(pythonExe.toString()) } returns true
venvRoot.resolve("pyvenv.cfg").writeText("home = /usr/bin")

val result = EnvironmentDetector.detectEnvironmentType(pythonExe.toString())

Expand All @@ -169,8 +151,6 @@ class EnvironmentDetectorTest {
val pipenvPython = pipenvBin.resolve("python")
Files.createFile(pipenvPython)

every { PythonSdkUtil.isVirtualEnv(pipenvPython.toString()) } returns true

mockkStatic(System::class)
every { System.getenv("WORKON_HOME") } returns workonDir.toString()
every { System.getenv("HOME") } returns tempDir.toString()
Expand Down
12 changes: 7 additions & 5 deletions src/test/kotlin/com/github/pyvenvmanage/sdk/SdkFactoryTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import java.nio.file.Path

import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.mockkStatic
import io.mockk.unmockkObject
import io.mockk.unmockkStatic
import kotlin.io.path.createDirectories
import kotlin.io.path.writeText
Expand All @@ -24,12 +26,12 @@ import com.intellij.openapi.vfs.pointers.VirtualFilePointerManager
import com.intellij.python.community.impl.conda.icons.PythonCommunityImplCondaIcons
import com.intellij.python.community.impl.pipenv.PIPENV_ICON
import com.intellij.python.community.impl.poetry.common.icons.PythonCommunityImplPoetryCommonIcons
import com.intellij.python.community.impl.uv.common.icons.PythonCommunityImplUVCommonIcons
import com.intellij.python.hatch.icons.PythonHatchIcons
import com.intellij.python.uv.common.icons.PythonUvCommonIcons
import com.intellij.python.venv.icons.PythonVenvIcons
import com.intellij.python.venv.sdk.flavors.VirtualEnvSdkFlavor

import com.jetbrains.python.PythonPluginDisposable
import com.jetbrains.python.sdk.flavors.VirtualEnvSdkFlavor

class SdkFactoryTest {
@BeforeEach
Expand All @@ -40,21 +42,21 @@ class SdkFactoryTest {
every { app.getService(VirtualFilePointerManager::class.java) } returns mockk(relaxed = true)
mockkStatic(PythonPluginDisposable::class)
every { PythonPluginDisposable.getInstance() } returns mockk<Disposable>(relaxed = true)
mockkStatic(VirtualEnvSdkFlavor::class)
mockkObject(VirtualEnvSdkFlavor.Companion)
every { VirtualEnvSdkFlavor.getInstance() } returns mockk(relaxed = true)
}

@AfterEach
fun tearDown() {
unmockkStatic(ApplicationManager::class)
unmockkStatic(PythonPluginDisposable::class)
unmockkStatic(VirtualEnvSdkFlavor::class)
unmockkObject(VirtualEnvSdkFlavor.Companion)
}

@Test
fun `getIconForEnvironmentType returns UV icon`() {
assertEquals(
PythonCommunityImplUVCommonIcons.UV,
PythonUvCommonIcons.UV,
SdkFactory.getIconForEnvironmentType(PythonEnvironmentType.UV),
)
}
Expand Down
Loading