PY-78001 Support adding existing venv's as UV

Add UvExistingEnvironmentSelector

(cherry picked from commit be8827506d521c5487cf4fbb3ca15d979f760d44)

GitOrigin-RevId: 5a8cdf35bbd89e0473724554d3390d6d9eb19311
This commit is contained in:
Egor.Eliseev
2025-01-10 15:54:01 +01:00
committed by intellij-monorepo-bot
parent 1c33aab97d
commit dca31999f3
13 changed files with 161 additions and 55 deletions

View File

@@ -11,13 +11,10 @@ import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBundle import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBundle
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.jetbrains.python.sdk.* import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.basePath
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension
import com.jetbrains.python.sdk.uv.PY_PROJECT_TOML import com.jetbrains.python.sdk.uv.PY_PROJECT_TOML
import com.jetbrains.python.sdk.uv.impl.getUvExecutable import com.jetbrains.python.sdk.uv.impl.getUvExecutable
import com.jetbrains.python.sdk.uv.setupUvSdkUnderProgress import com.jetbrains.python.sdk.uv.setupUvSdkUnderProgress
import java.io.FileNotFoundException
import java.nio.file.Path
class PyUvSdkConfiguration : PyProjectSdkConfigurationExtension { class PyUvSdkConfiguration : PyProjectSdkConfigurationExtension {
companion object { companion object {
@@ -54,12 +51,7 @@ class PyUvSdkConfiguration : PyProjectSdkConfigurationExtension {
override fun supportsHeadlessModel(): Boolean = true override fun supportsHeadlessModel(): Boolean = true
private suspend fun createUv(module: Module): Result<Sdk> { private suspend fun createUv(module: Module): Result<Sdk> {
val basePath = module.basePath?.let { Path.of(it) } val sdk = setupUvSdkUnderProgress(ModuleOrProject.ModuleAndProject(module), ProjectJdkTable.getInstance().allJdks.toList(), null)
if (basePath == null) {
return Result.failure(FileNotFoundException("Can't find module base path"))
}
val sdk = setupUvSdkUnderProgress(module, basePath, ProjectJdkTable.getInstance().allJdks.toList(), null)
sdk.onSuccess { sdk.onSuccess {
SdkConfigurationUtil.addSdk(it) SdkConfigurationUtil.addSdk(it)
} }

View File

@@ -1,6 +1,7 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. // Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.sdk.add.v2 package com.jetbrains.python.sdk.add.v2
import com.intellij.openapi.module.Module
import com.intellij.openapi.observable.properties.ObservableMutableProperty import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.observable.util.notEqualsTo import com.intellij.openapi.observable.util.notEqualsTo
import com.intellij.openapi.ui.validation.DialogValidationRequestor import com.intellij.openapi.ui.validation.DialogValidationRequestor
@@ -11,7 +12,6 @@ import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.sdk.ModuleOrProject import com.jetbrains.python.sdk.ModuleOrProject
import com.jetbrains.python.sdk.PySdkUtil import com.jetbrains.python.sdk.PySdkUtil
import com.jetbrains.python.sdk.PythonSdkUtil import com.jetbrains.python.sdk.PythonSdkUtil
import com.jetbrains.python.sdk.poetry.pyProjectToml
import com.jetbrains.python.statistics.InterpreterCreationMode import com.jetbrains.python.statistics.InterpreterCreationMode
import com.jetbrains.python.statistics.InterpreterType import com.jetbrains.python.statistics.InterpreterType
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -31,7 +31,7 @@ abstract class CustomExistingEnvironmentSelector(private val name: String, model
model.scope.launch { model.scope.launch {
val modulePath = when (moduleOrProject) { val modulePath = when (moduleOrProject) {
is ModuleOrProject.ProjectOnly -> moduleOrProject.project.basePath?.let { Path.of(it) } is ModuleOrProject.ProjectOnly -> moduleOrProject.project.basePath?.let { Path.of(it) }
is ModuleOrProject.ModuleAndProject -> pyProjectToml(moduleOrProject.module)?.let { Path.of(it.parent.path) } is ModuleOrProject.ModuleAndProject -> findModulePath(moduleOrProject.module)
} }
if (modulePath != null) { if (modulePath != null) {
@@ -49,6 +49,12 @@ abstract class CustomExistingEnvironmentSelector(private val name: String, model
message("sdk.create.custom.venv.missing.text", name), message("sdk.create.custom.venv.missing.text", name),
).component ).component
addInterpretersComboBox(panel)
}
}
protected open fun addInterpretersComboBox(panel: Panel) {
with(panel) {
row(message("sdk.create.custom.existing.env.title", name.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() })) { row(message("sdk.create.custom.existing.env.title", name.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() })) {
comboBox = pythonInterpreterComboBox(selectedEnv, model, { path -> addEnvByPath(path) }, model.interpreterLoading) comboBox = pythonInterpreterComboBox(selectedEnv, model, { path -> addEnvByPath(path) }, model.interpreterLoading)
.align(Align.FILL) .align(Align.FILL)
@@ -82,4 +88,5 @@ abstract class CustomExistingEnvironmentSelector(private val name: String, model
internal abstract val executable: ObservableMutableProperty<String> internal abstract val executable: ObservableMutableProperty<String>
internal abstract val interpreterType: InterpreterType internal abstract val interpreterType: InterpreterType
internal abstract suspend fun detectEnvironments(modulePath: Path) internal abstract suspend fun detectEnvironments(modulePath: Path)
internal abstract suspend fun findModulePath(module: Module): Path?
} }

View File

@@ -33,7 +33,7 @@ class EnvironmentCreatorUv(model: PythonMutableTargetAddInterpreterModel, privat
} }
val python = homePath?.let { Path.of(it) } val python = homePath?.let { Path.of(it) }
return setupUvSdkUnderProgress(module, Path.of(projectPath), baseSdks, python) return setupUvSdkUnderProgress(ModuleOrProject.ModuleAndProject(module), baseSdks, python)
} }
override suspend fun detectExecutable() { override suspend fun detectExecutable() {

View File

@@ -1,19 +1,22 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. // Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.sdk.add.v2 package com.jetbrains.python.sdk.add.v2
import com.intellij.openapi.module.Module
import com.intellij.openapi.observable.properties.ObservableMutableProperty import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.projectRoots.ProjectJdkTable import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.vfs.toNioPathOrNull
import com.jetbrains.python.sdk.ModuleOrProject import com.jetbrains.python.sdk.ModuleOrProject
import com.jetbrains.python.sdk.poetry.detectPoetryEnvs import com.jetbrains.python.sdk.poetry.detectPoetryEnvs
import com.jetbrains.python.sdk.poetry.isPoetry import com.jetbrains.python.sdk.poetry.isPoetry
import com.jetbrains.python.sdk.poetry.pyProjectToml
import com.jetbrains.python.sdk.poetry.setupPoetrySdkUnderProgress import com.jetbrains.python.sdk.poetry.setupPoetrySdkUnderProgress
import com.jetbrains.python.statistics.InterpreterType import com.jetbrains.python.statistics.InterpreterType
import com.jetbrains.python.statistics.version import com.jetbrains.python.statistics.version
import java.nio.file.Path import java.nio.file.Path
import kotlin.io.path.pathString import kotlin.io.path.pathString
class PoetryExistingEnvironmentSelector(model: PythonMutableTargetAddInterpreterModel, moduleOrProject: ModuleOrProject) : CustomExistingEnvironmentSelector("poetry", model, moduleOrProject) { internal class PoetryExistingEnvironmentSelector(model: PythonMutableTargetAddInterpreterModel, moduleOrProject: ModuleOrProject) : CustomExistingEnvironmentSelector("poetry", model, moduleOrProject) {
override val executable: ObservableMutableProperty<String> = model.state.poetryExecutable override val executable: ObservableMutableProperty<String> = model.state.poetryExecutable
override val interpreterType: InterpreterType = InterpreterType.POETRY override val interpreterType: InterpreterType = InterpreterType.POETRY
@@ -37,4 +40,6 @@ class PoetryExistingEnvironmentSelector(model: PythonMutableTargetAddInterpreter
existingEnvironments.value = existingEnvs existingEnvironments.value = existingEnvs
} }
override suspend fun findModulePath(module: Module): Path? = pyProjectToml(module)?.toNioPathOrNull()?.parent
} }

View File

@@ -38,6 +38,7 @@ class PythonAddCustomInterpreter(val model: PythonMutableTargetAddInterpreterMod
put(PYTHON, PythonExistingEnvironmentSelector(model)) put(PYTHON, PythonExistingEnvironmentSelector(model))
put(CONDA, CondaExistingEnvironmentSelector(model, errorSink)) put(CONDA, CondaExistingEnvironmentSelector(model, errorSink))
if (moduleOrProject != null) put(POETRY, PoetryExistingEnvironmentSelector(model, moduleOrProject)) if (moduleOrProject != null) put(POETRY, PoetryExistingEnvironmentSelector(model, moduleOrProject))
if (moduleOrProject != null) put(UV, UvExistingEnvironmentSelector(model, moduleOrProject))
} }
val currentSdkManager: PythonAddEnvironment val currentSdkManager: PythonAddEnvironment

View File

@@ -0,0 +1,59 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.sdk.add.v2
import com.intellij.openapi.module.Module
import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.vfs.toNioPathOrNull
import com.jetbrains.python.sdk.ModuleOrProject
import com.jetbrains.python.sdk.associatedModulePath
import com.jetbrains.python.sdk.isAssociatedWithModule
import com.jetbrains.python.sdk.uv.isUv
import com.jetbrains.python.sdk.uv.pyProjectToml
import com.jetbrains.python.sdk.uv.setupUvSdkUnderProgress
import com.jetbrains.python.statistics.InterpreterType
import com.jetbrains.python.statistics.version
import java.io.FileNotFoundException
import java.nio.file.Path
import kotlin.io.path.pathString
internal class UvExistingEnvironmentSelector(model: PythonMutableTargetAddInterpreterModel, moduleOrProject: ModuleOrProject)
: CustomExistingEnvironmentSelector("uv", model, moduleOrProject) {
override val executable: ObservableMutableProperty<String> = model.state.uvExecutable
override val interpreterType: InterpreterType = InterpreterType.UV
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Result<Sdk> {
val selectedInterpreterPath = selectedEnv.get()?.homePath ?: return Result.failure(FileNotFoundException("No selected interpreter"))
val existingSdk = ProjectJdkTable.getInstance().allJdks.find { it.homePath == selectedInterpreterPath }
val associatedModule = extractModule(moduleOrProject)
// uv sdk in current module
if (existingSdk != null && existingSdk.isUv && existingSdk.isAssociatedWithModule(associatedModule)) {
return Result.success(existingSdk)
}
val existingWorkingDir = existingSdk?.associatedModulePath?.let { Path.of(it) }
val usePip = existingWorkingDir!= null && !existingSdk.isUv
return setupUvSdkUnderProgress(moduleOrProject, ProjectJdkTable.getInstance().allJdks.toList(), Path.of(selectedInterpreterPath), existingWorkingDir, usePip)
}
override suspend fun detectEnvironments(modulePath: Path) {
val existingEnvs = ProjectJdkTable.getInstance().allJdks.filter {
it.isUv && (it.associatedModulePath == modulePath.pathString || it.associatedModulePath == null)
}.mapNotNull { env ->
env.homePath?.let { path -> DetectedSelectableInterpreter(path, env.version) }
}
existingEnvironments.value = existingEnvs
}
override suspend fun findModulePath(module: Module): Path? = pyProjectToml(module)?.toNioPathOrNull()?.parent
private fun extractModule(moduleOrProject: ModuleOrProject): Module? =
when (moduleOrProject) {
is ModuleOrProject.ModuleAndProject -> moduleOrProject.module
else -> null
}
}

View File

@@ -264,12 +264,10 @@ internal fun Row.pythonInterpreterComboBox(
} }
} }
return cell return cell
} }
class PythonInterpreterComboBox( internal class PythonInterpreterComboBox(
val backingProperty: ObservableMutableProperty<PythonSelectableInterpreter?>, private val backingProperty: ObservableMutableProperty<PythonSelectableInterpreter?>,
val controller: PythonAddInterpreterModel, val controller: PythonAddInterpreterModel,
val onPathSelected: (String) -> Unit, val onPathSelected: (String) -> Unit,
) : ComboBox<PythonSelectableInterpreter?>() { ) : ComboBox<PythonSelectableInterpreter?>() {

View File

@@ -16,6 +16,6 @@ interface UvLowLevel {
suspend fun listPackages(): Result<List<PythonPackage>> suspend fun listPackages(): Result<List<PythonPackage>>
suspend fun listOutdatedPackages(): Result<List<PythonOutdatedPackage>> suspend fun listOutdatedPackages(): Result<List<PythonOutdatedPackage>>
suspend fun installPackage(name: PythonPackageSpecification, options: List<String>): Result<Unit> suspend fun installPackage(name: PythonPackageSpecification, options: List<String>, usePip: Boolean = false): Result<Unit>
suspend fun uninstallPackage(name: PythonPackage): Result<Unit> suspend fun uninstallPackage(name: PythonPackage, usePip: Boolean = false): Result<Unit>
} }

View File

@@ -5,24 +5,24 @@ import com.intellij.openapi.module.Module
import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.NlsSafe import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.toNioPathOrNull
import com.intellij.platform.ide.progress.withBackgroundProgress import com.intellij.platform.ide.progress.withBackgroundProgress
import com.intellij.util.PathUtil import com.intellij.util.PathUtil
import com.jetbrains.python.PyBundle import com.jetbrains.python.PyBundle
import com.jetbrains.python.icons.PythonIcons import com.jetbrains.python.icons.PythonIcons
import com.jetbrains.python.sdk.createSdk import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.findAmongRoots
import com.jetbrains.python.sdk.setAssociationToModule
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 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
import javax.swing.Icon
import kotlin.io.path.pathString import kotlin.io.path.pathString
internal val Sdk.isUv: Boolean internal val Sdk.isUv: Boolean
get() = sdkAdditionalData is UvSdkAdditionalData get() = sdkAdditionalData is UvSdkAdditionalData
internal suspend fun uvLock(module: com.intellij.openapi.module.Module): VirtualFile? { internal suspend fun uvLock(module: Module): VirtualFile? {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
findAmongRoots(module, UV_LOCK) findAmongRoots(module, UV_LOCK)
} }
@@ -38,32 +38,54 @@ internal fun suggestedSdkName(basePath: Path): @NlsSafe String {
return "uv (${PathUtil.getFileName(basePath.pathString)})" return "uv (${PathUtil.getFileName(basePath.pathString)})"
} }
val UV_ICON = PythonIcons.UV val UV_ICON: Icon = PythonIcons.UV
val UV_LOCK: String = "uv.lock" const val UV_LOCK: String = "uv.lock"
// FIXME: move pyprojecttoml code out to common package // FIXME: move pyprojecttoml code out to common package
val PY_PROJECT_TOML: String = "pyproject.toml" const val PY_PROJECT_TOML: String = "pyproject.toml"
suspend fun setupUvSdkUnderProgress( suspend fun setupUvSdkUnderProgress(
module: Module, moduleOrProject: ModuleOrProject,
projectPath: Path,
existingSdks: List<Sdk>, existingSdks: List<Sdk>,
python: Path? python: Path?,
existingSdkWorkingDir: Path? = null,
usePip: Boolean = false,
): Result<Sdk> { ): Result<Sdk> {
val uv = createUvLowLevel(projectPath, createUvCli())
val init = pyProjectToml(module) == null val (pyProjectToml, moduleWorkingDirectory) = resolveWorkingDirectory(moduleOrProject)
val init = pyProjectToml == null
val uvWorkingDir = existingSdkWorkingDir ?: moduleWorkingDirectory
val uv = createUvLowLevel(uvWorkingDir, createUvCli())
val envExecutable = val envExecutable =
withBackgroundProgress(module.project, PyBundle.message("python.sdk.dialog.title.setting.up.uv.environment"), true) { if (existingSdkWorkingDir == null) {
uv.initializeEnvironment(init, python) withBackgroundProgress(moduleOrProject.project, PyBundle.message("python.sdk.dialog.title.setting.up.uv.environment"), true) {
}.getOrElse { uv.initializeEnvironment(init, python)
return Result.failure(it) }.getOrElse {
return Result.failure(it)
}
} }
else {
python
} ?: throw IllegalArgumentException("Python executable is required to setup uv environment")
val sdk = createSdk(envExecutable, existingSdks, projectPath.pathString, suggestedSdkName(projectPath), UvSdkAdditionalData()) val sdk = createSdk(envExecutable, existingSdks, moduleWorkingDirectory.pathString, suggestedSdkName(moduleWorkingDirectory), UvSdkAdditionalData(existingSdkWorkingDir, usePip))
sdk.onSuccess { sdk.onSuccess {
it.setAssociationToModule(module) it.setAssociationToPath(moduleWorkingDirectory.pathString)
} }
return sdk return sdk
}
private suspend fun resolveWorkingDirectory(moduleOrProject: ModuleOrProject): Pair<VirtualFile?, Path> {
var pyProjectToml: VirtualFile? = null
val workingDirectory = when (moduleOrProject) {
is ModuleOrProject.ModuleAndProject -> {
pyProjectToml = pyProjectToml(moduleOrProject.module)
pyProjectToml?.toNioPathOrNull()?.parent ?: moduleOrProject.module.basePath?.let { Path.of(it) }
}
else -> moduleOrProject.project.basePath?.let { Path.of(it) }
} ?: throw IllegalArgumentException("Path to module or working directory is required")
return Pair(pyProjectToml, workingDirectory)
} }

View File

@@ -22,7 +22,7 @@ internal class UvPackageManager(project: Project, sdk: Sdk, val uv: UvLowLevel)
var outdatedPackages: Map<String, PythonOutdatedPackage> = emptyMap() var outdatedPackages: Map<String, PythonOutdatedPackage> = emptyMap()
override suspend fun installPackageCommand(specification: PythonPackageSpecification, options: List<String>): Result<String> { override suspend fun installPackageCommand(specification: PythonPackageSpecification, options: List<String>): Result<String> {
uv.installPackage(specification, options).getOrElse { uv.installPackage(specification, options, (sdk.sdkAdditionalData as? UvSdkAdditionalData)?.usePip ?: false).getOrElse {
return Result.failure(it) return Result.failure(it)
} }
@@ -31,7 +31,7 @@ internal class UvPackageManager(project: Project, sdk: Sdk, val uv: UvLowLevel)
} }
override suspend fun updatePackageCommand(specification: PythonPackageSpecification): Result<String> { override suspend fun updatePackageCommand(specification: PythonPackageSpecification): Result<String> {
uv.installPackage(specification, emptyList()).getOrElse { uv.installPackage(specification, emptyList(), (sdk.sdkAdditionalData as? UvSdkAdditionalData)?.usePip ?: false).getOrElse {
return Result.failure(it) return Result.failure(it)
} }
@@ -40,7 +40,7 @@ internal class UvPackageManager(project: Project, sdk: Sdk, val uv: UvLowLevel)
} }
override suspend fun uninstallPackageCommand(pkg: PythonPackage): Result<String> { override suspend fun uninstallPackageCommand(pkg: PythonPackage): Result<String> {
uv.uninstallPackage(pkg).getOrElse { uv.uninstallPackage(pkg, (sdk.sdkAdditionalData as? UvSdkAdditionalData)?.usePip ?: false).getOrElse {
return Result.failure(it) return Result.failure(it)
} }
@@ -64,7 +64,8 @@ class UvPackageManagerProvider : PythonPackageManagerProvider {
return null return null
} }
val uv = createUvLowLevel(Path.of(project.basePath!!), createUvCli()) val uvWorkingDirectory = (sdk.sdkAdditionalData as? UvSdkAdditionalData)?.uvWorkingDirectory ?: Path.of(project.basePath!!)
val uv = createUvLowLevel(uvWorkingDirectory, createUvCli())
return UvPackageManager(project, sdk, uv) return UvPackageManager(project, sdk, uv)
} }
} }

