mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-81238 Packages: Add PyPackageManagerUI
Signed-off-by: Nikita.Ashihmin <nikita.ashihmin@jetbrains.com> GitOrigin-RevId: 810446f634befa8631be89ec6c50f957a5f6124e
This commit is contained in:
committed by
intellij-monorepo-bot
parent
4b1d24562a
commit
c232ee240e
+15
-29
@@ -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
-5
@@ -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()
|
||||
}
|
||||
+4
-6
@@ -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
|
||||
|
||||
+1
-1
@@ -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].
|
||||
|
||||
-42
@@ -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;
|
||||
}
|
||||
}
|
||||
+31
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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) }
|
||||
}
|
||||
|
||||
+24
-43
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+66
@@ -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 }
|
||||
}
|
||||
+97
@@ -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
|
||||
|
||||
+64
-61
@@ -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)
|
||||
}
|
||||
|
||||
+1
-5
@@ -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())
|
||||
+2
-2
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
+1
-3
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -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) {
|
||||
|
||||
+5
-4
@@ -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>()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
-9
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-7
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+4
-17
@@ -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
|
||||
|
||||
+3
-2
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user