From 9b76b13e69d93e124905975509ad37f681237710 Mon Sep 17 00:00:00 2001 From: Aleksandr Sorotskii Date: Fri, 8 Nov 2024 23:16:21 +0100 Subject: [PATCH] basic support for uv env & package manager; PY-75983 GitOrigin-RevId: 2597e4de17e167d8a0b0038190b5127a9dc4b155 --- python/intellij.python.community.impl.iml | 1 + .../pluginCore/resources/META-INF/plugin.xml | 5 + .../UvPackageVersionsInspection.html | 7 + .../metaInformation.json | 4 + .../messages/PyBundle.properties | 17 +- .../python/packaging/common/packages.kt | 4 + .../sdk/add/v2/CustomNewEnvironmentCreator.kt | 6 +- ...entCreator.kt => EnvironmentCreatorPip.kt} | 2 +- ...Creator.kt => EnvironmentCreatorPoetry.kt} | 2 +- .../python/sdk/add/v2/EnvironmentCreatorUv.kt | 43 ++++ ...nvCreator.kt => EnvironmentCreatorVenv.kt} | 2 +- .../sdk/add/v2/PythonAddCustomInterpreter.kt | 18 +- .../com/jetbrains/python/sdk/add/v2/common.kt | 2 + .../com/jetbrains/python/sdk/add/v2/models.kt | 48 ++-- python/src/com/jetbrains/python/sdk/uv/Uv.kt | 21 ++ .../src/com/jetbrains/python/sdk/uv/UvExt.kt | 87 +++++++ .../jetbrains/python/sdk/uv/UvInspections.kt | 80 +++++++ .../python/sdk/uv/UvPackageManager.kt | 67 ++++++ .../jetbrains/python/sdk/uv/UvQuickFixes.kt | 64 ++++++ .../python/sdk/uv/UvSdkFlavorAndData.kt | 59 +++++ .../jetbrains/python/sdk/uv/UvSdkProvider.kt | 72 ++++++ .../com/jetbrains/python/sdk/uv/UvUtils.kt | 25 ++ .../com/jetbrains/python/sdk/uv/impl/UvCli.kt | 63 +++++ .../python/sdk/uv/impl/UvLowLevel.kt | 105 +++++++++ .../python/sdk/uv/ui/AddNewUvPanel.kt | 216 ++++++++++++++++++ .../python/statistics/PyStatisticTools.kt | 2 +- 26 files changed, 976 insertions(+), 46 deletions(-) create mode 100644 python/pluginResources/inspectionDescriptions/UvPackageVersionsInspection.html rename python/src/com/jetbrains/python/sdk/add/v2/{PipEnvNewEnvironmentCreator.kt => EnvironmentCreatorPip.kt} (91%) rename python/src/com/jetbrains/python/sdk/add/v2/{PoetryNewEnvironmentCreator.kt => EnvironmentCreatorPoetry.kt} (96%) create mode 100644 python/src/com/jetbrains/python/sdk/add/v2/EnvironmentCreatorUv.kt rename python/src/com/jetbrains/python/sdk/add/v2/{PythonNewVirtualenvCreator.kt => EnvironmentCreatorVenv.kt} (98%) create mode 100644 python/src/com/jetbrains/python/sdk/uv/Uv.kt create mode 100644 python/src/com/jetbrains/python/sdk/uv/UvExt.kt create mode 100644 python/src/com/jetbrains/python/sdk/uv/UvInspections.kt create mode 100644 python/src/com/jetbrains/python/sdk/uv/UvPackageManager.kt create mode 100644 python/src/com/jetbrains/python/sdk/uv/UvQuickFixes.kt create mode 100644 python/src/com/jetbrains/python/sdk/uv/UvSdkFlavorAndData.kt create mode 100644 python/src/com/jetbrains/python/sdk/uv/UvSdkProvider.kt create mode 100644 python/src/com/jetbrains/python/sdk/uv/UvUtils.kt create mode 100644 python/src/com/jetbrains/python/sdk/uv/impl/UvCli.kt create mode 100644 python/src/com/jetbrains/python/sdk/uv/impl/UvLowLevel.kt create mode 100644 python/src/com/jetbrains/python/sdk/uv/ui/AddNewUvPanel.kt diff --git a/python/intellij.python.community.impl.iml b/python/intellij.python.community.impl.iml index 47086ef55bb4..5c913392467a 100644 --- a/python/intellij.python.community.impl.iml +++ b/python/intellij.python.community.impl.iml @@ -157,5 +157,6 @@ + \ No newline at end of file diff --git a/python/pluginCore/resources/META-INF/plugin.xml b/python/pluginCore/resources/META-INF/plugin.xml index 25a522f05b51..f3723e3a74b3 100644 --- a/python/pluginCore/resources/META-INF/plugin.xml +++ b/python/pluginCore/resources/META-INF/plugin.xml @@ -59,6 +59,7 @@ The Python plug-in provides smart editing for Python scripts. The feature set of + + + @@ -858,6 +861,8 @@ The Python plug-in provides smart editing for Python scripts. The feature set of + + diff --git a/python/pluginResources/inspectionDescriptions/UvPackageVersionsInspection.html b/python/pluginResources/inspectionDescriptions/UvPackageVersionsInspection.html new file mode 100644 index 000000000000..14b8075f9f29 --- /dev/null +++ b/python/pluginResources/inspectionDescriptions/UvPackageVersionsInspection.html @@ -0,0 +1,7 @@ + + +

Reports outdated versions of packages in [dependencies] and [dev-dependencies] + sections of pyproject.toml. +

