PY-85503 Introduce PyPIPackageRanking service for managing PyPI package rankings. Added logic to not asking confirmation for installing well-known packages by quick-fix.

GitOrigin-RevId: 5e57bb637d8ad66ffd186b8dcc130da7f090e731
This commit is contained in:
Timur Malanin
2025-11-13 19:34:24 +00:00
committed by intellij-monorepo-bot
parent a7842dfbda
commit 32b3d56932
3 changed files with 16091 additions and 16 deletions
File diff suppressed because it is too large Load Diff
@@ -11,6 +11,7 @@ import com.intellij.openapi.ui.Messages
import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.PyRequirement
import com.jetbrains.python.packaging.ui.PyChooseRequirementsDialog
import com.jetbrains.python.packaging.utils.PyPIPackageRanking
import com.jetbrains.python.statistics.PyPackagesUsageCollector
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -21,34 +22,47 @@ internal object PyPackageManagerUiConfirmationHelpers {
internal suspend fun getConfirmedPackages(pyRequirements: List<PyRequirement>, project: Project): List<PyRequirement> {
if (pyRequirements.isEmpty())
return emptyList()
val (rankedPackages, unrankedPackages) = pyRequirements.partition { requirement ->
PyPIPackageRanking().rankedPackages.await().map { it.name }.contains(requirement.name)
}
if (unrankedPackages.isEmpty()) {
return pyRequirements
}
val confirmationEnabled = PropertiesComponent.getInstance()
.getBoolean(CONFIRM_PACKAGE_INSTALLATION_PROPERTY, true)
if (!confirmationEnabled)
if (!confirmationEnabled) {
return pyRequirements
}
if (pyRequirements.size == 1) {
val pyPackage = pyRequirements.first()
val confirmedUnranked = if (unrankedPackages.size == 1) {
val pyPackage = unrankedPackages.first()
if (askSingleFileConfirmation(pyPackage, project)) {
return listOf(pyPackage)
listOf(pyPackage)
}
else {
PyPackagesUsageCollector.installAllCanceledEvent.log()
return listOf()
emptyList()
}
}
else {
withContext(Dispatchers.EDT) {
val dialog = PyChooseRequirementsDialog(project, unrankedPackages) { it.presentableText }
val result = dialog.showAndGet()
if (!result) {
PyPackagesUsageCollector.installAllCanceledEvent.log()
emptyList()
}
else {
dialog.markedElements
}
}
}
return withContext(Dispatchers.EDT) {
val dialog = PyChooseRequirementsDialog(project, pyRequirements) { it.presentableText }
val result = dialog.showAndGet()
if (!result) {
PyPackagesUsageCollector.installAllCanceledEvent.log()
listOf()
}
else {
dialog.markedElements
}
}
return rankedPackages + confirmedUnranked
}
private suspend fun askSingleFileConfirmation(pyRequirement: PyRequirement, project: Project): Boolean = withContext(Dispatchers.EDT) {
@@ -0,0 +1,57 @@
// 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.google.common.io.Resources
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.jetbrains.python.packaging.PyPackageName
import kotlinx.coroutines.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.jetbrains.annotations.ApiStatus
import java.net.URL
@ApiStatus.Internal
fun PyPIPackageRanking(): PyPIPackageRanking = service<PyPIPackageRankingImpl>()
@ApiStatus.Internal
interface PyPIPackageRanking {
val rankedPackages: Deferred<Set<PyPackageName>>
}
@Serializable
private data class Packages(
val packages: List<PackageEntry>
)
@Serializable
private data class PackageEntry(
val name: String,
val downloads: Int
)
@Service(Service.Level.APP)
private class PyPIPackageRankingImpl(coroutineScope: CoroutineScope) : PyPIPackageRanking {
override val rankedPackages: Deferred<HashSet<PyPackageName>> = coroutineScope.async(Dispatchers.IO) {
loadAndParseRankingData()
}
private suspend fun loadAndParseRankingData(): HashSet<PyPackageName> = withContext(Dispatchers.IO) {
val resource = loadResource()
parseJsonToList(resource).packages.map { PyPackageName.from(it.name) }.toHashSet()
}
private fun loadResource(): URL =
PyPIPackageRanking::class.java.getResource(RANKING_RESOURCE_PATH)
?: error("Python package ranking not found")
private fun parseJsonToList(resource: URL): Packages {
val jsonString = Resources.asCharSource(resource, Charsets.UTF_8).read()
return Json.decodeFromString<Packages>(jsonString)
}
private companion object {
const val RANKING_RESOURCE_PATH = "/packaging/pypi-rank.json"
}
}