mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-05 01:50:56 +07:00
basic support for uv env & package manager; PY-75983
GitOrigin-RevId: 2597e4de17e167d8a0b0038190b5127a9dc4b155
This commit is contained in:
committed by
intellij-monorepo-bot
parent
6de95881e2
commit
9b76b13e69
@@ -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>
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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>
|
||||
@@ -12,6 +12,10 @@
|
||||
"id": "PoetryPackageVersionsInspection",
|
||||
"codeQualityCategory": "Sanity"
|
||||
},
|
||||
{
|
||||
"id": "UvPackageVersionsInspection",
|
||||
"codeQualityCategory": "Sanity"
|
||||
},
|
||||
{
|
||||
"id": "PyStubPackagesCompatibilityInspection",
|
||||
"codeQualityCategory": "Sanity"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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")) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
21
python/src/com/jetbrains/python/sdk/uv/Uv.kt
Normal file
21
python/src/com/jetbrains/python/sdk/uv/Uv.kt
Normal 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>
|
||||
}
|
||||
87
python/src/com/jetbrains/python/sdk/uv/UvExt.kt
Normal file
87
python/src/com/jetbrains/python/sdk/uv/UvExt.kt
Normal 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
|
||||
}
|
||||
80
python/src/com/jetbrains/python/sdk/uv/UvInspections.kt
Normal file
80
python/src/com/jetbrains/python/sdk/uv/UvInspections.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
67
python/src/com/jetbrains/python/sdk/uv/UvPackageManager.kt
Normal file
67
python/src/com/jetbrains/python/sdk/uv/UvPackageManager.kt
Normal 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
|
||||
}
|
||||
}
|
||||
64
python/src/com/jetbrains/python/sdk/uv/UvQuickFixes.kt
Normal file
64
python/src/com/jetbrains/python/sdk/uv/UvQuickFixes.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
59
python/src/com/jetbrains/python/sdk/uv/UvSdkFlavorAndData.kt
Normal file
59
python/src/com/jetbrains/python/sdk/uv/UvSdkFlavorAndData.kt
Normal 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
|
||||
}
|
||||
}
|
||||
72
python/src/com/jetbrains/python/sdk/uv/UvSdkProvider.kt
Normal file
72
python/src/com/jetbrains/python/sdk/uv/UvSdkProvider.kt
Normal 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 }
|
||||
}
|
||||
25
python/src/com/jetbrains/python/sdk/uv/UvUtils.kt
Normal file
25
python/src/com/jetbrains/python/sdk/uv/UvUtils.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
63
python/src/com/jetbrains/python/sdk/uv/impl/UvCli.kt
Normal file
63
python/src/com/jetbrains/python/sdk/uv/impl/UvCli.kt
Normal 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)
|
||||
}
|
||||
105
python/src/com/jetbrains/python/sdk/uv/impl/UvLowLevel.kt
Normal file
105
python/src/com/jetbrains/python/sdk/uv/impl/UvLowLevel.kt
Normal 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)
|
||||
}
|
||||
216
python/src/com/jetbrains/python/sdk/uv/ui/AddNewUvPanel.kt
Normal file
216
python/src/com/jetbrains/python/sdk/uv/ui/AddNewUvPanel.kt
Normal 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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user