diff --git a/plugins/evaluation-plugin/languages/python/src/com/intellij/cce/python/evaluation/SetupPythonInterpreterStep.kt b/plugins/evaluation-plugin/languages/python/src/com/intellij/cce/python/evaluation/SetupPythonInterpreterStep.kt
index 831cf1333042..b8b9998704eb 100644
--- a/plugins/evaluation-plugin/languages/python/src/com/intellij/cce/python/evaluation/SetupPythonInterpreterStep.kt
+++ b/plugins/evaluation-plugin/languages/python/src/com/intellij/cce/python/evaluation/SetupPythonInterpreterStep.kt
@@ -22,8 +22,8 @@ import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification
+import com.jetbrains.python.packaging.management.PythonPackageInstallRequest
import com.jetbrains.python.packaging.management.PythonPackageManager
-import com.jetbrains.python.packaging.management.toInstallRequest
import com.jetbrains.python.packaging.pip.PipPythonPackageManager
import com.jetbrains.python.packaging.requirement.PyRequirementRelation
import com.jetbrains.python.sdk.PythonSdkType
@@ -146,7 +146,7 @@ private class SetupPythonInterpreterStep(
val packageManager = PythonPackageManager.forSdk(project, sdk)
packageManager.reloadPackages()
- val packages = readRequiredPackages(packageManager).filterNot { packageManager.packageExists(PythonPackage(it.name, "", false)) }
+ val packages = readRequiredPackages(packageManager).filterNot { packageManager.isPackageInstalled(PythonPackage(it.name, "", false)) }
if (packages.isEmpty()) {
println("No packages to install. Skipping.")
return
@@ -160,7 +160,9 @@ private class SetupPythonInterpreterStep(
// resolves `'runBlockingCancellable' is forbidden in the Write Action` from PythonSdkUpdater.scheduleUpdate
keepTasksAsynchronousInHeadlessMode {
- packageManager.installPackages(packages.map { it.toInstallRequest() }, cacheOptions, false)
+ val installRequest = PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications(packages)
+
+ packageManager.installPackage(installRequest, cacheOptions)
println("Installed packages: ${packages.joinToString(", ") { it.name }}")
}
}
@@ -175,7 +177,7 @@ private class SetupPythonInterpreterStep(
reader.readLines().filter { it.isNotBlank() }.mapNotNull { line ->
val relation = PyRequirementRelation.entries.find { line.contains(it.presentableText) }
val parts = if (relation != null) line.split(relation.presentableText).map { it.trim() } else listOf(line.trim())
- packageManager.createPackageSpecification(
+ packageManager.findPackageSpecification(
packageName = parts[0],
version = parts.getOrNull(1),
relation = relation ?: PyRequirementRelation.EQ
diff --git a/python/pluginResources/intellij.python.community.impl.xml b/python/pluginResources/intellij.python.community.impl.xml
index a48958a40737..2534301df469 100644
--- a/python/pluginResources/intellij.python.community.impl.xml
+++ b/python/pluginResources/intellij.python.community.impl.xml
@@ -39,11 +39,11 @@
+ implementationClass="com.jetbrains.python.requirements.inspections.tools.NotInstalledRequirementInspection"/>
+ implementationClass="com.jetbrains.python.requirements.inspections.tools.OutdatedRequirementInspection"/>
@@ -81,11 +81,6 @@
defaultValue="true"
restartRequired="false"
description="Use bare python instead of conda run for conda"/>
-
diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties
index d1e494375c38..0fca552b04e1 100644
--- a/python/pluginResources/messages/PyBundle.properties
+++ b/python/pluginResources/messages/PyBundle.properties
@@ -423,13 +423,6 @@ 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
-# Composite package manager
-python.packaging.composite.install.package.error=No suitable package managers were found to install the package: {0}. Tried the following managers: {1}.
-python.packaging.composite.update.package.error=No suitable package managers were found to update the package: {0}. Tried the following managers: {1}.
-python.packaging.composite.uninstall.package.error=No suitable package managers were found to uninstall the package: {0}. Tried the following managers: {1}.
-python.packaging.composite.reload.packages.error=No suitable package managers were found to update the packages. Tried the following managers: {0}.
-python.packaging.composite.list.outdated.packages.error=No suitable package managers were found to get the outdated packages. Tried the following managers: {0}.
-
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
@@ -972,6 +965,7 @@ python.new.project.synchronization.not.configured.dialog.message=Local/Remote sy
python.new.project.remote.path.not.provided=Remote path not provided
python.new.project.more.settings=Mor&e Settings
python.new.project.install.failed.title=Install {0} Failed
+python.new.project.update.failed.title=Update {0} Failed
python.new.project.error.solution.another.sdk=Choose another SDK
python.project.view.remote.libraries=Remote Libraries
@@ -981,10 +975,13 @@ python.project.view.bundled.stubs=Bundled Stubs
python.project.view.user.skeletons.node=Extended Definitions
python.packaging.installing.packaging.tools=Installing packaging tools\u2026
+python.packaging.installing.requirements=Installing requirements\u2026
python.packaging.installing.packages=Installing packages\u2026
python.packaging.installing.package=Installing package {0}\u2026
python.packaging.updating.packages=Updating packages\u2026
python.packaging.updating.package=Updating package {0}\u2026
+python.packaging.uninstall.packages=Uninstalling packages\u2026
+python.packaging.uninstall.package=Uninstalling package {0}\u2026
python.packaging.warning=Warning
python.packaging.create.setup.py=Create setup.py
python.packaging.failed.to.run.task=Failed to run task: {0}
@@ -1396,11 +1393,13 @@ python.packaging.notification.installed=Package {0} installed
python.packaging.notification.deleted=Package {0} deleted
python.packaging.notification.update.failed=Failed to Update {0}
python.packaging.notification.updated=Package {0} updated to version {1}
+python.packaging.notification.all.updated=Packages are updated
python.packaging.button.install.package=Install package
python.packages.no.details.in.repo=
Could not read package description from {0}.
python.packaging.error.no.repository.url=There is no repository url for {0} defined
python.packaging.error.not.simple.repository.url=Not simple repository api url defined {0}, it has to have {1} ending
python.packaging.error.package.is.not.listed.in.repositories=Package {0} is not listed in available repositories
+python.packaging.error.package.is.not.listed.in.repository=Package {0} is not listed in repository {1}
python.packaging.interpreter.widget.manage.packages=Manage Packages\u2026
# Conda
diff --git a/python/src/com/jetbrains/python/black/configuration/BlackFormatterConfigurable.kt b/python/src/com/jetbrains/python/black/configuration/BlackFormatterConfigurable.kt
index 470aefa0e0da..45b783bc6709 100644
--- a/python/src/com/jetbrains/python/black/configuration/BlackFormatterConfigurable.kt
+++ b/python/src/com/jetbrains/python/black/configuration/BlackFormatterConfigurable.kt
@@ -131,7 +131,7 @@ class BlackFormatterConfigurable(val project: Project) : BoundConfigurable(PyBun
installButton = button(PyBundle.message("black.install.button.label")) {
runWithModalProgressBlocking(ModalTaskOwner.project(project), PyBundle.message("black.installing.modal.title")) {
if (selectedSdk != null) {
- PyPackageInstallUtils.installPackage(project, selectedSdk!!, BlackFormatterUtil.PACKAGE_NAME, false).onSuccess {
+ PyPackageInstallUtils.installPackage(project, selectedSdk!!, BlackFormatterUtil.PACKAGE_NAME).onSuccess {
withContext(Dispatchers.EDT) {
isBlackFormatterPackageInstalled = true
enableOnReformatCheckBox.isSelected = true
diff --git a/python/src/com/jetbrains/python/inspections/quickfix/InstallAllPackagesQuickFix.kt b/python/src/com/jetbrains/python/inspections/quickfix/InstallAllPackagesQuickFix.kt
index 71ca34cbbb49..5e07ae62a24b 100644
--- a/python/src/com/jetbrains/python/inspections/quickfix/InstallAllPackagesQuickFix.kt
+++ b/python/src/com/jetbrains/python/inspections/quickfix/InstallAllPackagesQuickFix.kt
@@ -8,7 +8,7 @@ import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.project.Project
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PyPsiPackageUtil.moduleToPackageName
-import com.jetbrains.python.packaging.getConfirmedPackages
+import com.jetbrains.python.packaging.PyPackageInstallUtils
import com.jetbrains.python.packaging.pyRequirement
import com.jetbrains.python.sdk.PythonSdkUtil
import com.jetbrains.python.statistics.PyPackagesUsageCollector
@@ -20,8 +20,10 @@ class InstallAllPackagesQuickFix(private val packageNames: List) : Local
val element = descriptor.psiElement ?: return
val module = ModuleUtilCore.findModuleForPsiElement(element) ?: return
val sdk = PythonSdkUtil.findPythonSdk(element) ?: return
+
val normalizedPackageNames = packageNames.map { moduleToPackageName(it) }
- val confirmedPackages = getConfirmedPackages(normalizedPackageNames.map { pyRequirement(it) }, project)
+ val pyRequirements = normalizedPackageNames.map { pyRequirement(it) }
+ val confirmedPackages = PyPackageInstallUtils.getConfirmedPackages(pyRequirements, project)
if (confirmedPackages.isEmpty()) return
diff --git a/python/src/com/jetbrains/python/newProjectWizard/PyV3Utils.kt b/python/src/com/jetbrains/python/newProjectWizard/PyV3Utils.kt
index e93365b629d4..c7d1cdc0c06e 100644
--- a/python/src/com/jetbrains/python/newProjectWizard/PyV3Utils.kt
+++ b/python/src/com/jetbrains/python/newProjectWizard/PyV3Utils.kt
@@ -17,14 +17,15 @@ import org.jetbrains.annotations.CheckReturnValue
suspend fun installPackages(project: Project, sdk: Sdk, vararg packages: String): Result {
val packageManager = PythonPackageManager.forSdk(project, sdk)
return supervisorScope { // Not install other packages if one failed
+ packageManager.waitForInit()
val specifications = packages.map {
- packageManager.createPackageSpecification(it)
+ packageManager.findPackageSpecification(it)
?: return@supervisorScope Result.failure(IllegalArgumentException(
PyBundle.message("python.packaging.error.package.is.not.listed.in.repositories", it)
))
}
- val requests = specifications.map { PythonPackageInstallRequest.ByRepositoryPythonPackageSpecification(it) }
- return@supervisorScope packageManager.installPackages(requests, emptyList(), withBackgroundProgress = true).map {
+ val installRequest = PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications(specifications)
+ return@supervisorScope packageManager.installPackage(installRequest, emptyList()).map {
// We don't care about result, we just want to fail if any package failed to install
}
}
diff --git a/python/src/com/jetbrains/python/packaging/PyPackageInstallUtils.kt b/python/src/com/jetbrains/python/packaging/PyPackageInstallUtils.kt
index f904eadcbc7b..c4bb00639e1f 100644
--- a/python/src/com/jetbrains/python/packaging/PyPackageInstallUtils.kt
+++ b/python/src/com/jetbrains/python/packaging/PyPackageInstallUtils.kt
@@ -21,7 +21,6 @@ import com.jetbrains.python.PyPsiPackageUtil
import com.jetbrains.python.codeInsight.stdlib.PyStdlibUtil
import com.jetbrains.python.inspections.quickfix.InstallPackageQuickFix
import com.jetbrains.python.packaging.common.PythonPackage
-import com.jetbrains.python.packaging.common.runPackagingOperationOrShowErrorDialog
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.management.toInstallRequest
import com.jetbrains.python.packaging.requirement.PyRequirementVersionSpec
@@ -40,6 +39,23 @@ import javax.swing.UIManager
*/
@ApiStatus.Internal
object PyPackageInstallUtils {
+ internal fun getConfirmedPackages(packageNames: List, project: Project): Set {
+ val confirmationEnabled = PropertiesComponent.getInstance()
+ .getBoolean(InstallPackageQuickFix.CONFIRM_PACKAGE_INSTALLATION_PROPERTY, true)
+
+ if (!confirmationEnabled || packageNames.isEmpty()) return packageNames.toSet()
+
+ val dialog = PyChooseRequirementsDialog(project, packageNames) { it.presentableTextWithoutVersion }
+
+ if (!dialog.showAndGet()) {
+ PyPackagesUsageCollector.installAllCanceledEvent.log()
+ return emptySet()
+ }
+
+ return dialog.markedElements.toSet()
+ }
+
+
fun offeredPackageForNotFoundModule(project: Project, sdk: Sdk, moduleName: String): String? {
val shouldToInstall = checkShouldToInstall(project, sdk, moduleName)
if (!shouldToInstall)
@@ -73,7 +89,7 @@ object PyPackageInstallUtils {
}
if (!isConfirmed)
return
- val result = installPackage(project, sdk, packageName, true, versionSpec = versionSpec)
+ val result = installPackage(project, sdk, packageName, versionSpec = versionSpec)
result.getOrThrow()
}
@@ -97,7 +113,7 @@ object PyPackageInstallUtils {
suspend fun upgradePackage(project: Project, sdk: Sdk, packageName: String, version: String? = null): Result> {
val pythonPackageManager = getPackageManagerOrNull(project, sdk)
- val packageSpecification = pythonPackageManager?.repositoryManager?.repositories?.firstOrNull()?.createPackageSpecification(packageName, version)
+ val packageSpecification = pythonPackageManager?.repositoryManager?.repositories?.firstOrNull()?.findPackageSpecification(packageName, version)
?: return Result.failure(Exception("Could not find any repositories"))
return pythonPackageManager.updatePackages(packageSpecification)
@@ -107,7 +123,6 @@ object PyPackageInstallUtils {
project: Project,
sdk: Sdk,
packageName: String,
- withBackgroundProgress: Boolean,
versionSpec: PyRequirementVersionSpec? = null,
options: List = emptyList(),
): Result> {
@@ -118,11 +133,10 @@ object PyPackageInstallUtils {
return Result.failure(it)
}
- val spec = withContext(Dispatchers.IO) {
- pythonPackageManager.createPackageSpecificationWithSpec(packageName, versionSpec)
- } ?: return Result.failure(Exception("Package $packageName not found in any repository"))
+ val spec = pythonPackageManager.findPackageSpecificationWithVersionSpec(packageName, versionSpec)
+ ?: return Result.failure(Exception("Package $packageName not found in any repository"))
- return pythonPackageManager.installPackage(spec.toInstallRequest(), options, withBackgroundProgress)
+ return pythonPackageManager.installPackage(spec.toInstallRequest(), options)
}
catch (t: Throwable) {
Result.failure(t)
@@ -154,29 +168,26 @@ object PyPackageInstallUtils {
suspend fun uninstall(project: Project, sdk: Sdk, libName: String) {
val pythonPackageManager = getPackageManagerOrNull(project, sdk) ?: return
val pythonPackage = getPackage(project, sdk, libName) ?: return
- pythonPackageManager.uninstallPackage(pythonPackage)
+ pythonPackageManager.uninstallPackage(pythonPackage.name)
}
fun invokeInstallPackage(project: Project, pythonSdk: Sdk, packageName: String, point: RelativePoint, versionSpec: PyRequirementVersionSpec? = null) {
PyPackageCoroutine.launch(project) {
- runPackagingOperationOrShowErrorDialog(pythonSdk, PyBundle.message("python.new.project.install.failed.title", packageName),
- packageName) {
- val loadBalloon = showBalloon(point, PyBundle.message("python.packaging.installing.package", packageName), BalloonStyle.INFO)
- try {
- confirmAndInstall(project, pythonSdk, packageName, versionSpec = versionSpec)
- loadBalloon.hide()
- PyPackagesUsageCollector.installPackageFromConsole.log(project)
- showBalloon(point, PyBundle.message("python.packaging.notification.description.installed.packages", packageName), BalloonStyle.SUCCESS)
- }
- catch (t: Throwable) {
- loadBalloon.hide()
- PyPackagesUsageCollector.failInstallPackageFromConsole.log(project)
- showBalloon(point, PyBundle.message("python.new.project.install.failed.title", packageName), BalloonStyle.ERROR)
- throw t
- }
- Result.success(Unit)
+ val loadBalloon = showBalloon(point, PyBundle.message("python.packaging.installing.package", packageName), BalloonStyle.INFO)
+ try {
+ confirmAndInstall(project, pythonSdk, packageName, versionSpec = versionSpec)
+ loadBalloon.hide()
+ PyPackagesUsageCollector.installPackageFromConsole.log(project)
+ showBalloon(point, PyBundle.message("python.packaging.notification.description.installed.packages", packageName), BalloonStyle.SUCCESS)
}
+ catch (t: Throwable) {
+ loadBalloon.hide()
+ PyPackagesUsageCollector.failInstallPackageFromConsole.log(project)
+ showBalloon(point, PyBundle.message("python.new.project.install.failed.title", packageName), BalloonStyle.ERROR)
+ throw t
+ }
+ Result.success(Unit)
}
}
@@ -222,19 +233,4 @@ object PyPackageInstallUtils {
enum class BalloonStyle { ERROR, INFO, SUCCESS }
}
-internal fun getConfirmedPackages(packageNames: List, project: Project): Set {
- val confirmationEnabled = PropertiesComponent.getInstance()
- .getBoolean(InstallPackageQuickFix.CONFIRM_PACKAGE_INSTALLATION_PROPERTY, true)
-
- if (!confirmationEnabled || packageNames.isEmpty()) return packageNames.toSet()
-
- val dialog = PyChooseRequirementsDialog(project, packageNames) { it.presentableTextWithoutVersion }
-
- if (!dialog.showAndGet()) {
- PyPackagesUsageCollector.installAllCanceledEvent.log()
- return emptySet()
- }
-
- return dialog.markedElements.toSet()
-}
diff --git a/python/src/com/jetbrains/python/packaging/bridge/PythonPackageManagementServiceBridge.kt b/python/src/com/jetbrains/python/packaging/bridge/PythonPackageManagementServiceBridge.kt
index d179f84f48dd..46863b8fbc2b 100644
--- a/python/src/com/jetbrains/python/packaging/bridge/PythonPackageManagementServiceBridge.kt
+++ b/python/src/com/jetbrains/python/packaging/bridge/PythonPackageManagementServiceBridge.kt
@@ -18,11 +18,10 @@ import com.jetbrains.python.getOrThrow
import com.jetbrains.python.packaging.PyPackagingSettings
import com.jetbrains.python.packaging.common.PythonPackageDetails
import com.jetbrains.python.packaging.common.PythonSimplePackageDetails
-import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification
-import com.jetbrains.python.packaging.common.runPackagingOperationOrShowErrorDialog
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.management.packagesByRepository
import com.jetbrains.python.packaging.management.toInstallRequest
+import com.jetbrains.python.packaging.normalizePackageName
import com.jetbrains.python.packaging.repository.PyPIPackageRepository
import com.jetbrains.python.packaging.repository.PyPackageRepository
import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService
@@ -95,7 +94,7 @@ class PythonPackageManagementServiceBridge(project: Project, sdk: Sdk) : PyPacka
val specification = specForPackage(repoPackage.name, version, repository)
runningUnderOldUI = true
listener.operationStarted(specification.name)
- val result = manager.installPackage(specification.toInstallRequest(), emptyList(), withBackgroundProgress = true)
+ val result = manager.installPackage(specification.toInstallRequest(), emptyList())
val exception = if (result.isFailure) mutableListOf(result.exceptionOrNull() as ExecutionException) else null
listener.operationFinished(specification.name,
toErrorDescription(exception, mySdk, specification.name))
@@ -111,15 +110,12 @@ class PythonPackageManagementServiceBridge(project: Project, sdk: Sdk) : PyPacka
scope.launch(Dispatchers.IO + ModalityState.current().asContextElement()) {
try {
runningUnderOldUI = true
- val namesToDelete = installedPackages.map { it.name.lowercase() }
- manager
+ val namesToDelete = installedPackages.map { normalizePackageName(it.name) }
+ val pythonPackages = manager
.installedPackages
- .filter { it.name.lowercase() in namesToDelete }
- .forEach {
- runPackagingOperationOrShowErrorDialog(sdk, PyBundle.message("python.packaging.operation.failed.title")) {
- manager.uninstallPackage(it)
- }
- }
+ .filter { it.name in namesToDelete }
+ .map { it.name }.toTypedArray()
+ manager.uninstallPackage(*pythonPackages)
listener.operationFinished(namesToDelete.first(), null)
}
@@ -185,12 +181,13 @@ class PythonPackageManagementServiceBridge(project: Project, sdk: Sdk) : PyPacka
}
}
- private fun specForPackage(packageName: String, version: String? = null, repository: PyPackageRepository? = null): PythonRepositoryPackageSpecification {
- return when (repository) {
- null -> manager.createPackageSpecification(packageName, version)
- ?: throw IllegalArgumentException(PyBundle.message("python.packaging.error.package.is.not.listed.in.repositories", packageName))
- else -> repository.createPackageSpecification(packageName, version)
- }
+ private fun specForPackage(packageName: String, version: String? = null, repository: PyPackageRepository? = null) = if (repository != null) {
+ repository.findPackageSpecification(packageName, version)
+ ?: throw IllegalArgumentException(PyBundle.message("python.packaging.error.package.is.not.listed.in.repository", packageName, repository.name))
+ }
+ else {
+ manager.findPackageSpecification(packageName, version)
+ ?: throw IllegalArgumentException(PyBundle.message("python.packaging.error.package.is.not.listed.in.repositories", packageName))
}
override fun shouldFetchLatestVersionsForOnlyInstalledPackages(): Boolean = !(isConda && useConda)
diff --git a/python/src/com/jetbrains/python/packaging/common/packages.kt b/python/src/com/jetbrains/python/packaging/common/packages.kt
index 4c079e2d794c..960ccef50c00 100644
--- a/python/src/com/jetbrains/python/packaging/common/packages.kt
+++ b/python/src/com/jetbrains/python/packaging/common/packages.kt
@@ -47,6 +47,21 @@ open class PythonPackage(name: String, val version: String, val isEditableMode:
class PythonOutdatedPackage(name: String, version: String, val latestVersion: String)
: PythonPackage(name, version, false) {
override fun toString(): String = "PythonOutdatedPackage(name='$name', version='$version', latestVersion='$latestVersion')"
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+ if (!super.equals(other)) return false
+
+ other as PythonOutdatedPackage
+
+ return latestVersion == other.latestVersion
+ }
+
+ override fun hashCode(): Int {
+ var result = super.hashCode()
+ result = 31 * result + latestVersion.hashCode()
+ return result
+ }
}
interface PythonPackageDetails {
@@ -57,7 +72,7 @@ interface PythonPackageDetails {
val description: String?
val descriptionContentType: String?
val documentationUrl: String?
- fun toPackageSpecification(version: String? = null): PythonRepositoryPackageSpecification = repository.createPackageSpecification(name, version)
+ fun toPackageSpecification(version: String? = null): PythonRepositoryPackageSpecification? = repository.findPackageSpecification(name, version)
}
data class PythonSimplePackageDetails(
@@ -76,15 +91,15 @@ data class PythonSimplePackageDetails(
/**
* Please use one of the following factory methods:
*
- * 1) [com.jetbrains.python.packaging.management.PythonPackageManager.createPackageSpecification]
+ * 1) [com.jetbrains.python.packaging.management.PythonPackageManager.findPackageSpecification]
* Use this method if you have a package manager instance or an SDK.
* It will locate the appropriate repository for the package from the available options.
*
- * 2) [PyPackageRepository.createPackageSpecification]
+ * 2) [PyPackageRepository.findPackageSpecification]
* Use this method if you already have a specific repository instance and want to look up within it only.
* The following well-known public repositories are also available for direct access:
- * - PyPI (https://pypi.org): [com.jetbrains.python.packaging.repository.PyPIPackageRepository.createPackageSpecification]
- * - Conda: [com.jetbrains.python.packaging.conda.CondaPackageRepository.createPackageSpecification]
+ * - PyPI (https://pypi.org): [com.jetbrains.python.packaging.repository.PyPIPackageRepository.findPackageSpecification]
+ * - Conda: [com.jetbrains.python.packaging.conda.CondaPackageRepository.findPackageSpecification]
*/
data class PythonRepositoryPackageSpecification(
val repository: PyPackageRepository,
diff --git a/python/src/com/jetbrains/python/packaging/common/util.kt b/python/src/com/jetbrains/python/packaging/common/util.kt
index 7d5cd572f46f..9f1d4845c25b 100644
--- a/python/src/com/jetbrains/python/packaging/common/util.kt
+++ b/python/src/com/jetbrains/python/packaging/common/util.kt
@@ -49,9 +49,7 @@ internal suspend fun runPackagingOperationOrShowErrorDialog(
operation: suspend (() -> Result),
): Result {
try {
- val result = withContext(Dispatchers.IO) {
- operation.invoke()
- }
+ val result = operation()
result.exceptionOrNull()?.let { throw it }
return result
}
diff --git a/python/src/com/jetbrains/python/packaging/conda/CompositePythonPackageManager.kt b/python/src/com/jetbrains/python/packaging/conda/CompositePythonPackageManager.kt
deleted file mode 100644
index c7da373784d4..000000000000
--- a/python/src/com/jetbrains/python/packaging/conda/CompositePythonPackageManager.kt
+++ /dev/null
@@ -1,131 +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.packaging.conda
-
-import com.intellij.openapi.project.Project
-import com.intellij.openapi.projectRoots.Sdk
-import com.jetbrains.python.PyBundle
-import com.jetbrains.python.packaging.common.PythonOutdatedPackage
-import com.jetbrains.python.packaging.common.PythonPackage
-import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification
-import com.jetbrains.python.packaging.management.PythonPackageInstallRequest
-import com.jetbrains.python.packaging.management.PythonPackageManager
-import com.jetbrains.python.packaging.management.PythonRepositoryManager
-
-internal class CompositePythonPackageManager(
- project: Project,
- sdk: Sdk,
- private val managers: List,
-) : PythonPackageManager(project, sdk) {
-
- @Volatile
- override var installedPackages: List = emptyList()
- override var dependencies: List = emptyList()
-
- override var repositoryManager: PythonRepositoryManager =
- CompositePythonRepositoryManager(project, managers.map { it.repositoryManager })
-
- private val managerNames = managers.joinToString { it.javaClass.simpleName }
-
- override suspend fun loadOutdatedPackagesCommand(): Result> {
- val results = mutableListOf()
- val exceptions = mutableListOf()
-
- for (manager in managers) {
- manager.loadOutdatedPackagesCommand()
- .onSuccess { results.addAll(it) }
- .onFailure { exceptions.add(it) }
- }
-
- return if (results.isNotEmpty()) {
- Result.success(results)
- }
- else {
- Result.failure(createCompositeException(
- exceptions,
- PyBundle.message("python.packaging.composite.list.outdated.packages.error", managerNames)
- ))
- }
- }
-
- override suspend fun installPackageCommand(installRequest: PythonPackageInstallRequest, options: List): Result {
- return processPackageOperation(
- errorMessageKey = "python.packaging.composite.install.package.error",
- operation = { it.installPackageCommand(installRequest, options) },
- name = installRequest.title
- )
- }
-
- override suspend fun updatePackageCommand(specification: PythonRepositoryPackageSpecification): Result {
- return processPackageOperation(
- errorMessageKey = "python.packaging.composite.update.package.error",
- operation = { it.updatePackageCommand(specification) },
- name = specification.name
- )
- }
-
- override suspend fun uninstallPackageCommand(pkg: PythonPackage): Result {
- return processPackageOperation(
- errorMessageKey = "python.packaging.composite.uninstall.package.error",
- operation = { it.uninstallPackageCommand(pkg) },
- name = pkg.name
- )
- }
-
- override suspend fun reloadPackagesCommand(): Result> {
- val results = mutableListOf()
- val exceptions = mutableListOf()
-
- for (manager in managers) {
- manager.reloadPackages()
- .onSuccess { results.addAll(it) }
- .onFailure { exceptions.add(it) }
- }
-
- return if (results.isNotEmpty()) {
- Result.success(results)
- }
- else {
- Result.failure(createCompositeException(
- exceptions,
- PyBundle.message("python.packaging.composite.reload.packages.error", managerNames)
- ))
- }
- }
-
- override suspend fun reloadDependencies(): List = dependencies
-
- override fun listDependencies(): List = dependencies
-
- private suspend fun processPackageOperation(
- errorMessageKey: String,
- operation: suspend (PythonPackageManager) -> Result<*>,
- name: String,
- ): Result {
- val exceptions = mutableListOf()
- for (manager in managers) {
- operation(manager)
- .onSuccess { return Result.success(Unit) }
- .onFailure { exceptions.add(it) }
- }
-
- return Result.failure(createCompositeException(
- exceptions,
- PyBundle.message(errorMessageKey, name, managerNames)
- ))
- }
-
- fun createCompositeException(
- exceptions: List,
- defaultMessage: String,
- ): RuntimeException {
- if (exceptions.isEmpty()) {
- return RuntimeException(defaultMessage)
- }
-
- val concatenatedMessages = exceptions.joinToString(separator = "; ") { exception ->
- exception.message ?: exception.toString()
- }
-
- return RuntimeException(concatenatedMessages)
- }
-}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/conda/CondaPackageManager.kt b/python/src/com/jetbrains/python/packaging/conda/CondaPackageManagerEngine.kt
similarity index 62%
rename from python/src/com/jetbrains/python/packaging/conda/CondaPackageManager.kt
rename to python/src/com/jetbrains/python/packaging/conda/CondaPackageManagerEngine.kt
index 6b8be72b4167..30b3b5b700d4 100644
--- a/python/src/com/jetbrains/python/packaging/conda/CondaPackageManager.kt
+++ b/python/src/com/jetbrains/python/packaging/conda/CondaPackageManagerEngine.kt
@@ -1,4 +1,4 @@
-// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+// 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.conda
import com.intellij.execution.ExecutionException
@@ -15,43 +15,33 @@ import com.jetbrains.python.packaging.common.PythonOutdatedPackage
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification
import com.jetbrains.python.packaging.management.PythonPackageInstallRequest
-import com.jetbrains.python.packaging.management.PythonPackageManager
+import com.jetbrains.python.packaging.management.PythonPackageManagerEngine
import com.jetbrains.python.packaging.management.PythonPackageManagerRunner
-import com.jetbrains.python.packaging.management.PythonRepositoryManager
import com.jetbrains.python.sdk.flavors.conda.PyCondaFlavorData
import com.jetbrains.python.sdk.getOrCreateAdditionalData
import com.jetbrains.python.sdk.targetEnvConfiguration
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
-import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nls
-private fun PythonPackageInstallRequest.buildInstallationArguments(): Result> = when (this) {
- is PythonPackageInstallRequest.ByRepositoryPythonPackageSpecification -> {
- when (this.specification.repository) {
- is CondaPackageRepository -> Result.success(listOf(this.specification.nameWithVersionSpec))
- else -> Result.failure(UnsupportedOperationException("CondaManager installer supports only conda repositories, got ${this.specification.repository} "))
- }
- }
- is PythonPackageInstallRequest.AllRequirements -> Result.success(emptyList())
- is PythonPackageInstallRequest.ByLocation -> Result.failure(UnsupportedOperationException("CondaManager does not support installing from location uri"))
-}
-
-@ApiStatus.Experimental
-class CondaPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(project, sdk) {
- @Volatile
- override var installedPackages: List = emptyList()
- override var dependencies: List = emptyList()
- override val repositoryManager: PythonRepositoryManager = CondaRepositoryManger(project, sdk)
-
+internal class CondaPackageManagerEngine(
+ private val project: Project,
+ private val sdk: Sdk,
+) : PythonPackageManagerEngine {
override suspend fun loadOutdatedPackagesCommand(): Result> {
- return runCatching {
- val jsonResult = runConda("update", listOf("--dry-run", "--all", "--json"),
- message("conda.packaging.list.outdated.progress"),
- withBackgroundProgress = false)
+ val jsonResult = try {
+ runConda("update", listOf("--dry-run", "--all", "--json"),
+ message("conda.packaging.list.outdated.progress"),
+ withBackgroundProgress = false)
+ }
+ catch (ex: ExecutionException) {
+ return Result.failure(ex)
+ }
+ val parsed = withContext(Dispatchers.Default) {
CondaParseUtils.parseOutdatedOutputs(jsonResult)
}
+ return Result.success(parsed)
}
override suspend fun installPackageCommand(installRequest: PythonPackageInstallRequest, options: List): Result {
@@ -66,25 +56,33 @@ class CondaPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(pro
}
}
- override suspend fun updatePackageCommand(specification: PythonRepositoryPackageSpecification): Result =
+ override suspend fun updatePackageCommand(vararg specifications: PythonRepositoryPackageSpecification): Result =
try {
- runConda("update", listOf(specification.name, "-y"), message("conda.packaging.update.progress", specification.name))
+ val packages = specifications.map { it.name }
+ runConda("install", packages + listOf("-y"),
+ message("conda.packaging.update.progress", packages.joinToString(", ")))
Result.success(Unit)
}
catch (ex: ExecutionException) {
Result.failure(ex)
}
- override suspend fun uninstallPackageCommand(pkg: PythonPackage): Result =
- try {
- runConda("uninstall", listOf(pkg.name, "-y"), message("conda.packaging.uninstall.progress", pkg.name))
+ override suspend fun uninstallPackageCommand(vararg pythonPackages: String): Result {
+ if (pythonPackages.isEmpty())
+ return Result.success(Unit)
+
+ return try {
+ runConda("uninstall", pythonPackages.toList() + listOf("-y"),
+ message("conda.packaging.uninstall.progress", pythonPackages.joinToString(", ")),
+ withBackgroundProgress = false)
Result.success(Unit)
}
catch (ex: ExecutionException) {
Result.failure(ex)
}
+ }
- override suspend fun reloadPackagesCommand(): Result> =
+ override suspend fun loadPackagesCommand(): Result> =
try {
val output = runConda("list", emptyList(), message("conda.packaging.list.progress"))
Result.success(parseCondaPackageList(output))
@@ -93,14 +91,10 @@ class CondaPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(pro
Result.failure(ex)
}
- override suspend fun reloadDependencies(): List = dependencies
-
- override fun listDependencies(): List = dependencies
-
private fun parseCondaPackageList(text: String): List {
return text.lineSequence()
.filterNot { it.startsWith("#") }
- .map { line -> line.split("\\s+".toRegex()) }
+ .map { line -> line.split(listLineParser) }
.filterNot { it.size < 2 }
//TODO: fix
.map { CondaPackage(it[0], it[1], editableMode = false, installedWithPip = (it.size >= 4 && it[3] == "pypi")) }
@@ -127,7 +121,7 @@ class CondaPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(pro
val commandLine = targetedCommandLine.collectCommandsSynchronously()
val commandLineString = StringUtil.join(commandLine, " ")
- val result = PythonPackageManagerRunner.runProcess(this@CondaPackageManager, process, commandLineString, text, withBackgroundProgress)
+ val result = PythonPackageManagerRunner.runProcess(project, process, commandLineString, text, withBackgroundProgress)
result.checkSuccess(thisLogger())
if (result.isTimeout) throw PyExecutionException(message("conda.packaging.exception.timeout"), operation, arguments, result)
@@ -137,4 +131,25 @@ class CondaPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(pro
else result.stdout
}
}
+
+ private fun PythonPackageInstallRequest.buildInstallationArguments(): Result> = when (this) {
+ is PythonPackageInstallRequest.AllRequirements -> Result.success(emptyList())
+ is PythonPackageInstallRequest.ByLocation -> Result.failure(UnsupportedOperationException("CondaManager does not support installing from location uri"))
+ is PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications -> {
+ val condaSpecs = specifications.filter { it.repository is CondaPackageRepository }
+ val specs = condaSpecs.map { it.nameWithVersionSpec }
+
+ //https://docs.conda.io/projects/conda/en/latest/user-guide/concepts/pkg-specs.html#package-match-specifications
+ //When using the command line, put double quotes around any package version specification that
+ // contains the space character or any of the following characters: <, >, *, or |.
+ //Ido not know why we need put single prefix quota but it does not work in EEL with suffix quota
+ val quoted = specs.map { "\"$it" }
+
+ Result.success(quoted)
+ }
+ }
+
+ companion object {
+ private val listLineParser = "\\s+".toRegex()
+ }
}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/conda/CondaPackageManagerProvider.kt b/python/src/com/jetbrains/python/packaging/conda/CondaPackageManagerProvider.kt
index 5f745895ecd0..92d3e992a4a5 100644
--- a/python/src/com/jetbrains/python/packaging/conda/CondaPackageManagerProvider.kt
+++ b/python/src/com/jetbrains/python/packaging/conda/CondaPackageManagerProvider.kt
@@ -3,10 +3,8 @@ package com.jetbrains.python.packaging.conda
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
-import com.intellij.openapi.util.registry.Registry
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.sdk.conda.isConda
import org.jetbrains.annotations.ApiStatus
@@ -16,10 +14,5 @@ class CondaPackageManagerProvider : PythonPackageManagerProvider {
if (sdk.isConda()) createCondaPackageManager(project, sdk) else null
private fun createCondaPackageManager(project: Project, sdk: Sdk): PythonPackageManager =
- if (Registry.`is`("python.packaging.conda.chain.installation")) {
- CompositePythonPackageManager(project, sdk, listOf(CondaPackageManager(project, sdk), PipPythonPackageManager(project, sdk)))
- }
- else {
- CondaPackageManager(project, sdk)
- }
+ CondaWithPipFallbackPackageManager(project, sdk)
}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/conda/CondaPackageRepository.kt b/python/src/com/jetbrains/python/packaging/conda/CondaPackageRepository.kt
new file mode 100644
index 000000000000..df889fd2a3cc
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/conda/CondaPackageRepository.kt
@@ -0,0 +1,44 @@
+// 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.conda
+
+import com.intellij.openapi.components.service
+import com.jetbrains.python.errorProcessing.MessageError
+import com.jetbrains.python.errorProcessing.PyResult
+import com.jetbrains.python.getOrNull
+import com.jetbrains.python.packaging.common.PythonPackageDetails
+import com.jetbrains.python.packaging.repository.PyPIPackageRepository
+import com.jetbrains.python.packaging.repository.PyPackageRepository
+import com.jetbrains.python.packaging.repository.buildPackageDetailsBySimpleDetailsProtocol
+import com.jetbrains.python.packaging.requirement.PyRequirementVersionSpec
+
+object CondaPackageRepository : PyPackageRepository("Conda", null, null) {
+ override fun getPackages(): Set {
+ return service().packages
+ }
+
+ override fun buildPackageDetails(packageName: String): PyResult {
+ val versions = getVersionForPackage(packageName)
+ ?: return PyResult.failure(MessageError("No conda package versions in cache"))
+
+ val pypiSimpleDetails = PyPIPackageRepository.buildPackageDetailsBySimpleDetailsProtocol(packageName).getOrNull()
+ val condaDetails = pypiSimpleDetails.toCondaPackageDetails(packageName, versions)
+
+ return PyResult.success(condaDetails)
+ }
+
+ override fun hasPackage(
+ packageName: String,
+ versionSpecs: PyRequirementVersionSpec?,
+ ): Boolean {
+ val availableVersions = getVersionForPackage(packageName) ?: return false
+
+ if (versionSpecs == null)
+ return true
+
+ return availableVersions.any {
+ versionSpecs.matches(it)
+ }
+ }
+
+ private fun getVersionForPackage(packageName: String) = service()[packageName]
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/conda/CondaPackagingToolwindowActionProvider.kt b/python/src/com/jetbrains/python/packaging/conda/CondaPackagingToolwindowActionProvider.kt
index f5c6400646aa..7ea9180527b6 100644
--- a/python/src/com/jetbrains/python/packaging/conda/CondaPackagingToolwindowActionProvider.kt
+++ b/python/src/com/jetbrains/python/packaging/conda/CondaPackagingToolwindowActionProvider.kt
@@ -12,7 +12,7 @@ import org.jetbrains.annotations.ApiStatus
@ApiStatus.Experimental
class CondaPackagingToolwindowActionProvider : PythonPackagingToolwindowActionProvider {
override fun getInstallActions(details: PythonPackageDetails, packageManager: PythonPackageManager): List? {
- if (packageManager is CondaPackageManager) {
+ if (packageManager is CondaWithPipFallbackPackageManager) {
return if (details is CondaPackageDetails) {
listOf(SimplePythonPackageInstallAction(PyBundle.message("conda.packaging.button.install.with.conda"), packageManager.project))
} else listOf(SimplePythonPackageInstallAction(PyBundle.message("conda.packaging.button.install.with.pip"), packageManager.project))
diff --git a/python/src/com/jetbrains/python/packaging/conda/CondaRepositoryManger.kt b/python/src/com/jetbrains/python/packaging/conda/CondaRepositoryManger.kt
index 9eff27d685aa..cc1532c669a7 100644
--- a/python/src/com/jetbrains/python/packaging/conda/CondaRepositoryManger.kt
+++ b/python/src/com/jetbrains/python/packaging/conda/CondaRepositoryManger.kt
@@ -15,7 +15,7 @@ internal class CondaRepositoryManger(
) : PipBasedRepositoryManager() {
override val repositories: List
- get() = listOf(CondaPackageRepository) + super.repositories
+ get() = listOf(CondaPackageRepository)
override suspend fun refreshCaches() {
super.refreshCaches()
diff --git a/python/src/com/jetbrains/python/packaging/conda/CondaWithPipFallbackPackageManager.kt b/python/src/com/jetbrains/python/packaging/conda/CondaWithPipFallbackPackageManager.kt
new file mode 100644
index 000000000000..978a04a18d1f
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/conda/CondaWithPipFallbackPackageManager.kt
@@ -0,0 +1,112 @@
+// 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.conda
+
+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.PythonRepositoryPackageSpecification
+import com.jetbrains.python.packaging.management.PythonPackageInstallRequest
+import com.jetbrains.python.packaging.management.PythonPackageManager
+import com.jetbrains.python.packaging.management.PythonPackageManagerEngine
+import com.jetbrains.python.packaging.management.PythonRepositoryManager
+import com.jetbrains.python.packaging.pip.PipPackageManagerEngine
+import com.jetbrains.python.packaging.pip.PipRepositoryManager
+import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
+
+class CondaWithPipFallbackPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(project, sdk) {
+ private val condaPackageEngine = CondaPackageManagerEngine(project, sdk)
+ private val condaRepositoryManger = CondaRepositoryManger(project, sdk)
+ private val pipRepositoryManger = PipRepositoryManager(project)
+ private val pipPackageEngine = PipPackageManagerEngine(project, sdk)
+
+ override var repositoryManager: PythonRepositoryManager = CompositePythonRepositoryManager(project,
+ listOf(condaRepositoryManger, pipRepositoryManger))
+
+ override suspend fun loadPackagesCommand(): Result> = condaPackageEngine.loadPackagesCommand()
+
+ override suspend fun loadOutdatedPackagesCommand(): Result> = coroutineScope {
+ val condaOutdated = async {
+ condaPackageEngine.loadOutdatedPackagesCommand()
+ }
+ val pipOutdated = async {
+ pipPackageEngine.loadOutdatedPackagesCommand()
+ }
+
+ val condaPackages = condaOutdated.await().getOrElse {
+ return@coroutineScope Result.failure(it)
+ }
+ val pipPackages = pipOutdated.await().getOrElse {
+ return@coroutineScope Result.failure(it)
+ }
+
+ val onlyPipOutdated = pipPackages.filter { outdatedPackage ->
+ val pythonPackage = installedPackages.firstOrNull { it.name == outdatedPackage.name } ?: return@filter false
+ pythonPackage !is CondaPackage || pythonPackage.installedWithPip
+ }
+ Result.success(condaPackages + onlyPipOutdated)
+ }
+
+ override suspend fun installPackageCommand(installRequest: PythonPackageInstallRequest, options: List): Result = when (installRequest) {
+ PythonPackageInstallRequest.AllRequirements -> condaPackageEngine.installPackageCommand(installRequest, options)
+ is PythonPackageInstallRequest.ByLocation -> pipPackageEngine.installPackageCommand(installRequest, options)
+ is PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications -> installSeveralPackages(installRequest.specifications, options)
+ }
+
+ private suspend fun installSeveralPackages(specifications: List, options: List) =
+ performOperation(specifications) { manager, specs ->
+ val managerRequest = PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications(specs)
+ manager.installPackageCommand(managerRequest, options)
+ }
+
+ override suspend fun updatePackageCommand(vararg specifications: PythonRepositoryPackageSpecification): Result =
+ performOperation(specifications.toList()) { manager, specs ->
+ manager.updatePackageCommand(*specs.toTypedArray())
+ }
+
+ override suspend fun uninstallPackageCommand(vararg pythonPackages: String): Result {
+ val installedPackagesForRemove = installedPackages.mapNotNull {
+ it.takeIf { it.name in pythonPackages }
+ }
+ val condaPackages = installedPackagesForRemove.filter { it is CondaPackage && !it.installedWithPip }
+ val pipPackages = installedPackagesForRemove - condaPackages
+
+ if (condaPackages.isNotEmpty()) {
+ condaPackageEngine.uninstallPackageCommand(*condaPackages.map { it.name }.toTypedArray()).onFailure {
+ return Result.failure(it)
+ }
+ }
+ if (pipPackages.isNotEmpty()) {
+ pipPackageEngine.uninstallPackageCommand(*pipPackages.map { it.name }.toTypedArray()).onFailure {
+ return Result.failure(it)
+ }
+ }
+
+ return Result.success(Unit)
+ }
+
+ private suspend fun performOperation(
+ specifications: List,
+ operation: suspend (PythonPackageManagerEngine, List) -> Result,
+ ): Result {
+ val engineWithSpecs = splitByEngine(specifications)
+ engineWithSpecs.forEach { (manager, specs) ->
+ if (specs.isEmpty())
+ return@forEach
+ operation(manager, specs).onFailure {
+ return Result.failure(it)
+ }
+ }
+
+ return Result.success(Unit)
+ }
+
+ private fun splitByEngine(specifications: List) = specifications.groupBy { specification ->
+ val repository = specification.repository
+ if (repository is CondaPackageRepository)
+ condaPackageEngine
+ else
+ pipPackageEngine
+ }
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/conda/common.kt b/python/src/com/jetbrains/python/packaging/conda/common.kt
index b82d24caf1cf..61d6f020cb8a 100644
--- a/python/src/com/jetbrains/python/packaging/conda/common.kt
+++ b/python/src/com/jetbrains/python/packaging/conda/common.kt
@@ -1,16 +1,10 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.packaging.conda
-import com.intellij.openapi.components.service
import com.jetbrains.python.PyBundle
-import com.jetbrains.python.errorProcessing.MessageError
-import com.jetbrains.python.errorProcessing.PyResult
-import com.jetbrains.python.getOrNull
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.common.PythonPackageDetails
-import com.jetbrains.python.packaging.repository.PyPIPackageRepository
import com.jetbrains.python.packaging.repository.PyPackageRepository
-import com.jetbrains.python.packaging.repository.buildPackageDetailsBySimpleDetailsProtocol
class CondaPackage(
name: String, version: String,
@@ -48,20 +42,3 @@ fun PythonPackageDetails?.toCondaPackageDetails(packageName: String, availableVe
documentationUrl = documentationUrl
)
}
-
-
-object CondaPackageRepository : PyPackageRepository("Conda", null, null) {
- override fun getPackages(): Set {
- return service().packages
- }
-
- override fun buildPackageDetails(packageName: String): PyResult {
- val versions = service()[packageName]
- ?: return PyResult.failure(MessageError("No conda package versions in cache"))
-
- val pypiSimpleDetails = PyPIPackageRepository.buildPackageDetailsBySimpleDetailsProtocol(packageName).getOrNull()
- val condaDetails = pypiSimpleDetails.toCondaPackageDetails(packageName, versions)
-
- return PyResult.success(condaDetails)
- }
-}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/management/PythonPackageInstallRequest.kt b/python/src/com/jetbrains/python/packaging/management/PythonPackageInstallRequest.kt
new file mode 100644
index 000000000000..b4d4bc86dcb2
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/management/PythonPackageInstallRequest.kt
@@ -0,0 +1,19 @@
+// 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.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification
+import org.jetbrains.annotations.ApiStatus
+import java.net.URI
+
+@ApiStatus.Internal
+sealed class PythonPackageInstallRequest(val title: String) {
+ data object AllRequirements : PythonPackageInstallRequest("All Requirements")
+ data class ByLocation(val location: URI) : PythonPackageInstallRequest(location.toString())
+ data class ByRepositoryPythonPackageSpecifications(val specifications: List) : PythonPackageInstallRequest(
+ specifications.joinToString(", ") { it.nameWithVersionSpec })
+}
+
+@ApiStatus.Internal
+fun PythonRepositoryPackageSpecification.toInstallRequest(): PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications {
+ return PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications(listOf(this))
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/management/PythonPackageInstaller.kt b/python/src/com/jetbrains/python/packaging/management/PythonPackageInstaller.kt
index 2402c49f581e..a421359a6f65 100644
--- a/python/src/com/jetbrains/python/packaging/management/PythonPackageInstaller.kt
+++ b/python/src/com/jetbrains/python/packaging/management/PythonPackageInstaller.kt
@@ -49,19 +49,21 @@ class PythonPackagesInstaller {
indicator.isIndeterminate = true
val installAllRequirementsSpecification = PythonPackageInstallRequest.AllRequirements
- return manager.installPackage(installAllRequirementsSpecification, emptyList(), withBackgroundProgress = true).map { }
+ return manager.installPackage(installAllRequirementsSpecification, emptyList()).map { }
}
- private suspend fun installWithRequirements(
+ suspend fun installWithRequirements(
manager: PythonPackageManager,
- requirements: List,
+ requirements: Collection,
extraArgs: List,
): Result {
- val installRequests = requirements.map { requirement ->
- manager.createPackageSpecificationWithSpec(requirement.name, versionSpec = requirement.versionSpecs.firstOrNull())
+ manager.waitForInit()
+ val packageSpecifications = requirements.map { requirement ->
+ manager.findPackageSpecificationWithVersionSpec(requirement.name, versionSpec = requirement.versionSpecs.firstOrNull())
?: return Result.failure(ExecutionException(PyBundle.message("python.packaging.error.package.is.not.listed.in.repositories", requirement.name)))
- }.map { it.toInstallRequest() }
- return manager.installPackages(installRequests, extraArgs, withBackgroundProgress = true).map { }
+ }
+ val request = PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications(packageSpecifications)
+ return manager.installPackage(request, extraArgs).map { }
}
@JvmStatic
@@ -82,13 +84,7 @@ class PythonPackagesInstaller {
suspend fun uninstallPackagesProcess(manager: PythonPackageManager, packages: List): Result {
- for (pkg in packages) {
- manager.uninstallPackage(pkg).onFailure {
- return Result.failure(it)
- }
- }
-
- return Result.success(Unit)
+ return manager.uninstallPackage(*packages.map { it.name }.toTypedArray()).map {}
}
private fun PyPackage.toPythonPackage(): PythonPackage = PythonPackage(this.name, this.version, false)
diff --git a/python/src/com/jetbrains/python/packaging/management/PythonPackageManager.kt b/python/src/com/jetbrains/python/packaging/management/PythonPackageManager.kt
index 534be1b9c9be..2b9eef9ecb7b 100644
--- a/python/src/com/jetbrains/python/packaging/management/PythonPackageManager.kt
+++ b/python/src/com/jetbrains/python/packaging/management/PythonPackageManager.kt
@@ -1,4 +1,6 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+@file:Suppress("DEPRECATION", "removal")
+
package com.jetbrains.python.packaging.management
import com.intellij.openapi.application.ApplicationManager
@@ -6,45 +8,49 @@ import com.intellij.openapi.application.edtWriteAction
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.project.Project
+import com.intellij.openapi.project.modules
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.roots.OrderRootType
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.SystemInfoRt
import com.intellij.openapi.vfs.VfsUtil
-import com.intellij.platform.ide.progress.withBackgroundProgress
-import com.intellij.platform.util.progress.reportSequentialProgress
import com.intellij.util.messages.Topic
import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.PyPackageManager
-import com.jetbrains.python.packaging.common.*
+import com.jetbrains.python.packaging.PythonDependenciesExtractor
+import com.jetbrains.python.packaging.common.PythonOutdatedPackage
+import com.jetbrains.python.packaging.common.PythonPackage
+import com.jetbrains.python.packaging.common.PythonPackageManagementListener
+import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification
+import com.jetbrains.python.packaging.pyRequirementVersionSpec
import com.jetbrains.python.packaging.requirement.PyRequirementRelation
import com.jetbrains.python.packaging.requirement.PyRequirementVersionSpec
import com.jetbrains.python.sdk.PythonSdkCoroutineService
import com.jetbrains.python.sdk.PythonSdkUpdater
-import kotlinx.coroutines.Dispatchers
+import com.jetbrains.python.sdk.pythonSdk
+import kotlinx.coroutines.future.asCompletableFuture
+import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import org.jetbrains.annotations.ApiStatus
-import java.net.URI
+import org.jetbrains.annotations.Nls
-@ApiStatus.Internal
-
-sealed class PythonPackageInstallRequest(val title: String) {
- data object AllRequirements : PythonPackageInstallRequest("All Requirements")
- data class ByLocation(val location: URI) : PythonPackageInstallRequest(location.toString())
- data class ByRepositoryPythonPackageSpecification(val specification: PythonRepositoryPackageSpecification) : PythonPackageInstallRequest(specification.nameWithVersionSpec)
-}
-
-@ApiStatus.Internal
-
-fun PythonRepositoryPackageSpecification.toInstallRequest(): PythonPackageInstallRequest.ByRepositoryPythonPackageSpecification {
- return PythonPackageInstallRequest.ByRepositoryPythonPackageSpecification(this)
-}
@ApiStatus.Experimental
abstract class PythonPackageManager(val project: Project, val sdk: Sdk) {
- abstract var installedPackages: List
- @get:ApiStatus.Internal @set:ApiStatus.Internal
- protected abstract var dependencies: List
+ private val lazyInitialization by lazy {
+ service().cs.launch {
+ repositoryManager.initCaches()
+ reloadPackages()
+ }.asCompletableFuture()
+ }
+
+ @get:ApiStatus.Internal
+ @set:ApiStatus.Internal
+ protected open var dependencies: List = emptyList()
+
+ @Volatile
+ open var installedPackages: List = emptyList()
+ protected set
@ApiStatus.Internal
@Volatile
@@ -53,21 +59,48 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) {
abstract val repositoryManager: PythonRepositoryManager
- suspend fun installPackage(
- installRequest: PythonPackageInstallRequest,
- options: List = emptyList(),
- withBackgroundProgress: Boolean,
- ): Result> = installPackages(listOf(installRequest), options, withBackgroundProgress)
- suspend fun installPackages(
- installRequests: List,
- options: List = emptyList(),
- withBackgroundProgress: Boolean,
- ): Result> {
- return if (withBackgroundProgress)
- installPackagesWithBackgroundProcess(installRequests, options)
- else
- installPackagesSilently(installRequests, options)
+ fun isPackageInstalled(pkg: PythonPackage): Boolean = installedPackages.any { it.name == pkg.name }
+
+ fun findPackageSpecificationWithVersionSpec(
+ packageName: String,
+ versionSpec: PyRequirementVersionSpec? = null,
+ ): PythonRepositoryPackageSpecification? {
+ return repositoryManager.repositories.firstNotNullOfOrNull {
+ it.findPackageSpecificationWithSpec(packageName, versionSpec)
+ }
+ }
+
+ fun findPackageSpecification(
+ packageName: String,
+ version: String? = null,
+ relation: PyRequirementRelation = PyRequirementRelation.EQ,
+ ): PythonRepositoryPackageSpecification? {
+ val versionSpec = version?.let { pyRequirementVersionSpec(relation, version) }
+ return findPackageSpecificationWithVersionSpec(packageName, versionSpec)
+ }
+
+ suspend fun installPackage(installRequest: PythonPackageInstallRequest, options: List = emptyList()): Result> {
+ val progressTitle = when (installRequest) {
+ is PythonPackageInstallRequest.AllRequirements -> PyBundle.message("python.packaging.installing.requirements")
+ is PythonPackageInstallRequest.ByLocation -> PyBundle.message("python.packaging.installing.package", installRequest.title)
+ is PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications -> if (installRequest.specifications.size == 1) {
+ PyBundle.message("python.packaging.installing.package", installRequest.specifications.first().name)
+ }
+ else {
+ PyBundle.message("python.packaging.installing.packages")
+ }
+ }
+
+ executeCommand(progressTitle) {
+ waitForInit()
+ installPackageCommand(installRequest, options)
+ }.onFailure {
+ return Result.failure(it)
+ }
+
+
+ return reloadPackages()
}
@ApiStatus.Internal
@@ -79,40 +112,53 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) {
PyBundle.message("python.packaging.updating.package", packages.first().name)
}
- return withBackgroundProgress(project = project, progressTitle, cancellable = true) {
- reportSequentialProgress(packages.size) { reporter ->
- packages.forEach { specification ->
- reporter.itemStep(PyBundle.message("python.packaging.updating.package", specification.name))
- runCatching {
- updatePackageCommand(specification)
- }.onFailure {
- return@withBackgroundProgress Result.failure(it)
- }
- }
- }
-
- reloadPackages()
+ executeCommand(progressTitle) {
+ waitForInit()
+ updatePackageCommand(*packages)
+ }.onFailure {
+ return Result.failure(it)
}
+
+ return reloadPackages()
}
- suspend fun uninstallPackage(pkg: PythonPackage): Result> {
- thisLogger().info("Uninstall package $pkg: start")
- uninstallPackageCommand(pkg).onFailure { return Result.failure(it) }
- thisLogger().info("Uninstall package $pkg: finished")
- refreshPaths()
+
+ suspend fun uninstallPackage(vararg packages: String): Result> {
+ if (packages.isEmpty()) {
+ return Result.success(installedPackages)
+ }
+
+ val progressTitle = if (packages.size > 1) {
+ PyBundle.message("python.packaging.uninstall.packages")
+ }
+ else {
+ PyBundle.message("python.packaging.uninstall.package", packages.first())
+ }
+
+ executeCommand(progressTitle) {
+ waitForInit()
+ uninstallPackageCommand(*packages)
+ }.onFailure {
+ return Result.failure(it)
+ }
+
return reloadPackages()
}
open suspend fun reloadPackages(): Result> {
- thisLogger().info("Reload packages: start")
- val packages = reloadPackagesCommand().getOrElse {
+ val progressTitle = PyBundle.message("python.toolwindow.packages.update.packages")
+ val packages = executeCommand(progressTitle) {
+ loadPackagesCommand()
+ }.getOrElse {
outdatedPackages = emptyMap()
installedPackages = emptyList()
return Result.failure(it)
}
- thisLogger().info("Reload packages: finish")
+ if (packages == installedPackages)
+ return Result.success(packages)
installedPackages = packages
+
ApplicationManager.getApplication().messageBus.apply {
syncPublisher(PACKAGE_MANAGEMENT_TOPIC).packagesChanged(sdk)
syncPublisher(PyPackageManager.PACKAGE_MANAGER_TOPIC).packagesRefreshed(sdk)
@@ -123,125 +169,85 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) {
}
}
+ refreshPaths()
return Result.success(packages)
}
- fun packageExists(pkg: PythonPackage): Boolean = installedPackages.any { it.name.equals(pkg.name, ignoreCase = true) }
-
- @ApiStatus.Internal
- abstract suspend fun installPackageCommand(installRequest: PythonPackageInstallRequest, options: List): Result
- @ApiStatus.Internal
- abstract suspend fun updatePackageCommand(specification: PythonRepositoryPackageSpecification): Result
- @ApiStatus.Internal
- abstract suspend fun uninstallPackageCommand(pkg: PythonPackage): Result
- @ApiStatus.Internal
- abstract suspend fun reloadPackagesCommand(): Result>
- @ApiStatus.Internal
- abstract suspend fun loadOutdatedPackagesCommand(): Result>
- @ApiStatus.Internal
- abstract suspend fun reloadDependencies(): List
- @ApiStatus.Internal
- abstract fun listDependencies(): List
-
- internal suspend fun refreshPaths() {
- edtWriteAction {
- // Background refreshing breaks structured concurrency: there is a some activity in background that locks files.
- // Temporary folders can't be deleted on Windows due to that.
- // That breaks tests.
- // This code should be deleted, but disabled temporary to fix tests
- if (!(ApplicationManager.getApplication().isUnitTestMode && SystemInfoRt.isWindows)) {
- VfsUtil.markDirtyAndRefresh(true, true, true, *sdk.rootProvider.getFiles(OrderRootType.CLASSES))
- }
- PythonSdkUpdater.scheduleUpdate(sdk, project)
- }
- }
-
- private suspend fun installPackagesWithBackgroundProcess(packages: List, options: List = emptyList()): Result> {
- val progressTitle = if (packages.size > 1) {
- PyBundle.message("python.packaging.installing.packages")
- }
- else {
- PyBundle.message("python.packaging.installing.package", packages.first().title)
- }
-
- return withBackgroundProgress(project = project, progressTitle, cancellable = true) {
- reportSequentialProgress(packages.size) { reporter ->
- packages.forEach { specification ->
- reporter.itemStep(PyBundle.message("python.packaging.installing.package", specification.title))
- runCatching {
- installPackageInternal(specification, options)
- }.onFailure { return@withBackgroundProgress Result.failure(it) }
- }
- }
-
- refreshPaths()
- reloadPackages()
- }
- }
-
- private suspend fun installPackagesSilently(
- specifications: List,
- options: List,
- ): Result> {
- specifications.forEach { specification ->
- val installResult = installPackageInternal(specification, options)
- installResult.onFailure {
- return Result.failure(it)
- }
- }
-
- refreshPaths()
- return reloadPackages()
- }
-
-
- private suspend fun installPackageInternal(specification: PythonPackageInstallRequest, options: List): Result {
- val result = runPackagingOperationOrShowErrorDialog(sdk, PyBundle.message("python.new.project.install.failed.title", specification.title), specification.title) {
- installPackageCommand(specification, options)
- }
- result.onFailure {
- thisLogger().info("install $specification: error. Output: \n${it.stackTraceToString()}")
- return Result.failure(it)
- }
- result.onSuccess {
- thisLogger().info("install $specification: success")
- return Result.success(Unit)
- }
- return result
- }
-
- @ApiStatus.Internal
- suspend fun reloadOutdatedPackages() {
+ private suspend fun reloadOutdatedPackages() {
if (installedPackages.isEmpty()) {
outdatedPackages = emptyMap()
return
}
val loadedPackages = loadOutdatedPackagesCommand().getOrElse {
- thisLogger().warn("Failed to load outdated packages", it)
+ thisLogger().error("Failed to load outdated packages", it)
emptyList()
}
+
val packageMap = loadedPackages.associateBy { it.name }
+ if (outdatedPackages == packageMap)
+ return
+
outdatedPackages = packageMap
ApplicationManager.getApplication().messageBus.apply {
syncPublisher(PACKAGE_MANAGEMENT_TOPIC).outdatedPackagesChanged(sdk)
}
}
- fun createPackageSpecificationWithSpec(packageName: String, versionSpec: PyRequirementVersionSpec? = null): PythonRepositoryPackageSpecification? {
- return repositoryManager.findPackageRepository(packageName)?.createPackageSpecificationWithSpec(packageName, versionSpec)
+
+ private suspend fun refreshPaths() = edtWriteAction {
+ // Background refreshing breaks structured concurrency: there is a some activity in background that locks files.
+ // Temporary folders can't be deleted on Windows due to that.
+ // That breaks tests.
+ // This code should be deleted, but disabled temporary to fix tests
+ if (!(ApplicationManager.getApplication().isUnitTestMode && SystemInfoRt.isWindows)) {
+ VfsUtil.markDirtyAndRefresh(true, true, true, *sdk.rootProvider.getFiles(OrderRootType.CLASSES))
+ }
+ PythonSdkUpdater.scheduleUpdate(sdk, project)
}
- fun createPackageSpecification(packageName: String, version: String? = null, relation: PyRequirementRelation = PyRequirementRelation.EQ): PythonRepositoryPackageSpecification? {
- return repositoryManager.findPackageRepository(packageName)?.createPackageSpecification(packageName, version, relation)
+ private suspend fun executeCommand(
+ progressTitle: @Nls String,
+ operation: suspend (() -> Result),
+ ): Result = PythonPackageManagerUIHelpers.runPackagingOperationBackground(project, progressTitle) {
+ operation()
}
+ suspend fun waitForInit() {
+ lazyInitialization.await()
+ }
+
+
+ @ApiStatus.Internal
+ protected abstract suspend fun installPackageCommand(installRequest: PythonPackageInstallRequest, options: List): Result
+
+ @ApiStatus.Internal
+ protected abstract suspend fun updatePackageCommand(vararg specifications: PythonRepositoryPackageSpecification): Result
+
+ @ApiStatus.Internal
+ protected abstract suspend fun uninstallPackageCommand(vararg pythonPackages: String): Result
+
+ @ApiStatus.Internal
+ protected abstract suspend fun loadPackagesCommand(): Result>
+
+ @ApiStatus.Internal
+ protected abstract suspend fun loadOutdatedPackagesCommand(): Result>
+
+ @ApiStatus.Internal
+ suspend fun reloadDependencies(): List {
+ val dependenciesExtractor = PythonDependenciesExtractor.forSdk(sdk) ?: return emptyList()
+ val targetModule = project.modules.find { it.pythonSdk == sdk } ?: return emptyList()
+ dependencies = dependenciesExtractor.extract(targetModule)
+ return dependencies
+ }
+
+ fun listDependencies(): List = dependencies
+
companion object {
fun forSdk(project: Project, sdk: Sdk): PythonPackageManager {
val pythonPackageManagerService = project.service()
val manager = pythonPackageManagerService.forSdk(project, sdk)
- pythonPackageManagerService.getServiceScope().launch(Dispatchers.IO) {
- manager.repositoryManager.initCaches()
- }
+ //We need to call the lazy load if not inited
+ manager.lazyInitialization
return manager
}
diff --git a/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerEngine.kt b/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerEngine.kt
new file mode 100644
index 000000000000..8d8a2678ee28
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerEngine.kt
@@ -0,0 +1,25 @@
+// 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.jetbrains.python.packaging.common.PythonOutdatedPackage
+import com.jetbrains.python.packaging.common.PythonPackage
+import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification
+import org.jetbrains.annotations.ApiStatus
+
+@ApiStatus.Internal
+interface PythonPackageManagerEngine {
+ @ApiStatus.Internal
+ suspend fun installPackageCommand(installRequest: PythonPackageInstallRequest, options: List): Result
+
+ @ApiStatus.Internal
+ suspend fun updatePackageCommand(vararg specifications: PythonRepositoryPackageSpecification): Result
+
+ @ApiStatus.Internal
+ suspend fun uninstallPackageCommand(vararg pythonPackages: String): Result
+
+ @ApiStatus.Internal
+ suspend fun loadPackagesCommand(): Result>
+
+ @ApiStatus.Internal
+ suspend fun loadOutdatedPackagesCommand(): Result>
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerExt.kt b/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerExt.kt
index 311a41572014..6869a6c9362c 100644
--- a/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerExt.kt
+++ b/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerExt.kt
@@ -11,17 +11,13 @@ import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.progress.EmptyProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.blockingContext
-import com.intellij.openapi.progress.runBlockingCancellable
import com.intellij.openapi.project.guessProjectDir
import com.intellij.openapi.util.registry.Registry
-import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.intellij.util.net.HttpConfigurable
-import com.jetbrains.python.PyBundle
import com.jetbrains.python.PySdkBundle
import com.jetbrains.python.PythonHelper
import com.jetbrains.python.packaging.PyExecutionException
-import com.jetbrains.python.packaging.common.runPackagingOperationOrShowErrorDialog
-import com.jetbrains.python.packaging.normalizePackageName
+import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.repository.PyPackageRepository
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
import com.jetbrains.python.run.buildTargetedCommandLine
@@ -33,14 +29,13 @@ import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nls
import kotlin.math.min
+
@ApiStatus.Internal
-@RequiresBackgroundThread
-fun PythonPackageManager.launchReload() {
- runBlockingCancellable {
- runPackagingOperationOrShowErrorDialog(sdk, PyBundle.message("python.packaging.operation.failed.title")) {
- reloadPackages()
- }
- }
+fun PythonPackageManager.hasInstalledPackage(packageName: String, version: String? = null): Boolean =
+ getPackage(packageName, version) != null
+
+fun PythonPackageManager.getPackage(packageName: String, version: String? = null): PythonPackage? {
+ return installedPackages.firstOrNull { it.name == packageName && (version == null || version == it.version) }
}
@ApiStatus.Internal
@@ -103,7 +98,7 @@ suspend fun PythonPackageManager.runPackagingTool(
thisLogger().debug("Running python packaging tool. Operation: $operation")
- val result = PythonPackageManagerRunner.runProcess(this@runPackagingTool, process, commandLineString, text, withBackgroundProgress)
+ val result = PythonPackageManagerRunner.runProcess(project, process, commandLineString, text, withBackgroundProgress)
if (result.isCancelled) throw RunCanceledByUserException()
result.checkSuccess(thisLogger())
val exitCode = result.exitCode
@@ -141,20 +136,4 @@ fun PythonRepositoryManager.packagesByRepository(): Sequence
- val contains = packageNames.contains(normalizePackageName) ||
- packageNames.any { normalizePackageName(it) == normalizePackageName }
- if (contains) repository else null
- }
- return repository ?: repositories.firstOrNull()
}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerRunner.kt b/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerRunner.kt
index 748616b39483..f710deac7056 100644
--- a/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerRunner.kt
+++ b/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerRunner.kt
@@ -5,6 +5,7 @@ import com.intellij.execution.process.ProcessOutput
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.blockingContext
import com.intellij.openapi.progress.coroutineToIndicator
+import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsContexts
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.jetbrains.python.packaging.conda.PyPackageProcessHandler
@@ -15,7 +16,7 @@ import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
internal object PythonPackageManagerRunner {
suspend fun runProcess(
- packageManager: PythonPackageManager,
+ project: Project,
process: Process,
command: String,
@NlsContexts.ProgressTitle backgroundProgressTitle: String,
@@ -26,7 +27,7 @@ internal object PythonPackageManagerRunner {
}
val processOutput = if (withBackgroundProgress)
- withBackgroundProgress(packageManager.project, backgroundProgressTitle, true) {
+ withBackgroundProgress(project, backgroundProgressTitle, true) {
runProcessInternal(handler)
}
else {
diff --git a/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerUIHelpers.kt b/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerUIHelpers.kt
new file mode 100644
index 000000000000..17d82b815421
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerUIHelpers.kt
@@ -0,0 +1,34 @@
+// 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.openapi.project.Project
+import com.intellij.openapi.util.NlsContexts.ProgressTitle
+import com.intellij.platform.ide.progress.withBackgroundProgress
+import com.jetbrains.python.packaging.PyExecutionException
+import com.jetbrains.python.util.ShowingMessageErrorSync
+
+internal object PythonPackageManagerUIHelpers {
+ suspend fun runPackagingOperationBackground(
+ project: Project,
+ @ProgressTitle title: String,
+ operation: suspend (() -> Result),
+ ): Result = withBackgroundProgress(project = project, title, cancellable = true) {
+ runPackagingOperationOrShowError {
+ operation()
+ }
+ }
+
+ private suspend fun runPackagingOperationOrShowError(
+ operation: suspend (() -> Result),
+ ): Result {
+ try {
+ val result = operation()
+ result.exceptionOrNull()?.let { throw it }
+ return result
+ }
+ catch (ex: PyExecutionException) {
+ ShowingMessageErrorSync.emit(ex.pyError)
+ return Result.failure(ex)
+ }
+ }
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/pip/PipPackageManagerEngine.kt b/python/src/com/jetbrains/python/packaging/pip/PipPackageManagerEngine.kt
new file mode 100644
index 000000000000..52ca72a8030c
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/pip/PipPackageManagerEngine.kt
@@ -0,0 +1,227 @@
+// 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.pip
+
+import com.intellij.execution.ExecutionException
+import com.intellij.execution.RunCanceledByUserException
+import com.intellij.execution.target.TargetProgressIndicator
+import com.intellij.execution.target.local.LocalTargetEnvironmentRequest
+import com.intellij.execution.target.value.targetPath
+import com.intellij.openapi.diagnostic.thisLogger
+import com.intellij.openapi.progress.EmptyProgressIndicator
+import com.intellij.openapi.progress.ProgressManager
+import com.intellij.openapi.progress.blockingContext
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.project.guessProjectDir
+import com.intellij.openapi.projectRoots.Sdk
+import com.intellij.openapi.util.registry.Registry
+import com.intellij.util.net.HttpConfigurable
+import com.jetbrains.python.PyBundle
+import com.jetbrains.python.PySdkBundle
+import com.jetbrains.python.PythonHelper
+import com.jetbrains.python.packaging.PyExecutionException
+import com.jetbrains.python.packaging.PyPIPackageUtil
+import com.jetbrains.python.packaging.common.PythonOutdatedPackage
+import com.jetbrains.python.packaging.common.PythonPackage
+import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification
+import com.jetbrains.python.packaging.management.PythonPackageInstallRequest
+import com.jetbrains.python.packaging.management.PythonPackageManager
+import com.jetbrains.python.packaging.management.PythonPackageManagerEngine
+import com.jetbrains.python.packaging.management.PythonPackageManagerRunner
+import com.jetbrains.python.packaging.repository.PyPIPackageRepository
+import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
+import com.jetbrains.python.run.buildTargetedCommandLine
+import com.jetbrains.python.run.ensureProjectSdkAndModuleDirsAreOnTarget
+import com.jetbrains.python.run.prepareHelperScriptExecution
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.annotations.Nls
+import kotlin.math.min
+
+class PipPackageManagerEngine(
+ private val project: Project,
+ private val sdk: Sdk,
+) : PythonPackageManagerEngine {
+ override suspend fun installPackageCommand(installRequest: PythonPackageInstallRequest, options: List): Result {
+ val manager = PythonPackageManager.forSdk(project, sdk)
+ PipManagementInstaller(sdk, manager).installManagementIfNeeded()
+ try {
+ runPackagingTool("install", installRequest.indexUrlIfApplicable() + options,
+ PyBundle.message("python.packaging.install.progress", installRequest.title),
+ withBackgroundProgress = false)
+ }
+ catch (ex: ExecutionException) {
+ return Result.failure(ex)
+ }
+
+ return Result.success(Unit)
+ }
+
+ override suspend fun loadOutdatedPackagesCommand(): Result> = runCatching {
+ val output = runPackagingTool("list_outdated", listOf(), PyBundle.message("python.packaging.list.outdated.progress"),
+ withBackgroundProgress = false)
+ output.lineSequence()
+ .drop(2) // skip header and separator line
+ .filter { it.isNotBlank() }
+ .map {
+ val line = it.split("\t", " ").filter { it.isNotBlank() }
+ PythonOutdatedPackage(line[0], line[1], latestVersion = line[2])
+ }
+ .toList()
+ }
+
+ override suspend fun updatePackageCommand(vararg specifications: PythonRepositoryPackageSpecification): Result {
+ val indexUrlIfApplicable = specifications.firstNotNullOfOrNull { it.indexUrlIfApplicable() } ?: emptyList()
+ try {
+ val packages = specifications.map { it.name }
+ runPackagingTool("install", packages + listOf("--upgrade") + indexUrlIfApplicable,
+ PyBundle.message("python.packaging.update.progress", packages.joinToString(", ")))
+ }
+ catch (ex: ExecutionException) {
+ return Result.failure(ex)
+ }
+
+ return Result.success(Unit)
+ }
+
+ override suspend fun uninstallPackageCommand(vararg pythonPackages: String): Result {
+ try {
+ runPackagingTool("uninstall", pythonPackages.toList(),
+ PyBundle.message("python.packaging.uninstall.progress", pythonPackages.joinToString(", ")),
+ withBackgroundProgress = false)
+ }
+ catch (ex: ExecutionException) {
+ return Result.failure(ex)
+ }
+
+ return Result.success(Unit)
+ }
+
+ override suspend fun loadPackagesCommand(): Result> {
+ val output = runPackagingTool("list", emptyList(), PyBundle.message("python.packaging.list.progress"))
+ val packages = output.lineSequence()
+ .filter { it.isNotBlank() }
+ .map {
+ val line = it.split("\t")
+ PythonPackage(line[0], line[1], isEditableMode = false)
+ }
+ .sortedWith(compareBy(PythonPackage::name))
+ .toList()
+
+ return Result.success(packages)
+ }
+
+ @ApiStatus.Internal
+ suspend fun runPackagingTool(
+ operation: String, arguments: List, @Nls text: String,
+ withBackgroundProgress: Boolean = true,
+ ): String = withContext(Dispatchers.IO) {
+ // todo[akniazev]: check for package management tools
+ val helpersAwareTargetRequest = PythonInterpreterTargetEnvironmentFactory.findPythonTargetInterpreter(sdk, project)
+ val targetEnvironmentRequest = helpersAwareTargetRequest.targetEnvironmentRequest
+ val pythonExecution = blockingContext { prepareHelperScriptExecution(PythonHelper.PACKAGING_TOOL, helpersAwareTargetRequest) }
+
+ if (targetEnvironmentRequest is LocalTargetEnvironmentRequest) {
+ if (Registry.`is`("python.packaging.tool.use.project.location.as.working.dir")) {
+ project.guessProjectDir()?.toNioPath()?.let {
+ pythonExecution.workingDir = targetPath(it)
+ }
+ }
+ }
+ else {
+ if (Registry.`is`("python.packaging.tool.upload.project")) {
+ project.guessProjectDir()?.toNioPath()?.let {
+ targetEnvironmentRequest.ensureProjectSdkAndModuleDirsAreOnTarget(project)
+ pythonExecution.workingDir = targetPath(it)
+ }
+ }
+ }
+
+ pythonExecution.addParameter(operation)
+ if (operation == "install") {
+ proxyString?.let {
+ pythonExecution.addParameter("--proxy")
+ pythonExecution.addParameter(it)
+ }
+ }
+
+ arguments.forEach(pythonExecution::addParameter)
+
+ // // todo[akniazev]: add extra args to package specification
+
+ val targetProgressIndicator = TargetProgressIndicator.EMPTY
+ val targetEnvironment = targetEnvironmentRequest.prepareEnvironment(targetProgressIndicator)
+
+ targetEnvironment.uploadVolumes.entries.forEach { (_, value) ->
+ value.upload(".", targetProgressIndicator)
+ }
+
+ val targetedCommandLine = pythonExecution.buildTargetedCommandLine(targetEnvironment, sdk, emptyList())
+
+ val indicator = ProgressManager.getInstance().progressIndicator ?: EmptyProgressIndicator()
+ // from targets package manager
+ // TODO [targets] Apply environment variables: setPythonUnbuffered(...), setPythonDontWriteBytecode(...), resetHomePathChanges(...)
+ // TODO [targets] Apply flavor from PythonSdkFlavor.getFlavor(mySdk)
+ // TODO [targets] check askForSudo
+
+ val process = blockingContext { targetEnvironment.createProcess(targetedCommandLine, indicator) }
+
+ val commandLine = targetedCommandLine.collectCommandsSynchronously()
+ val commandLineString = commandLine.joinToString(" ")
+
+ thisLogger().debug("Running python packaging tool. Operation: $operation")
+
+ val result = PythonPackageManagerRunner.runProcess(
+ project,
+ process,
+ commandLineString,
+ text,
+ withBackgroundProgress)
+ if (result.isCancelled)
+ throw RunCanceledByUserException()
+
+ result.checkSuccess(thisLogger())
+ val exitCode = result.exitCode
+ val helperPath = commandLine.firstOrNull() ?: ""
+ val args: List = commandLine.subList(min(1, commandLine.size), commandLine.size)
+ if (exitCode != 0) {
+ val message = if (result.stdout.isBlank() && result.stderr.isBlank()) PySdkBundle.message(
+ "python.conda.permission.denied")
+ else PySdkBundle.message("python.sdk.packaging.non.zero.exit.code", exitCode)
+ throw PyExecutionException(message, helperPath, args, result)
+ }
+
+ if (result.isTimeout) {
+ throw PyExecutionException.createForTimeout(PySdkBundle.message("python.sdk.packaging.timed.out"), helperPath, args)
+ }
+
+ return@withContext result.stdout
+ }
+
+ private val proxyString: String?
+ get() {
+ val settings = HttpConfigurable.getInstance()
+ if (settings != null && settings.USE_HTTP_PROXY) {
+ val credentials = if (settings.PROXY_AUTHENTICATION) "${settings.proxyLogin}:${settings.plainProxyPassword}@" else ""
+ return "http://$credentials${settings.PROXY_HOST}:${settings.PROXY_PORT}"
+ }
+ return null
+ }
+
+ private fun PythonPackageInstallRequest.indexUrlIfApplicable(): List = when (this) {
+ is PythonPackageInstallRequest.ByLocation -> listOf(location.toString())
+ is PythonPackageInstallRequest.AllRequirements -> emptyList()
+ is PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications -> {
+ val pypiSpecs = specifications.filter { it.repository is PyPIPackageRepository }
+ val index = pypiSpecs.firstNotNullOfOrNull { spec -> spec.indexUrlIfApplicable() } ?: emptyList()
+ pypiSpecs.map { it.nameWithVersionSpec } + index
+ }
+ }
+
+ private fun PythonRepositoryPackageSpecification.indexUrlIfApplicable(): List? {
+ val urlForInstallation = repository.urlForInstallation?.toString() ?: return null
+ if (urlForInstallation == PyPIPackageUtil.PYPI_LIST_URL)
+ return null
+ return listOf("--index-url", urlForInstallation)
+ }
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/pip/PipPythonPackageManager.kt b/python/src/com/jetbrains/python/packaging/pip/PipPythonPackageManager.kt
index 7c1a8320ac3e..8196728a91d3 100644
--- a/python/src/com/jetbrains/python/packaging/pip/PipPythonPackageManager.kt
+++ b/python/src/com/jetbrains/python/packaging/pip/PipPythonPackageManager.kt
@@ -7,113 +7,38 @@ import com.intellij.execution.process.CapturingProcessHandler
import com.intellij.execution.process.ProcessOutput
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
-import com.jetbrains.python.PyBundle
import com.jetbrains.python.PythonHelpersLocator.Companion.findPathInHelpers
-import com.jetbrains.python.packaging.PyPIPackageUtil
import com.jetbrains.python.packaging.PyPackageUtil
-import com.jetbrains.python.packaging.common.PythonOutdatedPackage
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification
import com.jetbrains.python.packaging.management.PythonPackageInstallRequest
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.management.PythonRepositoryManager
-import com.jetbrains.python.packaging.management.runPackagingTool
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.statistics.version
import org.jetbrains.annotations.ApiStatus
import java.nio.file.Path
-private fun PythonRepositoryPackageSpecification.buildPipInstallArguments(): List = buildList {
- add(nameWithVersionSpec)
- val urlForInstallation = repository.urlForInstallation.toString()
- urlForInstallation.takeIf { it.isNotBlank() && it != PyPIPackageUtil.PYPI_LIST_URL }?.let {
- add("--index-url")
- add(it)
- }
-}
-
-private fun PythonPackageInstallRequest.buildPipInstallArguments(): List = when (this) {
- is PythonPackageInstallRequest.ByRepositoryPythonPackageSpecification -> this.specification.buildPipInstallArguments()
- is PythonPackageInstallRequest.ByLocation -> listOf(location.toString())
- is PythonPackageInstallRequest.AllRequirements -> emptyList()
-}
-
@ApiStatus.Experimental
@ApiStatus.Internal
open class PipPythonPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(project, sdk) {
- @Volatile
- override var installedPackages: List = emptyList()
- override var dependencies: List = emptyList()
override val repositoryManager: PythonRepositoryManager = PipRepositoryManager(project)
+ private val engine = PipPackageManagerEngine(project, sdk)
- override suspend fun installPackageCommand(installRequest: PythonPackageInstallRequest, options: List): Result {
- PipManagementInstaller(sdk, this).installManagementIfNeeded()
- try {
- runPackagingTool("install", installRequest.buildPipInstallArguments() + options, PyBundle.message("python.packaging.install.progress", installRequest.title), withBackgroundProgress = false)
- }
- catch (ex: ExecutionException) {
- return Result.failure(ex)
- }
+ override suspend fun loadOutdatedPackagesCommand() = engine.loadOutdatedPackagesCommand()
- return Result.success(Unit)
- }
+ override suspend fun installPackageCommand(
+ installRequest: PythonPackageInstallRequest,
+ options: List,
+ ) = engine.installPackageCommand(installRequest, options)
- override suspend fun loadOutdatedPackagesCommand(): Result> = runCatching {
- val output = runPackagingTool("list_outdated", listOf(), PyBundle.message("python.packaging.list.outdated.progress"))
- output.lineSequence()
- .drop(2) // skip header and separator line
- .filter { it.isNotBlank() }
- .map {
- val line = it.split("\t", " ").filter { it.isNotBlank() }
- PythonOutdatedPackage(line[0], line[1], latestVersion = line[2])
- }
- .toList()
- }
+ override suspend fun updatePackageCommand(
+ vararg specifications: PythonRepositoryPackageSpecification,
+ ): Result = engine.updatePackageCommand(*specifications)
- override suspend fun updatePackageCommand(specification: PythonRepositoryPackageSpecification): Result {
- try {
- runPackagingTool("install", listOf("--upgrade") + specification.buildPipInstallArguments(), PyBundle.message("python.packaging.update.progress", specification.name))
- }
- catch (ex: ExecutionException) {
- return Result.failure(ex)
- }
+ override suspend fun uninstallPackageCommand(vararg pythonPackages: String) = engine.uninstallPackageCommand(*pythonPackages)
- return Result.success(Unit)
- }
-
- override suspend fun uninstallPackageCommand(pkg: PythonPackage): Result {
- try {
- runPackagingTool("uninstall", listOf(pkg.name), PyBundle.message("python.packaging.uninstall.progress", pkg.name))
- }
- catch (ex: ExecutionException) {
- return Result.failure(ex)
- }
-
- return Result.success(Unit)
- }
-
- override suspend fun reloadPackagesCommand(): Result> {
- try {
- val output = runPackagingTool("list", emptyList(), PyBundle.message("python.packaging.list.progress"))
- val packages = output.lineSequence()
- .filter { it.isNotBlank() }
- .map {
- val line = it.split("\t")
- PythonPackage(line[0], line[1], isEditableMode = false)
- }
- .sortedWith(compareBy(PythonPackage::name))
- .toList()
-
- return Result.success(packages)
- }
- catch (ex: ExecutionException) {
- return Result.failure(ex)
- }
- }
-
- override suspend fun reloadDependencies(): List = dependencies
-
- override fun listDependencies(): List = dependencies
+ override suspend fun loadPackagesCommand(): Result> = engine.loadPackagesCommand()
}
@ApiStatus.Internal
@@ -128,7 +53,7 @@ class PipManagementInstaller(private val sdk: Sdk, private val manager: PythonPa
private fun performManagementInstallation(): Boolean = installManagement()
fun hasManagement(): Boolean =
- languageLevel < LanguageLevel.PYTHON27 || (manager.packageExists(PIP_PACKAGE) && hasSetuptools())
+ languageLevel < LanguageLevel.PYTHON27 || (manager.isPackageInstalled(PIP_PACKAGE) && hasSetuptools())
private fun installManagement(): Boolean =
installWheelIfMissing(::hasPip, WheelFiles.PIP_WHEEL_NAME) &&
@@ -159,12 +84,12 @@ class PipManagementInstaller(private val sdk: Sdk, private val manager: PythonPa
throw ExecutionException(ex.message, ex)
}
- private fun hasPip(): Boolean = manager.packageExists(PIP_PACKAGE)
+ private fun hasPip(): Boolean = manager.isPackageInstalled(PIP_PACKAGE)
private fun hasSetuptools(): Boolean =
languageLevel >= LanguageLevel.PYTHON312 ||
- manager.packageExists(SETUPTOOLS_PACKAGE) ||
- manager.packageExists(DISTRIBUTE_PACKAGE)
+ manager.isPackageInstalled(SETUPTOOLS_PACKAGE) ||
+ manager.isPackageInstalled(DISTRIBUTE_PACKAGE)
private fun buildCommandArguments(wheelPath: Path, vararg additionalArgs: String): List =
listOfNotNull(sdk.homePath.toString(), wheelPath.toString(), "install") + additionalArgs
diff --git a/python/src/com/jetbrains/python/packaging/repository/PyPackageRepository.kt b/python/src/com/jetbrains/python/packaging/repository/PyPackageRepository.kt
index f563a17f069a..69f1ed32291a 100644
--- a/python/src/com/jetbrains/python/packaging/repository/PyPackageRepository.kt
+++ b/python/src/com/jetbrains/python/packaging/repository/PyPackageRepository.kt
@@ -19,6 +19,8 @@ import com.jetbrains.python.packaging.cache.PythonSimpleRepositoryCache
import com.jetbrains.python.packaging.common.PythonPackageDetails
import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification
import com.jetbrains.python.packaging.common.PythonSimplePackageDetails
+import com.jetbrains.python.packaging.normalizePackageName
+import com.jetbrains.python.packaging.pyRequirementVersionSpec
import com.jetbrains.python.packaging.requirement.PyRequirementRelation
import com.jetbrains.python.packaging.requirement.PyRequirementVersionSpec
import org.apache.http.client.utils.URIBuilder
@@ -63,7 +65,6 @@ internal fun PyPackageRepository.buildPackageDetailsBySimpleDetailsProtocol(pack
@ApiStatus.Internal
open class PyPackageRepository() {
-
var name: String = ""
internal set
var repositoryUrl: String? = null
@@ -82,12 +83,13 @@ open class PyPackageRepository() {
private val serviceName: String
get() = generateServiceName(SUBSYSTEM_NAME, name)
- val urlForInstallation: URL
- get() = repositoryUrl?.let { baseUrl ->
+ val urlForInstallation: URL?
+ get() {
+ val baseUrl = repositoryUrl ?: return null
val userLogin = login.takeUnless { it.isNullOrBlank() } ?: return URL(baseUrl)
val userPassword = getPassword() ?: return URL(baseUrl)
- buildAuthenticatedUrl(baseUrl, userLogin, userPassword)
- } ?: URL("")
+ return buildAuthenticatedUrl(baseUrl, userLogin, userPassword)
+ }
private fun buildAuthenticatedUrl(baseUrl: String, login: String, password: String): URL =
URIBuilder(baseUrl).setUserInfo(login, password).build().toURL()
@@ -108,19 +110,27 @@ open class PyPackageRepository() {
PasswordSafe.instance.set(attributes, null)
}
- fun createPackageSpecificationWithSpec(
- packageName: String,
- versionSpecs: PyRequirementVersionSpec? = null,
- ): PythonRepositoryPackageSpecification = PythonRepositoryPackageSpecification(this, packageName, versionSpecs)
+ fun findPackageSpecificationWithSpec(packageName: String, versionSpecs: PyRequirementVersionSpec? = null): PythonRepositoryPackageSpecification? =
+ if (hasPackage(packageName, versionSpecs))
+ PythonRepositoryPackageSpecification(this, packageName, versionSpecs)
+ else
+ null
- fun createPackageSpecification(
+ fun findPackageSpecification(
packageName: String,
version: String? = null,
relation: PyRequirementRelation = PyRequirementRelation.EQ,
- ): PythonRepositoryPackageSpecification = PythonRepositoryPackageSpecification(this, packageName, version, relation)
+ ): PythonRepositoryPackageSpecification? {
+ val versionSpec = version?.let { pyRequirementVersionSpec(relation, version) }
+ return findPackageSpecificationWithSpec(packageName, versionSpec)
+ }
- open fun getPackages(): Set =
- service()[this] ?: emptySet()
+
+ protected open fun hasPackage(packageName: String, versionSpecs: PyRequirementVersionSpec?): Boolean = getPackages().any {
+ normalizePackageName(it) == packageName
+ }
+
+ open fun getPackages(): Set = service()[this] ?: emptySet()
open fun buildPackageDetails(packageName: String): PyResult {
return buildPackageDetailsBySimpleDetailsProtocol(packageName)
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowService.kt b/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowService.kt
index 87aaaf551eb8..1752e3000e48 100644
--- a/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowService.kt
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowService.kt
@@ -26,7 +26,6 @@ import com.jetbrains.python.packaging.*
import com.jetbrains.python.packaging.common.PythonPackageDetails
import com.jetbrains.python.packaging.common.PythonPackageManagementListener
import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification
-import com.jetbrains.python.packaging.common.runPackagingOperationOrShowErrorDialog
import com.jetbrains.python.packaging.conda.CondaPackage
import com.jetbrains.python.packaging.management.PythonPackageInstallRequest
import com.jetbrains.python.packaging.management.PythonPackageManager
@@ -65,15 +64,20 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
subscribeToChanges()
}
- suspend fun detailsForPackage(selectedPackage: DisplayablePackage): PythonPackageDetails = withContext(Dispatchers.IO) {
+ suspend fun detailsForPackage(selectedPackage: DisplayablePackage): PythonPackageDetails? = withContext(Dispatchers.IO) {
PythonPackagesToolwindowStatisticsCollector.requestDetailsEvent.log(project)
val spec = when (selectedPackage) {
- is InstalledPackage -> manager.createPackageSpecification(selectedPackage.name)
- is InstallablePackage -> selectedPackage.repository.createPackageSpecification(selectedPackage.name)
- is ExpandResultNode -> selectedPackage.repository.createPackageSpecification(selectedPackage.name)
+ is InstalledPackage -> manager.findPackageSpecification(selectedPackage.name)
+ is InstallablePackage -> selectedPackage.repository.findPackageSpecification(selectedPackage.name)
+ is ExpandResultNode -> selectedPackage.repository.findPackageSpecification(selectedPackage.name)
+ else -> error("Invalidate package spec ${selectedPackage::class.java.name}")
}
- spec?.let { manager.repositoryManager.getPackageDetails(it).getOrThrow() } ?: error("Invalid package specification")
+ if (spec == null) {
+ return@withContext null
+ }
+
+ spec.let { manager.repositoryManager.getPackageDetails(it).getOrThrow() } ?: error("Invalid package specification")
}
@@ -116,32 +120,32 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
suspend fun installPackage(installRequest: PythonPackageInstallRequest, options: List = emptyList()) {
PythonPackagesToolwindowStatisticsCollector.installPackageEvent.log(project)
- val result = manager.installPackage(installRequest, options, withBackgroundProgress = true)
+ val result = manager.installPackage(installRequest, options)
if (result.isSuccess) {
handleActionCompleted(message("python.packaging.notification.installed", installRequest.title))
}
}
- suspend fun deletePackage(selectedPackage: InstalledPackage) {
+ suspend fun deletePackage(vararg selectedPackages: InstalledPackage) {
PythonPackagesToolwindowStatisticsCollector.uninstallPackageEvent.log(project)
- val result = runPackagingOperationOrShowErrorDialog(manager.sdk, message("python.packaging.operation.failed.title")) {
- manager.uninstallPackage(selectedPackage.instance)
- }
-
+ val result = manager.uninstallPackage(*selectedPackages.map { it.instance.name }.toTypedArray())
if (result.isSuccess) {
- handleActionCompleted(message("python.packaging.notification.deleted", selectedPackage.name))
+ handleActionCompleted(message("python.packaging.notification.deleted", selectedPackages.joinToString(", ") { it.name }))
}
}
- suspend fun updatePackage(specification: PythonRepositoryPackageSpecification) {
- val result = runPackagingOperationOrShowErrorDialog(manager.sdk, message("python.packaging.notification.update.failed", specification.name), specification.name) {
- manager.updatePackages(specification)
- }
+ suspend fun updatePackage(vararg specifications: PythonRepositoryPackageSpecification) {
+ val result = manager.updatePackages(*specifications)
- if (result.isSuccess) {
- val version = specification.versionSpec?.version
- handleActionCompleted(message("python.packaging.notification.updated", specification.name, version))
+ if (!result.isSuccess) return
+ val singlePackage = specifications.singleOrNull()
+ if (singlePackage != null) {
+ val version = singlePackage.versionSpec?.version
+ handleActionCompleted(message("python.packaging.notification.updated", singlePackage.name, version))
+ }
+ else {
+ handleActionCompleted(message("python.packaging.notification.all.updated"))
}
}
@@ -165,11 +169,6 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
}
manager = PythonPackageManager.forSdk(project, sdk)
- manager.repositoryManager.initCaches()
- runPackagingOperationOrShowErrorDialog(sdk, message("python.packaging.operation.failed.title")) {
- manager.reloadPackages()
- }
-
withContext(Dispatchers.EDT) {
toolWindowPanel?.contentVisible = currentSdk != null
if (currentSdk == null || currentSdk != previousSdk) {
@@ -219,7 +218,7 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
suspend fun refreshInstalledPackages() {
val packages = manager.installedPackages.map {
- val spec = manager.createPackageSpecification(it.name, it.version)
+ val spec = manager.findPackageSpecification(it.name, it.version)
val repository = spec?.repository
val nextVersionRaw = manager.outdatedPackages[it.name]?.latestVersion
val nextVersion = nextVersionRaw?.let { PyPackageVersionNormalizer.normalize(it) }
@@ -275,9 +274,7 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
serviceScope.launch(Dispatchers.IO) {
withBackgroundProgress(project, message("python.packaging.loading.packages.progress.text"), cancellable = false) {
reportRawProgress {
- runPackagingOperationOrShowErrorDialog(manager.sdk, message("python.packaging.operation.failed.title")) {
- manager.reloadPackages()
- }
+ manager.reloadPackages()
refreshInstalledPackages()
manager.repositoryManager.refreshCaches()
}
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/actions/ChangeVersionPackageAction.kt b/python/src/com/jetbrains/python/packaging/toolwindow/actions/ChangeVersionPackageAction.kt
index a8083e395b7a..a3a096517922 100644
--- a/python/src/com/jetbrains/python/packaging/toolwindow/actions/ChangeVersionPackageAction.kt
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/actions/ChangeVersionPackageAction.kt
@@ -23,7 +23,7 @@ internal class ChangeVersionPackageAction : DumbAwareAction() {
val pkg = e.selectedPackage as? InstalledPackage ?: return
val service = PyPackagingToolWindowService.getInstance(project)
PyPackageCoroutine.getIoScope(project).launch {
- val details = service.detailsForPackage(pkg)
+ val details = service.detailsForPackage(pkg) ?: return@launch
withContext(Dispatchers.EDT) {
PyPackagesUiComponents.createAvailableVersionsPopup(pkg, details, project).show(
RelativePoint(e.inputEvent as MouseEvent))
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/actions/DeletePackageAction.kt b/python/src/com/jetbrains/python/packaging/toolwindow/actions/DeletePackageAction.kt
index 42da7b219e05..9970e290ff78 100644
--- a/python/src/com/jetbrains/python/packaging/toolwindow/actions/DeletePackageAction.kt
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/actions/DeletePackageAction.kt
@@ -7,7 +7,6 @@ import com.intellij.openapi.components.service
import com.intellij.openapi.project.DumbAwareAction
import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService
import com.jetbrains.python.packaging.toolwindow.model.InstalledPackage
-import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents.selectedPackage
import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents.selectedPackages
import com.jetbrains.python.packaging.utils.PyPackageCoroutine
import kotlinx.coroutines.Dispatchers
@@ -15,21 +14,9 @@ import kotlinx.coroutines.Dispatchers
internal class DeletePackageAction : DumbAwareAction() {
override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return
- val selectedPackages = e.selectedPackages.filterIsInstance()
- if (selectedPackages.size > 1) {
- PyPackageCoroutine.launch(project, Dispatchers.IO) {
- selectedPackages.forEach { pkg ->
- project.service().deletePackage(pkg)
- }
- }
- return
- }
-
- val pkg = e.selectedPackage as? InstalledPackage ?: return
-
- val service = project.service()
+ val selectedPackages = e.selectedPackages.filterIsInstance().toTypedArray()
PyPackageCoroutine.launch(project, Dispatchers.IO) {
- service.deletePackage(pkg)
+ project.service().deletePackage(*selectedPackages)
}
}
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/actions/InstallPackageAction.kt b/python/src/com/jetbrains/python/packaging/toolwindow/actions/InstallPackageAction.kt
index 5fa247a42160..12dfa0baee82 100644
--- a/python/src/com/jetbrains/python/packaging/toolwindow/actions/InstallPackageAction.kt
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/actions/InstallPackageAction.kt
@@ -6,7 +6,7 @@ import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.components.service
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.ui.awt.RelativePoint
-import com.jetbrains.python.packaging.management.toInstallRequest
+import com.jetbrains.python.packaging.management.PythonPackageInstallRequest
import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService
import com.jetbrains.python.packaging.toolwindow.model.InstallablePackage
import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents
@@ -22,11 +22,12 @@ internal class InstallPackageAction : DumbAwareAction() {
val project = e.project ?: return
val selectedPackages = e.selectedPackages.filterIsInstance()
if (selectedPackages.size > 1) {
- PyPackageCoroutine.launch(project, Dispatchers.IO) {
- selectedPackages.forEach { pkg ->
- val specification = pkg.repository.createPackageSpecification(pkg.name, null)
- project.service().installPackage(specification.toInstallRequest())
+ PyPackageCoroutine.launch(project, Dispatchers.Default) {
+ val pyPackages = selectedPackages.mapNotNull { pkg ->
+ pkg.repository.findPackageSpecification(pkg.name, null)
}
+ val installRequest = PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications(pyPackages)
+ project.service().installPackage(installRequest)
}
return
}
@@ -34,14 +35,13 @@ internal class InstallPackageAction : DumbAwareAction() {
- PyPackageCoroutine.launch(project, Dispatchers.IO) {
+ PyPackageCoroutine.launch(project, Dispatchers.Default) {
val service = PyPackagingToolWindowService.getInstance(project)
- val details = service.detailsForPackage(pkg)
+ val details = service.detailsForPackage(pkg) ?: return@launch
withContext(Dispatchers.Main) {
val popup = PyPackagesUiComponents.createAvailableVersionsPopup(pkg, details, project)
popup.show(RelativePoint(e.inputEvent as MouseEvent))
}
-
}
}
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/actions/InstallWithOptionsPackageAction.kt b/python/src/com/jetbrains/python/packaging/toolwindow/actions/InstallWithOptionsPackageAction.kt
index 5c84f8ed2872..1adc3bbad69f 100644
--- a/python/src/com/jetbrains/python/packaging/toolwindow/actions/InstallWithOptionsPackageAction.kt
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/actions/InstallWithOptionsPackageAction.kt
@@ -27,7 +27,7 @@ internal class InstallWithOptionsPackageAction : DumbAwareAction() {
PyPackageCoroutine.launch(project, Dispatchers.IO) {
val service = PyPackagingToolWindowService.getInstance(project)
- val details = service.detailsForPackage(pkg)
+ val details = service.detailsForPackage(pkg) ?: return@launch
installWithOptions(project, details)
}
@@ -51,7 +51,7 @@ internal class InstallWithOptionsPackageAction : DumbAwareAction() {
val options = optionsString.split(' ').map { it.trim() }.filter { it.isNotBlank() }
- val specification = details.toPackageSpecification(version ?: details.availableVersions.first())
+ val specification = details.toPackageSpecification(version ?: details.availableVersions.first()) ?: return
project.service().installPackage(specification.toInstallRequest(), options)
}
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/actions/UpdatePackageToLatestAction.kt b/python/src/com/jetbrains/python/packaging/toolwindow/actions/UpdatePackageToLatestAction.kt
index 1772049549b6..decfc8524d96 100644
--- a/python/src/com/jetbrains/python/packaging/toolwindow/actions/UpdatePackageToLatestAction.kt
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/actions/UpdatePackageToLatestAction.kt
@@ -23,12 +23,10 @@ internal class UpdatePackageToLatestAction : DumbAwareAction() {
val service = PyPackagingToolWindowService.getInstance(project)
PyPackageCoroutine.getIoScope(project).launch {
- for (pkg in packages) {
- val specification = pkg.repository?.createPackageSpecification(pkg.name, pkg.nextVersion!!.presentableText)
- specification?.let {
- service.updatePackage(it)
- }
- }
+ val packageSpecifications = packages.mapNotNull { pkg ->
+ pkg.repository?.findPackageSpecification(pkg.name)
+ }.toTypedArray()
+ service.updatePackage(*packageSpecifications)
}
}
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/details/PyPackageDescriptionController.kt b/python/src/com/jetbrains/python/packaging/toolwindow/details/PyPackageDescriptionController.kt
index 991162a1ca09..c59927dd88a6 100644
--- a/python/src/com/jetbrains/python/packaging/toolwindow/details/PyPackageDescriptionController.kt
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/details/PyPackageDescriptionController.kt
@@ -25,11 +25,7 @@ import com.intellij.ui.JBColor
import com.intellij.ui.SideBorder
import com.intellij.ui.components.JBComboBoxLabel
import com.intellij.ui.components.JBOptionButton
-import com.intellij.ui.dsl.builder.BottomGap
-import com.intellij.ui.dsl.builder.RightGap
-import com.intellij.ui.dsl.builder.TopGap
-import com.intellij.ui.dsl.builder.bindText
-import com.intellij.ui.dsl.builder.panel
+import com.intellij.ui.dsl.builder.*
import com.intellij.ui.jcef.JCEFHtmlPanel
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.packaging.PyPackageUtil
@@ -51,12 +47,7 @@ import java.awt.Font
import java.awt.event.ActionEvent
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
-import javax.swing.AbstractAction
-import javax.swing.Action
-import javax.swing.BorderFactory
-import javax.swing.JComponent
-import javax.swing.JPanel
-import javax.swing.SwingConstants
+import javax.swing.*
class PyPackageDescriptionController(val project: Project) : Disposable {
private val latestText: String
@@ -79,7 +70,7 @@ class PyPackageDescriptionController(val project: Project) : Disposable {
private val installAction = wrapAction(message("action.PyInstallPackage.text"), message("progress.text.installing")) {
val details = selectedPackageDetails.get() ?: return@wrapAction
val version = versionSelector.text.takeIf { it != latestText }
- val specification = details.toPackageSpecification(version)
+ val specification = details.toPackageSpecification(version) ?: return@wrapAction
project.service().installPackage(specification.toInstallRequest())
}
@@ -196,7 +187,7 @@ class PyPackageDescriptionController(val project: Project) : Disposable {
private fun updatePackageVersion(newVersion: String) {
val details = selectedPackageDetails.get() ?: return
- val newVersionSpec = details.toPackageSpecification(newVersion)
+ val newVersionSpec = details.toPackageSpecification(newVersion) ?: return
val pyPackagingToolWindowService = PyPackagingToolWindowService.getInstance(project)
PyPackageCoroutine.launch(project, Dispatchers.IO) {
pyPackagingToolWindowService.installPackage(newVersionSpec.toInstallRequest())
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/details/PyPackageInfoPanel.kt b/python/src/com/jetbrains/python/packaging/toolwindow/details/PyPackageInfoPanel.kt
index c4d2173947d5..83bb4d2272fb 100644
--- a/python/src/com/jetbrains/python/packaging/toolwindow/details/PyPackageInfoPanel.kt
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/details/PyPackageInfoPanel.kt
@@ -60,7 +60,7 @@ class PyPackageInfoPanel(val project: Project) : Disposable {
val service = project.service()
updateJob = PyPackageCoroutine.getScope(project).launch {
try {
- val packageDetails = service.detailsForPackage(pyPackage)
+ val packageDetails = service.detailsForPackage(pyPackage) ?: return@launch
withContext(Dispatchers.EDT) {
infoController.setPackageDetails(packageDetails)
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/model/modelComponents.kt b/python/src/com/jetbrains/python/packaging/toolwindow/model/modelComponents.kt
index 3e798d6f893f..8a87888db91c 100644
--- a/python/src/com/jetbrains/python/packaging/toolwindow/model/modelComponents.kt
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/model/modelComponents.kt
@@ -34,10 +34,6 @@ class InstalledPackage(val instance: PythonPackage, repository: PyPackageReposit
currentVersion ?: return false
return nextVersion != null && PyPackageVersionComparator.compare(nextVersion, currentVersion) > 0
}
-
- fun withNextVersion(newVersion: PyPackageVersion?): InstalledPackage {
- return InstalledPackage(instance, repository, newVersion)
- }
}
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/packages/table/PyPackageTableMouseAdapter.kt b/python/src/com/jetbrains/python/packaging/toolwindow/packages/table/PyPackageTableMouseAdapter.kt
index 12416851efea..74223990b8da 100644
--- a/python/src/com/jetbrains/python/packaging/toolwindow/packages/table/PyPackageTableMouseAdapter.kt
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/packages/table/PyPackageTableMouseAdapter.kt
@@ -3,6 +3,7 @@ package com.jetbrains.python.packaging.toolwindow.packages.table
import com.intellij.codeInsight.hints.presentation.MouseButton
import com.intellij.codeInsight.hints.presentation.mouseButton
+import com.intellij.openapi.application.EDT
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.ui.awt.RelativePoint
@@ -32,20 +33,25 @@ internal class PyPackageTableMouseAdapter(private val table: PyPackagesTable) :
}
val hoveredRow = TableHoverListener.getHoveredRow(table)
- val selectedPackage = table.items.getOrNull(hoveredRow) ?: table.selectedItem() ?: return
+ val selectedPackage = table.items.getOrNull(hoveredRow) ?: table.selectedItem() ?: return
if (selectedPackage is InstallablePackage) {
- PyPackageCoroutine.launch(project, Dispatchers.IO) {
- val details = service.detailsForPackage(selectedPackage)
- withContext(Dispatchers.Main) {
+ PyPackageCoroutine.launch(project) {
+ val details = service.detailsForPackage(selectedPackage) ?: return@launch
+ withContext(Dispatchers.EDT) {
PyPackagesUiComponents.createAvailableVersionsPopup(selectedPackage, details, project).show(RelativePoint(e))
}
}
+ return
}
- else if (selectedPackage is InstalledPackage && selectedPackage.canBeUpdated && selectedPackage.repository != null) {
- PyPackageCoroutine.launch(project, Dispatchers.IO) {
- val nextVersion = selectedPackage.nextVersion ?: return@launch
- val specification = selectedPackage.repository.createPackageSpecification(selectedPackage.name, nextVersion.presentableText)
+
+ if (selectedPackage is InstalledPackage &&
+ selectedPackage.canBeUpdated &&
+ selectedPackage.repository != null &&
+ selectedPackage.nextVersion != null) {
+ val specification = selectedPackage.repository.findPackageSpecification(selectedPackage.name) ?: return
+
+ PyPackageCoroutine.launch(project) {
project.service().updatePackage(specification)
}
}
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/ui/PyPackagesUiComponents.kt b/python/src/com/jetbrains/python/packaging/toolwindow/ui/PyPackagesUiComponents.kt
index 5f8a702d0e31..ef3bea0c3f15 100644
--- a/python/src/com/jetbrains/python/packaging/toolwindow/ui/PyPackagesUiComponents.kt
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/ui/PyPackagesUiComponents.kt
@@ -42,9 +42,9 @@ object PyPackagesUiComponents {
override fun onChosen(selectedValue: String?, finalChoice: Boolean): PopupStep<*>? {
return doFinalStep {
val repository = checkNotNull(selectedPackage.repository)
- val specification = repository.createPackageSpecification(selectedPackage.name, selectedValue)
+ val specification = repository.findPackageSpecification(selectedPackage.name, selectedValue)
PyPackageCoroutine.getIoScope(project).launch(Dispatchers.IO) {
- project.service().installPackage(specification.toInstallRequest())
+ project.service().installPackage(specification!!.toInstallRequest())
}
}
}
diff --git a/python/src/com/jetbrains/python/requirements/RequirementsCompletionUtil.kt b/python/src/com/jetbrains/python/requirements/RequirementsCompletionUtil.kt
index b9d05103c603..5df19d446dcd 100644
--- a/python/src/com/jetbrains/python/requirements/RequirementsCompletionUtil.kt
+++ b/python/src/com/jetbrains/python/requirements/RequirementsCompletionUtil.kt
@@ -28,7 +28,7 @@ fun completePackageNames(project: Project, sdk: Sdk, result: CompletionResultSet
fun completeVersions(name: String, project: Project, sdk: Sdk, result: CompletionResultSet, addQuotes: Boolean) {
val packageManager = PythonPackageManager.forSdk(project, sdk)
val repositoryManager = packageManager.repositoryManager
- val packageSpecification = packageManager.createPackageSpecification(name) ?: return
+ val packageSpecification = packageManager.findPackageSpecification(name) ?: return
val versions = ApplicationUtil.runWithCheckCanceled({
runBlockingCancellable {
repositoryManager.getPackageDetails(packageSpecification).getOrThrow().availableVersions
diff --git a/python/src/com/jetbrains/python/requirements/RequirementsInspectionVisitor.kt b/python/src/com/jetbrains/python/requirements/RequirementsInspectionVisitor.kt
index e9e72d75d38d..df9bf3e5130a 100644
--- a/python/src/com/jetbrains/python/requirements/RequirementsInspectionVisitor.kt
+++ b/python/src/com/jetbrains/python/requirements/RequirementsInspectionVisitor.kt
@@ -28,7 +28,7 @@ open class RequirementsInspectionVisitor(val holder: ProblemsHolder,
super.visitElement(element)
}
- open fun visitRequirementsFile(element: RequirementsFile) {
- super.visitElement(element)
+ open fun visitRequirementsFile(requirementsFile: RequirementsFile) {
+ super.visitElement(requirementsFile)
}
}
diff --git a/python/src/com/jetbrains/python/requirements/UnsatisfiedRequirementInspection.kt b/python/src/com/jetbrains/python/requirements/UnsatisfiedRequirementInspection.kt
deleted file mode 100644
index 6aa62af2c76c..000000000000
--- a/python/src/com/jetbrains/python/requirements/UnsatisfiedRequirementInspection.kt
+++ /dev/null
@@ -1,245 +0,0 @@
-// 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.requirements
-
-import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
-import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo
-import com.intellij.codeInspection.*
-import com.intellij.codeInspection.util.IntentionFamilyName
-import com.intellij.icons.AllIcons
-import com.intellij.ide.util.PropertiesComponent
-import com.intellij.openapi.application.ApplicationManager
-import com.intellij.openapi.application.readAction
-import com.intellij.openapi.components.service
-import com.intellij.openapi.fileEditor.FileDocumentManager
-import com.intellij.openapi.module.Module
-import com.intellij.openapi.module.ModuleUtilCore
-import com.intellij.openapi.project.Project
-import com.intellij.openapi.ui.DoNotAskOption
-import com.intellij.openapi.ui.MessageDialogBuilder.Companion.yesNo
-import com.intellij.openapi.ui.Messages
-import com.intellij.openapi.vfs.findDocument
-import com.intellij.psi.PsiElementVisitor
-import com.intellij.psi.SmartPointerManager
-import com.intellij.psi.SmartPsiElementPointer
-import com.jetbrains.python.PyBundle
-import com.jetbrains.python.PyPsiBundle
-import com.jetbrains.python.inspections.quickfix.InstallPackageQuickFix
-import com.jetbrains.python.packaging.*
-import com.jetbrains.python.packaging.common.runPackagingOperationOrShowErrorDialog
-import com.jetbrains.python.packaging.management.PythonPackageManager
-import com.jetbrains.python.packaging.management.runPackagingTool
-import com.jetbrains.python.packaging.management.toInstallRequest
-import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService
-import com.jetbrains.python.requirements.psi.NameReq
-import com.jetbrains.python.requirements.psi.Requirement
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-
-class UnsatisfiedRequirementInspection : LocalInspectionTool() {
- override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor {
- return UnsatisfiedRequirementInspectionVisitor(holder, session)
- }
-}
-
-private class UnsatisfiedRequirementInspectionVisitor(
- holder: ProblemsHolder,
- session: LocalInspectionToolSession,
-) : RequirementsInspectionVisitor(holder, session) {
-
- override fun visitRequirementsFile(element: RequirementsFile) {
- val sdk = getPythonSdk(element) ?: return
- if (element.text.isNullOrBlank()) {
- val fixes = ModuleUtilCore.findModuleForPsiElement(element)?.let { module ->
- arrayOf(PyGenerateRequirementsFileQuickFix(module))
- } ?: emptyArray()
- holder.registerProblem(element, PyPsiBundle.message("INSP.package.requirements.requirements.file.empty"), ProblemHighlightType.GENERIC_ERROR_OR_WARNING, *fixes)
- return
- }
-
- val packageManager = PythonPackageManager.forSdk(element.project, sdk)
- val packages = packageManager.installedPackages.map { normalizePackageName(it.name) }
- val unsatisfiedRequirements = element.requirements().filter { requirement -> normalizePackageName(requirement.displayName) !in packages }
- unsatisfiedRequirements.forEach { requirement ->
- val fixes = arrayOf(
- InstallRequirementQuickFix(requirement),
- InstallAllRequirementsQuickFix(unsatisfiedRequirements),
- InstallProjectAsEditableQuickfix()
- )
- holder.registerProblem(requirement, PyBundle.message("INSP.requirements.package.not.installed", requirement.displayName), ProblemHighlightType.WARNING, *fixes)
- }
- }
-}
-
-private class InstallAllRequirementsQuickFix(requirements: List) : LocalQuickFix {
- val requirements: List> = requirements.map { SmartPointerManager.createPointer(it) }.toList()
-
- override fun getFamilyName(): String {
- return PyBundle.message("QFIX.NAME.install.all.requirements")
- }
-
- override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
- val requirementElements = requirements.mapNotNull { it.element }
- val confirmedPackages = getConfirmedPackages(requirementElements.map { pyRequirement(it.displayName) }, project)
-
- InstallRequirementQuickFix.installPackages(
- project,
- descriptor,
- requirementElements.filter { pkg -> confirmedPackages.any { it.equals(pkg.displayName) } }
- )
- }
-
- override fun generatePreview(project: Project, previewDescriptor: ProblemDescriptor): IntentionPreviewInfo {
- return IntentionPreviewInfo.EMPTY
- }
-
- override fun startInWriteAction(): Boolean {
- return false
- }
-}
-
-private class InstallRequirementQuickFix(requirement: Requirement) : LocalQuickFix {
-
- val requirement: SmartPsiElementPointer = SmartPointerManager.createPointer(requirement)
-
- override fun getFamilyName(): String {
- return PyBundle.message("QFIX.NAME.install.requirement", requirement.element?.displayName ?: "")
- }
-
- companion object {
- private const val CONFIRM_PACKAGE_INSTALLATION_PROPERTY: String = "python.confirm.package.installation"
-
- fun checkAndInstall(project: Project, descriptor: ProblemDescriptor, requirement: SmartPsiElementPointer) {
- val req = requirement.element ?: return
- val name = req.displayName
- val isWellKnownPackage = ApplicationManager.getApplication()
- .getService(PyPIPackageRanking::class.java)
- .packageRank.containsKey(normalizePackageName(name))
- val confirmationEnabled = PropertiesComponent.getInstance().getBoolean(CONFIRM_PACKAGE_INSTALLATION_PROPERTY, true)
- if (!isWellKnownPackage && confirmationEnabled) {
- val confirmed = yesNo(PyBundle.message("python.packaging.dialog.title.install.package.confirmation"),
- PyBundle.message("python.packaging.dialog.message.install.package.confirmation", name))
- .icon(AllIcons.General.WarningDialog)
- .doNotAsk(ConfirmPackageInstallationDoNotAskOption())
- .ask(project)
- if (!confirmed) {
- return
- }
- }
-
- installPackage(project, descriptor, req)
- }
-
- fun installPackage(project: Project, descriptor: ProblemDescriptor, requirement: Requirement) {
- val file = descriptor.psiElement.containingFile ?: return
- val sdk = getPythonSdk(file) ?: return
- val versionSpecStr = if (requirement is NameReq) requirement.versionspec?.text else null
- val versionSpec = versionSpecStr?.let { pyRequirementVersionSpec(it) }?.getOr { return }
- val name = requirement.displayName
-
- project.service().serviceScope.launch(Dispatchers.IO) {
- PyPackageInstallUtils.installPackage(project, sdk, name, true, versionSpec)
- DaemonCodeAnalyzer.getInstance(project).restart(file)
- }
- }
-
- fun installPackages(project: Project, descriptor: ProblemDescriptor, requirements: List) {
- val file = descriptor.psiElement.containingFile ?: return
-
- val serviceScope = project.service().serviceScope
- serviceScope.launch(Dispatchers.Default) {
- val sdk = getPythonSdk(file) ?: return@launch
-
- val infos = readAction {
- requirements.map { requirement ->
- val versionSpec = if (requirement is NameReq)
- requirement.versionspec?.text
- else
- null
- val name = requirement.displayName
- name to versionSpec
- }
- }
- val manager = PythonPackageManager.forSdk(project, sdk)
- val specs = infos.mapNotNull { (name, versionSpecStr) ->
- val versionSpec = versionSpecStr?.let { pyRequirementVersionSpec(it) }?.getOr { return@mapNotNull null }
- manager.createPackageSpecificationWithSpec(name, versionSpec)
- }
-
- if (specs.isEmpty())
- return@launch
-
- manager.installPackages(specs.map { it.toInstallRequest() }, emptyList(), withBackgroundProgress = true)
- DaemonCodeAnalyzer.getInstance(project).restart(file)
- }
- }
- }
-
- override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
- checkAndInstall(project, descriptor, requirement)
- }
-
- override fun generatePreview(project: Project, previewDescriptor: ProblemDescriptor): IntentionPreviewInfo {
- return IntentionPreviewInfo.EMPTY
- }
-
- override fun startInWriteAction(): Boolean {
- return false
- }
-
- private class ConfirmPackageInstallationDoNotAskOption : DoNotAskOption.Adapter() {
- override fun rememberChoice(isSelected: Boolean, exitCode: Int) {
- if (isSelected && exitCode == Messages.OK) {
- PropertiesComponent.getInstance().setValue(InstallPackageQuickFix.CONFIRM_PACKAGE_INSTALLATION_PROPERTY, false, true)
- }
- }
- }
-}
-
-private class InstallProjectAsEditableQuickfix : LocalQuickFix {
-
- override fun getFamilyName(): String {
- return PyBundle.message("python.pyproject.install.self.as.editable")
- }
-
- @Suppress("DialogTitleCapitalization")
- override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
- val file = descriptor.psiElement.containingFile ?: return
- val sdk = getPythonSdk(file) ?: return
- val manager = PythonPackageManager.forSdk(project, sdk)
- FileDocumentManager.getInstance().saveDocument(file.virtualFile.findDocument() ?: return)
-
- project.service().serviceScope.launch {
- runPackagingOperationOrShowErrorDialog(sdk, PyBundle.message("python.pyproject.install.self.error"), null) {
- manager.runPackagingTool("install", listOf("-e", "."), PyBundle.message("python.pyproject.install.self.as.editable.progress"))
- manager.refreshPaths()
- runPackagingOperationOrShowErrorDialog(sdk, PyBundle.message("python.packaging.operation.failed.title")) {
- manager.reloadPackages()
- }
- }
- DaemonCodeAnalyzer.getInstance(project).restart(file)
- }
-
- }
-
- override fun startInWriteAction(): Boolean {
- return false
- }
-
- override fun generatePreview(project: Project, previewDescriptor: ProblemDescriptor): IntentionPreviewInfo {
- return IntentionPreviewInfo.EMPTY
- }
-}
-
-private class PyGenerateRequirementsFileQuickFix(private val myModule: Module) : LocalQuickFix {
- override fun getFamilyName(): @IntentionFamilyName String {
- return PyPsiBundle.message("QFIX.add.imported.packages.to.requirements")
- }
-
- override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
- syncWithImports(myModule)
- }
-
- override fun startInWriteAction(): Boolean {
- return false
- }
-}
diff --git a/python/src/com/jetbrains/python/requirements/inspections/outdated/OutdatedRequirementInspection.kt b/python/src/com/jetbrains/python/requirements/inspections/outdated/OutdatedRequirementInspection.kt
deleted file mode 100644
index 525b7d22eebb..000000000000
--- a/python/src/com/jetbrains/python/requirements/inspections/outdated/OutdatedRequirementInspection.kt
+++ /dev/null
@@ -1,13 +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.requirements.inspections.outdated
-
-import com.intellij.codeInspection.LocalInspectionTool
-import com.intellij.codeInspection.LocalInspectionToolSession
-import com.intellij.codeInspection.ProblemsHolder
-import com.intellij.psi.PsiElementVisitor
-
-internal class OutdatedRequirementInspection : LocalInspectionTool() {
- override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor {
- return OutdatedRequirementInspectionVisitor(holder, session)
- }
-}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/requirements/inspections/outdated/OutdatedRequirementInspectionVisitor.kt b/python/src/com/jetbrains/python/requirements/inspections/outdated/OutdatedRequirementInspectionVisitor.kt
deleted file mode 100644
index 3fb2636ce151..000000000000
--- a/python/src/com/jetbrains/python/requirements/inspections/outdated/OutdatedRequirementInspectionVisitor.kt
+++ /dev/null
@@ -1,59 +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.requirements.inspections.outdated
-
-import com.intellij.codeInspection.LocalInspectionToolSession
-import com.intellij.codeInspection.ProblemsHolder
-import com.jetbrains.python.PyBundle
-import com.jetbrains.python.packaging.management.PythonPackageManager
-import com.jetbrains.python.packaging.normalizePackageName
-import com.jetbrains.python.requirements.RequirementsFile
-import com.jetbrains.python.requirements.RequirementsInspectionVisitor
-import com.jetbrains.python.requirements.getPythonSdk
-import com.jetbrains.python.requirements.inspections.outdated.quickfixes.UpdateAllRequirementQuickFix
-import com.jetbrains.python.requirements.inspections.outdated.quickfixes.UpdateRequirementQuickFix
-
-internal class OutdatedRequirementInspectionVisitor(
- holder: ProblemsHolder,
- session: LocalInspectionToolSession,
-) : RequirementsInspectionVisitor(holder, session) {
-
- override fun visitRequirementsFile(element: RequirementsFile) {
- if (element.text.isNullOrBlank()) {
- return
- }
-
- processOutdatedPackages(element)
- }
-
- private fun processOutdatedPackages(element: RequirementsFile) {
- val sdk = getPythonSdk(element) ?: return
- val packageManager = PythonPackageManager.forSdk(element.project, sdk)
-
- val outdatedPackages = packageManager.outdatedPackages
- if (outdatedPackages.isEmpty())
- return
-
- val outdatedRequirements = element.requirements().mapNotNull { requirement ->
- val packageName = normalizePackageName(requirement.displayName)
- if (packageName in outdatedPackages.keys)
- packageName to requirement
- else
- null
- }.toMap()
-
- outdatedRequirements.forEach { (name, requirement) ->
- val outdatedPackage = outdatedPackages[name] ?: return@forEach
- val description = PyBundle.message(
- "python.sdk.inspection.message.version.outdated.latest",
- requirement.displayName,
- outdatedPackage.version,
- outdatedPackage.latestVersion
- )
- val fixes = arrayOf(
- UpdateAllRequirementQuickFix(outdatedRequirements.keys),
- UpdateRequirementQuickFix(name)
- )
- holder.registerProblem(requirement, description, *fixes)
- }
- }
-}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/requirements/inspections/quickfixes/InstallAllRequirementsQuickFix.kt b/python/src/com/jetbrains/python/requirements/inspections/quickfixes/InstallAllRequirementsQuickFix.kt
new file mode 100644
index 000000000000..6b67a1243ac4
--- /dev/null
+++ b/python/src/com/jetbrains/python/requirements/inspections/quickfixes/InstallAllRequirementsQuickFix.kt
@@ -0,0 +1,42 @@
+// 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.requirements.inspections.quickfixes
+
+import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo
+import com.intellij.codeInspection.LocalQuickFix
+import com.intellij.codeInspection.ProblemDescriptor
+import com.intellij.openapi.project.Project
+import com.jetbrains.python.PyBundle
+import com.jetbrains.python.packaging.PyPackageInstallUtils
+import com.jetbrains.python.packaging.PyRequirement
+import com.jetbrains.python.packaging.management.PythonPackageManager
+import com.jetbrains.python.packaging.management.PythonPackagesInstaller
+import com.jetbrains.python.packaging.utils.PyPackageCoroutine
+import com.jetbrains.python.requirements.getPythonSdk
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+internal class InstallAllRequirementsQuickFix(val requirements: List) : LocalQuickFix {
+ override fun getFamilyName(): String {
+ return PyBundle.message("QFIX.NAME.install.all.requirements")
+ }
+
+ override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
+ val confirmedPackages = PyPackageInstallUtils.getConfirmedPackages(requirements, project)
+ if (confirmedPackages.isEmpty())
+ return
+
+ val file = descriptor.psiElement.containingFile ?: return
+
+ PyPackageCoroutine.getScope(project).launch(Dispatchers.Default) {
+ val sdk = getPythonSdk(file) ?: return@launch
+ val manager = PythonPackageManager.Companion.forSdk(project, sdk)
+ PythonPackagesInstaller.installWithRequirements(manager, confirmedPackages, emptyList())
+ }
+ }
+
+ override fun generatePreview(project: Project, previewDescriptor: ProblemDescriptor): IntentionPreviewInfo {
+ return IntentionPreviewInfo.EMPTY
+ }
+
+ override fun startInWriteAction(): Boolean = false
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/requirements/inspections/quickfixes/InstallRequirementQuickFix.kt b/python/src/com/jetbrains/python/requirements/inspections/quickfixes/InstallRequirementQuickFix.kt
new file mode 100644
index 000000000000..1575c8ee5e97
--- /dev/null
+++ b/python/src/com/jetbrains/python/requirements/inspections/quickfixes/InstallRequirementQuickFix.kt
@@ -0,0 +1,33 @@
+// 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.requirements.inspections.quickfixes
+
+import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo
+import com.intellij.codeInspection.LocalQuickFix
+import com.intellij.codeInspection.ProblemDescriptor
+import com.intellij.openapi.project.Project
+import com.jetbrains.python.PyBundle
+import com.jetbrains.python.packaging.PyPackageInstallUtils
+import com.jetbrains.python.packaging.PyRequirement
+import com.jetbrains.python.packaging.utils.PyPackageCoroutine
+import com.jetbrains.python.requirements.getPythonSdk
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+internal class InstallRequirementQuickFix(val requirement: PyRequirement) : LocalQuickFix {
+ override fun getFamilyName(): String {
+ return PyBundle.message("QFIX.NAME.install.requirement", requirement.name)
+ }
+
+ override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
+ val pythonSdk = getPythonSdk(descriptor.psiElement.containingFile) ?: return
+
+ PyPackageCoroutine.getScope(project).launch(Dispatchers.Default) {
+ PyPackageInstallUtils.confirmAndInstall(project, pythonSdk, requirement.name,
+ requirement.versionSpecs.firstOrNull())
+ }
+ }
+
+ override fun generatePreview(project: Project, previewDescriptor: ProblemDescriptor): IntentionPreviewInfo = IntentionPreviewInfo.EMPTY
+
+ override fun startInWriteAction(): Boolean = false
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/requirements/inspections/quickfixes/PyGenerateRequirementsFileQuickFix.kt b/python/src/com/jetbrains/python/requirements/inspections/quickfixes/PyGenerateRequirementsFileQuickFix.kt
new file mode 100644
index 000000000000..54246acd0c28
--- /dev/null
+++ b/python/src/com/jetbrains/python/requirements/inspections/quickfixes/PyGenerateRequirementsFileQuickFix.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.requirements.inspections.quickfixes
+
+import com.intellij.codeInspection.LocalQuickFix
+import com.intellij.codeInspection.ProblemDescriptor
+import com.intellij.codeInspection.util.IntentionFamilyName
+import com.intellij.openapi.module.Module
+import com.intellij.openapi.project.Project
+import com.jetbrains.python.PyPsiBundle
+import com.jetbrains.python.packaging.syncWithImports
+
+internal class PyGenerateRequirementsFileQuickFix(private val myModule: Module) : LocalQuickFix {
+ override fun getFamilyName(): @IntentionFamilyName String {
+ return PyPsiBundle.message("QFIX.add.imported.packages.to.requirements")
+ }
+
+ override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
+ syncWithImports(myModule)
+ }
+
+ override fun startInWriteAction(): Boolean {
+ return false
+ }
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/requirements/inspections/outdated/quickfixes/UpdateAllRequirementQuickFix.kt b/python/src/com/jetbrains/python/requirements/inspections/quickfixes/UpdateAllRequirementQuickFix.kt
similarity index 84%
rename from python/src/com/jetbrains/python/requirements/inspections/outdated/quickfixes/UpdateAllRequirementQuickFix.kt
rename to python/src/com/jetbrains/python/requirements/inspections/quickfixes/UpdateAllRequirementQuickFix.kt
index a7c68daa9a7d..06d9f9ff9eea 100644
--- a/python/src/com/jetbrains/python/requirements/inspections/outdated/quickfixes/UpdateAllRequirementQuickFix.kt
+++ b/python/src/com/jetbrains/python/requirements/inspections/quickfixes/UpdateAllRequirementQuickFix.kt
@@ -1,7 +1,6 @@
// 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.requirements.inspections.outdated.quickfixes
+package com.jetbrains.python.requirements.inspections.quickfixes
-import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
@@ -25,12 +24,11 @@ internal class UpdateAllRequirementQuickFix(val outdatedPyRequirements: Set
+ val pyRequirement = PyRequirementParser.fromLine(requirement.text) ?: return@mapNotNull null
+ if (pyRequirement.name in packages)
+ return@mapNotNull null
+ requirement to pyRequirement
+ }
+ val unsatisfiedRequirements = notInstalledRequirementsWithPsi.map { it.second }
+
+
+ val installAllRequirementsQuickFix = InstallAllRequirementsQuickFix(unsatisfiedRequirements).takeIf { unsatisfiedRequirements.size > 1 }
+ notInstalledRequirementsWithPsi.forEach { (psiRequirement, pyRequirement) ->
+ val fixes = listOfNotNull(
+ InstallRequirementQuickFix(pyRequirement),
+ installAllRequirementsQuickFix,
+ )
+ holder.registerProblem(psiRequirement, PyBundle.message("INSP.requirements.package.not.installed", psiRequirement.displayName),
+ ProblemHighlightType.WARNING,
+ *fixes.toTypedArray())
+ }
+ }
+ }
+
+ override fun isDumbAware(): Boolean = true
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/requirements/inspections/tools/OutdatedRequirementInspection.kt b/python/src/com/jetbrains/python/requirements/inspections/tools/OutdatedRequirementInspection.kt
new file mode 100644
index 000000000000..a973d24c40e0
--- /dev/null
+++ b/python/src/com/jetbrains/python/requirements/inspections/tools/OutdatedRequirementInspection.kt
@@ -0,0 +1,77 @@
+// 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.requirements.inspections.tools
+
+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.ModuleUtilCore
+import com.jetbrains.python.PyBundle
+import com.jetbrains.python.PyPsiBundle
+import com.jetbrains.python.packaging.management.PythonPackageManager
+import com.jetbrains.python.packaging.normalizePackageName
+import com.jetbrains.python.requirements.RequirementsFile
+import com.jetbrains.python.requirements.RequirementsInspectionVisitor
+import com.jetbrains.python.requirements.getPythonSdk
+import com.jetbrains.python.requirements.inspections.quickfixes.PyGenerateRequirementsFileQuickFix
+import com.jetbrains.python.requirements.inspections.quickfixes.UpdateAllRequirementQuickFix
+import com.jetbrains.python.requirements.inspections.quickfixes.UpdateRequirementQuickFix
+
+internal class OutdatedRequirementInspection : LocalInspectionTool() {
+ override fun buildVisitor(
+ holder: ProblemsHolder,
+ isOnTheFly: Boolean,
+ session: LocalInspectionToolSession,
+ ) = object : RequirementsInspectionVisitor(holder, session) {
+ override fun visitRequirementsFile(requirementsFile: RequirementsFile) {
+ val requirements = requirementsFile.requirements()
+
+ val psiFile = session.file
+ if (psiFile.text.isNullOrBlank()) {
+ val fixes = ModuleUtilCore.findModuleForPsiElement(psiFile)?.let { module ->
+ arrayOf(PyGenerateRequirementsFileQuickFix(module))
+ } ?: emptyArray()
+ holder.registerProblem(psiFile, PyPsiBundle.message("INSP.package.requirements.requirements.file.empty"), ProblemHighlightType.GENERIC_ERROR_OR_WARNING, *fixes)
+ return
+ }
+
+ val sdk = getPythonSdk(psiFile) ?: return
+
+ val packageManager = PythonPackageManager.Companion.forSdk(psiFile.project, sdk)
+
+ val outdatedPackages = packageManager.outdatedPackages.toMap()
+ if (outdatedPackages.isEmpty())
+ return
+
+ val outdatedRequirements = requirements.mapNotNull { requirement ->
+ val packageName = normalizePackageName(requirement.displayName)
+ if (packageName in outdatedPackages.keys)
+ packageName to requirement
+ else
+ null
+ }.toMap()
+
+
+ val updateAllRequirementQuickFix = UpdateAllRequirementQuickFix(outdatedRequirements.keys)
+ .takeIf { outdatedRequirements.size > 1 }
+ outdatedRequirements.forEach { (name, requirement) ->
+ val outdatedPackage = outdatedPackages[name] ?: return@forEach
+ val description = PyBundle.message(
+ "python.sdk.inspection.message.version.outdated.latest",
+ requirement.displayName,
+ outdatedPackage.version,
+ outdatedPackage.latestVersion
+ )
+ val fixes = listOfNotNull(
+ updateAllRequirementQuickFix,
+ UpdateRequirementQuickFix(name)
+ ).toTypedArray()
+ holder.registerProblem(requirement, description,
+ ProblemHighlightType.WEAK_WARNING, *fixes)
+ }
+
+ }
+ }
+
+ override fun isDumbAware(): Boolean = true
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/run/filter/PythonInstallPackageFilter.kt b/python/src/com/jetbrains/python/run/filter/PythonInstallPackageFilter.kt
index e4f4ba474de7..ce03691ab45e 100644
--- a/python/src/com/jetbrains/python/run/filter/PythonInstallPackageFilter.kt
+++ b/python/src/com/jetbrains/python/run/filter/PythonInstallPackageFilter.kt
@@ -3,7 +3,6 @@ package com.jetbrains.python.run.filter
import com.intellij.execution.filters.Filter
import com.intellij.openapi.editor.impl.EditorImpl
-import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.modules
import com.intellij.openapi.projectRoots.Sdk
@@ -13,7 +12,7 @@ import com.jetbrains.python.psi.PyFile
import com.jetbrains.python.sdk.PythonSdkUtil
import com.jetbrains.python.sdk.pythonSdk
-class PythonInstallPackageFilter(val project: Project, var editor: EditorImpl? = null) : Filter, DumbAware {
+class PythonInstallPackageFilter(val project: Project, var editor: EditorImpl? = null) : Filter {
override fun applyFilter(line: String, entireLength: Int): Filter.Result? {
val prefix = "ModuleNotFoundError: No module named '"
if (!line.startsWith(prefix))
@@ -39,4 +38,6 @@ class PythonInstallPackageFilter(val project: Project, var editor: EditorImpl? =
val pyPsiFile = viewProvider.allFiles.firstOrNull { it is PyFile } ?: return null
return PythonSdkUtil.findPythonSdk(pyPsiFile)
}
+
+ override fun isDumbAware(): Boolean = true
}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/run/runAnything/PyRunAnythingCondaProvider.kt b/python/src/com/jetbrains/python/run/runAnything/PyRunAnythingCondaProvider.kt
index 1bf647fdf926..9110c9a67db7 100644
--- a/python/src/com/jetbrains/python/run/runAnything/PyRunAnythingCondaProvider.kt
+++ b/python/src/com/jetbrains/python/run/runAnything/PyRunAnythingCondaProvider.kt
@@ -3,11 +3,11 @@ package com.jetbrains.python.run.runAnything
import com.intellij.openapi.actionSystem.DataContext
import com.jetbrains.python.PyBundle
-import com.jetbrains.python.packaging.conda.CondaPackageManager
+import com.jetbrains.python.icons.PythonIcons
import com.jetbrains.python.packaging.conda.CondaPackageRepository
+import com.jetbrains.python.packaging.conda.CondaWithPipFallbackPackageManager
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.repository.PyPackageRepository
-import com.jetbrains.python.icons.PythonIcons
import javax.swing.Icon
class PyRunAnythingCondaProvider : PyRunAnythingPackageProvider() {
@@ -33,7 +33,7 @@ class PyRunAnythingCondaProvider : PyRunAnythingPackageProvider() {
override fun getPackageManager(dataContext: DataContext): PythonPackageManager? {
val pythonSdk = getSdk(dataContext) ?: return null
- return (PythonPackageManager.forSdk(dataContext.project, pythonSdk) as? CondaPackageManager) ?: return null
+ return (PythonPackageManager.forSdk(dataContext.project, pythonSdk) as? CondaWithPipFallbackPackageManager)
}
override fun getPackageRepository(dataContext: DataContext): PyPackageRepository? {
@@ -43,5 +43,5 @@ class PyRunAnythingCondaProvider : PyRunAnythingPackageProvider() {
override fun getLogCommandType(): CommandType = CommandType.CONDA
}
-val CONDA_COMMANDS = listOf("clean", "compare", "config", "create", "help", "info", "init", "install", "list", "package", "remove",
- "uninstall", "run", "search", "update", "upgrade").asSequence()
\ No newline at end of file
+val CONDA_COMMANDS: Sequence = listOf("clean", "compare", "config", "create", "help", "info", "init", "install", "list", "package", "remove",
+ "uninstall", "run", "search", "update", "upgrade").asSequence()
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/run/runAnything/PyRunAnythingPackageProvider.kt b/python/src/com/jetbrains/python/run/runAnything/PyRunAnythingPackageProvider.kt
index 3a8b30e77d6e..9efa50ecf734 100644
--- a/python/src/com/jetbrains/python/run/runAnything/PyRunAnythingPackageProvider.kt
+++ b/python/src/com/jetbrains/python/run/runAnything/PyRunAnythingPackageProvider.kt
@@ -53,7 +53,7 @@ abstract class PyRunAnythingPackageProvider : RunAnythingCommandLineProvider() {
val packageName = last.substring(0, ind)
val packageManager = getPackageManager(dataContext) ?: return emptySequence()
initCaches(packageManager)
- val packageSpec = getPackageRepository(dataContext)?.createPackageSpecification(packageName) ?: return emptySequence()
+ val packageSpec = getPackageRepository(dataContext)?.findPackageSpecification(packageName) ?: return emptySequence()
return runBlockingCancellable {
withContext(Dispatchers.Default) {
val packageInfo = packageManager.repositoryManager.getPackageDetails(packageSpec).getOrThrow()
diff --git a/python/src/com/jetbrains/python/sdk/PySdkCommandRunner.kt b/python/src/com/jetbrains/python/sdk/PySdkCommandRunner.kt
index 18525405f44c..fe52585a7a48 100644
--- a/python/src/com/jetbrains/python/sdk/PySdkCommandRunner.kt
+++ b/python/src/com/jetbrains/python/sdk/PySdkCommandRunner.kt
@@ -12,6 +12,8 @@ import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyExecResult
import org.jetbrains.annotations.ApiStatus.Internal
import java.nio.file.Path
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.minutes
/**
@@ -24,10 +26,10 @@ import java.nio.file.Path
* @return A [Result] object containing the output of the command execution.
*/
@Internal
-suspend fun runExecutableWithProgress(executable: Path, workDir: Path?, vararg args: String): PyExecResult {
+suspend fun runExecutableWithProgress(executable: Path, workDir: Path?, timeout: Duration = 10.minutes, vararg args: String): PyExecResult {
val ansiDecoder = AnsiEscapeDecoder()
reportRawProgress { reporter ->
- return ExecService().execGetStdout(WhatToExec.Binary(executable), args.toList(), ExecOptions(workingDirectory = workDir), procListener = {
+ return ExecService().execGetStdout(WhatToExec.Binary(executable), args.toList(), ExecOptions(workingDirectory = workDir, timeout = timeout), procListener = {
when (it) {
is ProcessEvent.ProcessStarted, is ProcessEvent.ProcessEnded -> Unit
is ProcessEvent.ProcessOutput -> {
diff --git a/python/src/com/jetbrains/python/sdk/PythonSdkUpdater.java b/python/src/com/jetbrains/python/sdk/PythonSdkUpdater.java
index d8dbf2f01999..c992c181d20b 100644
--- a/python/src/com/jetbrains/python/sdk/PythonSdkUpdater.java
+++ b/python/src/com/jetbrains/python/sdk/PythonSdkUpdater.java
@@ -48,7 +48,6 @@ import com.jetbrains.python.codeInsight.userSkeletons.PyUserSkeletonsUtil;
import com.jetbrains.python.packaging.PyPackageManager;
import com.jetbrains.python.packaging.common.PythonPackage;
import com.jetbrains.python.packaging.management.PythonPackageManager;
-import com.jetbrains.python.packaging.management.PythonPackageManagerExt;
import com.jetbrains.python.psi.PyUtil;
import com.jetbrains.python.remote.UnsupportedPythonSdkTypeException;
import com.jetbrains.python.sdk.headless.PythonActivityKey;
@@ -62,8 +61,8 @@ import org.jetbrains.annotations.TestOnly;
import java.awt.*;
import java.io.File;
import java.time.Instant;
-import java.util.List;
import java.util.*;
+import java.util.List;
import java.util.function.Function;
/**
@@ -222,8 +221,8 @@ public final class PythonSdkUpdater {
indicator.setText(PyBundle.message("python.sdk.scanning.installed.packages"));
indicator.setText2("");
PyPackageManager.getInstance(sdk).refreshAndGetPackages(true);
- PythonPackageManager manager = PythonPackageManager.Companion.forSdk(myProject, mySdk);
- PythonPackageManagerExt.launchReload(manager);
+ //It internally invoke lazy list packages update on first call
+ PythonPackageManager.Companion.forSdk(myProject, mySdk);
}
catch (ExecutionException e) {
if (LOG.isDebugEnabled()) {
diff --git a/python/src/com/jetbrains/python/sdk/pipenv/PipenvCommandExecutor.kt b/python/src/com/jetbrains/python/sdk/pipenv/PipenvCommandExecutor.kt
index 0bc99cf8983e..b8611cf97e5a 100644
--- a/python/src/com/jetbrains/python/sdk/pipenv/PipenvCommandExecutor.kt
+++ b/python/src/com/jetbrains/python/sdk/pipenv/PipenvCommandExecutor.kt
@@ -21,11 +21,12 @@ import org.jetbrains.annotations.ApiStatus.Internal
import org.jetbrains.annotations.SystemDependent
import java.io.FileNotFoundException
import java.nio.file.Path
+import kotlin.time.Duration.Companion.minutes
@Internal
suspend fun runPipEnv(dirPath: Path?, vararg args: String): Result {
val executable = getPipEnvExecutable().getOrElse { return Result.failure(it) }
- return runExecutableWithProgress(executable, dirPath, *args).asKotlinResult()
+ return runExecutableWithProgress(executable, dirPath, 10.minutes, *args).asKotlinResult()
}
/**
diff --git a/python/src/com/jetbrains/python/sdk/poetry/PoetryCommandExecutor.kt b/python/src/com/jetbrains/python/sdk/poetry/PoetryCommandExecutor.kt
index 9dcd4aac8fb2..6f3050418a35 100644
--- a/python/src/com/jetbrains/python/sdk/poetry/PoetryCommandExecutor.kt
+++ b/python/src/com/jetbrains/python/sdk/poetry/PoetryCommandExecutor.kt
@@ -9,7 +9,6 @@ import com.intellij.openapi.ui.ValidationInfo
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.registry.Registry
-import com.intellij.platform.util.progress.reportSequentialProgress
import com.intellij.python.community.execService.ExecOptions
import com.intellij.python.community.execService.ExecService
import com.intellij.python.community.execService.WhatToExec
@@ -42,6 +41,7 @@ import org.jetbrains.annotations.SystemIndependent
import java.nio.file.Path
import kotlin.io.path.exists
import kotlin.io.path.pathString
+import kotlin.time.Duration.Companion.minutes
/**
* This source code is edited by @koxudaxi Koudai Aono
@@ -53,7 +53,7 @@ private val VERSION_2 = "2.0.0".toVersion()
@Internal
suspend fun runPoetry(projectPath: Path?, vararg args: String): Result {
val executable = getPoetryExecutable().getOrElse { return Result.failure(it) }
- return runExecutableWithProgress(executable, projectPath, *args).asKotlinResult()
+ return runExecutableWithProgress(executable, projectPath, 10.minutes, *args).asKotlinResult()
}
@@ -97,14 +97,8 @@ suspend fun validatePoetryExecutable(poetryExecutable: Path?): ValidationInfo? =
@Internal
suspend fun runPoetryWithSdk(sdk: Sdk, vararg args: String): Result {
val projectPath = sdk.associatedModulePath?.let { Path.of(it) } ?: return Result.failure(poetryNotFoundException) // Choose a correct sdk
- return reportSequentialProgress(2) { reporter ->
- reporter.itemStep {
- runPoetry(projectPath, "env", "use", sdk.homePath!!)
- }
- reporter.itemStep {
- runPoetry(projectPath, *args)
- }
- }
+ runPoetry(projectPath, "env", "use", sdk.homePath!!)
+ return runPoetry(projectPath, *args)
}
@@ -154,25 +148,25 @@ suspend fun getPythonExecutable(homePath: String): String = withContext(Dispatch
/**
* Installs a Python package using Poetry.
- * Runs `poetry add [pkg] [extraArgs]`
+ * Runs `poetry add [packages] [extraArgs]`
*
- * @param [pkg] The name of the package to be installed.
+ * @param [packages] The name of the package to be installed.
* @param [extraArgs] Additional arguments to pass to the Poetry add command.
*/
@Internal
-suspend fun poetryInstallPackage(sdk: Sdk, pkg: String, extraArgs: List): Result {
- val args = listOf("add", pkg) + extraArgs
+suspend fun poetryInstallPackage(sdk: Sdk, packages: List, extraArgs: List): Result {
+ val args = listOf("add") + packages + extraArgs
return runPoetryWithSdk(sdk, *args.toTypedArray())
}
/**
* Uninstalls a Python package using Poetry.
- * Runs `poetry remove [pkg]`
+ * Runs `poetry remove [packages]`
*
- * @param [pkg] The name of the package to be uninstalled.
+ * @param [packages] The name of the package to be uninstalled.
*/
@Internal
-suspend fun poetryUninstallPackage(sdk: Sdk, pkg: String): Result = runPoetryWithSdk(sdk, "remove", pkg)
+suspend fun poetryUninstallPackage(sdk: Sdk, vararg packages: String): Result = runPoetryWithSdk(sdk, "remove", *packages)
@Internal
fun parsePoetryShow(input: String): List {
diff --git a/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageManager.kt b/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageManager.kt
index aad5c54ca84b..3f92697cc840 100644
--- a/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageManager.kt
+++ b/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageManager.kt
@@ -2,9 +2,7 @@
package com.jetbrains.python.sdk.poetry
import com.intellij.openapi.project.Project
-import com.intellij.openapi.project.modules
import com.intellij.openapi.projectRoots.Sdk
-import com.jetbrains.python.packaging.PythonDependenciesExtractor
import com.jetbrains.python.packaging.common.PythonOutdatedPackage
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification
@@ -12,47 +10,32 @@ import com.jetbrains.python.packaging.management.PythonPackageInstallRequest
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.management.PythonRepositoryManager
import com.jetbrains.python.packaging.pip.PipRepositoryManager
-import com.jetbrains.python.sdk.pythonSdk
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.TestOnly
@ApiStatus.Internal
class PoetryPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(project, sdk) {
- @Volatile
- override var installedPackages: List = emptyList()
- override var dependencies: List = emptyList()
override val repositoryManager: PythonRepositoryManager = PipRepositoryManager(project)
-
override suspend fun installPackageCommand(installRequest: PythonPackageInstallRequest, options: List): Result {
- val packageWithVersion = when (installRequest) {
- is PythonPackageInstallRequest.ByRepositoryPythonPackageSpecification -> installRequest.specification.getPackageWithVersionInPoetryFormat()
- else -> {
- return Result.failure(UnsupportedOperationException("Poetry supports installing only single packages from repositories"))
- }
+ if (installRequest !is PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications) {
+ return Result.failure(UnsupportedOperationException("Poetry supports installing only packages from repositories"))
}
- poetryInstallPackage(sdk, packageWithVersion, options)
- .onFailure { return Result.failure(it) }
-
- return Result.success(Unit)
+ val packageSpecifications = installRequest.specifications
+ return addPackages(packageSpecifications, options)
}
- override suspend fun updatePackageCommand(specification: PythonRepositoryPackageSpecification): Result {
- poetryInstallPackage(sdk, specification.getPackageWithVersionInPoetryFormat(), emptyList())
- .onFailure { return Result.failure(it) }
-
- return Result.success(Unit)
+ override suspend fun updatePackageCommand(vararg specifications: PythonRepositoryPackageSpecification): Result {
+ return addPackages(specifications.map { it.copy(versionSpec = null) }, emptyList())
}
- override suspend fun uninstallPackageCommand(pkg: PythonPackage): Result {
- poetryUninstallPackage(sdk, pkg.name)
- .onFailure { return Result.failure(it) }
-
- return Result.success(Unit)
+ override suspend fun uninstallPackageCommand(vararg pythonPackages: String): Result {
+ return poetryUninstallPackage(sdk, *pythonPackages).map { }
}
- override suspend fun reloadPackagesCommand(): Result> {
+
+ override suspend fun loadPackagesCommand(): Result> {
val (installed, _) = poetryListPackages(sdk).getOrElse {
return Result.failure(it)
}
@@ -68,14 +51,17 @@ class PoetryPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(pr
it.values.toList()
}
- override suspend fun reloadDependencies(): List {
- val dependenciesExtractor = PythonDependenciesExtractor.forSdk(sdk) ?: return emptyList()
- val targetModule = project.modules.find { it.pythonSdk == sdk } ?: return emptyList()
- dependencies = dependenciesExtractor.extract(targetModule)
- return dependencies
+ private suspend fun addPackages(
+ packageSpecifications: List,
+ options: List,
+ ): Result {
+ val specifications = packageSpecifications.map {
+ it.getPackageWithVersionInPoetryFormat()
+ }
+
+ return poetryInstallPackage(sdk, specifications, options).map { }
}
- override fun listDependencies(): List = dependencies
private fun PythonRepositoryPackageSpecification.getPackageWithVersionInPoetryFormat(): String {
return versionSpec?.let { "$name@${it.presentableText}" } ?: name
diff --git a/python/src/com/jetbrains/python/sdk/uv/Uv.kt b/python/src/com/jetbrains/python/sdk/uv/Uv.kt
index c28930796d61..115755c273e0 100644
--- a/python/src/com/jetbrains/python/sdk/uv/Uv.kt
+++ b/python/src/com/jetbrains/python/sdk/uv/Uv.kt
@@ -24,14 +24,14 @@ interface UvLowLevel {
/**
* Manage project dependencies by adding/removing them to the project along side installation
*/
- suspend fun addDependency(name: PythonPackageInstallRequest, options: List): PyExecResult
- suspend fun removeDependency(name: PythonPackage): PyExecResult
+ suspend fun addDependency(pyPackages: PythonPackageInstallRequest, options: List): PyExecResult
+ suspend fun removeDependencies(pyPackages: Array): PyExecResult
/**
* Managing environment packages directly w/o depending or changing the project
*/
suspend fun installPackage(name: PythonPackageInstallRequest, options: List): PyExecResult
- suspend fun uninstallPackage(name: PythonPackage): PyExecResult
+ suspend fun uninstallPackages(pyPackages: Array): PyExecResult
suspend fun listPackages(): PyExecResult>
suspend fun listOutdatedPackages(): PyResult>
diff --git a/python/src/com/jetbrains/python/sdk/uv/UvPackageManager.kt b/python/src/com/jetbrains/python/sdk/uv/UvPackageManager.kt
index 5cf0fdc4e982..79300e6f4cff 100644
--- a/python/src/com/jetbrains/python/sdk/uv/UvPackageManager.kt
+++ b/python/src/com/jetbrains/python/sdk/uv/UvPackageManager.kt
@@ -2,27 +2,24 @@
package com.jetbrains.python.sdk.uv
import com.intellij.openapi.project.Project
-import com.intellij.openapi.project.modules
import com.intellij.openapi.projectRoots.Sdk
import com.jetbrains.python.errorProcessing.PyExecResult
import com.jetbrains.python.errorProcessing.asKotlinResult
-import com.jetbrains.python.packaging.PythonDependenciesExtractor
import com.jetbrains.python.packaging.common.PythonOutdatedPackage
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification
-import com.jetbrains.python.packaging.management.*
+import com.jetbrains.python.packaging.management.PythonPackageInstallRequest
+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.pythonSdk
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, private val uv: UvLowLevel) : PythonPackageManager(project, sdk) {
- override var installedPackages: List = emptyList()
- override var dependencies: List = emptyList()
override val repositoryManager: PythonRepositoryManager = PipRepositoryManager(project)
-
override suspend fun installPackageCommand(installRequest: PythonPackageInstallRequest, options: List): Result {
val result = if (sdk.uvUsePackageManagement) {
uv.installPackage(installRequest, emptyList())
@@ -38,20 +35,22 @@ internal class UvPackageManager(project: Project, sdk: Sdk, private val uv: UvLo
return Result.success(Unit)
}
- override suspend fun updatePackageCommand(specification: PythonRepositoryPackageSpecification): Result {
- installPackageCommand(specification.toInstallRequest(), emptyList()).getOrElse {
+ override suspend fun updatePackageCommand(vararg specifications: PythonRepositoryPackageSpecification): Result {
+ val specsWithoutVersion = specifications.map { it.copy(versionSpec = null) }
+ val request = PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications(specsWithoutVersion)
+ installPackageCommand(request, emptyList()).getOrElse {
return Result.failure(it)
}
return Result.success(Unit)
}
- override suspend fun uninstallPackageCommand(pkg: PythonPackage): Result {
+ override suspend fun uninstallPackageCommand(vararg pythonPackages: String): Result {
val result = if (sdk.uvUsePackageManagement) {
- uv.uninstallPackage(pkg)
+ uv.uninstallPackages(pythonPackages)
}
else {
- uv.removeDependency(pkg)
+ uv.removeDependencies(pythonPackages)
}.asKotlinResult()
result.getOrElse {
@@ -61,7 +60,7 @@ internal class UvPackageManager(project: Project, sdk: Sdk, private val uv: UvLo
return Result.success(Unit)
}
- override suspend fun reloadPackagesCommand(): Result> {
+ override suspend fun loadPackagesCommand(): Result> {
return uv.listPackages().asKotlinResult()
}
@@ -76,15 +75,6 @@ internal class UvPackageManager(project: Project, sdk: Sdk, private val uv: UvLo
suspend fun lock(): PyExecResult {
return uv.lock()
}
-
- override suspend fun reloadDependencies(): List {
- val dependenciesExtractor = PythonDependenciesExtractor.forSdk(sdk) ?: return emptyList()
- val targetModule = project.modules.find { it.pythonSdk == sdk } ?: return emptyList()
- dependencies = dependenciesExtractor.extract(targetModule)
- return dependencies
- }
-
- override fun listDependencies(): List = dependencies
}
class UvPackageManagerProvider : PythonPackageManagerProvider {
diff --git a/python/src/com/jetbrains/python/sdk/uv/impl/UvCli.kt b/python/src/com/jetbrains/python/sdk/uv/impl/UvCli.kt
index 0f0097f4d9af..019626419fb4 100644
--- a/python/src/com/jetbrains/python/sdk/uv/impl/UvCli.kt
+++ b/python/src/com/jetbrains/python/sdk/uv/impl/UvCli.kt
@@ -19,6 +19,7 @@ import kotlinx.coroutines.withContext
import java.nio.file.Path
import kotlin.io.path.exists
import kotlin.io.path.pathString
+import kotlin.time.Duration.Companion.minutes
private const val UV_PATH_SETTING: String = "PyCharm.Uv.Path"
@@ -40,7 +41,7 @@ private fun validateUvExecutable(uvPath: Path?): ValidationInfo? {
}
private suspend fun runUv(uv: Path, workingDir: Path, vararg args: String): PyExecResult {
- return runExecutableWithProgress(uv, workingDir, *args)
+ return runExecutableWithProgress(uv, workingDir, 10.minutes, *args)
}
private class UvCliImpl(val dispatcher: CoroutineDispatcher, uvPath: Path?) : UvCli {
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 2258fe9fec52..64e8132e51cb 100644
--- a/python/src/com/jetbrains/python/sdk/uv/impl/UvLowLevel.kt
+++ b/python/src/com/jetbrains/python/sdk/uv/impl/UvLowLevel.kt
@@ -142,29 +142,29 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev
}
override suspend fun installPackage(name: PythonPackageInstallRequest, options: List): PyExecResult {
- uvCli.runUv(cwd, "pip", "install", name.formatPackageName(), *options.toTypedArray())
+ uvCli.runUv(cwd, "pip", "install", *name.formatPackageName(), *options.toTypedArray())
.onFailure { return PyResult.failure(it) }
return PyExecResult.success(Unit)
}
- override suspend fun uninstallPackage(name: PythonPackage): PyExecResult {
+ override suspend fun uninstallPackages(pyPackages: Array): PyExecResult {
// TODO: check if package is in dependencies and reject it
- uvCli.runUv(cwd, "pip", "uninstall", name.name)
+ uvCli.runUv(cwd, "pip", "uninstall", *pyPackages)
.onFailure { return PyResult.failure(it) }
return PyExecResult.success(Unit)
}
- override suspend fun addDependency(name: PythonPackageInstallRequest, options: List): PyExecResult {
- uvCli.runUv(cwd, "add", name.formatPackageName(), *options.toTypedArray())
+ override suspend fun addDependency(pyPackages: PythonPackageInstallRequest, options: List