[pycharm] PY-79132 Replace base python with python version selection for uv

GitOrigin-RevId: b1b7cb2147244d2054ce9b3a8e2dfbd2bced567b
This commit is contained in:
David Lysenko
2025-08-05 15:11:12 +02:00
committed by intellij-monorepo-bot
parent 4d0b1dfecc
commit 04d67a09a7
7 changed files with 165 additions and 19 deletions

View File

@@ -51,6 +51,7 @@ jvm_library(
"@lib//:jetbrains-annotations", "@lib//:jetbrains-annotations",
"//python/python-pyproject:pyproject", "//python/python-pyproject:pyproject",
"//python/python-hatch:hatch", "//python/python-hatch:hatch",
"@lib//:io-github-z4kn4fein-semver-jvm",
], ],
runtime_deps = [":impl_resources"] runtime_deps = [":impl_resources"]
) )
@@ -112,6 +113,7 @@ jvm_library(
"//platform/testFramework/junit5", "//platform/testFramework/junit5",
"//platform/testFramework/junit5:junit5_test_lib", "//platform/testFramework/junit5:junit5_test_lib",
"@lib//:junit5", "@lib//:junit5",
"@lib//:io-github-z4kn4fein-semver-jvm",
], ],
runtime_deps = [":impl_resources"] runtime_deps = [":impl_resources"]
) )

View File

@@ -49,5 +49,6 @@
<orderEntry type="module" module-name="intellij.python.hatch" /> <orderEntry type="module" module-name="intellij.python.hatch" />
<orderEntry type="module" module-name="intellij.platform.testFramework.junit5" scope="TEST" /> <orderEntry type="module" module-name="intellij.platform.testFramework.junit5" scope="TEST" />
<orderEntry type="library" scope="TEST" name="JUnit5" level="project" /> <orderEntry type="library" scope="TEST" name="JUnit5" level="project" />
<orderEntry type="library" name="io.github.z4kn4fein.semver.jvm" level="project" />
</component> </component>
</module> </module>

View File

@@ -10,6 +10,8 @@ import com.intellij.execution.target.TargetEnvironmentWizard
import com.intellij.icons.AllIcons import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.module.Module import com.intellij.openapi.module.Module
import com.intellij.openapi.project.DumbAware import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
@@ -61,6 +63,9 @@ private class AddLocalInterpreterAction(
private val onSdkCreated: Consumer<Sdk>, private val onSdkCreated: Consumer<Sdk>,
) : AnAction(PyBundle.messagePointer("python.sdk.action.add.local.interpreter.text"), AllIcons.Nodes.HomeFolder), DumbAware { ) : AnAction(PyBundle.messagePointer("python.sdk.action.add.local.interpreter.text"), AllIcons.Nodes.HomeFolder), DumbAware {
override fun actionPerformed(e: AnActionEvent) { override fun actionPerformed(e: AnActionEvent) {
runInEdt {
FileDocumentManager.getInstance().saveAllDocuments()
}
addLocalInterpreter(moduleOrProject, onSdkCreated) addLocalInterpreter(moduleOrProject, onSdkCreated)
} }
} }

View File

