Skip to content

Commit 52932b8

Browse files
authored
✨ feat(sdk): support IntelliJ Platform 262 (2026.2) (#186)
PyCharm `262` (2026.2) reshaped the Python SDK API the plugin builds against, so the `2.3.x` line is binary incompatible there and was capped at `261` in #184. This brings the plugin forward onto the `262` API so 2026.2 gets a working build. 🚀 Four call sites had to move: `UvSdkAdditionalData` now takes interpreter paths as `String` rather than `Path`, `VirtualEnvSdkFlavor` relocated to `com.intellij.python.venv.sdk.flavors`, the uv icons relocated to `com.intellij.python.uv.common.icons`, and `PythonSdkUtil.isVirtualEnv(String)` was removed entirely. The first three are direct swaps. For the last one the detector now infers a virtualenv from the presence of `pyvenv.cfg` in the environment root, which every modern `venv`/`virtualenv` writes and which the uv and conda branches already rely on. One consequence worth noting: a legacy environment with no `pyvenv.cfg` now reports `SYSTEM` instead of `VIRTUALENV`. The build targets the latest `262` EAP (`262.6653.28-EAP-SNAPSHOT`, resolved from the snapshots channel), moves `sinceBuild` to `262`, reopens `untilBuild`, and bumps to `2.4.0-dev` (superseding the automated #185). The `261`-capped `2.3.x` line continues to serve 2026.1, so the two ranges coexist on the Marketplace. Because 2026.2 is still EAP, the API may shift again before GA and need a follow-up. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent 998842c commit 52932b8

7 files changed

Lines changed: 40 additions & 54 deletions

File tree

build.gradle.kts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ repositories {
5353
mavenCentral()
5454
intellijPlatform {
5555
defaultRepositories()
56+
// platformVersion targets a 2026.2 EAP build, which lives in the snapshots channel.
57+
snapshots()
5658
}
5759
}
5860

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

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

@@ -130,10 +133,11 @@ intellijPlatform {
130133
}
131134
pluginVerification {
132135
// The verifier's ignoredProblemsFile filters CompatibilityProblem instances only,
133-
// not ApiUsage (which is what INTERNAL_API_USAGES is). The 21 internal usages from
136+
// not ApiUsage (which is what INTERNAL_API_USAGES is). The internal usages from
134137
// SdkFactory/EnvironmentDetector reach into per-tool PyCharm SDK packages
135-
// (uv, hatch.sdk, poetry, pipenv) and per-tool icon classes, all sealed behind
136-
// @ApiStatus.Internal package-info markers with no public alternative on 261.
138+
// (uv, hatch.sdk, poetry, pipenv) and per-tool icon classes, plus the
139+
// PluginManagerCore plugin lookup, all sealed behind @ApiStatus.Internal with no
140+
// public alternative on 262.
137141
failureLevel =
138142
listOf(
139143
FailureLevel.COMPATIBILITY_PROBLEMS,
@@ -161,7 +165,7 @@ intellijPlatform {
161165
IntelliJPlatformType.PyCharmProfessional,
162166
)
163167
}
164-
ideTypes.forEach { create(it, platformVersion) }
168+
ideTypes.forEach { create(it, platformVersion) { useInstaller = false } }
165169
}
166170
}
167171
}

gradle.properties

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ nl.littlerobots.vcu.resolver=true
44
org.gradle.caching=true
55
org.gradle.configuration-cache=true
66
org.gradle.welcome=never
7-
platformVersion=2026.1
7+
platformVersion=262.6653.28-EAP-SNAPSHOT
88
pluginGroup=com.github.pyvenvmanage
99
pluginName=PyVenv Manage 2
1010
pluginRepositoryUrl=https://github.com/pyvenvmanage/PyVenvManage
11-
pluginSinceBuild=261
12-
pluginVersion=2.3.2-dev
11+
pluginSinceBuild=262
12+
pluginVersion=2.4.0-dev

