PY-81238 Packages: Add PyPackageManagerUI

Signed-off-by: Nikita.Ashihmin <nikita.ashihmin@jetbrains.com>

GitOrigin-RevId: 810446f634befa8631be89ec6c50f957a5f6124e
This commit is contained in:
Nikita.Ashihmin
2025-05-29 02:37:23 +00:00
committed by intellij-monorepo-bot
parent 4b1d24562a
commit c232ee240e
60 changed files with 735 additions and 1058 deletions
@@ -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<PythonRepositoryPackageSpecification> {
private fun readRequiredPackages(): List<PyRequirement> {
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 {
@@ -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()
}
@@ -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<Unit> {
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 {
@@ -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
@@ -22,7 +22,7 @@ public final class PyStdlibUtil {
private PyStdlibUtil() {
}
public static @Nullable Collection<String> getPackages() {
public static @Nullable Set<String> getPackages() {
return PACKAGES.get();
}
@@ -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].
@@ -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;
}
}
@@ -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
}
}
@@ -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? {
@@ -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() {
@@ -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
@@ -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());
}
};
}
@@ -88,7 +88,7 @@ class PyRequirementVisitor(
ignoredPackages: Set<String?>,
): List<PyRequirement> {
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
}
@@ -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
@@ -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<TYPE_SPECIFIC_SETTINGS : PyV3ProjectType
// Either base settings (which create venv) might generate some or type-specific settings (like Django) may.
// So we expand it right after SDK generation, but if there are no files yet, we do it again after project generation
uiServices.expandProjectTreeView(project)
typeSpecificSettings.generateProject(module, baseDir, sdk).onFailure {
uiServices.errorSink.emit(it)
}
typeSpecificSettings.generateProject(module, baseDir, sdk, uiServices.errorSink)
uiServices.expandProjectTreeView(project)
}
}
@@ -4,8 +4,7 @@ package com.jetbrains.python.newProjectWizard
import com.intellij.openapi.module.Module
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.vfs.VirtualFile
import com.jetbrains.python.errorProcessing.PyResult
import org.jetbrains.annotations.CheckReturnValue
import com.jetbrains.python.errorProcessing.ErrorSink
/**
* Implementation must have fields and generate project-specific (like Django) things using them i.e:
@@ -21,9 +20,6 @@ fun interface PyV3ProjectTypeSpecificSettings {
/**
* Generate project-specific things in [baseDir].
* You might need to [installPackages] on [sdk].
*
* Returns error if generation failed due to execution error
*/
@CheckReturnValue
suspend fun generateProject(module: Module, baseDir: VirtualFile, sdk: Sdk): PyResult<Unit>
suspend fun generateProject(module: Module, baseDir: VirtualFile, sdk: Sdk, errorSink: ErrorSink)
}
@@ -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<Unit> {
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
}
}
}
@@ -13,17 +13,15 @@ import com.jetbrains.python.sdk.PythonSdkUtil
internal class PyDependencyCollector : DependencyCollector {
override suspend fun collectDependencies(project: Project): Collection<String> {
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()
}
}
@@ -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<List<PythonPackage>> {
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<String> = emptyList(),
): PyResult<List<PythonPackage>> {
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 }
}
@@ -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<ExecutionException> exceptions);
}
@@ -57,328 +29,11 @@ public final class PyPackageManagerUI {
public void install(final @Nullable List<PyRequirement> requirements, final @NotNull List<String> 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<PyPackage> packages) {
SideEffectGuard.checkSideEffectAllowed(SideEffectGuard.EffectType.EXEC);
if (checkDependents(packages)) {
return;
}
ProgressManager.getInstance().run(new UninstallTask(myProject, mySdk, myListener, packages));
}
private boolean checkDependents(@NotNull List<PyPackage> packages) {
try {
Map<String, Set<PyPackage>> dependentPackages = collectDependents(packages, mySdk);
if (dependentPackages.isEmpty()) {
return false;
}
boolean[] warning = {true};
ApplicationManager.getApplication().invokeAndWait(() -> {
if (dependentPackages.size() == 1) {
Map.Entry<String, Set<PyPackage>> packageToDependents = ContainerUtil.getOnlyItem(dependentPackages.entrySet());
assert packageToDependents != null;
Set<PyPackage> 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<String> dep = new ArrayList<>();
for (Map.Entry<String, Set<PyPackage>> 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<String, Set<PyPackage>> collectDependents(final @NotNull List<PyPackage> packages,
Sdk sdk) throws ExecutionException {
Map<String, Set<PyPackage>> dependentPackages = new HashMap<>();
for (PyPackage pkg : packages) {
final Set<PyPackage> 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<ExecutionException> 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<ExecutionException> exceptions) {
if (exceptions.isEmpty()) {
sendNotification(
getSuccessTitle(),
getSuccessDescription(),
NotificationType.INFORMATION,
exceptions,
null
);
}
else {
final List<Pair<String, String>> 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<String> 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<ExecutionException> 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 + " <a href=\"xxx\">" + PyBundle.message(key) + "</a>";
}
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<PyRequirement> myRequirements;
private final @NotNull List<String> myExtraArgs;
InstallTask(@Nullable Project project,
@NotNull Sdk sdk,
@Nullable List<PyRequirement> requirements,
@NotNull List<String> extraArgs,
@Nullable Listener listener) {
super(project, sdk, PyBundle.message("python.packaging.progress.title.installing.packages"), listener);
myRequirements = requirements;
myExtraArgs = extraArgs;
}
@Override
protected @NotNull List<ExecutionException> runTask(@NotNull ProgressIndicator indicator) {
final List<ExecutionException> 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<PyPackage> myPackages;
UninstallTask(@Nullable Project project,
@NotNull Sdk sdk,
@Nullable Listener listener,
@NotNull List<PyPackage> packages) {
super(project, sdk, PyBundle.message("python.packaging.progress.title.uninstalling.packages"), listener);
myPackages = packages;
}
@Override
protected @NotNull List<ExecutionException> runTask(@NotNull ProgressIndicator indicator) {
final List<ExecutionException> 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);
}
}
@@ -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<String, PyPackage>,
private val settings: PyPackageRequirementsSettings) {
class PyRequirementsFileVisitor(private val importedPackages: MutableMap<String, PythonPackage>,
private val settings: PyPackageRequirementsSettings
) {
private val collectedOutput: MutableMap<VirtualFile, MutableList<String>> = mutableMapOf()
private val unmatchedLines: MutableList<String> = mutableListOf()
@@ -91,7 +93,7 @@ class PyRequirementsFileVisitor(private val importedPackages: MutableMap<String,
collectedOutput[requirementsFile.virtualFile] = outputLines
}
private fun formatRequirement(requirement: PyRequirement, pkg: PyPackage, lines: List<String>): List<String> = when {
private fun formatRequirement(requirement: PyRequirement, pkg: PythonPackage, lines: List<String>): List<String> = when {
// keeping editable and vcs requirements
requirement.isEditable || vcsPrefixes.any { lines.first().startsWith(it) } -> lines
// existing version separators match the current package version
@@ -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<String>,
val baseFilesOutput: Map<VirtualFile, List<String>>,
val unhandledLines: List<String>,
val unchangedInBaseFiles: List<String>) {
data class PyRequirementsAnalysisResult(
val currentFileOutput: List<String>,
val baseFilesOutput: Map<VirtualFile, List<String>>,
val unhandledLines: List<String>,
val unchangedInBaseFiles: List<String>,
) {
companion object {
fun empty() = PyRequirementsAnalysisResult(emptyList(), emptyMap(), emptyList(), emptyList())
}
fun withImportedPackages(importedPackages: Map<String, PyPackage>, settings: PyPackageRequirementsSettings): PyRequirementsAnalysisResult {
fun withImportedPackages(importedPackages: MutableMap<String, PythonPackage>, 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<Set<String>, Exception>(module.project, title, true) {
override fun compute(indicator: ProgressIndicator): Set<String> {
@@ -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<String>) {
(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) }
}
@@ -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<InstalledPackage> {
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<RepoPackage> {
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<RepoPackage> {
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<InstalledPackage>, 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<InstalledPackage, @NlsSafe String> { it.name })
}
}
override fun fetchPackageVersions(packageName: String, consumer: CatchingConsumer<in List<String>, 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<in String, in Exception>) {
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<in String, in Exception>) {
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))
}
}
@@ -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
@@ -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<PyRequirement>?,
extraArgs: List<String>,
indicator: ProgressIndicator,
): PyResult<Unit> = 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<Unit> {
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<PyRequirement>,
extraArgs: List<String>,
): PyResult<Unit> {
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<PyPackage>, indicator: ProgressIndicator): PyResult<Unit> = 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<PythonPackage>): PyResult<Unit> {
return manager.uninstallPackage(*packages.map { it.name }.toTypedArray()).mapSuccess {}
}
private fun PyPackage.toPythonPackage(): PythonPackage = PythonPackage(this.name, this.version, false)
}
}
@@ -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<PythonPackage> = emptyList()
@ApiStatus.Internal
@Volatile
open var installedPackages: List<PythonPackage> = emptyList()
protected set
protected open var installedPackages: List<PythonPackage> = emptyList()
@ApiStatus.Internal
@Volatile
var outdatedPackages: Map<String, PythonOutdatedPackage> = emptyMap()
private set
protected var outdatedPackages: Map<String, PythonOutdatedPackage> = 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<String> = emptyList()): PyResult<List<PythonPackage>> {
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<List<PythonPackage>> {
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<List<PythonPackage>> {
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<PythonPackage> {
waitForInit()
return listInstalledPackagesSnapshot()
}
@ApiStatus.Internal
fun listInstalledPackagesSnapshot(): List<PythonPackage> {
return installedPackages
}
@ApiStatus.Internal
suspend fun listOutdatedPackages(): Map<String, PythonOutdatedPackage> {
waitForInit()
return listOutdatedPackagesSnapshot()
}
@ApiStatus.Internal
fun listOutdatedPackagesSnapshot(): Map<String, PythonOutdatedPackage> {
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 <T> executeCommand(
progressTitle: @Nls String,
operation: suspend (() -> PyResult<T>),
): PyResult<T> = 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<PythonPackageManagementListener> = Topic(PythonPackageManagementListener::class.java, Topic.BroadcastDirection.TO_DIRECT_CHILDREN)
val RUNNING_PACKAGING_TASKS: Key<Boolean> = Key.create("PyPackageRequirementsInspection.RunningPackagingTasks")
}
}
}
@@ -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<Pair<PyPackageRepository, Set<String>>> {
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) }
}
@@ -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 <T> runPackagingOperationBackground(
project: Project,
@ProgressTitle title: String,
operation: suspend (() -> PyResult<T>),
): PyResult<T> = withBackgroundProgress(project = project, title, cancellable = true) {
operation()
}
}
@@ -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<PythonPackage>? {
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<String> = emptyList(),
): List<PythonPackage>? {
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<PythonRepositoryPackageSpecification>,
): List<PythonPackage>? {
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<String>,
): List<PythonPackage>? {
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 <T> executeCommand(
progressTitle: @Nls String,
operation: suspend (() -> PyResult<T>?),
): 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)
}
}
}
@@ -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 <T> runPackagingOperationMaybeBackground(
project: Project,
errorSink: ErrorSink?,
@NlsContexts.ProgressTitle title: String,
operation: suspend (() -> PyResult<T>?),
): T? = withBackgroundProgress(project = project, title, cancellable = true) {
runPackagingOperationMaybeShowErrorDialog(errorSink) {
operation()
}
}
private suspend fun <T> runPackagingOperationMaybeShowErrorDialog(
errorSink: ErrorSink?,
operation: suspend (() -> PyResult<T>?),
): 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 }
}
@@ -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<PythonPackage>? =
updatePackagesByNamesBackground(listOf(pyPackage))
/**
* @return List of all installed packages or null if the operation was failed.
*/
@ApiStatus.Internal
suspend fun PythonPackageManagerUI.updatePackagesByNamesBackground(
packages: List<String>,
): List<PythonPackage>? {
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<PyRequirement>,
options: List<String> = emptyList(),
): List<PythonPackage>? {
//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<String> = emptyList(),
): List<PythonPackage>? = 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<String> = emptyList(),
): List<PythonPackage>? {
val pyRequirements = packages.map { pyRequirement(it) }
return installPyRequirementsBackground(pyRequirements,
options = options)
}
@@ -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<String> =
listOfNotNull(sdk.homePath.toString(), wheelPath.toString(), "install") + additionalArgs
@@ -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<String, InstalledPackage> = 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<PyInvalidRepositoryViewData>
get() = service<PyPackageRepositories>().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<String> = 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)
}
@@ -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())
}
}
@@ -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<DisplayablePackage>, val exactMatch: Int = -1, val moreItems: Int = 0)
open class PyPackagesViewData(val repository: PyPackageRepository, val packages: List<DisplayablePackage>, val exactMatch: Int = -1, val moreItems: Int = 0)
class PyInvalidRepositoryViewData(repository: PyPackageRepository) : PyPackagesViewData(repository, emptyList())
@@ -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()
}
@@ -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<PyPackagingToolWindowService>().updatePackage(specification)
project.service<PyPackagingToolWindowService>().updatePackages(selectedPackage.name)
}
}
}
@@ -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
@@ -169,12 +169,6 @@ public class PyPackageManagementService extends PackageManagementServiceEx {
PyPackageService.getInstance().addSdkToUserSite(mySdk.getHomePath(), newValue);
}
@Override
public @NotNull List<? extends InstalledPackage> getInstalledPackagesList() throws ExecutionException {
List<PyPackage> 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() {
@@ -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<PyRequirement>?, extraArgs: List<String>, 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<PyPackage>, listener: PyPackageManagerUI.Listener?) {
PyPackageCoroutine.getScope(project).launch {
val manager = PythonPackageManagerUI.forSdk(project, sdk)
listener?.started()
manager.uninstallPackagesBackground(packages.map { it.name })
listener?.finished(emptyList())
}
}
}
@@ -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) {
@@ -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<PyRequireme
PyPackageCoroutine.getScope(project).launch(Dispatchers.Default) {
val sdk = getPythonSdk(file) ?: return@launch
val manager = PythonPackageManager.Companion.forSdk(project, sdk)
PythonPackagesInstaller.installWithRequirements(manager, confirmedPackages, emptyList())
val manager = PythonPackageManagerUI.forSdk(project, sdk)
manager.installPyRequirementsBackground(confirmedPackages.toList(), emptyList<String>()
)
}
}
@@ -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<Stri
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val file = descriptor.psiElement.containingFile ?: return
val sdk = getPythonSdk(file) ?: return
val manager = PythonPackageManager.forSdk(project, sdk)
val specifications = outdatedPyRequirements.mapNotNull {
val latestVersion = manager.outdatedPackages[it]?.latestVersion ?: return@mapNotNull null
manager.findPackageSpecification(it, latestVersion, PyRequirementRelation.EQ) ?: return
}.toTypedArray()
val manager = PythonPackageManagerUI.forSdk(project, sdk)
PyPackageCoroutine.getScope(project).launch {
manager.updatePackages(*specifications)
val outdatedPackages = manager.manager.listOutdatedPackages()
val packagesForUpdate = outdatedPyRequirements.intersect(outdatedPackages.keys).ifEmpty {
return@launch
}
manager.updatePackagesByNamesBackground(packagesForUpdate.toList())
}
}
}
@@ -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,13 +20,10 @@ internal class UpdateRequirementQuickFix(private val packageName: String) : Loca
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val file = descriptor.psiElement.containingFile ?: return
val sdk = getPythonSdk(file) ?: return
val manager = PythonPackageManager.forSdk(project, sdk)
val latestVersion = manager.outdatedPackages[this.packageName]?.latestVersion ?: return
val packageSpecification = manager.findPackageSpecification(this.packageName, latestVersion, PyRequirementRelation.EQ) ?: return
val manager = PythonPackageManagerUI.forSdk(project, sdk)
PyPackageCoroutine.getScope(project).launch {
manager.updatePackages(packageSpecification)
manager.updatePackagesByNamesBackground(listOf(packageName))
}
}
}
@@ -39,7 +39,7 @@ class NotInstalledRequirementInspection : LocalInspectionTool() {
val packageManager = PythonPackageManager.forSdk(psiFile.project, sdk)
val packages = packageManager.installedPackages.map { it.name }
val packages = packageManager.listInstalledPackagesSnapshot().map { it.name }
val notInstalledRequirementsWithPsi = requirements.mapNotNull { requirement ->
val pyRequirement = PyRequirementParser.fromLine(requirement.text) ?: return@mapNotNull null
@@ -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
@@ -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)
@@ -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) {
@@ -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<String> installedPackageNames = ContainerUtil.map2Set(packageManager.getInstalledPackages(), PythonPackage::getName);
Set<String> installedPackageNames = ContainerUtil.map2Set(packageManager.listInstalledPackagesSnapshot(), PythonPackage::getName);
List<VirtualFile> 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) {
@@ -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)
@@ -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()
@@ -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<MetricEvent> {
val result = HashSet<MetricEvent>()
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
@@ -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()))
}
}
}
@@ -112,7 +112,7 @@ public class PyPackagingTest extends PyEnvTestCase {
final PyPackageManager manager = PyPackageManager.getInstance(venvSdk);
final List<PyPackage> 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<PyPackage> packages2 = manager.refreshAndGetPackages(false);
final PyPackage markdown2 = findPackage("Markdown", packages2);
@@ -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");
}
@@ -146,9 +146,9 @@ public class PyPackageUtilTest extends PyTestCase {
final Map<String, List<PyRequirement>> extrasRequirements = PyPackageUtil.findSetupPyExtrasRequire(module);
final ImmutableMap<String, List<PyRequirement>> 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);
@@ -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));
}
@@ -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<String, String> = installedPackages) {
private fun doTest(
versionSpecifier: PyRequirementsVersionSpecifierType = PyRequirementsVersionSpecifierType.STRONG_EQ,
removeUnused: Boolean = false,
modifyBaseFiles: Boolean = false,
packages: Map<String, String> = 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<String, String>) {
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<String, String>) : PyPackageManagers() {
@@ -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<PythonPackage>
override fun getServiceScope(): CoroutineScope {
return CoroutineScope(Job())
}
companion object {
@JvmStatic
fun replacePythonPackageManagerServiceWithTestInstance(project: Project, installedPackages: List<PythonPackage> = emptyList()) {
project.replaceService(PythonPackageManagerService::class.java, TestPythonPackageManagerService(installedPackages), project)
}
}
}
@TestOnly
@@ -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))
}
}