@@ -2,12 +2,26 @@
package com.jetbrains.python.sdk.add.v2.uv package com.jetbrains.python.sdk.add.v2.uv
import com.intellij.openapi.module.Module import com.intellij.openapi.module.Module
import com.intellij.openapi.observable.properties.AtomicBooleanProperty
import com.intellij.openapi.observable.properties.ObservableMutableProperty import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.observable.util.not
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.validation.DialogValidationRequestor
import com.intellij.python.pyproject.PyProjectToml
import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.bindItem
import com.intellij.ui.dsl.gridLayout.UnscaledGaps
import com.intellij.ui.dsl.listCellRenderer.textListCellRenderer
import com.intellij.uiDesigner.core.Spacer
import com.intellij.util.text.nullize import com.intellij.util.text.nullize
import com.intellij.util.ui.AsyncProcessIcon
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.errorProcessing.ErrorSink import com.jetbrains.python.errorProcessing.ErrorSink
import com.jetbrains.python.errorProcessing.PyResult import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.getOrNull
import com.jetbrains.python.newProjectWizard.collector.PythonNewProjectWizardCollector import com.jetbrains.python.newProjectWizard.collector.PythonNewProjectWizardCollector
import com.jetbrains.python.sdk.add.v2.CustomNewEnvironmentCreator import com.jetbrains.python.sdk.add.v2.CustomNewEnvironmentCreator
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMethod.SELECT_EXISTING import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMethod.SELECT_EXISTING
@@ -15,17 +29,27 @@ import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel
import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.PYTHON import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.PYTHON
import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.UV import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.UV
import com.jetbrains.python.sdk.add.v2.VenvExistenceValidationState import com.jetbrains.python.sdk.add.v2.VenvExistenceValidationState
import com.jetbrains.python.sdk.add.v2.executableSelector
import com.jetbrains.python.sdk.basePath import com.jetbrains.python.sdk.basePath
import com.jetbrains.python.sdk.uv.impl.createUvCli
import com.jetbrains.python.sdk.uv.impl.createUvLowLevel
import com.jetbrains.python.sdk.uv.impl.setUvExecutable import com.jetbrains.python.sdk.uv.impl.setUvExecutable
import com.jetbrains.python.sdk.uv.setupNewUvSdkAndEnvUnderProgress import com.jetbrains.python.sdk.uv.setupNewUvSdkAndEnvUnderProgress
import com.jetbrains.python.statistics.InterpreterType import com.jetbrains.python.statistics.InterpreterType
import com.jetbrains.python.venvReader.tryResolvePath import com.jetbrains.python.venvReader.tryResolvePath
import io.github.z4kn4fein.semver.Version
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext
import java.awt.Dimension
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.Paths import java.nio.file.Paths
import kotlin.io.path.exists import kotlin.io.path.exists
import kotlin.io.path.inputStream
internal class EnvironmentCreatorUv( internal class EnvironmentCreatorUv(
@@ -35,20 +59,103 @@ internal class EnvironmentCreatorUv(
) : CustomNewEnvironmentCreator("uv", model, errorSink) { ) : CustomNewEnvironmentCreator("uv", model, errorSink) {
override val interpreterType: InterpreterType = InterpreterType.UV override val interpreterType: InterpreterType = InterpreterType.UV
override val executable: ObservableMutableProperty<String> = model.state.uvExecutable override val executable: ObservableMutableProperty<String> = model.state.uvExecutable
private val executableFlow = MutableStateFlow(model.state.uvExecutable.get())
private lateinit var pythonVersion: ObservableMutableProperty<Version?>
private lateinit var versionComboBox: ComboBox<Version?>
private val loading = AtomicBooleanProperty(false)
init {
executable.afterChange {
executableFlow.value = it
}
}
override fun setupUI(panel: Panel, validationRequestor: DialogValidationRequestor) {
with(panel) {
row(message("sdk.create.python.version")) {
pythonVersion = propertyGraph.property(null)
versionComboBox = comboBox(listOf<Version?>(null), textListCellRenderer {
it?.let { "${it.major}.${it.minor}" } ?: "Default"
})
.bindItem(pythonVersion)
.enabledIf(loading.not())
.component
cell(AsyncProcessIcon("loader"))
.align(AlignX.LEFT)
.customize(UnscaledGaps(0))
.visibleIf(loading)
}
executableSelector(
executable = executable,
validationRequestor = validationRequestor,
labelText = message("sdk.create.custom.venv.executable.path", "uv"),
missingExecutableText = message("sdk.create.custom.venv.missing.text", "uv"),
installAction = createInstallFix(errorSink)
)
row("") {
venvExistenceValidationAlert(validationRequestor) {
onVenvSelectExisting()
}
}
}
}
override fun onShown(scope: CoroutineScope) { override fun onShown(scope: CoroutineScope) {
super.onShown(scope) model
.projectPathFlows
.projectPathWithDefault
.combine(executableFlow) { projectPath, executable -> projectPath to executable }
.onEach { (projectPath, executable) ->
val venvPath = projectPath.resolve(".venv")
model.projectPathFlows.projectPathWithDefault.onEach { projectPath -> withContext(Dispatchers.IO) {
val venvPath = projectPath.resolve(".venv") venvExistenceValidationState.set(
venvExistenceValidationState.set( if (venvPath.exists())
if (venvPath.exists()) VenvExistenceValidationState.Error(Paths.get(".venv"))
VenvExistenceValidationState.Error(Paths.get(".venv")) else
else VenvExistenceValidationState.Invisible
VenvExistenceValidationState.Invisible )
) }
}.launchIn(scope)
if (executable == "") {
return@onEach
}
versionComboBox.removeAllItems()
versionComboBox.addItem(null)
versionComboBox.selectedItem = null
try {
loading.set(true)
val pyProjectTomlPath = projectPath.resolve("pyproject.toml")
val pythonVersions = withContext(Dispatchers.IO) {
val versionRequest = if (pyProjectTomlPath.exists()) {
PyProjectToml.parse(pyProjectTomlPath.inputStream()).getOrNull()?.project?.requiresPython
} else {
null
}
val cli = createUvCli(Path.of(executable))
val uvLowLevel = createUvLowLevel(Path.of(""), cli)
uvLowLevel.listSupportedPythonVersions(versionRequest)
.getOr { return@withContext emptyList() }
.toList()
.sortedDescending()
}
pythonVersions.forEach {
versionComboBox.addItem(it)
}
} finally {
loading.set(false)
}
}
.launchIn(scope)
} }
override fun onVenvSelectExisting() { override fun onVenvSelectExisting() {
@@ -67,14 +174,20 @@ internal class EnvironmentCreatorUv(
setUvExecutable(savingPath) setUvExecutable(savingPath)
} }
override suspend fun setupEnvSdk(project: Project, module: Module?, baseSdks: List<Sdk>, projectPath: String, homePath: String?, installPackages: Boolean): PyResult<Sdk> { override suspend fun setupEnvSdk(
project: Project,
module: Module?,
baseSdks: List<Sdk>,
projectPath: String,
homePath: String?,
installPackages: Boolean,
): PyResult<Sdk> {
val workingDir = module?.basePath?.let { tryResolvePath(it) } ?: project.basePath?.let { tryResolvePath(it) } val workingDir = module?.basePath?.let { tryResolvePath(it) } ?: project.basePath?.let { tryResolvePath(it) }
if (workingDir == null) { if (workingDir == null) {
return PyResult.localizedError("working dir is not specified for uv environment setup") return PyResult.localizedError("working dir is not specified for uv environment setup")
} }
val python = homePath?.let { Path.of(it) } return setupNewUvSdkAndEnvUnderProgress(project, workingDir, baseSdks, pythonVersion.get())
return setupNewUvSdkAndEnvUnderProgress(project, workingDir, baseSdks, python)
} }
override suspend fun detectExecutable() { override suspend fun detectExecutable() {

View File

@@ -7,6 +7,7 @@ import com.jetbrains.python.packaging.common.NormalizedPythonPackageName
import com.jetbrains.python.packaging.common.PythonOutdatedPackage import com.jetbrains.python.packaging.common.PythonOutdatedPackage
import com.jetbrains.python.packaging.common.PythonPackage import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.management.PythonPackageInstallRequest import com.jetbrains.python.packaging.management.PythonPackageInstallRequest
import io.github.z4kn4fein.semver.Version
import org.jetbrains.annotations.ApiStatus import org.jetbrains.annotations.ApiStatus
import java.nio.file.Path import java.nio.file.Path
@@ -17,9 +18,10 @@ interface UvCli {
@ApiStatus.Internal @ApiStatus.Internal
interface UvLowLevel { interface UvLowLevel {
suspend fun initializeEnvironment(init: Boolean, python: Path?): PyResult<Path> suspend fun initializeEnvironment(init: Boolean, version: Version?): PyResult<Path>
suspend fun listUvPythons(): PyResult<Set<Path>> suspend fun listUvPythons(): PyResult<Set<Path>>
suspend fun listSupportedPythonVersions(versionRequest: String? = null): PyResult<Set<Version>>
/** /**
* Manage project dependencies by adding/removing them to the project along side installation * Manage project dependencies by adding/removing them to the project along side installation

View File

@@ -14,6 +14,7 @@ import com.jetbrains.python.sdk.createSdk
import com.jetbrains.python.sdk.getOrCreateAdditionalData import com.jetbrains.python.sdk.getOrCreateAdditionalData
import com.jetbrains.python.sdk.uv.impl.createUvCli import com.jetbrains.python.sdk.uv.impl.createUvCli
import com.jetbrains.python.sdk.uv.impl.createUvLowLevel import com.jetbrains.python.sdk.uv.impl.createUvLowLevel
import io.github.z4kn4fein.semver.Version
import java.nio.file.Path import java.nio.file.Path
import javax.swing.Icon import javax.swing.Icon
import kotlin.io.path.exists import kotlin.io.path.exists
@@ -49,13 +50,13 @@ suspend fun setupNewUvSdkAndEnvUnderProgress(
suspend fun setupNewUvSdkAndEnv( suspend fun setupNewUvSdkAndEnv(
workingDir: Path, workingDir: Path,
existingSdks: List<Sdk>, existingSdks: List<Sdk>,
basePython: Path?, version: Version?,
): PyResult<Sdk> { ): PyResult<Sdk> {
val toml = workingDir.resolve(PY_PROJECT_TOML) val toml = workingDir.resolve(PY_PROJECT_TOML)
val init = !toml.exists() val init = !toml.exists()
val uv = createUvLowLevel(workingDir, createUvCli()) val uv = createUvLowLevel(workingDir, createUvCli())
val envExecutable = uv.initializeEnvironment(init, basePython) val envExecutable = uv.initializeEnvironment(init, version)
.getOr { .getOr {
return it return it
} }

View File

@@ -17,6 +17,7 @@ import com.jetbrains.python.sdk.uv.UvCli
import com.jetbrains.python.sdk.uv.UvLowLevel import com.jetbrains.python.sdk.uv.UvLowLevel
import com.jetbrains.python.venvReader.VirtualEnvReader import com.jetbrains.python.venvReader.VirtualEnvReader
import com.jetbrains.python.venvReader.tryResolvePath import com.jetbrains.python.venvReader.tryResolvePath
import io.github.z4kn4fein.semver.Version
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.nio.file.Path import java.nio.file.Path
@@ -27,13 +28,14 @@ import kotlin.io.path.pathString
private const val NO_METADATA_MESSAGE = "does not contain a PEP 723 metadata tag" private const val NO_METADATA_MESSAGE = "does not contain a PEP 723 metadata tag"
private const val OUTDATED_ENV_MESSAGE = "The environment is outdated" private const val OUTDATED_ENV_MESSAGE = "The environment is outdated"
private val versionRegex = Regex("(\\d+\\.\\d+)\\.\\d+-.+\\s")
private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLevel { private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLevel {
override suspend fun initializeEnvironment(init: Boolean, python: Path?): PyResult<Path> { override suspend fun initializeEnvironment(init: Boolean, version: Version?): PyResult<Path> {
val addPythonArg: (MutableList<String>) -> Unit = { args -> val addPythonArg: (MutableList<String>) -> Unit = { args ->
python?.let { version?.let {
args.add("--python") args.add("--python")
args.add(python.pathString) args.add("${version.major}.${version.minor}")
} }
} }
@@ -89,6 +91,26 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev
return PyResult.success(pythons) return PyResult.success(pythons)
} }
override suspend fun listSupportedPythonVersions(versionRequest: String?): PyResult<Set<Version>> {
val args = mutableListOf("python", "list")
if (versionRequest != null) {
args += versionRequest
}
val out = uvCli.runUv(cwd, *args.toTypedArray()).getOr { return it }
val matches = versionRegex.findAll(out)
return PyResult.success(
matches.map {
Version.parse(
it.groupValues[1],
strict = false
)
}.toSet()
)
}
override suspend fun listPackages(): PyExecResult<List<PythonPackage>> { override suspend fun listPackages(): PyExecResult<List<PythonPackage>> {
val out = uvCli.runUv(cwd, "pip", "list", "--format", "json") val out = uvCli.runUv(cwd, "pip", "list", "--format", "json")
.getOr { return it } .getOr { return it }