src/main/kotlin/com/github/pyvenvmanage/sdk/EnvironmentDetector.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ import com.intellij.openapi.diagnostic.Logger
1111
import com.intellij.openapi.util.SystemInfo
1212
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
1313

14-
import com.jetbrains.python.sdk.PythonSdkUtil
15-
1614
enum class PythonEnvironmentType {
1715
UV,
1816
CONDA,
@@ -71,7 +69,7 @@ object EnvironmentDetector {
7169
PythonEnvironmentType.PIPENV
7270
}
7371

74-
PythonSdkUtil.isVirtualEnv(pythonExecutablePath).also { LOG.info("isVirtualEnv: $it") } -> {
72+
isVirtualEnv(venvRoot).also { LOG.info("isVirtualEnv: $it") } -> {
7573
PythonEnvironmentType.VIRTUALENV
7674
}
7775

@@ -131,6 +129,8 @@ object EnvironmentDetector {
131129
return dirs.any { venvRoot.startsWith(it) }
132130
}
133131

132+
private fun isVirtualEnv(venvRoot: Path): Boolean = venvRoot.resolve("pyvenv.cfg").exists()
133+
134134
private fun computePoetryDirs(): List<Path> {
135135
val dirs = mutableListOf<Path>()
136136

src/main/kotlin/com/github/pyvenvmanage/sdk/SdkFactory.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,16 @@ import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
1010
import com.intellij.python.community.impl.conda.icons.PythonCommunityImplCondaIcons
1111
import com.intellij.python.community.impl.pipenv.PIPENV_ICON
1212
import com.intellij.python.community.impl.poetry.common.icons.PythonCommunityImplPoetryCommonIcons
13-
import com.intellij.python.community.impl.uv.common.icons.PythonCommunityImplUVCommonIcons
1413
import com.intellij.python.hatch.icons.PythonHatchIcons
14+
import com.intellij.python.uv.common.icons.PythonUvCommonIcons
1515
import com.intellij.python.venv.icons.PythonVenvIcons
16+
import com.intellij.python.venv.sdk.flavors.VirtualEnvSdkFlavor
1617

1718
import com.jetbrains.python.hatch.sdk.HatchSdkAdditionalData
1819
import com.jetbrains.python.sdk.PythonSdkAdditionalData
1920
import com.jetbrains.python.sdk.PythonSdkType
2021
import com.jetbrains.python.sdk.flavors.PyFlavorAndData
2122
import com.jetbrains.python.sdk.flavors.PyFlavorData
22-
import com.jetbrains.python.sdk.flavors.VirtualEnvSdkFlavor
2323
import com.jetbrains.python.sdk.pipenv.PyPipEnvSdkFlavor
2424
import com.jetbrains.python.sdk.poetry.PyPoetrySdkFlavor
2525
import com.jetbrains.python.sdk.uv.UvSdkAdditionalData
@@ -73,7 +73,7 @@ object SdkFactory {
7373
PythonEnvironmentType.UV -> {
7474
val uvWorkingDir = findUvWorkingDir(projectBasePath)
7575
val venvPath = Path.of(pythonExecutable).parent?.parent
76-
UvSdkAdditionalData(uvWorkingDir, null, venvPath, null)
76+
UvSdkAdditionalData(uvWorkingDir, null, venvPath?.toString(), null)
7777
}
7878

7979
PythonEnvironmentType.POETRY -> {
@@ -129,7 +129,7 @@ object SdkFactory {
129129
PythonEnvironmentType.CONDA -> PythonCommunityImplCondaIcons.Anaconda
130130
PythonEnvironmentType.POETRY -> PythonCommunityImplPoetryCommonIcons.Poetry
131131
PythonEnvironmentType.HATCH -> PythonHatchIcons.Logo
132-
PythonEnvironmentType.UV -> PythonCommunityImplUVCommonIcons.UV
132+
PythonEnvironmentType.UV -> PythonUvCommonIcons.UV
133133
PythonEnvironmentType.PIPENV -> PIPENV_ICON
134134
PythonEnvironmentType.VIRTUALENV -> PythonVenvIcons.VirtualEnv
135135
PythonEnvironmentType.SYSTEM -> PythonVenvIcons.VirtualEnv

src/test/kotlin/com/github/pyvenvmanage/actions/ConfigurePythonActionAbstractTest.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import com.intellij.openapi.project.Project
2929
import com.intellij.openapi.projectRoots.ProjectJdkTable
3030
import com.intellij.openapi.projectRoots.Sdk
3131
import com.intellij.openapi.vfs.VirtualFile
32-
import com.intellij.python.community.impl.uv.common.icons.PythonCommunityImplUVCommonIcons
32+
import com.intellij.python.uv.common.icons.PythonUvCommonIcons
3333
import com.intellij.python.venv.icons.PythonVenvIcons
3434

3535
import com.jetbrains.python.configuration.PyConfigurableInterpreterList
@@ -123,11 +123,11 @@ class ConfigurePythonActionAbstractTest {
123123
every { EnvironmentDetector.detectEnvironmentType("/some/venv/bin/python") } returns
124124
PythonEnvironmentType.UV
125125
every { SdkFactory.getIconForEnvironmentType(PythonEnvironmentType.UV) } returns
126-
PythonCommunityImplUVCommonIcons.UV
126+
PythonUvCommonIcons.UV
127127

128128
action.update(event)
129129

130-
verify { presentation.icon = PythonCommunityImplUVCommonIcons.UV }
130+
verify { presentation.icon = PythonUvCommonIcons.UV }
131131
}
132132
}
133133

@@ -269,7 +269,7 @@ class ConfigurePythonActionAbstractTest {
269269
} returns
270270
newSdk
271271
every { SdkFactory.getIconForEnvironmentType(PythonEnvironmentType.UV) } returns
272-
PythonCommunityImplUVCommonIcons.UV
272+
PythonUvCommonIcons.UV
273273
every { newSdk.name } returns "Python 3.11 (venv)"
274274

275275
action.actionPerformed(event)

src/test/kotlin/com/github/pyvenvmanage/sdk/EnvironmentDetectorTest.kt

Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,11 @@ import io.mockk.mockkStatic
88
import io.mockk.unmockkStatic
99
import kotlin.io.path.createDirectories
1010
import kotlin.io.path.writeText
11-
import org.junit.jupiter.api.AfterEach
1211
import org.junit.jupiter.api.Assertions.assertEquals
1312
import org.junit.jupiter.api.BeforeEach
1413
import org.junit.jupiter.api.Test
1514
import org.junit.jupiter.api.io.TempDir
1615

17-
import com.jetbrains.python.sdk.PythonSdkUtil
18-
1916
class EnvironmentDetectorTest {
2017
@TempDir
2118
lateinit var tempDir: Path
@@ -32,13 +29,6 @@ class EnvironmentDetectorTest {
3229

3330
binDir.createDirectories()
3431
Files.createFile(pythonExe)
35-
36-
mockkStatic(PythonSdkUtil::class)
37-
}
38-
39-
@AfterEach
40-
fun tearDown() {
41-
unmockkStatic(PythonSdkUtil::class)
4232
}
4333

4434
@Test
@@ -84,9 +74,8 @@ class EnvironmentDetectorTest {
8474
}
8575

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

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

@@ -102,8 +91,6 @@ class EnvironmentDetectorTest {
10291

10392
@Test
10493
fun `returns SYSTEM when not a venv`() {
105-
every { PythonSdkUtil.isVirtualEnv(pythonExe.toString()) } returns false
106-
10794
val result = EnvironmentDetector.detectEnvironmentType(pythonExe.toString())
10895

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

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

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

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

131116
assertEquals(PythonEnvironmentType.CONDA, result)
132117
}
133118

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

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

141125
assertEquals(PythonEnvironmentType.VIRTUALENV, result)
142126
}
143127

144128
@Test
145-
fun `missing pyvenv cfg is not UV`() {
146-
every { PythonSdkUtil.isVirtualEnv(pythonExe.toString()) } returns true
147-
129+
fun `missing pyvenv cfg is SYSTEM`() {
148130
val result = EnvironmentDetector.detectEnvironmentType(pythonExe.toString())
149131

150-
assertEquals(PythonEnvironmentType.VIRTUALENV, result)
132+
assertEquals(PythonEnvironmentType.SYSTEM, result)
151133
}
152134

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

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

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

172-
every { PythonSdkUtil.isVirtualEnv(pipenvPython.toString()) } returns true
173-
174154
mockkStatic(System::class)
175155
every { System.getenv("WORKON_HOME") } returns workonDir.toString()
176156
every { System.getenv("HOME") } returns tempDir.toString()

src/test/kotlin/com/github/pyvenvmanage/sdk/SdkFactoryTest.kt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import java.nio.file.Path
55

66
import io.mockk.every
77
import io.mockk.mockk
8+
import io.mockk.mockkObject
89
import io.mockk.mockkStatic
10+
import io.mockk.unmockkObject
911
import io.mockk.unmockkStatic
1012
import kotlin.io.path.createDirectories
1113
import kotlin.io.path.writeText
@@ -24,12 +26,12 @@ import com.intellij.openapi.vfs.pointers.VirtualFilePointerManager
2426
import com.intellij.python.community.impl.conda.icons.PythonCommunityImplCondaIcons
2527
import com.intellij.python.community.impl.pipenv.PIPENV_ICON
2628
import com.intellij.python.community.impl.poetry.common.icons.PythonCommunityImplPoetryCommonIcons
27-
import com.intellij.python.community.impl.uv.common.icons.PythonCommunityImplUVCommonIcons
2829
import com.intellij.python.hatch.icons.PythonHatchIcons
30+
import com.intellij.python.uv.common.icons.PythonUvCommonIcons
2931
import com.intellij.python.venv.icons.PythonVenvIcons
32+
import com.intellij.python.venv.sdk.flavors.VirtualEnvSdkFlavor
3033

3134
import com.jetbrains.python.PythonPluginDisposable
32-
import com.jetbrains.python.sdk.flavors.VirtualEnvSdkFlavor
3335

3436
class SdkFactoryTest {
3537
@BeforeEach
@@ -40,21 +42,21 @@ class SdkFactoryTest {
4042
every { app.getService(VirtualFilePointerManager::class.java) } returns mockk(relaxed = true)
4143
mockkStatic(PythonPluginDisposable::class)
4244
every { PythonPluginDisposable.getInstance() } returns mockk<Disposable>(relaxed = true)
43-
mockkStatic(VirtualEnvSdkFlavor::class)
45+
mockkObject(VirtualEnvSdkFlavor.Companion)
4446
every { VirtualEnvSdkFlavor.getInstance() } returns mockk(relaxed = true)
4547
}
4648

4749
@AfterEach
4850
fun tearDown() {
4951
unmockkStatic(ApplicationManager::class)
5052
unmockkStatic(PythonPluginDisposable::class)
51-
unmockkStatic(VirtualEnvSdkFlavor::class)
53+
unmockkObject(VirtualEnvSdkFlavor.Companion)
5254
}
5355

5456
@Test
5557
fun `getIconForEnvironmentType returns UV icon`() {
5658
assertEquals(
57-
PythonCommunityImplUVCommonIcons.UV,
59+
PythonUvCommonIcons.UV,
5860
SdkFactory.getIconForEnvironmentType(PythonEnvironmentType.UV),
5961
)
6062
}

0 commit comments

Comments
 (0)