+ + \ No newline at end of file diff --git a/python/pluginResources/inspectionDescriptions/metaInformation.json b/python/pluginResources/inspectionDescriptions/metaInformation.json index 2d7d1b4096a4..bd0961ef7f5a 100644 --- a/python/pluginResources/inspectionDescriptions/metaInformation.json +++ b/python/pluginResources/inspectionDescriptions/metaInformation.json @@ -12,6 +12,10 @@ "id": "PoetryPackageVersionsInspection", "codeQualityCategory": "Sanity" }, + { + "id": "UvPackageVersionsInspection", + "codeQualityCategory": "Sanity" + }, { "id": "PyStubPackagesCompatibilityInspection", "codeQualityCategory": "Sanity" diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties index e73e8a991fc7..5a9a4b1d2666 100644 --- a/python/pluginResources/messages/PyBundle.properties +++ b/python/pluginResources/messages/PyBundle.properties @@ -273,7 +273,7 @@ python.console=Python Console python.console.history.root=Python Consoles python.console.run.anything.provider=Runs Python Console python.console.not.supported=Python console for {0} interpreter is not supported -python.console.toolbar.action.available.non.interactive=The action is not available for non-interactive shell +python.console.toolbar.action.available.non.interactive=The action is not available for non-interactive shell runcfg.unittest.dlg.test_function_title=Function run.configuration.remote.debug.name=Python Remote Debug @@ -395,6 +395,20 @@ python.sdk.poetry.install.packages.from.toml.checkbox.text=Install packages from python.sdk.poetry.dialog.message.poetry.interpreter.has.been.already.added=Poetry interpreter has been already added, select ''{0}'' python.sdk.poetry.dialog.add.new.environment.in.project.checkbox=Create an in-project environment +# UV +python.sdk.dialog.message.creating.virtual.environments.based.on.uv.environments.not.supported=Creating virtual environments based on UV environments is not supported +python.sdk.dialog.title.setting.up.uv.environment=Setting up UV environment +python.sdk.inspection.message.uv.interpreter.associated.with.another.project=Uv interpreter is associated with another {0}: {1} +python.sdk.inspection.message.uv.interpreter.not.associated.with.any.project=Uv interpreter is not associated with any {0} +python.sdk.intention.family.name.install.requirements.from.uv.lock=Install requirements from uv.lock +python.sdk.quickfix.use.uv.name=Use UV interpreter +python.sdk.uv.associated.module=Associated module: +python.sdk.uv.associated.project=Associated project: +python.sdk.uv.environment.panel.title=Uv Environment +python.sdk.uv.executable.not.found=UV executable is not found +python.sdk.uv.executable=Uv executable: +python.sdk.uv.install.packages.from.toml.checkbox.text=Install packages from pyproject.toml + python.sdk.pipenv.has.been.selected=Pipenv interpreter has been already added, select ''{0}'' in your interpreters list python.sdk.there.is.no.interpreter=No interpreter python.sdk.no.interpreter.configured.warning=No Python interpreter configured for the project @@ -541,6 +555,7 @@ sdk.create.custom.virtualenv=Virtualenv sdk.create.custom.conda=Conda sdk.create.custom.pipenv=Pipenv sdk.create.custom.poetry=Poetry +sdk.create.custom.uv=Uv sdk.create.custom.python=Python sdk.rendering.detected.grey.text=detected in the system diff --git a/python/src/com/jetbrains/python/packaging/common/packages.kt b/python/src/com/jetbrains/python/packaging/common/packages.kt index acb1b000b72d..861db2f96068 100644 --- a/python/src/com/jetbrains/python/packaging/common/packages.kt +++ b/python/src/com/jetbrains/python/packaging/common/packages.kt @@ -31,6 +31,10 @@ open class PythonPackage(val name: String, val version: String, val isEditableMo } } +open class PythonOutdatedPackage(name: String, version: String, isEditableMode: Boolean, val latestVersion: String) + : PythonPackage(name, version, isEditableMode) +{} + interface PythonPackageDetails { val name: String 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 4af525df7684..4e393d92ea54 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/CustomNewEnvironmentCreator.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/CustomNewEnvironmentCreator.kt @@ -66,9 +66,13 @@ abstract class CustomNewEnvironmentCreator(private val name: String, model: Pyth ProjectJdkTable.getInstance().allJdks.asList(), model.myProjectPathFlows.projectPathWithDefault.first().toString(), homePath, - false).getOrElse { return Result.failure(it) } + false) + .getOrElse { return Result.failure(it) } newSdk.persist() + + module?.excludeInnerVirtualEnv(newSdk) model.addInterpreter(newSdk) + return Result.success(newSdk) } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PipEnvNewEnvironmentCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/EnvironmentCreatorPip.kt similarity index 91% rename from python/src/com/jetbrains/python/sdk/add/v2/PipEnvNewEnvironmentCreator.kt rename to python/src/com/jetbrains/python/sdk/add/v2/EnvironmentCreatorPip.kt index b76716a05457..861b2694687f 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PipEnvNewEnvironmentCreator.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/EnvironmentCreatorPip.kt @@ -13,7 +13,7 @@ import com.jetbrains.python.statistics.InterpreterType import java.nio.file.Path -class PipEnvNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel) : CustomNewEnvironmentCreator("pipenv", model) { +class EnvironmentCreatorPip(model: PythonMutableTargetAddInterpreterModel) : CustomNewEnvironmentCreator("pipenv", model) { override val interpreterType: InterpreterType = InterpreterType.PIPENV override val executable: ObservableMutableProperty = model.state.pipenvExecutable override val installationScript: Path? = null diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PoetryNewEnvironmentCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/EnvironmentCreatorPoetry.kt similarity index 96% rename from python/src/com/jetbrains/python/sdk/add/v2/PoetryNewEnvironmentCreator.kt rename to python/src/com/jetbrains/python/sdk/add/v2/EnvironmentCreatorPoetry.kt index f4e27405ba87..1b1b56541ca5 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PoetryNewEnvironmentCreator.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/EnvironmentCreatorPoetry.kt @@ -34,7 +34,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.withContext import java.nio.file.Path -class PoetryNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel, private val moduleOrProject: ModuleOrProject?) : CustomNewEnvironmentCreator("poetry", model) { +class EnvironmentCreatorPoetry(model: PythonMutableTargetAddInterpreterModel, private val moduleOrProject: ModuleOrProject?) : CustomNewEnvironmentCreator("poetry", model) { override val interpreterType: InterpreterType = InterpreterType.POETRY override val executable: ObservableMutableProperty = model.state.poetryExecutable override val installationScript = PythonHelpersLocator.findPathInHelpers("pycharm_poetry_installer.py") diff --git a/python/src/com/jetbrains/python/sdk/add/v2/EnvironmentCreatorUv.kt b/python/src/com/jetbrains/python/sdk/add/v2/EnvironmentCreatorUv.kt new file mode 100644 index 000000000000..91607135a144 --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/add/v2/EnvironmentCreatorUv.kt @@ -0,0 +1,43 @@ +// 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 + +import com.intellij.ide.util.PropertiesComponent +import com.intellij.openapi.module.Module +import com.intellij.openapi.observable.properties.ObservableMutableProperty +import com.intellij.openapi.project.Project +import com.intellij.openapi.projectRoots.Sdk +import com.jetbrains.python.sdk.ModuleOrProject +import com.jetbrains.python.sdk.uv.uvPath +import com.jetbrains.python.sdk.uv.setupUvSdkUnderProgress +import com.jetbrains.python.statistics.InterpreterType +import java.nio.file.Path + +class EnvironmentCreatorUv(model: PythonMutableTargetAddInterpreterModel, private val moduleOrProject: ModuleOrProject?) : CustomNewEnvironmentCreator("uv", model) { + override val interpreterType: InterpreterType = InterpreterType.UV + override val executable: ObservableMutableProperty = model.state.uvExecutable + // FIXME: support uv installation + override val installationScript = null + + override fun onShown() { + // FIXME: validate base interpreters against pyprojecttoml version. See poetry + basePythonComboBox.setItems(model.baseInterpreters) + } + + override fun savePathToExecutableToProperties() { + PropertiesComponent.getInstance().uvPath = Path.of(executable.get()) + } + + 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 Result.failure(Exception("module is null")) + } + + val python = homePath?.let { Path.of(it) } + return setupUvSdkUnderProgress(module, Path.of(projectPath), baseSdks, python) + } + + override suspend fun detectExecutable() { + model.detectUvExecutable() + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonNewVirtualenvCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/EnvironmentCreatorVenv.kt similarity index 98% rename from python/src/com/jetbrains/python/sdk/add/v2/PythonNewVirtualenvCreator.kt rename to python/src/com/jetbrains/python/sdk/add/v2/EnvironmentCreatorVenv.kt index dbd5dc53f3b8..7f15ddc35267 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PythonNewVirtualenvCreator.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/EnvironmentCreatorVenv.kt @@ -36,7 +36,7 @@ import java.nio.file.Paths import kotlin.io.path.exists import kotlin.io.path.isDirectory -class PythonNewVirtualenvCreator(model: PythonMutableTargetAddInterpreterModel) : PythonNewEnvironmentCreator(model) { +class EnvironmentCreatorVenv(model: PythonMutableTargetAddInterpreterModel) : PythonNewEnvironmentCreator(model) { private lateinit var versionComboBox: PythonInterpreterComboBox private val locationValidationFailed = propertyGraph.property(false) diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddCustomInterpreter.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddCustomInterpreter.kt index db36d8ec4d5d..d687a00663b4 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddCustomInterpreter.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddCustomInterpreter.kt @@ -27,10 +27,11 @@ class PythonAddCustomInterpreter(val model: PythonMutableTargetAddInterpreterMod private val existingInterpreterManager = propertyGraph.property(PYTHON) private val newInterpreterCreators = mapOf( - VIRTUALENV to PythonNewVirtualenvCreator(model), - CONDA to CondaNewEnvironmentCreator(model, errorSink), - PIPENV to PipEnvNewEnvironmentCreator(model), - POETRY to PoetryNewEnvironmentCreator(model, moduleOrProject), + VIRTUALENV to EnvironmentCreatorVenv(model), + CONDA to CondaNewEnvironmentCreator(model, errorSink), + PIPENV to EnvironmentCreatorPip(model), + POETRY to EnvironmentCreatorPoetry(model, moduleOrProject), + UV to EnvironmentCreatorUv(model, moduleOrProject), ) private val existingInterpreterSelectors = mapOf( @@ -52,15 +53,6 @@ class PythonAddCustomInterpreter(val model: PythonMutableTargetAddInterpreterMod navigator.existingEnvManager = existingInterpreterManager } - // todo delete this. testing busy state - //existingInterpreterManager.afterChange { - // model.scope.launch { - // model.interpreterLoading.value = true - // delay(5000) - // model.interpreterLoading.value = false - // } - //} - with(outerPanel) { buttonsGroup { row(message("sdk.create.custom.env.creation.type")) { diff --git a/python/src/com/jetbrains/python/sdk/add/v2/common.kt b/python/src/com/jetbrains/python/sdk/add/v2/common.kt index d867ae0d36b8..22f8e150f512 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/common.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/common.kt @@ -21,6 +21,7 @@ import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo import com.jetbrains.python.sdk.* import com.jetbrains.python.sdk.pipenv.PIPENV_ICON import com.jetbrains.python.sdk.poetry.POETRY_ICON +import com.jetbrains.python.sdk.uv.UV_ICON import com.jetbrains.python.statistics.InterpreterTarget import kotlinx.coroutines.CoroutineScope import javax.swing.Icon @@ -58,6 +59,7 @@ enum class PythonSupportedEnvironmentManagers(val nameKey: String, val icon: Ico CONDA("sdk.create.custom.conda", PythonIcons.Python.Anaconda), POETRY("sdk.create.custom.poetry", POETRY_ICON), PIPENV("sdk.create.custom.pipenv", PIPENV_ICON), + UV("sdk.create.custom.uv", UV_ICON), PYTHON("sdk.create.custom.python", com.jetbrains.python.psi.icons.PythonPsiApiIcons.Python) } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/models.kt b/python/src/com/jetbrains/python/sdk/add/v2/models.kt index 5a0f26c5a03c..6a341a2e49ec 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/models.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/models.kt @@ -2,7 +2,6 @@ package com.jetbrains.python.sdk.add.v2 import com.intellij.execution.target.TargetEnvironmentConfiguration -import com.intellij.ide.util.PropertiesComponent import com.intellij.openapi.application.EDT import com.intellij.openapi.diagnostic.getOrLogException import com.intellij.openapi.fileChooser.FileChooser @@ -22,8 +21,9 @@ import com.jetbrains.python.sdk.conda.suggestCondaPath import com.jetbrains.python.sdk.flavors.PythonSdkFlavor import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity -import com.jetbrains.python.sdk.pipenv.pipEnvPath -import com.jetbrains.python.sdk.poetry.poetryPath +import com.jetbrains.python.sdk.pipenv.getPipEnvExecutable +import com.jetbrains.python.sdk.poetry.getPoetryExecutable +import com.jetbrains.python.sdk.uv.getUvExecutable import com.jetbrains.python.util.ErrorSink import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -94,11 +94,6 @@ abstract class PythonAddInterpreterModel(params: PyInterpreterModelParams) { withContext(uiContext) { state.condaExecutable.set(suggestedCondaLocalPath?.toString().orEmpty()) } - - //val environments = suggestedCondaPath?.let { PyCondaEnv.getEnvs(executor, suggestedCondaPath).getOrLogException( - // PythonAddInterpreterPresenter.LOG) } - //baseConda = environments?.find { env -> env.envIdentity.let { it is PyCondaEnvIdentity.UnnamedEnv && it.isBase } } - } } @@ -172,34 +167,32 @@ abstract class PythonMutableTargetAddInterpreterModel(params: PyInterpreterModel super.initialize() detectPoetryExecutable() detectPipEnvExecutable() + detectUvExecutable() } suspend fun detectPoetryExecutable() { - // todo this is local case, fix for targets - val savedPath = PropertiesComponent.getInstance().poetryPath - if (savedPath != null) { - state.poetryExecutable.set(savedPath) - } - else { - com.jetbrains.python.sdk.poetry.detectPoetryExecutable().getOrNull()?.let { - withContext(Dispatchers.EDT) { - state.poetryExecutable.set(it.pathString) - } + // FIXME: support targets + getPoetryExecutable().getOrNull()?.let { + withContext(Dispatchers.EDT) { + state.poetryExecutable.set(it.pathString) } } } suspend fun detectPipEnvExecutable() { - // todo this is local case, fix for targets - val savedPath = PropertiesComponent.getInstance().pipEnvPath - if (savedPath != null) { - state.pipenvExecutable.set(savedPath) + // FIXME: support targets + getPipEnvExecutable().getOrNull()?.let { + withContext(Dispatchers.EDT) { + state.pipenvExecutable.set(it.pathString) + } } - else { - com.jetbrains.python.sdk.pipenv.detectPipEnvExecutable().getOrNull()?.let { - withContext(Dispatchers.EDT) { - state.pipenvExecutable.set(it.pathString) - } + } + + suspend fun detectUvExecutable() { + // FIXME: support targets + getUvExecutable()?.pathString?.let { + withContext(Dispatchers.EDT) { + state.uvExecutable.set(it) } } } @@ -289,6 +282,7 @@ class MutableTargetState(propertyGraph: PropertyGraph) : AddInterpreterState(pro val baseInterpreter: ObservableMutableProperty = propertyGraph.property(null) val newCondaEnvName: ObservableMutableProperty = propertyGraph.property("") val poetryExecutable: ObservableMutableProperty = propertyGraph.property("") + val uvExecutable: ObservableMutableProperty = propertyGraph.property("") val pipenvExecutable: ObservableMutableProperty = propertyGraph.property("") val venvPath: ObservableMutableProperty = propertyGraph.property("") val inheritSitePackages = propertyGraph.property(false) diff --git a/python/src/com/jetbrains/python/sdk/uv/Uv.kt b/python/src/com/jetbrains/python/sdk/uv/Uv.kt new file mode 100644 index 000000000000..bde58ff6e85e --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/uv/Uv.kt @@ -0,0 +1,21 @@ +// 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.uv + +import com.jetbrains.python.packaging.common.PythonPackage +import com.jetbrains.python.packaging.common.PythonOutdatedPackage +import com.jetbrains.python.packaging.common.PythonPackageSpecification +import java.nio.file.Path + +interface UvCli { + suspend fun runUv(workingDir: Path, vararg args: String): Result +} + +interface UvLowLevel { + suspend fun initializeEnvironment(init: Boolean, python: Path?): Result + + suspend fun listPackages(): Result> + suspend fun listOutdatedPackages(): Result> + + suspend fun installPackage(name: PythonPackageSpecification, options: List): Result + suspend fun uninstallPackage(name: PythonPackage): Result +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/uv/UvExt.kt b/python/src/com/jetbrains/python/sdk/uv/UvExt.kt new file mode 100644 index 000000000000..3faf20b1600e --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/uv/UvExt.kt @@ -0,0 +1,87 @@ +// 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.uv + +import com.intellij.ide.util.PropertiesComponent +import com.intellij.openapi.module.Module +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.util.NlsSafe +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.platform.ide.progress.withBackgroundProgress +import com.intellij.util.PathUtil +import com.jetbrains.python.PyBundle +import com.jetbrains.python.icons.PythonIcons +import com.jetbrains.python.sdk.createSdk +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.createUvLowLevel +import com.jetbrains.python.sdk.uv.impl.detectUvExecutable +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.nio.file.Path +import kotlin.io.path.exists +import kotlin.io.path.pathString + +internal const val UV_PATH_SETTING: String = "PyCharm.UV.Path" + +internal val Sdk.isUv: Boolean + get() = sdkAdditionalData is UvSdkAdditionalData + +internal suspend fun uvLock(module: com.intellij.openapi.module.Module): VirtualFile? { + return withContext(Dispatchers.IO) { + findAmongRoots(module, UV_LOCK) + } +} + +internal suspend fun pyProjectToml(module: Module): VirtualFile? { + return withContext(Dispatchers.IO) { + findAmongRoots(module, PY_PROJECT_TOML) + } +} + +internal fun suggestedSdkName(basePath: Path): @NlsSafe String { + return "UV (${PathUtil.getFileName(basePath.pathString)})" +} + +// FIXME: use proper icon +val UV_ICON = PythonIcons.Python.Pandas +val UV_LOCK: String = "uv.lock" + +// FIXME: move pyprojecttoml code out to common package +val PY_PROJECT_TOML: String = "pyproject.toml" + +var PropertiesComponent.uvPath: Path? + get() { + return getValue(UV_PATH_SETTING)?.let { Path.of(it) } + } + set(value) { + setValue(UV_PATH_SETTING, value.toString()) + } + +fun getUvExecutable(): Path? { + return PropertiesComponent.getInstance().uvPath?.takeIf { it.exists() } ?: detectUvExecutable() +} + +suspend fun setupUvSdkUnderProgress( + module: Module, + projectPath: Path, + existingSdks: List, + python: Path? +): Result { + val uv = createUvLowLevel(projectPath, createUvCli()) + + val init = pyProjectToml(module) == null + val envExecutable = + withBackgroundProgress(module.project, PyBundle.message("python.sdk.dialog.title.setting.up.uv.environment"), true) { + uv.initializeEnvironment(init, python) + }.getOrElse { + return Result.failure(it) + } + + val sdk = createSdk(envExecutable, existingSdks, projectPath.pathString, suggestedSdkName(projectPath), UvSdkAdditionalData()) + sdk.onSuccess { + it.setAssociationToModule(module) + } + + return sdk +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/uv/UvInspections.kt b/python/src/com/jetbrains/python/sdk/uv/UvInspections.kt new file mode 100644 index 000000000000..6f32f37a699e --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/uv/UvInspections.kt @@ -0,0 +1,80 @@ +// 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.uv + +import com.intellij.codeInspection.LocalInspectionTool +import com.intellij.codeInspection.LocalInspectionToolSession +import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleUtilCore +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.PsiFile +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread +import com.jetbrains.python.PyBundle +import com.jetbrains.python.packaging.management.PythonPackageManager +import com.jetbrains.python.sdk.PythonSdkUtil +import com.jetbrains.python.sdk.findAmongRoots +import org.toml.lang.psi.TomlKeyValue +import org.toml.lang.psi.TomlTable +import kotlin.collections.contains + +internal class UvPackageVersionsInspection : LocalInspectionTool() { + override fun buildVisitor( + holder: ProblemsHolder, + isOnTheFly: Boolean, + session: LocalInspectionToolSession, + ): PsiElementVisitor { + return UvFileVisitor(holder, session) + } + + internal class UvFileVisitor( + val holder: ProblemsHolder, + session: LocalInspectionToolSession, + ) : PsiElementVisitor() { + @RequiresBackgroundThread + private fun guessModule(element: PsiElement): Module? { + return ModuleUtilCore.findModuleForPsiElement(element) + } + + @RequiresBackgroundThread + private fun Module.pyProjectTomlBlocking(): VirtualFile? = findAmongRoots(this, PY_PROJECT_TOML) + + @RequiresBackgroundThread + override fun visitFile(file: PsiFile) { + val module = guessModule(file) + if (module == null) { + return + } + + val sdk = PythonSdkUtil.findPythonSdk(module) + if (sdk == null || !sdk.isUv) { + return + } + + if (file.virtualFile != module.pyProjectTomlBlocking()) { + return + } + + file.children + .filter { element -> + (element as? TomlTable)?.header?.key?.text in listOf("dependencies", "dev-dependencies") + }.flatMap { + it.children.mapNotNull { line -> line as? TomlKeyValue } + }.forEach { keyValue -> + val packageName = keyValue.key.text + val outdated = (PythonPackageManager.forSdk( + module.project, sdk) as? UvPackageManager)?.let { + it.outdatedPackages[packageName] + } + + if (outdated != null) { + val message = PyBundle.message("python.sdk.inspection.message.version.outdated.latest", + packageName, outdated.version, outdated.latestVersion) + holder.registerProblem(keyValue, message, ProblemHighlightType.WARNING) + } + } + } + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/uv/UvPackageManager.kt b/python/src/com/jetbrains/python/sdk/uv/UvPackageManager.kt new file mode 100644 index 000000000000..40c617b7abfa --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/uv/UvPackageManager.kt @@ -0,0 +1,67 @@ +// 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.uv + +import com.intellij.openapi.project.Project +import com.intellij.openapi.projectRoots.Sdk +import com.jetbrains.python.packaging.common.PythonOutdatedPackage +import com.jetbrains.python.packaging.common.PythonPackage +import com.jetbrains.python.packaging.common.PythonPackageSpecification +import com.jetbrains.python.packaging.management.PythonPackageManager +import com.jetbrains.python.packaging.management.PythonPackageManagerProvider +import com.jetbrains.python.packaging.management.PythonRepositoryManager +import com.jetbrains.python.packaging.pip.PipRepositoryManager +import com.jetbrains.python.sdk.uv.impl.createUvCli +import com.jetbrains.python.sdk.uv.impl.createUvLowLevel +import java.nio.file.Path + +internal class UvPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(project, sdk) { + override var installedPackages: List = emptyList() + override val repositoryManager: PythonRepositoryManager = PipRepositoryManager(project, sdk) + + private val uv: UvLowLevel = createUvLowLevel(Path.of(project.basePath!!), createUvCli()) + + @Volatile + var outdatedPackages: Map = emptyMap() + + override suspend fun installPackageCommand(specification: PythonPackageSpecification, options: List): Result { + uv.installPackage(specification, options).getOrElse { + return Result.failure(it) + } + + // FIXME: refactor command return value, it's not used + return Result.success("") + } + + override suspend fun updatePackageCommand(specification: PythonPackageSpecification): Result { + uv.installPackage(specification, emptyList()).getOrElse { + return Result.failure(it) + } + + // FIXME: refactor command return value, it's not used + return Result.success("") + } + + override suspend fun uninstallPackageCommand(pkg: PythonPackage): Result { + uv.uninstallPackage(pkg).getOrElse { + return Result.failure(it) + } + + // FIXME: refactor command return value, it's not used + return Result.success("") + } + + override suspend fun reloadPackagesCommand(): Result> { + // ignoring errors as handling outdated packages is pretty new option + uv.listOutdatedPackages().onSuccess { + outdatedPackages = it.associateBy { it.name } + } + + return uv.listPackages() + } +} + +class UvPackageManagerProvider : PythonPackageManagerProvider { + override fun createPackageManagerForSdk(project: Project, sdk: Sdk): PythonPackageManager? { + return if (sdk.isUv) UvPackageManager(project, sdk) else null + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/uv/UvQuickFixes.kt b/python/src/com/jetbrains/python/sdk/uv/UvQuickFixes.kt new file mode 100644 index 000000000000..1af0d11d6c1e --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/uv/UvQuickFixes.kt @@ -0,0 +1,64 @@ +// 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.uv + +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleUtilCore +import com.intellij.openapi.project.Project +import com.jetbrains.python.PyBundle +import com.jetbrains.python.inspections.PyPackageRequirementsInspection +import com.jetbrains.python.packaging.PyPackageManagerUI +import com.jetbrains.python.sdk.pythonSdk +import com.jetbrains.python.sdk.setAssociationToModule + +internal class UvAssociationQuickFix : LocalQuickFix { + private val quickFixName = PyBundle.message("python.sdk.quickfix.use.uv.name") + + override fun getFamilyName() = quickFixName + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val element = descriptor.psiElement + if (element == null) { + return + } + + val module = ModuleUtilCore.findModuleForPsiElement(element) + if (module == null) { + return + } + + module.pythonSdk?.setAssociationToModule(module) + } +} + +class UvInstallQuickFix : LocalQuickFix { + companion object { + fun uvInstall(project: Project, module: Module) { + val sdk = module.pythonSdk + if (sdk == null || !sdk.isUv) { + return + } + + val listener = PyPackageRequirementsInspection.RunningPackagingTasksListener(module) + val ui = PyPackageManagerUI(project, sdk, listener) + ui.install(null, listOf()) + } + } + + override fun getFamilyName() = PyBundle.message("python.sdk.intention.family.name.install.requirements.from.uv.lock") + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val element = descriptor.psiElement + if (element == null) { + return + } + + val module = ModuleUtilCore.findModuleForPsiElement(element) + if (module == null) { + return + } + + uvInstall(project, module) + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/uv/UvSdkFlavorAndData.kt b/python/src/com/jetbrains/python/sdk/uv/UvSdkFlavorAndData.kt new file mode 100644 index 000000000000..8b8133001bd6 --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/uv/UvSdkFlavorAndData.kt @@ -0,0 +1,59 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.jetbrains.python.sdk.uv + +import com.jetbrains.python.sdk.PythonSdkAdditionalData +import com.jetbrains.python.sdk.flavors.PyFlavorData +import com.jetbrains.python.sdk.flavors.PythonFlavorProvider +import com.jetbrains.python.sdk.flavors.PythonSdkFlavor +import org.jdom.Element + + +class UvSdkAdditionalData : PythonSdkAdditionalData { + constructor() : super(UvSdkFlavor) + constructor(data: PythonSdkAdditionalData) : super(data) + + override fun save(element: Element) { + super.save(element) + element.setAttribute(IS_UV, "true") + } + + companion object { + private const val IS_UV = "IS_UV" + + @JvmStatic + fun load(element: Element): UvSdkAdditionalData? { + return when { + element.getAttributeValue(IS_UV) == "true" -> { + UvSdkAdditionalData().apply { + load(element) + } + } + else -> null + } + } + + @JvmStatic + fun copy(data: PythonSdkAdditionalData): UvSdkAdditionalData { + return UvSdkAdditionalData(data) + } + } +} + +object UvSdkFlavor : PythonSdkFlavor() { + override fun getIcon() = UV_ICON + override fun getFlavorDataClass(): Class = PyFlavorData.Empty::class.java + + override fun isValidSdkPath(pathStr: String): Boolean { + return false + } + + override fun getName(): String { + return "Uv"; + } +} + +class UvSdkFlavorProvider : PythonFlavorProvider { + override fun getFlavor(platformIndependent: Boolean): PythonSdkFlavor<*> { + return UvSdkFlavor + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/uv/UvSdkProvider.kt b/python/src/com/jetbrains/python/sdk/uv/UvSdkProvider.kt new file mode 100644 index 000000000000..0af47d27fcd3 --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/uv/UvSdkProvider.kt @@ -0,0 +1,72 @@ +// 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.uv + +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.openapi.module.Module +import com.intellij.openapi.project.Project +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.projectRoots.SdkAdditionalData +import com.intellij.openapi.util.UserDataHolder +import com.jetbrains.python.PyBundle +import com.jetbrains.python.sdk.* +import com.jetbrains.python.sdk.add.PyAddNewEnvPanel +import com.jetbrains.python.sdk.uv.ui.PyAddNewUvPanel +import org.jdom.Element +import javax.swing.Icon + +/** + * This source code is created by @koxudaxi Koudai Aono + */ + +class UvSdkProvider : PySdkProvider { + override fun createEnvironmentAssociationFix( + module: Module, + sdk: Sdk, + isPyCharm: Boolean, + associatedModulePath: String?, + ): PyInterpreterInspectionQuickFixData? { + if (sdk.isUv) { + val projectUnit = if (isPyCharm) "project" else "module" + val message = when { + associatedModulePath != null -> + PyBundle.message("python.sdk.inspection.message.uv.interpreter.associated.with.another.project", projectUnit, associatedModulePath) + else -> PyBundle.message("python.sdk.inspection.message.uv.interpreter.not.associated.with.any.project", projectUnit) + } + return PyInterpreterInspectionQuickFixData(UvAssociationQuickFix(), message) + } + + return null + } + + override fun createInstallPackagesQuickFix(module: Module): LocalQuickFix? { + val sdk = PythonSdkUtil.findPythonSdk(module) ?: return null + return if (sdk.isUv) UvInstallQuickFix() else null + } + + override fun createNewEnvironmentPanel( + project: Project?, + module: Module?, + existingSdks: List, + newProjectPath: String?, + context: UserDataHolder, + ): PyAddNewEnvPanel { + return PyAddNewUvPanel(project, module, existingSdks, newProjectPath, context) + } + + override fun getSdkAdditionalText(sdk: Sdk): String? = if (sdk.isUv) sdk.versionString else null + + override fun getSdkIcon(sdk: Sdk): Icon? { + return if (sdk.isUv) UV_ICON else null + } + + override fun loadAdditionalDataForSdk(element: Element): SdkAdditionalData? { + return UvSdkAdditionalData.load(element) + } +} + +internal fun validateSdks(module: Module?, existingSdks: List, context: UserDataHolder): List { + val sdks = findBaseSdks(existingSdks, module, context).takeIf { it.isNotEmpty() } + ?: detectSystemWideSdks(module, existingSdks, context) + + return sdks.filter { it.sdkSeemsValid && !it.isUv } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/uv/UvUtils.kt b/python/src/com/jetbrains/python/sdk/uv/UvUtils.kt new file mode 100644 index 000000000000..7bf94581efe0 --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/uv/UvUtils.kt @@ -0,0 +1,25 @@ +// 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.uv + +import com.intellij.openapi.application.readAction +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.vfs.VirtualFile +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.apache.tuweni.toml.Toml +import java.io.IOException + +val LOGGER = Logger.getInstance("#com.jetbrains.python.sdk.uv") + +internal suspend fun getPyProjectTomlForUv(virtualFile: VirtualFile): VirtualFile? = + withContext(Dispatchers.IO) { + readAction { + try { + Toml.parse(virtualFile.inputStream).getTable("tool.uv")?.let { virtualFile } + } + catch (e: IOException) { + LOGGER.info(e) + null + } + } + } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/uv/impl/UvCli.kt b/python/src/com/jetbrains/python/sdk/uv/impl/UvCli.kt new file mode 100644 index 000000000000..e0b626499916 --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/uv/impl/UvCli.kt @@ -0,0 +1,63 @@ +// 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.uv.impl + +import com.intellij.execution.configurations.PathEnvironmentVariableUtil +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.util.SystemProperties +import com.jetbrains.python.PyBundle +import com.jetbrains.python.pathValidation.PlatformAndRoot +import com.jetbrains.python.pathValidation.ValidationRequest +import com.jetbrains.python.pathValidation.validateExecutableFile +import com.jetbrains.python.sdk.runExecutable +import com.jetbrains.python.sdk.uv.UvCli +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +import java.nio.file.Path + +import kotlin.io.path.exists +import kotlin.io.path.pathString + +internal fun detectUvExecutable(): Path? { + val name = "uv" + return PathEnvironmentVariableUtil.findInPath(name)?.toPath() ?: SystemProperties.getUserHome().let { homePath -> + Path.of(homePath, ".cargo", "bin", name).takeIf { it.exists() } + } +} + +internal fun validateUvExecutable(uvPath: Path?): ValidationInfo? { + return validateExecutableFile(ValidationRequest( + path = uvPath?.pathString, + fieldIsEmpty = PyBundle.message("python.sdk.uv.executable.not.found"), + // FIXME: support targets + platformAndRoot = PlatformAndRoot.local + )) +} + +internal suspend fun runUv(uv: Path, workingDir: Path, vararg args: String): Result { + return runExecutable(uv, workingDir, *args) +} + +internal class UvCliImpl(val dispatcher: CoroutineDispatcher, uvPath: Path?): UvCli { + val uv: Path + + init { + val path = uvPath ?: detectUvExecutable() + val error = validateUvExecutable(path) + if (error != null) { + throw RuntimeException(error.message) + } + + uv = path!! + } + + override suspend fun runUv(workingDir: Path, vararg args: String): Result { + with(Dispatchers.IO) { + return runUv(uv, workingDir, *args) + } + } +} + +fun createUvCli(dispatcher: CoroutineDispatcher = Dispatchers.IO, uv: Path? = null): UvCli { + return UvCliImpl(dispatcher, uv) +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/uv/impl/UvLowLevel.kt b/python/src/com/jetbrains/python/sdk/uv/impl/UvLowLevel.kt new file mode 100644 index 000000000000..ca628a0e5233 --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/uv/impl/UvLowLevel.kt @@ -0,0 +1,105 @@ +// 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.uv.impl + +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import com.jetbrains.python.packaging.common.PythonOutdatedPackage +import com.jetbrains.python.packaging.common.PythonPackage +import com.jetbrains.python.packaging.common.PythonPackageSpecification +import com.jetbrains.python.sdk.VirtualEnvReader +import com.jetbrains.python.sdk.uv.UvCli +import com.jetbrains.python.sdk.uv.UvLowLevel +import java.nio.file.Path +import kotlin.io.path.pathString + +internal class UvLowLevelImpl(val cwd: Path, val uvCli: UvCli) : UvLowLevel { + override suspend fun initializeEnvironment(init: Boolean, python: Path?): Result { + if (init) { + uvCli.runUv(cwd, "init").getOrElse { + return Result.failure(it) + } + } + + var args = mutableListOf("venv"); + if (python != null) { + args.add("--python") + args.add(python.pathString) + } + + uvCli.runUv(cwd, *args.toTypedArray()).getOrElse { + return Result.failure(it) + } + + val path = VirtualEnvReader.Instance.findPythonInPythonRoot(cwd.resolve(VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME)) + if (path == null) { + return Result.failure(RuntimeException("failed to initialize uv environment")) + } + + return Result.success(path) + } + + override suspend fun listPackages(): Result> { + val out = uvCli.runUv(cwd, "pip", "list", "--format", "json").getOrElse { + return Result.failure(it) + } + + data class PackageInfo(val name: String, val version: String) + + val mapper = jacksonObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + val packages = mapper.readValue>(out).map { + PythonPackage(it.name, it.version, true) + } + + return Result.success(packages) + } + + override suspend fun listOutdatedPackages(): Result> { + + val out = uvCli.runUv(cwd, "pip", "list", "--outdated", "--format", "json").getOrElse { + return Result.failure(it) + } + + data class OutdatedPackageInfo(val name: String, val version: String, val latest_version: String) + + try { + val mapper = jacksonObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + val packages = mapper.readValue>(out).map { + PythonOutdatedPackage(it.name, it.version, true, it.latest_version) + } + + return Result.success(packages) + } + catch (e: Exception) { + return Result.failure(e) + } + } + + override suspend fun installPackage(spec: PythonPackageSpecification, options: List): Result { + val version = if (spec.versionSpecs.isNullOrBlank()) spec.name else "${spec.name}${spec.versionSpecs}" + uvCli.runUv(cwd, "add", version, *options.toTypedArray()).getOrElse { + return Result.failure(it) + } + + return Result.success(Unit) + } + + override suspend fun uninstallPackage(name: PythonPackage): Result { + // TODO: check if package is in dependencies + val result = uvCli.runUv(cwd, "remove", name.name) + if (result.isFailure) { + // try just to uninstall + uvCli.runUv(cwd, "pip", "uninstall", name.name).onFailure { + return Result.failure(it) + } + } + + return Result.success(Unit) + } +} + +fun createUvLowLevel(cwd: Path, uvCli: UvCli = createUvCli()): UvLowLevel { + return UvLowLevelImpl(cwd, uvCli) +} \ 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 new file mode 100644 index 000000000000..48fba4937f04 --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/uv/ui/AddNewUvPanel.kt @@ -0,0 +1,216 @@ +// 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.uv.ui + +import com.intellij.application.options.ModuleListCellRenderer +import com.intellij.ide.util.PropertiesComponent +import com.intellij.openapi.components.service +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleUtil +import com.intellij.openapi.progress.runBlockingCancellable +import com.intellij.openapi.project.Project +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.TextFieldWithBrowseButton +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.openapi.util.UserDataHolder +import com.intellij.openapi.vfs.StandardFileSystems +import com.intellij.ui.DocumentAdapter +import com.intellij.ui.components.JBCheckBox +import com.intellij.ui.components.JBTextField +import com.intellij.util.PlatformUtils +import com.intellij.util.text.nullize +import com.intellij.util.ui.FormBuilder +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.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.basePath +import com.jetbrains.python.sdk.uv.* +import com.jetbrains.python.sdk.uv.impl.detectUvExecutable +import com.jetbrains.python.statistics.InterpreterTarget +import com.jetbrains.python.statistics.InterpreterType +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.awt.BorderLayout +import java.awt.Dimension +import java.awt.event.ItemEvent +import java.nio.file.Path +import javax.swing.Icon +import javax.swing.JComboBox +import javax.swing.event.DocumentEvent +import kotlin.io.path.absolutePathString +import kotlin.io.path.pathString + +// TODO: remove old UI support +// FIXME: code duplication w poetry +internal fun allModules(project: Project?): List { + return project?.let { + ModuleUtil.getModulesOfType(it, PythonModuleTypeBase.getInstance()) + }?.sortedBy { it.name } ?: emptyList() +} + +/** + * The UI panel for adding the uv interpreter for the project. + * + */ +class PyAddNewUvPanel( + private val project: Project?, + private val module: Module?, + private val existingSdks: List, + override var newProjectPath: String?, + context: UserDataHolder, +) : PyAddNewEnvPanel() { + override val envName = "Uv" + override val panelName: String get() = PyBundle.message("python.sdk.uv.environment.panel.title") + + override val icon: Icon = UV_ICON + + private val moduleField: JComboBox + private val baseSdkField = PySdkPathChoosingComboBox() + + init { + addInterpretersAsync(baseSdkField) { + validateSdks(module, existingSdks, context) + } + } + + private val installPackagesCheckBox = JBCheckBox(PyBundle.message("python.sdk.uv.install.packages.from.toml.checkbox.text")).apply { + service().cs.launch { + isVisible = projectPath?.let { + withContext(Dispatchers.IO) { + StandardFileSystems.local().findFileByPath(it)?.findChild(PY_PROJECT_TOML)?.let { file -> getPyProjectTomlForUv(file) } + } + } != null + isSelected = isVisible + } + } + + private val uvPathField = TextFieldWithBrowseButton().apply { + addBrowseFolderListener(null, FileChooserDescriptorFactory.createSingleFileDescriptor()) + val field = textField as? JBTextField ?: return@apply + service().cs.launch { + detectUvExecutable()?.let { field.emptyText.text = "Auto-detected: ${it.absolutePathString()}" } + PropertiesComponent.getInstance().uvPath?.let { + field.text = it.pathString + } + } + } + + init { + layout = BorderLayout() + + val modules = allModules(project) + + moduleField = ComboBox(modules.toTypedArray()).apply { + renderer = ModuleListCellRenderer() + preferredSize = Dimension(Int.MAX_VALUE, preferredSize.height) + addItemListener { + if (it.stateChange == ItemEvent.SELECTED) { + update() + } + } + } + + uvPathField.textField.document.addDocumentListener(object : DocumentAdapter() { + override fun textChanged(e: DocumentEvent) { + update() + } + }) + + val builder = FormBuilder.createFormBuilder().apply { + if (module == null && modules.size > 1) { + val associatedObjectLabel = if (PlatformUtils.isPyCharm()) { + PyBundle.message("python.sdk.uv.associated.module") + } + else { + PyBundle.message("python.sdk.uv.associated.project") + } + addLabeledComponent(associatedObjectLabel, moduleField) + } + addLabeledComponent(PySdkBundle.message("python.venv.base.label"), baseSdkField) + addComponent(installPackagesCheckBox) + addLabeledComponent(PyBundle.message("python.sdk.uv.executable"), uvPathField) + } + + add(builder.panel, BorderLayout.NORTH) + update() + } + + override fun getOrCreateSdk(): Sdk? { + val module = selectedModule + val path = newProjectPath + val python = baseSdkField.selectedSdk.homePath + + if (module == null || path == null || python == null) { + return null + } + + val uvPath = uvPathField.text.nullize()?.let { Path.of(it) } + uvPath?.let { PropertiesComponent.getInstance().uvPath = it } + val sdk = runBlockingCancellable { + setupUvSdkUnderProgress(module, Path.of(path), existingSdks, Path.of(python)) + } + + sdk.onSuccess { + PySdkSettings.instance.preferredVirtualEnvBaseSdk = baseSdkField.selectedSdk.homePath + } + + return sdk.getOrNull() + } + + override fun getStatisticInfo(): InterpreterStatisticsInfo { + return InterpreterStatisticsInfo(type = InterpreterType.UV, + target = InterpreterTarget.LOCAL, + globalSitePackage = false, + makeAvailableToAllProjects = false, + previouslyConfigured = false) + } + + override fun validateAll(): List = + emptyList() // Pre-target validation is not supported + + override fun addChangeListener(listener: Runnable) { + uvPathField.textField.document.addDocumentListener(object : DocumentAdapter() { + override fun textChanged(e: DocumentEvent) { + listener.run() + } + }) + super.addChangeListener(listener) + } + + /** + * Updates the view according to the current state of UI controls. + */ + private fun update() { + service().cs.launch { + selectedModule?.let { + installPackagesCheckBox.isEnabled = pyProjectToml(it) != null + } + } + } + + /** + * The effective module for which we add a new environment. + */ + private val selectedModule: Module? + get() = module ?: try { + moduleField.selectedItem + } + catch (e: NullPointerException) { + null + } as? Module + + + /** + * The effective project path for the new project or for the existing project. + */ + private val projectPath: String? + get() = newProjectPath ?: selectedModule?.basePath ?: project?.basePath +} diff --git a/python/src/com/jetbrains/python/statistics/PyStatisticTools.kt b/python/src/com/jetbrains/python/statistics/PyStatisticTools.kt index 262836bcfc5f..712614e9be35 100644 --- a/python/src/com/jetbrains/python/statistics/PyStatisticTools.kt +++ b/python/src/com/jetbrains/python/statistics/PyStatisticTools.kt @@ -18,7 +18,6 @@ import com.jetbrains.python.psi.LanguageLevel import com.jetbrains.python.remote.PyRemoteSdkAdditionalDataBase import com.jetbrains.python.sdk.PySdkUtil import com.jetbrains.python.sdk.PythonSdkAdditionalData -import com.jetbrains.python.sdk.PythonSdkType import com.jetbrains.python.sdk.PythonSdkUtil import com.jetbrains.python.sdk.flavors.PythonSdkFlavor import com.jetbrains.python.sdk.VirtualEnvReader @@ -125,6 +124,7 @@ enum class InterpreterType(val value: String) { REGULAR("regular"), POETRY("poetry"), PYENV("pyenv"), + UV("uv"), } enum class InterpreterCreationMode(val value: String) {