From 21165f01255af5954a9869475d2770e071a0db1e Mon Sep 17 00:00:00 2001 From: "Nikita.Ashihmin" Date: Tue, 20 May 2025 18:21:24 +0400 Subject: [PATCH] PY-81238 Packages: Support multipackages install, remove, update GitOrigin-RevId: 06378dff9ec4ea73cdf7b066f27276e896e19417 --- .../evaluation/SetupPythonInterpreterStep.kt | 10 +- .../intellij.python.community.impl.xml | 9 +- .../messages/PyBundle.properties | 13 +- .../BlackFormatterConfigurable.kt | 2 +- .../quickfix/InstallAllPackagesQuickFix.kt | 6 +- .../python/newProjectWizard/PyV3Utils.kt | 7 +- .../python/packaging/PyPackageInstallUtils.kt | 76 +++-- .../PythonPackageManagementServiceBridge.kt | 31 +- .../python/packaging/common/packages.kt | 25 +- .../jetbrains/python/packaging/common/util.kt | 4 +- .../conda/CompositePythonPackageManager.kt | 131 -------- ...anager.kt => CondaPackageManagerEngine.kt} | 91 +++--- .../conda/CondaPackageManagerProvider.kt | 9 +- .../packaging/conda/CondaPackageRepository.kt | 44 +++ .../CondaPackagingToolwindowActionProvider.kt | 2 +- .../packaging/conda/CondaRepositoryManger.kt | 2 +- .../CondaWithPipFallbackPackageManager.kt | 112 +++++++ .../python/packaging/conda/common.kt | 23 -- .../management/PythonPackageInstallRequest.kt | 19 ++ .../management/PythonPackageInstaller.kt | 24 +- .../management/PythonPackageManager.kt | 306 +++++++++--------- .../management/PythonPackageManagerEngine.kt | 25 ++ .../management/PythonPackageManagerExt.kt | 37 +-- .../management/PythonPackageManagerRunner.kt | 5 +- .../PythonPackageManagerUIHelpers.kt | 34 ++ .../packaging/pip/PipPackageManagerEngine.kt | 227 +++++++++++++ .../packaging/pip/PipPythonPackageManager.kt | 105 +----- .../repository/PyPackageRepository.kt | 36 ++- .../PyPackagingToolWindowService.kt | 55 ++-- .../actions/ChangeVersionPackageAction.kt | 2 +- .../toolwindow/actions/DeletePackageAction.kt | 17 +- .../actions/InstallPackageAction.kt | 16 +- .../InstallWithOptionsPackageAction.kt | 4 +- .../actions/UpdatePackageToLatestAction.kt | 10 +- .../details/PyPackageDescriptionController.kt | 17 +- .../toolwindow/details/PyPackageInfoPanel.kt | 2 +- .../toolwindow/model/modelComponents.kt | 4 - .../table/PyPackageTableMouseAdapter.kt | 22 +- .../toolwindow/ui/PyPackagesUiComponents.kt | 4 +- .../RequirementsCompletionUtil.kt | 2 +- .../RequirementsInspectionVisitor.kt | 4 +- .../UnsatisfiedRequirementInspection.kt | 245 -------------- .../outdated/OutdatedRequirementInspection.kt | 13 - .../OutdatedRequirementInspectionVisitor.kt | 59 ---- .../InstallAllRequirementsQuickFix.kt | 42 +++ .../quickfixes/InstallRequirementQuickFix.kt | 33 ++ .../PyGenerateRequirementsFileQuickFix.kt | 24 ++ .../UpdateAllRequirementQuickFix.kt | 6 +- .../quickfixes/UpdateRequirementQuickFix.kt | 6 +- .../NotInstalledRequirementInspection.kt | 56 ++++ .../tools/OutdatedRequirementInspection.kt | 77 +++++ .../run/filter/PythonInstallPackageFilter.kt | 5 +- .../runAnything/PyRunAnythingCondaProvider.kt | 10 +- .../PyRunAnythingPackageProvider.kt | 2 +- .../python/sdk/PySdkCommandRunner.kt | 6 +- .../python/sdk/PythonSdkUpdater.java | 7 +- .../sdk/pipenv/PipenvCommandExecutor.kt | 3 +- .../sdk/poetry/PoetryCommandExecutor.kt | 28 +- .../python/sdk/poetry/PoetryPackageManager.kt | 52 ++- python/src/com/jetbrains/python/sdk/uv/Uv.kt | 6 +- .../python/sdk/uv/UvPackageManager.kt | 34 +- .../com/jetbrains/python/sdk/uv/impl/UvCli.kt | 3 +- .../python/sdk/uv/impl/UvLowLevel.kt | 18 +- .../management/TestPackageManager.kt | 35 +- ...ackageManagerManagementInstallationTest.kt | 9 +- ...honPackageManagerNullAdditionalDataTest.kt | 7 +- .../UnsatisfiedRequirementInspectionTest.kt | 3 +- 67 files changed, 1228 insertions(+), 1135 deletions(-) delete mode 100644 python/src/com/jetbrains/python/packaging/conda/CompositePythonPackageManager.kt rename python/src/com/jetbrains/python/packaging/conda/{CondaPackageManager.kt => CondaPackageManagerEngine.kt} (62%) create mode 100644 python/src/com/jetbrains/python/packaging/conda/CondaPackageRepository.kt create mode 100644 python/src/com/jetbrains/python/packaging/conda/CondaWithPipFallbackPackageManager.kt create mode 100644 python/src/com/jetbrains/python/packaging/management/PythonPackageInstallRequest.kt create mode 100644 python/src/com/jetbrains/python/packaging/management/PythonPackageManagerEngine.kt create mode 100644 python/src/com/jetbrains/python/packaging/management/PythonPackageManagerUIHelpers.kt create mode 100644 python/src/com/jetbrains/python/packaging/pip/PipPackageManagerEngine.kt delete mode 100644 python/src/com/jetbrains/python/requirements/UnsatisfiedRequirementInspection.kt delete mode 100644 python/src/com/jetbrains/python/requirements/inspections/outdated/OutdatedRequirementInspection.kt delete mode 100644 python/src/com/jetbrains/python/requirements/inspections/outdated/OutdatedRequirementInspectionVisitor.kt create mode 100644 python/src/com/jetbrains/python/requirements/inspections/quickfixes/InstallAllRequirementsQuickFix.kt create mode 100644 python/src/com/jetbrains/python/requirements/inspections/quickfixes/InstallRequirementQuickFix.kt create mode 100644 python/src/com/jetbrains/python/requirements/inspections/quickfixes/PyGenerateRequirementsFileQuickFix.kt rename python/src/com/jetbrains/python/requirements/inspections/{outdated => }/quickfixes/UpdateAllRequirementQuickFix.kt (84%) rename python/src/com/jetbrains/python/requirements/inspections/{outdated => }/quickfixes/UpdateRequirementQuickFix.kt (82%) create mode 100644 python/src/com/jetbrains/python/requirements/inspections/tools/NotInstalledRequirementInspection.kt create mode 100644 python/src/com/jetbrains/python/requirements/inspections/tools/OutdatedRequirementInspection.kt 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): PyExecResult { + uvCli.runUv(cwd, "add", *pyPackages.formatPackageName(), *options.toTypedArray()) .onFailure { return PyResult.failure(it) } return PyExecResult.success(Unit) } - override suspend fun removeDependency(name: PythonPackage): PyExecResult { - uvCli.runUv(cwd, "remove", name.name) + override suspend fun removeDependencies(pyPackages: Array): PyExecResult { + uvCli.runUv(cwd, "remove", *pyPackages) .onFailure { return PyResult.failure(it) } return PyExecResult.success(Unit) @@ -218,8 +218,8 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev return args } - fun PythonPackageInstallRequest.formatPackageName(): String = when (this) { - is PythonPackageInstallRequest.ByRepositoryPythonPackageSpecification -> specification.nameWithVersionSpec + fun PythonPackageInstallRequest.formatPackageName(): Array = when (this) { + is PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications -> specifications.map { it.nameWithVersionSpec }.toTypedArray() is PythonPackageInstallRequest.AllRequirements -> error("UV supports only single requirement installation") is PythonPackageInstallRequest.ByLocation -> error("UV does not support installing from location uri") } diff --git a/python/testSrc/com/jetbrains/python/packaging/management/TestPackageManager.kt b/python/testSrc/com/jetbrains/python/packaging/management/TestPackageManager.kt index f57b0ca12054..dbee5f2722de 100644 --- a/python/testSrc/com/jetbrains/python/packaging/management/TestPackageManager.kt +++ b/python/testSrc/com/jetbrains/python/packaging/management/TestPackageManager.kt @@ -11,7 +11,6 @@ import org.jetbrains.annotations.TestOnly @TestOnly class TestPythonPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(project, sdk) { - override var installedPackages: List = DEFAULT_PACKAGES.toMutableList() override var dependencies: List = emptyList() private var packageNames: List = emptyList() @@ -25,41 +24,39 @@ class TestPythonPackageManager(project: Project, sdk: Sdk) : PythonPackageManage } override suspend fun installPackageCommand(installRequest: PythonPackageInstallRequest, options: List): Result { - if (installRequest !is PythonPackageInstallRequest.ByRepositoryPythonPackageSpecification) { + if (installRequest !is PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications) { return Result.failure(Exception("Test Manager supports only simple repository package specification")) } - return if (repositoryManager.allPackages().contains(installRequest.specification.name)) { - val version = installRequest.specification.versionSpec?.version.orEmpty() - installedPackages += PythonPackage(installRequest.specification.name, version, false) + val specification = installRequest.specifications.single() + return if (repositoryManager.allPackages().contains(specification.name)) { + val version = specification.versionSpec?.version.orEmpty() + installedPackages += PythonPackage(specification.name, version, false) Result.success(Unit) - } else { + } + else { Result.failure(Exception(PACKAGE_INSTALL_FAILURE_MESSAGE)) } } - override suspend fun updatePackageCommand(specification: PythonRepositoryPackageSpecification): Result { + override suspend fun updatePackageCommand(vararg specifications: PythonRepositoryPackageSpecification): Result { return Result.success(Unit) } - override suspend fun uninstallPackageCommand(pkg: PythonPackage): Result { - val packageToRemove = findPackageByName(pkg.name) - return if (packageToRemove != null) { + override suspend fun uninstallPackageCommand(vararg pythonPackages: String): Result { + pythonPackages.forEach { pyPackage -> + val packageToRemove = findPackageByName(pyPackage) + ?: return Result.failure(Exception(PACKAGE_UNINSTALL_FAILURE_MESSAGE)) installedPackages -= packageToRemove - Result.success(Unit) - } else { - Result.failure(Exception(PACKAGE_UNINSTALL_FAILURE_MESSAGE)) } + + return Result.success(Unit) } - override suspend fun reloadPackagesCommand(): Result> { + override suspend fun loadPackagesCommand(): Result> { return Result.success(installedPackages) } - override suspend fun reloadDependencies(): List = dependencies - - override fun listDependencies(): List = dependencies - private fun findPackageByName(name: String): PythonPackage? { return installedPackages.find { it.name == name } } @@ -94,7 +91,7 @@ class TestPythonPackageManager(project: Project, sdk: Sdk) : PythonPackageManage } @TestOnly -class TestPythonPackageManagerService(val installedPackages: List = emptyList()): PythonPackageManagerService { +class TestPythonPackageManagerService(val installedPackages: List = emptyList()) : PythonPackageManagerService { override fun forSdk(project: Project, sdk: Sdk): PythonPackageManager { installedPackages.ifEmpty { diff --git a/python/testSrc/com/jetbrains/python/packaging/pythonPackageManager/PythonPackageManagerManagementInstallationTest.kt b/python/testSrc/com/jetbrains/python/packaging/pythonPackageManager/PythonPackageManagerManagementInstallationTest.kt index 6d3e37c2849c..f5b137ae6fac 100644 --- a/python/testSrc/com/jetbrains/python/packaging/pythonPackageManager/PythonPackageManagerManagementInstallationTest.kt +++ b/python/testSrc/com/jetbrains/python/packaging/pythonPackageManager/PythonPackageManagerManagementInstallationTest.kt @@ -6,7 +6,6 @@ import com.jetbrains.env.EnvTestTagsRequired import com.jetbrains.env.PyEnvTestCase import com.jetbrains.env.PyExecutionFixtureTestTask import com.jetbrains.python.packaging.PyPackageUtil -import com.jetbrains.python.packaging.common.PythonPackage import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification import com.jetbrains.python.packaging.management.PythonPackageManager import com.jetbrains.python.packaging.management.toInstallRequest @@ -22,7 +21,7 @@ import org.junit.Test class PythonPackageManagerManagementInstallationTest : PyEnvTestCase() { companion object { - private val PKG_TO_INSTALL = PyPIPackageRepository.createPackageSpecification("pytest") + private val PKG_TO_INSTALL = PyPIPackageRepository.findPackageSpecification("pytest")!! } @EnvTestTagsRequired(tags = ["python3.8"]) @@ -79,12 +78,12 @@ open class PythonPackageManagerManagementInstallationTask(private val pkgToInsta } private suspend fun uninstallManagement(manager: PythonPackageManager) { - manager.uninstallPackage(PythonPackage(PyPackageUtil.PIP, EMPTY_STRING, false)) - manager.uninstallPackage(PythonPackage(PyPackageUtil.SETUPTOOLS, EMPTY_STRING, false)) + manager.uninstallPackage(PyPackageUtil.PIP) + manager.uninstallPackage(PyPackageUtil.SETUPTOOLS) } private suspend fun installPackage(manager: PythonPackageManager, spec: PythonRepositoryPackageSpecification) { - manager.installPackage(spec.toInstallRequest(), emptyList(), withBackgroundProgress = false) + manager.installPackage(spec.toInstallRequest(), emptyList()) } companion object { diff --git a/python/testSrc/com/jetbrains/python/packaging/pythonPackageManager/PythonPackageManagerNullAdditionalDataTest.kt b/python/testSrc/com/jetbrains/python/packaging/pythonPackageManager/PythonPackageManagerNullAdditionalDataTest.kt index 3097a5204adb..58255636ea45 100644 --- a/python/testSrc/com/jetbrains/python/packaging/pythonPackageManager/PythonPackageManagerNullAdditionalDataTest.kt +++ b/python/testSrc/com/jetbrains/python/packaging/pythonPackageManager/PythonPackageManagerNullAdditionalDataTest.kt @@ -9,7 +9,6 @@ import com.intellij.openapi.projectRoots.SdkTypeId import com.jetbrains.env.EnvTestTagsRequired import com.jetbrains.env.PyEnvTestCase import com.jetbrains.env.PyExecutionFixtureTestTask -import com.jetbrains.python.packaging.common.PythonPackage import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification import com.jetbrains.python.packaging.management.PythonPackageManager import com.jetbrains.python.packaging.management.toInstallRequest @@ -23,7 +22,7 @@ import org.junit.Test class PythonPackageManagerNullAdditionalDataTest : PyEnvTestCase() { companion object { - private val PKG = requireNotNull(PyPIPackageRepository.createPackageSpecification("requests")) + private val PKG = requireNotNull(PyPIPackageRepository.findPackageSpecification("requests")) } @EnvTestTagsRequired(tags = ["python3.8"]) @@ -89,10 +88,10 @@ class PythonPackageManagerNullAdditionalDataTask(private val pkg: PythonReposito val configuredSdk = createSdkWithNullAdditionalData(pythonSdkType, sdkHome, existingSdk) val manager = PythonPackageManager.forSdk(myFixture.project, configuredSdk) - manager.installPackage(pkg.toInstallRequest(), emptyList(), withBackgroundProgress = false) + manager.installPackage(pkg.toInstallRequest(), emptyList()) assertTrue("Package should be installed", manager.installedPackages.map { it.name }.contains(pkg.name)) - manager.uninstallPackage(PythonPackage(pkg.name, pkg.versionSpec?.version.orEmpty(), false)) + manager.uninstallPackage(pkg.name) assertTrue("Package should be uninstalled", !manager.installedPackages.map { it.name }.contains(pkg.name)) } } diff --git a/python/testSrc/com/jetbrains/python/requirements/UnsatisfiedRequirementInspectionTest.kt b/python/testSrc/com/jetbrains/python/requirements/UnsatisfiedRequirementInspectionTest.kt index 19fa0a38aaab..99dbc32f8676 100644 --- a/python/testSrc/com/jetbrains/python/requirements/UnsatisfiedRequirementInspectionTest.kt +++ b/python/testSrc/com/jetbrains/python/requirements/UnsatisfiedRequirementInspectionTest.kt @@ -4,6 +4,7 @@ package com.jetbrains.python.requirements import com.intellij.codeInspection.ex.InspectionProfileImpl import com.intellij.lang.annotation.HighlightSeverity import com.intellij.testFramework.TestDataPath +import com.jetbrains.python.requirements.inspections.tools.NotInstalledRequirementInspection import com.jetbrains.python.sdk.pythonSdk @TestDataPath("\$CONTENT_ROOT/../testData/requirements/inspections") @@ -32,7 +33,7 @@ class UnsatisfiedRequirementInspectionTest : PythonDependencyTestCase() { private fun doMultiFileTest(filename: String) { myFixture.copyDirectoryToProject(getTestName(false), "") myFixture.configureFromTempProjectFile(filename) - myFixture.enableInspections(UnsatisfiedRequirementInspection::class.java) + myFixture.enableInspections(NotInstalledRequirementInspection::class.java) myFixture.checkHighlighting(true, false, true, false) }