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 b8b9998704eb..7f3b9960f627 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 @@ -20,18 +20,19 @@ import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil import com.intellij.openapi.roots.ProjectRootManager 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.PyRequirement +import com.jetbrains.python.packaging.PyRequirementParser import com.jetbrains.python.packaging.management.PythonPackageManager +import com.jetbrains.python.packaging.management.ui.PythonPackageManagerUI +import com.jetbrains.python.packaging.management.ui.installPyRequirementsBackground import com.jetbrains.python.packaging.pip.PipPythonPackageManager -import com.jetbrains.python.packaging.requirement.PyRequirementRelation import com.jetbrains.python.sdk.PythonSdkType import com.jetbrains.python.sdk.configuration.PyProjectSdkConfiguration import java.io.IOException import java.nio.file.Path import kotlin.io.path.Path import kotlin.io.path.exists +import kotlin.io.path.readText class SetupPythonInterpreterStepFactory(private val project: Project) : SetupSdkStepFactory { override fun isApplicable(language: Language): Boolean = language == Language.PYTHON @@ -143,47 +144,32 @@ private class SetupPythonInterpreterStep( } private suspend fun installPackages(sdk: Sdk) { - val packageManager = PythonPackageManager.forSdk(project, sdk) - packageManager.reloadPackages() - - val packages = readRequiredPackages(packageManager).filterNot { packageManager.isPackageInstalled(PythonPackage(it.name, "", false)) } + val packages = readRequiredPackages() if (packages.isEmpty()) { println("No packages to install. Skipping.") return } - val cacheOptions = if (preferences.cacheDir == null) emptyList() - else when (packageManager) { - is PipPythonPackageManager -> listOf("--cache-dir=${preferences.cacheDir}/pip") - else -> emptyList() - } - // resolves `'runBlockingCancellable' is forbidden in the Write Action` from PythonSdkUpdater.scheduleUpdate keepTasksAsynchronousInHeadlessMode { - val installRequest = PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications(packages) - - packageManager.installPackage(installRequest, cacheOptions) + val cacheOptions = if (preferences.cacheDir == null) emptyList() + else when (PythonPackageManager.forSdk(project, sdk)) { + is PipPythonPackageManager -> listOf("--cache-dir=${preferences.cacheDir}/pip") + else -> emptyList() + } + val packageManager = PythonPackageManagerUI.forSdk(project, sdk) + packageManager.installPyRequirementsBackground(packages, cacheOptions) ?: return@keepTasksAsynchronousInHeadlessMode println("Installed packages: ${packages.joinToString(", ") { it.name }}") } } - private fun readRequiredPackages(packageManager: PythonPackageManager): List { + private fun readRequiredPackages(): List { val projectPath = project.basePath ?: return emptyList() val requirementsTxt = Path.of(projectPath).resolve("requirements.txt") if (!requirementsTxt.exists()) { return emptyList() } - return requirementsTxt.toFile().inputStream().bufferedReader().use { reader -> - 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.findPackageSpecification( - packageName = parts[0], - version = parts.getOrNull(1), - relation = relation ?: PyRequirementRelation.EQ - ) - } - } + return PyRequirementParser.fromText(requirementsTxt.readText()) } private fun isProjectLocal(path: String?): Boolean { diff --git a/python/huggingFace/src/com/intellij/python/community/impl/huggingFace/service/HuggingFaceLibrariesTracker.kt b/python/huggingFace/src/com/intellij/python/community/impl/huggingFace/service/HuggingFaceLibrariesTracker.kt index 8cf6fe563573..26fa99597694 100644 --- a/python/huggingFace/src/com/intellij/python/community/impl/huggingFace/service/HuggingFaceLibrariesTracker.kt +++ b/python/huggingFace/src/com/intellij/python/community/impl/huggingFace/service/HuggingFaceLibrariesTracker.kt @@ -8,9 +8,9 @@ import com.intellij.openapi.project.modules import com.intellij.openapi.projectRoots.Sdk import com.intellij.python.community.impl.huggingFace.cache.HuggingFaceCacheFillService import com.intellij.util.messages.MessageBusConnection -import com.jetbrains.python.packaging.PyPackageInstallUtils import com.jetbrains.python.packaging.common.PythonPackageManagementListener import com.jetbrains.python.packaging.management.PythonPackageManager +import com.jetbrains.python.packaging.management.hasInstalledPackage import com.jetbrains.python.sdk.PythonSdkUtil import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -60,7 +60,7 @@ class HuggingFaceLibrariesTracker( private fun getProjectPythonSdk(): Sdk? = PythonSdkUtil.findPythonSdk(project.modules.firstOrNull()) - private fun updateHFLibraryInstallStatus() { + private suspend fun updateHFLibraryInstallStatus() { if (isAnyHFLibraryInstalled) return // assuming that if was found once - always relevant val sdk = getProjectPythonSdk() ?: return @@ -72,9 +72,12 @@ class HuggingFaceLibrariesTracker( } } - private fun isAnyHFLibraryInstalledInSdk(sdk: Sdk): Boolean = relevantLibraries.any { lib -> - PyPackageInstallUtils.getPackageVersion(project, sdk, lib) != null + private suspend fun isAnyHFLibraryInstalledInSdk(sdk: Sdk): Boolean { + val packageManager = PythonPackageManager.forSdk(project, sdk) + return relevantLibraries.any { lib -> + packageManager.hasInstalledPackage(lib) + } } - override fun dispose() = detachSdkListener() + override fun dispose(): Unit = detachSdkListener() } \ No newline at end of file diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/newProjectWizard/impl/emptyProject/PyV3EmptyProjectSettings.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/newProjectWizard/impl/emptyProject/PyV3EmptyProjectSettings.kt index be1d71375745..9dc1d77ee83e 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/newProjectWizard/impl/emptyProject/PyV3EmptyProjectSettings.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/newProjectWizard/impl/emptyProject/PyV3EmptyProjectSettings.kt @@ -9,16 +9,16 @@ import com.intellij.openapi.project.rootManager import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.vfs.VirtualFile import com.intellij.pycharm.community.ide.impl.newProjectWizard.welcome.PyWelcome -import com.jetbrains.python.Result -import com.jetbrains.python.errorProcessing.PyResult +import com.jetbrains.python.errorProcessing.ErrorSink import com.jetbrains.python.newProjectWizard.PyV3ProjectTypeSpecificSettings import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext class PyV3EmptyProjectSettings(var generateWelcomeScript: Boolean = false) : PyV3ProjectTypeSpecificSettings { - override suspend fun generateProject(module: Module, baseDir: VirtualFile, sdk: Sdk): PyResult { - if (!generateWelcomeScript) return Result.success(Unit) + override suspend fun generateProject(module: Module, baseDir: VirtualFile, sdk: Sdk, errorSink: ErrorSink) { + if (!generateWelcomeScript) + return val sourceRoot = module.rootManager.sourceRoots.firstOrNull() ?: baseDir val file = edtWriteAction { @@ -29,8 +29,6 @@ class PyV3EmptyProjectSettings(var generateWelcomeScript: Boolean = false) : PyV file.navigate(true) } } - - return Result.success(Unit) } override fun toString(): String { diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties index 4d209090438a..089e8118a38b 100644 --- a/python/pluginResources/messages/PyBundle.properties +++ b/python/pluginResources/messages/PyBundle.properties @@ -973,6 +973,8 @@ 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.list.packages=Loading installed packages\u2026 +python.packaging.check.is.package.installed=Checking is package {0} installed\u2026 python.packaging.installing.requirements=Installing requirements\u2026 python.packaging.installing.packages=Installing packages\u2026 python.packaging.installing.package=Installing package {0}\u2026 @@ -1027,12 +1029,9 @@ python.packaging.notification.description.details.link=Details\u2026 python.packaging.notification.description.installed.all.requirements=Installed all requirements python.packaging.notification.description.installed.packages=Installed packages: {0} python.packaging.notification.description.installed.python.packaging.tools=Installed Python packaging tools -python.packaging.notification.description.uninstalled.packages=Uninstalled packages: {0} python.packaging.notification.title.error.occurred={0}: error occurred. python.packaging.notification.title.install.packages.failed=Installing packages failed python.packaging.notification.title.packages.installed.successfully=Packages installed successfully -python.packaging.notification.title.packages.uninstalled.successfully=Packages uninstalled successfully -python.packaging.notification.title.uninstall.packages.failed=Uninstalling packages failed python.packaging.progress.text.installing.specific.package=Installing package ''{0}''\u2026 python.packaging.progress.title.installing.packages=Installing packages python.packaging.progress.title.uninstalling.packages=Uninstalling packages diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/stdlib/PyStdlibUtil.java b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/stdlib/PyStdlibUtil.java index 80dc6b13be0b..d84246162d81 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/stdlib/PyStdlibUtil.java +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/stdlib/PyStdlibUtil.java @@ -22,7 +22,7 @@ public final class PyStdlibUtil { private PyStdlibUtil() { } - public static @Nullable Collection getPackages() { + public static @Nullable Set getPackages() { return PACKAGES.get(); } diff --git a/python/python-psi-impl/src/com/jetbrains/python/packaging/PyRequirements.kt b/python/python-psi-impl/src/com/jetbrains/python/packaging/PyRequirements.kt index 2a81033919fa..2f099bebcf07 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/packaging/PyRequirements.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/packaging/PyRequirements.kt @@ -17,7 +17,10 @@ import com.jetbrains.python.packaging.requirement.PyRequirementVersionSpec * @see PyRequirementParser.fromText * @see PyRequirementParser.fromFile */ -fun pyRequirement(name: String): PyRequirement = PyRequirementImpl(name, emptyList(), listOf(name), "") +fun pyRequirement(name: String, versionSpec: PyRequirementVersionSpec? = null): PyRequirement = PyRequirementImpl(name, + listOfNotNull(versionSpec), + listOf(name), + "") /** * This helper is not an API, consider using methods listed below. @@ -32,6 +35,7 @@ fun pyRequirement(name: String): PyRequirement = PyRequirementImpl(name, emptyLi fun pyRequirement(name: String, relation: PyRequirementRelation, version: String): PyRequirement = pyRequirement(name, relation, version, "") + /** * This helper is not an API, consider using methods listed below. * If given version could not be normalized, then specified relation will be replaced with [PyRequirementRelation.STR_EQ]. diff --git a/python/python-restructuredtext/src/com/intellij/restructuredtext/python/RestPythonUtil.java b/python/python-restructuredtext/src/com/intellij/restructuredtext/python/RestPythonUtil.java deleted file mode 100644 index f58a287d371f..000000000000 --- a/python/python-restructuredtext/src/com/intellij/restructuredtext/python/RestPythonUtil.java +++ /dev/null @@ -1,42 +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.intellij.restructuredtext.python; - -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.actionSystem.CommonDataKeys; -import com.intellij.openapi.actionSystem.PlatformCoreDataKeys; -import com.intellij.openapi.actionSystem.Presentation; -import com.intellij.openapi.module.Module; -import com.intellij.openapi.module.ModuleManager; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.projectRoots.Sdk; -import com.jetbrains.python.packaging.management.PythonPackageManager; -import com.jetbrains.python.packaging.management.PythonPackageManagerExt; -import com.jetbrains.python.sdk.PythonSdkUtil; - -/** - * User : catherine - */ -public final class RestPythonUtil { - private RestPythonUtil() {} - - public static Presentation updateSphinxQuickStartRequiredAction(final AnActionEvent e) { - final Presentation presentation = e.getPresentation(); - - final Project project = e.getData(CommonDataKeys.PROJECT); - if (project != null) { - Module module = e.getData(PlatformCoreDataKeys.MODULE); - if (module == null) { - Module[] modules = ModuleManager.getInstance(project).getModules(); - module = modules.length == 0 ? null : modules [0]; - } - if (module != null) { - final Sdk sdk = PythonSdkUtil.findPythonSdk(module); - if (sdk != null) { - PythonPackageManager manager = PythonPackageManager.Companion.forSdk(project, sdk); - presentation.setEnabled(PythonPackageManagerExt.isInstalled(manager, "Sphinx")); - } - } - } - return presentation; - } -} \ No newline at end of file diff --git a/python/python-restructuredtext/src/com/intellij/restructuredtext/python/RestPythonUtil.kt b/python/python-restructuredtext/src/com/intellij/restructuredtext/python/RestPythonUtil.kt new file mode 100644 index 000000000000..a0e9f121553b --- /dev/null +++ b/python/python-restructuredtext/src/com/intellij/restructuredtext/python/RestPythonUtil.kt @@ -0,0 +1,31 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.restructuredtext.python + +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.actionSystem.PlatformCoreDataKeys +import com.intellij.openapi.actionSystem.Presentation +import com.intellij.openapi.module.ModuleManager.Companion.getInstance +import com.jetbrains.python.packaging.management.PythonPackageManager +import com.jetbrains.python.packaging.management.hasInstalledPackageSnapshot +import com.jetbrains.python.sdk.PythonSdkUtil + +/** + * User : catherine + */ +object RestPythonUtil { + @JvmStatic + fun updateSphinxQuickStartRequiredAction(e: AnActionEvent): Presentation { + val presentation = e.presentation + + val project = e.getData(CommonDataKeys.PROJECT) ?: return presentation + val module = e.getData(PlatformCoreDataKeys.MODULE) + ?: getInstance(project).modules.firstOrNull() + ?: return presentation + + val sdk = PythonSdkUtil.findPythonSdk(module) ?: return presentation + val packageManager = PythonPackageManager.forSdk(project, sdk) + presentation.isEnabled = packageManager.hasInstalledPackageSnapshot("Sphinx") + return presentation + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/black/BlackFormatterUtil.kt b/python/src/com/jetbrains/python/black/BlackFormatterUtil.kt index 263eb0c2c524..0420948ab77b 100644 --- a/python/src/com/jetbrains/python/black/BlackFormatterUtil.kt +++ b/python/src/com/jetbrains/python/black/BlackFormatterUtil.kt @@ -13,10 +13,11 @@ import com.intellij.openapi.vfs.VirtualFile import com.jetbrains.python.PyBundle import com.jetbrains.python.PythonFileType import com.jetbrains.python.packaging.management.PythonPackageManager -import com.jetbrains.python.pyi.PyiFileType +import com.jetbrains.python.packaging.management.hasInstalledPackageSnapshot import com.jetbrains.python.pathValidation.PlatformAndRoot import com.jetbrains.python.pathValidation.ValidationRequest import com.jetbrains.python.pathValidation.validateExecutableFile +import com.jetbrains.python.pyi.PyiFileType import org.jetbrains.annotations.SystemDependent import java.io.File @@ -34,9 +35,7 @@ class BlackFormatterUtil { fun isBlackFormatterInstalledOnProjectSdk(project: Project, sdk: Sdk?): Boolean { val packageManager = sdk?.let { PythonPackageManager.forSdk(project, sdk) } - return packageManager?.let { - it.installedPackages.any { pyPackage -> pyPackage.name == PACKAGE_NAME } - } ?: false + return packageManager?.hasInstalledPackageSnapshot(PACKAGE_NAME) ?: false } fun detectBlackExecutable(): File? { diff --git a/python/src/com/jetbrains/python/black/BlackFormatterVersionService.kt b/python/src/com/jetbrains/python/black/BlackFormatterVersionService.kt index 10e45b31dcc1..264be3c58878 100644 --- a/python/src/com/jetbrains/python/black/BlackFormatterVersionService.kt +++ b/python/src/com/jetbrains/python/black/BlackFormatterVersionService.kt @@ -13,14 +13,14 @@ import com.intellij.openapi.progress.runBlockingCancellable import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.util.Version -import com.intellij.platform.ide.progress.withBackgroundProgress import com.intellij.util.VersionUtil -import com.jetbrains.python.PyBundle import com.jetbrains.python.black.BlackFormatterUtil.Companion.PACKAGE_NAME import com.jetbrains.python.black.configuration.BlackFormatterConfiguration import com.jetbrains.python.packaging.common.PythonPackage import com.jetbrains.python.packaging.common.PythonPackageManagementListener import com.jetbrains.python.packaging.management.PythonPackageManager +import com.jetbrains.python.packaging.management.getInstalledPackage +import com.jetbrains.python.packaging.management.ui.PythonPackageManagerUI import kotlinx.coroutines.* import java.util.regex.Pattern @@ -106,24 +106,16 @@ class BlackFormatterVersionService(private val project: Project, val serviceScop } } - fun getVersionForPackage(sdk: Sdk?, project: Project): Version { + suspend fun getVersionForPackage(sdk: Sdk?, project: Project): Version { return getBlackFormatterPackageInfo(sdk, project)?.let { pythonPackage -> parseVersionString(pythonPackage.version) } ?: UNKNOWN_VERSION } - private fun getBlackFormatterPackageInfo(sdk: Sdk?, project: Project): PythonPackage? { - val packageManager = sdk?.let { PythonPackageManager.forSdk(project, sdk) } ?: return null - if (packageManager.installedPackages.isEmpty()) { - runBlockingCancellable { - withBackgroundProgress(project, PyBundle.message("black.getting.black.version"), cancellable = true) { - packageManager.reloadPackages() - } - } - } - return packageManager.let { - it.installedPackages.firstOrNull { pyPackage -> pyPackage.name == PACKAGE_NAME } - } + private suspend fun getBlackFormatterPackageInfo(sdk: Sdk?, project: Project): PythonPackage? { + sdk ?: return null + val manager = PythonPackageManager.forSdk(project, sdk) + return manager.getInstalledPackage(PACKAGE_NAME) } private fun subscribeOnChanges() { diff --git a/python/src/com/jetbrains/python/black/configuration/BlackFormatterConfigurable.kt b/python/src/com/jetbrains/python/black/configuration/BlackFormatterConfigurable.kt index 513befd4300b..0720527dd50a 100644 --- a/python/src/com/jetbrains/python/black/configuration/BlackFormatterConfigurable.kt +++ b/python/src/com/jetbrains/python/black/configuration/BlackFormatterConfigurable.kt @@ -33,8 +33,8 @@ import com.jetbrains.python.black.BlackFormatterVersionService import com.jetbrains.python.black.BlackFormatterVersionService.Companion.UNKNOWN_VERSION import com.jetbrains.python.black.configuration.BlackFormatterConfiguration.BlackFormatterOption.Companion.toCliOptionFlags import com.jetbrains.python.newProject.steps.createPythonSdkComboBox -import com.jetbrains.python.onSuccess -import com.jetbrains.python.packaging.PyPackageInstallUtils +import com.jetbrains.python.packaging.management.ui.PythonPackageManagerUI +import com.jetbrains.python.packaging.management.ui.installPackageBackground import com.jetbrains.python.sdk.pythonSdk import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -132,7 +132,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).onSuccess { + PythonPackageManagerUI.forSdk(project, selectedSdk!!).installPackageBackground(BlackFormatterUtil.PACKAGE_NAME)?.let { withContext(Dispatchers.EDT) { isBlackFormatterPackageInstalled = true enableOnReformatCheckBox.isSelected = true diff --git a/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.java b/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.java index 6186e3635de1..7e93f1b5d5cb 100644 --- a/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.java +++ b/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.java @@ -165,7 +165,7 @@ public class PyIntegratedToolsConfigurable implements SearchableConfigurable { } } }); - ui.install(Collections.singletonList(PyRequirementsKt.pyRequirement(name)), Collections.emptyList()); + ui.install(Collections.singletonList(PyRequirementsKt.pyRequirement(name,null)), Collections.emptyList()); } }; } diff --git a/python/src/com/jetbrains/python/inspections/requirement/PyRequirementVisitor.kt b/python/src/com/jetbrains/python/inspections/requirement/PyRequirementVisitor.kt index 4ad8421b0c0b..4972f1e2619f 100644 --- a/python/src/com/jetbrains/python/inspections/requirement/PyRequirementVisitor.kt +++ b/python/src/com/jetbrains/python/inspections/requirement/PyRequirementVisitor.kt @@ -88,7 +88,7 @@ class PyRequirementVisitor( ignoredPackages: Set, ): List { val requirements = getRequirements(module) ?: return emptyList() - val installedPackages = manager.installedPackages + val installedPackages = manager.listInstalledPackagesSnapshot() val modulePackages = collectPackagesInModule(module) return requirements.filter { requirement -> @@ -198,7 +198,7 @@ class PyRequirementVisitor( val requirements = getListedRequirements(module) if (requirements == null) return null - val packages = packageManager.installedPackages + val packages = packageManager.listInstalledPackagesSnapshot() return getTransitiveRequirements(packages.toPyPackages(), requirements, HashSet()) + requirements } diff --git a/python/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesInspection.kt b/python/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesInspection.kt index 2e5aa7d420ec..510839c2dacb 100644 --- a/python/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesInspection.kt +++ b/python/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesInspection.kt @@ -181,7 +181,7 @@ class PyUnresolvedReferencesInspection : PyUnresolvedReferencesInspectionBase() } private fun createInstallAndImportQuickFix(project: Project, pythonSdk: Sdk, packageName: String, asName: String?): LocalQuickFix? { - return if (PyPackageInstallUtils.checkShouldToInstall(project, pythonSdk, packageName)) + return if (PyPackageInstallUtils.checkShouldToInstallSnapshot(project, pythonSdk, packageName)) InstallAndImportPackageQuickFix(packageName, asName) else null diff --git a/python/src/com/jetbrains/python/newProjectWizard/PyV3ProjectBaseGenerator.kt b/python/src/com/jetbrains/python/newProjectWizard/PyV3ProjectBaseGenerator.kt index 0b01591fc74f..78d27da8fe0b 100644 --- a/python/src/com/jetbrains/python/newProjectWizard/PyV3ProjectBaseGenerator.kt +++ b/python/src/com/jetbrains/python/newProjectWizard/PyV3ProjectBaseGenerator.kt @@ -20,7 +20,6 @@ import com.jetbrains.python.newProjectWizard.collector.PythonNewProjectWizardCol import com.jetbrains.python.newProjectWizard.impl.PyV3GeneratorPeer import com.jetbrains.python.newProjectWizard.impl.PyV3UIServicesProd import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathFlows.Companion.validatePath -import com.jetbrains.python.onFailure import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode import com.jetbrains.python.statistics.version import kotlinx.coroutines.CoroutineScope @@ -91,9 +90,7 @@ abstract class PyV3ProjectBaseGenerator + suspend fun generateProject(module: Module, baseDir: VirtualFile, sdk: Sdk, errorSink: ErrorSink) } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/newProjectWizard/PyV3Utils.kt b/python/src/com/jetbrains/python/newProjectWizard/PyV3Utils.kt deleted file mode 100644 index 1a68eb16df2f..000000000000 --- a/python/src/com/jetbrains/python/newProjectWizard/PyV3Utils.kt +++ /dev/null @@ -1,32 +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.newProjectWizard - -import com.intellij.openapi.project.Project -import com.intellij.openapi.projectRoots.Sdk -import com.jetbrains.python.PyBundle -import com.jetbrains.python.errorProcessing.PyResult -import com.jetbrains.python.packaging.management.PythonPackageInstallRequest -import com.jetbrains.python.packaging.management.PythonPackageManager -import kotlinx.coroutines.supervisorScope -import org.jetbrains.annotations.CheckReturnValue - -/** - * Install [packages] to [sdk]. - * Returns error if packages couldn't be installed due to execution error - */ -@CheckReturnValue -suspend fun installPackages(project: Project, sdk: Sdk, vararg packages: String): PyResult { - val packageManager = PythonPackageManager.forSdk(project, sdk) - return supervisorScope { // Not install other packages if one failed - packageManager.waitForInit() - val specifications = packages.map { - packageManager.findPackageSpecification(it) - ?: return@supervisorScope PyResult.localizedError(PyBundle.message("python.packaging.error.package.is.not.listed.in.repositories", it)) - } - val installRequest = PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications(specifications) - return@supervisorScope packageManager.installPackage(installRequest, emptyList()).mapSuccess { - // 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/PyDependencyCollector.kt b/python/src/com/jetbrains/python/packaging/PyDependencyCollector.kt index 92c87516fa9c..d11344c6c611 100644 --- a/python/src/com/jetbrains/python/packaging/PyDependencyCollector.kt +++ b/python/src/com/jetbrains/python/packaging/PyDependencyCollector.kt @@ -13,17 +13,15 @@ import com.jetbrains.python.sdk.PythonSdkUtil internal class PyDependencyCollector : DependencyCollector { override suspend fun collectDependencies(project: Project): Collection { - return readAction { - ModuleManager.getInstance(project).modules.asSequence() - .flatMap { module -> - val pythonSdk = PythonSdkUtil.findPythonSdk(module) ?: return@flatMap emptySequence() - - val pyPackageManager = PythonPackageManager.forSdk(project, pythonSdk) - pyPackageManager.installedPackages.asSequence() - .map { it.name } - } - .toSet() + val modules = readAction { + ModuleManager.getInstance(project).modules } + return modules.flatMap { module -> + val pythonSdk = readAction { PythonSdkUtil.findPythonSdk(module) } ?: return@flatMap emptySequence() + + val pyPackageManager = PythonPackageManager.forSdk(project, pythonSdk) + pyPackageManager.listInstalledPackages().asSequence().map { it.name } + }.toSet() } } diff --git a/python/src/com/jetbrains/python/packaging/PyPackageInstallUtils.kt b/python/src/com/jetbrains/python/packaging/PyPackageInstallUtils.kt index 87271b511173..9f06beb00f41 100644 --- a/python/src/com/jetbrains/python/packaging/PyPackageInstallUtils.kt +++ b/python/src/com/jetbrains/python/packaging/PyPackageInstallUtils.kt @@ -10,30 +10,21 @@ import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.ui.DoNotAskOption import com.intellij.openapi.ui.MessageDialogBuilder.Companion.yesNo import com.intellij.openapi.ui.Messages -import com.intellij.openapi.ui.popup.Balloon -import com.intellij.openapi.ui.popup.JBPopupFactory -import com.intellij.openapi.util.NlsContexts -import com.intellij.openapi.util.Version -import com.intellij.ui.awt.RelativePoint -import com.intellij.util.ui.JBUI import com.jetbrains.python.PyBundle import com.jetbrains.python.PyPsiPackageUtil import com.jetbrains.python.codeInsight.stdlib.PyStdlibUtil -import com.jetbrains.python.errorProcessing.PyResult -import com.jetbrains.python.getOrThrow import com.jetbrains.python.inspections.quickfix.InstallPackageQuickFix -import com.jetbrains.python.packaging.common.PythonPackage +import com.jetbrains.python.packaging.PyPIPackageUtil.INSTANCE import com.jetbrains.python.packaging.management.PythonPackageManager -import com.jetbrains.python.packaging.management.toInstallRequest +import com.jetbrains.python.packaging.management.hasInstalledPackageSnapshot +import com.jetbrains.python.packaging.management.ui.PythonPackageManagerUI +import com.jetbrains.python.packaging.management.ui.installPackageBackground import com.jetbrains.python.packaging.requirement.PyRequirementVersionSpec import com.jetbrains.python.packaging.ui.PyChooseRequirementsDialog -import com.jetbrains.python.packaging.utils.PyPackageCoroutine import com.jetbrains.python.statistics.PyPackagesUsageCollector import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.jetbrains.annotations.ApiStatus -import javax.swing.JLabel -import javax.swing.UIManager /** * PyCharm doesn't provide any API for package management for external plugins. @@ -59,29 +50,24 @@ object PyPackageInstallUtils { fun offeredPackageForNotFoundModule(project: Project, sdk: Sdk, moduleName: String): String? { - val shouldToInstall = checkShouldToInstall(project, sdk, moduleName) + val shouldToInstall = checkShouldToInstallSnapshot(project, sdk, moduleName) if (!shouldToInstall) return null return PyPsiPackageUtil.moduleToPackageName(moduleName) } - fun checkShouldToInstall(project: Project, sdk: Sdk, moduleName: String): Boolean { + fun checkShouldToInstallSnapshot(project: Project, sdk: Sdk, moduleName: String): Boolean { val packageName = PyPsiPackageUtil.moduleToPackageName(moduleName) - return !checkIsInstalled(project, sdk, packageName) && checkExistsInRepository(packageName) + return !checkIsInstalledSnapshot(project, sdk, packageName) && INSTANCE.isInPyPI(packageName) } - fun checkIsInstalled(project: Project, sdk: Sdk, packageName: String): Boolean { - val isStdLib = (PyStdlibUtil.getPackages() as Set<*>).contains(packageName) + fun checkIsInstalledSnapshot(project: Project, sdk: Sdk, packageName: String): Boolean { + val isStdLib = PyStdlibUtil.getPackages()?.contains(packageName) ?: false if (isStdLib) { return true } - val packageManager = getPackageManagerOrNull(project, sdk) ?: return false - return packageManager.installedPackages.any { normalizePackageName(it.name) == packageName } - } - - private fun checkExistsInRepository(packageName: String): Boolean { - val normalizedName = normalizePackageName(packageName) - return PyPIPackageUtil.INSTANCE.isInPyPI(normalizedName) + val packageManager = PythonPackageManager.forSdk(project, sdk) + return packageManager.hasInstalledPackageSnapshot(packageName) } @@ -91,8 +77,7 @@ object PyPackageInstallUtils { } if (!isConfirmed) return - val result = installPackage(project, sdk, packageName, versionSpec = versionSpec) - result.getOrThrow() + PythonPackageManagerUI.forSdk(project, sdk).installPackageBackground(packageName, versionSpec = versionSpec) } fun confirmInstall(project: Project, packageName: String): Boolean { @@ -113,86 +98,6 @@ object PyPackageInstallUtils { return true } - suspend fun upgradePackage(project: Project, sdk: Sdk, packageName: String, version: String? = null): PyResult> { - val pythonPackageManager = getPackageManagerOrNull(project, sdk) - val packageSpecification = pythonPackageManager?.repositoryManager?.repositories?.firstOrNull()?.findPackageSpecification(packageName, version) - ?: return PyResult.localizedError("Could not find any repositories") - - return pythonPackageManager.updatePackages(packageSpecification) - } - - suspend fun installPackage( - project: Project, - sdk: Sdk, - packageName: String, - versionSpec: PyRequirementVersionSpec? = null, - options: List = emptyList(), - ): PyResult> { - val pythonPackageManager = PythonPackageManager.forSdk(project, sdk) - - val spec = pythonPackageManager.findPackageSpecificationWithVersionSpec(packageName, versionSpec) - ?: return PyResult.localizedError("Package $packageName not found in any repository") - - return pythonPackageManager.installPackage(spec.toInstallRequest(), options) - } - - - /** - * NOTE calling this functions REQUIRED init package list before the calling! - */ - fun getPackageVersion(project: Project, sdk: Sdk, packageName: String): Version? { - val pythonPackage = getPackage(project, sdk, packageName) - val version = pythonPackage?.version ?: return null - return Version.parseVersion(version) - } - - private fun getPackage( - project: Project, - sdk: Sdk, - packageName: String, - ): PythonPackage? { - val pythonPackageManager = getPackageManagerOrNull(project, sdk) - val installedPackages = pythonPackageManager?.installedPackages ?: return null - - val pythonPackage = installedPackages.firstOrNull { it.name == packageName } - return pythonPackage - } - - 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.name) - } - - - fun invokeInstallPackage(project: Project, pythonSdk: Sdk, packageName: String, point: RelativePoint, versionSpec: PyRequirementVersionSpec? = null) { - PyPackageCoroutine.launch(project) { - 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 - } - PyResult.success(Unit) - } - } - - private fun getPackageManagerOrNull( - project: Project, - sdk: Sdk, - ): PythonPackageManager? = try { - PythonPackageManager.forSdk(project, sdk) - } - catch (_: Throwable) { - null - } private class ConfirmPackageInstallationDoNotAskOption : DoNotAskOption.Adapter() { override fun rememberChoice(isSelected: Boolean, exitCode: Int) { @@ -201,29 +106,6 @@ object PyPackageInstallUtils { } } } - - private suspend fun showBalloon(point: RelativePoint, @NlsContexts.DialogMessage text: String, style: BalloonStyle): Balloon = - withContext(Dispatchers.EDT) { - val content = JLabel() - val (borderColor, fillColor) = when (style) { - BalloonStyle.SUCCESS -> JBUI.CurrentTheme.Banner.SUCCESS_BORDER_COLOR to JBUI.CurrentTheme.Banner.SUCCESS_BACKGROUND - BalloonStyle.INFO -> JBUI.CurrentTheme.Banner.INFO_BORDER_COLOR to JBUI.CurrentTheme.Banner.INFO_BACKGROUND - BalloonStyle.ERROR -> JBUI.CurrentTheme.Validator.errorBorderColor() to JBUI.CurrentTheme.Validator.errorBackgroundColor() - } - val balloonBuilder = JBPopupFactory.getInstance() - .createBalloonBuilder(content) - .setBorderInsets(UIManager.getInsets("Balloon.error.textInsets")) - .setBorderColor(borderColor) - .setFillColor(fillColor) - .setHideOnClickOutside(true) - .setHideOnFrameResize(false) - content.text = text - val balloon = balloonBuilder.createBalloon() - balloon.show(point, Balloon.Position.below) - balloon - } - - enum class BalloonStyle { ERROR, INFO, SUCCESS } } diff --git a/python/src/com/jetbrains/python/packaging/PyPackageManagerUI.java b/python/src/com/jetbrains/python/packaging/PyPackageManagerUI.java index 0dfb8f7e1c5d..4a2961db3e41 100644 --- a/python/src/com/jetbrains/python/packaging/PyPackageManagerUI.java +++ b/python/src/com/jetbrains/python/packaging/PyPackageManagerUI.java @@ -2,50 +2,22 @@ package com.jetbrains.python.packaging; import com.intellij.execution.ExecutionException; -import com.intellij.ide.IdeBundle; import com.intellij.model.SideEffectGuard; -import com.intellij.notification.Notification; -import com.intellij.notification.NotificationListener; -import com.intellij.notification.NotificationType; -import com.intellij.notification.NotificationsManager; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.application.ModalityState; -import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.progress.ProgressIndicator; -import com.intellij.openapi.progress.ProgressManager; -import com.intellij.openapi.progress.Task; import com.intellij.openapi.project.Project; import com.intellij.openapi.projectRoots.Sdk; -import com.intellij.openapi.ui.MessageDialogBuilder; -import com.intellij.openapi.util.NlsContexts; -import com.intellij.openapi.util.NlsSafe; -import com.intellij.openapi.util.Pair; -import com.intellij.openapi.util.text.StringUtil; -import com.intellij.util.containers.ContainerUtil; -import com.jetbrains.python.PyBundle; -import com.jetbrains.python.errorProcessing.ExecError; -import com.jetbrains.python.errorProcessing.PyError; -import com.jetbrains.python.packaging.management.PythonPackagesInstaller; -import com.jetbrains.python.packaging.ui.PyPackageManagementService; -import org.jetbrains.annotations.Nls; +import com.jetbrains.python.packaging.utils.PyPackagesManagerUIBridge; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.PropertyKey; -import javax.swing.event.HyperlinkEvent; -import java.util.*; -import java.util.stream.Collectors; +import java.util.List; public final class PyPackageManagerUI { - private static final @NotNull Logger LOG = Logger.getInstance(PyPackageManagerUI.class); - private final @Nullable Listener myListener; private final @NotNull Project myProject; private final @NotNull Sdk mySdk; public interface Listener { void started(); - void finished(List exceptions); } @@ -57,328 +29,11 @@ public final class PyPackageManagerUI { public void install(final @Nullable List requirements, final @NotNull List extraArgs) { SideEffectGuard.checkSideEffectAllowed(SideEffectGuard.EffectType.EXEC); - ProgressManager.getInstance().run(new InstallTask(myProject, mySdk, requirements, extraArgs, myListener)); + PyPackagesManagerUIBridge.runInstallInBackground(myProject, mySdk, requirements, extraArgs, myListener); } public void uninstall(final @NotNull List packages) { SideEffectGuard.checkSideEffectAllowed(SideEffectGuard.EffectType.EXEC); - if (checkDependents(packages)) { - return; - } - ProgressManager.getInstance().run(new UninstallTask(myProject, mySdk, myListener, packages)); - } - - private boolean checkDependents(@NotNull List packages) { - try { - Map> dependentPackages = collectDependents(packages, mySdk); - if (dependentPackages.isEmpty()) { - return false; - } - - boolean[] warning = {true}; - ApplicationManager.getApplication().invokeAndWait(() -> { - if (dependentPackages.size() == 1) { - Map.Entry> packageToDependents = ContainerUtil.getOnlyItem(dependentPackages.entrySet()); - assert packageToDependents != null; - Set dependents = packageToDependents.getValue(); - String message = PyBundle.message("python.packaging.dialog.description.attempt.to.uninstall.for.one.dependent.package", - packageToDependents.getKey(), StringUtil.join(dependents, ", "), dependents.size()); - warning[0] = - MessageDialogBuilder.yesNo(PyBundle.message("python.packaging.warning"), message) - .asWarning() - .ask(myProject); - } - else { - List dep = new ArrayList<>(); - for (Map.Entry> entry : dependentPackages.entrySet()) { - dep.add(PyBundle.message( - "python.packaging.dialog.description.attempt.to.uninstall.for.several.dependent.packages.single.package.description", - entry.getKey(), - StringUtil.join(entry.getValue(), ", "))); - } - String message = PyBundle.message("python.packaging.dialog.description.attempt.to.uninstall.for.several.dependent.packages", - StringUtil.join(dep, "\n")); - warning[0] = MessageDialogBuilder.yesNo(PyBundle.message("python.packaging.warning"), message) - .asWarning() - .ask(myProject); - } - }, ModalityState.current()); - if (!warning[0]) { - return true; - } - } - catch (ExecutionException e) { - LOG.info("Error loading packages dependents: " + e.getMessage(), e); - } - return false; - } - - private static Map> collectDependents(final @NotNull List packages, - Sdk sdk) throws ExecutionException { - Map> dependentPackages = new HashMap<>(); - for (PyPackage pkg : packages) { - final Set dependents = PyPackageManager.getInstance(sdk).getDependents(pkg); - if (!dependents.isEmpty()) { - for (PyPackage dependent : dependents) { - if (!packages.contains(dependent)) { - dependentPackages.put(pkg.getName(), dependents); - } - } - } - } - return dependentPackages; - } - - private abstract static class PackagingTask extends Task.Backgroundable { - private static final String PACKAGING_GROUP_ID = "Packaging"; - - protected final @NotNull Sdk mySdk; - protected final @Nullable Listener myListener; - - PackagingTask(@Nullable Project project, - @NotNull Sdk sdk, - @NotNull @NlsContexts.ProgressTitle String title, - @Nullable Listener listener) { - super(project, title); - mySdk = sdk; - myListener = listener; - } - - @Override - public void run(@NotNull ProgressIndicator indicator) { - taskStarted(indicator); - taskFinished(runTask(indicator)); - } - - protected abstract @NotNull List runTask(@NotNull ProgressIndicator indicator); - - protected abstract @NotNull @NlsContexts.NotificationTitle String getSuccessTitle(); - - protected abstract @NotNull @NlsContexts.NotificationContent String getSuccessDescription(); - - protected abstract @NotNull @NlsContexts.NotificationTitle String getFailureTitle(); - - protected void taskStarted(@NotNull ProgressIndicator indicator) { - final PackagingNotification[] notifications = - NotificationsManager.getNotificationsManager().getNotificationsOfType(PackagingNotification.class, getProject()); - for (PackagingNotification notification : notifications) { - notification.expire(); - } - indicator.setText(getTitle() + "..."); - if (myListener != null) { - ApplicationManager.getApplication().invokeLater(() -> myListener.started()); - } - } - - protected void taskFinished(final @NotNull List exceptions) { - if (exceptions.isEmpty()) { - sendNotification( - getSuccessTitle(), - getSuccessDescription(), - NotificationType.INFORMATION, - exceptions, - null - ); - } - else { - final List> requirements = - this instanceof InstallTask && ((InstallTask)this).myRequirements != null ? ContainerUtil.flatMap( - ((InstallTask)this).myRequirements, - req -> ContainerUtil.map(req.getInstallOptions(), option -> Pair.create(option, req.getName()))) : null; - final List packageManagerArguments = exceptions.stream() - .flatMap(e -> { - if (e instanceof PyExecutionException pyExecutionException) { - PyError pyError = pyExecutionException.getPyError(); - if (pyError instanceof ExecError execError) { - return Arrays.stream(execError.getArgs()); - } - } - return null; - }) - .toList(); - final String packageNames = requirements != null ? requirements.stream() - .filter(req -> packageManagerArguments.contains(req.first)) - .map(req -> req.second) - .collect(Collectors.joining(", ")) : ""; - - final PyPackageManagementService.PyPackageInstallationErrorDescription description = PyPackageManagementService. - toErrorDescription(exceptions, mySdk, packageNames); - if (description != null) { - final String firstLine = PyBundle.message("python.packaging.notification.title.error.occurred", getTitle()); - final NotificationListener listener = new NotificationListener() { - @Override - public void hyperlinkUpdate(@NotNull Notification notification, - @NotNull HyperlinkEvent event) { - assert myProject != null; - final PyPackageInstallationErrorDialog dialog = - new PyPackageInstallationErrorDialog(packageNames.isEmpty() - ? IdeBundle.message("failed.to.install.packages.dialog.title") - : IdeBundle.message("failed.to.install.package.dialog.title", packageNames), - description); - dialog.show(); - } - }; - String content = wrapIntoLink(firstLine, "python.packaging.notification.description.details.link"); - - sendNotification( - getFailureTitle(), - content, - NotificationType.ERROR, - exceptions, - listener - ); - } - } - } - - private void sendNotification( - @NlsSafe @NotNull String title, - @NlsSafe @NotNull String content, - @NotNull NotificationType type, - List exceptions, - @Nullable NotificationListener listener - ) { - ApplicationManager.getApplication().invokeLater(() -> { - Notification notification = new PackagingNotification( - PACKAGING_GROUP_ID, - title, - content, - type, - listener - ); - - notification.notify(myProject); - - if (myListener != null) { - myListener.finished(exceptions); - } - }); - } - - @SuppressWarnings("HardCodedStringLiteral") - private static @NotNull @Nls String wrapIntoLink(@NotNull @Nls String prefix, - @NotNull @PropertyKey(resourceBundle = PyBundle.BUNDLE) String key) { - return prefix + " " + PyBundle.message(key) + ""; - } - - private static class PackagingNotification extends Notification { - PackagingNotification(@NotNull String groupDisplayId, - @NotNull @NlsContexts.NotificationTitle String title, - @NotNull @NlsContexts.NotificationContent String content, - @NotNull NotificationType type, - @Nullable NotificationListener listener) { - super(groupDisplayId, title, content, type); - if (listener != null) setListener(listener); - } - } - } - - private static class InstallTask extends PackagingTask { - private final @Nullable List myRequirements; - private final @NotNull List myExtraArgs; - - InstallTask(@Nullable Project project, - @NotNull Sdk sdk, - @Nullable List requirements, - @NotNull List extraArgs, - @Nullable Listener listener) { - super(project, sdk, PyBundle.message("python.packaging.progress.title.installing.packages"), listener); - myRequirements = requirements; - myExtraArgs = extraArgs; - } - - @Override - protected @NotNull List runTask(@NotNull ProgressIndicator indicator) { - final List exceptions = new ArrayList<>(); - if (myProject == null) { - // FIXME: proper error - return exceptions; - } - - var result = PythonPackagesInstaller.Companion.installPackages( - myProject, - mySdk, - myRequirements, - myExtraArgs, - indicator - ); - - // FIXME: use packaging tool window service for managing error dialog - var error = result.getErrorOrNull(); - if (error != null) { - exceptions.add(new ExecutionException(error.getMessage())); - } - - return exceptions; - } - - @Override - protected @NotNull String getSuccessTitle() { - return PyBundle.message("python.packaging.notification.title.packages.installed.successfully"); - } - - @Override - protected @NotNull String getSuccessDescription() { - return myRequirements != null - ? PyBundle.message("python.packaging.notification.description.installed.packages", - PyPackageUtil.requirementsToString(myRequirements)) - : PyBundle.message("python.packaging.notification.description.installed.all.requirements"); - } - - @Override - protected @NotNull String getFailureTitle() { - return PyBundle.message("python.packaging.notification.title.install.packages.failed"); - } - } - - private static class UninstallTask extends PackagingTask { - private final @NotNull List myPackages; - - UninstallTask(@Nullable Project project, - @NotNull Sdk sdk, - @Nullable Listener listener, - @NotNull List packages) { - super(project, sdk, PyBundle.message("python.packaging.progress.title.uninstalling.packages"), listener); - myPackages = packages; - } - - @Override - protected @NotNull List runTask(@NotNull ProgressIndicator indicator) { - final List exceptions = new ArrayList<>(); - if (myProject == null) { - return exceptions; - } - - var result = PythonPackagesInstaller.Companion.uninstallPackages( - myProject, - mySdk, - myPackages, - indicator - ); - - // FIXME: use packaging tool window service for managing error dialog - var error = result.getErrorOrNull(); - if (error != null) { - exceptions.add(new ExecutionException(error.getMessage())); - } - - return exceptions; - } - - @Override - protected @NotNull String getSuccessTitle() { - return PyBundle.message("python.packaging.notification.title.packages.uninstalled.successfully"); - } - - @Override - protected @NotNull String getSuccessDescription() { - final String packagesString = StringUtil.join(myPackages, pkg -> "'" + pkg.getName() + "'", ", "); - return PyBundle.message("python.packaging.notification.description.uninstalled.packages", packagesString); - } - - @Override - protected @NotNull String getFailureTitle() { - return PyBundle.message("python.packaging.notification.title.uninstall.packages.failed"); - } + PyPackagesManagerUIBridge.runUninstallInBackground(myProject, mySdk, packages, myListener); } } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/packaging/PyRequirementsFileVisitor.kt b/python/src/com/jetbrains/python/packaging/PyRequirementsFileVisitor.kt index badf8f902fc5..cada4f662856 100644 --- a/python/src/com/jetbrains/python/packaging/PyRequirementsFileVisitor.kt +++ b/python/src/com/jetbrains/python/packaging/PyRequirementsFileVisitor.kt @@ -5,12 +5,14 @@ import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiDirectory import com.intellij.psi.PsiFile import com.jetbrains.python.PyBundle +import com.jetbrains.python.packaging.common.PythonPackage import org.jetbrains.annotations.ApiStatus import java.util.* @ApiStatus.Internal -class PyRequirementsFileVisitor(private val importedPackages: MutableMap, - private val settings: PyPackageRequirementsSettings) { +class PyRequirementsFileVisitor(private val importedPackages: MutableMap, + private val settings: PyPackageRequirementsSettings +) { private val collectedOutput: MutableMap> = mutableMapOf() private val unmatchedLines: MutableList = mutableListOf() @@ -91,7 +93,7 @@ class PyRequirementsFileVisitor(private val importedPackages: MutableMap): List = when { + private fun formatRequirement(requirement: PyRequirement, pkg: PythonPackage, lines: List): List = when { // keeping editable and vcs requirements requirement.isEditable || vcsPrefixes.any { lines.first().startsWith(it) } -> lines // existing version separators match the current package version diff --git a/python/src/com/jetbrains/python/packaging/PyRequirementsTxtUtil.kt b/python/src/com/jetbrains/python/packaging/PyRequirementsTxtUtil.kt index 9e444aa3454f..9a7bfe746aa2 100644 --- a/python/src/com/jetbrains/python/packaging/PyRequirementsTxtUtil.kt +++ b/python/src/com/jetbrains/python/packaging/PyRequirementsTxtUtil.kt @@ -14,6 +14,8 @@ import com.intellij.openapi.fileTypes.FileTypeRegistry import com.intellij.openapi.module.Module import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.Task +import com.intellij.openapi.progress.runBlockingCancellable +import com.intellij.openapi.progress.runBlockingMaybeCancellable import com.intellij.openapi.project.Project import com.intellij.openapi.project.rootManager import com.intellij.openapi.projectRoots.Sdk @@ -29,12 +31,15 @@ import com.jetbrains.python.PyBundle import com.jetbrains.python.PyPsiPackageUtil import com.jetbrains.python.PySdkBundle import com.jetbrains.python.PythonFileType +import com.jetbrains.python.packaging.common.PythonPackage +import com.jetbrains.python.packaging.management.PythonPackageManager import com.jetbrains.python.psi.PyFile import com.jetbrains.python.sdk.PySdkPopupFactory import com.jetbrains.python.sdk.PythonSdkUtil +import com.jetbrains.python.util.runWithModalBlockingOrInBackground import org.jetbrains.annotations.ApiStatus import java.nio.file.Paths -import java.util.Locale +import java.util.* /** @@ -46,25 +51,28 @@ import java.util.Locale */ @ApiStatus.Internal -data class PyRequirementsAnalysisResult(val currentFileOutput: List, - val baseFilesOutput: Map>, - val unhandledLines: List, - val unchangedInBaseFiles: List) { +data class PyRequirementsAnalysisResult( + val currentFileOutput: List, + val baseFilesOutput: Map>, + val unhandledLines: List, + val unchangedInBaseFiles: List, +) { companion object { fun empty() = PyRequirementsAnalysisResult(emptyList(), emptyMap(), emptyList(), emptyList()) } - fun withImportedPackages(importedPackages: Map, settings: PyPackageRequirementsSettings): PyRequirementsAnalysisResult { + fun withImportedPackages(importedPackages: MutableMap, settings: PyPackageRequirementsSettings): PyRequirementsAnalysisResult { val newCurrentFile = currentFileOutput + importedPackages.values.map { - if (settings.specifyVersion) "${it.name}${settings.versionSpecifier.separator}${it.version}" else it.name + if (settings.specifyVersion) "${it.presentableName}${settings.versionSpecifier.separator}${it.version}" else it.presentableName } return PyRequirementsAnalysisResult(newCurrentFile, baseFilesOutput, unhandledLines, unchangedInBaseFiles) } } + private class PyCollectImportsTask( private val module: Module, private val psiManager: PsiManager, - @NlsContexts.DialogTitle title: String + @NlsContexts.DialogTitle title: String, ) : Task.WithResult, Exception>(module.project, title, true) { override fun compute(indicator: ProgressIndicator): Set { @@ -142,11 +150,13 @@ internal fun syncWithImports(module: Module) { } } -private fun showNotification(notificationGroup: NotificationGroup, - type: NotificationType, - @NlsContexts.NotificationContent text: String, - project: Project, - action: NotificationAction? = null) { +private fun showNotification( + notificationGroup: NotificationGroup, + type: NotificationType, + @NlsContexts.NotificationContent text: String, + project: Project, + action: NotificationAction? = null, +) { val notification = notificationGroup.createNotification(PyBundle.message("python.requirements.balloon"), text, type) if (action != null) notification.addAction(action) notification.notify(project) @@ -160,11 +170,14 @@ private fun prepareRequirementsText(module: Module, sdk: Sdk, settings: PyPackag val task = PyCollectImportsTask(module, psiManager, dialogTitle) task.queue() - val installedPackages = PyPackageManager.getInstance(sdk).refreshAndGetPackages(false) + val installedPackages = runWithModalBlockingOrInBackground(module.project, PyBundle.message("python.packaging.list.packages")) { + PythonPackageManager.forSdk(module.project, sdk).listInstalledPackages() + } + val importedPackages = task.result.asSequence() .flatMap { topLevelPackage -> val alias = PyPsiPackageUtil.moduleToPackageName(topLevelPackage, default = "") - sequence { + sequence { yield(topLevelPackage) if (alias.isNotEmpty()) yield(alias) }.mapNotNull { name -> installedPackages.find { StringUtil.equalsIgnoreCase(it.name, name) } } @@ -194,7 +207,7 @@ private fun showSyncSettingsDialog(project: Project, settings: PyPackageRequirem .focused() } row(PyBundle.message("python.requirements.version.label")) { - comboBox(PyRequirementsVersionSpecifierType.values().asList()) + comboBox(PyRequirementsVersionSpecifierType.entries) .bindItem(settings::getVersionSpecifier, settings::setVersionSpecifier) .align(AlignX.FILL) } @@ -224,6 +237,6 @@ private fun showSyncSettingsDialog(project: Project, settings: PyPackageRequirem private fun addImports(file: PyFile, imported: MutableSet) { (file.importTargets.asSequence().mapNotNull { it.importedQName?.firstComponent } + - file.fromImports.asSequence().mapNotNull { it.importSourceQName?.firstComponent }) + file.fromImports.asSequence().mapNotNull { it.importSourceQName?.firstComponent }) .forEach { imported.add(it) } } diff --git a/python/src/com/jetbrains/python/packaging/bridge/PythonPackageManagementServiceBridge.kt b/python/src/com/jetbrains/python/packaging/bridge/PythonPackageManagementServiceBridge.kt index 1603d7020c0c..14e78ddd09f5 100644 --- a/python/src/com/jetbrains/python/packaging/bridge/PythonPackageManagementServiceBridge.kt +++ b/python/src/com/jetbrains/python/packaging/bridge/PythonPackageManagementServiceBridge.kt @@ -1,7 +1,6 @@ // 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.bridge -import com.intellij.execution.ExecutionException import com.intellij.openapi.Disposable import com.intellij.openapi.application.ModalityState import com.intellij.openapi.application.asContextElement @@ -9,25 +8,29 @@ import com.intellij.openapi.components.service import com.intellij.openapi.progress.runBlockingCancellable import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.util.NlsSafe import com.intellij.util.CatchingConsumer import com.intellij.webcore.packaging.InstalledPackage import com.intellij.webcore.packaging.PackageVersionComparator import com.intellij.webcore.packaging.RepoPackage import com.jetbrains.python.PyBundle -import com.jetbrains.python.errorProcessing.asKotlinResult 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.management.PythonPackageManager +import com.jetbrains.python.packaging.management.findPackageSpecification 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.management.ui.PythonPackageManagerUI +import com.jetbrains.python.packaging.management.ui.installPackageBackground +import com.jetbrains.python.packaging.pyRequirementVersionSpec import com.jetbrains.python.packaging.repository.PyPIPackageRepository import com.jetbrains.python.packaging.repository.PyPackageRepository +import com.jetbrains.python.packaging.requirement.PyRequirementRelation import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService import com.jetbrains.python.packaging.ui.PyPackageManagementService import com.jetbrains.python.sdk.conda.isConda +import com.jetbrains.python.util.runWithModalBlockingOrInBackground import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.launch @@ -39,20 +42,25 @@ class PythonPackageManagementServiceBridge(project: Project, sdk: Sdk) : PyPacka private val manager: PythonPackageManager get() = PythonPackageManager.forSdk(project, sdk) + private val repositoryManager + get() = manager.repositoryManager + private val managerUI: PythonPackageManagerUI + get() = PythonPackageManagerUI.forSdk(project, sdk) + var useConda: Boolean = true val isConda: Boolean get() = sdk.isConda() override fun getInstalledPackagesList(): List { - if (manager.installedPackages.isEmpty()) runBlockingCancellable { - manager.reloadPackages() + val packages = runWithModalBlockingOrInBackground(project, PyBundle.message("python.packaging.list.packages")) { + manager.listInstalledPackages() } - - return manager.installedPackages.map { InstalledPackage(it.name, it.version) } + return packages.map { InstalledPackage(it.name, it.version) } } + override fun getAllPackages(): List { - val packagesWithRepositories = manager.repositoryManager.packagesByRepository() + val packagesWithRepositories = repositoryManager.packagesByRepository() return packagesWithRepositories .flatMap { (repository, packages) -> packages.asSequence().map { pkg -> @@ -73,7 +81,7 @@ class PythonPackageManagementServiceBridge(project: Project, sdk: Sdk) : PyPacka override fun reloadAllPackages(): List { return runBlockingCancellable { - manager.repositoryManager.refreshCaches() + repositoryManager.refreshCaches() allPackages } } @@ -87,56 +95,29 @@ class PythonPackageManagementServiceBridge(project: Project, sdk: Sdk) : PyPacka installToUser: Boolean, ) { scope.launch(Dispatchers.IO + ModalityState.current().asContextElement()) { - val repository = if (repoPackage.repoUrl != null) { - manager.repositoryManager.repositories.find { it.repositoryUrl == repoPackage.repoUrl } - } - else null - try { - val specification = specForPackage(repoPackage.name, version, repository) - runningUnderOldUI = true - listener.operationStarted(specification.name) - val result = manager.installPackage(specification.toInstallRequest(), emptyList()).asKotlinResult() - val exception = if (result.isFailure) mutableListOf(result.exceptionOrNull() as ExecutionException) else null - listener.operationFinished(specification.name, - toErrorDescription(exception, mySdk, specification.name)) - } - finally { - runningUnderOldUI = false - } + val versionSpec = version?.let { pyRequirementVersionSpec(PyRequirementRelation.EQ, version) } + managerUI.installPackageBackground(repoPackage.name, versionSpec, listOfNotNull(extraOptions)) } } override fun uninstallPackages(installedPackages: List, listener: Listener) { scope.launch(Dispatchers.IO + ModalityState.current().asContextElement()) { - try { - runningUnderOldUI = true - val namesToDelete = installedPackages.map { normalizePackageName(it.name) } - val pythonPackages = manager - .installedPackages - .filter { it.name in namesToDelete } - .map { it.name }.toTypedArray() - manager.uninstallPackage(*pythonPackages) - - listener.operationFinished(namesToDelete.first(), null) - } - finally { - runningUnderOldUI = false - } + managerUI.uninstallPackagesBackground(installedPackages.map { it.name }) } } override fun fetchPackageVersions(packageName: String, consumer: CatchingConsumer, in Exception>) { scope.launch { - val details = manager.repositoryManager.getPackageDetails(specForPackage(packageName)).getOrThrow() + val details = repositoryManager.getPackageDetails(specForPackage(packageName)).getOrThrow() consumer.consume(details.availableVersions.sortedWith(PackageVersionComparator.VERSION_COMPARATOR.reversed())) } } override fun fetchPackageDetails(packageName: String, consumer: CatchingConsumer) { scope.launch { - val details = manager.repositoryManager.getPackageDetails(specForPackage(packageName)).getOrThrow() + val details = repositoryManager.getPackageDetails(specForPackage(packageName)).getOrThrow() consumer.consume(buildDescription(details)) } } @@ -147,7 +128,7 @@ class PythonPackageManagementServiceBridge(project: Project, sdk: Sdk) : PyPacka override fun fetchLatestVersion(pkg: InstalledPackage, consumer: CatchingConsumer) { scope.launch { - val details = manager.repositoryManager.getPackageDetails(specForPackage(pkg.name, pkg.version)).getOrThrow() + val details = repositoryManager.getPackageDetails(specForPackage(pkg.name, pkg.version)).getOrThrow() consumer.consume(PyPackagingSettings.getInstance(project).selectLatestVersion(details.availableVersions)) } } diff --git a/python/src/com/jetbrains/python/packaging/common/packages.kt b/python/src/com/jetbrains/python/packaging/common/packages.kt index 9ccc4cd2444e..cdb383b2c35d 100644 --- a/python/src/com/jetbrains/python/packaging/common/packages.kt +++ b/python/src/com/jetbrains/python/packaging/common/packages.kt @@ -1,6 +1,7 @@ // 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.common +import com.jetbrains.python.packaging.management.findPackageSpecification import com.jetbrains.python.packaging.normalizePackageName import com.jetbrains.python.packaging.pyRequirementVersionSpec import com.jetbrains.python.packaging.repository.PyPackageRepository diff --git a/python/src/com/jetbrains/python/packaging/management/PythonPackageInstaller.kt b/python/src/com/jetbrains/python/packaging/management/PythonPackageInstaller.kt deleted file mode 100644 index 1a2744ada78a..000000000000 --- a/python/src/com/jetbrains/python/packaging/management/PythonPackageInstaller.kt +++ /dev/null @@ -1,86 +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.management - -import com.intellij.openapi.progress.ProgressIndicator -import com.intellij.openapi.progress.runBlockingCancellable -import com.intellij.openapi.project.Project -import com.intellij.openapi.projectRoots.Sdk -import com.intellij.util.concurrency.annotations.RequiresBackgroundThread -import com.jetbrains.python.PyBundle -import com.jetbrains.python.errorProcessing.PyResult -import com.jetbrains.python.packaging.PyPackage -import com.jetbrains.python.packaging.PyRequirement -import com.jetbrains.python.packaging.common.PythonPackage -import org.jetbrains.annotations.ApiStatus -import org.jetbrains.annotations.CheckReturnValue - -@ApiStatus.Internal -class PythonPackagesInstaller { - companion object { - @JvmStatic - @RequiresBackgroundThread - @CheckReturnValue - fun installPackages( - project: Project, - sdk: Sdk, - requirements: List?, - extraArgs: List, - indicator: ProgressIndicator, - ): PyResult = runBlockingCancellable { - val manager = PythonPackageManager.forSdk(project, sdk) - - return@runBlockingCancellable if (requirements.isNullOrEmpty()) { - installWithoutRequirements(manager, indicator) - } - else { - installWithRequirements(manager, requirements, extraArgs) - } - } - - @CheckReturnValue - private suspend fun installWithoutRequirements( - manager: PythonPackageManager, - indicator: ProgressIndicator, - ): PyResult { - indicator.text = PyBundle.message("python.packaging.installing.packages") - indicator.isIndeterminate = true - - val installAllRequirementsSpecification = PythonPackageInstallRequest.AllRequirements - return manager.installPackage(installAllRequirementsSpecification, emptyList()).mapSuccess { } - } - - @CheckReturnValue - suspend fun installWithRequirements( - manager: PythonPackageManager, - requirements: Collection, - extraArgs: List, - ): PyResult { - manager.waitForInit() - val packageSpecifications = requirements.map { requirement -> - manager.findPackageSpecificationWithVersionSpec(requirement.name, versionSpec = requirement.versionSpecs.firstOrNull()) - ?: return PyResult.localizedError(PyBundle.message("python.packaging.error.package.is.not.listed.in.repositories", requirement.name)) - } - val request = PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications(packageSpecifications) - return manager.installPackage(request, extraArgs).mapSuccess { } - } - - @JvmStatic - @CheckReturnValue - fun uninstallPackages(project: Project, sdk: Sdk, packages: List, indicator: ProgressIndicator): PyResult = runBlockingCancellable { - indicator.isIndeterminate = true - - val manager = PythonPackageManager.forSdk(project, sdk) - val pythonPackages = packages.map { it.toPythonPackage() } - - return@runBlockingCancellable uninstallPackagesProcess(manager, pythonPackages) - } - - - @CheckReturnValue - suspend fun uninstallPackagesProcess(manager: PythonPackageManager, packages: List): PyResult { - return manager.uninstallPackage(*packages.map { it.name }.toTypedArray()).mapSuccess {} - } - - private fun PyPackage.toPythonPackage(): PythonPackage = PythonPackage(this.name, this.version, false) - } -} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/packaging/management/PythonPackageManager.kt b/python/src/com/jetbrains/python/packaging/management/PythonPackageManager.kt index e498c138080f..0bb15a2bdd0e 100644 --- a/python/src/com/jetbrains/python/packaging/management/PythonPackageManager.kt +++ b/python/src/com/jetbrains/python/packaging/management/PythonPackageManager.kt @@ -15,7 +15,6 @@ import com.intellij.openapi.util.Key import com.intellij.openapi.util.SystemInfoRt import com.intellij.openapi.vfs.VfsUtil import com.intellij.util.messages.Topic -import com.jetbrains.python.PyBundle import com.jetbrains.python.errorProcessing.PyResult import com.jetbrains.python.getOrNull import com.jetbrains.python.onFailure @@ -25,8 +24,7 @@ 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.normalizePackageName import com.jetbrains.python.packaging.requirement.PyRequirementVersionSpec import com.jetbrains.python.sdk.PythonSdkCoroutineService import com.jetbrains.python.sdk.PythonSdkUpdater @@ -36,9 +34,12 @@ import kotlinx.coroutines.future.await import kotlinx.coroutines.launch import org.jetbrains.annotations.ApiStatus import org.jetbrains.annotations.CheckReturnValue -import org.jetbrains.annotations.Nls +/** + * Represents a Python package manager for a specific Python SDK. Encapsulate main operations with package managers + * @see com.jetbrains.python.packaging.management.ui.PythonPackageManagerUI to execute commands with UI handlers + */ @ApiStatus.Experimental abstract class PythonPackageManager(val project: Project, val sdk: Sdk) { private val lazyInitialization by lazy { @@ -52,20 +53,16 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) { @set:ApiStatus.Internal protected open var dependencies: List = emptyList() + @ApiStatus.Internal @Volatile - open var installedPackages: List = emptyList() - protected set + protected open var installedPackages: List = emptyList() @ApiStatus.Internal @Volatile - var outdatedPackages: Map = emptyMap() - private set + protected var outdatedPackages: Map = emptyMap() abstract val repositoryManager: PythonRepositoryManager - @ApiStatus.Internal - fun isPackageInstalled(pkg: PythonPackage): Boolean = installedPackages.any { it.name == pkg.name } - @ApiStatus.Internal fun findPackageSpecificationWithVersionSpec( packageName: String, @@ -76,51 +73,18 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) { } } - @ApiStatus.Internal - fun findPackageSpecification( - packageName: String, - version: String? = null, - relation: PyRequirementRelation = PyRequirementRelation.EQ, - ): PythonRepositoryPackageSpecification? { - val versionSpec = version?.let { pyRequirementVersionSpec(relation, version) } - return findPackageSpecificationWithVersionSpec(packageName, versionSpec) - } - @ApiStatus.Internal suspend fun installPackage(installRequest: PythonPackageInstallRequest, options: List = emptyList()): PyResult> { - 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) - }.getOr { return it } - + waitForInit() + installPackageCommand(installRequest, options).getOr { return it } return reloadPackages() } @ApiStatus.Internal suspend fun updatePackages(vararg packages: PythonRepositoryPackageSpecification): PyResult> { - val progressTitle = if (packages.size > 1) { - PyBundle.message("python.packaging.updating.packages") - } - else { - PyBundle.message("python.packaging.updating.package", packages.first().name) - } - - executeCommand(progressTitle) { - waitForInit() - updatePackageCommand(*packages) - }.getOr { return it } + waitForInit() + updatePackageCommand(*packages).getOr { return it } return reloadPackages() } @@ -131,27 +95,15 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) { return PyResult.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) - }.getOr { return it } - + waitForInit() + val normalizedPackagesNames = packages.map { normalizePackageName(it) } + uninstallPackageCommand(*normalizedPackagesNames.toTypedArray()).getOr { return it } return reloadPackages() } @ApiStatus.Internal open suspend fun reloadPackages(): PyResult> { - val progressTitle = PyBundle.message("python.toolwindow.packages.update.packages") - val packages = executeCommand(progressTitle) { - loadPackagesCommand() - }.getOr { + val packages = loadPackagesCommand().getOr { outdatedPackages = emptyMap() installedPackages = emptyList() return it @@ -175,13 +127,36 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) { return PyResult.success(packages) } + @ApiStatus.Internal + suspend fun listInstalledPackages(): List { + waitForInit() + return listInstalledPackagesSnapshot() + } + + @ApiStatus.Internal + fun listInstalledPackagesSnapshot(): List { + return installedPackages + } + + @ApiStatus.Internal + suspend fun listOutdatedPackages(): Map { + waitForInit() + return listOutdatedPackagesSnapshot() + } + + + @ApiStatus.Internal + fun listOutdatedPackagesSnapshot(): Map { + return outdatedPackages + } + private suspend fun reloadOutdatedPackages() { if (installedPackages.isEmpty()) { outdatedPackages = emptyMap() return } val loadedPackages = loadOutdatedPackagesCommand().onFailure { - thisLogger().warn("Failed to load outdated packages $it") + thisLogger().warn("Failed to load outdated packages $it") }.getOrNull() ?: emptyList() val packageMap = loadedPackages.associateBy { it.name } @@ -194,7 +169,6 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) { } } - 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. @@ -206,13 +180,6 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) { PythonSdkUpdater.scheduleUpdate(sdk, project) } - private suspend fun executeCommand( - progressTitle: @Nls String, - operation: suspend (() -> PyResult), - ): PyResult = PythonPackageManagerUIHelpers.runPackagingOperationBackground(project, progressTitle) { - operation() - } - @ApiStatus.Internal suspend fun waitForInit() { lazyInitialization.await() @@ -261,4 +228,6 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) { val PACKAGE_MANAGEMENT_TOPIC: Topic = Topic(PythonPackageManagementListener::class.java, Topic.BroadcastDirection.TO_DIRECT_CHILDREN) val RUNNING_PACKAGING_TASKS: Key = Key.create("PyPackageRequirementsInspection.RunningPackagingTasks") } -} \ 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 05ef177f1975..cb15caabc406 100644 --- a/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerExt.kt +++ b/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerExt.kt @@ -4,25 +4,51 @@ package com.jetbrains.python.packaging.management import com.jetbrains.python.packaging.common.PythonPackage +import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification +import com.jetbrains.python.packaging.normalizePackageName +import com.jetbrains.python.packaging.pyRequirementVersionSpec import com.jetbrains.python.packaging.repository.PyPackageRepository +import com.jetbrains.python.packaging.requirement.PyRequirementRelation import org.jetbrains.annotations.ApiStatus @ApiStatus.Internal -fun PythonPackageManager.hasInstalledPackage(packageName: String, version: String? = null): Boolean = - getPackage(packageName, version) != null +fun PythonPackageManager.getInstalledPackageSnapshot(packageName: String, version: String? = null): PythonPackage? { + val normalizedPackage = normalizePackageName(packageName) + return listInstalledPackagesSnapshot().firstOrNull { it.name == normalizedPackage && (version == null || version == it.version) } +} @ApiStatus.Internal -fun PythonPackageManager.getPackage(packageName: String, version: String? = null): PythonPackage? { - return installedPackages.firstOrNull { it.name == packageName && (version == null || version == it.version) } +fun PythonPackageManager.hasInstalledPackageSnapshot(packageName: String, version: String? = null): Boolean = + getInstalledPackageSnapshot(packageName, version) != null + + +@ApiStatus.Internal +suspend fun PythonPackageManager.hasInstalledPackage(pyPackage: PythonPackage): Boolean = + getInstalledPackage(pyPackage.name, pyPackage.version) != null + +@ApiStatus.Internal +suspend fun PythonPackageManager.hasInstalledPackage(packageName: String, version: String? = null): Boolean = + getInstalledPackage(packageName, version) != null + + +@ApiStatus.Internal +suspend fun PythonPackageManager.getInstalledPackage(packageName: String, version: String? = null): PythonPackage? { + waitForInit() + return getInstalledPackageSnapshot(packageName, version) +} + +@ApiStatus.Internal +fun PythonPackageManager.findPackageSpecification( + packageName: String, + version: String? = null, + relation: PyRequirementRelation = PyRequirementRelation.EQ, +): PythonRepositoryPackageSpecification? { + val versionSpec = version?.let { pyRequirementVersionSpec(relation, version) } + return findPackageSpecificationWithVersionSpec(packageName, versionSpec) } @ApiStatus.Internal fun PythonRepositoryManager.packagesByRepository(): Sequence>> { return repositories.asSequence().map { it to it.getPackages() } -} - -@ApiStatus.Internal -fun PythonPackageManager.isInstalled(name: String): Boolean { - return installedPackages.any { it.name.equals(name, ignoreCase = true) } } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerUIHelpers.kt b/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerUIHelpers.kt deleted file mode 100644 index 0efdf7cbd3ad..000000000000 --- a/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerUIHelpers.kt +++ /dev/null @@ -1,18 +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.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.errorProcessing.PyResult - -internal object PythonPackageManagerUIHelpers { - suspend fun runPackagingOperationBackground( - project: Project, - @ProgressTitle title: String, - operation: suspend (() -> PyResult), - ): PyResult = withBackgroundProgress(project = project, title, cancellable = true) { - operation() - } -} - diff --git a/python/src/com/jetbrains/python/packaging/management/ui/PythonPackageManagerUI.kt b/python/src/com/jetbrains/python/packaging/management/ui/PythonPackageManagerUI.kt new file mode 100644 index 000000000000..ff2f548fe5af --- /dev/null +++ b/python/src/com/jetbrains/python/packaging/management/ui/PythonPackageManagerUI.kt @@ -0,0 +1,120 @@ +// 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.ui + +import com.intellij.openapi.project.Project +import com.intellij.openapi.projectRoots.Sdk +import com.jetbrains.python.PyBundle +import com.jetbrains.python.errorProcessing.ErrorSink +import com.jetbrains.python.errorProcessing.PyResult +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.util.ShowingMessageErrorSync +import org.jetbrains.annotations.ApiStatus +import org.jetbrains.annotations.Nls + +/** + * A class responsible for managing Python packages within a specific SDK and project. + * Provides functionality for reloading, installing, updating, and uninstalling Python packages. + * + * This class is used for work with packages with the process and handling all execution errors and providing background process + * All functions return the calculated result or null if the operation was failed. + * Prefer this class for external usage + * + * @see PythonPackageManager if you need more control over the process and handling errors + * + * @constructor Creates an instance of the PythonPackageManagerUI class. + * @param project The project within which the operations are performed. + * @param sdk The Python SDK associated with the operations. + * @param sink The error sink used for reporting errors during operations. + */ +@ApiStatus.Internal +class PythonPackageManagerUI(val manager: PythonPackageManager, val sink: ErrorSink = ShowingMessageErrorSync) { + val project: Project = manager.project + val sdk: Sdk = manager.sdk + + /** + * @return List of installed packages or null if the operation was failed. + */ + suspend fun reloadPackagesBackground(): List? { + return executeCommand(PyBundle.message("python.packaging.list.packages")) { + manager.reloadPackages() + } + } + + /** + * @return List of all installed packages or null if the operation was failed. + */ + suspend fun installPackagesBackground( + installRequest: PythonPackageInstallRequest, + options: List = emptyList(), + ): List? { + 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") + } + } + + return executeCommand(progressTitle) { + manager.installPackage(installRequest, options) + } + } + + /** + * @return List of all installed packages or null if the operation was failed. + */ + suspend fun updatePackagesBackground( + packages: List, + ): List? { + val progressTitle = if (packages.size > 1) { + PyBundle.message("python.packaging.updating.packages") + } + else { + PyBundle.message("python.packaging.updating.package", packages.first().name) + } + + return executeCommand(progressTitle) { + manager.updatePackages(*packages.toTypedArray()) + } + } + + /** + * @return List of all installed packages or null if the operation was failed. + */ + suspend fun uninstallPackagesBackground( + packages: List, + ): List? { + val progressTitle = if (packages.size > 1) { + PyBundle.message("python.packaging.uninstall.packages") + } + else { + PyBundle.message("python.packaging.uninstall.package", packages.first()) + } + + return executeCommand(progressTitle + ) { + manager.uninstallPackage(*packages.toTypedArray()) + } + } + + private suspend fun executeCommand( + progressTitle: @Nls String, + operation: suspend (() -> PyResult?), + ): T? = PythonPackageManagerUIHelpers.runPackagingOperationMaybeBackground(manager.project, sink, progressTitle) { + operation() + } + + companion object { + @ApiStatus.Internal + fun forSdk(project: Project, sdk: Sdk, sink: ErrorSink = ShowingMessageErrorSync): PythonPackageManagerUI { + val packageManager = PythonPackageManager.forSdk(project, sdk) + return PythonPackageManagerUI(packageManager, sink) + } + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/packaging/management/ui/PythonPackageManagerUIHelpers.kt b/python/src/com/jetbrains/python/packaging/management/ui/PythonPackageManagerUIHelpers.kt new file mode 100644 index 000000000000..a25715fc63ab --- /dev/null +++ b/python/src/com/jetbrains/python/packaging/management/ui/PythonPackageManagerUIHelpers.kt @@ -0,0 +1,66 @@ +// 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.ui + +import com.intellij.openapi.application.EDT +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.popup.Balloon +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.openapi.util.NlsContexts +import com.intellij.platform.ide.progress.withBackgroundProgress +import com.intellij.ui.awt.RelativePoint +import com.intellij.util.ui.JBUI +import com.jetbrains.python.errorProcessing.ErrorSink +import com.jetbrains.python.errorProcessing.PyResult +import com.jetbrains.python.getOrNull +import com.jetbrains.python.onFailure +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.swing.JLabel +import javax.swing.UIManager + +internal object PythonPackageManagerUIHelpers { + suspend fun runPackagingOperationMaybeBackground( + project: Project, + errorSink: ErrorSink?, + @NlsContexts.ProgressTitle title: String, + operation: suspend (() -> PyResult?), + ): T? = withBackgroundProgress(project = project, title, cancellable = true) { + runPackagingOperationMaybeShowErrorDialog(errorSink) { + operation() + } + } + + private suspend fun runPackagingOperationMaybeShowErrorDialog( + errorSink: ErrorSink?, + operation: suspend (() -> PyResult?), + ): T? { + val pyResult = operation() ?: return null + return pyResult.onFailure { + errorSink?.emit(it) + }.getOrNull() + } + + + suspend fun showBalloon(point: RelativePoint, @NlsContexts.DialogMessage text: String, style: BalloonStyle): Balloon = + withContext(Dispatchers.EDT) { + val content = JLabel() + val (borderColor, fillColor) = when (style) { + BalloonStyle.SUCCESS -> JBUI.CurrentTheme.Banner.SUCCESS_BORDER_COLOR to JBUI.CurrentTheme.Banner.SUCCESS_BACKGROUND + BalloonStyle.INFO -> JBUI.CurrentTheme.Banner.INFO_BORDER_COLOR to JBUI.CurrentTheme.Banner.INFO_BACKGROUND + BalloonStyle.ERROR -> JBUI.CurrentTheme.Validator.errorBorderColor() to JBUI.CurrentTheme.Validator.errorBackgroundColor() + } + val balloonBuilder = JBPopupFactory.getInstance() + .createBalloonBuilder(content) + .setBorderInsets(UIManager.getInsets("Balloon.error.textInsets")) + .setBorderColor(borderColor) + .setFillColor(fillColor) + .setHideOnClickOutside(true) + .setHideOnFrameResize(false) + content.text = text + val balloon = balloonBuilder.createBalloon() + balloon.show(point, Balloon.Position.below) + balloon + } + + enum class BalloonStyle { ERROR, INFO, SUCCESS } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/packaging/management/ui/PythonPackageManagerUiExtensions.kt b/python/src/com/jetbrains/python/packaging/management/ui/PythonPackageManagerUiExtensions.kt new file mode 100644 index 000000000000..5f5fed97e081 --- /dev/null +++ b/python/src/com/jetbrains/python/packaging/management/ui/PythonPackageManagerUiExtensions.kt @@ -0,0 +1,97 @@ +// 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.ui + +import com.intellij.ui.awt.RelativePoint +import com.jetbrains.python.PyBundle +import com.jetbrains.python.packaging.PyPackageInstallUtils.confirmAndInstall +import com.jetbrains.python.packaging.PyRequirement +import com.jetbrains.python.packaging.common.PythonPackage +import com.jetbrains.python.packaging.management.PythonPackageInstallRequest +import com.jetbrains.python.packaging.pyRequirement +import com.jetbrains.python.packaging.requirement.PyRequirementVersionSpec +import com.jetbrains.python.packaging.utils.PyPackageCoroutine +import com.jetbrains.python.statistics.PyPackagesUsageCollector +import org.jetbrains.annotations.ApiStatus + +/** + * @return List of all installed packages or null if the operation was failed. + */ +@ApiStatus.Internal +suspend fun PythonPackageManagerUI.updatePackageBackground( + pyPackage: String, +): List? = + updatePackagesByNamesBackground(listOf(pyPackage)) + +/** + * @return List of all installed packages or null if the operation was failed. + */ +@ApiStatus.Internal +suspend fun PythonPackageManagerUI.updatePackagesByNamesBackground( + packages: List, +): List? { + val specifications = packages.mapNotNull { + manager.findPackageSpecificationWithVersionSpec(it) + } + return updatePackagesBackground(specifications) +} + +@ApiStatus.Internal +fun PythonPackageManagerUI.launchInstallPackageWithBalloonBackground(packageName: String, point: RelativePoint, versionSpec: PyRequirementVersionSpec? = null) { + PyPackageCoroutine.launch(project) { + val loadBalloon = PythonPackageManagerUIHelpers.showBalloon(point, PyBundle.message("python.packaging.installing.package", packageName), + PythonPackageManagerUIHelpers.BalloonStyle.INFO) + try { + confirmAndInstall(project, sdk, packageName, versionSpec = versionSpec) + loadBalloon.hide() + PyPackagesUsageCollector.installPackageFromConsole.log(project) + PythonPackageManagerUIHelpers.showBalloon(point, PyBundle.message("python.packaging.notification.description.installed.packages", packageName), PythonPackageManagerUIHelpers.BalloonStyle.SUCCESS) + } + catch (t: Throwable) { + loadBalloon.hide() + PyPackagesUsageCollector.failInstallPackageFromConsole.log(project) + PythonPackageManagerUIHelpers.showBalloon(point, PyBundle.message("python.new.project.install.failed.title", packageName), PythonPackageManagerUIHelpers.BalloonStyle.ERROR) + throw t + } + } +} + +/** + * @return List of all installed packages or null if the operation was failed. + */ +@ApiStatus.Internal +suspend fun PythonPackageManagerUI.installPyRequirementsBackground( + packages: List, + options: List = emptyList(), +): List? { + //Wait here to load spec + manager.waitForInit() + val specifications = packages.mapNotNull { + manager.findPackageSpecificationWithVersionSpec(it.name, it.versionSpecs.firstOrNull()) + } + return installPackagesBackground(PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications(specifications), + options = options) +} + +/** + * @return List of all installed packages or null if the operation was failed. + */ +@ApiStatus.Internal +suspend fun PythonPackageManagerUI.installPackageBackground( + pyPackage: String, + versionSpec: PyRequirementVersionSpec? = null, + options: List = emptyList(), +): List? = installPyRequirementsBackground(listOf(pyRequirement(pyPackage, versionSpec)), + options = options) + +/** + * @return List of all installed packages or null if the operation was failed. + */ +@ApiStatus.Internal +suspend fun PythonPackageManagerUI.installPackagesByNamesBackground( + vararg packages: String, + options: List = emptyList(), +): List? { + val pyRequirements = packages.map { pyRequirement(it) } + return installPyRequirementsBackground(pyRequirements, + options = options) +} \ 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 7a73cefd1384..1b497d97e329 100644 --- a/python/src/com/jetbrains/python/packaging/pip/PipPythonPackageManager.kt +++ b/python/src/com/jetbrains/python/packaging/pip/PipPythonPackageManager.kt @@ -16,6 +16,7 @@ import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecificatio 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.hasInstalledPackage import com.jetbrains.python.psi.LanguageLevel import com.jetbrains.python.statistics.version import org.jetbrains.annotations.ApiStatus @@ -47,21 +48,21 @@ open class PipPythonPackageManager(project: Project, sdk: Sdk) : PythonPackageMa class PipManagementInstaller(private val sdk: Sdk, private val manager: PythonPackageManager) { private val languageLevel: LanguageLevel = sdk.version - fun installManagementIfNeeded(): Boolean { + suspend fun installManagementIfNeeded(): Boolean { if (hasManagement()) return true return performManagementInstallation() } - private fun performManagementInstallation(): Boolean = installManagement() + private suspend fun performManagementInstallation(): Boolean = installManagement() - fun hasManagement(): Boolean = - languageLevel < LanguageLevel.PYTHON27 || (manager.isPackageInstalled(PIP_PACKAGE) && hasSetuptools()) + suspend fun hasManagement(): Boolean = + languageLevel < LanguageLevel.PYTHON27 || (manager.hasInstalledPackage(PIP_PACKAGE) && hasSetuptools()) - private fun installManagement(): Boolean = + private suspend fun installManagement(): Boolean = installWheelIfMissing(::hasPip, WheelFiles.PIP_WHEEL_NAME) && installWheelIfMissing(::hasSetuptools, WheelFiles.SETUPTOOLS_WHEEL_NAME) - private fun installWheelIfMissing(requirementCheck: () -> Boolean, wheelNameToInstall: String): Boolean { + private suspend fun installWheelIfMissing(requirementCheck: suspend () -> Boolean, wheelNameToInstall: String): Boolean { if (!requirementCheck()) { val wheelPathToInstall = findPathInHelpers(wheelNameToInstall)?.toString() ?: return false return installUsingPipWheel("--no-index", wheelPathToInstall) @@ -86,12 +87,12 @@ class PipManagementInstaller(private val sdk: Sdk, private val manager: PythonPa throw ExecutionException(ex.message, ex) } - private fun hasPip(): Boolean = manager.isPackageInstalled(PIP_PACKAGE) + private suspend fun hasPip(): Boolean = manager.hasInstalledPackage(PIP_PACKAGE) - private fun hasSetuptools(): Boolean = + private suspend fun hasSetuptools(): Boolean = languageLevel >= LanguageLevel.PYTHON312 || - manager.isPackageInstalled(SETUPTOOLS_PACKAGE) || - manager.isPackageInstalled(DISTRIBUTE_PACKAGE) + manager.hasInstalledPackage(SETUPTOOLS_PACKAGE) || + manager.hasInstalledPackage(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/toolwindow/PyPackagingToolWindowService.kt b/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowService.kt index 5d31da6f25a2..cdb8e1aceb2e 100644 --- a/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowService.kt +++ b/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowService.kt @@ -18,22 +18,15 @@ import com.intellij.openapi.roots.ModuleRootEvent import com.intellij.openapi.roots.ModuleRootListener import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.vfs.VirtualFileManager -import com.intellij.platform.ide.progress.withBackgroundProgress -import com.intellij.platform.util.progress.reportRawProgress import com.jetbrains.python.PyBundle.message -import com.jetbrains.python.errorProcessing.ErrorSink -import com.jetbrains.python.errorProcessing.PyResult -import com.jetbrains.python.getOrNull -import com.jetbrains.python.onFailure -import com.jetbrains.python.onSuccess +import com.jetbrains.python.getOrThrow 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.conda.CondaPackage -import com.jetbrains.python.packaging.management.PythonPackageInstallRequest -import com.jetbrains.python.packaging.management.PythonPackageManager -import com.jetbrains.python.packaging.management.packagesByRepository +import com.jetbrains.python.packaging.management.* +import com.jetbrains.python.packaging.management.ui.PythonPackageManagerUI +import com.jetbrains.python.packaging.management.ui.updatePackagesByNamesBackground import com.jetbrains.python.packaging.repository.PyPackageRepositories import com.jetbrains.python.packaging.repository.PyPackageRepository import com.jetbrains.python.packaging.repository.PyRepositoriesList @@ -43,19 +36,25 @@ import com.jetbrains.python.packaging.toolwindow.model.* import com.jetbrains.python.sdk.PythonSdkUtil import com.jetbrains.python.sdk.pythonSdk import com.jetbrains.python.statistics.modules -import com.jetbrains.python.util.ShowingMessageErrorSync import kotlinx.coroutines.* import org.jetbrains.annotations.Nls @Service(Service.Level.PROJECT) class PyPackagingToolWindowService(val project: Project, val serviceScope: CoroutineScope) : Disposable { private var toolWindowPanel: PyPackagingToolWindowPanel? = null - lateinit var manager: PythonPackageManager + private var installedPackages: Map = emptyMap() - internal var currentSdk: Sdk? = null + private var searchJob: Job? = null private var currentQuery: String = "" - private val errorSink: ErrorSink = ShowingMessageErrorSync + + + internal var currentSdk: Sdk? = null + private var managerUI: PythonPackageManagerUI? = null + + private val manager: PythonPackageManager? + get() = managerUI?.manager + private val invalidRepositories: List get() = service().invalidRepositories.map(::PyInvalidRepositoryViewData) @@ -70,23 +69,28 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou subscribeToChanges() } - suspend fun detailsForPackage(selectedPackage: DisplayablePackage): PythonPackageDetails? = withContext(Dispatchers.IO) { - PythonPackagesToolwindowStatisticsCollector.requestDetailsEvent.log(project) - val spec = when (selectedPackage) { - is InstalledPackage -> manager.findPackageSpecification(selectedPackage.name) - is InstallablePackage -> selectedPackage.repository.findPackageSpecification(selectedPackage.name) - is ExpandResultNode -> selectedPackage.repository.findPackageSpecification(selectedPackage.name) - } + suspend fun detailsForPackage(selectedPackage: DisplayablePackage): PythonPackageDetails? { + val packageManager = manager ?: return null + return withContext(Dispatchers.IO) { + PythonPackagesToolwindowStatisticsCollector.requestDetailsEvent.log(project) + val spec = when (selectedPackage) { + is InstalledPackage -> packageManager.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}") + } - if (spec == null) { - return@withContext null - } + if (spec == null) { + return@withContext null + } - manager.repositoryManager.getPackageDetails(spec).onFailure { errorSink.emit(it) }.getOrNull() + spec.let { packageManager.repositoryManager.getPackageDetails(it).getOrThrow() } ?: error("Invalid package specification") + } } fun handleSearch(query: String) { + val manager = manager ?: return val prevSelected = toolWindowPanel?.getSelectedPackage() currentQuery = query @@ -100,6 +104,7 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou } } + val packagesFromRepos = manager.repositoryManager.searchPackages(query).map { sortPackagesForRepo(it.value, query, it.key) }.toList() @@ -125,29 +130,33 @@ 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) - result.handleActionCompleted(message("python.packaging.notification.installed", installRequest.title)) + managerUI?.installPackagesBackground(installRequest, options)?.let { + handleActionCompleted(message("python.packaging.notification.installed", installRequest.title)) + } } suspend fun deletePackage(vararg selectedPackages: InstalledPackage) { PythonPackagesToolwindowStatisticsCollector.uninstallPackageEvent.log(project) - val result = manager.uninstallPackage(*selectedPackages.map { it.instance.name }.toTypedArray()) - result.handleActionCompleted(message("python.packaging.notification.deleted", selectedPackages.joinToString(", ") { it.name })) + managerUI?.uninstallPackagesBackground(selectedPackages.map { it.instance.name }) ?: return + handleActionCompleted(message("python.packaging.notification.deleted", selectedPackages.joinToString(", ") { it.name })) + } - suspend fun updatePackage(vararg specifications: PythonRepositoryPackageSpecification) { - val result = manager.updatePackages(*specifications) + suspend fun updatePackages(vararg packages: String) { + managerUI?.updatePackagesByNamesBackground(packages.toList()) ?: return - val singlePackage = specifications.singleOrNull() - val title = if (singlePackage != null) { - val version = singlePackage.versionSpec?.version - message("python.packaging.notification.updated", singlePackage.name, version) + + val singlePackage = packages.singleOrNull() + if (singlePackage != null) { + val version = manager?.getInstalledPackage(singlePackage)?.version ?: return + handleActionCompleted(message("python.packaging.notification.updated", singlePackage, version)) + } + else { + handleActionCompleted(message("python.packaging.notification.all.updated")) } - else message("python.packaging.notification.all.updated") - - result.handleActionCompleted(title) } + internal suspend fun initForSdk(sdk: Sdk?) { if (sdk == null) { toolWindowPanel?.packageListController?.setLoadingState(false) @@ -163,17 +172,19 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou val previousSdk = currentSdk currentSdk = sdk + managerUI = sdk?.let { PythonPackageManagerUI.forSdk(project, it) } if (sdk == null) { return } - manager = PythonPackageManager.forSdk(project, sdk) + withContext(Dispatchers.EDT) { toolWindowPanel?.contentVisible = currentSdk != null if (currentSdk == null || currentSdk != previousSdk) { toolWindowPanel?.setEmpty() } } + refreshInstalledPackages() } private fun subscribeToChanges() { @@ -216,13 +227,13 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou } suspend fun refreshInstalledPackages() { - val packages = manager.installedPackages.map { - val spec = manager.findPackageSpecification(it.name, it.version) + val packages = manager?.listInstalledPackages()?.map { + val spec = manager?.findPackageSpecification(it.name, it.version) val repository = spec?.repository - val nextVersionRaw = manager.outdatedPackages[it.name]?.latestVersion + val nextVersionRaw = manager?.listOutdatedPackagesSnapshot()[it.name]?.latestVersion val nextVersion = nextVersionRaw?.let { PyPackageVersionNormalizer.normalize(it) } InstalledPackage(it, repository, nextVersion) - } + } ?: emptyList() installedPackages = packages.associateBy { it.name } @@ -231,14 +242,10 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou } } - private suspend fun PyResult<*>.handleActionCompleted(text: @Nls String) = this - .onSuccess { - VirtualFileManager.getInstance().asyncRefresh() - showPackagingNotification(text) - } - .onFailure { - errorSink.emit(it) - } + private suspend fun handleActionCompleted(text: @Nls String) { + VirtualFileManager.getInstance().asyncRefresh() + showPackagingNotification(text) + } private suspend fun showPackagingNotification(text: @Nls String) { val notification = NotificationGroupManager.getInstance() @@ -274,14 +281,9 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou } fun reloadPackages() { - serviceScope.launch(Dispatchers.IO) { - withBackgroundProgress(project, message("python.packaging.loading.packages.progress.text"), cancellable = false) { - reportRawProgress { - manager.reloadPackages() - refreshInstalledPackages() - manager.repositoryManager.refreshCaches() - } - } + serviceScope.launch(Dispatchers.Default) { + managerUI?.reloadPackagesBackground() + refreshInstalledPackages() } } @@ -312,7 +314,8 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou } } - fun getMoreResultsForRepo(repository: PyPackageRepository, skipItems: Int): PyPackagesViewData { + fun getMoreResultsForRepo(repository: PyPackageRepository, skipItems: Int): PyPackagesViewData? { + val manager = manager ?: return null if (currentQuery.isNotEmpty()) { return sortPackagesForRepo(manager.repositoryManager.searchPackages(currentQuery, repository), currentQuery, repository, skipItems) } 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 decfc8524d96..ca27127ec4ad 100644 --- a/python/src/com/jetbrains/python/packaging/toolwindow/actions/UpdatePackageToLatestAction.kt +++ b/python/src/com/jetbrains/python/packaging/toolwindow/actions/UpdatePackageToLatestAction.kt @@ -21,12 +21,8 @@ internal class UpdatePackageToLatestAction : DumbAwareAction() { } val service = PyPackagingToolWindowService.getInstance(project) - PyPackageCoroutine.getIoScope(project).launch { - val packageSpecifications = packages.mapNotNull { pkg -> - pkg.repository?.findPackageSpecification(pkg.name) - }.toTypedArray() - service.updatePackage(*packageSpecifications) + service.updatePackages(*packages.map { it.name }.toTypedArray()) } } 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 8a87888db91c..387f0a3d47c0 100644 --- a/python/src/com/jetbrains/python/packaging/toolwindow/model/modelComponents.kt +++ b/python/src/com/jetbrains/python/packaging/toolwindow/model/modelComponents.kt @@ -41,6 +41,6 @@ class InstallablePackage(name: String, override val repository: PyPackageReposit class ExpandResultNode(var more: Int, override val repository: PyPackageRepository) : DisplayablePackage("", repository) -open class PyPackagesViewData(@NlsSafe val repository: PyPackageRepository, val packages: List, val exactMatch: Int = -1, val moreItems: Int = 0) +open class PyPackagesViewData(val repository: PyPackageRepository, val packages: List, val exactMatch: Int = -1, val moreItems: Int = 0) class PyInvalidRepositoryViewData(repository: PyPackageRepository) : PyPackagesViewData(repository, emptyList()) \ No newline at end of file diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/packages/PyPackagesListController.kt b/python/src/com/jetbrains/python/packaging/toolwindow/packages/PyPackagesListController.kt index 0238f15fe1bb..6b4e54f31e4c 100644 --- a/python/src/com/jetbrains/python/packaging/toolwindow/packages/PyPackagesListController.kt +++ b/python/src/com/jetbrains/python/packaging/toolwindow/packages/PyPackagesListController.kt @@ -29,14 +29,14 @@ class PyPackagesListController(val project: Project, val controller: PyPackaging private val tablesView = PyPackagingTablesView(project, packageListPanel, controller) - val scrollingPackageListComponent = ScrollPaneFactory.createScrollPane(packageListPanel, true) + private val scrollingPackageListComponent = ScrollPaneFactory.createScrollPane(packageListPanel, true) private val loadingPanel = JBPanelWithEmptyText().apply { emptyText.appendLine(AnimatedIcon.Default.INSTANCE, message("python.toolwindow.packages.description.panel.loading"), SimpleTextAttributes.SIMPLE_CELL_ATTRIBUTES, null) } - val component = JPanel().apply { + val component: JPanel = JPanel().apply { layout = BorderLayout() } 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 74223990b8da..698a487fce97 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 @@ -49,10 +49,8 @@ internal class PyPackageTableMouseAdapter(private val table: PyPackagesTable) : selectedPackage.canBeUpdated && selectedPackage.repository != null && selectedPackage.nextVersion != null) { - val specification = selectedPackage.repository.findPackageSpecification(selectedPackage.name) ?: return - PyPackageCoroutine.launch(project) { - project.service().updatePackage(specification) + project.service().updatePackages(selectedPackage.name) } } } diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/packages/table/PyPackagesTable.kt b/python/src/com/jetbrains/python/packaging/toolwindow/packages/table/PyPackagesTable.kt index b24bf838031a..86cc73caead8 100644 --- a/python/src/com/jetbrains/python/packaging/toolwindow/packages/table/PyPackagesTable.kt +++ b/python/src/com/jetbrains/python/packaging/toolwindow/packages/table/PyPackagesTable.kt @@ -24,6 +24,7 @@ import javax.swing.AbstractAction import javax.swing.KeyStroke import javax.swing.ListSelectionModel +@Suppress("UsePropertyAccessSyntax") internal class PyPackagesTable( val project: Project, tablesView: PyPackagingTablesView, @@ -143,7 +144,7 @@ internal class PyPackagesTable( } private fun loadMoreItems(service: PyPackagingToolWindowService, node: ExpandResultNode) { - val result = service.getMoreResultsForRepo(node.repository, items.size - 1) + val result = service.getMoreResultsForRepo(node.repository, items.size - 1) ?: return items = items.dropLast(1) + result.packages if (result.moreItems > 0) { node.more = result.moreItems diff --git a/python/src/com/jetbrains/python/packaging/ui/PyPackageManagementService.java b/python/src/com/jetbrains/python/packaging/ui/PyPackageManagementService.java index b94b85ecd9ba..b637fb0fb49f 100644 --- a/python/src/com/jetbrains/python/packaging/ui/PyPackageManagementService.java +++ b/python/src/com/jetbrains/python/packaging/ui/PyPackageManagementService.java @@ -169,12 +169,6 @@ public class PyPackageManagementService extends PackageManagementServiceEx { PyPackageService.getInstance().addSdkToUserSite(mySdk.getHomePath(), newValue); } - @Override - public @NotNull List getInstalledPackagesList() throws ExecutionException { - List packages = new ArrayList<>(PyPackageManager.getInstance(mySdk).refreshAndGetPackages(true)); - packages.sort(Comparator.comparing(InstalledPackage::getName)); - return packages; - } @Override public void installPackage(@NotNull RepoPackage repoPackage, @@ -201,7 +195,7 @@ public class PyPackageManagementService extends PackageManagementServiceEx { extraArgs.add("-U"); } final PyRequirement req = version == null - ? PyRequirementsKt.pyRequirement(packageName) + ? PyRequirementsKt.pyRequirement(packageName,null) : PyRequirementsKt.pyRequirement(packageName, PyRequirementRelation.EQ, version); final PyPackageManagerUI ui = new PyPackageManagerUI(myProject, mySdk, new PyPackageManagerUI.Listener() { diff --git a/python/src/com/jetbrains/python/packaging/utils/PyPackagesManagerUIBridge.kt b/python/src/com/jetbrains/python/packaging/utils/PyPackagesManagerUIBridge.kt new file mode 100644 index 000000000000..c6037af8bc9c --- /dev/null +++ b/python/src/com/jetbrains/python/packaging/utils/PyPackagesManagerUIBridge.kt @@ -0,0 +1,41 @@ +// 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.utils + +import com.intellij.openapi.project.Project +import com.intellij.openapi.projectRoots.Sdk +import com.jetbrains.python.packaging.PyPackage +import com.jetbrains.python.packaging.PyPackageManagerUI +import com.jetbrains.python.packaging.PyRequirement +import com.jetbrains.python.packaging.management.PythonPackageInstallRequest +import com.jetbrains.python.packaging.management.ui.PythonPackageManagerUI +import com.jetbrains.python.packaging.management.ui.installPyRequirementsBackground +import kotlinx.coroutines.launch +import org.jetbrains.annotations.ApiStatus + +@ApiStatus.Internal +internal object PyPackagesManagerUIBridge { + @JvmStatic + fun runInstallInBackground(project: Project, sdk: Sdk, requirements: List?, extraArgs: List, listener: PyPackageManagerUI.Listener?) { + PyPackageCoroutine.getScope(project).launch { + val manager = PythonPackageManagerUI.forSdk(project, sdk) + listener?.started() + if (requirements.isNullOrEmpty()) { + manager.installPackagesBackground(PythonPackageInstallRequest.AllRequirements, emptyList()) + } + else { + manager.installPyRequirementsBackground(requirements.toList(), extraArgs) + } + listener?.finished(emptyList()) + } + } + + @JvmStatic + fun runUninstallInBackground(project: Project, sdk: Sdk, packages: List, listener: PyPackageManagerUI.Listener?) { + PyPackageCoroutine.getScope(project).launch { + val manager = PythonPackageManagerUI.forSdk(project, sdk) + listener?.started() + manager.uninstallPackagesBackground(packages.map { it.name }) + listener?.finished(emptyList()) + } + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/requirements/RequirementsCompletionUtil.kt b/python/src/com/jetbrains/python/requirements/RequirementsCompletionUtil.kt index 5df19d446dcd..1f072d798d96 100644 --- a/python/src/com/jetbrains/python/requirements/RequirementsCompletionUtil.kt +++ b/python/src/com/jetbrains/python/requirements/RequirementsCompletionUtil.kt @@ -12,6 +12,7 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk import com.jetbrains.python.getOrThrow import com.jetbrains.python.packaging.management.PythonPackageManager +import com.jetbrains.python.packaging.management.findPackageSpecification import com.jetbrains.python.psi.icons.PythonPsiApiIcons fun completePackageNames(project: Project, sdk: Sdk, result: CompletionResultSet) { diff --git a/python/src/com/jetbrains/python/requirements/inspections/quickfixes/InstallAllRequirementsQuickFix.kt b/python/src/com/jetbrains/python/requirements/inspections/quickfixes/InstallAllRequirementsQuickFix.kt index 6b67a1243ac4..dce1d1cc7be2 100644 --- a/python/src/com/jetbrains/python/requirements/inspections/quickfixes/InstallAllRequirementsQuickFix.kt +++ b/python/src/com/jetbrains/python/requirements/inspections/quickfixes/InstallAllRequirementsQuickFix.kt @@ -8,8 +8,8 @@ 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.management.ui.PythonPackageManagerUI +import com.jetbrains.python.packaging.management.ui.installPyRequirementsBackground import com.jetbrains.python.packaging.utils.PyPackageCoroutine import com.jetbrains.python.requirements.getPythonSdk import kotlinx.coroutines.Dispatchers @@ -29,8 +29,9 @@ internal class InstallAllRequirementsQuickFix(val requirements: List() + ) } } diff --git a/python/src/com/jetbrains/python/requirements/inspections/quickfixes/UpdateAllRequirementQuickFix.kt b/python/src/com/jetbrains/python/requirements/inspections/quickfixes/UpdateAllRequirementQuickFix.kt index 06d9f9ff9eea..895d22f408e4 100644 --- a/python/src/com/jetbrains/python/requirements/inspections/quickfixes/UpdateAllRequirementQuickFix.kt +++ b/python/src/com/jetbrains/python/requirements/inspections/quickfixes/UpdateAllRequirementQuickFix.kt @@ -6,8 +6,8 @@ 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.management.PythonPackageManager -import com.jetbrains.python.packaging.requirement.PyRequirementRelation +import com.jetbrains.python.packaging.management.ui.PythonPackageManagerUI +import com.jetbrains.python.packaging.management.ui.updatePackagesByNamesBackground import com.jetbrains.python.packaging.utils.PyPackageCoroutine import com.jetbrains.python.requirements.getPythonSdk import kotlinx.coroutines.launch @@ -20,15 +20,14 @@ internal class UpdateAllRequirementQuickFix(val outdatedPyRequirements: Set val pyRequirement = PyRequirementParser.fromLine(requirement.text) ?: return@mapNotNull null diff --git a/python/src/com/jetbrains/python/requirements/inspections/tools/OutdatedRequirementInspection.kt b/python/src/com/jetbrains/python/requirements/inspections/tools/OutdatedRequirementInspection.kt index b2b6d62f8837..a2393780be87 100644 --- a/python/src/com/jetbrains/python/requirements/inspections/tools/OutdatedRequirementInspection.kt +++ b/python/src/com/jetbrains/python/requirements/inspections/tools/OutdatedRequirementInspection.kt @@ -28,7 +28,7 @@ internal class OutdatedRequirementInspection : LocalInspectionTool() { val packageManager = PythonPackageManager.Companion.forSdk(psiFile.project, sdk) - val outdatedPackages = packageManager.outdatedPackages.toMap() + val outdatedPackages = packageManager.listOutdatedPackagesSnapshot().toMap() if (outdatedPackages.isEmpty()) return diff --git a/python/src/com/jetbrains/python/run/filter/InstallPackageButtonItem.kt b/python/src/com/jetbrains/python/run/filter/InstallPackageButtonItem.kt index a94d2490433c..21f91bc7729b 100644 --- a/python/src/com/jetbrains/python/run/filter/InstallPackageButtonItem.kt +++ b/python/src/com/jetbrains/python/run/filter/InstallPackageButtonItem.kt @@ -12,7 +12,8 @@ import com.intellij.openapi.projectRoots.Sdk import com.intellij.ui.awt.RelativePoint import com.jetbrains.python.PyBundle import com.jetbrains.python.icons.PythonIcons -import com.jetbrains.python.packaging.PyPackageInstallUtils +import com.jetbrains.python.packaging.management.ui.PythonPackageManagerUI +import com.jetbrains.python.packaging.management.ui.launchInstallPackageWithBalloonBackground import java.awt.Cursor import java.awt.Point import java.awt.event.MouseEvent @@ -36,7 +37,7 @@ class InstallPackageButtonItem( ) { event: MouseEvent?, _: Point -> val component = event?.component ?: return@referenceOnHover val relativePoint = RelativePoint(component, event.point) - PyPackageInstallUtils.invokeInstallPackage(project, pythonSdk, packageName, relativePoint) + PythonPackageManagerUI.forSdk(project, pythonSdk).launchInstallPackageWithBalloonBackground(packageName, relativePoint) } val presentation = factory.withCursorOnHover(basePresentation, Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)) return PresentationRenderer(presentation) diff --git a/python/src/com/jetbrains/python/run/runAnything/PyRunAnythingPackageProvider.kt b/python/src/com/jetbrains/python/run/runAnything/PyRunAnythingPackageProvider.kt index 9efa50ecf734..cad86a3d230c 100644 --- a/python/src/com/jetbrains/python/run/runAnything/PyRunAnythingPackageProvider.kt +++ b/python/src/com/jetbrains/python/run/runAnything/PyRunAnythingPackageProvider.kt @@ -44,7 +44,7 @@ abstract class PyRunAnythingPackageProvider : RunAnythingCommandLineProvider() { it.startsWith(commandLine.toComplete) }.asSequence() } - return packageManager.installedPackages.map { it.name }.filter { it.startsWith(commandLine.toComplete) }.asSequence() + return packageManager.listInstalledPackagesSnapshot().map { it.name }.filter { it.startsWith(commandLine.toComplete) }.asSequence() } val last = commandLine.parameters.last() if (isInstall) { diff --git a/python/src/com/jetbrains/python/sdk/PythonSdkUpdater.java b/python/src/com/jetbrains/python/sdk/PythonSdkUpdater.java index c992c181d20b..d7e393672c8f 100644 --- a/python/src/com/jetbrains/python/sdk/PythonSdkUpdater.java +++ b/python/src/com/jetbrains/python/sdk/PythonSdkUpdater.java @@ -174,7 +174,7 @@ public final class PythonSdkUpdater { ContainerUtil.addIfNotNull(allStubRoots, PyTypeShed.INSTANCE.getThirdPartyStubRoot()); ContainerUtil.addIfNotNull(allStubRoots, PyBundledStubs.INSTANCE.getRoot()); PythonPackageManager packageManager = PythonPackageManager.Companion.forSdk(myProject, sdk); - Set installedPackageNames = ContainerUtil.map2Set(packageManager.getInstalledPackages(), PythonPackage::getName); + Set installedPackageNames = ContainerUtil.map2Set(packageManager.listInstalledPackagesSnapshot(), PythonPackage::getName); List bundledStubRoots = StreamEx.of(allStubRoots) .flatArray(root -> root.getChildren()) .filter(VirtualFile::isDirectory) @@ -222,6 +222,7 @@ public final class PythonSdkUpdater { indicator.setText2(""); PyPackageManager.getInstance(sdk).refreshAndGetPackages(true); //It internally invoke lazy list packages update on first call + PythonPackageManager.Companion.forSdk(myProject, mySdk); } catch (ExecutionException e) { diff --git a/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageVersionsInspection.kt b/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageVersionsInspection.kt index f6e2d80a970a..f6d78ca534a5 100644 --- a/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageVersionsInspection.kt +++ b/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageVersionsInspection.kt @@ -60,7 +60,7 @@ internal class PoetryPackageVersionsInspection : LocalInspectionTool() { it.children.mapNotNull { line -> line as? TomlKeyValue } }.forEach { keyValue -> val packageName = normalizePackageName(keyValue.key.text) - val outdatedVersion = PythonPackageManager.forSdk(module.project, sdk).outdatedPackages[packageName] + val outdatedVersion = PythonPackageManager.forSdk(module.project, sdk).listOutdatedPackagesSnapshot()[packageName] if (outdatedVersion != null) { val message = PyBundle.message("python.sdk.inspection.message.version.outdated.latest", packageName, outdatedVersion.version, outdatedVersion.latestVersion) diff --git a/python/src/com/jetbrains/python/statistics/PyPackageDaemonListener.kt b/python/src/com/jetbrains/python/statistics/PyPackageDaemonListener.kt index e7e6f6aadd87..2262620138b5 100644 --- a/python/src/com/jetbrains/python/statistics/PyPackageDaemonListener.kt +++ b/python/src/com/jetbrains/python/statistics/PyPackageDaemonListener.kt @@ -68,7 +68,7 @@ class PackageDaemonTaskExecutor(private val cs: CoroutineScope) { val packages2Versions = sdk?.let { // it's mock sdk if (sdk.sdkAdditionalData == null) return@let emptyMap() - val packagesFromPackageManager = PythonPackageManager.forSdk(project, sdk).installedPackages + val packagesFromPackageManager = PythonPackageManager.forSdk(project, sdk).listInstalledPackagesSnapshot() packagesFromPackageManager.associate { it.name to it.version } } ?: emptyMap() diff --git a/python/src/com/jetbrains/python/statistics/PyPackageUsagesCollector.kt b/python/src/com/jetbrains/python/statistics/PyPackageUsagesCollector.kt index 309e14fa7fe6..b1f890ebb727 100644 --- a/python/src/com/jetbrains/python/statistics/PyPackageUsagesCollector.kt +++ b/python/src/com/jetbrains/python/statistics/PyPackageUsagesCollector.kt @@ -6,7 +6,9 @@ import com.intellij.internal.statistic.eventLog.EventLogGroup import com.intellij.internal.statistic.eventLog.events.EventFields import com.intellij.internal.statistic.service.fus.collectors.ProjectUsagesCollector import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.runBlockingCancellable import com.intellij.openapi.project.Project +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.jetbrains.python.extensions.getSdk import com.jetbrains.python.packaging.PyPIPackageCache import com.jetbrains.python.packaging.PyPackageManager @@ -61,6 +63,7 @@ internal class PyPackageVersionUsagesCollector : ProjectUsagesCollector() { return result } + @RequiresBackgroundThread(generateAssertion = false) private fun getInstalledPackages(project: Project): Set { val result = HashSet() val pypiPackages = PyPIPackageCache.getInstance() @@ -70,7 +73,10 @@ internal class PyPackageVersionUsagesCollector : ProjectUsagesCollector() { if (sdk.sdkAdditionalData !is PythonSdkAdditionalData) continue val executionType = sdk.executionType val interpreterType = sdk.interpreterType - PythonPackageManager.forSdk(project, sdk).installedPackages + val installedPackages = runBlockingCancellable { + PythonPackageManager.forSdk(project, sdk).listInstalledPackages() + } + installedPackages .filter { pypiPackages.containsPackage(it.name) } .forEach { pythonPackage -> val version = pythonPackage.version diff --git a/python/testSrc/com/jetbrains/env/python/PipPackageManagerTestBase.kt b/python/testSrc/com/jetbrains/env/python/PipPackageManagerTestBase.kt index aa5f2224ddec..bd64f0273c34 100644 --- a/python/testSrc/com/jetbrains/env/python/PipPackageManagerTestBase.kt +++ b/python/testSrc/com/jetbrains/env/python/PipPackageManagerTestBase.kt @@ -2,14 +2,11 @@ package com.jetbrains.env.python import com.intellij.testFramework.ProjectRule -import com.jetbrains.python.getOrThrow import com.jetbrains.python.packaging.pip.PipPythonPackageManager -import com.jetbrains.python.packaging.pip.runPackagingTool import kotlinx.coroutines.test.runTest import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.empty import org.hamcrest.Matchers.not -import org.junit.Assert.assertTrue import org.junit.Test import kotlin.time.Duration.Companion.minutes @@ -24,13 +21,11 @@ abstract class PipPackageManagerTestBase { protected abstract val sdkRule: PySDKRule - @Test fun testList(): Unit = runTest(timeout = 5.minutes) { - val pipListStdout = runPackagingTool(projectRule.project, sdkRule.sdk, "list", emptyList(), "").getOrThrow() PipPythonPackageManager(projectRule.project, sdkRule.sdk).apply { - assertThat("No packages return", reloadPackages().getOrThrow(), not(empty())) - assertTrue("Output shouldn't be empty", pipListStdout.isNotBlank()) + assertThat("No packages return", reloadPackages().successOrNull, not(empty())) + assertThat("Installed packages shouldn't be empty", listInstalledPackages(), not(empty())) } } } \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/env/python/PyPackagingTest.java b/python/testSrc/com/jetbrains/env/python/PyPackagingTest.java index f6542ad4fa98..101881171d0d 100644 --- a/python/testSrc/com/jetbrains/env/python/PyPackagingTest.java +++ b/python/testSrc/com/jetbrains/env/python/PyPackagingTest.java @@ -112,7 +112,7 @@ public class PyPackagingTest extends PyEnvTestCase { final PyPackageManager manager = PyPackageManager.getInstance(venvSdk); final List packages1 = manager.refreshAndGetPackages(false); // TODO: Install Markdown from a local file - manager.install(Arrays.asList(pyRequirement("Markdown", PyRequirementRelation.LTE, "3.3.3"), pyRequirement("httplib2")), + manager.install(Arrays.asList(pyRequirement("Markdown", PyRequirementRelation.LTE, "3.3.3"), pyRequirement("httplib2",null)), Collections.emptyList()); final List packages2 = manager.refreshAndGetPackages(false); final PyPackage markdown2 = findPackage("Markdown", packages2); diff --git a/python/testSrc/com/jetbrains/python/inspections/PyPackageRequirementsInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyPackageRequirementsInspectionTest.java index e37332531bf2..d14d5b925190 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyPackageRequirementsInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyPackageRequirementsInspectionTest.java @@ -3,13 +3,10 @@ package com.jetbrains.python.inspections; import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.testFramework.ServiceContainerUtil; import com.intellij.util.containers.ContainerUtil; import com.jetbrains.python.fixtures.PyInspectionTestCase; import com.jetbrains.python.packaging.PyRequirement; import com.jetbrains.python.packaging.common.PythonPackage; -import com.jetbrains.python.packaging.management.PythonPackageManagerService; -import com.jetbrains.python.packaging.management.TestPythonPackageManagerService; import com.jetbrains.python.psi.LanguageLevel; import com.jetbrains.python.sdk.PythonSdkUtil; import com.jetbrains.python.sdk.pipenv.PipenvFilesUtilsKt; @@ -18,6 +15,8 @@ import org.jetbrains.annotations.NotNull; import java.util.Collections; import java.util.List; +import static com.jetbrains.python.packaging.management.TestPythonPackageManagerService.replacePythonPackageManagerServiceWithTestInstance; + public class PyPackageRequirementsInspectionTest extends PyInspectionTestCase { @NotNull @@ -26,23 +25,13 @@ public class PyPackageRequirementsInspectionTest extends PyInspectionTestCase { return PyPackageRequirementsInspection.class; } - private void replacePythonPackageManagerServiceWithTestInstance( - PythonPackageManagerService serviceInstance) { - ServiceContainerUtil.replaceService( - myFixture.getProject(), - PythonPackageManagerService.class, - serviceInstance, - myFixture.getProject() - ); - } @Override public void setUp() throws Exception { super.setUp(); final Sdk sdk = PythonSdkUtil.findPythonSdk(myFixture.getModule()); assertNotNull(sdk); - - replacePythonPackageManagerServiceWithTestInstance(new TestPythonPackageManagerService()); + replacePythonPackageManagerServiceWithTestInstance(myFixture.getProject(), List.of()); } public void testPartiallySatisfiedRequirementsTxt() { @@ -129,9 +118,7 @@ public class PyPackageRequirementsInspectionTest extends PyInspectionTestCase { public void testRequirementMismatchWarningDisappearsOnInstall() { PythonPackage zopeInterfacePackage = new PythonPackage("zope.interface", "5.4.0", false); - replacePythonPackageManagerServiceWithTestInstance( - new TestPythonPackageManagerService(Collections.singletonList(zopeInterfacePackage)) - ); + replacePythonPackageManagerServiceWithTestInstance(myFixture.getProject(), Collections.singletonList(zopeInterfacePackage)); doMultiFileTest("a.py"); } diff --git a/python/testSrc/com/jetbrains/python/packaging/PyPackageUtilTest.java b/python/testSrc/com/jetbrains/python/packaging/PyPackageUtilTest.java index a347f283184b..78129ebc1daf 100644 --- a/python/testSrc/com/jetbrains/python/packaging/PyPackageUtilTest.java +++ b/python/testSrc/com/jetbrains/python/packaging/PyPackageUtilTest.java @@ -146,9 +146,9 @@ public class PyPackageUtilTest extends PyTestCase { final Map> extrasRequirements = PyPackageUtil.findSetupPyExtrasRequire(module); final ImmutableMap> expected = ImmutableMap.of( - "e1", Collections.singletonList(PyRequirementsKt.pyRequirement("r1")), - "e2", Collections.singletonList(PyRequirementsKt.pyRequirement("r2")), - "e3", Arrays.asList(PyRequirementsKt.pyRequirement("r3"), PyRequirementsKt.pyRequirement("r4")) + "e1", Collections.singletonList(PyRequirementsKt.pyRequirement("r1",null)), + "e2", Collections.singletonList(PyRequirementsKt.pyRequirement("r2",null)), + "e3", Arrays.asList(PyRequirementsKt.pyRequirement("r3",null), PyRequirementsKt.pyRequirement("r4",null)) ); assertEquals(expected, extrasRequirements); diff --git a/python/testSrc/com/jetbrains/python/packaging/PyRequirementTest.java b/python/testSrc/com/jetbrains/python/packaging/PyRequirementTest.java index b186edcee5ab..f54bad241f0a 100644 --- a/python/testSrc/com/jetbrains/python/packaging/PyRequirementTest.java +++ b/python/testSrc/com/jetbrains/python/packaging/PyRequirementTest.java @@ -1945,15 +1945,15 @@ public class PyRequirementTest extends PyTestCase { // TODO: hashes // https://www.python.org/dev/peps/pep-0508/#names public void testRequirement() { - assertEquals(pyRequirement("Orange-Bioinformatics"), fromLine("Orange-Bioinformatics")); - assertEquals(pyRequirement("MOCPy"), fromLine("MOCPy")); - assertEquals(pyRequirement("score.webassets"), fromLine("score.webassets")); - assertEquals(pyRequirement("pip_helpers"), fromLine("pip_helpers")); - assertEquals(pyRequirement("Django"), fromLine("Django")); - assertEquals(pyRequirement("django"), fromLine("django")); - assertEquals(pyRequirement("pinax-utils"), fromLine("pinax-utils")); - assertEquals(pyRequirement("no_limit_nester"), fromLine("no_limit_nester")); - assertEquals(pyRequirement("Flask-Celery-py3"), fromLine("Flask-Celery-py3")); + assertEquals(pyRequirement("Orange-Bioinformatics",null), fromLine("Orange-Bioinformatics")); + assertEquals(pyRequirement("MOCPy",null), fromLine("MOCPy")); + assertEquals(pyRequirement("score.webassets",null), fromLine("score.webassets")); + assertEquals(pyRequirement("pip_helpers",null), fromLine("pip_helpers")); + assertEquals(pyRequirement("Django",null), fromLine("Django")); + assertEquals(pyRequirement("django",null), fromLine("django")); + assertEquals(pyRequirement("pinax-utils",null), fromLine("pinax-utils")); + assertEquals(pyRequirement("no_limit_nester",null), fromLine("no_limit_nester")); + assertEquals(pyRequirement("Flask-Celery-py3",null), fromLine("Flask-Celery-py3")); } // https://www.python.org/dev/peps/pep-0440/ @@ -2384,7 +2384,9 @@ public class PyRequirementTest extends PyTestCase { final VirtualFile requirementsFile = getVirtualFileByName(getTestDataPath() + "/requirement/recursive/requirements.txt"); assertNotNull(requirementsFile); - assertEquals(Arrays.asList(pyRequirement("bitly_api"), pyRequirement("numpy"), pyRequirement("SomeProject")), + assertEquals(Arrays.asList(pyRequirement("bitly_api",null), + pyRequirement("numpy",null), + pyRequirement("SomeProject",null)), PyRequirementParser.fromFile(requirementsFile)); } diff --git a/python/testSrc/com/jetbrains/python/packaging/PyRequirementsGenerationTest.kt b/python/testSrc/com/jetbrains/python/packaging/PyRequirementsGenerationTest.kt index 6bb34bcc288a..66a5dcf284b5 100644 --- a/python/testSrc/com/jetbrains/python/packaging/PyRequirementsGenerationTest.kt +++ b/python/testSrc/com/jetbrains/python/packaging/PyRequirementsGenerationTest.kt @@ -11,6 +11,8 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk import com.intellij.testFramework.registerServiceInstance import com.jetbrains.python.fixtures.PyTestCase +import com.jetbrains.python.packaging.common.PythonPackage +import com.jetbrains.python.packaging.management.TestPythonPackageManagerService import org.easymock.EasyMock class PyRequirementsGenerationTest : PyTestCase() { @@ -19,7 +21,7 @@ class PyRequirementsGenerationTest : PyTestCase() { private val installedPackages = mapOf("Django" to "3.0.0", "requests" to "2.22.0", "Jinja2" to "2.11.1", - "pandas" to "1.0.1" , + "pandas" to "1.0.1", "cookiecutter" to "1.7.0", "numpy" to "1.18.1", "tox" to "3.14.4", @@ -46,10 +48,12 @@ class PyRequirementsGenerationTest : PyTestCase() { fun testBaseFileUpdate() = doTest(modifyBaseFiles = true) fun testBaseFileCleanup() = doTest(modifyBaseFiles = true, removeUnused = true) - private fun doTest(versionSpecifier: PyRequirementsVersionSpecifierType = PyRequirementsVersionSpecifierType.STRONG_EQ, - removeUnused: Boolean = false, - modifyBaseFiles: Boolean = false, - packages: Map = installedPackages) { + private fun doTest( + versionSpecifier: PyRequirementsVersionSpecifierType = PyRequirementsVersionSpecifierType.STRONG_EQ, + removeUnused: Boolean = false, + modifyBaseFiles: Boolean = false, + packages: Map = installedPackages, + ) { val settings = PyPackageRequirementsSettings.getInstance(myFixture.module) val oldRequirementsPath = settings.requirementsPath val oldVersionSpecifier = settings.versionSpecifier @@ -107,6 +111,9 @@ class PyRequirementsGenerationTest : PyTestCase() { private fun overrideInstalledPackages(packages: Map) { ApplicationManager.getApplication().registerServiceInstance(PyPackageManagers::class.java, MockPyPackageManagers(packages)) + + val packages = packages.map { PythonPackage(it.key, it.value, false) } + TestPythonPackageManagerService.replacePythonPackageManagerServiceWithTestInstance(project = myFixture.project, packages) } private class MockPyPackageManagers(val packages: Map) : PyPackageManagers() { diff --git a/python/testSrc/com/jetbrains/python/packaging/management/TestPackageManager.kt b/python/testSrc/com/jetbrains/python/packaging/management/TestPackageManager.kt index f526cf1bd5b0..3947156d5da0 100644 --- a/python/testSrc/com/jetbrains/python/packaging/management/TestPackageManager.kt +++ b/python/testSrc/com/jetbrains/python/packaging/management/TestPackageManager.kt @@ -3,6 +3,7 @@ package com.jetbrains.python.packaging.management import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk +import com.intellij.testFramework.replaceService import com.jetbrains.python.errorProcessing.PyResult import com.jetbrains.python.packaging.bridge.PythonPackageManagementServiceBridge import com.jetbrains.python.packaging.common.* @@ -112,6 +113,13 @@ class TestPythonPackageManagerService(val installedPackages: List override fun getServiceScope(): CoroutineScope { return CoroutineScope(Job()) } + + companion object { + @JvmStatic + fun replacePythonPackageManagerServiceWithTestInstance(project: Project, installedPackages: List = emptyList()) { + project.replaceService(PythonPackageManagerService::class.java, TestPythonPackageManagerService(installedPackages), project) + } + } } @TestOnly diff --git a/python/testSrc/com/jetbrains/python/packaging/pythonPackageManager/PythonPackageManagerNullAdditionalDataTest.kt b/python/testSrc/com/jetbrains/python/packaging/pythonPackageManager/PythonPackageManagerNullAdditionalDataTest.kt index 58255636ea45..0fda61c743dd 100644 --- a/python/testSrc/com/jetbrains/python/packaging/pythonPackageManager/PythonPackageManagerNullAdditionalDataTest.kt +++ b/python/testSrc/com/jetbrains/python/packaging/pythonPackageManager/PythonPackageManagerNullAdditionalDataTest.kt @@ -11,6 +11,7 @@ import com.jetbrains.env.PyEnvTestCase import com.jetbrains.env.PyExecutionFixtureTestTask import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification import com.jetbrains.python.packaging.management.PythonPackageManager +import com.jetbrains.python.packaging.management.hasInstalledPackage import com.jetbrains.python.packaging.management.toInstallRequest import com.jetbrains.python.packaging.repository.PyPIPackageRepository import com.jetbrains.python.sdk.PythonSdkType @@ -89,10 +90,10 @@ class PythonPackageManagerNullAdditionalDataTask(private val pkg: PythonReposito val manager = PythonPackageManager.forSdk(myFixture.project, configuredSdk) manager.installPackage(pkg.toInstallRequest(), emptyList()) - assertTrue("Package should be installed", manager.installedPackages.map { it.name }.contains(pkg.name)) + assertTrue("Package should be installed", manager.hasInstalledPackage(pkg.name)) manager.uninstallPackage(pkg.name) - assertTrue("Package should be uninstalled", !manager.installedPackages.map { it.name }.contains(pkg.name)) + assertTrue("Package should be uninstalled", !manager.hasInstalledPackage(pkg.name)) } }