basic support for uv env & package manager; PY-75983

GitOrigin-RevId: 2597e4de17e167d8a0b0038190b5127a9dc4b155
This commit is contained in:
Aleksandr Sorotskii
2024-11-08 23:16:21 +01:00
committed by intellij-monorepo-bot
parent 6de95881e2
commit 9b76b13e69
26 changed files with 976 additions and 46 deletions

View File

@@ -157,5 +157,6 @@
<orderEntry type="library" name="kotlinx-datetime-jvm" level="project" />
<orderEntry type="module" module-name="intellij.platform.ide.remote" />
<orderEntry type="module" module-name="intellij.platform.ide.ui" />
<orderEntry type="library" name="jackson-module-kotlin" level="project" />
</component>
</module>

View File

@@ -59,6 +59,7 @@ The Python plug-in provides smart editing for Python scripts. The feature set of
<extensions defaultExtensionNs="com.intellij">
<localInspection language="TOML" enabledByDefault="true" implementationClass="com.jetbrains.python.sdk.poetry.PoetryPackageVersionsInspection" key="INSP.poetry.package.versions.display.name" bundle="messages.PyBundle" groupKey="INSP.GROUP.python" suppressId="PoetryPackageVersions" shortName="PoetryPackageVersionsInspection"/>
<localInspection language="TOML" enabledByDefault="true" implementationClass="com.jetbrains.python.sdk.uv.UvPackageVersionsInspection" key="INSP.poetry.package.versions.display.name" bundle="messages.PyBundle" groupKey="INSP.GROUP.python" suppressId="UvPackageVersions" shortName="UvPackageVersionsInspection"/>
<fileType name="Requirements.txt"
implementationClass="com.jetbrains.python.requirements.RequirementsFileType"
@@ -830,6 +831,8 @@ The Python plug-in provides smart editing for Python scripts. The feature set of
<pyAddSdkProvider implementation="com.jetbrains.python.sdk.poetry.PyAddPoetrySdkProvider"/>
<pythonFlavorProvider implementation="com.jetbrains.python.sdk.poetry.PyPoetrySdkFlavorProvider"/>
<pythonFlavorProvider implementation="com.jetbrains.python.sdk.uv.UvSdkFlavorProvider"/>
<!-- SDK Flavors -->
<pythonSdkFlavor implementation="com.jetbrains.python.sdk.flavors.conda.CondaEnvSdkFlavor"/>
<pythonSdkFlavor implementation="com.jetbrains.python.sdk.flavors.JythonSdkFlavor"/>
@@ -858,6 +861,8 @@ The Python plug-in provides smart editing for Python scripts. The feature set of
<packageManagerProvider implementation="com.jetbrains.python.sdk.poetry.PyPoetryPackageManagerProvider"/>
<pythonPackageManagerProvider implementation="com.jetbrains.python.sdk.poetry.PoetryPackageManagerProvider"/>
<pySdkProvider implementation="com.jetbrains.python.sdk.uv.UvSdkProvider"/>
<pythonPackageManagerProvider implementation="com.jetbrains.python.sdk.uv.UvPackageManagerProvider"/>
<pythonPackageManagerProvider implementation="com.jetbrains.python.packaging.pip.PipPackageManagerProvider" order="last"/>
<pythonPackageManagerProvider implementation="com.jetbrains.python.packaging.conda.CondaPackageManagerProvider"/>

View File

@@ -0,0 +1,7 @@
<html>
<body>
<p>Reports outdated versions of packages in <code>[dependencies]</code> and <code>[dev-dependencies]</code>
sections of <code>pyproject.toml</code>.
</p>
</body>
</html>

View File

@@ -12,6 +12,10 @@
"id": "PoetryPackageVersionsInspection",
"codeQualityCategory": "Sanity"
},
{
"id": "UvPackageVersionsInspection",
"codeQualityCategory": "Sanity"
},
{
"id": "PyStubPackagesCompatibilityInspection",
"codeQualityCategory": "Sanity"

View File

@@ -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

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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<String> = model.state.pipenvExecutable
override val installationScript: Path? = null

View File

@@ -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<String> = model.state.poetryExecutable
override val installationScript = PythonHelpersLocator.findPathInHelpers("pycharm_poetry_installer.py")

View File

@@ -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<String> = 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<Sdk>, projectPath: String, homePath: String?, installPackages: Boolean): Result<Sdk> {
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()
}
}

