diff --git a/python/pluginCore/resources/META-INF/plugin.xml b/python/pluginCore/resources/META-INF/plugin.xml
index 39bc1ac3b723..1ddd800dc502 100644
--- a/python/pluginCore/resources/META-INF/plugin.xml
+++ b/python/pluginCore/resources/META-INF/plugin.xml
@@ -572,9 +572,6 @@ The Python plug-in provides smart editing for Python scripts. The feature set of
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties
index 07d69576d0c5..849e5d56a8fb 100644
--- a/python/pluginResources/messages/PyBundle.properties
+++ b/python/pluginResources/messages/PyBundle.properties
@@ -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 poetry lock or poetry update
python.sdk.poetry.pip.file.notification.content.without.updating=Run poetry lock, poetry lock --no-update or poetry update
-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
diff --git a/python/python-hatch/src/com/intellij/python/hatch/cli/HatchCli.kt b/python/python-hatch/src/com/intellij/python/hatch/cli/HatchCli.kt
index 0e8d0ae5c230..53d38c5a7840 100644
--- a/python/python-hatch/src/com/intellij/python/hatch/cli/HatchCli.kt
+++ b/python/python-hatch/src/com/intellij/python/hatch/cli/HatchCli.kt
@@ -146,19 +146,16 @@ class HatchCli(private val runtime: HatchRuntime) {
/**
* Run commands within project environments
*/
- suspend fun run(envName: String, vararg command: String): Result {
- val envRuntime = runtime.withEnv(HatchConstants.AppEnvVars.ENV to envName)
+ suspend fun run(envName: String? = null, vararg command: String): Result {
+ 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")
}
}
diff --git a/python/python-hatch/src/com/intellij/python/hatch/hatch.kt b/python/python-hatch/src/com/intellij/python/hatch/hatch.kt
index 74c7636a22ac..a8c976fce169 100644
--- a/python/python-hatch/src/com/intellij/python/hatch/hatch.kt
+++ b/python/python-hatch/src/com/intellij/python/hatch/hatch.kt
@@ -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
+ suspend fun syncDependencies(envName: String? = null): Result
suspend fun isHatchManagedProject(): Result
@@ -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 {
- return CliBasedHatchService(hatchExecutablePath = hatchExecutablePath, workingDirectoryPath = this)
+suspend fun Path?.getHatchService(hatchExecutablePath: Path? = null, hatchEnvironmentName: String? = null): Result {
+ return CliBasedHatchService(hatchExecutablePath = hatchExecutablePath, workingDirectoryPath = this, hatchEnvironmentName = hatchEnvironmentName)
}
/**
diff --git a/python/python-hatch/src/com/intellij/python/hatch/service/CliBasedHatchService.kt b/python/python-hatch/src/com/intellij/python/hatch/service/CliBasedHatchService.kt
index eebad0acf14a..6708489ef24e 100644
--- a/python/python-hatch/src/com/intellij/python/hatch/service/CliBasedHatchService.kt
+++ b/python/python-hatch/src/com/intellij/python/hatch/service/CliBasedHatchService.kt
@@ -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 {
+ suspend operator fun invoke(workingDirectoryPath: Path?, hatchExecutablePath: Path? = null, hatchEnvironmentName: String? = null): Result {
+ 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 {
+ override suspend fun syncDependencies(envName: String?): Result {
return withContext(Dispatchers.IO) {
hatchRuntime.hatchCli().run(envName, "python", "--version")
}
diff --git a/python/src/com/jetbrains/python/ProcessExecutionErrorDialog.kt b/python/src/com/jetbrains/python/ProcessExecutionErrorDialog.kt
index e40854043e87..8c5d41a19877 100644
--- a/python/src/com/jetbrains/python/ProcessExecutionErrorDialog.kt
+++ b/python/src/com/jetbrains/python/ProcessExecutionErrorDialog.kt
@@ -70,7 +70,7 @@ fun showProcessExecutionErrorDialog(
override fun createActions(): Array = arrayOf(okAction)
override fun createCenterPanel(): JComponent = formBuilder.panel.apply {
- preferredSize = Dimension(600, 300)
+ preferredSize = Dimension(820, 400)
}
}.showAndGet()
}
diff --git a/python/src/com/jetbrains/python/hatch/packaging/HatchPackageManager.kt b/python/src/com/jetbrains/python/hatch/packaging/HatchPackageManager.kt
index d4c7e0d0b59d..4323abdf53a1 100644
--- a/python/src/com/jetbrains/python/hatch/packaging/HatchPackageManager.kt
+++ b/python/src/com/jetbrains/python/hatch/packaging/HatchPackageManager.kt
@@ -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 {
+ val data = getSdkAdditionalData()
+ val workingDirectory = data.hatchWorkingDirectory
+ return workingDirectory.getHatchService(hatchEnvironmentName = data.hatchEnvironmentName)
+ }
}
internal class HatchPackageManagerProvider : PythonPackageManagerProvider {
diff --git a/python/src/com/jetbrains/python/hatch/packaging/HatchPackageManagerAction.kt b/python/src/com/jetbrains/python/hatch/packaging/HatchPackageManagerAction.kt
new file mode 100644
index 000000000000..c3c57ddd1f56
--- /dev/null
+++ b/python/src/com/jetbrains/python/hatch/packaging/HatchPackageManagerAction.kt
@@ -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() {
+ 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 {
+ val service = manager.getHatchService().getOr { return it }
+ return service.syncDependencies()
+ }
+}
diff --git a/python/src/com/jetbrains/python/packaging/management/CancellableJobSerialRunner.kt b/python/src/com/jetbrains/python/packaging/management/CancellableJobSerialRunner.kt
new file mode 100644
index 000000000000..d07243099035
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/management/CancellableJobSerialRunner.kt
@@ -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("${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 run(
+ project: Project,
+ holder: UserDataHolder,
+ title: @ProgressTitle String,
+ runnable: suspend () -> Result,
+ ): Result {
+
+ 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
+ }
+ }
+ }
+}
diff --git a/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerAction.kt b/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerAction.kt
new file mode 100644
index 000000000000..30f561070edd
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerAction.kt
@@ -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 : DumbAwareAction() {
+ protected val errorSink: ErrorSink = ShowingMessageErrorSync
+ protected val scope: CoroutineScope = service().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
+
+ 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 {
+ 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 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 PythonPackageManager.runSynchronized(
+ title: @ProgressTitle String,
+ runnable: suspend () -> Result,
+): Result {
+ return CancellableJobSerialRunner.run(this.project, this.sdk, title, runnable)
+}
diff --git a/python/src/com/jetbrains/python/poetry/package-info.java b/python/src/com/jetbrains/python/poetry/package-info.java
new file mode 100644
index 000000000000..0118edb24f6b
--- /dev/null
+++ b/python/src/com/jetbrains/python/poetry/package-info.java
@@ -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;
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/poetry/packaging/PoetryPackageManagerAction.kt b/python/src/com/jetbrains/python/poetry/packaging/PoetryPackageManagerAction.kt
new file mode 100644
index 000000000000..0a05cc60d620
--- /dev/null
+++ b/python/src/com/jetbrains/python/poetry/packaging/PoetryPackageManagerAction.kt
@@ -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() {
+ override fun getManager(e: AnActionEvent): PoetryPackageManager? = e.getPythonPackageManager()
+}
+
+internal class PoetryLockAction() : PoetryPackageManagerAction() {
+ override suspend fun execute(e: AnActionEvent, manager: PoetryPackageManager): Result {
+ return runPoetryWithManager(manager, listOf("lock"))
+ }
+}
+
+internal class PoetryUpdateAction() : PoetryPackageManagerAction() {
+ override suspend fun execute(e: AnActionEvent, manager: PoetryPackageManager): Result {
+ return runPoetryWithManager(manager, listOf("update"))
+ }
+}
+
+private suspend fun runPoetryWithManager(manager: PoetryPackageManager, args: List): Result {
+ val result = runPoetryWithSdk(manager.sdk, *args.toTypedArray())
+ return result.asPythonResult()
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/sdk/add/v2/poetry/EnvironmentCreatorPoetry.kt b/python/src/com/jetbrains/python/sdk/add/v2/poetry/EnvironmentCreatorPoetry.kt
index 52f92b300c58..5e619d59eb79 100644
--- a/python/src/com/jetbrains/python/sdk/add/v2/poetry/EnvironmentCreatorPoetry.kt
+++ b/python/src/com/jetbrains/python/sdk/add/v2/poetry/EnvironmentCreatorPoetry.kt
@@ -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, @NlsSafe description: String) {
- service().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?, projectPath: @SystemIndependent @NonNls String?): List {
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)) }
diff --git a/python/src/com/jetbrains/python/sdk/poetry/PoetryFilesUtils.kt b/python/src/com/jetbrains/python/sdk/poetry/PoetryFilesUtils.kt
index 54caf83c2225..40b59b09d18a 100644
--- a/python/src/com/jetbrains/python/sdk/poetry/PoetryFilesUtils.kt
+++ b/python/src/com/jetbrains/python/sdk/poetry/PoetryFilesUtils.kt
@@ -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) } }
diff --git a/python/src/com/jetbrains/python/sdk/poetry/PoetryPyProjectTomlWatcher.kt b/python/src/com/jetbrains/python/sdk/poetry/PoetryPyProjectTomlWatcher.kt
deleted file mode 100644
index 27c6ff119dcf..000000000000
--- a/python/src/com/jetbrains/python/sdk/poetry/PoetryPyProjectTomlWatcher.kt
+++ /dev/null
@@ -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("PyProjectToml.notification.active")
- private val documentChangedMutex = Mutex()
-
- override fun documentChanged(event: DocumentEvent) {
- service().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().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("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().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)
- }
-}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/sdk/poetry/ui/PyAddNewPoetryPanel.kt b/python/src/com/jetbrains/python/sdk/poetry/ui/PyAddNewPoetryPanel.kt
index de83d4546fa0..e0e93db25b5c 100644
--- a/python/src/com/jetbrains/python/sdk/poetry/ui/PyAddNewPoetryPanel.kt
+++ b/python/src/com/jetbrains/python/sdk/poetry/ui/PyAddNewPoetryPanel.kt
@@ -169,7 +169,7 @@ class PyAddNewPoetryPanel(
private fun update() {
service().cs.launch {
selectedModule?.let {
- installPackagesCheckBox.isEnabled = pyProjectToml(it) != null
+ installPackagesCheckBox.isEnabled = findPyProjectToml(it) != null
}
}
}
diff --git a/python/src/com/jetbrains/python/sdk/uv/Uv.kt b/python/src/com/jetbrains/python/sdk/uv/Uv.kt
index 5b048dc82b82..d57d8ca0048f 100644
--- a/python/src/com/jetbrains/python/sdk/uv/Uv.kt
+++ b/python/src/com/jetbrains/python/sdk/uv/Uv.kt
@@ -29,4 +29,7 @@ interface UvLowLevel {
suspend fun listPackages(): Result>
suspend fun listOutdatedPackages(): Result>
+
+ suspend fun sync(): Result
+ suspend fun lock(): Result
}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/sdk/uv/UvPackageManager.kt b/python/src/com/jetbrains/python/sdk/uv/UvPackageManager.kt
index 9dbf86dd789f..b23b56716fa7 100644
--- a/python/src/com/jetbrains/python/sdk/uv/UvPackageManager.kt
+++ b/python/src/com/jetbrains/python/sdk/uv/UvPackageManager.kt
@@ -67,6 +67,14 @@ internal class UvPackageManager(project: Project, sdk: Sdk, private val uv: UvLo
return uv.listPackages()
}
+
+ suspend fun sync(): Result {
+ return uv.sync()
+ }
+
+ suspend fun lock(): Result {
+ return uv.lock()
+ }
}
class UvPackageManagerProvider : PythonPackageManagerProvider {
diff --git a/python/src/com/jetbrains/python/sdk/uv/impl/UvLowLevel.kt b/python/src/com/jetbrains/python/sdk/uv/impl/UvLowLevel.kt
index 934a58077291..339ef09bf496 100644
--- a/python/src/com/jetbrains/python/sdk/uv/impl/UvLowLevel.kt
+++ b/python/src/com/jetbrains/python/sdk/uv/impl/UvLowLevel.kt
@@ -172,6 +172,14 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev
return pythons
}
+
+ override suspend fun sync(): Result {
+ return uvCli.runUv(cwd, "sync")
+ }
+
+ override suspend fun lock(): Result {
+ return uvCli.runUv(cwd, "lock")
+ }
}
fun createUvLowLevel(cwd: Path, uvCli: UvCli = createUvCli()): UvLowLevel {
diff --git a/python/src/com/jetbrains/python/uv/package-info.java b/python/src/com/jetbrains/python/uv/package-info.java
new file mode 100644
index 000000000000..2185fd666c2f
--- /dev/null
+++ b/python/src/com/jetbrains/python/uv/package-info.java
@@ -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;
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/uv/packaging/UvPackageManagerAction.kt b/python/src/com/jetbrains/python/uv/packaging/UvPackageManagerAction.kt
new file mode 100644
index 000000000000..886f1b28e0c2
--- /dev/null
+++ b/python/src/com/jetbrains/python/uv/packaging/UvPackageManagerAction.kt
@@ -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() {
+ override fun getManager(e: AnActionEvent): UvPackageManager? = e.getPythonPackageManager()
+}
+
+internal class UvSyncAction() : UvPackageManagerAction() {
+ override suspend fun execute(e: AnActionEvent, manager: UvPackageManager): Result {
+ return manager.sync().asPythonResult()
+ }
+}
+
+internal class UvLockAction() : UvPackageManagerAction() {
+ override suspend fun execute(e: AnActionEvent, manager: UvPackageManager): Result {
+ return manager.lock().asPythonResult()
+ }
+}
\ No newline at end of file