PY-79792 Refactored repository handling in Python packaging subsystem. Unified repository management by removing redundant methods (packagesFromRepository, allPackages) and standardized package retrieval via getPackages from PyPackageRepository. Updated related classes to align with the new CompositePythonRepositoryManager and PythonRepositoryManager interfaces. Added Test. PY-80168 Replace latest text with real latest version.

GitOrigin-RevId: f9d826a84d469c75a0c66f34a308cdde16c2f5b0
This commit is contained in:
Timur Malanin
2025-04-10 13:11:48 +00:00
committed by intellij-monorepo-bot
parent 48211caacf
commit ff86ab4b82
21 changed files with 254 additions and 208 deletions

View File

@@ -145,5 +145,8 @@
<orderEntry type="library" name="io.github.z4kn4fein.semver.jvm" level="project" />
<orderEntry type="module" module-name="intellij.python.pyproject" />
<orderEntry type="module" module-name="intellij.python.hatch" />
<orderEntry type="module" module-name="intellij.platform.externalSystem.impl" />
<orderEntry type="module" module-name="intellij.platform.externalSystem" />
<orderEntry type="library" name="http-client" level="project" />
</component>
</module>

View File

@@ -6,6 +6,7 @@ import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.asContextElement
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.util.CatchingConsumer
@@ -15,90 +16,65 @@ import com.intellij.webcore.packaging.RepoPackage
import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.PyPackagingSettings
import com.jetbrains.python.packaging.common.*
import com.jetbrains.python.packaging.conda.CondaPackage
import com.jetbrains.python.packaging.conda.CondaPackageCache
import com.jetbrains.python.packaging.conda.CondaPackageManager
import com.jetbrains.python.packaging.conda.CondaPackageRepository
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.management.packagesByRepository
import com.jetbrains.python.packaging.management.runPackagingTool
import com.jetbrains.python.packaging.repository.PyPIPackageRepository
import com.jetbrains.python.packaging.repository.PyPackageRepository
import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService
import com.jetbrains.python.packaging.ui.PyPackageManagementService
import kotlinx.coroutines.*
import com.jetbrains.python.sdk.conda.isConda
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
class PythonPackageManagementServiceBridge(project: Project,sdk: Sdk) : PyPackageManagementService(project, sdk), Disposable {
class PythonPackageManagementServiceBridge(project: Project, sdk: Sdk) : PyPackageManagementService(project, sdk), Disposable {
private val scope = CoroutineScope(Dispatchers.IO)
private val scope = project.service<PyPackagingToolWindowService>().serviceScope
private val manager: PythonPackageManager
get() = PythonPackageManager.forSdk(project, sdk)
var useConda = true
var useConda: Boolean = true
val isConda: Boolean
get() = manager is CondaPackageManager
get() = sdk.isConda()
override fun getInstalledPackagesList(): List<InstalledPackage> {
if (manager.installedPackages.isEmpty()) runBlocking {
if (manager.installedPackages.isEmpty()) runBlockingCancellable {
manager.reloadPackages()
}
if (isConda) {
if (useConda) {
return manager.installedPackages.asSequence()
.filterIsInstance<CondaPackage>()
.filter { it.installedWithPip != useConda }
.map { InstalledPackage(it.name, it.version) }
.toList()
}
else {
return runBlocking {
val result = runPackagingOperationOrShowErrorDialog(sdk, PyBundle.message("python.packaging.operation.failed.title")) {
val output = manager.runPackagingTool("list", emptyList(), PyBundle.message("python.packaging.list.progress"))
val packages = output.lineSequence()
.filter { it.isNotBlank() }
.map {
val line = it.split("\t")
PythonPackage(line[0], line[1], isEditableMode = false)
}
.sortedWith(compareBy(PythonPackage::name))
.toList()
Result.success(packages)
}
return@runBlocking if (result.isSuccess) {
result.getOrThrow().map { InstalledPackage(it.name, it.version) }
} else emptyList()
}
}
}
return manager.installedPackages.map { InstalledPackage(it.name, it.version) }
}
override fun getAllPackages(): List<RepoPackage> {
if (isConda && useConda) {
val settings = PyPackagingSettings.getInstance(project)
val cache = service<CondaPackageCache>()
return manager
.repositoryManager
.packagesFromRepository(CondaPackageRepository)
.asSequence()
.map { RepoPackage(it, null, settings.selectLatestVersion(cache[it] ?: emptyList())) }
.toMutableList()
val packagesWithRepositories = manager.repositoryManager.packagesByRepository()
return packagesWithRepositories
.flatMap { (repository, packages) ->
packages.asSequence().map { pkg ->
createRepoPackage(pkg, repository)
}
}
.toList()
}
private fun createRepoPackage(pkg: String, repository: PyPackageRepository): RepoPackage {
val repositoryUrl = when {
repository.isCustom -> repository.repositoryUrl
else -> null
}
val latestVersion = getLatestVersion(pkg)
return RepoPackage(pkg, repositoryUrl, latestVersion)
}
val hasRepositories = manager
.repositoryManager
.repositories
.any { it !is PyPIPackageRepository && it !is CondaPackageRepository }
// TODO unify logic of retrieving package versions for pypi and conda
private fun getLatestVersion(pkg: String): String? {
if (!isConda || !useConda) return null
return manager
.repositoryManager
.packagesByRepository()
.filterNot { it.first is CondaPackageRepository }
.flatMap { (repo, pkgs) -> pkgs.asSequence().map { RepoPackage(it, if (hasRepositories) repo.repositoryUrl else null) } }
.toMutableList()
val settings = PyPackagingSettings.getInstance(project)
val cache = service<CondaPackageCache>()
val versions = cache[pkg] ?: emptyList()
return settings.selectLatestVersion(versions)
}
override fun getAllPackagesCached(): List<RepoPackage> {
@@ -106,7 +82,7 @@ class PythonPackageManagementServiceBridge(project: Project,sdk: Sdk) : PyPackag
}
override fun reloadAllPackages(): List<RepoPackage> {
return runBlocking {
return runBlockingCancellable {
manager.repositoryManager.refreshCaches()
allPackages
}
@@ -185,15 +161,12 @@ class PythonPackageManagementServiceBridge(project: Project,sdk: Sdk) : PyPackag
}
}
private fun findRepositoryForPackage(name: String): PyPackageRepository {
if (manager is CondaPackageManager && useConda) return CondaPackageRepository
return manager
private fun findRepositoryForPackage(name: String): PyPackageRepository =
manager
.repositoryManager
.packagesByRepository()
.filterNot { it.first is CondaPackageRepository || it.first is PyPIPackageRepository }
.firstOrNull { (_, packages) -> name in packages }
?.first ?: PyPIPackageRepository
}
private fun buildDescription(details: PythonPackageDetails): String {
return buildString {
@@ -235,6 +208,6 @@ class PythonPackageManagementServiceBridge(project: Project,sdk: Sdk) : PyPackag
}
companion object {
var runningUnderOldUI = false
var runningUnderOldUI: Boolean = false
}
}

View File

@@ -99,7 +99,7 @@ interface PythonPackageSpecification {
}
if (repository != null && repository != PyPIPackageRepository) {
add("--index-url")
add(repository!!.urlForInstallation)
add(repository!!.urlForInstallation.toString())
}
}
}

View File

@@ -19,7 +19,7 @@ internal class CompositePythonPackageManager(
override var installedPackages: List<PythonPackage> = emptyList()
override var repositoryManager: PythonRepositoryManager =
CompositePythonRepositoryManager(project, sdk, managers.map { it.repositoryManager })
CompositePythonRepositoryManager(project, managers.map { it.repositoryManager }, sdk)
private val managerNames = managers.joinToString { it.javaClass.simpleName }

View File

@@ -6,13 +6,13 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.PyPackageVersion
import com.jetbrains.python.packaging.PyPackageVersionComparator
import com.jetbrains.python.packaging.common.EmptyPythonPackageDetails
import com.jetbrains.python.packaging.common.PythonPackageDetails
import com.jetbrains.python.packaging.common.PythonPackageSpecification
import com.jetbrains.python.packaging.management.PythonPackageManagerService
import com.jetbrains.python.packaging.management.PythonRepositoryManager
import com.jetbrains.python.packaging.repository.PyPackageRepository
import io.github.z4kn4fein.semver.toVersion
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.Semaphore
@@ -21,32 +21,16 @@ import kotlinx.coroutines.sync.withPermit
import java.util.concurrent.atomic.AtomicBoolean
internal class CompositePythonRepositoryManager(
project: Project,
sdk: Sdk,
override val project: Project,
private val managers: List<PythonRepositoryManager>,
) : PythonRepositoryManager(project, sdk) {
@Deprecated("Don't use sdk from here") override val sdk: Sdk
) : PythonRepositoryManager {
override val repositories: List<PyPackageRepository> =
managers.flatMap { it.repositories }
override fun allPackages(): Set<String> {
return managers.fold(emptySet()) { acc, manager -> acc + manager.allPackages() }
}
override fun packagesFromRepository(repository: PyPackageRepository): Set<String> {
return findPackagesInRepository(repository)
?: error("No packages for requested repository in cache")
}
private fun findPackagesInRepository(repository: PyPackageRepository): Set<String>? {
for (manager in managers) {
val packages = manager.packagesFromRepository(repository)
if (packages.isNotEmpty()) {
return packages
}
}
return null
}
override fun allPackages(): Set<String> =
repositories.flatMap { it.getPackages() }.toSet()
override suspend fun getPackageDetails(pkg: PythonPackageSpecification): PythonPackageDetails {
for (manager in managers) {
@@ -61,9 +45,7 @@ internal class CompositePythonRepositoryManager(
var latestVersion: PyPackageVersion? = null
for (manager in managers) {
val version = manager.getLatestVersion(spec)
if (version != null &&
(latestVersion == null || version.presentableText.toVersion() > latestVersion.presentableText.toVersion())
) {
if (version != null && (latestVersion == null || PyPackageVersionComparator.compare(version, latestVersion) > 0)) {
latestVersion = version
}
}

View File

@@ -16,17 +16,14 @@ import com.jetbrains.python.packaging.repository.PyPackageRepository
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
internal class CondaRepositoryManger(project: Project, sdk: Sdk) : PipBasedRepositoryManager(project, sdk) {
internal class CondaRepositoryManger(
override val project: Project,
@Deprecated("Don't use sdk from here") override val sdk: Sdk
) : PipBasedRepositoryManager() {
override val repositories: List<PyPackageRepository>
get() = listOf(CondaPackageRepository) + super.repositories
override fun allPackages(): Set<String> = service<CondaPackageCache>().packages
override fun packagesFromRepository(repository: PyPackageRepository): Set<String> {
return if (repository is CondaPackageRepository) service<CondaPackageCache>().packages else super.packagesFromRepository(repository)
}
override fun buildPackageDetails(rawInfo: String?, spec: PythonPackageSpecification): PythonPackageDetails {
if (spec is CondaPackageSpecification) {
val versions = service<CondaPackageCache>()[spec.name] ?: error("No conda package versions in cache")

View File

@@ -23,7 +23,7 @@ class CondaPackageSpecification(name: String,
relation: PyRequirementRelation? = null) : PythonPackageSpecificationBase(name, version, relation, CondaPackageRepository) {
override val repository: PyPackageRepository = CondaPackageRepository
override var versionSpecs: String? = null
get() = if (field != null) "${field}" else if (version != null) "${relation?.presentableText ?: "="}$version" else ""
get() = if (field != null) "${field}" else if (version != null) "${relation?.presentableText ?: "=="}$version" else ""
override fun buildInstallationString(): List<String> {
return listOf("$name$versionSpecs")
@@ -42,8 +42,18 @@ class CondaPackageDetails(override val name: String,
}
}
object CondaPackageRepository : PyPackageRepository("Conda", "", "") {
object CondaPackageRepository : PyPackageRepository("Conda", null, null) {
override fun createPackageSpecification(packageName: String, version: String?, relation: PyRequirementRelation?): PythonPackageSpecification {
return CondaPackageSpecification(packageName, version, relation)
}
override fun createForcedSpecPackageSpecification(packageName: String, versionSpecs: String?): PythonPackageSpecification {
val spec = CondaPackageSpecification(packageName, null, null)
spec.versionSpecs = versionSpecs
return spec
}
override fun getPackages(): Set<String> {
return service<CondaPackageCache>().packages
}
}

View File

@@ -141,7 +141,7 @@ private val proxyString: String?
}
fun PythonRepositoryManager.packagesByRepository(): Sequence<Pair<PyPackageRepository, Set<String>>> {
return repositories.asSequence().map { it to packagesFromRepository(it) }
return repositories.asSequence().map { it to it.getPackages() }
}
fun PythonPackageManager.isInstalled(name: String): Boolean {

View File

@@ -8,23 +8,25 @@ import com.jetbrains.python.packaging.common.PythonPackageDetails
import com.jetbrains.python.packaging.common.PythonPackageSpecification
import com.jetbrains.python.packaging.repository.PyPackageRepository
import org.jetbrains.annotations.ApiStatus
import java.io.IOException
@ApiStatus.Experimental
abstract class PythonRepositoryManager(val project: Project, val sdk: Sdk) {
abstract val repositories: List<PyPackageRepository>
interface PythonRepositoryManager {
@Deprecated("Don't use sdk from here")
val sdk: Sdk
val project: Project
val repositories: List<PyPackageRepository>
abstract fun allPackages(): Set<String>
fun allPackages(): Set<String>
fun searchPackages(query: String): Map<PyPackageRepository, List<String>>
fun searchPackages(query: String, repository: PyPackageRepository): List<String>
abstract fun packagesFromRepository(repository: PyPackageRepository): Set<String>
abstract suspend fun getPackageDetails(pkg: PythonPackageSpecification): PythonPackageDetails
abstract suspend fun getLatestVersion(spec: PythonPackageSpecification): PyPackageVersion?
suspend fun getPackageDetails(pkg: PythonPackageSpecification): PythonPackageDetails
suspend fun getLatestVersion(spec: PythonPackageSpecification): PyPackageVersion?
fun buildPackageDetails(rawInfo: String?, spec: PythonPackageSpecification): PythonPackageDetails
abstract suspend fun refreshCaches()
abstract suspend fun initCaches()
abstract fun buildPackageDetails(rawInfo: String?, spec: PythonPackageSpecification): PythonPackageDetails
abstract fun searchPackages(query: String, repository: PyPackageRepository): List<String>
abstract fun searchPackages(query: String): Map<PyPackageRepository, List<String>>
}
@Throws(IOException::class)
suspend fun refreshCaches()
@Throws(IOException::class)
suspend fun initCaches()
}

View File

@@ -5,25 +5,30 @@ import com.github.benmanes.caffeine.cache.Caffeine
import com.google.gson.Gson
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.text.StringUtil
import com.intellij.util.io.HttpRequests
import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.*
import com.jetbrains.python.packaging.PyPIPackageUtil
import com.jetbrains.python.packaging.PyPackageVersion
import com.jetbrains.python.packaging.PyPackageVersionComparator
import com.jetbrains.python.packaging.PyPackageVersionNormalizer
import com.jetbrains.python.packaging.cache.PythonSimpleRepositoryCache
import com.jetbrains.python.packaging.common.EmptyPythonPackageDetails
import com.jetbrains.python.packaging.common.PythonPackageDetails
import com.jetbrains.python.packaging.common.PythonPackageSpecification
import com.jetbrains.python.packaging.common.PythonSimplePackageDetails
import com.jetbrains.python.packaging.management.PythonRepositoryManager
import com.jetbrains.python.packaging.management.packagesByRepository
import com.jetbrains.python.packaging.repository.*
import com.jetbrains.python.packaging.normalizePackageName
import com.jetbrains.python.packaging.repository.PyEmptyPackagePackageRepository
import com.jetbrains.python.packaging.repository.PyPIPackageRepository
import com.jetbrains.python.packaging.repository.PyPackageRepositories
import com.jetbrains.python.packaging.repository.PyPackageRepository
import com.jetbrains.python.packaging.repository.withBasicAuthorization
import org.jetbrains.annotations.ApiStatus
import java.time.Duration
@ApiStatus.Experimental
internal abstract class PipBasedRepositoryManager(project: Project, sdk: Sdk) : PythonRepositoryManager(project, sdk) {
internal abstract class PipBasedRepositoryManager() : PythonRepositoryManager {
override val repositories: List<PyPackageRepository>
get() = listOf(PyPIPackageRepository) + service<PythonSimpleRepositoryCache>().repositories
@@ -34,9 +39,8 @@ internal abstract class PipBasedRepositoryManager(project: Project, sdk: Sdk) :
.expireAfterWrite(Duration.ofHours(1))
.build<PythonPackageSpecification, PythonPackageDetails> {
// todo[akniazev] make it possible to show info from several repos
val repositoryUrl = it.repository?.repositoryUrl ?: PyPIPackageRepository.repositoryUrl!!
val repositoryUrl = it.repository?.repositoryUrl ?: PyPIPackageRepository.repositoryUrl ?: ""
val result = runCatching {
val packageDetailsUrl = PyPIPackageUtil.buildDetailsUrl(repositoryUrl, it.name)
HttpRequests.request(packageDetailsUrl)
.withBasicAuthorization(it.repository)
@@ -72,7 +76,7 @@ internal abstract class PipBasedRepositoryManager(project: Project, sdk: Sdk) :
if (rawInfo == null) {
val versions = tryParsingVersionsFromPage(spec.name, spec.repository?.repositoryUrl)
val repository = if (spec.repository !is PyEmptyPackagePackageRepository) spec.repository else PyPIPackageRepository
val repositoryName = repository?.name ?: PyPIPackageRepository.name!!
val repositoryName = repository?.name ?: PyPIPackageRepository.name
if (versions != null) {
return PythonSimplePackageDetails(
spec.name,
@@ -133,14 +137,8 @@ internal abstract class PipBasedRepositoryManager(project: Project, sdk: Sdk) :
service<PythonSimpleRepositoryCache>().refresh()
}
override fun allPackages(): Set<String> {
return packagesByRepository().fold(emptySet()) { acc, manager -> acc + manager.second }
}
override fun packagesFromRepository(repository: PyPackageRepository): Set<String> {
return if (repository is PyPIPackageRepository) service<PypiPackageCache>().packages
else service<PythonSimpleRepositoryCache>()[repository] ?: error("No packages for requested repository in cache")
}
override fun allPackages(): Set<String> =
repositories.flatMap { it.getPackages() }.toSet()
override suspend fun getPackageDetails(pkg: PythonPackageSpecification): PythonPackageDetails {
return packageDetailsCache[pkg]
@@ -152,7 +150,7 @@ internal abstract class PipBasedRepositoryManager(project: Project, sdk: Sdk) :
override fun searchPackages(query: String, repository: PyPackageRepository): List<String> {
val normalizedQuery = normalizePackageName(query)
return packagesFromRepository(repository).filter { StringUtil.containsIgnoreCase(normalizePackageName(it), normalizedQuery) }
return repository.getPackages().filter { StringUtil.containsIgnoreCase(normalizePackageName(it), normalizedQuery) }
}
override fun searchPackages(query: String): Map<PyPackageRepository, List<String>> {

View File

@@ -6,4 +6,7 @@ import com.intellij.openapi.projectRoots.Sdk
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
internal class PipRepositoryManager(project: Project, sdk: Sdk) : PipBasedRepositoryManager(project, sdk)
internal class PipRepositoryManager(
override val project: Project,
@Deprecated("Don't use sdk from here") override val sdk: Sdk,
) : PipBasedRepositoryManager()

View File

@@ -3,4 +3,4 @@ package com.jetbrains.python.packaging.repository
import com.jetbrains.python.PyBundle
class InstalledPyPackagedRepository : PyPackageRepository(PyBundle.message("python.toolwindow.packages.installed.label"), "", "") {}
class InstalledPyPackagedRepository : PyPackageRepository(PyBundle.message("python.toolwindow.packages.installed.label"), null, null)

View File

@@ -5,70 +5,86 @@ import com.intellij.credentialStore.CredentialAttributes
import com.intellij.credentialStore.Credentials
import com.intellij.credentialStore.generateServiceName
import com.intellij.ide.passwordSafe.PasswordSafe
import com.intellij.openapi.components.BaseState
import com.intellij.openapi.components.service
import com.intellij.util.xmlb.annotations.Transient
import com.jetbrains.python.packaging.cache.PythonSimpleRepositoryCache
import com.jetbrains.python.packaging.common.PythonPackageSpecification
import com.jetbrains.python.packaging.common.PythonSimplePackageSpecification
import com.jetbrains.python.packaging.conda.CondaPackageRepository
import com.jetbrains.python.packaging.requirement.PyRequirementRelation
import org.apache.http.client.utils.URIBuilder
import org.jetbrains.annotations.ApiStatus
import java.net.URL
@ApiStatus.Internal
open class PyPackageRepository() : BaseState() {
var name by string("")
var repositoryUrl by string("")
var authorizationType by enum(PyPackageRepositoryAuthenticationType.NONE)
var login by string("")
open class PyPackageRepository() {
val urlForInstallation: String
get() {
val fullUrl = repositoryUrl!!
if (login != null && login!!.isNotBlank()) {
val password = getPassword()
if (password != null) {
val protocol = fullUrl.substringBefore("//")
val url = fullUrl.substringAfter("//")
return "$protocol//${encodeCredentialsForUrl(login!!, password)}@$url"
}
}
return fullUrl
}
var name: String = ""
internal set
var repositoryUrl: String? = null
internal set
var login: String? = null
internal set
var authorizationType: PyPackageRepositoryAuthenticationType = PyPackageRepositoryAuthenticationType.NONE
internal set
constructor(name: String, repositoryUrl: String?, login: String?) : this() {
this.name = name
this.repositoryUrl = repositoryUrl
this.login = login
}
internal val isCustom: Boolean
get() = this !is PyPIPackageRepository && this !is CondaPackageRepository
private val serviceName: String
get() = generateServiceName(SUBSYSTEM_NAME, name)
val urlForInstallation: URL
get() = repositoryUrl?.let { baseUrl ->
val userLogin = login.takeUnless { it.isNullOrBlank() } ?: return URL(baseUrl)
val userPassword = getPassword() ?: return URL(baseUrl)
buildAuthenticatedUrl(baseUrl, userLogin, userPassword)
} ?: URL("")
private fun buildAuthenticatedUrl(baseUrl: String, login: String, password: String): URL =
URIBuilder(baseUrl).setUserInfo(login, password).build().toURL()
@Transient
fun getPassword(): String? {
val serviceName = generateServiceName("PyCharm", name!!)
val attributes = CredentialAttributes(serviceName, login)
return PasswordSafe.instance.getPassword(attributes)
}
fun setPassword(pass: String?) {
val serviceName = generateServiceName("PyCharm", name!!)
val attributes = CredentialAttributes(serviceName, login)
PasswordSafe.instance.set(attributes, Credentials(login, pass))
}
fun clearCredentials() {
val serviceName = generateServiceName("PyCharm", name!!)
val attributes = CredentialAttributes(serviceName, login)
PasswordSafe.instance.set(attributes, null)
}
constructor(name: String, repositoryUrl: String, username: String) : this() {
this.name = name
this.repositoryUrl = repositoryUrl
this.login = username
}
open fun createPackageSpecification(
packageName: String,
version: String? = null,
relation: PyRequirementRelation? = null,
): PythonPackageSpecification =
PythonSimplePackageSpecification(packageName, version, this, relation)
open fun createPackageSpecification(packageName: String,
version: String? = null,
relation: PyRequirementRelation? = null): PythonPackageSpecification {
return PythonSimplePackageSpecification(packageName, version, this, relation)
}
open fun createForcedSpecPackageSpecification(
packageName: String,
versionSpecs: String? = null,
): PythonPackageSpecification =
PythonSimplePackageSpecification(packageName, null, this).apply {
this.versionSpecs = versionSpecs
}
open fun createForcedSpecPackageSpecification(packageName: String,
versionSpecs: String? = null): PythonPackageSpecification {
val spec = PythonSimplePackageSpecification(packageName, null, this)
spec.versionSpecs = versionSpecs
return spec
open fun getPackages(): Set<String> =
service<PythonSimpleRepositoryCache>()[this] ?: emptySet()
companion object {
private const val SUBSYSTEM_NAME = "PyCharm"
}
}

View File

@@ -2,13 +2,15 @@
@file:JvmName("PyPackageRepositoryUtil")
package com.jetbrains.python.packaging.repository
import com.intellij.openapi.components.service
import com.intellij.util.io.HttpRequests
import com.intellij.util.io.RequestBuilder
import com.jetbrains.python.packaging.PyPIPackageUtil
import com.jetbrains.python.packaging.pip.PypiPackageCache
import org.jetbrains.annotations.ApiStatus
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import java.util.*
import java.util.Base64
@ApiStatus.Experimental
internal fun RequestBuilder.withBasicAuthorization(repository: PyPackageRepository?): RequestBuilder {
@@ -37,7 +39,11 @@ internal fun encodeCredentialsForUrl(login: String, password: String): String {
}
@ApiStatus.Experimental
object PyEmptyPackagePackageRepository : PyPackageRepository("empty repository", "", "")
object PyEmptyPackagePackageRepository : PyPackageRepository("empty repository", null, null)
@ApiStatus.Experimental
object PyPIPackageRepository : PyPackageRepository("PyPI", PyPIPackageUtil.PYPI_LIST_URL, "")
object PyPIPackageRepository : PyPackageRepository("PyPI", PyPIPackageUtil.PYPI_LIST_URL, null) {
override fun getPackages(): Set<String> {
return service<PypiPackageCache>().packages
}
}

View File

@@ -20,8 +20,9 @@ class PyRepositoriesList(val project: Project) : MasterDetailsComponent() {
init {
initTree()
service<PyPackageRepositories>().repositories
.map { MyNode(PyRepositoryListItem(it)) }
service<PyPackageRepositories>()
.repositories
.map { MyNode(PyRepositoryListItem(it, project)) }
.forEach { addNode(it, myRoot) }
}
@@ -34,7 +35,7 @@ class PyRepositoriesList(val project: Project) : MasterDetailsComponent() {
PyBundle.message("python.packaging.repository.form.default.name"),
remainingRepositories().map { it.name }.toMutableList())
val newNode = MyNode(PyRepositoryListItem(PyPackageRepository(uniqueName, "", "")))
val newNode = MyNode(PyRepositoryListItem(PyPackageRepository(uniqueName, null, null), project))
addNode(newNode, myRoot)
selectNodeInTree(newNode)
}

View File

@@ -1,7 +1,9 @@
// 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.repository
import com.intellij.openapi.components.service
import com.intellij.openapi.observable.properties.PropertyGraph
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.NamedConfigurable
import com.intellij.openapi.util.NlsSafe
import com.intellij.ui.components.JBTextField
@@ -10,6 +12,8 @@ import com.intellij.ui.dsl.builder.bindText
import com.intellij.ui.dsl.builder.panel
import com.intellij.util.ui.JBUI
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService
import kotlinx.coroutines.launch
import org.jetbrains.annotations.ApiStatus
import java.awt.BorderLayout
import java.awt.Dimension
@@ -17,7 +21,7 @@ import javax.swing.JComponent
import javax.swing.JPanel
@ApiStatus.Internal
internal class PyRepositoryListItem(val repository: PyPackageRepository) : NamedConfigurable<PyPackageRepository>(true, null) {
internal class PyRepositoryListItem(val repository: PyPackageRepository, private val project: Project) : NamedConfigurable<PyPackageRepository>(true, null) {
@NlsSafe
private var currentName = repository.name!!
private var password = repository.getPassword()
@@ -25,7 +29,7 @@ internal class PyRepositoryListItem(val repository: PyPackageRepository) : Named
private val propertyGraph = PropertyGraph()
private val urlProperty = propertyGraph.lazyProperty { repository.repositoryUrl ?: "" }
private val loginProperty = propertyGraph.lazyProperty { repository.login ?: "" }
private val passwordProperty = propertyGraph.lazyProperty { repository.getPassword() ?: "" }
private val passwordProperty = propertyGraph.lazyProperty { getPassword(repository) }
private val authorizationTypeProperty = propertyGraph.lazyProperty { repository.authorizationType }
override fun getDisplayName(): String {
@@ -87,7 +91,7 @@ internal class PyRepositoryListItem(val repository: PyPackageRepository) : Named
.bindText(urlProperty)
}
row(message("python.packaging.repository.form.authorization")) {
segmentedButton(PyPackageRepositoryAuthenticationType.values().toList()) { text = it.text }
segmentedButton(PyPackageRepositoryAuthenticationType.entries) { text = it.text }
.bind(authorizationTypeProperty)
}
val row1 = row(message("python.packaging.repository.form.login")) {
@@ -95,7 +99,7 @@ internal class PyRepositoryListItem(val repository: PyPackageRepository) : Named
.bindText(loginProperty)
}.visible(repository.authorizationType != PyPackageRepositoryAuthenticationType.NONE)
val row2 = row(message("python.packaging.repository.form.password")) {
passwordField().applyToComponent { text = repository.getPassword() }
passwordField().applyToComponent { text = getPassword(repository) }
.apply { component.preferredSize = Dimension(250, component.preferredSize.height) }
.bindText(passwordProperty)
}.visible(repository.authorizationType != PyPackageRepositoryAuthenticationType.NONE)
@@ -109,4 +113,13 @@ internal class PyRepositoryListItem(val repository: PyPackageRepository) : Named
mainPanel.add(repositoryForm, BorderLayout.CENTER)
return mainPanel
}
private fun getPassword(repository: PyPackageRepository): String {
val toolWindowService = project.service<PyPackagingToolWindowService>()
var retrievedPassword: String? = null
toolWindowService.serviceScope.launch {
retrievedPassword = repository.getPassword()
}
return retrievedPassword ?: ""
}
}

View File

@@ -22,7 +22,12 @@ 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.packaging.*
import com.jetbrains.python.packaging.PyPIPackageRanking
import com.jetbrains.python.packaging.PyPIPackageUtil
import com.jetbrains.python.packaging.PyPackageService
import com.jetbrains.python.packaging.PyPackageVersion
import com.jetbrains.python.packaging.PyPackageVersionComparator
import com.jetbrains.python.packaging.PyPackageVersionNormalizer
import com.jetbrains.python.packaging.common.PythonPackageDetails
import com.jetbrains.python.packaging.common.PythonPackageManagementListener
import com.jetbrains.python.packaging.common.PythonPackageSpecification
@@ -30,13 +35,30 @@ import com.jetbrains.python.packaging.common.runPackagingOperationOrShowErrorDia
import com.jetbrains.python.packaging.conda.CondaPackage
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.management.packagesByRepository
import com.jetbrains.python.packaging.repository.*
import com.jetbrains.python.packaging.normalizePackageName
import com.jetbrains.python.packaging.repository.PyEmptyPackagePackageRepository
import com.jetbrains.python.packaging.repository.PyPackageRepositories
import com.jetbrains.python.packaging.repository.PyPackageRepository
import com.jetbrains.python.packaging.repository.PyRepositoriesList
import com.jetbrains.python.packaging.repository.checkValid
import com.jetbrains.python.packaging.statistics.PythonPackagesToolwindowStatisticsCollector
import com.jetbrains.python.packaging.toolwindow.model.*
import com.jetbrains.python.packaging.toolwindow.model.DisplayablePackage
import com.jetbrains.python.packaging.toolwindow.model.InstallablePackage
import com.jetbrains.python.packaging.toolwindow.model.InstalledPackage
import com.jetbrains.python.packaging.toolwindow.model.PyInvalidRepositoryViewData
import com.jetbrains.python.packaging.toolwindow.model.PyPackagesViewData
import com.jetbrains.python.sdk.PythonSdkUtil
import com.jetbrains.python.sdk.pythonSdk
import com.jetbrains.python.statistics.modules
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.cancel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.Nls
@Service(Service.Level.PROJECT)
@@ -353,7 +375,7 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
return sortPackagesForRepo(manager.repositoryManager.searchPackages(currentQuery, repository), currentQuery, repository, skipItems)
}
else {
val packagesFromRepo = manager.repositoryManager.packagesFromRepository(repository)
val packagesFromRepo = repository.getPackages()
val page = packagesFromRepo.asSequence().limitResultAndFilterOutInstalled(repository, skipItems)
return PyPackagesViewData(repository, page, moreItems = packagesFromRepo.size - (PACKAGES_LIMIT + skipItems))
}

View File

@@ -5,6 +5,7 @@ import com.intellij.ide.BrowserUtil
import com.intellij.ide.plugins.newui.OneLineProgressIndicator
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.UiDataProvider
import com.intellij.openapi.application.EDT
import com.intellij.openapi.components.service
import com.intellij.openapi.observable.properties.AtomicBooleanProperty
import com.intellij.openapi.observable.properties.AtomicProperty
@@ -24,7 +25,11 @@ import com.intellij.ui.JBColor
import com.intellij.ui.SideBorder
import com.intellij.ui.components.JBComboBoxLabel
import com.intellij.ui.components.JBOptionButton
import com.intellij.ui.dsl.builder.*
import com.intellij.ui.dsl.builder.BottomGap
import com.intellij.ui.dsl.builder.RightGap
import com.intellij.ui.dsl.builder.TopGap
import com.intellij.ui.dsl.builder.bindText
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.jcef.JCEFHtmlPanel
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.packaging.PyPackageUtil
@@ -38,19 +43,25 @@ import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents
import com.jetbrains.python.packaging.utils.PyPackageCoroutine
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.Nls
import java.awt.BorderLayout
import java.awt.Font
import java.awt.event.ActionEvent
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.swing.*
import javax.swing.AbstractAction
import javax.swing.Action
import javax.swing.BorderFactory
import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.SwingConstants
class PyPackageDescriptionController(val project: Project) : Disposable {
private val latestText: String
get() = message("python.toolwindow.packages.latest.version.label")
val service = project.service<PyPackagingToolWindowService>()
val service: PyPackagingToolWindowService = project.service<PyPackagingToolWindowService>()
internal val selectedPackage = AtomicProperty<DisplayablePackage?>(null)
private val isManagement = AtomicBooleanProperty(false)
@@ -114,23 +125,26 @@ class PyPackageDescriptionController(val project: Project) : Disposable {
private val rightPanel = panel {
row {
cell(progressIndicatorComponent).gap(RightGap.SMALL).visibleIf(progressEnabledProperty)
versionSelector.apply {
versionSelector.text = packageVersionProperty.get()
addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent?) {
val versions = listOf(latestText) + (selectedPackageDetails.get()?.availableVersions ?: emptyList())
val availableVersions = selectedPackageDetails.get()?.availableVersions ?: emptyList()
val latestVersion = availableVersions.first()
val versions = listOf(latestText) + availableVersions
JBPopupFactory.getInstance().createListPopup(
object : BaseListPopupStep<String>(null, versions) {
override fun onChosen(@NlsContexts.Label selectedValue: String, finalChoice: Boolean): PopupStep<*>? {
packageVersionProperty.set(selectedValue)
suggestInstallPackage(selectedValue)
val effectiveVersion = if (selectedValue == latestText) latestVersion else selectedValue
suggestInstallPackage(effectiveVersion)
return FINAL_CHOICE
}
}, 8).showUnderneathOf(this@apply)
}
})
}
packageVersionProperty.afterChange {
versionSelector.text = it
}
@@ -165,7 +179,7 @@ class PyPackageDescriptionController(val project: Project) : Disposable {
add(htmlPanel.component, BorderLayout.CENTER)
}
val wrappedComponent = UiDataProvider.wrapComponent(component, UiDataProvider {})
val wrappedComponent: JComponent = UiDataProvider.wrapComponent(component, UiDataProvider {})
override fun dispose() {}
@@ -182,6 +196,7 @@ class PyPackageDescriptionController(val project: Project) : Disposable {
private fun updatePackageVersion(newVersion: String) {
val details = selectedPackageDetails.get() ?: return
val newVersionSpec = details.toPackageSpecification(newVersion)
println(newVersionSpec.versionSpecs)
val pyPackagingToolWindowService = PyPackagingToolWindowService.getInstance(project)
PyPackageCoroutine.launch(project, Dispatchers.IO) {
pyPackagingToolWindowService.installPackage(newVersionSpec)
@@ -218,7 +233,9 @@ class PyPackageDescriptionController(val project: Project) : Disposable {
actionPerformed()
}
finally {
progressEnabledProperty.set(false)
withContext(Dispatchers.EDT) {
progressEnabledProperty.set(false)
}
progressIndicator.stop()
}
}

View File

@@ -39,7 +39,7 @@ abstract class PyRunAnythingPackageProvider : RunAnythingCommandLineProvider() {
initCaches(packageManager)
if (isInstall) {
val packageRepository = getPackageRepository(dataContext) ?: return emptySequence()
return packageManager.repositoryManager.packagesFromRepository(packageRepository).filter {
return packageRepository.getPackages().filter {
it.startsWith(commandLine.toComplete)
}.asSequence()
}

View File

@@ -8,7 +8,6 @@ import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.common.PythonPackageDetails
import com.jetbrains.python.packaging.common.PythonPackageSpecification
import com.jetbrains.python.packaging.common.PythonSimplePackageDetails
import com.jetbrains.python.packaging.repository.PyEmptyPackagePackageRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import org.jetbrains.annotations.TestOnly
@@ -95,7 +94,7 @@ class TestPythonPackageManagerService(val installedPackages: List<PythonPackage>
return TestPythonPackageManager(project, sdk)
.withPackageInstalled(installedPackages)
.withPackageNames(installedPackages.map { it.name })
.withPackageDetails(PythonSimplePackageDetails(installedPackages.first().name, listOf(installedPackages.first().version),PyEmptyPackagePackageRepository))
.withPackageDetails(PythonSimplePackageDetails(installedPackages.first().name, listOf(installedPackages.first().version), TestPackageRepository(installedPackages.map { it.name }.toSet())))
}
override fun bridgeForSdk(project: Project, sdk: Sdk): PythonPackageManagementServiceBridge {

View File

@@ -6,12 +6,14 @@ import com.intellij.openapi.projectRoots.Sdk
import com.jetbrains.python.packaging.PyPackageVersion
import com.jetbrains.python.packaging.common.PythonPackageDetails
import com.jetbrains.python.packaging.common.PythonPackageSpecification
import com.jetbrains.python.packaging.repository.PyEmptyPackagePackageRepository
import com.jetbrains.python.packaging.repository.PyPackageRepository
import org.jetbrains.annotations.TestOnly
@TestOnly
class TestPythonRepositoryManager(project: Project, sdk: Sdk) : PythonRepositoryManager(project, sdk) {
internal class TestPythonRepositoryManager(
override val project: Project,
@Deprecated("Don't use sdk from here") override val sdk: Sdk
) : PythonRepositoryManager {
private var packageNames: Set<String> = emptySet()
private var packageDetails: PythonPackageDetails? = null
@@ -39,16 +41,12 @@ class TestPythonRepositoryManager(project: Project, sdk: Sdk) : PythonRepository
}
override val repositories: List<PyPackageRepository>
get() = listOf(PyEmptyPackagePackageRepository)
get() = listOf(TestPackageRepository(packageNames))
override fun allPackages(): Set<String> {
return packageNames
}
override fun packagesFromRepository(repository: PyPackageRepository): Set<String> {
return packageNames
}
override suspend fun getPackageDetails(pkg: PythonPackageSpecification): PythonPackageDetails {
assert(packageDetails != null)
return packageDetails!!
@@ -63,4 +61,10 @@ class TestPythonRepositoryManager(project: Project, sdk: Sdk) : PythonRepository
override suspend fun initCaches() {
}
}
internal class TestPackageRepository(private val packages: Set<String>): PyPackageRepository("test repository", null, null) {
override fun getPackages(): Set<String> {
return packages
}
}