View File

@@ -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)

View File

@@ -27,10 +27,11 @@ class PythonAddCustomInterpreter(val model: PythonMutableTargetAddInterpreterMod
private val existingInterpreterManager = propertyGraph.property(PYTHON)
private val newInterpreterCreators = mapOf(
VIRTUALENV to PythonNewVirtualenvCreator(model),
VIRTUALENV to EnvironmentCreatorVenv(model),
CONDA to CondaNewEnvironmentCreator(model, errorSink),
PIPENV to PipEnvNewEnvironmentCreator(model),
POETRY to PoetryNewEnvironmentCreator(model, moduleOrProject),
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")) {

View File

@@ -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)
}

View File

@@ -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,36 +167,34 @@ 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 {
// 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)
}
else {
com.jetbrains.python.sdk.pipenv.detectPipEnvExecutable().getOrNull()?.let {
// FIXME: support targets
getPipEnvExecutable().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<PythonSelectableInterpreter?> = propertyGraph.property(null)
val newCondaEnvName: ObservableMutableProperty<String> = propertyGraph.property("")
val poetryExecutable: ObservableMutableProperty<String> = propertyGraph.property("")
val uvExecutable: ObservableMutableProperty<String> = propertyGraph.property("")
val pipenvExecutable: ObservableMutableProperty<String> = propertyGraph.property("")
val venvPath: ObservableMutableProperty<String> = propertyGraph.property("")
val inheritSitePackages = propertyGraph.property(false)

View File

@@ -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<String>
}
interface UvLowLevel {
suspend fun initializeEnvironment(init: Boolean, python: Path?): Result<Path>
suspend fun listPackages(): Result<List<PythonPackage>>
suspend fun listOutdatedPackages(): Result<List<PythonOutdatedPackage>>
suspend fun installPackage(name: PythonPackageSpecification, options: List<String>): Result<Unit>
suspend fun uninstallPackage(name: PythonPackage): Result<Unit>
}

View File

@@ -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<Sdk>,
python: Path?
): Result<Sdk> {
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
}

View File

@@ -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)
}
}
}
}
}

View File

@@ -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<PythonPackage> = emptyList()
override val repositoryManager: PythonRepositoryManager = PipRepositoryManager(project, sdk)
private val uv: UvLowLevel = createUvLowLevel(Path.of(project.basePath!!), createUvCli())
@Volatile
var outdatedPackages: Map<String, PythonOutdatedPackage> = emptyMap()
override suspend fun installPackageCommand(specification: PythonPackageSpecification, options: List<String>): Result<String> {
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<String> {
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<String> {
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<List<PythonPackage>> {
// 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
}
}

View File

@@ -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)
}
}

View File

@@ -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<PyFlavorData.Empty>() {
override fun getIcon() = UV_ICON
override fun getFlavorDataClass(): Class<PyFlavorData.Empty> = 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
}
}

View File

@@ -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 <koxudaxi@gmail.com>
*/
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<Sdk>,
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<Sdk>, context: UserDataHolder): List<Sdk> {
val sdks = findBaseSdks(existingSdks, module, context).takeIf { it.isNotEmpty() }
?: detectSystemWideSdks(module, existingSdks, context)
return sdks.filter { it.sdkSeemsValid && !it.isUv }
}

View File

@@ -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
}
}
}

View File

@@ -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<String> {
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<String> {
with(Dispatchers.IO) {
return runUv(uv, workingDir, *args)
}
}
}
fun createUvCli(dispatcher: CoroutineDispatcher = Dispatchers.IO, uv: Path? = null): UvCli {
return UvCliImpl(dispatcher, uv)
}

View File

@@ -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<Path> {
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<List<PythonPackage>> {
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<List<PackageInfo>>(out).map {
PythonPackage(it.name, it.version, true)
}
return Result.success(packages)
}
override suspend fun listOutdatedPackages(): Result<List<PythonOutdatedPackage>> {
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<List<OutdatedPackageInfo>>(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<String>): Result<Unit> {
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<Unit> {
// 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)
}

View File

@@ -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<Module> {
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<Sdk>,
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<Module>
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<PythonSdkCoroutineService>().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<PythonSdkCoroutineService>().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<ValidationInfo> =
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<PythonSdkCoroutineService>().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
}

View File

@@ -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) {