mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-03-22 06:50:54 +07:00
[python] implement python package manager actions (PY-79451)
* PythonPackageManagerJobService.kt added to manage tool jobs * Base PythonPackageManagerAction.kt was added to cover all python package manager actions * Implementations for Poetry / Hatch / uv * Poetry pyproject.toml watcher was removed (replaced with poetry actions) (cherry picked from commit 0bbc5a7802826674140ca1c80be27b6cd7d0f59e) GitOrigin-RevId: d3b6486ca9a24ecd7188e8c5308fb38aae5ed318
This commit is contained in:
committed by
intellij-monorepo-bot
parent
ae542388a8
commit
c5ca662b4b
@@ -572,9 +572,6 @@ The Python plug-in provides smart editing for Python scripts. The feature set of
|
||||
<suggestedRefactoringSupport language="Python"
|
||||
implementationClass="com.jetbrains.python.refactoring.suggested.PySuggestedRefactoringSupport"/>
|
||||
|
||||
<!-- Poetry -->
|
||||
<editorFactoryListener implementation="com.jetbrains.python.sdk.poetry.PoetryPyProjectTomlWatcher"/>
|
||||
|
||||
<!-- Targets API -->
|
||||
<registryKey key="enable.conda.on.targets" defaultValue="false" description="Enables Conda configuration on targets."/>
|
||||
<registryKey key="python.packaging.tool.use.project.location.as.working.dir" defaultValue="false"
|
||||
@@ -1048,6 +1045,40 @@ The Python plug-in provides smart editing for Python scripts. The feature set of
|
||||
|
||||
<add-to-group group-id="Internal"/>
|
||||
</group>
|
||||
|
||||
<group id="PythonPackageManagerActions" searchable="false">
|
||||
<separator/>
|
||||
|
||||
<action id="UvLockAction"
|
||||
class="com.jetbrains.python.uv.packaging.UvLockAction"
|
||||
icon="com.intellij.icons.AllIcons.Diff.Lock"/>
|
||||
|
||||
<action id="UvSyncAction"
|
||||
class="com.jetbrains.python.uv.packaging.UvSyncAction"
|
||||
icon="com.intellij.icons.AllIcons.Actions.Refresh"/>
|
||||
|
||||
|
||||
<separator/>
|
||||
|
||||
<action id="PoetryLockAction"
|
||||
class="com.jetbrains.python.poetry.packaging.PoetryLockAction"
|
||||
icon="com.intellij.icons.AllIcons.Diff.Lock"/>
|
||||
|
||||
<action id="PoetryUpdateAction"
|
||||
class="com.jetbrains.python.poetry.packaging.PoetryUpdateAction"
|
||||
icon="com.intellij.icons.AllIcons.Actions.Refresh"/>
|
||||
|
||||
<separator/>
|
||||
|
||||
<action id="HatchRunAction"
|
||||
class="com.jetbrains.python.hatch.packaging.HatchRunAction"
|
||||
icon="com.intellij.icons.AllIcons.Actions.Refresh"/>
|
||||
|
||||
<separator/>
|
||||
|
||||
<add-to-group group-id="EditorContextBarMenu" anchor="last"/>
|
||||
<!--<add-to-group group-id="EditorPopupMenu" relative-to-action="ShowIntentionsGroup" anchor="after"/>-->
|
||||
</group>
|
||||
</actions>
|
||||
|
||||
<extensions defaultExtensionNs="com.intellij.spellchecker">
|
||||
|
||||
@@ -378,9 +378,6 @@ python.sdk.poetry.pip.file.lock.not.found=poetry.lock is not found
|
||||
python.sdk.poetry.pip.file.lock.out.of.date=poetry.lock is out of date
|
||||
python.sdk.poetry.pip.file.notification.content=Run <a href='#lock'>poetry lock</a> or <a href='#update'>poetry update</a>
|
||||
python.sdk.poetry.pip.file.notification.content.without.updating=Run <a href='#lock'>poetry lock</a>, <a href='#noupdate'>poetry lock --no-update</a> or <a href='#update'>poetry update</a>
|
||||
python.sdk.poetry.pip.file.notification.locking=Locking poetry.lock
|
||||
python.sdk.poetry.pip.file.notification.locking.without.updating=Locking poetry.lock without updating
|
||||
python.sdk.poetry.pip.file.notification.updating=Updating Poetry environment
|
||||
python.sdk.poetry.pip.file.watcher=pyproject.toml Watcher
|
||||
python.sdk.dialog.title.setting.up.poetry.environment=Setting up poetry environment
|
||||
python.sdk.intention.family.name.install.requirements.from.poetry.lock=Install requirements from poetry.lock
|
||||
@@ -1600,6 +1597,12 @@ python.toolwindow.packages.collapse.all.action=Collapse All
|
||||
django.template.language=Template Language
|
||||
python.error=Error
|
||||
|
||||
action.UvSyncAction.text=Uv Sync
|
||||
action.UvLockAction.text=Uv Lock
|
||||
action.PoetryUpdateAction.text=Poetry Update
|
||||
action.PoetryLockAction.text=Poetry Lock
|
||||
action.HatchRunAction.text=Hatch Run (Sync Dependencies)
|
||||
|
||||
|
||||
python.survey.user.job.notification.group=PyCharm Job Survey
|
||||
python.survey.user.job.notification.title=Feedback In IDE
|
||||
|
||||
@@ -146,19 +146,16 @@ class HatchCli(private val runtime: HatchRuntime) {
|
||||
/**
|
||||
* Run commands within project environments
|
||||
*/
|
||||
suspend fun run(envName: String, vararg command: String): Result<String, ExecException> {
|
||||
val envRuntime = runtime.withEnv(HatchConstants.AppEnvVars.ENV to envName)
|
||||
suspend fun run(envName: String? = null, vararg command: String): Result<String, ExecException> {
|
||||
val envRuntime = envName?.let { runtime.withEnv(HatchConstants.AppEnvVars.ENV to it) } ?: runtime
|
||||
return envRuntime.executeAndHandleErrors("run", *command) { output ->
|
||||
if (output.exitCode != 0) return@executeAndHandleErrors Result.failure(null)
|
||||
|
||||
val scenario = output.stderr.trim()
|
||||
val content = when {
|
||||
output.exitCode == 0 -> {
|
||||
val installDetailsContent = output.stdout.replace("─", "").trim()
|
||||
val info = installDetailsContent.lines().drop(1).dropLast(2).joinToString("\n")
|
||||
"$scenario\n$info"
|
||||
}
|
||||
else -> scenario
|
||||
}
|
||||
Result.success(content)
|
||||
val installDetailsContent = output.stdout.replace("─", "").trim()
|
||||
val info = installDetailsContent.lines().drop(1).dropLast(2).joinToString("\n")
|
||||
|
||||
Result.success("$scenario\n$info")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ import com.jetbrains.python.errorProcessing.PyError
|
||||
import com.jetbrains.python.sdk.basePath
|
||||
import java.nio.file.Path
|
||||
|
||||
const val HATCH_TOML: String = "hatch.toml"
|
||||
|
||||
sealed class HatchError(message: @NlsSafe String) : PyError.Message(message)
|
||||
|
||||
class HatchExecutableNotFoundHatchError(path: Path?) : HatchError(
|
||||
@@ -79,7 +81,7 @@ data class ProjectStructure(
|
||||
interface HatchService {
|
||||
fun getWorkingDirectoryPath(): Path
|
||||
|
||||
suspend fun syncDependencies(envName: String): Result<String, PyError>
|
||||
suspend fun syncDependencies(envName: String? = null): Result<String, PyError>
|
||||
|
||||
suspend fun isHatchManagedProject(): Result<Boolean, PyError>
|
||||
|
||||
@@ -97,8 +99,8 @@ interface HatchService {
|
||||
/**
|
||||
* Hatch Service for working directory (where hatch.toml / pyproject.toml is usually placed)
|
||||
*/
|
||||
suspend fun Path.getHatchService(hatchExecutablePath: Path? = null): Result<HatchService, PyError> {
|
||||
return CliBasedHatchService(hatchExecutablePath = hatchExecutablePath, workingDirectoryPath = this)
|
||||
suspend fun Path?.getHatchService(hatchExecutablePath: Path? = null, hatchEnvironmentName: String? = null): Result<HatchService, PyError> {
|
||||
return CliBasedHatchService(hatchExecutablePath = hatchExecutablePath, workingDirectoryPath = this, hatchEnvironmentName = hatchEnvironmentName)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.intellij.python.hatch.*
|
||||
import com.intellij.python.hatch.cli.ENV_TYPE_VIRTUAL
|
||||
import com.intellij.python.hatch.cli.HatchEnvironment
|
||||
import com.intellij.python.hatch.cli.HatchEnvironments
|
||||
import com.intellij.python.hatch.runtime.HatchConstants
|
||||
import com.intellij.python.hatch.runtime.HatchRuntime
|
||||
import com.intellij.python.hatch.runtime.createHatchRuntime
|
||||
import com.jetbrains.python.PythonBinary
|
||||
@@ -29,12 +30,14 @@ internal class CliBasedHatchService private constructor(
|
||||
private val hatchRuntime: HatchRuntime,
|
||||
) : HatchService {
|
||||
companion object {
|
||||
suspend operator fun invoke(workingDirectoryPath: Path, hatchExecutablePath: Path?): Result<CliBasedHatchService, PyError> {
|
||||
suspend operator fun invoke(workingDirectoryPath: Path?, hatchExecutablePath: Path? = null, hatchEnvironmentName: String? = null): Result<CliBasedHatchService, PyError> {
|
||||
val envVars = hatchEnvironmentName?.let { mapOf(HatchConstants.AppEnvVars.ENV to it) } ?: emptyMap()
|
||||
val hatchRuntime = createHatchRuntime(
|
||||
hatchExecutablePath = hatchExecutablePath,
|
||||
workingDirectoryPath = workingDirectoryPath,
|
||||
envVars = envVars
|
||||
).getOr { return it }
|
||||
return Result.success(CliBasedHatchService(workingDirectoryPath, hatchRuntime))
|
||||
return Result.success(CliBasedHatchService(workingDirectoryPath!!, hatchRuntime))
|
||||
}
|
||||
|
||||
private val concurrencyLimit = Semaphore(permits = 5)
|
||||
@@ -50,7 +53,7 @@ internal class CliBasedHatchService private constructor(
|
||||
|
||||
override fun getWorkingDirectoryPath(): Path = workingDirectoryPath
|
||||
|
||||
override suspend fun syncDependencies(envName: String): Result<String, PyError> {
|
||||
override suspend fun syncDependencies(envName: String?): Result<String, PyError> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
hatchRuntime.hatchCli().run(envName, "python", "--version")
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ fun showProcessExecutionErrorDialog(
|
||||
override fun createActions(): Array<Action> = arrayOf(okAction)
|
||||
|
||||
override fun createCenterPanel(): JComponent = formBuilder.panel.apply {
|
||||
preferredSize = Dimension(600, 300)
|
||||
preferredSize = Dimension(820, 400)
|
||||
}
|
||||
}.showAndGet()
|
||||
}
|
||||
|
||||
@@ -3,11 +3,15 @@ package com.jetbrains.python.hatch.packaging
|
||||
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.python.hatch.HatchService
|
||||
import com.intellij.python.hatch.getHatchService
|
||||
import com.jetbrains.python.errorProcessing.PyError
|
||||
import com.jetbrains.python.hatch.sdk.HatchSdkAdditionalData
|
||||
import com.jetbrains.python.hatch.sdk.isHatch
|
||||
import com.jetbrains.python.packaging.management.PythonPackageManager
|
||||
import com.jetbrains.python.packaging.management.PythonPackageManagerProvider
|
||||
import com.jetbrains.python.packaging.pip.PipPythonPackageManager
|
||||
import com.jetbrains.python.Result
|
||||
|
||||
internal class HatchPackageManager(project: Project, sdk: Sdk) : PipPythonPackageManager(project, sdk) {
|
||||
fun getSdkAdditionalData(): HatchSdkAdditionalData {
|
||||
@@ -16,6 +20,12 @@ internal class HatchPackageManager(project: Project, sdk: Sdk) : PipPythonPackag
|
||||
"additional data has to be ${HatchSdkAdditionalData::class.java.name}, " +
|
||||
"but was ${sdk.sdkAdditionalData?.javaClass?.name}")
|
||||
}
|
||||
|
||||
suspend fun getHatchService(): Result<HatchService, PyError> {
|
||||
val data = getSdkAdditionalData()
|
||||
val workingDirectory = data.hatchWorkingDirectory
|
||||
return workingDirectory.getHatchService(hatchEnvironmentName = data.hatchEnvironmentName)
|
||||
}
|
||||
}
|
||||
|
||||
internal class HatchPackageManagerProvider : PythonPackageManagerProvider {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.hatch.packaging
|
||||
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.python.hatch.HATCH_TOML
|
||||
import com.intellij.python.pyproject.PY_PROJECT_TOML
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.errorProcessing.PyError
|
||||
import com.jetbrains.python.packaging.management.PythonPackageManagerAction
|
||||
import com.jetbrains.python.packaging.management.getPythonPackageManager
|
||||
import kotlin.text.Regex.Companion.escape
|
||||
|
||||
internal sealed class HatchPackageManagerAction : PythonPackageManagerAction<HatchPackageManager, String>() {
|
||||
override val fileNamesPattern: Regex = """^(${escape(HATCH_TOML)}|${escape(PY_PROJECT_TOML)})$""".toRegex()
|
||||
|
||||
override fun getManager(e: AnActionEvent): HatchPackageManager? = e.getPythonPackageManager()
|
||||
}
|
||||
|
||||
internal class HatchRunAction() : HatchPackageManagerAction() {
|
||||
override suspend fun execute(e: AnActionEvent, manager: HatchPackageManager): Result<String, PyError> {
|
||||
val service = manager.getHatchService().getOr { return it }
|
||||
return service.syncDependencies()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.packaging.management
|
||||
|
||||
import com.intellij.ide.ActivityTracker
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.Key
|
||||
import com.intellij.openapi.util.NlsContexts.ProgressTitle
|
||||
import com.intellij.openapi.util.UserDataHolder
|
||||
import com.intellij.openapi.util.getOrCreateUserDataUnsafe
|
||||
import com.intellij.platform.ide.progress.withBackgroundProgress
|
||||
import com.jetbrains.python.errorProcessing.PyError
|
||||
import com.jetbrains.python.Result
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
private val MUTEX_KEY = Key.create<Mutex>("${CancellableJobSerialRunner::class.java.name}.mutex")
|
||||
|
||||
/**
|
||||
* This service allows asynchronous and potentially long-running jobs tied to a specific [UserDataHolder] instance
|
||||
* to be executed safely, ensuring that only one job for a given object is active at a time.
|
||||
*
|
||||
* The service is thread-safe.
|
||||
*
|
||||
* It also provides activity tracking signals to ensure proper UI updates.
|
||||
*/
|
||||
internal object CancellableJobSerialRunner {
|
||||
|
||||
private fun getMutex(holder: UserDataHolder): Mutex {
|
||||
return synchronized(holder) {
|
||||
holder.getOrCreateUserDataUnsafe(MUTEX_KEY) { Mutex() }
|
||||
}
|
||||
}
|
||||
|
||||
fun isRunLocked(holder: UserDataHolder): Boolean = getMutex(holder).isLocked
|
||||
|
||||
/**
|
||||
* Runs a [runnable] job synchronized on a given [UserDataHolder] instance.
|
||||
* The method ensures that only one runnable per holder can be active at a time.
|
||||
* It always creates cancellable background progress for each job so the job queue might be managed from the UI.
|
||||
* If an active job already exists for the given holder, the coroutine suspends until it can acquire the holder's mutex.
|
||||
* Upon completion of the job, it triggers activity tracking to refresh UI components visibility.
|
||||
*
|
||||
* @param [runnable] a suspendable function that represents the job to be performed,
|
||||
* producing a [PyResult] with a success type [V] or a failure type [PyError].
|
||||
*/
|
||||
suspend fun <V> run(
|
||||
project: Project,
|
||||
holder: UserDataHolder,
|
||||
title: @ProgressTitle String,
|
||||
runnable: suspend () -> Result<V, PyError>,
|
||||
): Result<V, PyError> {
|
||||
|
||||
val mutex = getMutex(holder)
|
||||
|
||||
return withBackgroundProgress(project, title, cancellable = true) {
|
||||
mutex.withLock {
|
||||
runnable.invoke()
|
||||
}.also {
|
||||
ActivityTracker.getInstance().inc() // it forces the next update cycle to give all waiting/currently disabled actions a callback
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.packaging.management
|
||||
|
||||
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
|
||||
import com.intellij.openapi.actionSystem.ActionUpdateThread
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.actionSystem.CommonDataKeys
|
||||
import com.intellij.openapi.application.readAction
|
||||
import com.intellij.openapi.application.runInEdt
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.editor.Document
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManager
|
||||
import com.intellij.openapi.module.ModuleUtil
|
||||
import com.intellij.openapi.project.DumbAwareAction
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.NlsContexts.ProgressTitle
|
||||
import com.intellij.openapi.vfs.findPsiFile
|
||||
import com.intellij.platform.util.progress.reportSequentialProgress
|
||||
import com.intellij.python.pyproject.PY_PROJECT_TOML
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.errorProcessing.ErrorSink
|
||||
import com.jetbrains.python.errorProcessing.PyError
|
||||
import com.jetbrains.python.onFailure
|
||||
import com.jetbrains.python.onSuccess
|
||||
import com.jetbrains.python.packaging.PyPackageManager
|
||||
import com.jetbrains.python.sdk.PythonSdkCoroutineService
|
||||
import com.jetbrains.python.sdk.PythonSdkUtil
|
||||
import com.jetbrains.python.sdk.associatedModuleDir
|
||||
import com.jetbrains.python.sdk.pythonSdk
|
||||
import com.jetbrains.python.util.ShowingMessageErrorSync
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.text.Regex.Companion.escape
|
||||
|
||||
/**
|
||||
* Abstract base class representing an action that interacts with a Python package manager to perform tool-specific operations on sdk.
|
||||
* Such as installing, updating, or uninstalling Python packages, updating lock files, etc.
|
||||
*
|
||||
* @param T The type of the Python package manager this action operates on.
|
||||
* @param V The result type of the background jobs performed by this action.
|
||||
*/
|
||||
abstract class PythonPackageManagerAction<T : PythonPackageManager, V> : DumbAwareAction() {
|
||||
protected val errorSink: ErrorSink = ShowingMessageErrorSync
|
||||
protected val scope: CoroutineScope = service<PythonSdkCoroutineService>().cs
|
||||
protected val context: CoroutineContext = Dispatchers.IO
|
||||
|
||||
/**
|
||||
* The regex pattern that matches the file names that this action is applicable to.
|
||||
*/
|
||||
protected open val fileNamesPattern: Regex = """^${escape(PY_PROJECT_TOML)}$""".toRegex()
|
||||
|
||||
/**
|
||||
* Retrieves the manager instance associated with the given action event, see [AnActionEvent.getPythonPackageManager]
|
||||
*
|
||||
* @return the manager instance of type [T] associated with the action event, or null if there is no [T]-manager associated.
|
||||
*/
|
||||
protected abstract fun getManager(e: AnActionEvent): T?
|
||||
|
||||
/**
|
||||
* Executes the main logic of the action using the provided event and manager.
|
||||
*
|
||||
* @return [Result] which contains the successful result of type [V] or an error of type [PyError] if it fails.
|
||||
*/
|
||||
protected abstract suspend fun execute(e: AnActionEvent, manager: T): Result<V, PyError>
|
||||
|
||||
override fun update(e: AnActionEvent) {
|
||||
val isWatchedFile = e.editor()?.virtualFile?.name?.let { fileNamesPattern.matches(it) } ?: false
|
||||
val manager = if (isWatchedFile) getManager(e) else null
|
||||
|
||||
with(e.presentation) {
|
||||
isVisible = manager != null
|
||||
isEnabled = manager?.isRunLocked() == false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execution success callback, refreshes the environment and re-runs the inspection check.
|
||||
* Might be overridden by subclasses.
|
||||
*/
|
||||
private suspend fun onSuccess(manager: T, document: Document?) {
|
||||
manager.refreshEnvironment()
|
||||
document?.reloadIntentions(manager.project)
|
||||
}
|
||||
|
||||
private suspend fun executeScenarioWithinProgress(manager: T, e: AnActionEvent, document: Document?): Result<V, PyError> {
|
||||
return reportSequentialProgress(2) { reporter ->
|
||||
reporter.itemStep {
|
||||
execute(e, manager)
|
||||
}.onSuccess {
|
||||
reporter.itemStep(PyBundle.message("python.sdk.scanning.installed.packages")) {
|
||||
onSuccess(manager, document)
|
||||
}
|
||||
}.onFailure {
|
||||
errorSink.emit(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This action saves the current document on fs because tools are command line tools, and they need actual files to be up to date
|
||||
* Handles errors via [errorSink]
|
||||
*/
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val manager = getManager(e) ?: return
|
||||
val document = e.editor()?.document
|
||||
|
||||
document?.let {
|
||||
runInEdt {
|
||||
FileDocumentManager.getInstance().saveDocument(document)
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(context) {
|
||||
manager.runSynchronized(e.presentation.text) {
|
||||
executeScenarioWithinProgress(manager, e, document)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
|
||||
}
|
||||
|
||||
private fun AnActionEvent.editor(): Editor? = this.getData(CommonDataKeys.EDITOR)
|
||||
|
||||
private fun Document.virtualFile() = FileDocumentManager.getInstance().getFile(this)
|
||||
|
||||
private fun Editor.getPythonPackageManager(): PythonPackageManager? {
|
||||
val virtualFile = this.document.virtualFile() ?: return null
|
||||
val module = project?.let { ModuleUtil.findModuleForFile(virtualFile, it) } ?: return null
|
||||
val manager = module.pythonSdk?.let { sdk ->
|
||||
PythonPackageManager.forSdk(module.project, sdk)
|
||||
}
|
||||
return manager
|
||||
}
|
||||
|
||||
internal inline fun <reified T : PythonPackageManager> AnActionEvent.getPythonPackageManager(): T? {
|
||||
return editor()?.getPythonPackageManager() as? T
|
||||
}
|
||||
|
||||
/**
|
||||
* 1) Reloads package caches.
|
||||
* 2) [PyPackageManager] is deprecated but its implementations still have their own package caches, so need to refresh them too.
|
||||
* 3) some files likes uv.lock / poetry.lock might be added, so need to refresh module dir too.
|
||||
*/
|
||||
private suspend fun PythonPackageManager.refreshEnvironment() {
|
||||
PythonSdkUtil.getSitePackagesDirectory(sdk)?.refresh(true, true)
|
||||
sdk.associatedModuleDir?.refresh(true, false)
|
||||
PyPackageManager.getInstance(sdk).refreshAndGetPackages(true)
|
||||
reloadPackages()
|
||||
}
|
||||
|
||||
/**
|
||||
* re-runs the inspection check using updated dependencies
|
||||
*/
|
||||
private suspend fun Document.reloadIntentions(project: Project) {
|
||||
readAction {
|
||||
val virtualFile = virtualFile() ?: return@readAction null
|
||||
virtualFile.findPsiFile(project)
|
||||
}?.let { psiFile ->
|
||||
DaemonCodeAnalyzer.getInstance(project).restart(psiFile)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal fun PythonPackageManager.isRunLocked(): Boolean {
|
||||
return CancellableJobSerialRunner.isRunLocked(this.sdk)
|
||||
}
|
||||
|
||||
internal suspend fun <V> PythonPackageManager.runSynchronized(
|
||||
title: @ProgressTitle String,
|
||||
runnable: suspend () -> Result<V, PyError>,
|
||||
): Result<V, PyError> {
|
||||
return CancellableJobSerialRunner.run(this.project, this.sdk, title, runnable)
|
||||
}
|
||||
5
python/src/com/jetbrains/python/poetry/package-info.java
Normal file
5
python/src/com/jetbrains/python/poetry/package-info.java
Normal file
@@ -0,0 +1,5 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
@ApiStatus.Internal
|
||||
package com.jetbrains.python.poetry;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.poetry.packaging
|
||||
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.errorProcessing.PyError
|
||||
import com.jetbrains.python.errorProcessing.asPythonResult
|
||||
import com.jetbrains.python.packaging.management.PythonPackageManagerAction
|
||||
import com.jetbrains.python.packaging.management.getPythonPackageManager
|
||||
import com.jetbrains.python.sdk.poetry.PoetryPackageManager
|
||||
import com.jetbrains.python.sdk.poetry.runPoetryWithSdk
|
||||
|
||||
internal sealed class PoetryPackageManagerAction : PythonPackageManagerAction<PoetryPackageManager, String>() {
|
||||
override fun getManager(e: AnActionEvent): PoetryPackageManager? = e.getPythonPackageManager()
|
||||
}
|
||||
|
||||
internal class PoetryLockAction() : PoetryPackageManagerAction() {
|
||||
override suspend fun execute(e: AnActionEvent, manager: PoetryPackageManager): Result<String, PyError> {
|
||||
return runPoetryWithManager(manager, listOf("lock"))
|
||||
}
|
||||
}
|
||||
|
||||
internal class PoetryUpdateAction() : PoetryPackageManagerAction() {
|
||||
override suspend fun execute(e: AnActionEvent, manager: PoetryPackageManager): Result<String, PyError> {
|
||||
return runPoetryWithManager(manager, listOf("update"))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun runPoetryWithManager(manager: PoetryPackageManager, args: List<String>): Result<String, PyError> {
|
||||
val result = runPoetryWithSdk(manager.sdk, *args.toTypedArray())
|
||||
return result.asPythonResult()
|
||||
}
|
||||
@@ -30,11 +30,9 @@ import com.jetbrains.python.errorProcessing.PyError
|
||||
import com.jetbrains.python.errorProcessing.asPythonResult
|
||||
import com.jetbrains.python.newProjectWizard.collector.PythonNewProjectWizardCollector
|
||||
import com.jetbrains.python.sdk.add.v2.CustomNewEnvironmentCreator
|
||||
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMethod
|
||||
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMethod.*
|
||||
import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel
|
||||
import com.jetbrains.python.sdk.add.v2.PythonSelectableInterpreter
|
||||
import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers
|
||||
import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.*
|
||||
import com.jetbrains.python.sdk.add.v2.VenvExistenceValidationState.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -154,7 +152,7 @@ internal class PoetryConfigService : PersistentStateComponent<PoetryConfigServic
|
||||
val hasPoetryToml = poetryToml(module) != null
|
||||
|
||||
if (isInProjectEnv || hasPoetryToml) {
|
||||
val modulePath = withContext(Dispatchers.IO) { pyProjectToml(module)?.parent?.toNioPath() ?: module.basePath?.let { Path.of(it) } }
|
||||
val modulePath = withContext(Dispatchers.IO) { findPyProjectToml(module)?.parent?.toNioPath() ?: module.basePath?.let { Path.of(it) } }
|
||||
configurePoetryEnvironment(modulePath, "virtualenvs.in-project", isInProjectEnv.toString(), "--local")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import com.jetbrains.python.sdk.add.v2.DetectedSelectableInterpreter
|
||||
import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel
|
||||
import com.jetbrains.python.sdk.poetry.detectPoetryEnvs
|
||||
import com.jetbrains.python.sdk.poetry.isPoetry
|
||||
import com.jetbrains.python.sdk.poetry.pyProjectToml
|
||||
import com.jetbrains.python.sdk.poetry.findPyProjectToml
|
||||
import com.jetbrains.python.sdk.poetry.setupPoetrySdkUnderProgress
|
||||
import com.jetbrains.python.statistics.InterpreterType
|
||||
import com.jetbrains.python.statistics.version
|
||||
@@ -47,5 +47,5 @@ internal class PoetryExistingEnvironmentSelector(model: PythonMutableTargetAddIn
|
||||
existingEnvironments.value = existingEnvs
|
||||
}
|
||||
|
||||
override suspend fun findModulePath(module: Module): Path? = pyProjectToml(module)?.toNioPathOrNull()?.parent
|
||||
override suspend fun findModulePath(module: Module): Path? = findPyProjectToml(module)?.toNioPathOrNull()?.parent
|
||||
}
|
||||
@@ -1,29 +1,22 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.poetry
|
||||
|
||||
import com.intellij.execution.ExecutionException
|
||||
import com.intellij.execution.configurations.PathEnvironmentVariableUtil
|
||||
import com.intellij.ide.util.PropertiesComponent
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.ui.ValidationInfo
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import com.intellij.openapi.util.SystemInfo
|
||||
import com.intellij.openapi.util.io.FileUtil
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.platform.ide.progress.withBackgroundProgress
|
||||
import com.intellij.python.community.impl.poetry.poetryPath
|
||||
import com.intellij.util.SystemProperties
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.packaging.PyPackage
|
||||
import com.jetbrains.python.packaging.PyPackageManager
|
||||
import com.jetbrains.python.packaging.PyRequirement
|
||||
import com.jetbrains.python.packaging.PyRequirementParser
|
||||
import com.jetbrains.python.packaging.common.PythonOutdatedPackage
|
||||
import com.jetbrains.python.packaging.common.PythonPackage
|
||||
import com.jetbrains.python.packaging.management.PythonPackageManager
|
||||
import com.jetbrains.python.pathValidation.PlatformAndRoot
|
||||
import com.jetbrains.python.pathValidation.ValidationRequest
|
||||
import com.jetbrains.python.pathValidation.validateExecutableFile
|
||||
@@ -31,7 +24,6 @@ import com.jetbrains.python.sdk.*
|
||||
import com.jetbrains.python.venvReader.VirtualEnvReader
|
||||
import io.github.z4kn4fein.semver.Version
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import io.github.z4kn4fein.semver.toVersion
|
||||
import org.jetbrains.annotations.ApiStatus.Internal
|
||||
@@ -126,28 +118,6 @@ suspend fun setupPoetry(projectPath: Path, python: String?, installPackages: Boo
|
||||
return runPoetry(projectPath, "env", "info", "-p")
|
||||
}
|
||||
|
||||
internal fun runPoetryInBackground(module: Module, args: List<String>, @NlsSafe description: String) {
|
||||
service<PythonSdkCoroutineService>().cs.launch {
|
||||
withBackgroundProgress(module.project, "$description...", true) {
|
||||
val sdk = module.pythonSdk ?: return@withBackgroundProgress
|
||||
try {
|
||||
val result = runPoetryWithSdk(sdk, *args.toTypedArray()).exceptionOrNull()
|
||||
if (result is ExecutionException) {
|
||||
withContext(Dispatchers.EDT) {
|
||||
showSdkExecutionException(sdk, result, PyBundle.message("sdk.create.custom.venv.run.error.message", "poetry"))
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
PythonSdkUtil.getSitePackagesDirectory(sdk)?.refresh(true, true)
|
||||
sdk.associatedModuleDir?.refresh(true, false)
|
||||
PythonPackageManager.forSdk(module.project, sdk).reloadPackages()
|
||||
PyPackageManager.getInstance(sdk).refreshAndGetPackages(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun detectPoetryEnvs(module: Module?, existingSdkPaths: Set<String>?, projectPath: @SystemIndependent @NonNls String?): List<PyDetectedSdk> {
|
||||
val path = module?.basePath?.let { Path.of(it) } ?: projectPath?.let { Path.of(it) } ?: return emptyList()
|
||||
return getPoetryEnvs(path).filter { existingSdkPaths?.contains(getPythonExecutable(it)) != false }.map { PyDetectedSdk(getPythonExecutable(it)) }
|
||||
|
||||
@@ -64,7 +64,7 @@ suspend fun getPyProjectTomlForPoetry(virtualFile: VirtualFile): VirtualFile? =
|
||||
* The PyProject.toml found in the main content root of the module.
|
||||
*/
|
||||
@Internal
|
||||
suspend fun pyProjectToml(module: Module): VirtualFile? = withContext(Dispatchers.IO) { findAmongRoots(module, PY_PROJECT_TOML) }
|
||||
suspend fun findPyProjectToml(module: Module): VirtualFile? = withContext(Dispatchers.IO) { findAmongRoots(module, PY_PROJECT_TOML) }
|
||||
|
||||
internal suspend fun poetryToml(module: Module): VirtualFile? = withContext(Dispatchers.IO) {
|
||||
findAmongRoots(module, POETRY_TOML)?.takeIf { readAction { ProjectFileIndex.getInstance(module.project).isInProject(it) } }
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.poetry
|
||||
|
||||
import com.intellij.notification.NotificationGroupManager
|
||||
import com.intellij.notification.NotificationListener
|
||||
import com.intellij.notification.NotificationType
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.editor.Document
|
||||
import com.intellij.openapi.editor.event.DocumentEvent
|
||||
import com.intellij.openapi.editor.event.DocumentListener
|
||||
import com.intellij.openapi.editor.event.EditorFactoryEvent
|
||||
import com.intellij.openapi.editor.event.EditorFactoryListener
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManager
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.module.ModuleUtil
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.Key
|
||||
import com.intellij.openapi.util.NlsContexts.NotificationContent
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.platform.util.coroutines.limitedParallelism
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.sdk.PythonSdkCoroutineService
|
||||
import com.jetbrains.python.sdk.findAmongRoots
|
||||
import com.jetbrains.python.sdk.pythonSdk
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.annotations.Nls
|
||||
|
||||
private class PoetryProjectTomlListener(val module: Module) : DocumentListener {
|
||||
private val LOCK_NOTIFICATION_GROUP by lazy { NotificationGroupManager.getInstance().getNotificationGroup("pyproject.toml Watcher") }
|
||||
private val notificationActive = Key.create<Boolean>("PyProjectToml.notification.active")
|
||||
private val documentChangedMutex = Mutex()
|
||||
|
||||
override fun documentChanged(event: DocumentEvent) {
|
||||
service<PythonSdkCoroutineService>().cs.launch {
|
||||
if (!FileDocumentManager.getInstance().isDocumentUnsaved(event.document)) return@launch
|
||||
|
||||
documentChangedMutex.withLock(module) {
|
||||
if (isNotificationActive()) return@launch
|
||||
setNotificationActive(true)
|
||||
}
|
||||
|
||||
notifyPyProjectTomlChanged(module)
|
||||
}
|
||||
}
|
||||
|
||||
@NotificationContent
|
||||
private suspend fun content(): @Nls String = if (getPoetryVersion()?.let { it < "1.1.1" } == true) {
|
||||
PyBundle.message("python.sdk.poetry.pip.file.notification.content")
|
||||
}
|
||||
else {
|
||||
PyBundle.message("python.sdk.poetry.pip.file.notification.content.without.updating")
|
||||
}
|
||||
|
||||
private suspend fun poetryLock(module: Module) = withContext(Dispatchers.IO) { findAmongRoots(module, POETRY_LOCK) }
|
||||
|
||||
fun isNotificationActive(): Boolean = module.getUserData(notificationActive) == true
|
||||
|
||||
fun setNotificationActive(isActive: Boolean): Unit = module.putUserData(notificationActive, isActive.takeIf { it })
|
||||
|
||||
private suspend fun notifyPyProjectTomlChanged(module: Module) {
|
||||
@Suppress("DialogTitleCapitalization") val title = when (poetryLock(module)) {
|
||||
null -> PyBundle.message("python.sdk.poetry.pip.file.lock.not.found")
|
||||
else -> PyBundle.message("python.sdk.poetry.pip.file.lock.out.of.date")
|
||||
}
|
||||
|
||||
val notification = LOCK_NOTIFICATION_GROUP.createNotification(title, content(), NotificationType.INFORMATION).setListener(
|
||||
NotificationListener { notification, event ->
|
||||
FileDocumentManager.getInstance().saveAllDocuments()
|
||||
when (event.description) {
|
||||
"#lock" ->
|
||||
runPoetryInBackground(module, listOf("lock"), PyBundle.message("python.sdk.poetry.pip.file.notification.locking"))
|
||||
"#noupdate" ->
|
||||
runPoetryInBackground(module, listOf("lock", "--no-update"),
|
||||
PyBundle.message("python.sdk.poetry.pip.file.notification.locking.without.updating"))
|
||||
"#update" ->
|
||||
runPoetryInBackground(module, listOf("update"), PyBundle.message("python.sdk.poetry.pip.file.notification.updating"))
|
||||
}
|
||||
notification.expire()
|
||||
})
|
||||
|
||||
notification.whenExpired {
|
||||
service<PythonSdkCoroutineService>().cs.launch {
|
||||
setNotificationActive(false)
|
||||
}
|
||||
}
|
||||
|
||||
notification.notify(module.project)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Watches for edits in PyProjectToml inside modules with a poetry SDK set.
|
||||
*/
|
||||
internal class PoetryPyProjectTomlWatcher : EditorFactoryListener {
|
||||
private val changeListenerKey = Key.create<PoetryProjectTomlListener>("Poetry.PyProjectToml.change.listener")
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val queueDispatcher = Dispatchers.Default.limitedParallelism(1, "PoetryPyProjectTomlWatcher Queue Dispatcher")
|
||||
|
||||
|
||||
private fun Document.addPoetryListener(module: Module) = synchronized(changeListenerKey) {
|
||||
getUserData(changeListenerKey)?.let { return@addPoetryListener }
|
||||
|
||||
PoetryProjectTomlListener(module).let {
|
||||
addDocumentListener(it)
|
||||
putUserData(changeListenerKey, it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Document.removePoetryListenerIfExists() = synchronized(changeListenerKey) {
|
||||
getUserData(changeListenerKey)?.let { listener ->
|
||||
removeDocumentListener(listener)
|
||||
putUserData(changeListenerKey, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun queuedLaunch(block: suspend () -> Unit) {
|
||||
service<PythonSdkCoroutineService>().cs.launch {
|
||||
withContext(queueDispatcher) {
|
||||
block()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun editorCreated(event: EditorFactoryEvent) = queuedLaunch {
|
||||
val project = event.editor.project ?: return@queuedLaunch
|
||||
|
||||
val editablePyProjectTomlFile = event.editor.document.virtualFile?.takeIf { it.name == PY_PROJECT_TOML } ?: return@queuedLaunch
|
||||
val module = getModule(editablePyProjectTomlFile, project) ?: return@queuedLaunch
|
||||
val poetryManagedTomlFile = module.takeIf { it.pythonSdk?.isPoetry == true }?.let { pyProjectToml(it) }
|
||||
|
||||
if (editablePyProjectTomlFile == poetryManagedTomlFile) {
|
||||
event.editor.document.addPoetryListener(module)
|
||||
}
|
||||
}
|
||||
|
||||
override fun editorReleased(event: EditorFactoryEvent) = queuedLaunch {
|
||||
event.editor.document.removePoetryListenerIfExists()
|
||||
}
|
||||
|
||||
private val Document.virtualFile: VirtualFile?
|
||||
get() = FileDocumentManager.getInstance().getFile(this)
|
||||
|
||||
private suspend fun getModule(file: VirtualFile, project: Project): Module? = withContext(Dispatchers.IO) {
|
||||
ModuleUtil.findModuleForFile(file, project)
|
||||
}
|
||||
}
|
||||
@@ -169,7 +169,7 @@ class PyAddNewPoetryPanel(
|
||||
private fun update() {
|
||||
service<PythonSdkCoroutineService>().cs.launch {
|
||||
selectedModule?.let {
|
||||
installPackagesCheckBox.isEnabled = pyProjectToml(it) != null
|
||||
installPackagesCheckBox.isEnabled = findPyProjectToml(it) != null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,4 +29,7 @@ interface UvLowLevel {
|
||||
|
||||
suspend fun listPackages(): Result<List<PythonPackage>>
|
||||
suspend fun listOutdatedPackages(): Result<List<PythonOutdatedPackage>>
|
||||
|
||||
suspend fun sync(): Result<String>
|
||||
suspend fun lock(): Result<String>
|
||||
}
|
||||
@@ -67,6 +67,14 @@ internal class UvPackageManager(project: Project, sdk: Sdk, private val uv: UvLo
|
||||
|
||||
return uv.listPackages()
|
||||
}
|
||||
|
||||
suspend fun sync(): Result<String> {
|
||||
return uv.sync()
|
||||
}
|
||||
|
||||
suspend fun lock(): Result<String> {
|
||||
return uv.lock()
|
||||
}
|
||||
}
|
||||
|
||||
class UvPackageManagerProvider : PythonPackageManagerProvider {
|
||||
|
||||
@@ -172,6 +172,14 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev
|
||||
|
||||
return pythons
|
||||
}
|
||||
|
||||
override suspend fun sync(): Result<String> {
|
||||
return uvCli.runUv(cwd, "sync")
|
||||
}
|
||||
|
||||
override suspend fun lock(): Result<String> {
|
||||
return uvCli.runUv(cwd, "lock")
|
||||
}
|
||||
}
|
||||
|
||||
fun createUvLowLevel(cwd: Path, uvCli: UvCli = createUvCli()): UvLowLevel {
|
||||
|
||||
5
python/src/com/jetbrains/python/uv/package-info.java
Normal file
5
python/src/com/jetbrains/python/uv/package-info.java
Normal file
@@ -0,0 +1,5 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
@ApiStatus.Internal
|
||||
package com.jetbrains.python.uv;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.uv.packaging
|
||||
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.errorProcessing.PyError
|
||||
import com.jetbrains.python.errorProcessing.asPythonResult
|
||||
import com.jetbrains.python.packaging.management.PythonPackageManagerAction
|
||||
import com.jetbrains.python.packaging.management.getPythonPackageManager
|
||||
import com.jetbrains.python.sdk.uv.UvPackageManager
|
||||
|
||||
internal sealed class UvPackageManagerAction : PythonPackageManagerAction<UvPackageManager, String>() {
|
||||
override fun getManager(e: AnActionEvent): UvPackageManager? = e.getPythonPackageManager()
|
||||
}
|
||||
|
||||
internal class UvSyncAction() : UvPackageManagerAction() {
|
||||
override suspend fun execute(e: AnActionEvent, manager: UvPackageManager): Result<String, PyError> {
|
||||
return manager.sync().asPythonResult()
|
||||
}
|
||||
}
|
||||
|
||||
internal class UvLockAction() : UvPackageManagerAction() {
|
||||
override suspend fun execute(e: AnActionEvent, manager: UvPackageManager): Result<String, PyError> {
|
||||
return manager.lock().asPythonResult()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user