From 5cd9d71beeb60a4acf5d6a014cdb975c2672da89 Mon Sep 17 00:00:00 2001 From: Aleksandr Sorotskii Date: Wed, 12 Mar 2025 18:55:10 +0100 Subject: [PATCH] support for uv configuration in idea sdk management GitOrigin-RevId: 17207321c24bf3746742648efac8163b39aa88ef --- .../configuration/PyUvSdkConfiguration.kt | 10 ++- .../pluginCore/resources/META-INF/plugin.xml | 2 + .../sdk/add/v2/CustomNewEnvironmentCreator.kt | 2 +- .../sdk/add/v2/EnvironmentCreatorPip.kt | 2 +- .../v2/hatch/HatchNewEnvironmentCreator.kt | 4 +- .../add/v2/poetry/EnvironmentCreatorPoetry.kt | 2 +- .../sdk/add/v2/uv/EnvironmentCreatorUv.kt | 15 ++-- .../v2/uv/UvExistingEnvironmentSelector.kt | 20 +++-- .../src/com/jetbrains/python/sdk/uv/UvExt.kt | 77 ++++++++++--------- .../python/sdk/uv/ui/AddNewUvPanel.kt | 54 ++++++++++--- 10 files changed, 121 insertions(+), 67 deletions(-) diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyUvSdkConfiguration.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyUvSdkConfiguration.kt index c860db174b07..e458ee254f8a 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyUvSdkConfiguration.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyUvSdkConfiguration.kt @@ -14,7 +14,8 @@ import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.jetbrains.python.sdk.* import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension import com.jetbrains.python.sdk.uv.impl.getUvExecutable -import com.jetbrains.python.sdk.uv.setupUvSdkUnderProgress +import com.jetbrains.python.sdk.uv.setupNewUvSdkAndEnvUnderProgress +import com.jetbrains.python.venvReader.tryResolvePath class PyUvSdkConfiguration : PyProjectSdkConfigurationExtension { companion object { @@ -51,7 +52,12 @@ class PyUvSdkConfiguration : PyProjectSdkConfigurationExtension { override fun supportsHeadlessModel(): Boolean = true private suspend fun createUv(module: Module): Result { - val sdk = setupUvSdkUnderProgress(ModuleOrProject.ModuleAndProject(module), ProjectJdkTable.getInstance().allJdks.toList(), null) + val workingDir = tryResolvePath(module.basePath) + if (workingDir == null) { + return Result.failure(IllegalStateException("Can't determine working dir for the module")) + } + + val sdk = setupNewUvSdkAndEnvUnderProgress(module.project, workingDir, ProjectJdkTable.getInstance().allJdks.toList(), null) sdk.onSuccess { SdkConfigurationUtil.addSdk(it) } diff --git a/python/pluginCore/resources/META-INF/plugin.xml b/python/pluginCore/resources/META-INF/plugin.xml index 57b57e380f01..3194ff5b5698 100644 --- a/python/pluginCore/resources/META-INF/plugin.xml +++ b/python/pluginCore/resources/META-INF/plugin.xml @@ -838,6 +838,8 @@ The Python plug-in provides smart editing for Python scripts. The feature set of + + diff --git a/python/src/com/jetbrains/python/sdk/add/v2/CustomNewEnvironmentCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/CustomNewEnvironmentCreator.kt index 56a583749c08..a82ae5cc4cb1 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/CustomNewEnvironmentCreator.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/CustomNewEnvironmentCreator.kt @@ -160,7 +160,7 @@ internal abstract class CustomNewEnvironmentCreator(private val name: String, mo */ internal abstract fun savePathToExecutableToProperties(path: Path?) - protected abstract suspend fun setupEnvSdk(project: Project?, module: Module?, baseSdks: List, projectPath: String, homePath: String?, installPackages: Boolean): Result + protected abstract suspend fun setupEnvSdk(project: Project, module: Module?, baseSdks: List, projectPath: String, homePath: String?, installPackages: Boolean): Result internal abstract suspend fun detectExecutable() } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/add/v2/EnvironmentCreatorPip.kt b/python/src/com/jetbrains/python/sdk/add/v2/EnvironmentCreatorPip.kt index 8673a1d64371..4670537a3593 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/EnvironmentCreatorPip.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/EnvironmentCreatorPip.kt @@ -26,7 +26,7 @@ internal class EnvironmentCreatorPip(model: PythonMutableTargetAddInterpreterMod PropertiesComponent.getInstance().pipEnvPath = savingPath } - override suspend fun setupEnvSdk(project: Project?, module: Module?, baseSdks: List, projectPath: String, homePath: String?, installPackages: Boolean): Result = + override suspend fun setupEnvSdk(project: Project, module: Module?, baseSdks: List, projectPath: String, homePath: String?, installPackages: Boolean): Result = setupPipEnvSdkUnderProgress(project, module, baseSdks, projectPath, homePath, installPackages).asPythonResult() override suspend fun detectExecutable() { diff --git a/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchNewEnvironmentCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchNewEnvironmentCreator.kt index 75f9eed31ad3..7d5793f0c26a 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchNewEnvironmentCreator.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchNewEnvironmentCreator.kt @@ -58,8 +58,8 @@ internal class HatchNewEnvironmentCreator( HatchConfiguration.persistPathForTarget(hatchExecutablePath = savingPath) } - override suspend fun setupEnvSdk(project: Project?, module: Module?, baseSdks: List, projectPath: String, homePath: String?, installPackages: Boolean): Result { - project ?: return Result.failure(HatchUIError.ProjectIsNotSelected()) + override suspend fun setupEnvSdk(project: Project, module: Module?, baseSdks: List, projectPath: String, homePath: String?, installPackages: Boolean): Result { + // FIXME: should work with the project only module ?: return Result.failure(HatchUIError.ModuleIsNotSelected()) val selectedEnv = hatchEnvironmentProperty.get() ?: return Result.failure(HatchUIError.HatchEnvironmentIsNotSelected()) diff --git a/python/src/com/jetbrains/python/sdk/add/v2/poetry/EnvironmentCreatorPoetry.kt b/python/src/com/jetbrains/python/sdk/add/v2/poetry/EnvironmentCreatorPoetry.kt index 46fab4903b38..a3663ab99c78 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/poetry/EnvironmentCreatorPoetry.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/poetry/EnvironmentCreatorPoetry.kt @@ -66,7 +66,7 @@ internal class EnvironmentCreatorPoetry(model: PythonMutableTargetAddInterpreter PropertiesComponent.getInstance().poetryPath = savingPath } - override suspend fun setupEnvSdk(project: Project?, module: Module?, baseSdks: List, projectPath: String, homePath: String?, installPackages: Boolean): Result { + override suspend fun setupEnvSdk(project: Project, module: Module?, baseSdks: List, projectPath: String, homePath: String?, installPackages: Boolean): Result { module?.let { service().setInProjectEnv(it) } return setupPoetrySdkUnderProgress(project, module, baseSdks, projectPath, homePath, installPackages).asPythonResult() } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/uv/EnvironmentCreatorUv.kt b/python/src/com/jetbrains/python/sdk/add/v2/uv/EnvironmentCreatorUv.kt index 053e49ba81fe..bf0aa6b5bcd4 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/uv/EnvironmentCreatorUv.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/uv/EnvironmentCreatorUv.kt @@ -6,10 +6,11 @@ import com.intellij.openapi.observable.properties.ObservableMutableProperty import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk import com.intellij.util.text.nullize -import com.jetbrains.python.sdk.ModuleOrProject +import com.jetbrains.python.sdk.basePath import com.jetbrains.python.sdk.uv.impl.setUvExecutable -import com.jetbrains.python.sdk.uv.setupUvSdkUnderProgress +import com.jetbrains.python.sdk.uv.setupNewUvSdkAndEnvUnderProgress import com.jetbrains.python.statistics.InterpreterType +import com.jetbrains.python.venvReader.tryResolvePath import java.nio.file.Path import com.jetbrains.python.Result import com.jetbrains.python.errorProcessing.PyError @@ -32,14 +33,14 @@ internal class EnvironmentCreatorUv(model: PythonMutableTargetAddInterpreterMode setUvExecutable(savingPath) } - override suspend fun setupEnvSdk(project: Project?, module: Module?, baseSdks: List, projectPath: String, homePath: String?, installPackages: Boolean): Result { - if (module == null) { - // FIXME: should not happen, proper error - return kotlin.Result.failure(Exception("module is null")).asPythonResult() + override suspend fun setupEnvSdk(project: Project, module: Module?, baseSdks: List, projectPath: String, homePath: String?, installPackages: Boolean): Result { + val workingDir = module?.basePath?.let { tryResolvePath(it) } ?: project.basePath?.let { tryResolvePath(it) } + if (workingDir == null) { + return kotlin.Result.failure(Exception("working dir is not specified for uv environment setup")).asPythonResult() } val python = homePath?.let { Path.of(it) } - return setupUvSdkUnderProgress(ModuleOrProject.ModuleAndProject(module), baseSdks, python).asPythonResult() + return setupNewUvSdkAndEnvUnderProgress(project, workingDir, baseSdks, python).asPythonResult() } override suspend fun detectExecutable() { diff --git a/python/src/com/jetbrains/python/sdk/add/v2/uv/UvExistingEnvironmentSelector.kt b/python/src/com/jetbrains/python/sdk/add/v2/uv/UvExistingEnvironmentSelector.kt index f152e93badb2..6e96e47f8e5b 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/uv/UvExistingEnvironmentSelector.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/uv/UvExistingEnvironmentSelector.kt @@ -16,11 +16,13 @@ import com.jetbrains.python.sdk.add.v2.CustomExistingEnvironmentSelector import com.jetbrains.python.sdk.add.v2.DetectedSelectableInterpreter import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel import com.jetbrains.python.sdk.associatedModulePath +import com.jetbrains.python.sdk.basePath import com.jetbrains.python.sdk.isAssociatedWithModule import com.jetbrains.python.sdk.uv.isUv -import com.jetbrains.python.sdk.uv.setupUvSdkUnderProgress +import com.jetbrains.python.sdk.uv.setupExistingEnvAndSdk import com.jetbrains.python.statistics.InterpreterType import com.jetbrains.python.statistics.version +import com.jetbrains.python.venvReader.tryResolvePath import java.nio.file.Path import kotlin.io.path.pathString @@ -30,19 +32,27 @@ internal class UvExistingEnvironmentSelector(model: PythonMutableTargetAddInterp override val interpreterType: InterpreterType = InterpreterType.UV override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Result { - val selectedInterpreterPath = selectedEnv.get()?.homePath ?: return failure("No selected interpreter") - val existingSdk = ProjectJdkTable.getInstance().allJdks.find { it.homePath == selectedInterpreterPath } + val selectedInterpreterPath = tryResolvePath(selectedEnv.get()?.homePath) ?: return failure("No selected interpreter") + val allSdk = ProjectJdkTable.getInstance().allJdks + val existingSdk = allSdk.find { it.homePath == selectedInterpreterPath.pathString } val associatedModule = extractModule(moduleOrProject) + val projectDir = tryResolvePath(associatedModule?.basePath ?: moduleOrProject.project.basePath) ?: return failure("No base path") // 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 existingWorkingDir = existingSdk?.associatedModulePath?.let { tryResolvePath(it) } val usePip = existingWorkingDir != null && !existingSdk.isUv - return setupUvSdkUnderProgress(moduleOrProject, ProjectJdkTable.getInstance().allJdks.toList(), Path.of(selectedInterpreterPath), existingWorkingDir, usePip).asPythonResult() + return setupExistingEnvAndSdk( + selectedInterpreterPath, + existingWorkingDir, + usePip, + projectDir, + allSdk.toList() + ).asPythonResult() } override suspend fun detectEnvironments(modulePath: Path) { diff --git a/python/src/com/jetbrains/python/sdk/uv/UvExt.kt b/python/src/com/jetbrains/python/sdk/uv/UvExt.kt index c2fddad7bbc3..da7fda978d04 100644 --- a/python/src/com/jetbrains/python/sdk/uv/UvExt.kt +++ b/python/src/com/jetbrains/python/sdk/uv/UvExt.kt @@ -2,11 +2,10 @@ package com.jetbrains.python.sdk.uv import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.project.Project import com.intellij.openapi.util.NlsSafe -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.openapi.vfs.toNioPathOrNull import com.intellij.platform.ide.progress.withBackgroundProgress -import com.intellij.python.pyproject.PyProjectToml +import com.intellij.python.pyproject.PY_PROJECT_TOML import com.intellij.util.PathUtil import com.jetbrains.python.PyBundle import com.jetbrains.python.icons.PythonIcons @@ -15,6 +14,7 @@ import com.jetbrains.python.sdk.uv.impl.createUvCli import com.jetbrains.python.sdk.uv.impl.createUvLowLevel import java.nio.file.Path import javax.swing.Icon +import kotlin.io.path.exists import kotlin.io.path.pathString internal val Sdk.isUv: Boolean @@ -32,48 +32,51 @@ internal fun suggestedSdkName(basePath: Path): @NlsSafe String { val UV_ICON: Icon = PythonIcons.UV -suspend fun setupUvSdkUnderProgress( - moduleOrProject: ModuleOrProject, +suspend fun setupNewUvSdkAndEnvUnderProgress( + project: Project, + workingDir: Path, existingSdks: List, - python: Path?, - existingSdkWorkingDir: Path? = null, - usePip: Boolean = false, + basePython: Path?, ): Result { + return withBackgroundProgress(project, PyBundle.message("python.sdk.dialog.title.setting.up.uv.environment"), true) { + setupNewUvSdkAndEnv(workingDir, existingSdks, basePython) + } +} - val (pyProjectToml, moduleWorkingDirectory) = resolveWorkingDirectory(moduleOrProject) - val init = pyProjectToml == null - val uvWorkingDir = existingSdkWorkingDir ?: moduleWorkingDirectory - val uv = createUvLowLevel(uvWorkingDir, createUvCli()) +suspend fun setupNewUvSdkAndEnv( + workingDir: Path, + existingSdks: List, + basePython: Path?, +): Result { + val toml = workingDir.resolve(PY_PROJECT_TOML) + val init = !toml.exists() - val envExecutable = - if (existingSdkWorkingDir == null) { - withBackgroundProgress(moduleOrProject.project, PyBundle.message("python.sdk.dialog.title.setting.up.uv.environment"), true) { - uv.initializeEnvironment(init, python) - }.getOrElse { - return Result.failure(it) - } + val uv = createUvLowLevel(workingDir, createUvCli()) + val envExecutable = uv.initializeEnvironment(init, basePython) + .getOrElse { + return Result.failure(it) } - else { - python - } ?: throw IllegalArgumentException("Python executable is required to setup uv environment") - val sdk = createSdk(envExecutable, existingSdks, moduleWorkingDirectory.pathString, suggestedSdkName(moduleWorkingDirectory), UvSdkAdditionalData(existingSdkWorkingDir, usePip)) + return setupExistingEnvAndSdk(envExecutable, null, false, workingDir, existingSdks) +} + +suspend fun setupExistingEnvAndSdk( + envExecutable: Path, + envWorkingDir: Path?, + usePip: Boolean, + projectDir: Path, + existingSdks: List, +): Result { + val sdk = createSdk( + envExecutable, + existingSdks, + projectDir.toString(), + suggestedSdkName(projectDir), + UvSdkAdditionalData(envWorkingDir, usePip)) + sdk.onSuccess { - it.setAssociationToPath(moduleWorkingDirectory.pathString) + it.setAssociationToPath(projectDir.pathString) } return sdk -} - -private suspend fun resolveWorkingDirectory(moduleOrProject: ModuleOrProject): Pair { - val (file, path) = when (moduleOrProject) { - is ModuleOrProject.ModuleAndProject -> PyProjectToml.findModuleWorkingDirectory(moduleOrProject.module) - is ModuleOrProject.ProjectOnly -> Pair(null, PyProjectToml.findProjectWorkingDirectory(moduleOrProject.project)) - } - - if (path == null) { - throw IllegalArgumentException("Path to module or working directory is required") - } - - return file to path } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/uv/ui/AddNewUvPanel.kt b/python/src/com/jetbrains/python/sdk/uv/ui/AddNewUvPanel.kt index ddbbb924de6f..41584b7a86f6 100644 --- a/python/src/com/jetbrains/python/sdk/uv/ui/AddNewUvPanel.kt +++ b/python/src/com/jetbrains/python/sdk/uv/ui/AddNewUvPanel.kt @@ -26,19 +26,20 @@ import com.jetbrains.python.PyBundle import com.jetbrains.python.PySdkBundle import com.jetbrains.python.PythonModuleTypeBase import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo -import com.jetbrains.python.sdk.ModuleOrProject import com.jetbrains.python.sdk.PySdkSettings import com.jetbrains.python.sdk.PythonSdkCoroutineService -import com.jetbrains.python.sdk.add.PyAddNewEnvPanel -import com.jetbrains.python.sdk.add.PySdkPathChoosingComboBox -import com.jetbrains.python.sdk.add.addInterpretersAsync +import com.jetbrains.python.sdk.add.* import com.jetbrains.python.sdk.basePath -import com.jetbrains.python.sdk.uv.* +import com.jetbrains.python.sdk.uv.UV_ICON +import com.jetbrains.python.sdk.uv.getPyProjectTomlForUv import com.jetbrains.python.sdk.uv.impl.detectUvExecutable import com.jetbrains.python.sdk.uv.impl.getUvExecutable import com.jetbrains.python.sdk.uv.impl.setUvExecutable +import com.jetbrains.python.sdk.uv.setupNewUvSdkAndEnvUnderProgress +import com.jetbrains.python.sdk.uv.validateSdks import com.jetbrains.python.statistics.InterpreterTarget import com.jetbrains.python.statistics.InterpreterType +import com.jetbrains.python.venvReader.tryResolvePath import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -46,6 +47,7 @@ import java.awt.BorderLayout import java.awt.Dimension import java.awt.event.ItemEvent import java.nio.file.Path +import java.util.function.Supplier import javax.swing.Icon import javax.swing.JComboBox import javax.swing.event.DocumentEvent @@ -77,6 +79,16 @@ class PyAddNewUvPanel( override val icon: Icon = UV_ICON private val moduleField: JComboBox + + private val environmentLocation = TextFieldWithBrowseButton().apply { + addBrowseFolderListener(project, FileChooserDescriptorFactory.createSingleFolderDescriptor()) + val path = module?.basePath ?: project?.basePath + text = when { + path != null -> path + else -> System.getProperty("user.home") + } + } + private val baseSdkField = PySdkPathChoosingComboBox() init { @@ -97,7 +109,7 @@ class PyAddNewUvPanel( } private val uvPathField = TextFieldWithBrowseButton().apply { - addBrowseFolderListener(null, FileChooserDescriptorFactory.createSingleFileDescriptor()) + addBrowseFolderListener(project, FileChooserDescriptorFactory.createSingleFileDescriptor()) val field = textField as? JBTextField ?: return@apply service().cs.launch { detectUvExecutable()?.let { @@ -140,6 +152,7 @@ class PyAddNewUvPanel( } addLabeledComponent(associatedObjectLabel, moduleField) } + addLabeledComponent(PySdkBundle.message("python.venv.location.label"), environmentLocation) addLabeledComponent(PySdkBundle.message("python.venv.base.label"), baseSdkField) addComponent(installPackagesCheckBox) addLabeledComponent(PyBundle.message("python.sdk.uv.executable"), uvPathField) @@ -150,11 +163,10 @@ class PyAddNewUvPanel( } override fun getOrCreateSdk(): Sdk? { - val module = selectedModule - val path = newProjectPath - val python = baseSdkField.selectedSdk.homePath + val path = tryResolvePath(newProjectPath ?: project?.basePath) + val python = tryResolvePath(baseSdkField.selectedSdk.homePath) - if (module == null || path == null || python == null) { + if (project == null || path == null || python == null) { return null } @@ -162,8 +174,9 @@ class PyAddNewUvPanel( uvPath?.let { setUvExecutable(it) } + val sdk = runBlockingCancellable { - setupUvSdkUnderProgress(ModuleOrProject.ModuleAndProject(module), existingSdks, Path.of(python)) + setupNewUvSdkAndEnvUnderProgress(project, path, existingSdks, python) } sdk.onSuccess { @@ -222,3 +235,22 @@ class PyAddNewUvPanel( private val projectPath: String? get() = newProjectPath ?: selectedModule?.basePath ?: project?.basePath } + +class PyAddUvSdkProvider : PyAddSdkProvider { + override fun createView( + project: Project?, + module: Module?, + newProjectPath: String?, + existingSdks: List, + context: UserDataHolder, + ): PyAddSdkPanel { + val panel = PyAddNewUvPanel(project, module, existingSdks, null, context) + // TODO: support for adding existing uv environments + + return PyAddSdkGroupPanel(Supplier { "uv environment" }, UV_ICON, listOf(panel), panel) + } + + private fun allowCreatingNewEnvironments(project: Project?): Boolean { + return project != null || !PlatformUtils.isPyCharm() || PlatformUtils.isPyCharmEducational() + } +} \ No newline at end of file