View File

@@ -6,15 +6,30 @@ import com.jetbrains.python.sdk.flavors.PyFlavorData
import com.jetbrains.python.sdk.flavors.PythonFlavorProvider import com.jetbrains.python.sdk.flavors.PythonFlavorProvider
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
import org.jdom.Element import org.jdom.Element
import java.nio.file.Path
import javax.swing.Icon
import kotlin.io.path.pathString
class UvSdkAdditionalData : PythonSdkAdditionalData { class UvSdkAdditionalData : PythonSdkAdditionalData {
constructor() : super(UvSdkFlavor) val uvWorkingDirectory: Path?
constructor(data: PythonSdkAdditionalData) : super(data) val usePip: Boolean
constructor(uvWorkingDirectory: Path? = null, usePip: Boolean = false) : super(UvSdkFlavor) {
this.uvWorkingDirectory = uvWorkingDirectory
this.usePip = usePip
}
constructor(data: PythonSdkAdditionalData, uvWorkingDirectory: Path? = null, usePip: Boolean = false) : super(data) {
this.uvWorkingDirectory = uvWorkingDirectory
this.usePip = usePip
}
override fun save(element: Element) { override fun save(element: Element) {
super.save(element) super.save(element)
element.setAttribute(IS_UV, "true") element.setAttribute(IS_UV, "true")
element.setAttribute(UV_WORKING_DIR, uvWorkingDirectory?.pathString ?: "")
element.setAttribute(USE_PIP, usePip.toString())
} }
companion object { companion object {
@@ -24,7 +39,9 @@ class UvSdkAdditionalData : PythonSdkAdditionalData {
fun load(element: Element): UvSdkAdditionalData? { fun load(element: Element): UvSdkAdditionalData? {
return when { return when {
element.getAttributeValue(IS_UV) == "true" -> { element.getAttributeValue(IS_UV) == "true" -> {
UvSdkAdditionalData().apply { val uvWorkingDirectory = if (element.getAttributeValue(UV_WORKING_DIR).isNullOrEmpty()) null else Path.of(element.getAttributeValue(UV_WORKING_DIR))
val usePip = element.getAttributeValue(USE_PIP)?.toBoolean() ?: false
UvSdkAdditionalData(uvWorkingDirectory, usePip).apply {
load(element) load(element)
} }
} }
@@ -39,8 +56,8 @@ class UvSdkAdditionalData : PythonSdkAdditionalData {
} }
} }
object UvSdkFlavor : PythonSdkFlavor<PyFlavorData.Empty>() { object UvSdkFlavor : CPythonSdkFlavor<PyFlavorData.Empty>() {
override fun getIcon() = UV_ICON override fun getIcon(): Icon = UV_ICON
override fun getFlavorDataClass(): Class<PyFlavorData.Empty> = PyFlavorData.Empty::class.java override fun getFlavorDataClass(): Class<PyFlavorData.Empty> = PyFlavorData.Empty::class.java
override fun isValidSdkPath(pathStr: String): Boolean { override fun isValidSdkPath(pathStr: String): Boolean {

View File

@@ -15,7 +15,7 @@ import java.nio.file.Path
import kotlin.io.path.exists import kotlin.io.path.exists
import kotlin.io.path.pathString import kotlin.io.path.pathString
internal class UvLowLevelImpl(val cwd: Path, val uvCli: UvCli) : UvLowLevel { internal class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLevel {
override suspend fun initializeEnvironment(init: Boolean, python: Path?): Result<Path> { override suspend fun initializeEnvironment(init: Boolean, python: Path?): Result<Path> {
val addPythonArg: (MutableList<String>) -> Unit = { args -> val addPythonArg: (MutableList<String>) -> Unit = { args ->
python?.let { python?.let {
@@ -93,25 +93,29 @@ internal class UvLowLevelImpl(val cwd: Path, val uvCli: UvCli) : UvLowLevel {
} }
} }
override suspend fun installPackage(spec: PythonPackageSpecification, options: List<String>): Result<Unit> { override suspend fun installPackage(name: PythonPackageSpecification, options: List<String>, usePip: Boolean): Result<Unit> {
val version = if (spec.versionSpecs.isNullOrBlank()) spec.name else "${spec.name}${spec.versionSpecs}" val version = if (name.versionSpecs.isNullOrBlank()) name.name else "${name.name}${name.versionSpecs}"
uvCli.runUv(cwd, "add", version, *options.toTypedArray()).getOrElse { val command = if (usePip) listOf("pip", "install") else listOf("add")
uvCli.runUv(cwd, *command.toTypedArray(), version, *options.toTypedArray()).getOrElse {
return Result.failure(it) return Result.failure(it)
} }
return Result.success(Unit) return Result.success(Unit)
} }
override suspend fun uninstallPackage(name: PythonPackage): Result<Unit> { override suspend fun uninstallPackage(name: PythonPackage, usePip: Boolean): Result<Unit> {
// TODO: check if package is in dependencies // TODO: check if package is in dependencies
val result = uvCli.runUv(cwd, "remove", name.name) val command = if (usePip) listOf("pip", "uninstall") else listOf("remove")
if (result.isFailure) { val result = uvCli.runUv(cwd, *command.toTypedArray(), name.name)
if (result.isFailure && !usePip) {
// try just to uninstall // try just to uninstall
uvCli.runUv(cwd, "pip", "uninstall", name.name).onFailure { uvCli.runUv(cwd, "pip", "uninstall", name.name).onFailure {
return Result.failure(it) return Result.failure(it)
} }
return Result.success(Unit)
} }
result.onFailure { return Result.failure(it) }
return Result.success(Unit) return Result.success(Unit)
} }
} }

View File

@@ -2,7 +2,6 @@
package com.jetbrains.python.sdk.uv.ui package com.jetbrains.python.sdk.uv.ui
import com.intellij.application.options.ModuleListCellRenderer import com.intellij.application.options.ModuleListCellRenderer
import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.components.service import com.intellij.openapi.components.service
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.module.Module import com.intellij.openapi.module.Module
@@ -25,6 +24,7 @@ import com.jetbrains.python.PyBundle
import com.jetbrains.python.PySdkBundle import com.jetbrains.python.PySdkBundle
import com.jetbrains.python.PythonModuleTypeBase import com.jetbrains.python.PythonModuleTypeBase
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.sdk.ModuleOrProject
import com.jetbrains.python.sdk.PySdkSettings import com.jetbrains.python.sdk.PySdkSettings
import com.jetbrains.python.sdk.PythonSdkCoroutineService import com.jetbrains.python.sdk.PythonSdkCoroutineService
import com.jetbrains.python.sdk.add.PyAddNewEnvPanel import com.jetbrains.python.sdk.add.PyAddNewEnvPanel
@@ -161,7 +161,7 @@ class PyAddNewUvPanel(
setUvExecutable(it) setUvExecutable(it)
} }
val sdk = runBlockingCancellable { val sdk = runBlockingCancellable {
setupUvSdkUnderProgress(module, Path.of(path), existingSdks, Path.of(python)) setupUvSdkUnderProgress(ModuleOrProject.ModuleAndProject(module), existingSdks, Path.of(python))
} }
sdk.onSuccess { sdk.onSuccess {